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
Follow-up to #830 (architect session revival). #829 shipped builder conversation resume via on-disk jsonl discovery. That same mechanism extends cleanly to the main architect (committed on the #829 branch) because main is the only architect using Tower's automatic launchInstance path. It does not extend to named sibling architects added via afx workspace add-architect (Spec 755), because they share cwd = workspacePath with each other and with main — and the jsonl-discovery heuristic (newest *.jsonl by mtime) cannot tell which jsonl belongs to which named architect.
This issue scopes the multi-architect disambiguation work that #830 left open.
Problem
A workspace with main + named siblings (e.g., architect-2, reviewer-bob) has multiple Tower-managed Claude PTYs running in the same cwd. Each writes its session jsonl to ~/.claude/projects/<encoded-workspacePath>/. After a reboot:
The reboot kills every architect's shellper.
On afx workspace start, Tower's launchInstance re-spawns main. Spec 786 Phase 3 then iterates state.db.architect to re-add each persisted sibling via addArchitect().
So named siblings come back as fresh sessions, losing all conversation context. Operators using multi-architect workspaces lose more on reboot than single-architect users do.
Why the named-architect case is sharper than main's
For specialised siblings (reviewer, demos, casa, etc.), there is no native per-architect role-doc loading in Codev — every architect boots with the same shared codev/roles/architect.md, and specialisation comes from the first user message after afx workspace add-architect (the brief that sets identity, scope, operating mode, output header conventions, etc.). That brief lives in the conversation, not on disk.
When a sibling loses its conversation, it loses the brief. It doesn't come back as "reviewer without recent context" — it comes back as a generic architect that doesn't know its lane. Main has the same role-doc preloaded plus an incremental conversation, so a main-only context loss is annoying; a sibling context loss erases the specialisation itself. The user has to re-send the brief by hand to restore identity. This is the failure mode worth fixing — not just "siblings lose context" but "siblings lose their job description."
A second, silent path: shellper auto-restart
The body above focuses on the cold-spawn paths (launchInstance after reboot, addArchitect during workspace start). There is a second path that loses sibling context without any Tower restart or reboot: when claude crashes inside a still-alive shellper, the shellper auto-restarts it using restart options pre-baked at tower-terminals.ts:635-680. Those restart options go through buildArchitectArgs — role injection, no --resume.
So a long-running workspace can silently lose sibling conversation on any internal claude crash, well before the user notices anything is wrong. The fix proposed below needs to extend to this site, not only the cold-spawn surfaces.
The current conservative guard in launchInstance that this lands removes
Today the multi-architect risk is mitigated by skipping main's resume too when siblings are persisted, via this check in launchInstance (tower-instances.ts:441-558):
// Single architect (main only, or empty before first spawn) → safe.// Multiple → unsafe (sibling jsonls collide with main's in the same cwd).safeToResume=getArchitects(resolvedPath).length<=1;
When the count is > 1, main skips findLatestSessionId and spawns fresh — because picking the wrong jsonl is worse than losing context. The warning logged at this site explicitly names #832. So this fix is also a regression-fix for main's resume in any multi-architect workspace, not just a feature for siblings.
Why jsonl-discovery alone can't solve this
The jsonl filename is the Claude session UUID. The jsonl directory is encoded from cwd. There is no per-architect metadata in either the filename or the directory path. Peeking inside a jsonl to read its content for an architect-name marker is brittle (undocumented internal format, breaks on Claude version bumps).
The only robust fix is to persist a per-architect session identifier somewhere Tower controls.
Proposed approach
Add claude_session_id TEXT to the existing architect SQLite table (already created by Spec 755 for sibling persistence; Spec 786 Phase 3 extended it to main).
Precedent: the table already carries cross-restart anchors
The architect table is already in the right shape for this. Spec 786 Phase 2 preserves architect identity across shellper auto-restart via the existing role_id column:
// tower-terminals.ts:651 — preserves CODEV_ARCHITECT_NAME across shellper restartcleanEnv['CODEV_ARCHITECT_NAME']=dbSession.role_id||'main';
That's the established pattern: store a per-architect anchor in state.db, read it on every spawn / restart path. The proposed claude_session_id is a sibling field that slots into the exact same machinery — Tower already knows how to read a per-architect field from state.db at the moment it's building command args; this work adds a second field to read at the same site, plus a --resume branch on the args-building side. No new infrastructure, one column added to a table that's already extended for this purpose.
Spawn / revive sites
At spawn time (in both launchInstance for main and addArchitect for siblings):
Generate crypto.randomUUID().
Pass --session-id <uuid> to claude in cmdArgs.
After successful PTY creation, store the UUID in the architect row via setArchitect({ name, cmd, startedAt, terminalId, claudeSessionId }).
At revive time (launchInstance re-firing after reboot, or sibling restoration loop calling addArchitect):
Look up the stored UUID via the architect-by-name getter.
If present: pass --resume <uuid> to claude (skip role injection — the saved conversation already contains the role/system prompt).
If absent (legacy row): current behavior — fresh session with role injection.
Fallback chain for consistency with builders:
For architects, lookup priority becomes: stored UUID → (skip jsonl-discovery — ambiguous for shared cwd) → fresh spawn.
For builders, the chain stays: jsonl-discovery → fresh spawn. The two mechanisms coexist because the constraint differs (unique cwd for builders, shared cwd for architects). Documented in code.
Shellper auto-restart site
The shellper-restart options-baking site at tower-terminals.ts:635-680 builds restartOptions for the architect type. Today it calls buildArchitectArgs unconditionally (role injection, no --resume). It needs the same UUID-lookup branch:
Read claude_session_id from the architect row (by role_id / name).
If present: build args with --resume <uuid>, skip role injection.
If absent (legacy row, or this is the first spawn): current behaviour — buildArchitectArgs with role injection.
This site runs every time a shellper auto-restarts claude (claude crash, OOM kill, etc.). Without this branch, the silent-context-loss path from the Problem section stays open even after the cold-spawn paths are fixed. Same lookup logic as the cold-spawn sites; just lives in reconcileTerminalSessions' Phase 1 path rather than launchInstance / addArchitect.
Once this lands, the conservative safeToResume = getArchitects().length <= 1 guard in launchInstance becomes dead code and can be removed in the same change.
Backwards compatibility
Architect rows from before this lands have no claude_session_id. On their first revival they fall back to fresh spawn (no regression vs current behavior). After that first revival their row gets a stored UUID and subsequent revivals resume cleanly.
Acceptance criteria
After reboot of a workspace with main + N named siblings, running afx workspace start revives every architect and each lands in its own prior conversation (no cross-attachment).
After a claude crash inside a still-alive shellper, the shellper auto-restart picks up the same conversation (the silent-context-loss path closes).
Adding a new named architect via afx workspace add-architect --name foo stores the new UUID at spawn time.
Removing a named architect (workspace remove-architect) clears its UUID alongside the architect row.
Legacy architect rows (no stored UUID) gracefully fall back to fresh spawn.
The conservative getArchitects().length <= 1 guard in launchInstance is removed; main resumes its conversation in multi-architect workspaces just as it does in single-architect ones.
Specialised siblings (reviewer, demos, etc.) keep their brief — the first-message specialisation that defines their lane — across all revival surfaces.
Tests cover: spawn-stores-UUID, revive-reads-UUID (both cold-spawn and shellper-restart sites), legacy-fallback, removal-clears-UUID, two siblings revive independently to correct conversations, main resume in a multi-architect workspace.
Workspace-level recovery command for architects (the existing afx workspace start flow handles re-spawn; this issue only handles the conversation-context restoration during that re-spawn).
Spec 755 — multi-architect support (named siblings; persistence).
Spec 786 Phase 3 — main + sibling architect persistence across stop+start.
packages/codev/src/agent-farm/servers/tower-instances.ts — launchInstance (main, with the current safeToResume guard) and addArchitect (siblings) cold-spawn paths.
packages/codev/src/agent-farm/servers/tower-terminals.ts:635-680 — the shellper restart-options-baking site that needs the same UUID-lookup branch as the cold-spawn sites (covers the silent-context-loss path for in-process claude crashes).
packages/codev/src/agent-farm/utils/claude-session-discovery.ts — the jsonl-discovery helper this design intentionally bypasses for siblings.
Spec 786 Phase 2 — established the architect.role_id precedent for "store a per-architect anchor in state.db, read it on every restart path." The proposed claude_session_id reuses that pattern.
Context
Follow-up to #830 (architect session revival). #829 shipped builder conversation resume via on-disk jsonl discovery. That same mechanism extends cleanly to the main architect (committed on the #829 branch) because main is the only architect using Tower's automatic launchInstance path. It does not extend to named sibling architects added via
afx workspace add-architect(Spec 755), because they sharecwd = workspacePathwith each other and with main — and the jsonl-discovery heuristic (newest*.jsonlby mtime) cannot tell which jsonl belongs to which named architect.This issue scopes the multi-architect disambiguation work that #830 left open.
Problem
A workspace with main + named siblings (e.g.,
architect-2,reviewer-bob) has multiple Tower-managed Claude PTYs running in the same cwd. Each writes its session jsonl to~/.claude/projects/<encoded-workspacePath>/. After a reboot:afx workspace start, Tower'slaunchInstancere-spawns main. Spec 786 Phase 3 then iteratesstate.db.architectto re-add each persisted sibling viaaddArchitect().addArchitect— deliberate) or, if naively enabled, attach to whichever architect's jsonl was most recent — possibly the wrong one.So named siblings come back as fresh sessions, losing all conversation context. Operators using multi-architect workspaces lose more on reboot than single-architect users do.
Why the named-architect case is sharper than main's
For specialised siblings (
reviewer,demos,casa, etc.), there is no native per-architect role-doc loading in Codev — every architect boots with the same sharedcodev/roles/architect.md, and specialisation comes from the first user message afterafx workspace add-architect(the brief that sets identity, scope, operating mode, output header conventions, etc.). That brief lives in the conversation, not on disk.When a sibling loses its conversation, it loses the brief. It doesn't come back as "reviewer without recent context" — it comes back as a generic architect that doesn't know its lane. Main has the same role-doc preloaded plus an incremental conversation, so a main-only context loss is annoying; a sibling context loss erases the specialisation itself. The user has to re-send the brief by hand to restore identity. This is the failure mode worth fixing — not just "siblings lose context" but "siblings lose their job description."
A second, silent path: shellper auto-restart
The body above focuses on the cold-spawn paths (
launchInstanceafter reboot,addArchitectduring workspace start). There is a second path that loses sibling context without any Tower restart or reboot: when claude crashes inside a still-alive shellper, the shellper auto-restarts it using restart options pre-baked attower-terminals.ts:635-680. Those restart options go throughbuildArchitectArgs— role injection, no--resume.So a long-running workspace can silently lose sibling conversation on any internal claude crash, well before the user notices anything is wrong. The fix proposed below needs to extend to this site, not only the cold-spawn surfaces.
The current conservative guard in
launchInstancethat this lands removesToday the multi-architect risk is mitigated by skipping main's resume too when siblings are persisted, via this check in
launchInstance(tower-instances.ts:441-558):When the count is
> 1, main skipsfindLatestSessionIdand spawns fresh — because picking the wrong jsonl is worse than losing context. The warning logged at this site explicitly names #832. So this fix is also a regression-fix for main's resume in any multi-architect workspace, not just a feature for siblings.Why jsonl-discovery alone can't solve this
The jsonl filename is the Claude session UUID. The jsonl directory is encoded from cwd. There is no per-architect metadata in either the filename or the directory path. Peeking inside a jsonl to read its content for an architect-name marker is brittle (undocumented internal format, breaks on Claude version bumps).
The only robust fix is to persist a per-architect session identifier somewhere Tower controls.
Proposed approach
Add
claude_session_id TEXTto the existingarchitectSQLite table (already created by Spec 755 for sibling persistence; Spec 786 Phase 3 extended it to main).Precedent: the table already carries cross-restart anchors
The
architecttable is already in the right shape for this. Spec 786 Phase 2 preserves architect identity across shellper auto-restart via the existingrole_idcolumn:That's the established pattern: store a per-architect anchor in state.db, read it on every spawn / restart path. The proposed
claude_session_idis a sibling field that slots into the exact same machinery — Tower already knows how to read a per-architect field from state.db at the moment it's building command args; this work adds a second field to read at the same site, plus a--resumebranch on the args-building side. No new infrastructure, one column added to a table that's already extended for this purpose.Spawn / revive sites
At spawn time (in both
launchInstancefor main andaddArchitectfor siblings):crypto.randomUUID().--session-id <uuid>to claude incmdArgs.setArchitect({ name, cmd, startedAt, terminalId, claudeSessionId }).At revive time (
launchInstancere-firing after reboot, or sibling restoration loop callingaddArchitect):--resume <uuid>to claude (skip role injection — the saved conversation already contains the role/system prompt).Fallback chain for consistency with builders:
For architects, lookup priority becomes: stored UUID → (skip jsonl-discovery — ambiguous for shared cwd) → fresh spawn.
For builders, the chain stays: jsonl-discovery → fresh spawn. The two mechanisms coexist because the constraint differs (unique cwd for builders, shared cwd for architects). Documented in code.
Shellper auto-restart site
The shellper-restart options-baking site at
tower-terminals.ts:635-680buildsrestartOptionsfor the architect type. Today it callsbuildArchitectArgsunconditionally (role injection, no--resume). It needs the same UUID-lookup branch:claude_session_idfrom the architect row (byrole_id/ name).--resume <uuid>, skip role injection.buildArchitectArgswith role injection.This site runs every time a shellper auto-restarts claude (claude crash, OOM kill, etc.). Without this branch, the silent-context-loss path from the Problem section stays open even after the cold-spawn paths are fixed. Same lookup logic as the cold-spawn sites; just lives in
reconcileTerminalSessions' Phase 1 path rather thanlaunchInstance/addArchitect.Once this lands, the conservative
safeToResume = getArchitects().length <= 1guard inlaunchInstancebecomes dead code and can be removed in the same change.Backwards compatibility
Architect rows from before this lands have no
claude_session_id. On their first revival they fall back to fresh spawn (no regression vs current behavior). After that first revival their row gets a stored UUID and subsequent revivals resume cleanly.Acceptance criteria
afx workspace startrevives every architect and each lands in its own prior conversation (no cross-attachment).afx workspace add-architect --name foostores the new UUID at spawn time.workspace remove-architect) clears its UUID alongside the architect row.getArchitects().length <= 1guard inlaunchInstanceis removed; main resumes its conversation in multi-architect workspaces just as it does in single-architect ones.reviewer,demos, etc.) keep their brief — the first-message specialisation that defines their lane — across all revival surfaces.Out of scope
afx workspace startflow handles re-spawn; this issue only handles the conversation-context restoration during that re-spawn).References
afx workspace recover(builder process revival + conversation resume).packages/codev/src/agent-farm/servers/tower-instances.ts—launchInstance(main, with the currentsafeToResumeguard) andaddArchitect(siblings) cold-spawn paths.packages/codev/src/agent-farm/servers/tower-terminals.ts:635-680— the shellper restart-options-baking site that needs the same UUID-lookup branch as the cold-spawn sites (covers the silent-context-loss path for in-process claude crashes).packages/codev/src/agent-farm/utils/claude-session-discovery.ts— the jsonl-discovery helper this design intentionally bypasses for siblings.architect.role_idprecedent for "store a per-architect anchor in state.db, read it on every restart path." The proposedclaude_session_idreuses that pattern.