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>
115 lines
6.6 KiB
Markdown
115 lines
6.6 KiB
Markdown
# 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 |
|