authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-08-06 20:55:20-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-06 17:55:20-07:00
logc471145e96fc2399b4df57ce78c72aa7fb7ccabf
tree2e4f1f7b372a2180fffa527abfa0e32b014789c3
parent64a991ad455234e6fdd808a5a3caadc51f18a152
signature Signed by PGP key B5690EEEBB952194

fix(server): stopping a Claude thread no longer shows an ede_diagnostic error (#5557)

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

2 files changed, 84 insertions(+), 1 deletions(-)

apps/server/src/provider/Layers/ClaudeAdapter.test.ts+61
...@@ -1450,6 +1450,67 @@ describe("ClaudeAdapterLive", () => {...@@ -1450,6 +1450,67 @@ describe("ClaudeAdapterLive", () => {
1450 );1450 );
1451 });1451 });
14521452
1453 it.effect("treats aborted_tools results as interrupted and hides ede_diagnostic errors", () => {
1454 const harness = makeHarness();
1455 return Effect.gen(function* () {
1456 const adapter = yield* ClaudeAdapter;
1457
1458 const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 6).pipe(
1459 Stream.runCollect,
1460 Effect.forkChild,
1461 );
1462
1463 const session = yield* adapter.startSession({
1464 threadId: THREAD_ID,
1465 provider: ProviderDriverKind.make("claudeAgent"),
1466 runtimeMode: "full-access",
1467 });
1468
1469 const turn = yield* adapter.sendTurn({
1470 threadId: session.threadId,
1471 input: "hello",
1472 attachments: [],
1473 });
1474
1475 // Exact shape the CLI emits when Stop lands mid-tool-call: is_error
1476 // is true and the only error is internal diagnostic telemetry.
1477 harness.query.emit({
1478 type: "result",
1479 subtype: "error_during_execution",
1480 is_error: true,
1481 errors: ["[ede_diagnostic] result_type=user last_content_type=n/a stop_reason=tool_use"],
1482 stop_reason: "tool_use",
1483 terminal_reason: "aborted_tools",
1484 session_id: "sdk-session-abort-tools",
1485 uuid: "result-abort-tools",
1486 } as unknown as SDKMessage);
1487
1488 const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber));
1489 assert.deepEqual(
1490 runtimeEvents.map((event) => event.type),
1491 [
1492 "session.started",
1493 "session.configured",
1494 "session.state.changed",
1495 "turn.started",
1496 "thread.started",
1497 "turn.completed",
1498 ],
1499 );
1500
1501 const turnCompleted = runtimeEvents[runtimeEvents.length - 1];
1502 assert.equal(turnCompleted?.type, "turn.completed");
1503 if (turnCompleted?.type === "turn.completed") {
1504 assert.equal(String(turnCompleted.turnId), String(turn.turnId));
1505 assert.equal(turnCompleted.payload.state, "interrupted");
1506 assert.equal(turnCompleted.payload.errorMessage, undefined);
1507 }
1508 }).pipe(
1509 Effect.provideService(Random.Random, makeDeterministicRandomService()),
1510 Effect.provide(harness.layer),
1511 );
1512 });
1513
1453 it.effect("interruptTurn stops every live task before interrupting the turn", () => {1514 it.effect("interruptTurn stops every live task before interrupting the turn", () => {
1454 const harness = makeHarness();1515 const harness = makeHarness();
1455 return Effect.gen(function* () {1516 return Effect.gen(function* () {
apps/server/src/provider/Layers/ClaudeAdapter.ts+23-1
...@@ -348,7 +348,29 @@ function resultErrorsText(result: SDKResultMessage): string {...@@ -348,7 +348,29 @@ function resultErrorsText(result: SDKResultMessage): string {
348 : "";348 : "";
349}349}
350350
351/**
352 * First user-facing error from a non-success result. "[ede_diagnostic] ..."
353 * entries are CLI-internal telemetry (the CLI hides them from its own UI too),
354 * so they must never become the error banner.
355 */
356function resultUserFacingError(result: SDKResultMessage): string | undefined {
357 if (result.subtype === "success" || !Array.isArray(result.errors)) {
358 return undefined;
359 }
360 return result.errors.find((error) => !error.startsWith("[ede_diagnostic]"));
361}
362
351function isInterruptedResult(result: SDKResultMessage): boolean {363function isInterruptedResult(result: SDKResultMessage): boolean {
364 // The CLI stamps user aborts explicitly: interrupting mid-tool-call yields
365 // "aborted_tools" (with an internal "[ede_diagnostic] ..." error and
366 // is_error: true), interrupting mid-stream yields "aborted_streaming".
367 if (
368 result.terminal_reason === "aborted_tools" ||
369 result.terminal_reason === "aborted_streaming"
370 ) {
371 return true;
372 }
373
352 const errors = resultErrorsText(result);374 const errors = resultErrorsText(result);
353 if (errors.includes("interrupt")) {375 if (errors.includes("interrupt")) {
354 return true;376 return true;
...@@ -2919,7 +2941,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (...@@ -2919,7 +2941,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
2919 }2941 }
29202942
2921 const status = turnStatusFromResult(message);2943 const status = turnStatusFromResult(message);
2922 const errorMessage = message.subtype === "success" ? undefined : message.errors[0];2944 const errorMessage = resultUserFacingError(message);
29232945
2924 if (status === "failed") {2946 if (status === "failed") {
2925 yield* emitRuntimeError(context, errorMessage ?? "Claude turn failed.");2947 yield* emitRuntimeError(context, errorMessage ?? "Claude turn failed.");