feat(coder): v2.6 Phase 0 — AgentBackend foundations (no behavior change)

Schema, interface, and service scaffold for v2.6 persistent agent sessions.
Nothing in this batch alters runtime behavior.

- schema.sql: add session_worktrees (one shared worktree per session, FK
  sessions(id)) and agent_sessions (one backend session per (session, agent),
  with backend/status CHECKs); add pending_changes.agent column for DiffPanel
  attribution. All three statements idempotent (IF NOT EXISTS).
- services/agent-backend.ts: AgentBackend interface + AgentSessionHandle,
  EnsureSessionOpts, PromptCtx, TurnResult, and the normalized transport-agnostic
  AgentEvent union (text/reasoning/tool_call/tool_update/commands). Types only.
- services/agent-pool.ts: lazy get-or-create AgentPool keyed by
  `${sessionId}:${agent}` + shared `agentPool` singleton. Empty in Phase 0.
- index.ts: widen onClose to await dispatcher.stop() then agentPool.dispose()
  (pool empty, so dispose() is inert).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 02:50:17 +00:00
parent a97293b5d9
commit 140ff26204
4 changed files with 162 additions and 1 deletions

View File

@@ -0,0 +1,44 @@
/**
* v2.6 — AgentPool (Phase 0 scaffold).
*
* Lazy get-or-create registry of `AgentBackend` instances keyed by
* `${sessionId}:${agent}`. Phase 0 ships the skeleton only: an in-memory Map,
* lookup / register / health, and clean disposal wired to the server's onClose.
* Spawning lands in Phase 1/2; nothing populates the map yet.
*
* Spec: openspec/changes/v2-6-persistent-agent-sessions/design.md §2.
*/
import type { AgentBackend } from './agent-backend.js';
export class AgentPool {
private readonly backends = new Map<string, AgentBackend>();
private key(sessionId: string, agent: string): string {
return `${sessionId}:${agent}`;
}
/** Map lookup only. Spawning is Phase 1/2 — never creates here. */
get(sessionId: string, agent: string): AgentBackend | undefined {
return this.backends.get(this.key(sessionId, agent));
}
/** Store a backend instance for this (session, agent). */
register(sessionId: string, agent: string, backend: AgentBackend): void {
this.backends.set(this.key(sessionId, agent), backend);
}
/** Summary for the health endpoint. */
health(): { size: number } {
return { size: this.backends.size };
}
/** Dispose every backend and clear the map. Tolerates throwing backends. */
async dispose(): Promise<void> {
const entries = [...this.backends.values()];
this.backends.clear();
await Promise.allSettled(entries.map((b) => b.dispose()));
}
}
/** Single shared instance — referenced only by the server's onClose hook in Phase 0. */
export const agentPool = new AgentPool();