Files
boocode/openspec/changes/claude-sdk-sessionstore/proposal.md
indifferentketchup f3a0197d6a feat: Claude Agent SDK backend + clean-room PostgresSessionStore (v2.7.5)
Lands the lean-SDK direction (boocode_code_review_v2 §1 #9) behind a flag.
Adds @anthropic-ai/claude-agent-sdk@0.3.159 (Commercial Terms, runtime dep).

- PostgresSessionStore: clean-room impl of the SDK's real SessionStore type
  over a new claude_session_entries table. Typechecks against the SDK type;
  8 DB-integration tests.
- ClaudeSdkBackend (implements AgentBackend): one warm query() per (chat,claude)
  in streaming-input mode via a pushable async-iterable pump, sessionStore +
  resume continuity, pure mapSdkMessage->AgentEvent, session_id from init,
  usage/cost onto agent_sessions (backend CHECK gains 'claude_sdk').
- Routing env-gated by CLAUDE_SDK_BACKEND (default off) -> PTY path UNCHANGED.
- Built against real SDK 0.3.159 types (install paid off: partial=stream_event
  needing includePartialMessages, MessageParam, result error arm).
- Fix latent test-infra deadlock: serialize DB suites (fileParallelism:false).

Coder 269 passing default / 290 with DB; tsc clean vs SDK types; builds clean.
LIVE pump + resume + actual claude turn need a host smoke (CLAUDE_SDK_BACKEND=1
+ claude binary + auth). zod peer-dep wants ^4 (workspace 3.25). Builds on v2.7.4.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 13:37:57 +00:00

5.0 KiB

Claude Agent SDK backend + clean-room PostgresSessionStore (#9)

Status: in progress (started 2026-06-01) Source: boocode_code_review_v2.md §1 #9, §5h/§5i (happy + SDK .d.ts). Decision §6.2: lean SDK. SDK: @anthropic-ai/claude-agent-sdk@0.3.159 (installed, Commercial Terms — runtime dep OK, code reference-only; the store is clean-room from the real interface, not vendored).

Replace BooCoder's one-shot PTY claude dispatch with a warm, resumable Claude-SDK backend. Two parts: the clean-room session store (fully testable here) and the backend + wiring (live pump needs a host smoke against real claude).

Ground-truth SDK API (from the installed sdk.d.ts)

  • query({ prompt: string | AsyncIterable<SDKUserMessage>, options?: Options }): Query where Query extends AsyncGenerator<SDKMessage, void>.
  • Options: sessionStore?: SessionStore, resume?: string, model?, cwd?, pathToClaudeCodeExecutable?, canUseTool?, permissionMode?, env?, allowedTools?.
  • SessionStore = { append(key, entries): Promise<void>; load(key): Promise<SessionStoreEntry[]|null>; listSessions?(projectKey): Promise<{sessionId,mtime}[]>; delete?(key): Promise<void>; listSubkeys?({projectKey,sessionId}): Promise<string[]> }.
  • SessionKey = { projectKey: string; sessionId: string; subpath?: string } (undefined subpath = main transcript; empty string invalid — store maps undefined→'' internally).
  • SessionStoreEntry = { type: string; uuid?: string; timestamp?: string; [k]: unknown } (opaque JSONL).
  • Messages: SDKSystemMessage{subtype:'init'} carries session_id (+ model/tools); SDKResultMessage (success/error) ends a turn with result, usage, total_cost_usd; SDKPartialAssistantMessage / SDKAssistantMessage carry text/thinking/tool blocks.

Part 1 — Clean-room PostgresSessionStore (testable now)

  • Schema (apps/coder/src/schema.sql): a generic append-only entry table claude_session_entries(id BIGSERIAL PK, project_key TEXT, session_id TEXT, subpath TEXT DEFAULT '', entry JSONB, created_at TIMESTAMPTZ DEFAULT clock_timestamp()) + index (project_key, session_id, subpath, id). (The store is generic per the SDK's key; the chat↔session ownership lives in agent_sessions, not here.)
  • apps/coder/src/services/backends/claude-session-store.ts: PostgresSessionStore implementing the real SessionStore type over Sql. append = ordered multi-INSERT (id = order); load = SELECT ORDER BY id → array or null; listSessions = group main-transcript rows, mtime = max(created_at) ms; delete = scoped delete (subpath given → that subpath; omitted → whole session); listSubkeys = DISTINCT non-'' subpaths. Pure SQL, no SDK import needed beyond the SessionStore type.
  • Tests __tests__/claude-session-store.test.ts (DB-opt-in, mirror checkpoints.test.ts): append→load round-trip + order, null on unseen key, subpath isolation (main vs subagent), listSessions mtime, delete scoping, listSubkeys.

Part 2 — ClaudeSdkBackend + wiring (live pump needs host smoke)

  • agent_sessions.backend CHECK adds 'claude_sdk'.
  • apps/coder/src/services/backends/claude-sdk.ts: a ClaudeSdkBackend implementing AgentBackend (mirror warm-acp.ts/opencode-server.ts). ensureSession resolves the resume id from agent_sessions(chat_id,'claude').agent_session_id; prompt drives one persistent query() in streaming-input mode (a pushable AsyncIterable<SDKUserMessage> fed per turn) with { sessionStore, resume, model, cwd: worktreePath, pathToClaudeCodeExecutable: installPath }, reads the AsyncGenerator<SDKMessage> until result, captures session_id from the init message and persists it to agent_sessions. A pure mapSdkMessage(msg): AgentEvent[] (unit-tested) maps partial/assistant/tool/thinking → the existing AgentEvent union; result.usage/total_cost_usd accumulate onto agent_sessions (like opencode U.6). isBusy/closeSession/crash mirror the ACP backend.
  • Routing: add claude to the warm path (warm-acp-routing.ts or a sibling shouldUseClaudeSdk), with the existing PTY runExternalAgent kept as the fallback (session-less creators + if the SDK backend fails to start). Provider registry: claude stays selectable; transport reflects the SDK path.
  • Frames + persistence identical to the warm-ACP path (persistExternalAgentTurn, broker frames).

Verify

  • Part 1: pnpm -C apps/coder test + DB-opt-in store tests against dev postgres; build clean.
  • Part 2: pnpm -C apps/coder build + npx tsc -p apps/coder/tsconfig.json --noEmit (typechecks against the REAL SDK types) + pure-mapper unit tests. Live pump + resume across turns: host smoke against real claude (auth required) — cannot run from the dev container.

Open flags

  • SDK peer-deps want zod@^4; workspace is zod@3.25.76 (installed with a warning) — watch at runtime.
  • pathToClaudeCodeExecutable from available_agents.install_path; the SDK spawns the same claude binary the PTY path uses. ANTHROPIC auth/env must reach the child (host concern).