Files
boocode/openspec/changes/v2-6-persistent-agent-sessions/tasks.md
indifferentketchup 457010391a docs(changelog): v2.6.7-interrupt-guard + reconcile roadmap/review/openspec
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>
2026-05-31 21:31:47 +00:00

9.7 KiB
Raw Blame History

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.agent column. Later re-keyed to (chat_id, agent) + worktrees table in P1.5-b.
  • 0.2 AgentBackend / AgentSessionHandle interface + normalized AgentEvent union — apps/coder/src/services/agent-backend.ts.
  • 0.3 agent-pool.ts scaffolded (lazy get-or-create, health, dispose(), onClose hook).

Phase 1 — OpenCode server backend (multi-turn, warm) — SHIPPED v2.6.1-phase1-opencode (Smoke 1 verified)

  • 1.1 @opencode-ai/sdk added to apps/coder/package.json.
  • 1.2 backends/opencode-server.ts: spawn opencode serve, allocated port, wait for ready line. OPENCODE_SERVER_PASSWORD deferred — loopback-unsecured.
  • 1.3 SSE read loop + demux + text/reasoning/tool mapping. Superseded by per-session SSE (P1.5-a); events are session.next.*, not message.part.*.
  • 1.4 Paseo streamedPartKeys reasoning dedup (delta vs final part).
  • 1.5 ensureSession reuse/resume. Re-keyed (chat_id, agent) in P1.5-b.
  • 1.6 prompt via SDK with worktree directory + model.
  • 1.7 Dispatcher routes agent==='opencode' to the pool backend; broker frames + persistExternalAgentTurn identical.
  • 1.8 Persistent worktree, chat-keyed, base commit captured, reused across turns/agents. Now the first-class worktrees table (P1.5-b).
  • 1.9 Per-session concurrency: Map<sessionId,Promise>; poll() skips in-flight sessions.
  • 1.10 Per-turn diff supersedes prior pending_changes row (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): one event.subscribe({directory}) per live opencode session, each with an AbortController; sessionID demux 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-class worktrees table (v2.6.3-chatkey-and-skills); tasks.chat_id threaded; runOpenCodeServerTask resolve-or-creates a chat for session-less creators; cross-chunk dcp-strip. FK convergence to SET 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.agent at queue time (worktree path → task agent; native write tools → 'boocode'; manual RightRail create → NULL).
  • U.2 Add agent to listPending response + frontend PendingChange type.
  • 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-sessions route + api.coder.agentSessions + useAgentSessions(sessionId) (refetch on message_complete) (§9b).
  • U.5 AgentComposerBar optional sessionId prop → 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 --acp is a validated reference (real stdio multi-session, loadSession/resume) — wire qwen into the existing acp-dispatch.ts stack. goose ACP has no loadSession/resume → cross-restart resume needs a different design (re-session/new + accept memory loss, or replay). Cross-check qwen @agentclientprotocol/sdk@^0.14 vs BooCode ^0.22 before relying on unstable_resumeSession. Do qwen first to de-risk.

  • 2.1 backends/warm-acp.ts: persistent spawn + ClientSideConnection; initialize + session/new once; reuse acp-dispatch.ts handleSessionUpdate.
  • 2.2 prompt: session/prompt on 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/qwen to 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 from agent_sessions.
  • 3.2 Crash recovery: opencode server restart recreates sessions; ACP re-session/new.
  • 3.3 Chat close/archive hook → closeSession for every (chat, agent) + remove the chat's worktrees row + worktree (NOT session_worktrees — superseded P1.5-b); mark agent rows status='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.1T.3 exist yet)

  • T.1 agent-pool unit: 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.md BooCoder-dispatch section done (v2.6.1 / v2.6.4 doc-syncs); BOOCODER.md health/contract still pending (no v2.6 warm-server mentions).
  • [~] D.2 @opencode-ai/sdk dep noted; OPENCODE_SERVER_PASSWORD env n/a (deferred — loopback-unsecured).
  • D.3 CHANGELOG.md entries per tag (v2.6.0v2.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 build clean.
  • B.2 pnpm -C apps/server test green. (v2.6-specific T.1T.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.error for 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/consumeTerminal over swallowNextTerminal) wired into opencode-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.
  1. F.1 interrupt-bug fix shipped v2.6.7-interrupt-guard (3 regression tests, TDD).
  2. Phase 1-UX (U.1U.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.
  3. Phase 2 — warm ACP, qwen first then goose — qwen has a validated --acp reference; goose's missing resume is the open design question, so qwen de-risks the pattern. Smoke 2 + 2b (the switch round-trip success criterion).
  4. 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).
  5. Tests T.1T.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.