Skip to content

Multi-architect conversation resume via persisted per-architect session ID (#832) - #1116

Merged
amrmelsayed merged 53 commits into
mainfrom
builder/pir-832
Jun 29, 2026
Merged

Multi-architect conversation resume via persisted per-architect session ID (#832)#1116
amrmelsayed merged 53 commits into
mainfrom
builder/pir-832

Conversation

@amrmelsayed

Copy link
Copy Markdown
Collaborator

PIR Review: Multi-architect conversation resume via persisted per-architect session ID

Fixes #832

Summary

Tower now revives every architect — main and named siblings (Spec 755) — into its own prior Claude conversation after a reboot, restart, or in-process crash, instead of only main. Each architect row persists an agent-neutral session_id generated at spawn; every revive surface reads it back and resumes via --resume. This removes #830's conservative getArchitects() <= 1 guard that disabled main's resume whenever any sibling existed, and closes the silent-context-loss path where a specialised sibling (reviewer, demos, …) would come back as a generic architect that had lost its first-message brief.

Files Changed

git diff --stat against the merge-base (0065189):

  • codev/plans/832-multi-architect-conversation-r.md (+321 / -0)
  • codev/projects/832-multi-architect-conversation-r/status.yaml (+22 / -0)
  • codev/state/pir-832_thread.md (+294 / -0)
  • packages/codev/src/agent-farm/db/schema.ts (+1 / -0)
  • packages/codev/src/agent-farm/db/index.ts (+20 / -0) — migration v12
  • packages/codev/src/agent-farm/db/types.ts (+2 / -0)
  • packages/codev/src/agent-farm/types.ts (+4 / -0)
  • packages/codev/src/agent-farm/state.ts (+10 / -0)
  • packages/codev/src/agent-farm/utils/harness.ts (+19 / -0) — session capability
  • packages/codev/src/agent-farm/servers/tower-utils.ts (+48 / -0) — resolveArchitectLaunch
  • packages/codev/src/agent-farm/servers/tower-instances.ts (+108 / -? net) — main + sibling spawn/revive
  • packages/codev/src/agent-farm/servers/tower-terminals.ts (+41 / -?) — both restart-bake sites
  • packages/codev/src/agent-farm/__tests__/tower-utils.test.ts (+75 / -0)
  • packages/codev/src/agent-farm/__tests__/state.test.ts (+54 / -0)
  • packages/codev/src/agent-farm/__tests__/harness.test.ts (+17 / -0)
  • packages/codev/src/agent-farm/__tests__/claude-session-discovery.test.ts (+5 / -0)
  • codev/resources/lessons-learned.md (+1 lesson, this commit)

Net: 16 source/test files, +976 / -65. (The plan/thread/status files are process artifacts.)

Commits

git log main..HEAD --oneline (implementation commits; porch chore commits omitted):

Note on the commit arc: earlier commits built a transitional backfill (script + Tower setter route + TowerClient method + live-process lsof/cmdline capture) to bridge pre-#832 running architects. At the dev-approval gate this was found to be unworkable for siblings and unnecessary for main, and fully removed in 274fbdc4. The net diff carries none of it. See "Things to Look At" and the plan's Revision note 2.

Test Results

  • pnpm build (core + codev): ✓ pass
  • pnpm test (from the worktree): ✓ 3389 passed | 48 skipped
  • New tests cover: session_id round-trip + removal-clears-id + two-siblings-distinct (state.test.ts); migration v12 adds the column with legacy rows reading back null; resolveArchitectLaunch resume / fresh / no-session / sibling-isolation (tower-utils.test.ts); CLAUDE_HARNESS.session produces --session-id / --resume while Codex/Gemini omit it (harness.test.ts); findLatestSessionId discovery (claude-session-discovery.test.ts).
  • Manual verification (human, at dev-approval): reviewed the running worktree; the live --all --dry-run backfill run (before that path was removed) empirically confirmed the root-cause finding that drove the final design (Claude holds no jsonl fd open).

Architecture Updates

No arch.md change. Architect-revival mechanics live in code (resolveArchitectLaunch + the spawn/revive sites) and are documented in this review and the plan; the predecessor #830 mechanism was likewise never lifted into arch.md, so adding it now would be per-spec noise. The change reinforces the existing hot-tier invariant "state lives in state.db (single source of truth)" — the architect's resumable identity is now a column on its row — rather than altering any documented invariant. The arch-critical.md hot tier is unchanged (no new always-on system-shape fact; the cap is preserved).

Lessons Learned Updates

Routed one lesson to COLD codev/resources/lessons-learned.md → Architecture ([From #832]): a recovery mechanism that keys off a process holding a resource open (fd/lock) is only as sound as that assumption — verify it empirically before building on it (Claude closes its session jsonl between writes; lsof against a live architect falsified the whole capture subsystem in minutes, and it had passed unit tests only because the tests exercised the fallback). Corollary: don't delete a working self-recovery path to rebuild it as a bridge that can't cover the hard case. Not hot-tier: it's a spec-narrow design lesson, and the hot lessons-critical.md already carries the general "captured raw data beats speculation" rule it specialises. Hot tier unchanged (cap preserved).

Things to Look At During PR Review

  • The design pivot (274fbdc4) is the crux. The branch history builds and then removes an entire transitional backfill layer. Confirm the net diff contains no orphans: no scripts/backfill-architect-sessions.ts, no setArchitectSessionId (state/route/TowerClient), no captureRunningClaudeSession/extractSessionIdFromCmdline. A repo-wide grep for those symbols should be empty outside dist/.
  • The getArchitects() <= 1 check changed meaning, not just location. In Workspace recover: revive Tower-managed architects after machine reboot #830 it gated resume wholesale (the bug: any sibling → main spawns fresh). Now it gates only the legacy jsonl fallback in launchInstance; stored-UUID resume applies regardless of architect count. The issue's acceptance criterion literally says "remove the guard" — we kept a <= 1 check but repurposed it. Worth a careful read of tower-instances.ts launchInstance against the plan's Revision note 2 to confirm the reinterpretation is sound.
  • Self-migration on first revival. resolveArchitectLaunch returns the resolved sessionId, and every caller persists it. A legacy sole-main row therefore resumes via jsonl-discovery and gains a stored id in the same revival. Confirm the persist happens on the resume branch, not only the fresh branch.
  • Siblings never use jsonl-discovery (addArchitect passes storedSessionId ?? null) and the shellper-restart sites have no jsonl fallback (matching Workspace recover: revive Tower-managed architects after machine reboot #830). Both intentionally self-heal once for legacy rows — verify this is deliberate, not an omission.
  • Migration v12 is an additive ALTER TABLE architect ADD COLUMN session_id TEXT in try/catch (idempotent). Old binaries ignore the nullable column.

How to Test Locally

For reviewers pulling the branch:

  • View diff: VSCode sidebar → right-click builder pir-832View Diff
  • Run dev server: VSCode sidebar → Run Dev Server, or afx dev pir-832 (from the main workspace root)
  • What to verify (maps to the plan's Test Plan):
    • Go-forward multi-architect resume: spawn main + a named sibling under this code (ids stored at spawn), afx workspace stop + afx workspace start; each resumes its own conversation and the sibling keeps its brief. Watch Tower logs for Resuming architect '<name>' session <id8>… at each site.
    • Legacy sole-main bridge: a single-main workspace with a pre-Multi-architect conversation resume: disambiguate via per-architect session UUID #832 row (no stored id) resumes via findLatestSessionId, and the row then carries a stored id (self-migration) so the next restart takes the exact-id path.
    • Removal: afx workspace remove-architect <name> then re-add → starts fresh (row deletion clears session_id).

…rchitectLaunch helper + jsonl-existence guard
… main (eliminates upgrade-boundary loss)
…sionId, sessionFileExists, deleteArchitectSessionFile)
…tureRunningClaudeSession; drop derived-id helpers
…captureArchitectSessions; simplify removeArchitect
…t the conversation sessions (which persist on disk); capture-before-deactivation is for lsof on the live process
… script

Remove the --capture-sessions flag, the /capture-sessions REST route, the
captureArchitectSessions client method + Tower function. Replace with
scripts/backfill-architect-sessions.ts (library-only, reads pids from
terminal_sessions, writes via new targeted setArchitectSessionId). Keeps the
transitional backfill out of the day-to-day CLI/API surface.
…nt HarnessProvider interface

The interface now carries only the steady-state pin/resume contract
(newSessionArgs/resumeArgs). The backfill script calls captureRunningClaudeSession
directly (Claude-specific, transitional), gated on the harness.session capability.
Keeps backfill-only concerns out of the core agent abstraction.
Runs the full (read-only) resolution and prints the exact session id each
architect would receive, without writing. Re-run without --dry-run to apply.
…tect for clarity (vs the PtySession terminal id)
… architects in one run

Enumerates targets via SELECT DISTINCT workspace_path FROM terminal_sessions
WHERE type='architect'. Refactors per-workspace logic into backfillWorkspace();
--all and --dry-run compose. Single-workspace path arg still supported.
…Client, script is now a thin client

Add PUT /api/workspaces/:ws/architects/:name/session-id (narrow transitional
setter -> setArchitectSessionId) + TowerClient.setArchitectSessionId. Rewrite
backfill script as a pure Tower client: enumerate via listWorkspaces, read live
architect pids via getWorkspaceStatus, capture via lsof, write through the setter.
No state.db/config imports -> no cwd footgun; runs from anywhere. Avoids putting
session_id on the wire ArchitectState (captures every live architect; idempotent
re-write for already-#832 ones).
…ch resume site

resolveArchitectLaunch now returns resumed:boolean; the four callers (launchInstance,
addArchitect, both restart-bake sites) emit an INFO log when a stored session is
resumed, so end-to-end resume is verifiable from Tower logs.
…correlation

Claude does not hold its session jsonl open, so the lsof process->open-file
correlation in captureRunningClaudeSession never matched — every capture came
from the sole-architect mtime fallback, which can't disambiguate siblings.

Read --session-id/--resume <uuid> straight off the process subtree's command
line instead: exact, and unambiguous for siblings sharing a cwd. Extract a pure
extractSessionIdFromCmdline helper (+8 unit tests). Keep the sole-architect
mtime fallback for pre-#832 fresh spawns; siblings without the arg self-heal on
first #832 revival. Skip message now explains the per-case reason.
… for main

The backfill (script + Tower setter route + TowerClient method + live-process
capture) could only ever rescue main, never siblings: Claude holds no jsonl fd
open, so process->jsonl correlation never worked, and a pre-#832 sibling's id
lives only as a jsonl filename with no robust pid bridge.

But main was already self-recovering under #830 via sole-architect jsonl
discovery -- a path this branch had removed. Restore it instead: launchInstance
falls back to findLatestSessionId when getArchitects()<=1, and resolveArchitectLaunch
persists the resolved id so it self-migrates into the stored-UUID path. The <=1
check now gates only the legacy fallback, not resume itself -- stored-UUID resume
applies regardless of architect count.

Deletes backfill-architect-sessions.ts, state.setArchitectSessionId, the PUT
session-id route, TowerClient.setArchitectSessionId, captureRunningClaudeSession/
extractSessionIdFromCmdline/processSubtree/execCapture, and their tests. Every
architect self-heals in one revival cycle; single-architect main never regresses.

Core + codev build; suite 3389 passed | 48 skipped.
…ANGES)

Codex's review-phase consult flagged two planned-but-missing tests (migration v12,
tower-terminals restart resume) and an overstated coverage claim in the review.

- Add pir-832-migration.test.ts: v12 ALTER adds session_id to a real post-v11 table,
  legacy rows read null, idempotent, duplicate-column swallowed on fresh installs.
- Extract the duplicated restart-bake glue at both tower-terminals.ts sites into
  resolveArchitectRestart (tower-utils.ts) and unit-test it: stored id -> --resume,
  legacy/no-row -> fresh, per-name no cross-attachment.
- Correct the review's Test Results + document the disposition.

Full suite 3397 passed | 48 skipped (+8).
@amrmelsayed

Copy link
Copy Markdown
Collaborator Author

Architect Integration Review

High-risk PR (state.db migration, launchInstance / addArchitect / shellper-restart paths, conversation-resume semantics). Independently verified each of #832's six acceptance criteria against the source on PR head, plus the codex-disposition additions in 333e4e0.

Acceptance criteria — all present

# Criterion Status Evidence
1 Migration v12 + schema column db/index.ts:599-617 adds session_id TEXT to architect; db/schema.ts:38; db/types.ts:17-26; state.ts:121-161 helpers updated; idempotent + handles fresh-install duplicate-column case
2 Spawn-time UUID gen + persist tower-instances.ts:483-493, 929-938 mint crypto.randomUUID() via resolveArchitectLaunch; persist via setArchitect[ByName] with sessionId field (lines 549-554, 612-617, 988-994, 1043-1049)
3 Revive-time UUID lookup + role-skip on resume resolveArchitectLaunch (tower-utils.ts:215-243) — three branches (no session harness → fresh, storedSessionId present → --resume with resumed:true and env:{} skip-role, else → mint fresh with --session-id)
4 Shellper auto-restart site (the silent-loss path) tower-terminals.ts:656-667 and :899-904 both call resolveArchitectRestart (tower-utils.ts:259-267) which composes getArchitectByName + resolveArchitectLaunch. INFO logs identify 'Resuming architect <name> session <id>… on restart/reconnect' so the resume path is observable
5 safeToResume guard removed The literal safeToResume = getArchitects().length <= 1 gate in launchInstance is gone. The same condition survives as a narrowly-scoped fallback for the legacy jsonl-discovery path only (line 486) — only fires when the stored sessionId is absent, which matches the issue's "legacy row gracefully falls back" criterion
6 Agent-neutral harness wiring New HarnessProvider.session? interface (harness.ts:61-66). CLAUDE_HARNESS implements newSessionArgs → --session-id, resumeArgs → --resume (harness.ts:96-99). CODEX_HARNESS / GEMINI_HARNESS omit session entirely → resolveArchitectLaunch early-returns without persisting an id. Persisted column is session_id, not claude_session_id — agent-neutral as the recent memory rule asks for. Sharper than my issue body suggested

Codex-disposition additions in 333e4e0

  • pir-832-migration.test.ts (123 lines, 4 tests): spins up a pre-v12 DB, runs the migration, asserts the column exists, legacy rows read null, migration is idempotent, fresh-install duplicate-column doesn't throw. Faithful exercise of the migration. ✓
  • resolveArchitectRestart tests in tower-utils.test.ts (58 lines, 4 tests): stored-id → --resume with no role injection; no-stored-id → fresh --session-id with role injection; missing-row → fresh fallback; per-name isolation (reviewer gets its own id, casa gets its own — no cross-attachment). The cross-attachment test is the explicit defense against the original problem Multi-architect conversation resume: disambiguate via per-architect session UUID #832 was filed for. ✓
  • state.test.ts session-id round-trip block (+54 lines, 5 tests): set/get for main, set/get for sibling, legacy null, removal-clears, sibling-distinct. Full coverage of the new accessor paths. ✓
  • tower-instances.test.ts -102 lines (verified via git diff --stat main pr-1116): removes the Support codex as an architect #929 regression block (Issue #929 — architect resume gated on harness) that asserted the old codex/gemini-skip-resume behaviour. Once per-architect UUIDs disambiguate (and the safeToResume gate is gone), the harness-based skip becomes the only remaining gate — and that gate moved to resolveArchitectLaunch's if (!harness.session) early-return where it's tested directly. Intentional deletion, not a coverage regression. ✓
  • Review file correction (.builders/pir-832/codev/reviews/832-multi-architect-conversation-r.md): the overstated "3389 passed" claim that omitted the missing migration test was corrected to "3397 passed (+8)" with explicit "(added at review)" tags on each addition, plus a disposition block naming each codex finding as ADDRESSED. Honest correction of the prior overstatement. ✓

Design quality observations (beyond acceptance criteria)

  • Helper extraction (resolveArchitectLaunch + resolveArchitectRestart) is good architecture: single-responsibility, testable in isolation, reused at every spawn/revive site (4 callsites). The shellper-restart and cold-spawn paths now share the same UUID-lookup logic, which is the structural answer to the issue's "the fix must touch both surfaces" framing.
  • Agent-neutral column naming: storing session_id (not claude_session_id) makes the schema agent-neutral and matches the recently-saved feedback_respect_harness_abstraction rule. When agy / codex later add their own session-resume semantics, the column slots in without rename.
  • Observable resume path: the 'Resuming architect <name> session <id>… on restart/reconnect' log lines (Tower's INFO channel) give operators a clear signal that resume actually fired, which directly addresses the "silent loss" framing of Multi-architect conversation resume: disambiguate via per-architect session UUID #832's expanded body.

Risk surface that's now closed

Per the issue's expanded body, the named-architect specialisation-loss problem (a reviewer or demos coming back as a generic architect after a shellper auto-restart because the brief was conversation-only) is closed by the resolveArchitectRestart wiring. A specialised architect now retains its conversation across:

  • Tower-only restart with shellper survival (was already fine; still fine)
  • Claude crash inside live shellper → shellper auto-restart (was the silent-loss path; now fixed)
  • Machine reboot → workspace start → launchInstance + addArchitect cold-spawn (was the cold-loss path; now fixed)

The single-architect workspace's main-resume is also restored (the safeToResume.length <= 1 gate previously turned main-resume OFF whenever any sibling existed; that conditional is gone).

Recommendation

APPROVE. All acceptance criteria present and tested; the codex disposition is genuine (not papered-over); the design quality is high (clean helper extraction, agent-neutral schema, observable resume path); the documented risk surfaces all close. Strong PR.


Architect integration review

# Conflicts:
#	codev/resources/lessons-learned.md
#	packages/codev/src/agent-farm/servers/tower-instances.ts
#	packages/codev/src/agent-farm/utils/harness.ts
@amrmelsayed
amrmelsayed merged commit 4ad1fca into main Jun 29, 2026
6 checks passed
amrmelsayed added a commit that referenced this pull request Jun 29, 2026
…sume + PR #1095/#1114 Other fixes + PR #1109 Polish (post-v3.2.1)
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 conversation resume: disambiguate via per-architect session UUID

1 participant