diff --git a/packages/opencode/src/altimate/telemetry/error-detail.ts b/packages/opencode/src/altimate/telemetry/error-detail.ts index 3d2ef5e78d..c47539a7bd 100644 --- a/packages/opencode/src/altimate/telemetry/error-detail.ts +++ b/packages/opencode/src/altimate/telemetry/error-detail.ts @@ -400,6 +400,14 @@ export function describeToolError(raw: string) { } } +/** + * A tool part the processor closed because the session itself was aborted (user cancel) is + * not a tool failure. Interrupts the session did not request are still reported. + */ +export function isRequestedToolAbort(state: { status: string; metadata?: Record }, sessionAborted: boolean) { + return sessionAborted && state.status === "error" && state.metadata?.interrupted === true +} + export function buildErrorFingerprintEvent( sessionID: string, err: { diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index ecedc18104..1769fa92ba 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -2241,6 +2241,8 @@ export namespace SessionPrompt { lastToolName = part.tool // altimate_change end if (toolChain.length < 50) toolChain.push(part.tool) + // altimate_change — a tool interrupted by a requested session abort is neither an error nor a recovery + if (ErrorDetail.isRequestedToolAbort(part.state, abort.aborted)) continue const isError = part.state?.status === "error" if (isError) { toolErrorCount++ diff --git a/packages/opencode/test/telemetry/error-detail-requested-abort.test.ts b/packages/opencode/test/telemetry/error-detail-requested-abort.test.ts new file mode 100644 index 0000000000..11206b6553 --- /dev/null +++ b/packages/opencode/test/telemetry/error-detail-requested-abort.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, test } from "bun:test" +import fs from "fs" +import path from "path" +import { ErrorDetail } from "../../src/altimate/telemetry/error-detail" + +// Regression: cancelling a session while sql_execute ran reported "tool aborted" +// error_fingerprint events, though the tool itself never failed. +describe("isRequestedToolAbort", () => { + const interrupted = { status: "error", error: "Tool execution aborted", metadata: { interrupted: true } } + + test("interrupted part of a requested session abort is excluded", () => { + expect(ErrorDetail.isRequestedToolAbort(interrupted, true)).toBe(true) + expect(ErrorDetail.templateErrorMessage(interrupted.error)).toBe("tool aborted") + }) + + test("interrupts the session did not request are still reported", () => { + expect(ErrorDetail.isRequestedToolAbort(interrupted, false)).toBe(false) + }) + + test("real tool errors during an aborted session are still reported", () => { + expect(ErrorDetail.isRequestedToolAbort({ status: "error", metadata: {} }, true)).toBe(false) + expect(ErrorDetail.isRequestedToolAbort({ status: "error" }, true)).toBe(false) + expect(ErrorDetail.isRequestedToolAbort({ status: "completed", metadata: { interrupted: true } }, true)).toBe(false) + }) + + test("prompt loop skips requested aborts before recording tool errors", () => { + const src = fs.readFileSync(path.join(__dirname, "../../src/session/prompt.ts"), "utf8") + const skip = src.indexOf("ErrorDetail.isRequestedToolAbort(part.state, abort.aborted)") + const record = src.indexOf('const isError = part.state?.status === "error"') + expect(skip).toBeGreaterThan(-1) + expect(record).toBeGreaterThan(skip) + }) +})