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>
5.9 KiB
5.9 KiB
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_sessionstables (per(session_id, agent)) toapps/coder/src/schema.sql(idempotent; see design §3). - 0.2 Define
AgentBackend/AgentSessionHandleinterface + normalizedonEventevent union (reuse shapes fromacp-dispatch.ts). - 0.3 Scaffold
agent-pool.tswith lazy get-or-create keyed by(chat, agent), health,dispose(); wireapp.addHook('onClose')to dispose alongside dispatcherstop().
Phase 1 — OpenCode server backend (multi-turn, warm)
- 1.1 Add
@opencode-ai/sdktoapps/coder/package.json; pin to installed opencode major. - 1.2
backends/opencode-server.ts: spawnopencode serveonce (randomOPENCODE_SERVER_PASSWORD, allocated port),createOpencodeClient, wait for ready line. - 1.3 Single
/eventSSE read loop; demux byproperties.sessionID; mapmessage.part.delta/updated(text + reasoning) + tool parts toonEvent. - 1.4 Port Paseo
streamedPartKeysreasoning dedup (delta vs final part). - 1.5
ensureSession: reuse the(chat, opencode)agent_sessionsrow if present (resume on switch-back), elseclient.session.create()→ storeagent_session_id. - 1.6
prompt: send via SDK withx-opencode-directory= session worktree +model. - 1.7 Dispatcher: when
agent==='opencode', route to pool backend instead ofdispatchViaAcp; keep broker frames +persistExternalAgentTurnidentical. - 1.8 Persistent worktree: chat-keyed
createWorktree(shared across agents); capture base commit insession_worktrees; reuse across turns and agents. - 1.9 Per-session concurrency: replace global
runningwithMap<sessionId,Promise>;poll()skips sessions with an in-flight turn. - 1.10 Per-turn diff → supersede prior
pending_changesrow for the session (latest-wins). - Smoke 1: two messages in one opencode chat → same
agent_session_id, same worktree, no secondcreateWorktree; agent references turn-1 edits; reasoning shows once; turn-2 faster.
Phase 1 (UX) — Attribution & switch affordances (design §9)
- U.1 Stamp
pending_changes.agentat queue time (worktree path → task agent; native write tools →'boocode'; manual RightRail create → NULL). - U.2 Add
agenttolistPendingresponse + frontendPendingChangetype. - 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-sessionsroute +api.coder.agentSessions+useAgentSessions(sessionId)(refetch onmessage_complete) (§9b). - U.5
AgentComposerBaroptionalsessionIdprop → 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/newonce; reuseacp-dispatch.tshandleSessionUpdate. - 2.2
prompt:session/prompton 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/qwento 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 fromagent_sessions. - 3.2 Crash recovery: opencode server restart recreates sessions; ACP re-
session/new. - 3.3 Chat close/archive hook →
closeSessionfor every(chat, agent)+ remove the sharedsession_worktreesrow + worktree; mark agent rowsstatus='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-poolunit: 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.mdhealth/contract. - D.2 Note opencode
@opencode-ai/sdkdep +OPENCODE_SERVER_PASSWORDenv in env docs. - D.3
CHANGELOG.mdentry on tag (v2.6.0-persistent-agent-sessions).
Build / deploy gate
- B.1
pnpm -C apps/server build && pnpm -C apps/coder buildclean. - B.2
pnpm -C apps/server test(+ DB-opt-in) green. - B.3 Deploy:
sudo systemctl restart boocoder;curl :9502/api/healthreports tool count.