# 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) - [ ] 0.1 Add `session_worktrees` + `agent_sessions` tables (per `(session_id, agent)`) to `apps/coder/src/schema.sql` (idempotent; see design §3). - [ ] 0.2 Define `AgentBackend` / `AgentSessionHandle` interface + normalized `onEvent` event union (reuse shapes from `acp-dispatch.ts`). - [ ] 0.3 Scaffold `agent-pool.ts` with lazy get-or-create keyed by `(chat, agent)`, health, `dispose()`; wire `app.addHook('onClose')` to dispose alongside dispatcher `stop()`. ## Phase 1 — OpenCode server backend (multi-turn, warm) - [ ] 1.1 Add `@opencode-ai/sdk` to `apps/coder/package.json`; pin to installed opencode major. - [ ] 1.2 `backends/opencode-server.ts`: spawn `opencode serve` once (random `OPENCODE_SERVER_PASSWORD`, allocated port), `createOpencodeClient`, wait for ready line. - [ ] 1.3 Single `/event` SSE read loop; demux by `properties.sessionID`; map `message.part.delta`/`updated` (text + reasoning) + tool parts to `onEvent`. - [ ] 1.4 Port Paseo `streamedPartKeys` reasoning dedup (delta vs final part). - [ ] 1.5 `ensureSession`: reuse the `(chat, opencode)` `agent_sessions` row if present (resume on switch-back), else `client.session.create()` → store `agent_session_id`. - [ ] 1.6 `prompt`: send via SDK with `x-opencode-directory` = session worktree + `model`. - [ ] 1.7 Dispatcher: when `agent==='opencode'`, route to pool backend instead of `dispatchViaAcp`; keep broker frames + `persistExternalAgentTurn` identical. - [ ] 1.8 Persistent worktree: chat-keyed `createWorktree` (shared across agents); capture base commit in `session_worktrees`; reuse across turns and agents. - [ ] 1.9 Per-session concurrency: replace global `running` with `Map`; `poll()` skips sessions with an in-flight turn. - [ ] 1.10 Per-turn diff → supersede prior `pending_changes` row for the session (latest-wins). - [ ] **Smoke 1:** two messages in one opencode chat → same `agent_session_id`, same worktree, no second `createWorktree`; agent references turn-1 edits; reasoning shows once; turn-2 faster. ## Phase 1 (UX) — Attribution & switch affordances (design §9) - [ ] 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). - [ ] **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) - [ ] 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 - [ ] 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 shared `session_worktrees` row + worktree; 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 - [ ] 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). - [ ] T.3 per-session concurrency: two sessions run concurrently, one session serializes. ## Docs - [ ] D.1 Update `CLAUDE.md` (BooCoder dispatch section) + `BOOCODER.md` health/contract. - [ ] D.2 Note opencode `@opencode-ai/sdk` dep + `OPENCODE_SERVER_PASSWORD` env in env docs. - [ ] D.3 `CHANGELOG.md` entry on tag (`v2.6.0-persistent-agent-sessions`). ## Build / deploy gate - [ ] B.1 `pnpm -C apps/server build && pnpm -C apps/coder build` clean. - [ ] B.2 `pnpm -C apps/server test` (+ DB-opt-in) green. - [ ] B.3 Deploy: `sudo systemctl restart boocoder`; `curl :9502/api/health` reports tool count.