From 57fd6f897f7b0fe74c893e168c3acca86f2a6e82 Mon Sep 17 00:00:00 2001 From: d3oxy Date: Wed, 29 Jul 2026 19:30:43 +0530 Subject: [PATCH] fix(claude): classify stop diagnostics as interrupted --- .../src/provider/Layers/ClaudeAdapter.test.ts | 57 +++++++++++++++++++ .../src/provider/Layers/ClaudeAdapter.ts | 52 +++++++++++++---- 2 files changed, 99 insertions(+), 10 deletions(-) diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.test.ts b/apps/server/src/provider/Layers/ClaudeAdapter.test.ts index 760f0e7fbab..a1655e3c311 100644 --- a/apps/server/src/provider/Layers/ClaudeAdapter.test.ts +++ b/apps/server/src/provider/Layers/ClaudeAdapter.test.ts @@ -3959,4 +3959,61 @@ describe("ClaudeAdapterLive", () => { Effect.provide(harness.layer), ); }); + + it.effect( + "treats a diagnostic-only Claude result after an explicit interrupt as interrupted", + () => { + const harness = makeHarness(); + return Effect.gen(function* () { + const adapter = yield* ClaudeAdapter; + + const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 6).pipe( + Stream.runCollect, + Effect.forkChild, + ); + + const session = yield* adapter.startSession({ + threadId: THREAD_ID, + provider: ProviderDriverKind.make("claudeAgent"), + runtimeMode: "full-access", + }); + + const turn = yield* adapter.sendTurn({ + threadId: session.threadId, + input: "hello", + attachments: [], + }); + + yield* adapter.interruptTurn(session.threadId, turn.turnId); + + harness.query.emit({ + type: "result", + subtype: "error_during_execution", + is_error: true, + errors: ["[ede_diagnostic] result_type=user last_content_type=n/a stop_reason=tool_use"], + stop_reason: "tool_use", + session_id: "sdk-session-diagnostic-interrupt", + uuid: "result-diagnostic-interrupt", + } as unknown as SDKMessage); + + const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); + assert.equal(harness.query.interruptCalls.length, 1); + assert.notInclude( + runtimeEvents.map((event) => event.type), + "runtime.error", + ); + + const turnCompleted = runtimeEvents[runtimeEvents.length - 1]; + assert.equal(turnCompleted?.type, "turn.completed"); + if (turnCompleted?.type === "turn.completed") { + assert.equal(String(turnCompleted.turnId), String(turn.turnId)); + assert.equal(turnCompleted.payload.state, "interrupted"); + assert.equal(turnCompleted.payload.stopReason, "tool_use"); + } + }).pipe( + Effect.provideService(Random.Random, makeDeterministicRandomService()), + Effect.provide(harness.layer), + ); + }, + ); }); diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.ts b/apps/server/src/provider/Layers/ClaudeAdapter.ts index f87d5be7446..4927d3cae81 100644 --- a/apps/server/src/provider/Layers/ClaudeAdapter.ts +++ b/apps/server/src/provider/Layers/ClaudeAdapter.ts @@ -201,6 +201,7 @@ interface ClaudeSessionContext { lastKnownTotalProcessedTokens: number | undefined; lastAssistantUuid: string | undefined; lastThreadStartedId: string | undefined; + interruptRequestedTurnId: TurnId | undefined; stopped: boolean; } @@ -303,18 +304,31 @@ function resultErrorsText(result: SDKResultMessage): string { : ""; } -function isInterruptedResult(result: SDKResultMessage): boolean { +function isClaudeExecutionDiagnosticOnlyResult(result: SDKResultMessage): boolean { + return ( + result.subtype === "error_during_execution" && + "errors" in result && + result.errors.length === 1 && + result.errors[0]?.startsWith("[ede_diagnostic]") === true + ); +} + +function isInterruptedResult( + result: SDKResultMessage, + explicitInterruptRequested = false, +): boolean { const errors = resultErrorsText(result); if (errors.includes("interrupt")) { return true; } return ( - result.subtype === "error_during_execution" && - result.is_error === false && - (errors.includes("request was aborted") || - errors.includes("interrupted by user") || - errors.includes("aborted")) + (result.subtype === "error_during_execution" && + result.is_error === false && + (errors.includes("request was aborted") || + errors.includes("interrupted by user") || + errors.includes("aborted"))) || + (explicitInterruptRequested && isClaudeExecutionDiagnosticOnlyResult(result)) ); } @@ -994,13 +1008,16 @@ const buildUserMessageEffect = Effect.fn("buildUserMessageEffect")(function* ( return buildUserMessage({ sdkContent }); }); -function turnStatusFromResult(result: SDKResultMessage): ProviderRuntimeTurnStatus { +function turnStatusFromResult( + result: SDKResultMessage, + explicitInterruptRequested = false, +): ProviderRuntimeTurnStatus { if (result.subtype === "success") { return "completed"; } const errors = resultErrorsText(result); - if (isInterruptedResult(result)) { + if (isInterruptedResult(result, explicitInterruptRequested)) { return "interrupted"; } if (errors.includes("cancel")) { @@ -2553,7 +2570,11 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( return; } - const status = turnStatusFromResult(message); + const explicitInterruptRequested = + context.interruptRequestedTurnId !== undefined && + context.interruptRequestedTurnId === context.turnState?.turnId; + context.interruptRequestedTurnId = undefined; + const status = turnStatusFromResult(message, explicitInterruptRequested); const errorMessage = message.subtype === "success" ? undefined : message.errors[0]; if (status === "failed") { @@ -3640,6 +3661,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( lastKnownTotalProcessedTokens: undefined, lastAssistantUuid: resumeState?.resumeSessionAt, lastThreadStartedId: undefined, + interruptRequestedTurnId: undefined, stopped: false, }; yield* Ref.set(contextRef, context); @@ -3825,10 +3847,20 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( const interruptTurn: ClaudeAdapterShape["interruptTurn"] = Effect.fn("interruptTurn")( function* (threadId, _turnId) { const context = yield* requireSession(threadId); + const interruptRequestedTurnId = context.turnState?.turnId; + context.interruptRequestedTurnId = interruptRequestedTurnId; yield* Effect.tryPromise({ try: () => context.query.interrupt(), catch: (cause) => toRequestError(threadId, "turn/interrupt", cause), - }); + }).pipe( + Effect.tapError(() => + Effect.sync(() => { + if (context.interruptRequestedTurnId === interruptRequestedTurnId) { + context.interruptRequestedTurnId = undefined; + } + }), + ), + ); }, );