Skip to content

Multi-architect conversation resume: disambiguate via per-architect session UUID #832

Description

@amrmelsayed

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 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:

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 restart
cleanEnv['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):

  1. Generate crypto.randomUUID().
  2. Pass --session-id <uuid> to claude in cmdArgs.
  3. 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):

  1. Look up the stored UUID via the architect-by-name getter.
  2. If present: pass --resume <uuid> to claude (skip role injection — the saved conversation already contains the role/system prompt).
  3. 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:

  1. Read claude_session_id from the architect row (by role_id / name).
  2. If present: build args with --resume <uuid>, skip role injection.
  3. 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.

Out of scope

References

  • afx workspace recover: revive builders after machine reboot/crash #829afx workspace recover (builder process revival + conversation resume).
  • Workspace recover: revive Tower-managed architects after machine reboot #830 — architect session revival follow-up (where main-only resume landed; this multi-architect work was deferred).
  • Builder conversation resume: restore Claude session on recovery via jsonl discovery #831 — builder conversation resume via jsonl discovery.
  • 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.tslaunchInstance (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.

Metadata

Metadata

Assignees

Labels

area/towerArea: Tower server / agent farm CLI

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions