# 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, options?: Options }): Query` where `Query extends AsyncGenerator`. - `Options`: `sessionStore?: SessionStore`, `resume?: string`, `model?`, `cwd?`, `pathToClaudeCodeExecutable?`, `canUseTool?`, `permissionMode?`, `env?`, `allowedTools?`. - `SessionStore = { append(key, entries): Promise; load(key): Promise; listSessions?(projectKey): Promise<{sessionId,mtime}[]>; delete?(key): Promise; listSubkeys?({projectKey,sessionId}): Promise }`. - `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` fed per turn) with `{ sessionStore, resume, model, cwd: worktreePath, pathToClaudeCodeExecutable: installPath }`, reads the `AsyncGenerator` 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).