Skip to content

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

Description

@waleedkadous

Summary

Builder→architect messages are silently misrouted to main (instead of the spawning sibling architect) when run inside a builder worktree that has its own .agent-farm/state.db. The affinity primitive shipped in v3.0.5 is bypassed end-to-end for sibling-architect workspaces — every sibling architect (ob-refine in the reporting case) gets cut out of its own builders' messages.

Repro

In a workspace with two architects registered:

  • main (terminal fe6b1cd8...)
  • ob-refine (terminal 48ad2d87...)

ob-refine spawned builder-bugfix-1599. The builder's .agent-farm/state.db row correctly records:

builder-bugfix-1599 | implementing | spawned_by_architect=ob-refine | terminal=513db860...

The builder then ran afx send architect "..." from inside .builders/bugfix-1599/. Tower log:

[2026-05-19T22:01:13.318Z] [INFO] Message sent: bugfix-1599 → architect (terminal fe6b1cd8...)

Terminal fe6b1cd8... is main, not ob-refine. Wrong terminal.

Root cause

The sender logged is bugfix-1599, not the canonical builder-bugfix-1599. That mismatch breaks the affinity lookup downstream.

detectCurrentBuilderId() (packages/codev/src/agent-farm/commands/send.ts:48) does:

const worktreeDirName = match[1];                 // 'bugfix-1599'
const state = loadState();                        // <-- singleton DB
const builder = state.builders.find(b => /* worktree path match */);
return builder ? builder.id : worktreeDirName;    // fallback when miss

loadState() uses the singleton getDb(), which resolves stateDir via findWorkspaceRoot(). When CWD is .builders/<id>/, findWorkspaceRoot() returns the worktree path, not the workspace path, because the worktree is itself a full git checkout with its own codev/ (config.ts:79). So getDb() opens:

/Users/.../shannon/.builders/bugfix-1599/.agent-farm/state.db

This file exists (created on first DB open) and is empty:

$ sqlite3 .../bugfix-1599/.agent-farm/state.db "SELECT COUNT(*) FROM builders;"
0

So the state.builders.find(...) returns undefined, and detectCurrentBuilderId falls back to the worktree directory name bugfix-1599. Tower receives from='bugfix-1599'.

In tower-messages.ts:318, lookupBuilderSpawningArchitect('bugfix-1599', '/Users/.../shannon') returns undefined because no row exists with id='bugfix-1599' — the row is keyed builder-bugfix-1599. With spawningArchitect === undefined, the routing skips the builder-context branch (line 320-342) and falls through to the non-builder-sender branch at line 344-347:

const terminalId =
  entry.architects.get(DEFAULT_ARCHITECT_NAME) ?? entry.architects.values().next().value!;
return { terminalId, workspacePath, agent: 'architect' };

→ Routes to main. The spawning architect is silently bypassed.

Why this matters

Affinity routing was the headline feature of v3.0.5/v3.0.6 multi-architect. This bug means every sibling-architect workspace loses messages from its builders today. The porch wake-up path (gate-approved, pr-ready notifications) and the builder's own architect-bound updates all hit the wrong terminal. The data is correct in state.db, the dashboard is correct, the in-memory architect registry is correct — only the sender-ID resolution at send time is wrong.

Fix sketch

detectCurrentBuilderId should open the workspace's state.db directly, not the singleton (which resolves to the worktree's). The correct pattern already exists in state.ts:380 (lookupBuilderSpawningArchitect):

const dbPath = path.join(workspacePath, '.agent-farm', 'state.db');
if (!existsSync(dbPath)) return undefined;
const wsDb = new Database(dbPath, { readonly: true });
try {
  // ... lookup ...
} finally {
  wsDb.close();
}

Apply the same pattern in detectCurrentBuilderId: from CWD .builders/<id>/, the workspace root is two levels up; open that state.db (readonly), find the builder by worktree-path match, return its canonical id. The fallback to worktreeDirName should remain as a safety net but should never be hit in practice.

Add a regression test: spawn-fixture, builder in worktree with empty worktree-level state.db, assert detectCurrentBuilderId() returns the canonical builder-<X> ID by reading the workspace's state.db.

Verification

Manual repro on shannon@2026-05-19:

  • ob-refine spawned bugfix-1599
  • builder's afx send architect → routed to main terminal fe6b1cd8...
  • Expected: routed to ob-refine terminal 48ad2d87...

Severity

High — this silently breaks the multi-architect feature for every adopter (Shannon in production). User-visible symptom is "my sibling architect never hears from its builders." No data loss, but the feature is non-functional.

Affected versions

v3.0.5 onward (anything with sibling-architect support).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions