CHANGELOG + roadmap (through v2.6.11) + openspec v2-6 Phase 3 fully closed (3.7 + apps/server close-hook caller done). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
10 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) — ✅ SHIPPED v2.6.8-agent-attribution (Smoke U pending live frontend deploy)
- U.1 Stamp
pending_changes.agentat queue time — native tools default'boocode', dispatched external →task.agent, manual RightRail →NULL(pending_changes.ts,dispatcher.ts). - U.2
agentflows throughlistPending+ backend & frontendPendingChangetypes. - U.3 Shared
components/coder/providerIcons.tsx; DiffPanel per-row agent badge + "Changes from X, Y" multi-agent note (§9a). - U.4
GET /api/sessions/:id/agent-sessionsroute +api.coder.agentSessions+useAgentSessionshook (refetch on message-complete) (§9b). - U.5
AgentComposerBaroptionalsessionIdprop → resumed/history/new-session chip; hidden on fresh chats + other callers (§9b). - U.6 Consume opencode
session.next.step.ended→ accumulateinput_tokens/output_tokens/costonagent_sessions(new cols). Backend persist only; UI surfacing deferred. - 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. (pending live frontend deploy — Docker container rebuild)
Phase 2 — Warm ACP backend (goose, qwen) — ✅ SHIPPED v2.6.9-warm-acp (Smoke 2/2b pending live)
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.tsWarmAcpBackend— persistent spawn +ClientSideConnection;initialize+session/newonce per(chat,agent).handleSessionUpdateextracted to a shared pureacp-event-map.ts(one-shot path byte-identical). - 2.2
prompt:session/prompton the warm connection per turn; abort =session/cancelthe prompt only (never kills the child). - 2.3 Child supervision: pool-owned lifetime;
exitmarksagent_sessions.status='crashed'→ re-spawn next turn. - 2.4 Dispatcher routes
goose/qwenchat-tab tasks to the warm backend via pureshouldUseWarmBackend(task)(needssession_id+chat_id); one-shotrunExternalAgentfallback kept for arena/MCP/new_task. (SDK note resolved: installed@agentclientprotocol/sdk@^0.22.1has stableresumeSession/loadSession; resume moot in the warm hot path, deferred to Phase 3.) - 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 — ✅ COMPLETE (v2.6.10 3.1–3.6; v2.6.11 closed 3.7 + the apps/server close-hook caller)
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 per
(chat, agent)(AGENT_POOL_IDLE_TTL_MS=30min) + LRU cap (AGENT_POOL_MAX_LIVE=10), busy never evicted; reattach next turn. Purelifecycle-decisions.ts(TDD). - 3.2 Crash recovery: openchamber health-monitor + busy-aware-restart + stale-grace state machine in
opencode-server.ts(+ port reclaim) +warm-acp.ts. opencode → fresh sessions; ACP → re-session/new. F.1 guard + U.6 usage preserved. - 3.3 Close hooks (
/api/chats/:id/close,/api/sessions/:id/close) →closeChatevicts backends + archives theworktreesrow + removes the worktree. apps/server caller wired inv2.6.11(coder-notify.ts, fire-and-forget on session-delete + chat archive/delete). - 3.4 Orphan worktree reaper (periodic, 1h grace, superset-style dirty/unpushed preflight, Paseo soft-delete) + LRU cap on the pool.
- 3.5 Re-baseline
worktrees.base_commitafter a successfulapply_pending(both apply routes). - 3.6 Reconnect integration test (DB-opt-in): restart mid-session → next turn reattaches/recreates from
agent_sessions/worktrees. - 3.7 Staging-boundary hint in DiffPanel (§9c) —
v2.6.11: muted one-liner when the selected provider can't see another agent's unapplied worktree edits (derived from per-changeagent+ 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)— ✅ shippedv2.6.8-agent-attribution(3 parallel agents, disjoint files; 9 new tests). Smoke U pending the frontend Docker rebuild.Phase 2 — warm ACP, qwen first then goose— ✅ shippedv2.6.9-warm-acp(15 new tests; one-shot path preserved). Smoke 2 + 2b pending live exercise post-deploy.- 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.