Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/opencode/src/session/processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,14 @@ export namespace SessionProcessor {
export function shouldTrackStreamingError(error: unknown, sessionAborted: boolean): boolean {
return !(sessionAborted && MessageV2.AbortedError.isInstance(error))
}

// The same rule for tool parts: an unfinished tool closed by interruptedToolPart
// after the session's abort signal fired is the requested cancellation, not a
// tool failure. Interruptions the session did not request are still reported.
/** @internal Exported for focused telemetry tests. */
export function shouldTrackToolError(part: MessageV2.ToolPart, sessionAborted: boolean): boolean {
return !(sessionAborted && part.state.status === "error" && part.state.metadata?.interrupted === true)
}
// altimate_change end

// A provider may repeat one malformed raw tool-call id for concurrent calls.
Expand Down
2 changes: 2 additions & 0 deletions packages/opencode/src/session/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2195,6 +2195,8 @@ export namespace SessionPrompt {
// altimate_change end
if (toolChain.length < 50) toolChain.push(part.tool)
const isError = part.state?.status === "error"
// A tool the user's own abort interrupted did not fail; it is neither an error nor a recovery.
if (isError && !SessionProcessor.shouldTrackToolError(part, abort.aborted)) continue
if (isError) {
toolErrorCount++
// Flush previous unrecovered error before recording new one
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// A user's Stop/Esc closes every unfinished tool part as "Tool execution aborted"
// with metadata.interrupted. Those parts were reported as `error_fingerprint`
// tool errors ("tool <x> aborted" on warehouse_test). Only interruptions the
// session requested are excluded; every other tool error is still reported.
import { describe, expect, test } from "bun:test"
import type { MessageV2 } from "../../src/session/message-v2"
import { SessionProcessor } from "../../src/session/processor"
import { ErrorDetail } from "../../src/altimate/telemetry/error-detail"
import { Telemetry } from "../../src/altimate/telemetry"

// interruptedToolPart (src/session/processor.ts) is the only producer of this tool error.
const INTERRUPTED_ERROR = "Tool execution aborted"

describe("reported warehouse_test fingerprint", () => {
test("is exactly the fingerprint of an interrupted tool part", () => {
expect(ErrorDetail.describeToolError(INTERRUPTED_ERROR)).toEqual({
errorHash: "18cc068fbdbecb19",
errorMessage: "tool <x> aborted",
})
expect(Telemetry.classifyError(INTERRUPTED_ERROR)).toBe("unknown")
})
})

function toolPart(state: Record<string, unknown>): MessageV2.ToolPart {
return {
id: "prt_1",
sessionID: "ses_1",
messageID: "msg_1",
type: "tool",
callID: "call_1",
tool: "warehouse_test",
state: { input: { name: "wh" }, time: { start: 1, end: 2 }, ...state },
} as unknown as MessageV2.ToolPart
}

const interrupted = toolPart({ status: "error", error: "Tool execution aborted", metadata: { interrupted: true } })

describe("SessionProcessor.shouldTrackToolError", () => {
test("a tool interrupted by the session's own abort is not tracked", () => {
expect(SessionProcessor.shouldTrackToolError(interrupted, true)).toBe(false)
})

test("a tool interrupted without a requested abort is still tracked", () => {
expect(SessionProcessor.shouldTrackToolError(interrupted, false)).toBe(true)
})

test("a genuine tool failure is tracked even after the session aborted", () => {
const failed = toolPart({ status: "error", error: "connection refused", metadata: {} })
expect(SessionProcessor.shouldTrackToolError(failed, true)).toBe(true)
expect(SessionProcessor.shouldTrackToolError(failed, false)).toBe(true)
})
})
Loading