feat(coder): v2.6 Phase 1 — OpenCode warm server backend

Persistent multi-turn opencode backend: one `opencode serve` HTTP server per
BooCoder process, one opencode session per BooCode session (resumed on
switch-back), single SSE read loop demuxed by session id.

- backends/opencode-server.ts: AgentBackend implementation — spawn with
  waitForReady, session.next.* SSE event translation (text/reasoning/tool
  deltas), Paseo-ported reasoning dedup (streamedPartKeys), promptAsync
  fire-and-forget settled by session.idle, per-turn inactivity watchdog
  (180s) + reconnect reconciliation via session.messages, stale-session
  guard (crashed-not-resumed + config_hash fingerprint on model).
- dispatcher.ts: opencode routes to pool backend (ensureSession→prompt);
  per-session concurrency Map replaces global running boolean (1.9);
  model coalesce (empty→DEFAULT_MODEL) + llama-swap/ prefix for opencode;
  diff-supersede (DELETE+INSERT pending_changes by session, stamp agent).
- worktrees.ts: ensureSessionWorktree (session-keyed, captures base_commit,
  persists to session_worktrees); diffWorktree gains optional baseRef.
- agent-probe.ts: mergeLlamaSwap branch fetches /v1/models, prefixes with
  llama-swap/, populates opencode's available_agents.models (was 0).
- provider-snapshot.ts: export fetchLlamaSwapModels for probe reuse.
- schema.sql: session_worktrees + agent_sessions tables (Phase 0) +
  config_hash column on agent_sessions, pending_changes.agent column.
- package.json: @opencode-ai/sdk ~1.15.0 (resolved 1.15.12).

Known Phase 1 limitation: single SSE stream scoped to most-recent session's
directory; concurrent opencode sessions in different worktrees collide
(warning logged, watchdog prevents hang). Phase 2 moves to per-session SSE.

Smoke 1 verified: two turns in one session, both produce real tokens, same
agent_session_id reused, same server port, turn 2 is 9x faster (no spawn).
goose/qwen/claude paths untouched (runExternalAgent md5 identical).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 20:37:11 +00:00
parent 140ff26204
commit e30a9e8b23
8 changed files with 1171 additions and 41 deletions

View File

@@ -4,7 +4,7 @@ import { exec as execCb, execFile as execFileCb } from 'node:child_process';
import { promisify } from 'node:util';
import { PROVIDERS_BY_NAME } from './provider-registry.js';
import { resolveAcpProbeBinaries } from './acp-spawn.js';
import { clearProviderSnapshotCache } from './provider-snapshot.js';
import { clearProviderSnapshotCache, fetchLlamaSwapModels, prefixLlamaSwapModels } from './provider-snapshot.js';
import { readQwenSettingsModels } from './qwen-settings.js';
import { loadConfig } from '../config.js';
import { loadProviderConfig } from './provider-config-registry.js';
@@ -117,6 +117,15 @@ export async function probeAgents(sql: Sql, log: FastifyBaseLogger): Promise<voi
if (agentName === 'qwen') {
models = await readQwenSettingsModels();
}
if (providerDef?.mergeLlamaSwap) {
try {
const config = loadConfig();
const llamaModels = prefixLlamaSwapModels(await fetchLlamaSwapModels(config));
models = [...models, ...llamaModels];
} catch (err) {
log.warn({ agent: agentName, err: err instanceof Error ? err.message : String(err) }, 'agent-probe: llama-swap model fetch failed (non-fatal)');
}
}
}
const label = resolved.configLabel ?? resolved.label;