# 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` - [x] 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.* - [x] 0.2 `AgentBackend` / `AgentSessionHandle` interface + normalized `AgentEvent` union — `apps/coder/src/services/agent-backend.ts`. - [x] 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) - [x] 1.1 `@opencode-ai/sdk` added to `apps/coder/package.json`. - [x] 1.2 `backends/opencode-server.ts`: spawn `opencode serve`, allocated port, wait for ready line. *`OPENCODE_SERVER_PASSWORD` deferred — loopback-unsecured.* - [x] 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.*`.* - [x] 1.4 Paseo `streamedPartKeys` reasoning dedup (delta vs final part). - [x] 1.5 `ensureSession` reuse/resume. *Re-keyed `(chat_id, agent)` in P1.5-b.* - [x] 1.6 `prompt` via SDK with worktree `directory` + `model`. - [x] 1.7 Dispatcher routes `agent==='opencode'` to the pool backend; broker frames + `persistExternalAgentTurn` identical. - [x] 1.8 Persistent worktree, chat-keyed, base commit captured, reused across turns/agents. *Now the first-class `worktrees` table (P1.5-b).* - [x] 1.9 Per-session concurrency: `Map`; `poll()` skips in-flight sessions. - [x] 1.10 Per-turn diff supersedes prior `pending_changes` row (latest-wins). - [x] **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 - [x] 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`). - [x] 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.1–T.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). - [x] D.3 `CHANGELOG.md` entries 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) - [x] B.1 `pnpm -C apps/server build && pnpm -C apps/coder build` clean. - [x] B.2 `pnpm -C apps/server test` green. *(v2.6-specific T.1–T.3 units still unwritten.)* - [x] B.3 Deployed (`sudo systemctl restart boocoder`; `curl :9502/api/health`). ----- ## Fix-next (before Phase 2) — ✅ SHIPPED `v2.6.7-interrupt-guard` - [x] 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`. ## Remaining — recommended order (implementation plan, 2026-05-31) 1. ~~**F.1 interrupt-bug fix**~~ — ✅ shipped `v2.6.7-interrupt-guard` (3 regression tests, TDD). 2. **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. 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.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.