You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Consolidate state.db tables into global.db (single user-global database) to eliminate the cwd-dependent fragmentation that causes "missing architect state after restart" #1118
.agent-farm/state.db is named and located as if it's workspace-local, but it actually holds rows from every workspace Tower has ever interacted with while parked in that directory. Combined with Tower's start-cwd determining which state.db file is the "active" one for a session, this produces a fragmentation pattern where:
Tower running from workspace A → reads/writes A/.agent-farm/state.db
During that session, any cross-workspace interaction (afx send from workspace B, dashboard switching to workspace C, VS Code extension calling Tower from workspace D) lands its rows in A's state.db.
Reboot. Tower next starts from workspace B → reads/writes B/.agent-farm/state.db. Rows from previous session in A's file are now invisible to the running Tower, even though they're intact on disk.
User-facing symptom (verified against this user's machine): "after a computer restart, some architects in my workspaces are missing their state / session data." The architects whose rows happened to land in Tower's previous start-cwd's state.db are stranded; the architects whose rows are in the current Tower's state.db work fine. Hence "some" architects missing, not "all."
Audit confirming the pattern
8 state.db files exist across a multi-workspace user's local checkouts:
0 (empty file, lazy-init from a CLI command run there)
Workspaces appearing in multiple state.db files (the fragmentation symptom):
cluesmith/codev → codev's DB + shannon's DB
cluesmith/shannon → codev's DB + shannon's DB
cluesmith/codev-streamdeck → codev's DB + kidscanspell's DB
Multiple builder worktrees scattered across 3-4 different files
Population affected
Verified against the spawn paths:
User class
Affected?
Single-workspace user with manual afx tower start from project root
No
Single-workspace user with VS Code extension auto-start
No
Multi-workspace user with manual afx tower start from "wherever they happen to be"
Yes
Multi-workspace user with VS Code extension auto-start
Yes (the extension passes cwd: workspacePath to spawn, so whichever Codev project is opened first after a Tower-restart determines the location)
Local-install dev user
Yes (every pnpm -w run local-install runs afx tower start from the script's invocation cwd)
This is a default-behaviour issue for any user with more than one Codev project on disk and any Tower-restart history (machine reboot, pnpm -w run local-install, manual afx tower stop/start, VS Code switching projects). It is not a developer-environment-only issue.
Verified spawn-path code locations
packages/codev/src/agent-farm/commands/tower.ts — afx tower start spawns with cwd: process.cwd(). Inherits the shell's cwd.
packages/codev/src/agent-farm/utils/config.ts — findWorkspaceRoot() walks up from cwd looking for .git + codev/; falls back to cwd if not found (which is how ~/.agent-farm/state.db got created when Tower was started from $HOME once).
packages/vscode/src/tower-starter.ts:47-51 — VS Code extension's autoStartTower spawns Tower with cwd: workspacePath ?? undefined. The VS Code workspace folder becomes Tower's cwd.
packages/codev/src/agent-farm/db/index.ts — getDb() lazy-initialises state.db at <workspaceRoot>/.agent-farm/state.db, creating an empty file if absent. This is why workspaces where Tower never ran still have state.db files (CLI commands run from inside them lazy-created the file).
Why two databases at all?
state.db and global.db exist as separate files for archaeological reasons, not principled design. state.db predates the system-wide Tower architecture: back when Tower was per-workspace, each workspace had its own daemon and its own genuinely-workspace-local file. When Tower's architecture shifted to a system-wide singleton, the right move would have been to relocate state.db to a user-global location to match the new scope. Instead, Bugfix #826 added a workspace_path column (Migration v11) so rows from different workspaces could coexist in the same file — a symptom-patching fix that left state.db's location workspace-local while making its scope effectively global.
global.db was added separately around Spec 0090 / TICK-001 for inherently-cross-workspace tables (terminal_sessions, port_allocations, file_tabs, cron_tasks, known_workspaces). The split has no principled boundary today — it's "tables added before the singleton transition" vs "tables added after." Both files are functionally user-global; only state.db lies about its scope via its file location.
Proposed approach: collapse state.db tables into global.db (single user-global database)
All state.db tables (architect, builders, utils, annotations) move into ~/.agent-farm/global.db. The per-workspace state.db file is retired. Tower keeps one connection to one file regardless of which workspace it's serving; rows are disambiguated by the existing workspace_path column (already in place since Migration v11, Bugfix #826).
What changes
All state.db tables migrate into global.db in a new migration. global.db's migration system absorbs the schemas; a new global.db migration version becomes "absorb state.db tables (architect, builders, utils, annotations)".
getDb() and getGlobalDb() consolidate — getDb() always returns the global.db connection. The cwd-dependent state.db creation logic at db/index.ts is removed.
findWorkspaceRoot() is no longer load-bearing for db location — it stays for protocol/template resolution (the four-tier file resolver, unchanged), but the db path is now always ~/.agent-farm/global.db.
architect, builders, utils, annotations table schemas (workspace_path column stays as the row-disambiguator; same role it has today, just inside the single shared DB).
Migration v12's session_id column work — independent.
~/.agent-farm/global.db location.
The known_workspaces table in global.db (still the registry of workspaces Tower has ever touched).
The per-workspace .agent-farm/ directory — left in place (forward-compat for future per-workspace files; don't actively delete during migration).
Migration strategy
One-time on-disk migration during the Tower restart that follows the upgrade:
Discover all existing state.db files by reading known_workspaces and checking each workspace's .agent-farm/state.db. Also include ~/.agent-farm/state.db (the home-fallback that gets created when Tower was started from $HOME).
Per file: union its architect, builders, utils, annotations rows into global.db using INSERT OR REPLACE keyed on the primary key. Conflict resolution: latest-started_at wins. The workspace_path column preserves identity of rows from different workspaces.
Dry-run available: afx tower start --dry-run-migration lists every row that would be merged + flags conflicts (same row written to multiple state.db files with different content). --apply-migration (or running plain afx tower start after the upgrade) commits.
Source files preserved: do not delete state.db files during migration. Rename them to state.db.pre-merge-<timestamp> so users can recover if needed.
Idempotent: a marker row in global.db's _migrations table tracks completion. Subsequent Tower starts are no-ops on the migration check.
Stale-row hygiene
The "self-cleaning on workspace deletion" benefit of per-workspace state.db files is replaced by an explicit prune command:
afx prune-state — removes rows from architect, builders, utils, annotations whose workspace_path is not in known_workspaces. Dry-run by default; --apply to commit. Handles the stale-row accumulation case (the 17 test-workspace rows in this user's audit are exactly what it removes). Run on user demand, not automatically — the per-workspace "free cleanup via rm" pattern was always opt-in in practice.
afx workspace forget <path> — removes a workspace from known_workspaces AND prunes its associated state rows in one command. The clean way to retire a workspace whose directory has been deleted or moved.
Acceptance criteria
architect, builders, utils, annotations tables exist in global.db (added via a new migration).
getDb() returns the global.db connection from all callsites in state.ts.
A spawn flow in workspace A writes its architect row to ~/.agent-farm/global.db regardless of where Tower was started.
Reboot scenario: stop Tower from workspace A, start Tower from workspace B, verify workspace A's architects (now in global.db) are readable from Tower running at B.
Migration: existing state.db files are scanned, rows merged via INSERT OR REPLACE with latest-started_at-wins conflict resolution; source files renamed (not deleted).
Migration is idempotent: re-running doesn't re-migrate.
The cwd-dependent state.db creation logic is removed; findWorkspaceRoot() no longer drives db location.
afx prune-state removes rows whose workspace_path is not in known_workspaces; dry-run by default, --apply to commit.
afx workspace forget <path> removes a workspace from known_workspaces and prunes its rows in one command.
Unit tests cover: cross-workspace row isolation via workspace_path; migration row-routing correctness on multi-source merge with conflicts; migration idempotency; prune-state correctness against known_workspaces; workspace forget end-to-end.
Alternatives considered
Per-workspace state.db (each <workspace>/.agent-farm/state.db holds only its own rows; Tower opens N connections lazily, LRU-bounded). Gains: free cleanup on workspace deletion (rm the dir, state goes with it); portability (move the dir, state moves with it); self-cleaning of stale rows. Costs: N database handles requiring an LRU pool; per-callsite audit to thread workspace_path through every state.ts function; cross-workspace queries (dashboard's all-workspaces view, reconciliation passes) become iterations over known_workspaces opening each state.db on demand; the terminal_sessions ↔ architect/builder join stays cross-file; adds an architectural decision for future tables ("which workspace's DB?"). The cleanup and portability gains assume workflows (frequent rm of workspaces, moving workspace dirs) that aren't dominant; the self-cleaning gain is achievable cheaper via the prune-state command above. Rejected as more complex than the problem warrants.
Just-move state.db to ~/.agent-farm/state.db (keep the two-DB split, just relocate the path). Fixes the cwd-dependence; doesn't address the deeper architectural debt that the state.db / global.db split was always arbitrary. New table additions would still face the "which DB?" question. Rejected as a half-measure.
Force-migration on Tower startup with no opt-out / no dry-run. The migration is straightforward and well-defined, but a hard-blocking migration on first launch under new code feels coarse. The proposed approach makes migration automatic with a dry-run preview available and sources preserved as *.pre-merge-<timestamp> files. Same net effect, gentler upgrade story. Adopted in the proposal above.
Hybrid: per-workspace state.db AND user-global state.db with sync. Two sources of truth; sync correctness becomes its own problem. Rejected.
Out of scope
Moving terminal_sessions, port_allocations, cron_tasks, file_tabs out of global.db. They're already correctly user-global; no change needed.
Removing the workspace_path column from architect / builders / etc. It's still the row-disambiguator within the single shared DB; could be revisited as a v2 cleanup once the merge has lived for a while, but not for v1.
Auto-pruning stale rows on every Tower start. afx prune-state is opt-in.
Touching Migration v12's session_id work or any other in-flight migration.
Per-workspace .agent-farm/ directory cleanup. The directory stays for forward compat; let it die naturally if unused.
Protocol
PIR. This is a significant infrastructure change with multiple design decisions (migration dry-run UX, prune-state semantics, conflict-resolution policy on merge, the workspace forget command shape) that benefit from plan-gate validation, plus dev-gate verification of:
the multi-workspace reboot scenario on a running Tower
the dry-run migration preview against a real machine with fragmented state.db files
the prune-state command against synthetic known_workspaces/stale-row fixtures
Architect-row residue — the 17 test-workspace rows + 11 builder-worktree rows in this user's codev state.db (audited in the conversation that spawned this issue) are what afx prune-state is designed to remove cleanly.
Problem
.agent-farm/state.dbis named and located as if it's workspace-local, but it actually holds rows from every workspace Tower has ever interacted with while parked in that directory. Combined with Tower's start-cwd determining whichstate.dbfile is the "active" one for a session, this produces a fragmentation pattern where:A/.agent-farm/state.dbafx sendfrom workspace B, dashboard switching to workspace C, VS Code extension calling Tower from workspace D) lands its rows in A's state.db.B/.agent-farm/state.db. Rows from previous session in A's file are now invisible to the running Tower, even though they're intact on disk.User-facing symptom (verified against this user's machine): "after a computer restart, some architects in my workspaces are missing their state / session data." The architects whose rows happened to land in Tower's previous start-cwd's state.db are stranded; the architects whose rows are in the current Tower's state.db work fine. Hence "some" architects missing, not "all."
Audit confirming the pattern
8
state.dbfiles exist across a multi-workspace user's local checkouts:~/.agent-farm/state.db~/repos/cluesmith/codev/.agent-farm/state.db~/repos/cluesmith/shannon/.agent-farm/state.db~/repos/insighttrail/kidscanspell/.agent-farm/state.db~/repos/bb/MPPS2/.agent-farm/state.db~/repos/amrmelsayed/codev/.agent-farm/state.db~/repos/insighttrail/autotoggl/.agent-farm/state.db~/repos/cluesmith/codev/worktrees/changelog/.agent-farm/state.dbWorkspaces appearing in multiple state.db files (the fragmentation symptom):
cluesmith/codev→ codev's DB + shannon's DBcluesmith/shannon→ codev's DB + shannon's DBcluesmith/codev-streamdeck→ codev's DB + kidscanspell's DBPopulation affected
Verified against the spawn paths:
afx tower startfrom project rootafx tower startfrom "wherever they happen to be"cwd: workspacePathto spawn, so whichever Codev project is opened first after a Tower-restart determines the location)pnpm -w run local-installrunsafx tower startfrom the script's invocation cwd)This is a default-behaviour issue for any user with more than one Codev project on disk and any Tower-restart history (machine reboot,
pnpm -w run local-install, manualafx tower stop/start, VS Code switching projects). It is not a developer-environment-only issue.Verified spawn-path code locations
packages/codev/src/agent-farm/commands/tower.ts—afx tower startspawns withcwd: process.cwd(). Inherits the shell's cwd.packages/codev/src/agent-farm/utils/config.ts—findWorkspaceRoot()walks up from cwd looking for.git+codev/; falls back to cwd if not found (which is how~/.agent-farm/state.dbgot created when Tower was started from$HOMEonce).packages/vscode/src/tower-starter.ts:47-51— VS Code extension'sautoStartTowerspawns Tower withcwd: workspacePath ?? undefined. The VS Code workspace folder becomes Tower's cwd.packages/codev/src/agent-farm/db/index.ts—getDb()lazy-initialises state.db at<workspaceRoot>/.agent-farm/state.db, creating an empty file if absent. This is why workspaces where Tower never ran still have state.db files (CLI commands run from inside them lazy-created the file).Why two databases at all?
state.dbandglobal.dbexist as separate files for archaeological reasons, not principled design.state.dbpredates the system-wide Tower architecture: back when Tower was per-workspace, each workspace had its own daemon and its own genuinely-workspace-local file. When Tower's architecture shifted to a system-wide singleton, the right move would have been to relocate state.db to a user-global location to match the new scope. Instead, Bugfix #826 added aworkspace_pathcolumn (Migration v11) so rows from different workspaces could coexist in the same file — a symptom-patching fix that left state.db's location workspace-local while making its scope effectively global.global.dbwas added separately around Spec 0090 / TICK-001 for inherently-cross-workspace tables (terminal_sessions, port_allocations, file_tabs, cron_tasks, known_workspaces). The split has no principled boundary today — it's "tables added before the singleton transition" vs "tables added after." Both files are functionally user-global; only state.db lies about its scope via its file location.Proposed approach: collapse state.db tables into global.db (single user-global database)
All state.db tables (
architect,builders,utils,annotations) move into~/.agent-farm/global.db. The per-workspacestate.dbfile is retired. Tower keeps one connection to one file regardless of which workspace it's serving; rows are disambiguated by the existingworkspace_pathcolumn (already in place since Migration v11, Bugfix #826).What changes
getDb()andgetGlobalDb()consolidate —getDb()always returns the global.db connection. The cwd-dependent state.db creation logic atdb/index.tsis removed.findWorkspaceRoot()is no longer load-bearing for db location — it stays for protocol/template resolution (the four-tier file resolver, unchanged), but the db path is now always~/.agent-farm/global.db.workspace_pathas their first argument (post-Bugfix-CRITICAL: Sibling architects leak across workspaces — launchInstance reconcile reads global state.db.architect without workspace filtering #826), which is now the row-disambiguator within the single shared DB rather than the path-resolver.SELECT JOIN. The afx: status builders table should include PID + terminal session id (parity with the architects section) #1115 PID-parity follow-up benefits directly.What doesn't change
architect,builders,utils,annotationstable schemas (workspace_pathcolumn stays as the row-disambiguator; same role it has today, just inside the single shared DB).session_idcolumn work — independent.~/.agent-farm/global.dblocation.known_workspacestable in global.db (still the registry of workspaces Tower has ever touched)..agent-farm/directory — left in place (forward-compat for future per-workspace files; don't actively delete during migration).Migration strategy
One-time on-disk migration during the Tower restart that follows the upgrade:
known_workspacesand checking each workspace's.agent-farm/state.db. Also include~/.agent-farm/state.db(the home-fallback that gets created when Tower was started from$HOME).architect,builders,utils,annotationsrows into global.db usingINSERT OR REPLACEkeyed on the primary key. Conflict resolution: latest-started_atwins. Theworkspace_pathcolumn preserves identity of rows from different workspaces.afx tower start --dry-run-migrationlists every row that would be merged + flags conflicts (same row written to multiple state.db files with different content).--apply-migration(or running plainafx tower startafter the upgrade) commits.state.db.pre-merge-<timestamp>so users can recover if needed._migrationstable tracks completion. Subsequent Tower starts are no-ops on the migration check.Stale-row hygiene
The "self-cleaning on workspace deletion" benefit of per-workspace state.db files is replaced by an explicit prune command:
afx prune-state— removes rows fromarchitect,builders,utils,annotationswhoseworkspace_pathis not inknown_workspaces. Dry-run by default;--applyto commit. Handles the stale-row accumulation case (the 17 test-workspace rows in this user's audit are exactly what it removes). Run on user demand, not automatically — the per-workspace "free cleanup via rm" pattern was always opt-in in practice.afx workspace forget <path>— removes a workspace fromknown_workspacesAND prunes its associated state rows in one command. The clean way to retire a workspace whose directory has been deleted or moved.Acceptance criteria
architect,builders,utils,annotationstables exist in global.db (added via a new migration).getDb()returns the global.db connection from all callsites in state.ts.~/.agent-farm/global.dbregardless of where Tower was started.findWorkspaceRoot()no longer drives db location.afx prune-stateremoves rows whoseworkspace_pathis not inknown_workspaces; dry-run by default,--applyto commit.afx workspace forget <path>removes a workspace from known_workspaces and prunes its rows in one command.workspace_path; migration row-routing correctness on multi-source merge with conflicts; migration idempotency; prune-state correctness against known_workspaces;workspace forgetend-to-end.Alternatives considered
Per-workspace state.db (each
<workspace>/.agent-farm/state.dbholds only its own rows; Tower opens N connections lazily, LRU-bounded). Gains: free cleanup on workspace deletion (rm the dir, state goes with it); portability (move the dir, state moves with it); self-cleaning of stale rows. Costs: N database handles requiring an LRU pool; per-callsite audit to thread workspace_path through every state.ts function; cross-workspace queries (dashboard's all-workspaces view, reconciliation passes) become iterations overknown_workspacesopening each state.db on demand; the terminal_sessions ↔ architect/builder join stays cross-file; adds an architectural decision for future tables ("which workspace's DB?"). The cleanup and portability gains assume workflows (frequentrmof workspaces, moving workspace dirs) that aren't dominant; the self-cleaning gain is achievable cheaper via theprune-statecommand above. Rejected as more complex than the problem warrants.Just-move state.db to
~/.agent-farm/state.db(keep the two-DB split, just relocate the path). Fixes the cwd-dependence; doesn't address the deeper architectural debt that the state.db / global.db split was always arbitrary. New table additions would still face the "which DB?" question. Rejected as a half-measure.Force-migration on Tower startup with no opt-out / no dry-run. The migration is straightforward and well-defined, but a hard-blocking migration on first launch under new code feels coarse. The proposed approach makes migration automatic with a dry-run preview available and sources preserved as
*.pre-merge-<timestamp>files. Same net effect, gentler upgrade story. Adopted in the proposal above.Hybrid: per-workspace state.db AND user-global state.db with sync. Two sources of truth; sync correctness becomes its own problem. Rejected.
Out of scope
terminal_sessions,port_allocations,cron_tasks,file_tabsout of global.db. They're already correctly user-global; no change needed.workspace_pathcolumn fromarchitect/builders/ etc. It's still the row-disambiguator within the single shared DB; could be revisited as a v2 cleanup once the merge has lived for a while, but not for v1.afx prune-stateis opt-in.session_idwork or any other in-flight migration..agent-farm/directory cleanup. The directory stays for forward compat; let it die naturally if unused.Protocol
PIR. This is a significant infrastructure change with multiple design decisions (migration dry-run UX, prune-state semantics, conflict-resolution policy on merge, the
workspace forgetcommand shape) that benefit from plan-gate validation, plus dev-gate verification of:before opening the PR.
Related
workspace_pathto thearchitecttable primary key when cross-workspace contamination was first discovered. That was the symptom-patching response; this issue is the structural fix that retires the now-redundant per-workspace file location.global.dbfor inherently-cross-workspace tables; this issue extends the same principle to the remaining state.db tables, which are also effectively user-global since Bugfix CRITICAL: Sibling architects leak across workspaces — launchInstance reconcile reads global state.db.architect without workspace filtering #826.afx statusbuilders PID parity. Benefits directly from the consolidation: the terminal_sessions ↔ builders join becomes a single-file query.afx prune-stateis designed to remove cleanly.