diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.test.ts b/apps/server/src/provider/Layers/ClaudeAdapter.test.ts index 8697505ef24..62ba0d5bac5 100644 --- a/apps/server/src/provider/Layers/ClaudeAdapter.test.ts +++ b/apps/server/src/provider/Layers/ClaudeAdapter.test.ts @@ -1450,6 +1450,67 @@ describe("ClaudeAdapterLive", () => { ); }); + it.effect("treats aborted_tools results as interrupted and hides ede_diagnostic errors", () => { + 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: [], + }); + + // Exact shape the CLI emits when Stop lands mid-tool-call: is_error + // is true and the only error is internal diagnostic telemetry. + 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", + terminal_reason: "aborted_tools", + session_id: "sdk-session-abort-tools", + uuid: "result-abort-tools", + } as unknown as SDKMessage); + + const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); + assert.deepEqual( + runtimeEvents.map((event) => event.type), + [ + "session.started", + "session.configured", + "session.state.changed", + "turn.started", + "thread.started", + "turn.completed", + ], + ); + + 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.errorMessage, undefined); + } + }).pipe( + Effect.provideService(Random.Random, makeDeterministicRandomService()), + Effect.provide(harness.layer), + ); + }); + it.effect("interruptTurn stops every live task before interrupting the turn", () => { const harness = makeHarness(); return Effect.gen(function* () { diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.ts b/apps/server/src/provider/Layers/ClaudeAdapter.ts index 812a7310928..9db864d1ba2 100644 --- a/apps/server/src/provider/Layers/ClaudeAdapter.ts +++ b/apps/server/src/provider/Layers/ClaudeAdapter.ts @@ -348,7 +348,29 @@ function resultErrorsText(result: SDKResultMessage): string { : ""; } +/** + * First user-facing error from a non-success result. "[ede_diagnostic] ..." + * entries are CLI-internal telemetry (the CLI hides them from its own UI too), + * so they must never become the error banner. + */ +function resultUserFacingError(result: SDKResultMessage): string | undefined { + if (result.subtype === "success" || !Array.isArray(result.errors)) { + return undefined; + } + return result.errors.find((error) => !error.startsWith("[ede_diagnostic]")); +} + function isInterruptedResult(result: SDKResultMessage): boolean { + // The CLI stamps user aborts explicitly: interrupting mid-tool-call yields + // "aborted_tools" (with an internal "[ede_diagnostic] ..." error and + // is_error: true), interrupting mid-stream yields "aborted_streaming". + if ( + result.terminal_reason === "aborted_tools" || + result.terminal_reason === "aborted_streaming" + ) { + return true; + } + const errors = resultErrorsText(result); if (errors.includes("interrupt")) { return true; @@ -2919,7 +2941,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( } const status = turnStatusFromResult(message); - const errorMessage = message.subtype === "success" ? undefined : message.errors[0]; + const errorMessage = resultUserFacingError(message); if (status === "failed") { yield* emitRuntimeError(context, errorMessage ?? "Claude turn failed.");