CHANGELOG entry for v2.6.7. Plus the session's doc reconciliation: roadmap shipped record synced through v2.6.7 (v2.3 lifecycle marked shipped, relicense AGPL->MIT batch, fork-sweep lift items, claude-agent-sdk SessionStore, ACP package fix); boocode_code_review_v2 (two fork sweeps, relicense decision = 3 AGPL files, jinja gate green); openspec v2-3 reconciled to shipped (v2.5.4-v2.5.13); openspec v2-6 Phase 0/1 + P1.5 shipped, F.1 done, remaining-phase plan + lift sources. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
9.7 KiB
v2.6 Tasks — Persistent agent sessions
Phased so each phase is independently shippable and smoke-testable. Phase 1 (OpenCode server) delivers the most value on the cleanest API; goose/qwen warm ACP follows; hardening last.
Phase 0 — Foundations (no behavior change) — ✅ SHIPPED v2.6.0-phase0-foundations
- 0.1 Tables added to
apps/coder/src/schema.sql(idempotent) +pending_changes.agentcolumn. Later re-keyed to(chat_id, agent)+worktreestable in P1.5-b. - 0.2
AgentBackend/AgentSessionHandleinterface + normalizedAgentEventunion —apps/coder/src/services/agent-backend.ts. - 0.3
agent-pool.tsscaffolded (lazy get-or-create, health,dispose(),onClosehook).
Phase 1 — OpenCode server backend (multi-turn, warm) — ✅ SHIPPED v2.6.1-phase1-opencode (Smoke 1 verified)
- 1.1
@opencode-ai/sdkadded toapps/coder/package.json. - 1.2
backends/opencode-server.ts: spawnopencode serve, allocated port, wait for ready line.OPENCODE_SERVER_PASSWORDdeferred — loopback-unsecured. - 1.3 SSE read loop + demux + text/reasoning/tool mapping. Superseded by per-session SSE (P1.5-a); events are
session.next.*, notmessage.part.*. - 1.4 Paseo
streamedPartKeysreasoning dedup (delta vs final part). - 1.5
ensureSessionreuse/resume. Re-keyed(chat_id, agent)in P1.5-b. - 1.6
promptvia SDK with worktreedirectory+model. - 1.7 Dispatcher routes
agent==='opencode'to the pool backend; broker frames +persistExternalAgentTurnidentical. - 1.8 Persistent worktree, chat-keyed, base commit captured, reused across turns/agents. Now the first-class
worktreestable (P1.5-b). - 1.9 Per-session concurrency:
Map<sessionId,Promise>;poll()skips in-flight sessions. - 1.10 Per-turn diff supersedes prior
pending_changesrow (latest-wins). - Smoke 1 — verified end-to-end (two turns, same session + worktree, turn 2 ~9× faster, reasoning once).
Phase 1.5 — concurrency + chat-keying follow-ups (added during impl, not in original plan) — ✅ SHIPPED
- P1.5-a Per-session SSE (
v2.6.2-delete-guard-and-sse): oneevent.subscribe({directory})per live opencode session, each with anAbortController;sessionIDdemux guard + zombie-loop fix — replaces task 1.3's single global loop. Bundled: session-delete work-loss guard (/worktree-risk). - P1.5-b Re-key
agent_sessions→(chat_id, agent)+ first-classworktreestable (v2.6.3-chatkey-and-skills);tasks.chat_idthreaded;runOpenCodeServerTaskresolve-or-creates a chat for session-less creators; cross-chunk dcp-strip. FK convergence toSET NULL(v2.6.4-agent-sessions-fk).
Phase 1 (UX) — Attribution & switch affordances (design §9) — ⬜ REMAINING (pure read+display over already-shipped pending_changes.agent + agent_sessions)
- U.1 Stamp
pending_changes.agentat queue time (worktree path → task agent; native write tools →'boocode'; manual RightRail create → NULL). - U.2 Add
agenttolistPendingresponse + frontendPendingChangetype. - U.3 Extract
providerIcon()to a shared helper; DiffPanel renders an agent badge per row + a "Changes from X, Y" note when the pending set spans >1 agent (§9a). - U.4
GET /api/sessions/:id/agent-sessionsroute +api.coder.agentSessions+useAgentSessions(sessionId)(refetch onmessage_complete) (§9b). - U.5
AgentComposerBaroptionalsessionIdprop → resumed / history / new-session chip beside the Provider picker; hidden on fresh chats and other callers (§9b). - U.6 Consume opencode
session.next.step.ended{tokens, cost}→ fill ctx/token usage for opencode sessions (SDK already installed; closes the "no usage for external agents" gap; surface beside the §9b chip). Source:boocode_code_review_v2.md§1 #8, design §10. - Smoke U: stage edits with opencode then boocode → DiffPanel badges each row to the right agent; composer shows "resumed" when re-selecting opencode, "new session" for goose.
Phase 2 — Warm ACP backend (goose, qwen) — ⬜ REMAINING
Lift (design §10):
qwen --acpis a validated reference (real stdio multi-session,loadSession/resume) — wire qwen into the existingacp-dispatch.tsstack. goose ACP has noloadSession/resume → cross-restart resume needs a different design (re-session/new+ accept memory loss, or replay). Cross-check qwen@agentclientprotocol/sdk@^0.14vs BooCode^0.22before relying onunstable_resumeSession. Do qwen first to de-risk.
- 2.1
backends/warm-acp.ts: persistent spawn +ClientSideConnection;initialize+session/newonce; reuseacp-dispatch.tshandleSessionUpdate. - 2.2
prompt:session/prompton the warm connection per turn; per-turn abort signal only. - 2.3 Child supervision: detached lifetime, exit handler marks
status='crashed'. - 2.4 Dispatcher routes
goose/qwento warm backend; keep one-shot fallback for arena/MCP (or opt those into pool too — decide in review). - Smoke 2: two messages in a goose chat reuse the same process + ACP session + worktree; reasoning still renders; no per-turn respawn.
- Smoke 2b (switch round-trip): opencode → boocode → opencode in one chat — opencode
resumes the SAME
agent_session_id(memory intact), boocode saw opencode's turns as history, all three shared the one worktree, and no agent was locked to the chat.
Phase 3 — Lifecycle hardening — ⬜ REMAINING
Lift (design §10): hardening from openchamber (MIT, same warm-opencode-server architecture) — health-monitor + crash auto-restart + busy-aware restart + port reclaim (
killProcessOnPort/waitForPortRelease) + stall-SSE = a concrete state machine for 3.1/3.2/3.6. Reaper (3.3/3.4): Paseo worktree-archive cascade + superset destroy-saga (preflight dirty/unpushed inspect) + LRU cap on warm-server Maps. Do crash-recovery + reaper together (shared supervision loop).
- 3.1 Idle TTL eviction keyed per
(chat, agent); reattach-on-next-turn fromagent_sessions. - 3.2 Crash recovery: opencode server restart recreates sessions; ACP re-
session/new. - 3.3 Chat close/archive hook →
closeSessionfor every(chat, agent)+ remove the chat'sworktreesrow + worktree (NOTsession_worktrees— superseded P1.5-b); mark agent rowsstatus='closed'. - 3.4 Orphan worktree reaper (extend periodic sweeper) + max-live-worktrees LRU cap.
- 3.5 Re-baseline worktree diff after
apply_pending. - 3.6 Reconnect test: restart BooCoder mid-session → next turn reattaches/recreates cleanly.
- 3.7 Staging-boundary hint in DiffPanel (§9c): muted one-liner when the selected
provider can't see another agent's unapplied worktree edits (derived from per-change
agent+ current provider; no new state).
Tests — ⬜ REMAINING (none of T.1–T.3 exist yet)
- T.1
agent-poolunit: get-or-create, idle evict, dispose drains in-flight (DB-opt-in pattern). - T.2 opencode SSE demux + reasoning dedup unit (fixture event stream). Fold in an F.1 interrupt-bug regression case.
- T.3 per-session concurrency: two sessions run concurrently, one session serializes.
Docs
- [~] D.1
CLAUDE.mdBooCoder-dispatch section done (v2.6.1 / v2.6.4 doc-syncs);BOOCODER.mdhealth/contract still pending (no v2.6 warm-server mentions). - [~] D.2
@opencode-ai/sdkdep noted;OPENCODE_SERVER_PASSWORDenv n/a (deferred — loopback-unsecured). - D.3
CHANGELOG.mdentries per tag (v2.6.0–v2.6.4) — shipped as 5 tags, not the single planned-persistent-agent-sessions.
Build / deploy gate — ✅ (per shipped tags; re-run per remaining batch)
- B.1
pnpm -C apps/server build && pnpm -C apps/coder buildclean. - B.2
pnpm -C apps/server testgreen. (v2.6-specific T.1–T.3 units still unwritten.) - B.3 Deployed (
sudo systemctl restart boocoder;curl :9502/api/health).
Fix-next (before Phase 2) — ✅ SHIPPED v2.6.7-interrupt-guard
- F.1 Post-interrupt stale-terminal guard. opencode emits one trailing
session.idle/session.errorfor a cancelled turn (sessionID only, no turn id) which settled the next turn early. Fixed with a pure per-session guard (backends/turn-guard.ts:armAbortGuard/noteTurnActivity/consumeTerminaloverswallowNextTerminal) wired intoopencode-server.ts(arm on abort, swallow the orphan terminal, self-heal on next-turn activity). 3 regression tests (turn-guard.test.ts), TDD. Paseo parallel:1d38aac.
Remaining — recommended order (implementation plan, 2026-05-31)
F.1 interrupt-bug fix— ✅ shippedv2.6.7-interrupt-guard(3 regression tests, TDD).- Phase 1-UX (U.1–U.6) — pure read+display over already-shipped
pending_changes.agent+agent_sessions; no dispatch-logic or backend change, so it ships value on data that already exists. U.6 (token/ctx usage) rides the same opencode SSE. - Phase 2 — warm ACP, qwen first then goose — qwen has a validated
--acpreference; goose's missing resume is the open design question, so qwen de-risks the pattern. Smoke 2 + 2b (the switch round-trip success criterion). - Phase 3 — lifecycle hardening — lift openchamber's state machine; do crash-recovery (3.1/3.2/3.6) + worktree reaper (3.3/3.4 + LRU) together (shared supervision loop). Closes the two ⬜ success criteria (server-crash recovery, close→cleanup).
- Tests T.1–T.3 +
BOOCODER.md(D.1 remainder) — backfill alongside each phase, not at the end.
Each phase stays independently shippable + smoke-testable (original phasing holds). Tag monotonically from v2.6.7, one batch per phase.