Problem
After a machine crash or Tower recovery event, sibling architects that the user deliberately removed via afx workspace remove-architect --name X come back to life — with their old conversation restored via --resume. The user has to remove them again.
Reported by users 2026-07-08.
Root cause (from subagent investigation 2026-07-08)
removeArchitect (packages/codev/src/agent-farm/servers/tower-instances.ts:1115-1193) structurally does the right thing: sets intentionallyStopping, kills the shellper, deletes both DB rows (architects + terminal_sessions), awaits exit. BUT the DB deletes at tower-instances.ts:1174 and tower-instances.ts:1179 are wrapped in silent try { … } catch { /* best-effort cleanup */ }:
try { setArchitectByName(resolvedPath, name, null); } catch { /* best-effort cleanup */ }
try { _deps.deleteTerminalSession(terminalId); } catch { /* best-effort cleanup */ }
The architects row can survive intentional removal via three concerns, combining:
- Silent DB write failure (SQLITE_BUSY beyond 5s timeout, IO error) — user sees
"Removed architect 'X'." even though the row is intact.
- WAL fsync lag on OS crash:
packages/codev/src/agent-farm/db/index.ts:34-40 opens SQLite with synchronous = NORMAL in WAL mode. On power loss / kernel panic, recently committed transactions in WAL that weren't fsync'd can be lost (SQLite's own docs call this the tradeoff of NORMAL vs FULL).
launchInstance's sibling reconciliation loop (tower-instances.ts:687-699) reads getArchitects(resolvedPath) and unconditionally calls addArchitect() for each non-main row. addArchitect then reads the stored session_id and resumes via --resume. No liveness check, no cross-check against terminal_sessions.
Net: any residual architects row (from silent write failure, WAL loss, or a partial removal) causes the sibling to come back on the next Tower start — with its old conversation resumed via --resume, which for a stale session id also triggers the RC2 crash-loop.
Fix sketch — two parts
Part 1: remove the silent-fail in removeArchitect
Location: tower-instances.ts:1174-1180
If either DB delete throws, return { success: false, error } and surface it to the user instead of swallowing. Making the user retry when Tower is happy is worse than a false-positive "removed" that doesn't stick.
Part 2: gate sibling reconciliation on a liveness signal
Location: tower-instances.ts:687-699
Before addArchitect() in the reconcile loop, require either:
- A matching
terminal_sessions row for the same (workspace, role_id), OR
- A
session_id whose jsonl exists on disk
A row that has neither is a "dead" registration and should be pruned rather than respawned.
Optional (defense in depth)
Switch db/index.ts:40 to synchronous = FULL for architect-write transactions. Small blast radius, low write rate, closes the WAL loss window at OS-crash time.
Scope
packages/codev/src/agent-farm/servers/tower-instances.ts — removeArchitect (surface DB errors) + sibling reconcile loop (liveness gate)
- Optional:
packages/codev/src/agent-farm/db/index.ts (synchronous mode for architect writes)
- Tests:
- simulate DB write failure during
removeArchitect, verify user gets error not success: true
- simulate
architects row with no matching terminal_sessions row, verify reconcile prunes rather than respawns
- simulate
architects row with stale session_id (jsonl missing), verify reconcile prunes
Related
Problem
After a machine crash or Tower recovery event, sibling architects that the user deliberately removed via
afx workspace remove-architect --name Xcome back to life — with their old conversation restored via--resume. The user has to remove them again.Reported by users 2026-07-08.
Root cause (from subagent investigation 2026-07-08)
removeArchitect(packages/codev/src/agent-farm/servers/tower-instances.ts:1115-1193) structurally does the right thing: setsintentionallyStopping, kills the shellper, deletes both DB rows (architects+terminal_sessions), awaits exit. BUT the DB deletes attower-instances.ts:1174andtower-instances.ts:1179are wrapped in silenttry { … } catch { /* best-effort cleanup */ }:The
architectsrow can survive intentional removal via three concerns, combining:"Removed architect 'X'."even though the row is intact.packages/codev/src/agent-farm/db/index.ts:34-40opens SQLite withsynchronous = NORMALin WAL mode. On power loss / kernel panic, recently committed transactions in WAL that weren't fsync'd can be lost (SQLite's own docs call this the tradeoff of NORMAL vs FULL).launchInstance's sibling reconciliation loop (tower-instances.ts:687-699) readsgetArchitects(resolvedPath)and unconditionally callsaddArchitect()for each non-main row.addArchitectthen reads the storedsession_idand resumes via--resume. No liveness check, no cross-check againstterminal_sessions.Net: any residual
architectsrow (from silent write failure, WAL loss, or a partial removal) causes the sibling to come back on the next Tower start — with its old conversation resumed via--resume, which for a stale session id also triggers the RC2 crash-loop.Fix sketch — two parts
Part 1: remove the silent-fail in
removeArchitectLocation:
tower-instances.ts:1174-1180If either DB delete throws, return
{ success: false, error }and surface it to the user instead of swallowing. Making the user retry when Tower is happy is worse than a false-positive "removed" that doesn't stick.Part 2: gate sibling reconciliation on a liveness signal
Location:
tower-instances.ts:687-699Before
addArchitect()in the reconcile loop, require either:terminal_sessionsrow for the same (workspace, role_id), ORsession_idwhose jsonl exists on diskA row that has neither is a "dead" registration and should be pruned rather than respawned.
Optional (defense in depth)
Switch
db/index.ts:40tosynchronous = FULLfor architect-write transactions. Small blast radius, low write rate, closes the WAL loss window at OS-crash time.Scope
packages/codev/src/agent-farm/servers/tower-instances.ts—removeArchitect(surface DB errors) + sibling reconcile loop (liveness gate)packages/codev/src/agent-farm/db/index.ts(synchronous mode for architect writes)removeArchitect, verify user gets error notsuccess: truearchitectsrow with no matchingterminal_sessionsrow, verify reconcile prunes rather than respawnsarchitectsrow with stalesession_id(jsonl missing), verify reconcile prunesRelated
buildResumehas no ownership check)--resume) — surviving architect rows here often carry stale session_ids that trigger RC2's crash-loop as the visible symptom