Skip to content

[Bugfix #774] Open workspace state.db in detectCurrentBuilderId#775

Merged
waleedkadous merged 2 commits into
mainfrom
builder/bugfix-774
May 19, 2026
Merged

[Bugfix #774] Open workspace state.db in detectCurrentBuilderId#775
waleedkadous merged 2 commits into
mainfrom
builder/bugfix-774

Conversation

@waleedkadous
Copy link
Copy Markdown
Contributor

Summary

Fixes #774

Root Cause

detectCurrentBuilderId() (in packages/codev/src/agent-farm/commands/send.ts) used the singleton getDb() via loadState(). From a CWD inside .builders/<id>/, findWorkspaceRoot() resolves to the worktree (worktrees are full git checkouts with their own codev/), so getDb() opens the worktree-local .agent-farm/state.db.

That DB is empty (it gets created on first open with the schema, but no builder rows). state.builders.find(...) returns undefined, and the function falls back to the worktree directory name (e.g. bugfix-1599) — not the canonical builder-bugfix-1599.

Downstream, in tower-messages.ts:318, lookupBuilderSpawningArchitect('bugfix-1599', workspacePath) returns undefined because the row is keyed builder-bugfix-1599. The router skips the builder-context branch and routes to 'main' instead of the sibling architect (ob-refine) that spawned the builder. Sibling-architect affinity routing was effectively non-functional.

Fix

detectCurrentBuilderId() now opens the workspace's state.db directly (readonly), mirroring the per-workspace-handle pattern already used in lookupBuilderSpawningArchitect (state.ts:380):

  • Parse the workspace path out of the .builders/<id>/ CWD pattern (it's always two levels up).
  • Open <workspace>/.agent-farm/state.db readonly via a fresh better-sqlite3 handle.
  • Match by canonical worktree path first, then a tail-segment match as a legacy-row safety net.
  • Fall back to the worktree directory name only when the workspace DB is missing or has no matching row.

The worktree-local state.db is no longer touched by the send path. The loadState() import is retained — it's still used by the broadcast (--all) helper, which legitimately reads from the singleton because it's enumerating builders the singleton workspace knows about.

Test Plan

  • Reproduced the bug locally: from .builders/bugfix-774/, detectCurrentBuilderId() returned bugfix-774 and created an empty worktree-level state.db.
  • Verified after the fix: returns builder-bugfix-774 and does not create a worktree-level state.db.
  • Added 5 regression tests in bugfix-774-detect-builder-id.test.ts covering:
    • canonical resolution when CWD is the worktree (no worktree-local DB)
    • canonical resolution when the worktree has its own empty state.db (the original bug)
    • fallback to worktree dir name when workspace DB is missing
    • fallback when no row matches
    • returns null when CWD is not a builder worktree
  • All 1775 agent-farm unit tests pass (vitest run src/agent-farm).

CMAP Review

Skipped per BUGFIX scope — fix is < 200 LOC, narrowly scoped to a single function, with high-coverage regression tests. Happy to run if the architect would like.

Scope

  • 1 source change (commands/send.ts): replace singleton lookup with workspace-direct readonly DB read.
  • 1 new test file (5 cases).
  • Net diff: ~135 LOC (well under 300).

When a builder runs `afx send architect` from inside `.builders/<id>/`,
`detectCurrentBuilderId()` resolved the singleton DB via getDb(), which
walks up via findWorkspaceRoot() and stops at the worktree itself
(worktrees are full git checkouts with their own codev/). The result is
that getDb() opens the worktree-local `.agent-farm/state.db`, which is
empty — so the lookup misses and the function falls back to the
worktree directory name (e.g. `bugfix-1599`) instead of the canonical
`builder-bugfix-1599`.

Downstream, tower-messages.ts looks up the spawning architect by ID;
the mismatch causes lookupBuilderSpawningArchitect to return undefined,
and the message routes to 'main' instead of the sibling architect that
spawned the builder. Multi-architect affinity routing was non-functional.

Fix: derive workspace path from the CWD's `.builders/<id>/` pattern and
open that workspace's state.db directly (readonly), mirroring the
per-workspace handle pattern in lookupBuilderSpawningArchitect. The
worktree-local state.db is no longer touched.

Adds 5 regression tests covering the canonical path, the empty-worktree-DB
case (the original bug), missing-DB fallback, no-match fallback, and the
non-builder-CWD case.
@waleedkadous
Copy link
Copy Markdown
Contributor Author

Reviewed and verified end-to-end against shannon's real state.db. Walked through the fix from CWD /Users/.../shannon/.builders/bugfix-1599:

  • Regex extracts workspace path + worktree dir
  • Opens workspace state.db readonly
  • Canonical-worktree match → returns builder-bugfix-1599

That's the right answer. Downstream lookupBuilderSpawningArchitect then resolves ob-refine and routes to its terminal. Bug fully addressed.

Particularly liked:

  • readonly: true on the new handle — sidesteps the root cause (worktree state.db getting bootstrapped by stray getDb() calls from inside the worktree).
  • Two-stage match (canonical path → tail fallback) handles both the common case and any legacy rows from a different machine.
  • Mirrored the existing per-workspace-handle pattern from state.ts:380 rather than inventing a new one.

The 5 new tests cover the bug scenario (worktree has its own empty state.db) cleanly.

One thought for a future PR (NOT this one): worktree-local empty state.db files will keep getting created by other code paths that call getDb() from inside a worktree. Doesn't break routing anymore, but a guard at getDb() itself ("refuse to bootstrap state.db inside .builders/*/") would be a nice belt-and-braces. Out of scope here.

Pending architect approval to merge.

@waleedkadous waleedkadous merged commit 75b82be into main May 19, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Multi-architect routing: builder→architect messages misrouted to 'main' when worktree has its own state.db

1 participant