Idle TTL eviction per (chat,agent) + LRU cap (never a busy backend); pure lifecycle-decisions.ts (TDD). Crash recovery lifts openchamber's health-monitor + busy-aware-restart + stale-grace state machine into opencode-server.ts (+ port reclaim) and warm-acp.ts; opencode crash -> fresh sessions, ACP -> re-session/new. F.1 turn-guard + U.6 usage preserved (their tests pass). Orphan worktree reaper (1h grace, superset-style dirty/unpushed preflight, Paseo soft-delete) + close hooks + diff re-baseline after apply_pending. 35 new tests + DB-opt-in reconnect test; 215 coder tests pass; tsc + build clean. Completes v2.6. Follow-ups out of scope: apps/server close-hook caller, 3.7 DiffPanel staging hint, live smokes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
120 lines
5.3 KiB
TypeScript
120 lines
5.3 KiB
TypeScript
/**
|
|
* v2.6 — AgentBackend abstraction (Phase 0 scaffold; types only, zero runtime logic).
|
|
*
|
|
* The core abstraction for persistent agent sessions. Two implementations land
|
|
* later: `OpenCodeServerBackend` (Phase 1, opencode HTTP server) and
|
|
* `WarmAcpBackend` (Phase 2, long-lived ACP process). Backends emit
|
|
* transport-agnostic `AgentEvent`s; the dispatcher maps them to WS frames.
|
|
*
|
|
* Nothing imports this file yet — it must compile standalone.
|
|
* Spec: openspec/changes/v2-6-persistent-agent-sessions/design.md §2.
|
|
*/
|
|
import type { AcpToolSnapshot } from './acp-tool-snapshot.js';
|
|
import type { AgentCommand } from './provider-types.js';
|
|
|
|
/** Backend transport kind. Mirrors `agent_sessions.backend` CHECK in schema.sql. */
|
|
export type AgentBackendKind = 'opencode_server' | 'acp_warm';
|
|
|
|
/**
|
|
* Normalized, transport-agnostic events a backend emits during a turn (§2).
|
|
* Derived from acp-dispatch's session-update handling, but WITHOUT the WS
|
|
* envelope (message_id/chat_id) — the dispatcher owns frame mapping.
|
|
*
|
|
* `tool_call` vs `tool_update` are kept distinct on purpose: acp-dispatch
|
|
* currently merges both into one snapshot frame, but opencode's SSE
|
|
* distinguishes tool-start from tool-result, so the contract carries both.
|
|
* `commands` mirrors the ACP `available_commands_update` path (v2.5.10).
|
|
*/
|
|
export type AgentEvent =
|
|
| { type: 'text'; text: string }
|
|
| { type: 'reasoning'; text: string }
|
|
| { type: 'tool_call'; toolCall: AcpToolSnapshot }
|
|
| { type: 'tool_update'; toolCall: AcpToolSnapshot }
|
|
| { type: 'commands'; commands: AgentCommand[] };
|
|
|
|
/** Params to establish (or look up) a backend session (§2). */
|
|
export interface EnsureSessionOpts {
|
|
agent: string;
|
|
/** Resolved model id. */
|
|
model: string;
|
|
/** P1.5-b: the chat (tab) this turn belongs to. agent_sessions is keyed
|
|
* (chat_id, agent) — the tab/chat is the context unit. Always non-null:
|
|
* the dispatcher creates a chat for session-less tasks before calling. */
|
|
chatId: string;
|
|
/** Shared per-session worktree (one per `sessions.id`, not per pane). */
|
|
worktreePath: string;
|
|
/** P1.5-b: the `worktrees.id` for this session's worktree — stored on the
|
|
* agent_sessions row informationally (NOT the key). */
|
|
worktreeId: string;
|
|
projectId: string;
|
|
}
|
|
|
|
/** Opaque handle to a live backend session, persisted to `agent_sessions` (§2). */
|
|
export interface AgentSessionHandle {
|
|
sessionId: string;
|
|
agent: string;
|
|
backend: AgentBackendKind;
|
|
/** P1.5-b: the chat (tab) this session is keyed on (with agent). */
|
|
chatId: string;
|
|
/** P1.5-b: the worktree this session's chat runs in (informational link). */
|
|
worktreeId: string;
|
|
/** Provider's own session id (resume token); null until the backend assigns one. */
|
|
agentSessionId: string | null;
|
|
/** opencode HTTP server port; null for ACP backends. */
|
|
serverPort: number | null;
|
|
}
|
|
|
|
/** Per-turn context passed to `prompt` (§2). */
|
|
export interface PromptCtx {
|
|
worktreePath: string;
|
|
model: string;
|
|
signal: AbortSignal;
|
|
onEvent: (e: AgentEvent) => void;
|
|
/** Phase 2: per-turn task id, so a warm ACP backend can route permission /
|
|
* elicitation prompts back to the UI via the permission-waiter. Optional —
|
|
* the opencode-server backend (autonomous) ignores it. */
|
|
taskId?: string;
|
|
/** Phase 2: per-turn mode id (gates autonomous mode in the permission-waiter). */
|
|
modeId?: string;
|
|
}
|
|
|
|
/** Result of a completed turn (§2). Diff/persist happen outside the backend. */
|
|
export interface TurnResult {
|
|
ok: boolean;
|
|
error?: string;
|
|
}
|
|
|
|
/**
|
|
* The core backend abstraction (§2). Implementations: OpenCodeServerBackend
|
|
* (Phase 1), WarmAcpBackend (Phase 2).
|
|
*/
|
|
export interface AgentBackend {
|
|
/** Lazy: spawn server / warm process if not already up for this (session, agent). §2 */
|
|
ensureSession(sessionId: string, opts: EnsureSessionOpts): Promise<AgentSessionHandle>;
|
|
/** Send a prompt; stream events via ctx.onEvent; resolves when the turn completes. §2 */
|
|
prompt(handle: AgentSessionHandle, input: string, ctx: PromptCtx): Promise<TurnResult>;
|
|
/** Graceful teardown of one session (session close or idle timeout). §2 */
|
|
closeSession(handle: AgentSessionHandle): Promise<void>;
|
|
/** Full teardown — kills all spawned servers/processes. §2 */
|
|
dispose(): Promise<void>;
|
|
/** Liveness for health endpoint + dispatcher fallback decision. §2 */
|
|
health(): 'up' | 'down';
|
|
/**
|
|
* v2.6 Phase 3: true iff a turn is in flight on this backend. The pool's idle
|
|
* eviction + LRU cap NEVER evict a busy backend (design §6 busy rule); the
|
|
* health-monitor defers a restart while busy (stale-grace). Optional so the
|
|
* Phase-0 scaffold and any test double stay compatible — absent ⇒ treated as
|
|
* not busy. opencode-server (multi-session) is busy iff ANY session has an
|
|
* active turn; warm-acp (single session) iff its one slot is active.
|
|
*/
|
|
isBusy?(): boolean;
|
|
/**
|
|
* v2.6 Phase 3: optional proactive health probe + busy-aware self-restart, run
|
|
* by the pool's periodic sweep. The opencode-server backend implements it
|
|
* (detects a hung-but-not-exited server and restarts when non-busy). Backends
|
|
* with no long-lived shared process (warm-ACP recovers lazily on its own child
|
|
* exit) can omit it. Must never throw — the sweep ignores rejections.
|
|
*/
|
|
tickHealth?(now?: number): Promise<void>;
|
|
}
|