CHANGELOG entry for v2.6.7. Plus the session's doc reconciliation: roadmap shipped record synced through v2.6.7 (v2.3 lifecycle marked shipped, relicense AGPL->MIT batch, fork-sweep lift items, claude-agent-sdk SessionStore, ACP package fix); boocode_code_review_v2 (two fork sweeps, relicense decision = 3 AGPL files, jinja gate green); openspec v2-3 reconciled to shipped (v2.5.4-v2.5.13); openspec v2-6 Phase 0/1 + P1.5 shipped, F.1 done, remaining-phase plan + lift sources. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
123 lines
8.2 KiB
Markdown
123 lines
8.2 KiB
Markdown
# v2.6 Persistent agent sessions (warm processes + OpenCode server)
|
||
|
||
**Status:** Phase 0 + Phase 1 + P1.5-a/b **shipped** (`v2.6.0`–`v2.6.4`); Phase 1-UX, Phase 2, Phase 3, and unit tests **remaining.** (Reconciled 2026-05-31.)
|
||
**Depends on:** v2.2 Paseo providers (ACP dispatch), v2.3 provider lifecycle (registry/snapshot)
|
||
**Reference fork:** `/opt/forks/paseo`, `/opt/forks/opencode`; **remaining-phase lift sources in `boocode_code_review_v2.md`** (openchamber → Phase 3, qwen-code → Phase 2).
|
||
**Pairs with:** the v2.5.x MessageBubble "Thinking" render fix — reasoning already flows; this batch is about persistence, not capability.
|
||
|
||
> **Reconciliation note (2026-05-31).** Four design details below were revised *during* implementation; the original prose/SQL is now superseded:
|
||
> 1. **Per-session SSE** — one `event.subscribe({directory})` per live opencode session (P1.5-a, `v2.6.2`) replaced the single global `/event` read loop (design §2a).
|
||
> 2. **`agent_sessions` is keyed `(chat_id, agent)`**, and a first-class **`worktrees`** table replaced `session_worktrees` (P1.5-b, `v2.6.3`); `session_id`/`worktree_id` are informational `SET NULL` (`v2.6.4`). The design §3 SQL is the *original* shape.
|
||
> 3. **opencode streams `session.next.*` events**, not `message.part.*` (design §2a's event names were wrong).
|
||
> 4. **`OPENCODE_SERVER_PASSWORD` was deferred** — the warm server binds loopback unsecured (design §2a specified a random password). Basic-auth scheme since confirmed (openchamber, `boocode_code_review_v2.md` §5c) if ever wanted.
|
||
|
||
## 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
|
||
|
||
(Status reconciled 2026-05-31: ✅ met · 🟡 partial · ⬜ remaining)
|
||
|
||
- ✅ 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). *(opencode; Smoke 1, `v2.6.1`)*
|
||
- ✅ Warm-start latency for turn 2 materially below turn 1 (no spawn/handshake). *(turn 2 ~9× faster, `v2.6.1`)*
|
||
- ✅ 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). *(Phase 3 — `opencode-server.ts` still comments "recovery is Phase 3")*
|
||
- 🟡 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. *(opencode↔boocode works; goose/qwen warm side is Phase 2 → full round-trip = Smoke 2b, unshipped)*
|
||
- ⬜ Closing/archiving a session removes its worktree; BooCoder restart drains cleanly. *(delete-guard shipped `v2.6.2`, but the close→cleanup hook + orphan reaper are Phase 3)*
|
||
- ✅ Existing one-shot paths (arena, `new_task` tool, MCP create-task) still work. *(dispatcher resolve-or-create fallback)*
|
||
|
||
## Deliverables
|
||
|
||
| Doc | Purpose |
|
||
|-----|---------|
|
||
| [`design.md`](./design.md) | Architecture, backends, data model, worktree/diff strategy, lifecycle, risks |
|
||
| [`tasks.md`](./tasks.md) | Phased implementation checklist |
|