diff --git a/packages/opencode/src/altimate/telemetry/error-detail.ts b/packages/opencode/src/altimate/telemetry/error-detail.ts index a424f6c52a..bc86028eb1 100644 --- a/packages/opencode/src/altimate/telemetry/error-detail.ts +++ b/packages/opencode/src/altimate/telemetry/error-detail.ts @@ -400,6 +400,18 @@ export function describeToolError(raw: string) { } } +/** Error the processor records on a tool part still pending or running when its generation closes. */ +export const TOOL_ABORTED_ERROR = "Tool execution aborted" + +/** + * A tool part closed as aborted after the session's own abort signal fired (Stop/Esc, dispose) + * is a requested cancellation, already recorded as the session's "aborted" outcome; it is not a + * tool failure. The same error without a session abort is unexpected and is still reported. + */ +export function isRequestedToolAbort(error: unknown, sessionAborted: boolean): boolean { + return sessionAborted && error === TOOL_ABORTED_ERROR +} + export function buildErrorFingerprintEvent( sessionID: string, err: { diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index bcbbabca6a..0d7f7655a2 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -27,6 +27,7 @@ import { PartID } from "./schema" import type { SessionID, MessageID } from "./schema" // altimate_change start — import Telemetry for per-generation token tracking import { Telemetry } from "@/altimate/telemetry" +import { ErrorDetail } from "@/altimate/telemetry/error-detail" // altimate_change — shared aborted-tool error // altimate_change end // altimate_change start — write-starvation breaker + loop detection (fork-only // modules) and the run-mode flag that gates armed behavior. @@ -80,7 +81,7 @@ async function interruptedToolPart(part: MessageV2.ToolPart, capTokens: number): }, } } - return { ...part, state: { ...state, status: "error", error: "Tool execution aborted", metadata, time } } + return { ...part, state: { ...state, status: "error", error: ErrorDetail.TOOL_ABORTED_ERROR, metadata, time } } } // altimate_change end diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index f6e2c0c0f8..d7d65e3c8f 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -2194,6 +2194,9 @@ export namespace SessionPrompt { lastToolName = part.tool // altimate_change end if (toolChain.length < 50) toolChain.push(part.tool) + // altimate_change start — a tool the session's own abort interrupted is neither a failure nor a recovery + if (part.state?.status === "error" && ErrorDetail.isRequestedToolAbort(part.state.error, abort.aborted)) continue + // altimate_change end const isError = part.state?.status === "error" if (isError) { toolErrorCount++ diff --git a/packages/opencode/test/telemetry/requested-tool-abort.test.ts b/packages/opencode/test/telemetry/requested-tool-abort.test.ts new file mode 100644 index 0000000000..2aa7ba090d --- /dev/null +++ b/packages/opencode/test/telemetry/requested-tool-abort.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, test } from "bun:test" +import { ErrorDetail } from "../../src/altimate/telemetry/error-detail" + +const { TOOL_ABORTED_ERROR, describeToolError, isRequestedToolAbort } = ErrorDetail + +describe("isRequestedToolAbort", () => { + test("the aborted-tool error is the reported tool_error fingerprint", () => { + expect(describeToolError(TOOL_ABORTED_ERROR)).toEqual({ + errorHash: "18cc068fbdbecb19", + errorMessage: "tool aborted", + }) + }) + + test("excludes a tool the session's own abort interrupted", () => { + expect(isRequestedToolAbort(TOOL_ABORTED_ERROR, true)).toBe(true) + }) + + test("still reports an aborted tool when the session did not request the abort", () => { + expect(isRequestedToolAbort(TOOL_ABORTED_ERROR, false)).toBe(false) + }) + + test("still reports other tool errors after a session abort", () => { + expect(isRequestedToolAbort("oldString not found in content", true)).toBe(false) + expect(isRequestedToolAbort(undefined, true)).toBe(false) + }) +})