40 KiB
Fusion Dashboard Storage Audit (FN-1202)
Task-ID allocator authority and compatibility
distributed_task_id_stateis the authoritative local task-ID allocator state.nextSequenceis the active high-water mark used for local ID reservations.distributed_task_id_reservationstracks reserve/commit/abort lifecycle entries. Aborted/expired reservations are burned and never reissued.config.nextIdis retained only as a deprecated legacy compatibility field and optional one-time seed source. Fusion still reads it during reconciliation, but runtime task creation and settings writes no longer mutate it.- Startup/store-open allocator reconciliation bumps each active prefix sequence to
max(current nextSequence, max(tasks suffix)+1, max(archivedTasks suffix)+1, max(reservation sequence)+1)so stale allocator rows self-heal before local task creation resumes. - Create-class task persistence is intentionally non-destructive: new tasks use plain
INSERTsemantics, whileON CONFLICT(id) DO UPDATEremains update-only. If counters drift and a reserved ID still collides, the create fails and the existing SQLite row / task directory stays intact.
Task-ID integrity detection
Fusion runs a read-only task-ID integrity detector at startup and on demand to surface allocator regressions before operators lose track of overwritten cards. The detector checks for:
- duplicate task IDs inside
tasks - task IDs that exist in both
tasksandarchivedTasks distributed_task_id_state.nextSequencevalues that point at or below an already-used numeric suffix- committed reservation rows that still reference existing task IDs
- active task rows whose prefix falls outside the prefixes declared in
distributed_task_id_state
The latest report is exposed in two operator-facing places:
GET /api/healthreturns ataskIdIntegrityobject withstatus,checkedAt,anomalies, and arecommendedActionstring. When anomalies are present, the top-level healthstatusbecomes"degraded"even if the SQLite integrity check is still healthy.- The dashboard renders a non-dismissible task-ID integrity banner for anomalous reports so the operator sees the issue in the same session.
Operator playbook
When the detector reports an anomaly:
- Pause task delegation and avoid creating new tasks until the state is understood.
- Inspect the affected task IDs in the dashboard/database and confirm whether any live task content or archived records mismatched their IDs.
- If the historical allocator audit script is available in your checkout, run it before resuming normal task creation.
Detecting historical task-ID overwrites
If allocator state drifted before the current guards landed, historical task records may still contain overwrite evidence. Run the audit script from the project root:
node scripts/audit-task-id-collisions.mjs [--project-root /path/to/project]
The script checks for:
task.json.historytimestamps older than the active DB row'screatedAt- task-title mismatches between SQLite and the first
#heading inPROMPT.md - task-title mismatches against the latest
Fusion-Task-Idcommit subject onmain - active tasks that share an ID with an
archivedTasksrow
Treat flagged candidates as recovery leads, not automatic truth: review the surviving task files, logs, and commit history, then file a follow-up recovery task for any confirmed overwrite.
Reconciling stale task title/description vs canonical PROMPT.md
Use the one-shot reconciliation script only when the surviving evidence agrees on a single canonical task identity and the ambiguity is limited to stale metadata fields on that same task row:
node scripts/reconcile-fn-3909-identity.mjs [--project-root /path/to/project] [--apply]
The script is intentionally narrow and idempotent:
- dry-run is the default and prints the before/after title + description diff without mutating anything
--applyonly updates taskFN-3909throughTaskStore.updateTask(...)and appends an audit log entry referencingFN-4194- the script refuses to run if
PROMPT.mdno longer matches the expected canonical heading, if the stale heartbeat-scope row contents are not present, or if the row is already canonical without the reconciliation marker
Use this path for the confirmed FN-3909 mismatch (canonical UI-fix prompt/merge history, stale heartbeat-scope title/description). Do not use it for allocator-collision or overwrite incidents that may involve multiple tasks or conflicting survivors; run scripts/audit-task-id-collisions.mjs first and treat those cases as recovery/postmortem work instead of automatic metadata repair.
Forensic / historical-task reconciliation: where to read from
For any audit/forensic/reconciliation task that targets another task ID (for example FN-4194 reconciling FN-3909), source-of-truth locations are always at the project root:
- On-disk task artifacts:
<rootDir>/.fusion/tasks/{ID}/(task.json,PROMPT.md,attachments/, agent logs) - Task database row:
<rootDir>/.fusion/fusion.db(SQLite in WAL mode)
Important execution nuance:
.fusion/is gitignored, so worktrees branched frommaindo not contain other tasks' artifact directories or the live DB file.- The running worktree's own
.fusion/(when present) is scratch/session state for the running task only; do not treat it as authoritative evidence for historical tasks. - Triage spec writers inject this guidance via
TRIAGE_SYSTEM_PROMPTandFAST_TRIAGE_SYSTEM_PROMPTinpackages/engine/src/triage.ts. - Executor-side path normalization remains consistent with this rule through
scopePromptToWorktreeinpackages/engine/src/step-session-executor.ts, which rewrites accidental worktree-local.fusionreferences back to project-root.fusionpaths.
SQLite write-path lock recovery (FN-4042 / FN-4083)
- Every disk-backed SQLite connection that Fusion opens for project storage (
fusion.db), the central registry (fusion-central.db), archives (archive.db), and worktree hydration explicitly setsPRAGMA busy_timeout = 5000andPRAGMA journal_mode = WALat connection open time before write work begins. - Project database transactions now distinguish read and write intent:
Database.transaction()usesBEGIN(DEFERRED) for outermost transactions so read-only callers do not reserve the writer lock up front.Database.transactionImmediate()usesBEGIN IMMEDIATEfor write-heavy paths that must detect writer contention before user code runs.
- The shared task mutation path
atomicWriteTaskJsonWithAudit()usestransactionImmediate(), so the task-row upsert and matchingrunAuditEventsinsert still commit or roll back together, while lock contention is detected before the callback mutates in-memory state. CentralDatabase.transaction()remainsBEGIN IMMEDIATE-based because its current callers are write-oriented coordination updates; nested transactions still use SQLiteSAVEPOINT/ROLLBACK TO/RELEASEsemantics in both databases.- Recovery is intentionally bounded: transient
SQLITE_BUSY/SQLITE_LOCKEDfailures on outermostBEGIN IMMEDIATEandCOMMITare retried for a short additional window with small synchronous backoff sleeps. If the lock does not clear, the original write still fails loudly. - Concurrent-write guarantees are layered:
- per-task mutations inside one engine process are serialized by
TaskStore.withTaskLock() - cross-task writes rely on WAL mode plus
busy_timeout - write-heavy transactional hot paths acquire
BEGIN IMMEDIATEbefore mutating state - compatibility
task.jsonwrites still happen only after the SQLite transaction succeeds
- per-task mutations inside one engine process are serialized by
- Direct
recordRunAuditEvent()writes continue to execute inside the shared transaction helper so they benefit from the same lock recovery and do not duplicate rows during transient contention.
1) Summary
- localStorage keys in runtime dashboard code: 20
- Backend settings keys defined in
@fusion/core: 78 total- Global settings: 17 (
GlobalSettings) - Project settings: 61 (
ProjectSettings)
- Global settings: 17 (
- SQLite tables in project DB schema (
packages/core/src/db.ts): 43 (including migration-created tables) - Issues identified: 9
- High: 2
- Medium: 5
- Low: 2
High-level finding: the dashboard currently uses localStorage extensively for UX state and drafts (good for responsiveness), but several keys are not project-scoped in a multi-project app and some data has sync gaps against backend persistence (notably theme settings).
2) localStorage Inventory
| Storage Key | Component/Hook | Data Type | Category | Risk Level |
|---|---|---|---|---|
kb-dashboard-theme-mode |
hooks/useTheme.ts |
enum string (dark/light/system) |
settings overlap | Medium |
kb-dashboard-color-theme |
hooks/useTheme.ts |
enum string (color theme id) | settings overlap | Medium |
kb-dashboard-current-project |
hooks/useCurrentProject.ts |
JSON ProjectInfo object (includes id/name/path/status/etc.) |
project/identity | Medium |
kb-terminal-tabs |
hooks/useTerminalSessions.ts |
JSON array of tab objects (id, sessionId, title, active state, timestamp) |
UI preference (operational session state) | High |
fn-agent-tree-expanded |
hooks/useAgentHierarchy.ts |
JSON string[] of expanded agent ids | UI preference | Low |
kb-planning-last-description |
hooks/modalPersistence.ts (used by PlanningModeModal) |
free-text draft | user draft | Medium |
kb-subtask-last-description |
hooks/modalPersistence.ts (used by SubtaskBreakdownModal) |
free-text draft | user draft | Medium |
kb-mission-last-goal |
hooks/modalPersistence.ts (used by MissionInterviewModal) |
free-text draft | user draft | Medium |
kb-dashboard-view-mode |
App.tsx |
enum string (overview/project) |
UI preference | Low |
kb-dashboard-task-view |
App.tsx |
enum string (board/list/agents) |
UI preference | Low |
kb-dashboard-list-columns |
components/ListView.tsx |
JSON array of visible list columns | UI preference | Low |
kb-dashboard-hide-done |
components/ListView.tsx |
boolean string ("true"/"false") |
UI preference | Low |
kb-dashboard-list-collapsed |
components/ListView.tsx |
JSON array of collapsed column ids | UI preference | Low |
kb-dashboard-selected-tasks |
components/ListView.tsx |
JSON array of selected task IDs | UI preference | Medium |
kb-quick-entry-text |
components/QuickEntryBox.tsx |
free-text task draft | user draft | Medium |
kb-quick-entry-expanded |
components/QuickEntryBox.tsx (legacy cleanup via removeItem) |
legacy bool key (no longer used) | UI preference | Low |
kb-inline-create-text |
components/InlineCreateCard.tsx |
free-text task draft | user draft | Medium |
fn-agent-view |
components/AgentsView.tsx, components/AgentListModal.tsx |
enum string (board/list/tree in view; modal supports board/list) |
UI preference | Medium |
kb-usage-view-mode |
components/UsageIndicator.tsx |
enum string (used/remaining) |
UI preference | Low |
kb-dashboard-recent-projects |
components/ProjectOverview.tsx |
JSON array of recent project IDs | project/identity | Low |
Notes:
- Search scope:
packages/dashboard/app/**/*.ts(x)runtime code (tests excluded). useTheme.getThemeInitScript()also reads the same theme keys before hydration.
3) Backend Settings Inventory
API endpoints reviewed:
GET /api/settings(merged global + project view)PUT /api/settings(project updates)GET /api/settings/globalPUT /api/settings/globalGET /api/settings/scopes
3.1 Global settings (~/.fusion/settings.json)
| Setting Key | Scope | API Endpoint | Description |
|---|---|---|---|
themeMode |
Global | GET/PUT /api/settings/global (+ merged via GET /api/settings) |
Theme mode preference |
colorTheme |
Global | GET/PUT /api/settings/global |
Color/accent theme |
dashboardFontScalePct |
Global | GET/PUT /api/settings/global |
Dashboard Appearance font scale percentage (85–125, default 100) applied before hydration. |
defaultProvider |
Global | GET/PUT /api/settings/global |
Default model provider |
defaultModelId |
Global | GET/PUT /api/settings/global |
Default model id |
fallbackProvider |
Global | GET/PUT /api/settings/global |
Fallback model provider |
fallbackModelId |
Global | GET/PUT /api/settings/global |
Fallback model id |
defaultThinkingLevel |
Global | GET/PUT /api/settings/global |
Default reasoning effort |
ntfyEnabled |
Global | GET/PUT /api/settings/global |
Notifications enabled |
ntfyTopic |
Global | GET/PUT /api/settings/global |
Ntfy topic |
ntfyBaseUrl |
Global | GET/PUT /api/settings/global |
Custom ntfy server base URL override |
ntfyAccessToken |
Global | GET/PUT /api/settings/global |
Access token for authenticated ntfy publishes |
ntfyEvents |
Global | GET/PUT /api/settings/global |
Notification event filters |
ntfyDashboardHost |
Global | GET/PUT /api/settings/global |
Host for deep links |
defaultProjectId |
Global | GET/PUT /api/settings/global |
CLI default project |
setupComplete |
Global | GET/PUT /api/settings/global (internal first-run use) |
Setup wizard completion flag |
favoriteProviders |
Global | GET/PUT /api/settings/global |
Favorited providers |
favoriteModels |
Global | GET/PUT /api/settings/global |
Favorited models |
openrouterModelSync |
Global | GET/PUT /api/settings/global |
Startup model sync behavior |
modelOnboardingComplete |
Global | GET/PUT /api/settings/global |
Onboarding completion flag |
executionGlobalProvider |
Global | GET/PUT /api/settings/global |
Global baseline AI provider for task execution |
executionGlobalModelId |
Global | GET/PUT /api/settings/global |
Global baseline AI model ID for task execution |
planningGlobalProvider |
Global | GET/PUT /api/settings/global |
Global baseline AI provider for planning |
planningGlobalModelId |
Global | GET/PUT /api/settings/global |
Global baseline AI model ID for planning |
validatorGlobalProvider |
Global | GET/PUT /api/settings/global |
Global baseline AI provider for validator/reviewer |
validatorGlobalModelId |
Global | GET/PUT /api/settings/global |
Global baseline AI model ID for validator/reviewer |
titleSummarizerGlobalProvider |
Global | GET/PUT /api/settings/global |
Global baseline AI provider for title summarization |
titleSummarizerGlobalModelId |
Global | GET/PUT /api/settings/global |
Global baseline AI model ID for title summarization |
3.2 Project settings (.fusion/config.json / config.settings)
| Setting Key | Scope | API Endpoint | Description |
|---|---|---|---|
globalPause |
Project | GET/PUT /api/settings |
Hard stop for engine activity |
enginePaused |
Project | GET/PUT /api/settings |
Soft pause for dispatch |
maxConcurrent |
Project | GET/PUT /api/settings |
Max concurrent task-lane agents. Utility AI workflows bypass this limit. |
maxWorktrees |
Project | GET/PUT /api/settings |
Worktree cap |
pollIntervalMs |
Project | GET/PUT /api/settings |
Scheduler poll interval |
groupOverlappingFiles |
Project | GET/PUT /api/settings |
Serialize overlapping file work |
overlapIgnorePaths |
Project | GET/PUT /api/settings |
Project-relative file/directory paths ignored by overlap blocking |
autoMerge |
Project | GET/PUT /api/settings |
Enable auto merge |
mergeStrategy |
Project | GET/PUT /api/settings |
Direct vs PR merge strategy |
worktreeInitCommand |
Project | GET/PUT /api/settings |
Command run on worktree init |
testCommand |
Project | GET/PUT /api/settings |
Project test command |
buildCommand |
Project | GET/PUT /api/settings |
Project build command |
recycleWorktrees |
Project | GET/PUT /api/settings |
Worktree pool toggle |
worktreeNaming |
Project | GET/PUT /api/settings |
Worktree naming strategy |
taskPrefix |
Project | GET/PUT /api/settings |
Task ID prefix |
includeTaskIdInCommit |
Project | GET/PUT /api/settings |
Commit scope formatting |
defaultProviderOverride |
Project | GET/PUT /api/settings |
Project-level override for base default provider |
defaultModelIdOverride |
Project | GET/PUT /api/settings |
Project-level override for base default model ID |
executionProvider |
Project | GET/PUT /api/settings |
AI provider for task execution |
executionModelId |
Project | GET/PUT /api/settings |
AI model ID for task execution |
planningProvider |
Project | GET/PUT /api/settings |
Planning model provider |
planningModelId |
Project | GET/PUT /api/settings |
Planning model id |
planningFallbackProvider |
Project | GET/PUT /api/settings |
Planning fallback provider |
planningFallbackModelId |
Project | GET/PUT /api/settings |
Planning fallback model id |
validatorProvider |
Project | GET/PUT /api/settings |
Validator model provider |
validatorModelId |
Project | GET/PUT /api/settings |
Validator model id |
validatorFallbackProvider |
Project | GET/PUT /api/settings |
Validator fallback provider |
validatorFallbackModelId |
Project | GET/PUT /api/settings |
Validator fallback model id |
modelPresets |
Project | GET/PUT /api/settings |
Reusable model presets |
autoSelectModelPreset |
Project | GET/PUT /api/settings |
Auto-preset by task size |
defaultPresetBySize |
Project | GET/PUT /api/settings |
Size→preset mapping |
autoResolveConflicts |
Project | GET/PUT /api/settings |
Smart conflict auto-resolution |
smartConflictResolution |
Project | GET/PUT /api/settings |
Alias for conflict automation |
strictScopeEnforcement |
Project | GET/PUT /api/settings |
Block out-of-scope file changes |
buildRetryCount |
Project | GET/PUT /api/settings |
Build retry attempts |
buildTimeoutMs |
Project | GET/PUT /api/settings |
Build timeout |
requirePlanApproval |
Project | GET/PUT /api/settings |
Manual plan approval gate |
taskStuckTimeoutMs |
Project | GET/PUT /api/settings |
Stuck task timeout |
autoUnpauseEnabled |
Project | GET/PUT /api/settings |
Auto unpause on rate limits |
autoUnpauseBaseDelayMs |
Project | GET/PUT /api/settings |
Base backoff delay |
autoUnpauseMaxDelayMs |
Project | GET/PUT /api/settings |
Max backoff delay |
maxStuckKills |
Project | GET/PUT /api/settings |
Max detector retries |
maxSpawnedAgentsPerParent |
Project | GET/PUT /api/settings |
Child agents per parent |
maxSpawnedAgentsGlobal |
Project | GET/PUT /api/settings |
Total spawned-agent cap |
maintenanceIntervalMs |
Project | GET/PUT /api/settings |
Maintenance cadence |
autoUpdatePrStatus |
Project | GET/PUT /api/settings |
PR badge polling |
autoCreatePr |
Project | GET/PUT /api/settings |
Automatic PR creation |
autoBackupEnabled |
Project | GET/PUT /api/settings |
Scheduled backup toggle |
autoBackupSchedule |
Project | GET/PUT /api/settings |
Backup cron schedule |
autoBackupRetention |
Project | GET/PUT /api/settings |
Backup retention count |
autoBackupDir |
Project | GET/PUT /api/settings |
Backup directory |
autoSummarizeTitles |
Project | GET/PUT /api/settings |
Auto-title generation |
titleSummarizerProvider |
Project | GET/PUT /api/settings |
Title model provider |
titleSummarizerModelId |
Project | GET/PUT /api/settings |
Title model id |
titleSummarizerFallbackProvider |
Project | GET/PUT /api/settings |
Title fallback provider |
titleSummarizerFallbackModelId |
Project | GET/PUT /api/settings |
Title fallback model id |
scripts |
Project | GET/PUT /api/settings |
Named script map |
setupScript |
Project | GET/PUT /api/settings |
Named setup script reference |
insightExtractionEnabled |
Project | GET/PUT /api/settings |
Insight extraction toggle |
insightExtractionSchedule |
Project | GET/PUT /api/settings |
Insight extraction schedule |
insightExtractionMinIntervalMs |
Project | GET/PUT /api/settings |
Minimum extraction interval |
memoryEnabled |
Project | GET/PUT /api/settings |
Memory system toggle |
tokenCap |
Project | GET/PUT /api/settings |
Token cap for compacting |
runStepsInNewSessions |
Project | GET/PUT /api/settings |
Step session isolation |
maxParallelSteps |
Project | GET/PUT /api/settings |
Parallel step cap |
agentPrompts |
Project | GET/PUT /api/settings |
Per-role prompt templates |
Additional backend notes:
githubTokenConfiguredis returned byGET /api/settingsbut is computed server-side, not persisted.- Non-settings config persisted in backend include
nextId,workflowSteps, andnextWorkflowStepId(configrow / config JSON compatibility path). *Global*keys are never persisted in project settings — these belong exclusively to global settings. Conversely, project-only keys (defaultProviderOverride,executionProvider,planningProvider, etc.) are never persisted in global settings. The two scopes are strictly isolated.
4) SQLite Tables Inventory (packages/core/src/db.ts)
| Table | Purpose |
|---|---|
tasks |
Core task metadata and JSON-backed nested fields (priority, dependencies, steps, log, attachments, comments, model overrides, workflow results, merge details, assignment, mission linkage). |
The tasks.githubTracking JSON column stores per-task GitHub tracking state (enabled, optional repoOverride, linked issue metadata, and unlinkedAt). It is additive and default-off; imported-source issue metadata remains in issueInfo / sourceIssue. Behavior wiring (issue creation/lifecycle sync and UI surfacing) lands in FN-3870/FN-3873/FN-3874.
| config | Single-row project configuration (nextId, settings payload, workflow step counters). |
| workflow_steps | Workflow step definitions (prompt/script) with phase, template metadata, and model overrides. |
| activityLog | Per-project activity/event log with timestamp/type/task indexes. |
| archivedTasks | Archived task snapshots (compact JSON payload + archive timestamp). |
| automations | Scheduled automation definitions, run state, and run history. |
| agents | Agent registry/state/task assignment metadata. |
| agentHeartbeats | Heartbeat run events linked to agents (agentId FK cascade). |
| approval_requests | Durable approval request records: requester actor snapshot, target action payload (category/action/resource/context), lifecycle status (pending/approved/denied/completed), optional task/run context, and requested/decided/completed timestamps. |
| approval_request_audit_events | Append-only audit trail for approval requests. Each row stores event type (created/approved/denied/completed), immutable actor snapshot, optional note, and deterministic per-request ordering by (createdAt, rowid). |
| task_documents | Task-scoped document metadata/content keyed by (taskId, key) with current revision pointer. |
| task_document_revisions | Immutable revision history for task documents (content snapshots by revision). |
| __meta | Schema version + monotonic lastModified change detector. |
| missions | Mission-level planning hierarchy root. |
| milestones | Milestones under missions, including dependency lists and validation state. |
| slices | Slices under milestones with plan-state/activation metadata. |
| mission_features | Features under slices with optional task linkage and execution-loop counters/state. |
| mission_events | Mission event log with ordered sequence numbers and metadata payloads. |
| plugins | Plugin registry, lifecycle state, dependency metadata, and settings blobs. |
| routines | Routine definitions (trigger config, steps/command, catch-up policy, run history, and persisted agentId ownership metadata). Legacy databases missing routine fields (including agentId) are backfilled during init-time compatibility migration. |
| roadmaps | Roadmap plugin metadata (owned/registered by plugins/fusion-plugin-roadmap). |
| roadmap_milestones | Milestones within roadmaps (roadmapId FK), owned/registered by roadmap plugin schema hooks. |
| roadmap_features | Features within roadmap milestones (milestoneId FK), owned/registered by roadmap plugin schema hooks. |
| project_insights | Extracted project insights with fingerprint-based deduplication and provenance metadata. |
| project_insight_runs | Insight extraction run history with durable lifecycle metadata (lifecycle JSON includes terminalReason/cause, failureClass, retryable flag, cancellationRequestedAt, timeoutAt, retry lineage fields). Terminal rows are immutable for state transitions. |
| project_insight_run_events | Append-only per-run lifecycle trail (seq, type, message, optional status/classification/metadata) used by cancel/retry/timeout auditing and API inspection. |
| todo_lists | Project-scoped todo list metadata (projectId, title, created/updated timestamps). |
| todo_items | Todo list items (listId FK) with completion state, completion timestamp, and deterministic sortOrder. |
| ai_sessions (migration-created) | Persisted AI interactive sessions (planning/interview/subtask) with status and conversation history. |
| messages (migration-created) | Inter-agent/user message mailbox storage. |
| agentRatings (migration-created) | Agent performance ratings (1-5), optional reviewer metadata, and run/task attribution. |
| chat_sessions (migration-created) | Chat session metadata (agent/project/model/status/title timestamps). |
| chat_messages (migration-created) | Chat message history per session (role, content, thinking output, metadata). |
| chat_rooms (migration-created) | Room metadata (name, slug, description, projectId, createdBy, status and timestamps). |
| chat_room_members (migration-created) | Room membership map with composite PK (roomId, agentId) and role (owner/member). |
| chat_room_messages (migration-created) | Room message history with senderAgentId, JSON mentions, attachments/metadata blobs, ordered by createdAt. |
| runAuditEvents (migration-created) | Run audit trail events across database/git/filesystem mutation domains. |
| mission_contract_assertions (migration-created) | Milestone contract assertions used by mission validator workflows. |
| mission_feature_assertions (migration-created) | Many-to-many links between mission features and contract assertions. |
| mission_validator_runs (migration-created) | Validator run records for mission feature loop execution. |
| mission_validator_failures (migration-created) | Assertion failure records captured during validator runs. |
| mission_fix_feature_lineage (migration-created) | Source↔fix feature lineage for auto-generated mission fix features. |
| research_runs | Research run state (query, topic, status, lifecycle, sources, results, citations, events, exports, token usage). Supports project-scoped active-run uniqueness via (projectId, trigger, status) index. Terminal runs are immutable. |
| research_exports | Persisted export records for research runs (runId FK cascade). Stores format, content, and optional file path. |
| research_run_events | Append-only event log for research run lifecycle tracking (runId FK cascade, ordered by seq). Records status transitions, phase changes, step lifecycle, and failure classifications. |
| eval_runs | Eval run lifecycle state (status, trigger, scope, evaluation window boundaries, evaluated task IDs/counts, aggregate scores, provenance). |
| eval_task_results | Per-task eval outcomes linked to runs (runId FK cascade), including durable task snapshots and structured score payloads. categoryScores[] stores canonical per-category fields (category, deterministicScore, aiScore, finalScore, weight, band, rationale, evidence[]), plus overallScore derived from category finals. Also stores deterministic/AI signal payloads, summary rationale, structured follow-up suggestions (suggestionId, dedupeKey, recommendation, lifecycle state, suppression fields, optional createdTaskId linkage), and a bounded TaskEvaluationEvidenceBundle (fixed source-order groups, capped entry counts, max 500-char excerpts with truncation marker) embedded in result metadata for backward-compatible persistence. |
| eval_run_events | Append-only eval run event trail (runId FK cascade, ordered by seq) for orchestration/debug auditing and downstream API/UI drill-down. |
Schema self-heal on init
Database.init() runs versioned migrations first, then checks __meta.schemaCompatFingerprint against a process-local fingerprint derived from SCHEMA_VERSION plus the canonicalized table declarations from SCHEMA_SQL and MIGRATION_ONLY_TABLE_SCHEMAS.
- Fingerprint match: skip the expensive column-reconciliation walk, but still run the cheap idempotent side effects that must always happen on open (for example
CREATE INDEX IF NOT EXISTS ...and routines NULL backfills). - Fingerprint absent or mismatched: run the full schema-compatibility reconciliation pass, unioning table definitions from
SCHEMA_SQLplusMIGRATION_ONLY_TABLE_SCHEMASand backfilling missing columns on tables that already exist, then persist the new fingerprint.
Invariant: after init, every declared column for covered tables exists regardless of __meta.schemaVersion whenever the fingerprint is stale or missing, preventing legacy drift from causing no such column regressions on newly added fields while keeping unchanged-schema opens fast.
Chat rooms (migration 70)
ChatStore now persists room chat data across three tables: chat_rooms, chat_room_members, and chat_room_messages.
chat_roomsstores canonical room identity (id, normalizedname, uniqueslugscoped byprojectId), metadata (description,createdBy), lifecycle status, and timestamps.chat_room_memberslinks agents to rooms via composite primary key(roomId, agentId)and tracksroleplusaddedAt.chat_room_messagesstores room history with message role/content, optionalthinkingOutput, JSONmetadata, JSONattachments, optionalsenderAgentId, and JSONmentions.- Foreign keys from members/messages to
chat_rooms(id)useON DELETE CASCADE, so deleting a room automatically removes memberships and room message history.
5) Issues Found
-
Theme dual-storage sync gap
- Severity: High
- Affected:
hooks/useTheme.ts,App.tsx,SettingsModal.tsx, global settings API (/api/settings/global) - Problem: Theme is persisted in both localStorage (
kb-dashboard-theme-mode,kb-dashboard-color-theme) and backend global settings (themeMode,colorTheme), but app bootstrap uses localStorage-only theme hydration. If backend and browser cache diverge, cross-device consistency breaks. - Recommended fix: Make backend global settings the source of truth (or explicitly define local cache precedence + bidirectional sync strategy and conflict resolution).
-
Project-unscoped localStorage keys in multi-project UX state
- Severity: High
- Affected:
App.tsx,ListView.tsx,QuickEntryBox.tsx,InlineCreateCard.tsx,AgentsView.tsx,useTerminalSessions.ts,useAgentHierarchy.ts,UsageIndicator.tsx - Problem: Many keys are global (
kb-dashboard-task-view,kb-dashboard-list-*,kb-dashboard-selected-tasks,kb-quick-entry-text,kb-inline-create-text,kb-terminal-tabs, etc.) and are reused across projects. This can leak preferences/drafts/selections between projects unexpectedly. - Recommended fix: Namespace project-specific keys with
projectId(e.g.,kb:{projectId}:dashboard-list-columns). Keep only true global prefs unscoped.
-
kb-dashboard-selected-taskscan carry stale selections across projects- Severity: Medium
- Affected:
components/ListView.tsx - Problem: Selected task IDs persist globally. In multi-project setups with overlapping ID patterns, stale selections can reappear and affect bulk operations unexpectedly.
- Recommended fix: Project-scope this key, and/or treat selection as in-memory/session-only state.
-
Terminal session persistence stores operational identifiers in localStorage
- Severity: Medium
- Affected:
hooks/useTerminalSessions.ts(kb-terminal-tabs) - Problem: Session IDs and tab metadata persist client-side and are not project-scoped. This is operational state better owned by backend/session layer; stale tabs also survive cache until cleanup logic runs.
- Recommended fix: Move terminal tab/session state to server persistence (or at minimum sessionStorage + project scoping + TTL/versioning).
-
Current project persistence stores full
ProjectInfoobject (includes filesystem path)- Severity: Medium
- Affected:
hooks/useCurrentProject.ts(kb-dashboard-current-project) - Problem: Storing full project objects increases drift risk and stores more data than needed (including local path).
- Recommended fix: Persist only stable
projectId; resolve current object from backend project list each load.
-
Draft persistence is local-only (device/browser-bound)
- Severity: Medium
- Affected:
modalPersistence.ts,QuickEntryBox.tsx,InlineCreateCard.tsx - Problem: Planning/subtask/mission/task-entry drafts are lost on storage clear or browser/device switch.
- Recommended fix: Keep local quick-draft behavior, but add optional server-backed drafts (short TTL) for continuity.
-
Settings scope key lists drift from interfaces
- Severity: Medium
- Affected:
packages/core/src/types.ts,store.ts,routes.ts,SettingsModal.tsx - Problem:
GLOBAL_SETTINGS_KEYS(14) omitssetupComplete,favoriteProviders,favoriteModels;PROJECT_SETTINGS_KEYS(52) omits 9 project interface keys (strictScopeEnforcement,buildRetryCount,buildTimeoutMs,autoUnpause*,maintenanceIntervalMs,scripts,setupScript). This creates scope-classification and patch-filter inconsistencies. - Recommended fix: Generate key lists from schema/interface source (or enforce parity tests) to prevent drift.
-
fn-agent-viewshared by two UIs with different supported modes- Severity: Low
- Affected:
AgentsView.tsx,AgentListModal.tsx - Problem: Both share the same key, but one surface supports
treeand the modal supports onlyboard/list; behavior remains valid but coupling is implicit. - Recommended fix: Decide intentional shared behavior and document it; otherwise split keys by surface.
-
Workflow steps still persisted in config JSON compatibility path (known in-progress work)
- Severity: Low
- Affected:
config.settings/workflowSteps,db.tsconfig table - Problem: Workflow step storage is still tied to config blob structure; this is already being addressed by FN-1201 (migration to dedicated SQLite table).
- Recommended fix: Continue and complete FN-1201; remove config-blob coupling after migration.
6) Recommendations (Prioritized)
P0 — High impact / should do first
-
Unify theme persistence contract
- Backend global settings should be canonical for multi-device consistency.
- Keep localStorage only as startup cache, with explicit hydration/sync rules.
-
Project-scope localStorage keys for project-specific UX state
- Scope at least:
kb-dashboard-task-view, list settings (columns,hide-done,collapsed,selected-tasks), drafts, terminal tabs, agent hierarchy. - Preserve unscoped behavior only for truly global prefs (e.g., appearance if desired).
- Scope at least:
-
Fix settings key parity drift (
*_SETTINGS_KEYSvs interfaces)- Add tests to fail when interface keys and key arrays diverge.
- Prevent accidental mis-scoping and patch filtering anomalies.
P1 — Medium impact
-
Reduce persisted identity payloads
- Store only
projectIdfor current project selection, not full object/path.
- Store only
-
Rework terminal tab persistence model
- Prefer server-managed tab/session restoration or at minimum short-lived, project-scoped client persistence with cleanup/versioning.
-
Adjust selected-task persistence strategy
- Move selection to memory/session scope or project-scoped key with validation on project switch.
P2 — Lower effort / UX polish
-
Optional server-backed draft recovery
- Keep local fast drafts; add opt-in backend draft sync for cross-browser resilience.
-
Clarify shared
fn-agent-viewsemantics- Either intentionally share and document, or split keys by surface.
-
Complete FN-1201 workflow-step migration
- Keep as tracked in-progress storage hardening item.
7) Verification Checklist (for this audit)
- All runtime localStorage keys in
packages/dashboard/appcataloged - Theme dual-storage gap addressed
- Current-project persistence behavior addressed
- Planning/subtask/mission draft behavior addressed
- ListView state scoping addressed
- Terminal tab persistence addressed (
kb-terminal-tabs) - QuickEntry expanded key addressed (
kb-quick-entry-expandedlegacy cleanup) - Agent hierarchy expand state addressed (
fn-agent-tree-expanded) - Backend settings + API route inventory included
- SQLite table inventory included
- Known in-progress FN-1201 called out
Per-Worktree DB Hydration
Each git worktree has its own gitignored .fusion/ directory, so .fusion/fusion.db is local scratch state per worktree. That isolation created a cross-task lookup gap: executor prompts that query sibling/dependency rows directly from the worktree DB could see empty results. FN-3840 documented the manual ATTACH/INSERT OR REPLACE recovery, and FN-3832 was the breaking case that surfaced this in production.
Fusion now auto-hydrates the worktree DB during executor startup at three points:
- after fresh worktree creation (including init/setup commands),
- after pooled worktree acquire/reassignment,
- when reusing an existing on-disk worktree for resume.
Hydration copies only:
- current task row,
- transitive dependency task rows (BFS, depth cap 5, max 50 unique task IDs),
task_documentsrows for that same task-id set.
Implementation uses in-process SQLite streaming (DatabaseSync), source-side SELECT, destination-side INSERT OR REPLACE inside a destination transaction. Column lists are built from source/destination schema intersection (PRAGMA table_info), so schema drift degrades gracefully (dropped columns are logged once, and defaults apply on destination-only columns).
Example shape of the destination write:
INSERT OR REPLACE INTO tasks (<shared-columns...>) VALUES (<placeholders...>);
INSERT OR REPLACE INTO task_documents (<shared-columns...>) VALUES (<placeholders...>);
Expected executor log entry on success:
Hydrated worktree DB: 4 tasks, 12 task_documents
A concrete recovered failure mode now covered by tests: when a worktree directory exists but its local .fusion/ scratch state is missing, opening DatabaseSync(<worktree>/.fusion/fusion.db) can fail with unable to open database file. Hydration now performs destination bootstrap (mkdir -p .fusion + schema init) and retries the destination open once before degrading.
Failure policy remains strict non-blocking for genuinely unrecoverable cases: hydration warnings are logged, but worktree creation/execution continues. Examples that still intentionally degrade include source DB missing, destination write-permission failures, and irreconcilable schema/open errors after bootstrap retry. Canonical task data remains the root project TaskStore DB; if an agent needs non-hydrated rows immediately, fn_task_show remains the canonical fallback path.