authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-03-05 21:36:46-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-05 23:00:41-08:00
logd74b086aaf0f9bbc71db158074b302ec46a5d392
tree6163cc9ef2ff81d22c0a87c15fdc7c853bc9aeef
parent6496aedc92513fe138a511c7afb9f51152d08e88

split: move Claude SDK and plan doc to sibling stack

Co-authored-by: codex <codex@users.noreply.github.com>

2 files changed, 0 insertions(+), 552 deletions(-)

.plans/17-claude-code.md deleted-549
......@@ -1,549 +0,0 @@
1# Plan: Claude Code Integration (Orchestration Architecture)
2
3## Why this plan was rewritten
4
5The previous plan targeted a pre-orchestration architecture (`ProviderManager`, provider-native WS event methods, and direct provider UI wiring). The current app now routes everything through:
6
71. `orchestration.dispatchCommand` (client intent)
82. `OrchestrationEngine` (decide + persist + publish domain events)
93. `ProviderCommandReactor` (domain intent -> `ProviderService`)
104. `ProviderService` (adapter routing + canonical runtime stream)
115. `ProviderRuntimeIngestion` (provider runtime -> internal orchestration commands)
126. `orchestration.domainEvent` (single push channel consumed by web)
13
14Claude integration must plug into this path instead of reintroducing legacy provider-specific flows.
15
16---
17
18## Current constraints to design around (post-Stage 1)
19
201. Provider runtime ingestion expects canonical `ProviderRuntimeEvent` shapes, not provider-native payloads.
212. Start input now uses typed `providerOptions` and generic `resumeCursor`; top-level provider-specific fields were removed.
223. `resumeCursor` is intentionally opaque outside adapters and must never be synthesized from `providerThreadId`.
234. `ProviderService` still requires adapter `startSession()` to return a `ProviderSession` with `threadId`.
245. Checkpoint revert currently calls `providerService.rollbackConversation()`, so Claude adapter needs a rollback strategy compatible with current reactor behavior.
256. Web currently marks Claude as unavailable (`"Claude Code (soon)"`) and model picker is Codex-only.
26
27---
28
29## Architecture target
30
31Add Claude as a first-class provider adapter that emits canonical runtime events and works with existing orchestration reactors without adding new WS channels or bypass paths.
32
33Key decisions:
34
351. Keep orchestration provider-agnostic; adapt Claude inside adapter/layer boundaries.
362. Use the existing canonical runtime stream (`ProviderRuntimeEvent`) as the only ingestion contract.
373. Keep provider session routing in `ProviderService` and `ProviderSessionDirectory`.
384. Add explicit provider selection to turn-start intent so first turn can start Claude session intentionally.
39
40---
41
42## Phase 1: Contracts and command shape updates
43
44### 1.1 Provider-aware model contract
45
46Update `packages/contracts/src/model.ts` so model resolution can be provider-aware instead of Codex-only.
47
48Expected outcomes:
49
501. Introduce provider-scoped model lists (Codex + Claude).
512. Add helpers that resolve model by provider.
523. Preserve backwards compatibility for existing Codex defaults.
53
54### 1.2 Turn-start provider intent
55
56Update `packages/contracts/src/orchestration.ts`:
57
581. Add optional `provider: ProviderKind` to `ThreadTurnStartCommand`.
592. Carry provider through `ThreadTurnStartRequestedPayload`.
603. Keep existing command valid when provider is omitted.
61
62This removes the implicit “Codex unless session already exists” behavior as the only path.
63
64### 1.3 Provider session start input for Claude runtime knobs (completed)
65
66Update `packages/contracts/src/provider.ts`:
67
681. Move provider-specific start fields into typed `providerOptions`:
69 - `providerOptions.codex`
70 - `providerOptions.claudeCode`
712. Keep `resumeCursor` as the single cross-provider resume input in `ProviderSessionStartInput`.
723. Deprecate/remove `resumeThreadId` from the generic start contract.
734. Treat `resumeCursor` as adapter-owned opaque state.
74
75### 1.4 Contract tests (completed)
76
77Update/add tests in `packages/contracts/src/*.test.ts` for:
78
791. New command payload shape.
802. Provider-aware model resolution behavior.
813. Breaking-change expectations for removed top-level provider fields.
82
83---
84
85## Phase 2: Claude adapter implementation
86
87### 2.1 Add adapter service + layer
88
89Create:
90
911. `apps/server/src/provider/Services/ClaudeCodeAdapter.ts`
922. `apps/server/src/provider/Layers/ClaudeCodeAdapter.ts`
93
94Adapter must implement `ProviderAdapterShape<ProviderAdapterError>`.
95
96### 2.1.a SDK dependency and baseline config
97
98Add server dependency:
99
1001. `@anthropic-ai/claude-agent-sdk`
101
102Baseline adapter options to support from day one:
103
1041. `cwd`
1052. `model`
1063. `pathToClaudeCodeExecutable` (from `providerOptions.claudeCode.binaryPath`)
1074. `permissionMode` (from `providerOptions.claudeCode.permissionMode`)
1085. `maxThinkingTokens` (from `providerOptions.claudeCode.maxThinkingTokens`)
1096. `resume`
1107. `resumeSessionAt`
1118. `includePartialMessages`
1129. `canUseTool`
11310. `hooks`
11411. `env` and `additionalDirectories` (if needed for sandbox/workspace parity)
115
116### 2.2 Claude runtime bridge
117
118Implement a Claude runtime bridge (either directly in adapter layer or via dedicated manager file) that wraps Agent SDK query lifecycle.
119
120Required capabilities:
121
1221. Long-lived session context per adapter session.
1232. Multi-turn input queue.
1243. Interrupt support.
1254. Approval request/response bridge.
1265. Resume support via opaque `resumeCursor` (parsed inside Claude adapter only).
127
128#### 2.2.a Agent SDK details to preserve
129
130The adapter should explicitly rely on these SDK capabilities:
131
1321. `query()` returns an async iterable message stream and control methods (`interrupt`, `setModel`, `setPermissionMode`, `setMaxThinkingTokens`, account/status helpers).
1332. Multi-turn input is supported via async-iterable prompt input.
1343. Tool approval decisions are provided via `canUseTool`.
1354. Resume support uses `resume` and optional `resumeSessionAt`, both derived by parsing adapter-owned `resumeCursor`.
1365. Hooks can be used for lifecycle signals (`Stop`, `PostToolUse`, etc.) when we need adapter-originated checkpoint/runtime events.
137
138#### 2.2.b Effect-native session lifecycle skeleton
139
140```ts
141import { query } from "@anthropic-ai/claude-agent-sdk";
142import { Effect } from "effect";
143
144const acquireSession = (input: ProviderSessionStartInput) =>
145 Effect.acquireRelease(
146 Effect.tryPromise({
147 try: async () => {
148 const claudeOptions = input.providerOptions?.claudeCode;
149 const resumeState = readClaudeResumeState(input.resumeCursor);
150 const abortController = new AbortController();
151 const result = query({
152 prompt: makePromptAsyncIterable(),
153 options: {
154 cwd: input.cwd,
155 model: input.model,
156 permissionMode: claudeOptions?.permissionMode,
157 maxThinkingTokens: claudeOptions?.maxThinkingTokens,
158 pathToClaudeCodeExecutable: claudeOptions?.binaryPath,
159 resume: resumeState?.threadId,
160 resumeSessionAt: resumeState?.sessionAt,
161 signal: abortController.signal,
162 includePartialMessages: true,
163 canUseTool: makeCanUseTool(),
164 hooks: makeClaudeHooks(),
165 },
166 });
167 return { abortController, result };
168 },
169 catch: (cause) =>
170 new ProviderAdapterProcessError({
171 provider: "claudeCode",
172 sessionId: "pending",
173 detail: "Failed to start Claude runtime session.",
174 cause,
175 }),
176 }),
177 ({ abortController }) => Effect.sync(() => abortController.abort()),
178 );
179```
180
181#### 2.2.c AsyncIterable -> Effect Stream integration
182
183Preferred when available in the pinned Effect version:
184
185```ts
186const sdkMessageStream = Stream.fromAsyncIterable(
187 session.result,
188 (cause) =>
189 new ProviderAdapterProcessError({
190 provider: "claudeCode",
191 sessionId,
192 detail: "Claude runtime stream failed.",
193 cause,
194 }),
195);
196```
197
198Portable fallback (already aligned with current server patterns):
199
200```ts
201const queue = yield* Queue.unbounded<ClaudeSdkMessage>();
202yield* Effect.forkScoped(
203 Effect.tryPromise({
204 try: async () => {
205 for await (const message of session.result) {
206 Queue.offerAllUnsafe(queue, [message]);
207 }
208 },
209 catch: (cause) =>
210 new ProviderAdapterProcessError({
211 provider: "claudeCode",
212 sessionId,
213 detail: "Claude runtime stream pump failed.",
214 cause,
215 }),
216 }),
217);
218const sdkMessageStream = Stream.fromQueue(queue);
219```
220
221#### 2.2.d Multi-turn prompt queue pattern (Effect)
222
223Use an Effect queue as the single input boundary:
224
225```ts
226const promptQueue = yield* Queue.unbounded<ClaudePromptEnvelope>();
227
228const prompt: AsyncIterable<ClaudePromptEnvelope> = {
229 [Symbol.asyncIterator]() {
230 return {
231 next: async () => {
232 const item = await Effect.runPromise(Queue.take(promptQueue));
233 if (item.type === "terminate") {
234 return { done: true, value: undefined };
235 }
236 return { done: false, value: item };
237 },
238 };
239 },
240};
241```
242
243`sendTurn()` enqueues a user envelope, while `stopSession()` enqueues terminate and aborts.
244
245#### 2.2.e Approval bridge with `canUseTool`
246
247Map SDK approval checks to existing orchestration approval flows:
248
249```ts
250const canUseTool = async (toolName: string, toolInput: Record<string, unknown>) => {
251 if (approvalModeIsFullAccess(sessionId)) {
252 return { behavior: "allow", updatedInput: toolInput };
253 }
254
255 const requestId = ApprovalRequestId.makeUnsafe(crypto.randomUUID());
256 emitRuntimeEvent({
257 type: "approval.requested",
258 provider: "claudeCode",
259 sessionId,
260 requestId,
261 requestKind: classifyTool(toolName),
262 detail: summarizeToolRequest(toolName, toolInput),
263 // ... eventId/createdAt
264 });
265
266 const decision = await waitForApprovalDecision(requestId);
267 emitRuntimeEvent({
268 type: "approval.resolved",
269 provider: "claudeCode",
270 sessionId,
271 requestId,
272 decision,
273 // ... eventId/createdAt
274 });
275
276 return decision === "accept" || decision === "acceptForSession"
277 ? { behavior: "allow", updatedInput: toolInput }
278 : { behavior: "deny", message: "User declined tool execution." };
279};
280```
281
282#### 2.2.f Hooks and checkpoint signals
283
284If Claude hooks provide cleaner turn boundaries, convert them into canonical runtime events (`turn.completed` and optionally `checkpoint.captured`) so `CheckpointReactor` remains unchanged:
285
286```ts
287hooks: {
288 Stop: [
289 {
290 matcher: {},
291 hooks: [
292 async () => {
293 emitRuntimeEvent({
294 type: "checkpoint.captured",
295 provider: "claudeCode",
296 sessionId,
297 threadId,
298 turnId: activeTurnId,
299 turnCount: nextTurnCount(),
300 // ... eventId/createdAt
301 });
302 },
303 ],
304 },
305 ],
306}
307```
308
309#### 2.2.g Runtime control method wiring
310
311Adapter should expose SDK controls through existing service methods and runtime payload updates:
312
3131. `interruptTurn()` -> `result.interrupt()`
3142. Model override on send turn -> `result.setModel(model)` before enqueuing prompt
3153. Runtime mode changes -> `result.setPermissionMode(mode)` if we support live mode switching
3164. Thinking budget updates -> `result.setMaxThinkingTokens(tokens)` when configured
317
318### 2.3 Canonical event mapping
319
320Map Claude SDK events to `ProviderRuntimeEvent`:
321
3221. `session.started` / `session.exited`
3232. `turn.started` / `turn.completed`
3243. `message.delta` / `message.completed`
3254. `tool.started` / `tool.completed`
3265. `approval.requested` / `approval.resolved`
3276. `runtime.error`
328
329No provider-native event methods should leak beyond the adapter boundary.
330
331Reference mapping table:
332
333| Claude SDK message/callback | Canonical runtime event(s) | Notes |
334|---|---|---|
335| assistant partial text | `message.delta` | Emit deltas only (do not resend full content each chunk). |
336| assistant final message | `message.completed` | Must include stable `itemId` for dedupe/finalize behavior. |
337| result (success) | `turn.completed` (`status: "completed"`) | This is the main turn boundary for checkpointing. |
338| result (interrupted/cancelled) | `turn.completed` (`status: "interrupted"`/`"cancelled"`) | Preserve state semantics used by ingestion + UI. |
339| result (error) | `runtime.error` and `turn.completed` (`status: "failed"`) | Include concise error message. |
340| tool start | `tool.started` | Set `toolKind` using shared classifier (`command`, `file-change`, `other`). |
341| tool result | `tool.completed` | Include detail summary used by activity feed. |
342| canUseTool prompt | `approval.requested` | Source of request id shown in UI. |
343| approval decision returned | `approval.resolved` | Must correlate using same request id. |
344| session initialization | `session.started`, optional `thread.started` | Ensure `threadId` is emitted early and consistently. |
345| process exit/close | `session.exited` | Required for session status projection cleanup. |
346
347### 2.4 Session/thread/resume identifiers
348
349Define explicit adapter semantics:
350
3511. `sessionId`: adapter-owned stable session id.
3522. `threadId`: Claude conversation/session identifier returned as `ProviderThreadId`.
3533. `resumeCursor`: provider-specific cursor (for example thread id + message cursor) needed for precise recovery/rollback.
3544. Orchestration/shared services persist and forward `resumeCursor` unchanged without provider-specific parsing.
355
356### 2.5 Rollback/read strategy
357
358Implement `readThread()` and `rollbackThread()` in a way compatible with `CheckpointReactor`.
359
360Preferred:
361
3621. Track per-turn resume cursors and reconstruct session state on rollback.
3632. Keep adapter `sessionId` stable across internal rehydration/restart.
364
365Fallback (explicitly documented if chosen):
366
3671. Filesystem revert succeeds even when conversation rewind is partial.
3682. Reactor behavior is updated to surface a clear warning activity instead of silent drift.
369
370### 2.6 Adapter unit tests
371
372Add tests mirroring Codex adapter coverage:
373
3741. Provider validation (`provider === "claudeCode"`).
3752. Event mapping to canonical runtime events.
3763. Approval request lifecycle.
3774. Resume/recovery behavior.
3785. Rollback behavior.
379
380---
381
382## Phase 3: Register adapter in runtime composition
383
384### 3.1 Adapter registry wiring
385
386Update:
387
3881. `apps/server/src/provider/Layers/ProviderAdapterRegistry.ts`
389
390Register both Codex and Claude adapters.
391
392### 3.2 Server layer composition
393
394Update:
395
3961. `apps/server/src/serverLayers.ts`
397
398Provide `ClaudeCodeAdapterLive` alongside Codex in `makeServerProviderLayer()`.
399
400### 3.3 Composition tests
401
402Update integration tests to ensure:
403
4041. Registry exposes Claude provider.
4052. Provider service can route Claude sessions.
406
407---
408
409## Phase 4: Orchestration command/reactor updates
410
411### 4.1 Decider propagation (completed)
412
413Update `apps/server/src/orchestration/decider.ts`:
414
4151. Carry optional `provider` from `thread.turn.start` command into `thread.turn-start-requested` event payload.
4162. Keep this behavior provider-agnostic (no provider-specific runtime fields in the event payload).
417
418### 4.2 ProviderCommandReactor provider selection
419
420Update `apps/server/src/orchestration/Layers/ProviderCommandReactor.ts`:
421
4221. Prefer provider from turn-start event payload when starting a new session.
4232. Fallback to existing thread session provider when payload omitted.
4243. Fallback to default provider only when neither is present.
4254. On restart/rebind, forward the runtime session's persisted `resumeCursor` as-is (no reconstruction from `providerThreadId`).
426
427Switch behavior policy (explicit in implementation):
428
4291. If active session provider differs from requested provider, stop and recreate session before sending turn.
4302. Keep current provider when request omits provider.
431
432### 4.3 Reactor/invariant tests
433
434Update/add tests in:
435
4361. `apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts`
4372. `apps/server/src/orchestration/*.test.ts` as needed
438
439Validate first-turn provider choice, provider switching semantics, and backward compatibility when provider is omitted.
440
441---
442
443## Phase 5: Web provider UX enablement
444
445### 5.1 Enable Claude option
446
447Update `apps/web/src/session-logic.ts`:
448
4491. Mark Claude provider option as available.
450
451### 5.2 Provider selection in composer
452
453Update `apps/web/src/components/ChatView.tsx`:
454
4551. Add provider picker near model/runtime controls.
4562. Include selected provider in `thread.turn.start` command payload.
4573. Keep selected provider sticky per thread in client state (or derive from active session provider when present).
458
459### 5.3 Provider-aware model picker
460
461Update model picker usage so model options reflect selected provider.
462
463### 5.4 Web tests
464
465Update/add tests for:
466
4671. Provider picker interactions.
4682. Turn-start command includes provider.
4693. Model list switches by provider.
470
471---
472
473## Phase 6: Checkpoint and revert compatibility
474
475### 6.1 Validate checkpoint reactor expectations
476
477Exercise `apps/server/src/orchestration/Layers/CheckpointReactor.ts` flows with Claude:
478
4791. Turn baseline capture.
4802. Turn completion checkpoint capture.
4813. Revert + provider rollback coordination.
482
483### 6.2 Policy for partial rollback support
484
485If Claude cannot support full conversation rewind at parity with Codex:
486
4871. Document behavior clearly.
4882. Emit explicit thread activity for degraded rollback behavior.
4893. Ensure filesystem state remains correct and predictable.
490
491### 6.3 Checkpoint tests
492
493Add/update tests in checkpoint + provider service integration suites for Claude revert behavior.
494
495---
496
497## Phase 7: End-to-end hardening and observability
498
499### 7.1 Integration tests
500
501Add integration coverage for:
502
5031. `thread.turn.start` with `provider: "claudeCode"` from fresh thread.
5042. Session recovery after restart (provider session alias/recovery path).
5053. Approval request/respond flow.
5064. Interrupt behavior.
5075. Revert flow with rollback.
508
509### 7.2 WS behavior regression check
510
511Ensure no contract regressions:
512
5131. Client still consumes only `orchestration.domainEvent`.
5142. No new provider-specific WS channels introduced.
515
516### 7.3 Logging
517
518Confirm both native and canonical provider logs remain useful with multi-adapter setup:
519
5201. `provider-native.ndjson`
5212. `provider-canonical.ndjson`
522
523---
524
525## File checklist
526
527Likely remaining files to touch:
528
5291. `apps/server/src/provider/Services/ClaudeCodeAdapter.ts` (new)
5302. `apps/server/src/provider/Layers/ClaudeCodeAdapter.ts` (new)
5313. `apps/server/src/provider/Layers/ProviderAdapterRegistry.ts`
5324. `apps/server/src/serverLayers.ts`
5335. `apps/server/src/orchestration/Layers/ProviderCommandReactor.ts`
5346. `apps/web/src/session-logic.ts`
5357. `apps/web/src/components/ChatView.tsx`
5368. Related tests under `apps/server/src/provider/Layers`, `apps/server/src/orchestration/Layers`, `apps/server/integration`, and `apps/web/src`.
537
538---
539
540## Delivery order
541
5421. Claude adapter + unit tests on top of the new `providerOptions`/opaque-cursor contracts.
5432. Registry/layer wiring.
5443. Remaining reactor updates for provider-aware session selection/switching invariants.
5454. Web provider picker + provider-aware models.
5465. Checkpoint/revert compatibility.
5476. End-to-end integration tests and stabilization.
548
549This order keeps risk isolated and maintains a working orchestrated path at each stage.
bun.lock-3
......@@ -37,7 +37,6 @@
3737 "t3": "./dist/index.mjs",
3838 },
3939 "dependencies": {
40 "@anthropic-ai/claude-agent-sdk": "^0.2.62",
4140 "@effect/platform-node": "catalog:",
4241 "@effect/sql-sqlite-bun": "catalog:",
4342 "@pierre/diffs": "^1.1.0-beta.16",
......@@ -163,8 +162,6 @@
163162 "vitest": "^4.0.0",
164163 },
165164 "packages": {
166 "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.70", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.34.2", "@img/sharp-darwin-x64": "^0.34.2", "@img/sharp-linux-arm": "^0.34.2", "@img/sharp-linux-arm64": "^0.34.2", "@img/sharp-linux-x64": "^0.34.2", "@img/sharp-linuxmusl-arm64": "^0.34.2", "@img/sharp-linuxmusl-x64": "^0.34.2", "@img/sharp-win32-arm64": "^0.34.2", "@img/sharp-win32-x64": "^0.34.2" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-ABaB37jWt7dZXfIDHebHv99jX9GIyqc0aSjcz9nxq79eauOpa+64Cah5hx/yzhsWz7m5GEtjbMIZCClTfnRRhg=="],
167
168165 "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
169166
170167 "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="],