Files
boocode/openspec/changes/v2-6-persistent-agent-sessions/tasks.md
indifferentketchup 986c8a83a9 docs(changelog): v2.6.10-lifecycle-hardening (completes v2.6)
CHANGELOG + roadmap (through v2.6.10; v2.6 marked complete) + openspec v2-6 Phase 3 checked off (3.1-3.6; 3.7 frontend + apps/server caller as follow-ups).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 01:10:16 +00:00

10 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) — SHIPPED v2.6.8-agent-attribution (Smoke U pending live frontend deploy)

  • U.1 Stamp pending_changes.agent at queue time — native tools default 'boocode', dispatched external → task.agent, manual RightRail → NULL (pending_changes.ts, dispatcher.ts).
  • U.2 agent flows through listPending + backend & frontend PendingChange types.
  • 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-sessions route + api.coder.agentSessions + useAgentSessions hook (refetch on message-complete) (§9b).
  • U.5 AgentComposerBar optional sessionId prop → resumed/history/new-session chip; hidden on fresh chats + other callers (§9b).
  • U.6 Consume opencode session.next.step.ended → accumulate input_tokens/output_tokens/cost on agent_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 --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 WarmAcpBackend — persistent spawn + ClientSideConnection; initialize + session/new once per (chat,agent). handleSessionUpdate extracted to a shared pure acp-event-map.ts (one-shot path byte-identical).
  • 2.2 prompt: session/prompt on the warm connection per turn; abort = session/cancel the prompt only (never kills the child).
  • 2.3 Child supervision: pool-owned lifetime; exit marks agent_sessions.status='crashed' → re-spawn next turn.
  • 2.4 Dispatcher routes goose/qwen chat-tab tasks to the warm backend via pure shouldUseWarmBackend(task) (needs session_id+chat_id); one-shot runExternalAgent fallback kept for arena/MCP/new_task. (SDK note resolved: installed @agentclientprotocol/sdk@^0.22.1 has stable resumeSession/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 — SHIPPED v2.6.10-lifecycle-hardening (3.13.6; 3.7 frontend + apps/server close-hook caller are follow-ups)

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. Pure lifecycle-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) → closeChat evicts backends + archives the worktrees row + removes the worktree. (apps/server caller is a follow-up; idle-evict + reaper backstop it.)
  • 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_commit after a successful apply_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) — frontend follow-up (apps/web; deferred — Sam has uncommitted web work).

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) shipped v2.6.8-agent-attribution (3 parallel agents, disjoint files; 9 new tests). Smoke U pending the frontend Docker rebuild.
  3. Phase 2 — warm ACP, qwen first then goose shipped v2.6.9-warm-acp (15 new tests; one-shot path preserved). Smoke 2 + 2b pending live exercise post-deploy.
  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.