authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-09-04 06:23:12-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-04 06:23:12-07:00
log560afffdea82000d757c98ea79678aee75f8648c
tree9872fb6eca619670988401c2d4e856414186ca31
parentcaa8a0db98f9d32e98a1645caa7f7dd37b14f187
signature Signed by PGP key B5690EEEBB952194

fix(server): update Claude Agent SDK to 0.3.260 (#9135)

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

5 files changed, 357 insertions(+), 19 deletions(-)

apps/server/package.json+1-1
...@@ -22,7 +22,7 @@...@@ -22,7 +22,7 @@
22 "test": "vp test run"22 "test": "vp test run"
23 },23 },
24 "dependencies": {24 "dependencies": {
25 "@anthropic-ai/claude-agent-sdk": "^0.3.170",25 "@anthropic-ai/claude-agent-sdk": "^0.3.260",
26 "@effect/platform-bun": "catalog:",26 "@effect/platform-bun": "catalog:",
27 "@effect/platform-node": "catalog:",27 "@effect/platform-node": "catalog:",
28 "@effect/sql-sqlite-bun": "catalog:",28 "@effect/sql-sqlite-bun": "catalog:",
apps/server/src/provider/Layers/ClaudeAdapter.test.ts+244-4
...@@ -2090,6 +2090,177 @@ describe("ClaudeAdapterLive", () => {...@@ -2090,6 +2090,177 @@ describe("ClaudeAdapterLive", () => {
2090 );2090 );
2091 });2091 });
20922092
2093 it.effect("fails a turn when the result carries a give-up terminal_reason", () => {
2094 const harness = makeHarness();
2095 return Effect.gen(function* () {
2096 const adapter = yield* ClaudeAdapter;
2097
2098 const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 7).pipe(
2099 Stream.runCollect,
2100 Effect.forkChild,
2101 );
2102
2103 const session = yield* adapter.startSession({
2104 threadId: THREAD_ID,
2105 provider: ProviderDriverKind.make("claudeAgent"),
2106 runtimeMode: "full-access",
2107 });
2108
2109 const turn = yield* adapter.sendTurn({
2110 threadId: session.threadId,
2111 input: "hello",
2112 attachments: [],
2113 });
2114
2115 // The CLI stamps subtype success with an empty error list when it
2116 // gives up after exhausting API retries; the terminal_reason is the
2117 // only structured failure signal.
2118 harness.query.emit({
2119 type: "result",
2120 subtype: "success",
2121 is_error: false,
2122 result: "",
2123 errors: [],
2124 stop_reason: null,
2125 terminal_reason: "api_error",
2126 session_id: "sdk-session-api-error",
2127 uuid: "result-api-error",
2128 } as unknown as SDKMessage);
2129
2130 const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber));
2131 assert.deepEqual(
2132 runtimeEvents.map((event) => event.type),
2133 [
2134 "session.started",
2135 "session.configured",
2136 "session.state.changed",
2137 "turn.started",
2138 "thread.started",
2139 "runtime.error",
2140 "turn.completed",
2141 ],
2142 );
2143
2144 const turnCompleted = runtimeEvents[runtimeEvents.length - 1];
2145 assert.equal(turnCompleted?.type, "turn.completed");
2146 if (turnCompleted?.type === "turn.completed") {
2147 assert.equal(String(turnCompleted.turnId), String(turn.turnId));
2148 assert.equal(turnCompleted.payload.state, "failed");
2149 assert.equal(
2150 turnCompleted.payload.errorMessage,
2151 "Claude gave up after repeated API errors.",
2152 );
2153 }
2154 }).pipe(
2155 Effect.provideService(Random.Random, makeDeterministicRandomService()),
2156 Effect.provide(harness.layer),
2157 );
2158 });
2159
2160 it.effect("fails a turn for every dead-turn terminal_reason", () => {
2161 const reasons = [
2162 "blocking_limit",
2163 "rapid_refill_breaker",
2164 "prompt_too_long",
2165 "image_error",
2166 "model_error",
2167 "malformed_tool_use_exhausted",
2168 "budget_exhausted",
2169 "structured_output_retry_exhausted",
2170 "tool_deferred_unavailable",
2171 "turn_setup_failed",
2172 ];
2173 // One harness per reason: the fake query settles a single turn.
2174 const runDeadTurn = (reason: string) => {
2175 const harness = makeHarness();
2176 return Effect.gen(function* () {
2177 const adapter = yield* ClaudeAdapter;
2178 const completionFiber = yield* adapter.streamEvents.pipe(
2179 Stream.filter((event) => event.type === "turn.completed"),
2180 Stream.runHead,
2181 Effect.forkChild,
2182 );
2183 const session = yield* adapter.startSession({
2184 threadId: THREAD_ID,
2185 provider: ProviderDriverKind.make("claudeAgent"),
2186 runtimeMode: "full-access",
2187 });
2188 yield* adapter.sendTurn({ threadId: session.threadId, input: "hello", attachments: [] });
2189 harness.query.emit({
2190 type: "result",
2191 subtype: "success",
2192 is_error: false,
2193 result: "",
2194 errors: [],
2195 stop_reason: null,
2196 terminal_reason: reason,
2197 session_id: "sdk-session-dead-turn",
2198 uuid: `result-${reason}`,
2199 } as unknown as SDKMessage);
2200 const completed = yield* Fiber.join(completionFiber);
2201 assert.equal(completed._tag, "Some");
2202 if (completed._tag === "Some" && completed.value.type === "turn.completed") {
2203 assert.equal(completed.value.payload.state, "failed", reason);
2204 assert.ok(completed.value.payload.errorMessage, `${reason} carries an error message`);
2205 }
2206 }).pipe(
2207 Effect.provideService(Random.Random, makeDeterministicRandomService()),
2208 Effect.provide(harness.layer),
2209 );
2210 };
2211 return Effect.forEach(reasons, runDeadTurn, { discard: true });
2212 });
2213
2214 it.effect("fails a turn when a success result reports a 529 overload", () => {
2215 const harness = makeHarness();
2216 return Effect.gen(function* () {
2217 const adapter = yield* ClaudeAdapter;
2218
2219 const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 7).pipe(
2220 Stream.runCollect,
2221 Effect.forkChild,
2222 );
2223
2224 const session = yield* adapter.startSession({
2225 threadId: THREAD_ID,
2226 provider: ProviderDriverKind.make("claudeAgent"),
2227 runtimeMode: "full-access",
2228 });
2229
2230 yield* adapter.sendTurn({
2231 threadId: session.threadId,
2232 input: "hello",
2233 attachments: [],
2234 });
2235
2236 harness.query.emit({
2237 type: "result",
2238 subtype: "success",
2239 is_error: true,
2240 api_error_status: 529,
2241 result: "",
2242 errors: [],
2243 stop_reason: null,
2244 session_id: "sdk-session-overload",
2245 uuid: "result-overload",
2246 } as unknown as SDKMessage);
2247
2248 const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber));
2249 const turnCompleted = runtimeEvents[runtimeEvents.length - 1];
2250 assert.equal(turnCompleted?.type, "turn.completed");
2251 if (turnCompleted?.type === "turn.completed") {
2252 assert.equal(turnCompleted.payload.state, "failed");
2253 assert.equal(
2254 turnCompleted.payload.errorMessage,
2255 "Claude API is overloaded (529). Try again shortly.",
2256 );
2257 }
2258 }).pipe(
2259 Effect.provideService(Random.Random, makeDeterministicRandomService()),
2260 Effect.provide(harness.layer),
2261 );
2262 });
2263
2093 it.effect("interruptTurn settles live tasks and closes the provider session", () => {2264 it.effect("interruptTurn settles live tasks and closes the provider session", () => {
2094 const harness = makeHarness();2265 const harness = makeHarness();
2095 return Effect.gen(function* () {2266 return Effect.gen(function* () {
...@@ -3088,7 +3259,36 @@ describe("ClaudeAdapterLive", () => {...@@ -3088,7 +3259,36 @@ describe("ClaudeAdapterLive", () => {
3088 { type: "system", subtype: "plugin_install", session_id: "session", uuid: "pi" },3259 { type: "system", subtype: "plugin_install", session_id: "session", uuid: "pi" },
3089 { type: "system", subtype: "memory_recall", session_id: "session", uuid: "mr" },3260 { type: "system", subtype: "memory_recall", session_id: "session", uuid: "mr" },
3090 { type: "system", subtype: "elicitation_complete", session_id: "session", uuid: "ec" },3261 { type: "system", subtype: "elicitation_complete", session_id: "session", uuid: "ec" },
3262 {
3263 type: "system",
3264 subtype: "control_request_progress",
3265 request_id: "ctrl-1",
3266 status: "started",
3267 session_id: "session",
3268 uuid: "crp",
3269 },
3270 {
3271 type: "system",
3272 subtype: "worker_shutting_down",
3273 reason: "host_exit",
3274 session_id: "session",
3275 uuid: "wsd",
3276 },
3277 {
3278 type: "system",
3279 subtype: "informational",
3280 content: "Loaded 3 skills",
3281 level: "notice",
3282 session_id: "session",
3283 uuid: "info",
3284 },
3091 { type: "prompt_suggestion", suggestion: "try this", session_id: "session", uuid: "ps" },3285 { type: "prompt_suggestion", suggestion: "try this", session_id: "session", uuid: "ps" },
3286 {
3287 type: "conversation_reset",
3288 new_conversation_id: "conv-2",
3289 session_id: "session",
3290 uuid: "cr",
3291 },
3092 {3292 {
3093 type: "system",3293 type: "system",
3094 subtype: "notification",3294 subtype: "notification",
...@@ -3111,6 +3311,27 @@ describe("ClaudeAdapterLive", () => {...@@ -3111,6 +3311,27 @@ describe("ClaudeAdapterLive", () => {
3111 session_id: "session",3311 session_id: "session",
3112 uuid: "notif-high",3312 uuid: "notif-high",
3113 } as unknown as SDKMessage);3313 } as unknown as SDKMessage);
3314 // Warning-level informational notes and refusals without a fallback
3315 // model surface as warning rows too.
3316 harness.query.emit({
3317 type: "system",
3318 subtype: "informational",
3319 content: "Stop hook prevented continuation",
3320 level: "warning",
3321 prevent_continuation: true,
3322 session_id: "session",
3323 uuid: "info-warn",
3324 } as unknown as SDKMessage);
3325 harness.query.emit({
3326 type: "system",
3327 subtype: "model_refusal_no_fallback",
3328 original_model: "claude-opus-5",
3329 request_id: null,
3330 api_refusal_explanation: "The request was declined by the API.",
3331 content: "Model refused",
3332 session_id: "session",
3333 uuid: "mrnf",
3334 } as unknown as SDKMessage);
3114 // session_state_changed maps to the matching session states.3335 // session_state_changed maps to the matching session states.
3115 for (const [state, uuid] of [3336 for (const [state, uuid] of [
3116 ["running", "ssc-run"],3337 ["running", "ssc-run"],
...@@ -3141,10 +3362,15 @@ describe("ClaudeAdapterLive", () => {...@@ -3141,10 +3362,15 @@ describe("ClaudeAdapterLive", () => {
3141 yield* Effect.yieldNow;3362 yield* Effect.yieldNow;
31423363
3143 const warnings = runtimeEvents.filter((event) => event.type === "runtime.warning");3364 const warnings = runtimeEvents.filter((event) => event.type === "runtime.warning");
3144 // Exactly one warning: the high-priority notification. Nothing else.3365 // Exactly three warnings: the high-priority notification, the
3366 // warning-level informational note, and the refusal. Nothing else.
3145 assert.deepEqual(3367 assert.deepEqual(
3146 warnings.map((event) => event.payload.message),3368 warnings.map((event) => event.payload.message),
3147 ["context window nearly full"],3369 [
3370 "context window nearly full",
3371 "Stop hook prevented continuation",
3372 "The request was declined by the API.",
3373 ],
3148 );3374 );
3149 const sessionStates = runtimeEvents3375 const sessionStates = runtimeEvents
3150 .filter((event) => event.type === "session.state.changed")3376 .filter((event) => event.type === "session.state.changed")
...@@ -4190,6 +4416,7 @@ describe("ClaudeAdapterLive", () => {...@@ -4190,6 +4416,7 @@ describe("ClaudeAdapterLive", () => {
4190 { command: "pwd" },4416 { command: "pwd" },
4191 {4417 {
4192 signal: new AbortController().signal,4418 signal: new AbortController().signal,
4419 requestId: "request-1",
4193 suggestions: [4420 suggestions: [
4194 {4421 {
4195 type: "setMode",4422 type: "setMode",
...@@ -4301,6 +4528,7 @@ describe("ClaudeAdapterLive", () => {...@@ -4301,6 +4528,7 @@ describe("ClaudeAdapterLive", () => {
4301 { title: "hello" },4528 { title: "hello" },
4302 {4529 {
4303 signal: new AbortController().signal,4530 signal: new AbortController().signal,
4531 requestId: "request-2",
4304 suggestions: [],4532 suggestions: [],
4305 toolUseID: "tool-use-mcp-1",4533 toolUseID: "tool-use-mcp-1",
4306 },4534 },
...@@ -4327,6 +4555,7 @@ describe("ClaudeAdapterLive", () => {...@@ -4327,6 +4555,7 @@ describe("ClaudeAdapterLive", () => {
4327 { command: "git status" },4555 { command: "git status" },
4328 {4556 {
4329 signal: new AbortController().signal,4557 signal: new AbortController().signal,
4558 requestId: "request-3",
4330 suggestions: [4559 suggestions: [
4331 {4560 {
4332 type: "addRules",4561 type: "addRules",
...@@ -4385,6 +4614,7 @@ describe("ClaudeAdapterLive", () => {...@@ -4385,6 +4614,7 @@ describe("ClaudeAdapterLive", () => {
4385 {},4614 {},
4386 {4615 {
4387 signal: new AbortController().signal,4616 signal: new AbortController().signal,
4617 requestId: "request-4",
4388 toolUseID: "tool-agent-1",4618 toolUseID: "tool-agent-1",
4389 },4619 },
4390 );4620 );
...@@ -4409,6 +4639,7 @@ describe("ClaudeAdapterLive", () => {...@@ -4409,6 +4639,7 @@ describe("ClaudeAdapterLive", () => {
4409 { pattern: "foo", path: "src" },4639 { pattern: "foo", path: "src" },
4410 {4640 {
4411 signal: new AbortController().signal,4641 signal: new AbortController().signal,
4642 requestId: "request-5",
4412 toolUseID: "tool-grep-approval-1",4643 toolUseID: "tool-grep-approval-1",
4413 },4644 },
4414 );4645 );
...@@ -4951,6 +5182,7 @@ describe("ClaudeAdapterLive", () => {...@@ -4951,6 +5182,7 @@ describe("ClaudeAdapterLive", () => {
4951 },5182 },
4952 {5183 {
4953 signal: new AbortController().signal,5184 signal: new AbortController().signal,
5185 requestId: "request-6",
4954 toolUseID: "tool-exit-1",5186 toolUseID: "tool-exit-1",
4955 },5187 },
4956 );5188 );
...@@ -5073,7 +5305,7 @@ describe("ClaudeAdapterLive", () => {...@@ -5073,7 +5305,7 @@ describe("ClaudeAdapterLive", () => {
5073 dialogKind: "resume_return",5305 dialogKind: "resume_return",
5074 payload: { sessionAgeMinutes: 145, estimatedTokens: 275123 },5306 payload: { sessionAgeMinutes: 145, estimatedTokens: 275123 },
5075 },5307 },
5076 { signal: new AbortController().signal },5308 { signal: new AbortController().signal, requestId: "request-dialog" },
5077 );5309 );
50785310
5079 const requested = yield* Stream.runHead(adapter.streamEvents);5311 const requested = yield* Stream.runHead(adapter.streamEvents);
...@@ -5173,6 +5405,7 @@ describe("ClaudeAdapterLive", () => {...@@ -5173,6 +5405,7 @@ describe("ClaudeAdapterLive", () => {
51735405
5174 const permissionPromise = canUseTool("AskUserQuestion", askInput, {5406 const permissionPromise = canUseTool("AskUserQuestion", askInput, {
5175 signal: new AbortController().signal,5407 signal: new AbortController().signal,
5408 requestId: "request-7",
5176 toolUseID: "tool-ask-1",5409 toolUseID: "tool-ask-1",
5177 });5410 });
51785411
...@@ -5299,6 +5532,7 @@ describe("ClaudeAdapterLive", () => {...@@ -5299,6 +5532,7 @@ describe("ClaudeAdapterLive", () => {
52995532
5300 const permissionPromise = canUseTool("AskUserQuestion", askInput, {5533 const permissionPromise = canUseTool("AskUserQuestion", askInput, {
5301 signal: new AbortController().signal,5534 signal: new AbortController().signal,
5535 requestId: "request-8",
5302 toolUseID: "tool-ask-2",5536 toolUseID: "tool-ask-2",
5303 });5537 });
53045538
...@@ -5364,6 +5598,7 @@ describe("ClaudeAdapterLive", () => {...@@ -5364,6 +5598,7 @@ describe("ClaudeAdapterLive", () => {
5364 },5598 },
5365 {5599 {
5366 signal: controller.signal,5600 signal: controller.signal,
5601 requestId: "request-9",
5367 toolUseID: "tool-ask-abort",5602 toolUseID: "tool-ask-abort",
5368 },5603 },
5369 );5604 );
...@@ -5439,6 +5674,7 @@ describe("ClaudeAdapterLive", () => {...@@ -5439,6 +5674,7 @@ describe("ClaudeAdapterLive", () => {
5439 },5674 },
5440 {5675 {
5441 signal: controller.signal,5676 signal: controller.signal,
5677 requestId: "request-10",
5442 toolUseID: "tool-ask-pre-aborted",5678 toolUseID: "tool-ask-pre-aborted",
5443 },5679 },
5444 );5680 );
...@@ -5495,7 +5731,11 @@ describe("ClaudeAdapterLive", () => {...@@ -5495,7 +5731,11 @@ describe("ClaudeAdapterLive", () => {
5495 },5731 },
5496 ],5732 ],
5497 },5733 },
5498 { signal: new AbortController().signal, toolUseID: "tool-ask-stop" },5734 {
5735 signal: new AbortController().signal,
5736 requestId: "request-stop",
5737 toolUseID: "tool-ask-stop",
5738 },
5499 );5739 );
55005740
5501 const requestedEvent = yield* Stream.runHead(adapter.streamEvents);5741 const requestedEvent = yield* Stream.runHead(adapter.streamEvents);
apps/server/src/provider/Layers/ClaudeAdapter.ts+106-9
...@@ -434,10 +434,45 @@ function resultErrorsText(result: SDKResultMessage): string {...@@ -434,10 +434,45 @@ function resultErrorsText(result: SDKResultMessage): string {
434 * so they must never become the error banner.434 * so they must never become the error banner.
435 */435 */
436function resultUserFacingError(result: SDKResultMessage): string | undefined {436function resultUserFacingError(result: SDKResultMessage): string | undefined {
437 if (result.subtype === "success" || !Array.isArray(result.errors)) {437 const listed =
438 return undefined;438 result.subtype === "success" || !Array.isArray(result.errors)
439 ? undefined
440 : result.errors.find((error) => !error.startsWith("[ede_diagnostic]"));
441 if (listed) {
442 return listed;
443 }
444 // Structured failure markers for results whose error list is empty or
445 // diagnostic-only: an overloaded API (529) and the terminal reasons the
446 // CLI stamps when it gives up on a turn.
447 if (isOverloadedResult(result)) {
448 return "Claude API is overloaded (529). Try again shortly.";
449 }
450 switch (result.terminal_reason) {
451 case "api_error":
452 return "Claude gave up after repeated API errors.";
453 case "malformed_tool_use_exhausted":
454 return "Claude gave up after repeated malformed tool calls.";
455 case "budget_exhausted":
456 return "Claude stopped: the turn's token budget was exhausted.";
457 case "structured_output_retry_exhausted":
458 return "Claude could not produce the requested structured output.";
459 case "tool_deferred_unavailable":
460 return "Claude could not resume a deferred tool call: the tool is no longer available.";
461 case "turn_setup_failed":
462 return "Claude could not start the turn.";
463 case "blocking_limit":
464 return "Claude stopped: a usage limit blocked the request.";
465 case "rapid_refill_breaker":
466 return "Claude stopped: the context refilled too quickly after compaction.";
467 case "prompt_too_long":
468 return "Claude stopped: the prompt exceeds the model's context window.";
469 case "image_error":
470 return "Claude stopped: an image in the conversation could not be processed.";
471 case "model_error":
472 return "Claude stopped: the model returned an error.";
473 default:
474 return undefined;
439 }475 }
440 return result.errors.find((error) => !error.startsWith("[ede_diagnostic]"));
441}476}
442477
443function isInterruptedResult(result: SDKResultMessage): boolean {478function isInterruptedResult(result: SDKResultMessage): boolean {
...@@ -1390,7 +1425,43 @@ const buildUserMessageEffect = Effect.fn("buildUserMessageEffect")(function* (...@@ -1390,7 +1425,43 @@ const buildUserMessageEffect = Effect.fn("buildUserMessageEffect")(function* (
1390 return buildUserMessage({ sdkContent });1425 return buildUserMessage({ sdkContent });
1391});1426});
13921427
1428/**
1429 * terminal_reason values the CLI classifies as dead turns: the turn died
1430 * rather than finished, even when the result subtype is success and the
1431 * error list is empty. Kept in sync with the messages in
1432 * resultUserFacingError.
1433 */
1434const FAILED_TERMINAL_REASONS: ReadonlySet<NonNullable<SDKResultMessage["terminal_reason"]>> =
1435 new Set([
1436 "api_error",
1437 "malformed_tool_use_exhausted",
1438 "budget_exhausted",
1439 "structured_output_retry_exhausted",
1440 "tool_deferred_unavailable",
1441 "turn_setup_failed",
1442 "blocking_limit",
1443 "rapid_refill_breaker",
1444 "prompt_too_long",
1445 "image_error",
1446 "model_error",
1447 ]);
1448
1449/**
1450 * The CLI reports repeated 529 overload failures as a success-subtype result
1451 * with api_error_status 529 and an empty error list; the status code is the
1452 * only structured failure signal.
1453 */
1454function isOverloadedResult(result: SDKResultMessage): boolean {
1455 return result.subtype === "success" && result.api_error_status === 529;
1456}
1457
1393function turnStatusFromResult(result: SDKResultMessage): ProviderRuntimeTurnStatus {1458function turnStatusFromResult(result: SDKResultMessage): ProviderRuntimeTurnStatus {
1459 if (
1460 isOverloadedResult(result) ||
1461 (result.terminal_reason !== undefined && FAILED_TERMINAL_REASONS.has(result.terminal_reason))
1462 ) {
1463 return "failed";
1464 }
1394 if (result.subtype === "success") {1465 if (result.subtype === "success") {
1395 return "completed";1466 return "completed";
1396 }1467 }
...@@ -3174,15 +3245,11 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (...@@ -3174,15 +3245,11 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
3174 // Undeclared-but-real subtypes (absent from the SDK's union, so they can't3245 // Undeclared-but-real subtypes (absent from the SDK's union, so they can't
3175 // be switch cases): consumed intentionally without emitting, otherwise3246 // be switch cases): consumed intentionally without emitting, otherwise
3176 // they fall through to the unknown-subtype warning and surface as spurious3247 // they fall through to the unknown-subtype warning and surface as spurious
3177 // error rows in client work logs. `background_tasks_changed` is a roster3248 // error rows in client work logs. `vcs_state_changed`
3178 // snapshot ({tasks: [...]}) — the task_* lifecycle events carry the
3179 // authoritative per-agent data and the typed background_tasks control
3180 // request is the reconciliation source. `vcs_state_changed`
3181 // ({kind: commit|push|rebase}) and `code_change_published`3249 // ({kind: commit|push|rebase}) and `code_change_published`
3182 // ({provider, url, repo}) are informational CLI notices; the work log3250 // ({provider, url, repo}) are informational CLI notices; the work log
3183 // already shows the underlying git/gh tool calls.3251 // already shows the underlying git/gh tool calls.
3184 switch (message.subtype as string) {3252 switch (message.subtype as string) {
3185 case "background_tasks_changed":
3186 case "vcs_state_changed":3253 case "vcs_state_changed":
3187 case "code_change_published":3254 case "code_change_published":
3188 return;3255 return;
...@@ -3497,12 +3564,39 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (...@@ -3497,12 +3564,39 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
3497 return;3564 return;
3498 // Inner protocol/UX details with no T3 surface today — consumed3565 // Inner protocol/UX details with no T3 surface today — consumed
3499 // deliberately so they don't masquerade as unknown-subtype warnings.3566 // deliberately so they don't masquerade as unknown-subtype warnings.
3567 // `background_tasks_changed` is a roster snapshot ({tasks: [...]}); the
3568 // task_* lifecycle events carry the authoritative per-agent data and
3569 // the typed background_tasks control request is the reconciliation
3570 // source. `control_request_progress` is a liveness heartbeat for an
3571 // in-flight control request. `worker_shutting_down` is a Remote
3572 // Control worker notice; the session close path reports the outcome.
3500 case "model_refusal_fallback":3573 case "model_refusal_fallback":
3501 case "local_command_output":3574 case "local_command_output":
3502 case "plugin_install":3575 case "plugin_install":
3503 case "commands_changed":3576 case "commands_changed":
3504 case "memory_recall":3577 case "memory_recall":
3505 case "elicitation_complete":3578 case "elicitation_complete":
3579 case "background_tasks_changed":
3580 case "control_request_progress":
3581 case "worker_shutting_down":
3582 return;
3583 case "informational":
3584 // Transcript-level CLI notes. Only warnings (e.g. a Stop hook that
3585 // refused continuation) warrant a work-log row; info/notice/
3586 // suggestion levels are CLI chrome.
3587 if (message.level === "warning") {
3588 yield* emitRuntimeWarning(context, message.content, message);
3589 }
3590 return;
3591 case "model_refusal_no_fallback":
3592 // The API refused the request and no fallback model was available.
3593 // The terminal result reports the failed turn; this row carries the
3594 // refusal explanation the result's error list lacks.
3595 yield* emitRuntimeWarning(
3596 context,
3597 message.api_refusal_explanation?.trim() || message.content,
3598 message,
3599 );
3506 return;3600 return;
3507 case "permission_denied":3601 case "permission_denied":
3508 yield* offerRuntimeEvent({3602 yield* offerRuntimeEvent({
...@@ -3528,7 +3622,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (...@@ -3528,7 +3622,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
3528 // handled above, so `message` narrows to never here — a new SDK3622 // handled above, so `message` narrows to never here — a new SDK
3529 // release adding a subtype fails this typecheck instead of silently3623 // release adding a subtype fails this typecheck instead of silently
3530 // warning at runtime. The runtime fallback still catches undeclared3624 // warning at runtime. The runtime fallback still catches undeclared
3531 // wire-only subtypes (like background_tasks_changed used to be).3625 // wire-only subtypes (like vcs_state_changed).
3532 message satisfies never;3626 message satisfies never;
3533 const unknownMessage = message as never as { subtype: string };3627 const unknownMessage = message as never as { subtype: string };
3534 yield* emitRuntimeWarning(3628 yield* emitRuntimeWarning(
...@@ -3657,7 +3751,10 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (...@@ -3657,7 +3751,10 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
3657 yield* handleSdkTelemetryMessage(context, message);3751 yield* handleSdkTelemetryMessage(context, message);
3658 return;3752 return;
3659 // Composer prompt suggestions have no T3 surface; consumed deliberately.3753 // Composer prompt suggestions have no T3 surface; consumed deliberately.
3754 // `conversation_reset` announces a CLI-side conversation id swap
3755 // (e.g. /clear); T3 keeps its own thread identity and resume cursor.
3660 case "prompt_suggestion":3756 case "prompt_suggestion":
3757 case "conversation_reset":
3661 return;3758 return;
3662 default: {3759 default: {
3663 // Exhaustiveness guard (see handleSystemMessage): new SDK top-level3760 // Exhaustiveness guard (see handleSystemMessage): new SDK top-level
pnpm-lock.yaml+5-5
...@@ -476,8 +476,8 @@ importers:...@@ -476,8 +476,8 @@ importers:
476 apps/server:476 apps/server:
477 dependencies:477 dependencies:
478 '@anthropic-ai/claude-agent-sdk':478 '@anthropic-ai/claude-agent-sdk':
479 specifier: ^0.3.170479 specifier: ^0.3.260
480 version: 0.3.170(@anthropic-ai/sdk@0.93.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3)480 version: 0.3.260(@anthropic-ai/sdk@0.93.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3)
481 '@effect/platform-bun':481 '@effect/platform-bun':
482 specifier: 4.0.0-beta.103482 specifier: 4.0.0-beta.103
483 version: 4.0.0-beta.103(bufferutil@4.1.0)(effect@4.0.0-beta.103(patch_hash=af36b7948b6f9c56623074662b51dade5699880c1a7c71245de73e13c3185fb6))(utf-8-validate@6.0.6)483 version: 4.0.0-beta.103(bufferutil@4.1.0)(effect@4.0.0-beta.103(patch_hash=af36b7948b6f9c56623074662b51dade5699880c1a7c71245de73e13c3185fb6))(utf-8-validate@6.0.6)
...@@ -1011,8 +1011,8 @@ packages:...@@ -1011,8 +1011,8 @@ packages:
1011 '@alchemy.run/node-utils@0.0.5':1011 '@alchemy.run/node-utils@0.0.5':
1012 resolution: {integrity: sha512-5agdhQxWBodxa5hDRyjnpx91RTU3g+qd5fxYB7uNDCaOzB0XC47UU/KHR6zf6jhz/NXf61gJS+vhYwn8NHeRoQ==}1012 resolution: {integrity: sha512-5agdhQxWBodxa5hDRyjnpx91RTU3g+qd5fxYB7uNDCaOzB0XC47UU/KHR6zf6jhz/NXf61gJS+vhYwn8NHeRoQ==}
10131013
1014 '@anthropic-ai/claude-agent-sdk@0.3.170':1014 '@anthropic-ai/claude-agent-sdk@0.3.260':
1015 resolution: {integrity: sha512-pAvhfk+iTodXZ6RF18Kz7BEUWFjL7EcR3tKuhUNdPpE1NAYCR3mSHGbafi72JsrNwKEDIs7FU31z3fqhwy8QzA==}1015 resolution: {integrity: sha512-PmABtP4Rwd6l95itQrqzguv6rS9uACqikPB9g8BPeWRKZOpy3xpEOjJLYauof3BFk2wNZnfhr0Ttx8ttcZzq0w==}
1016 engines: {node: '>=18.0.0'}1016 engines: {node: '>=18.0.0'}
1017 peerDependencies:1017 peerDependencies:
1018 '@anthropic-ai/sdk': '>=0.93.0'1018 '@anthropic-ai/sdk': '>=0.93.0'
...@@ -10527,7 +10527,7 @@ snapshots:...@@ -10527,7 +10527,7 @@ snapshots:
1052710527
10528 '@alchemy.run/node-utils@0.0.5': {}10528 '@alchemy.run/node-utils@0.0.5': {}
1052910529
10530 '@anthropic-ai/claude-agent-sdk@0.3.170(@anthropic-ai/sdk@0.93.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3)':10530 '@anthropic-ai/claude-agent-sdk@0.3.260(@anthropic-ai/sdk@0.93.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3)':
10531 dependencies:10531 dependencies:
10532 '@anthropic-ai/sdk': 0.93.0(zod@4.4.3)10532 '@anthropic-ai/sdk': 0.93.0(zod@4.4.3)
10533 '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3)10533 '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3)
pnpm-workspace.yaml+1
...@@ -90,6 +90,7 @@ minimumReleaseAgeExclude:...@@ -90,6 +90,7 @@ minimumReleaseAgeExclude:
90 - expo-updates@57.0.1990 - expo-updates@57.0.19
91 - expo@57.0.1891 - expo@57.0.18
92 - expo-widgets@57.0.1592 - expo-widgets@57.0.15
93 - "@anthropic-ai/claude-agent-sdk@0.3.260"
9394
94overrides:95overrides:
95 # The SDK always receives the user's Claude executable, so its bundled binaries are unused.96 # The SDK always receives the user's Claude executable, so its bundled binaries are unused.