# v2.6 Persistent agent sessions (warm processes + OpenCode server) **Status:** Planned **Depends on:** v2.2 Paseo providers (ACP dispatch), v2.3 provider lifecycle (registry/snapshot) **Reference fork:** `/opt/forks/paseo`, `/opt/forks/opencode` **Pairs with:** the v2.5.x MessageBubble "Thinking" render fix — reasoning already flows; this batch is about persistence, not capability. ## Why BooCode dispatches external agents (opencode, goose, qwen) **one-shot per task**: per task the dispatcher cuts a fresh worktree (`createWorktree(projectPath, taskId)`), spawns `opencode acp` / `goose acp` / `qwen --acp`, runs **one** turn, then tears down the process *and* the worktree (`dispatcher.ts:runExternalAgent`). Consequences: - **No session continuity.** A follow-up message in the same chat creates a new task with a new worktree and a new agent process. The agent has no memory of the prior turn beyond what BooCode replays as chat history, and it cannot see the files it edited last turn (fresh worktree every time). - **Cold start every turn.** Each turn pays the process spawn + ACP `initialize` handshake (and, for some agents, model load) before any work happens. - **Diverges from Paseo.** Paseo runs **OpenCode as a long-lived HTTP server** (`opencode serve` + `@opencode-ai/sdk`, SSE `/event` stream) and keeps **goose / qwen as warm stdio-ACP processes** (`SpawnedACPProcess`: one ACP connection, `newSession()` once, many `prompt()`s). BooCode rebuilds the world per turn. This batch makes a BooCode chat map to a **persistent agent backend + a persistent worktree** that live for the whole conversation, so turns are warm and the agent sees its own accumulating edits. Reasoning passthrough is **already solved** (ACP `agent_thought_chunk` → `reasoning_delta` → the new MessageBubble Thinking block); this batch does not touch it beyond porting OpenCode's reasoning-dedup. ## Decisions locked (from design review) - **Worktree model:** *Persistent worktree per session.* A chat owns one worktree for the whole conversation; each turn the agent sees prior edits; pending_changes accumulate; worktree is cleaned on session close, not per turn. - **Agent switching:** *Free switch, per-agent memory.* The picker stays per-turn (not locked to a chat). The worktree is shared across agents; each agent keeps its own backend session, resumed when you switch back to it. Native boocode reconstructs from chat history (so it sees every agent's turns); a resumed agent does not auto- ingest the gap turns. Data model: one shared worktree per chat + one backend session per `(chat, agent)` pair. Caveat: unapplied edits don't cross the worktree↔project boundary between external agents and native boocode (a v2.5 review-model consequence). - **Transport per agent (matches Paseo exactly):** - **OpenCode** → one shared `opencode serve` HTTP server, driven via `@opencode-ai/sdk`; one opencode *session* per BooCode chat (multi-session, directory-routed via `x-opencode-directory`). - **Goose / Qwen** → warm **stdio** ACP process per live session. Their HTTP "server" modes are just ACP-over-HTTP wrappers (goose: undocumented/internal; qwen `serve`: an HTTP bridge around a single `qwen --acp` child) — no gain over stdio, so we keep stdio ACP like Paseo does. ## Scope ### In scope 1. **Agent process pool** (`apps/coder/src/services/agent-pool.ts`) — owns long-lived backends, lazy spawn, idle eviction, crash restart, shutdown drain. 2. **OpenCode server backend** — spawn `opencode serve`, hold SDK client + single SSE subscription demuxed by opencode `sessionID` → BooCode session; port + `OPENCODE_SERVER_PASSWORD` managed at boot. 3. **Warm ACP backend** — persistent `SpawnedACPProcess`-style connection for goose/qwen reused across turns (one `newSession()`, many prompts). 4. **Persistent worktree lifecycle** — worktree created on first turn of a session, reused, diffed incrementally into `pending_changes`, cleaned on session close. 5. **Session ↔ backend ↔ worktree mapping** — new `agent_sessions` table. 6. **Per-session concurrency** — replace the dispatcher's global single-flight `running` guard with per-session serialization (different sessions run concurrently; one turn at a time within a session). 7. **OpenCode reasoning dedup** — port Paseo's `streamedPartKeys` partID dedup so reasoning isn't double-emitted (delta + final part). 8. **Switch-aware UI** (design §9) — per-change agent attribution in the DiffPanel (`pending_changes.agent` column + badges), a resumed/new-session chip on the AgentComposerBar (chat-scoped `agent-sessions` endpoint), and a staging-boundary hint so the worktree↔project gap is legible. 9. **Tests + smoke** — pool lifecycle unit tests; multi-turn opencode smoke; switch round-trip smoke; attribution/indicator smoke. ### Out of scope (this batch) - Claude PTY→structured transport (separate deferred work — claude stays PTY here). - Goose/qwen HTTP server modes (intentionally not used). - Frontend redesign — existing CoderPane multi-turn chat UI already supports follow-ups; only backend continuity changes. - Replacing `acp-dispatch.ts` wholesale — warm backend reuses its event handlers. - Cross-host agent servers (opencode server stays local to the BooCoder host). ## Non-goals - Multi-user session sharing (single-user homelab). - Multiple concurrent turns within one agent session (the agent holds conversational state; turns within a session are serialized). ## Success criteria - Send two messages in one external-agent chat → second turn reuses the same agent session **and** the same worktree (verified: no second `createWorktree`, agent references files it edited in turn 1). - Warm-start latency for turn 2 materially below turn 1 (no spawn/handshake). - opencode reasoning shows once per thought (no dupes) in the Thinking block. - Killing the opencode server mid-session → pool restarts it and the next turn recovers (opencode persists sessions on disk). - Switch opencode → boocode → opencode in one chat → opencode resumes its *same* session (its memory intact), boocode saw opencode's turns as history, and all three shared the one worktree. No agent is locked to the chat. - Closing/archiving a session removes its worktree; BooCoder restart drains cleanly. - Existing one-shot paths (arena, `new_task` tool, MCP create-task) still work. ## Deliverables | Doc | Purpose | |-----|---------| | [`design.md`](./design.md) | Architecture, backends, data model, worktree/diff strategy, lifecycle, risks | | [`tasks.md`](./tasks.md) | Phased implementation checklist |