Working-tree config/doc changes (.gitignore, CLAUDE.md, AGENTS.md removal + data/AGENTS.md, codecontext Dockerfile/shim — pre-existing) plus this session's v2-6 persistent-agent-sessions openspec proposal/design/tasks (planning only; feature unimplemented, reserves the v2.6.0 tag) and the v2.5.2 CHANGELOG entry. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
6.6 KiB
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
initializehandshake (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/eventstream) and keeps goose / qwen as warm stdio-ACP processes (SpawnedACPProcess: one ACP connection,newSession()once, manyprompt()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 serveHTTP server, driven via@opencode-ai/sdk; one opencode session per BooCode chat (multi-session, directory-routed viax-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 singleqwen --acpchild) — no gain over stdio, so we keep stdio ACP like Paseo does.
- OpenCode → one shared
Scope
In scope
- Agent process pool (
apps/coder/src/services/agent-pool.ts) — owns long-lived backends, lazy spawn, idle eviction, crash restart, shutdown drain. - OpenCode server backend — spawn
opencode serve, hold SDK client + single SSE subscription demuxed by opencodesessionID→ BooCode session; port +OPENCODE_SERVER_PASSWORDmanaged at boot. - Warm ACP backend — persistent
SpawnedACPProcess-style connection for goose/qwen reused across turns (onenewSession(), many prompts). - Persistent worktree lifecycle — worktree created on first turn of a session,
reused, diffed incrementally into
pending_changes, cleaned on session close. - Session ↔ backend ↔ worktree mapping — new
agent_sessionstable. - Per-session concurrency — replace the dispatcher's global single-flight
runningguard with per-session serialization (different sessions run concurrently; one turn at a time within a session). - OpenCode reasoning dedup — port Paseo's
streamedPartKeyspartID dedup so reasoning isn't double-emitted (delta + final part). - Switch-aware UI (design §9) — per-change agent attribution in the DiffPanel
(
pending_changes.agentcolumn + badges), a resumed/new-session chip on the AgentComposerBar (chat-scopedagent-sessionsendpoint), and a staging-boundary hint so the worktree↔project gap is legible. - 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.tswholesale — 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_tasktool, MCP create-task) still work.
Deliverables
| Doc | Purpose |
|---|---|
design.md |
Architecture, backends, data model, worktree/diff strategy, lifecycle, risks |
tasks.md |
Phased implementation checklist |