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
12 changes: 12 additions & 0 deletions packages/opencode/src/altimate/telemetry/error-detail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
3 changes: 2 additions & 1 deletion packages/opencode/src/session/processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand Down
3 changes: 3 additions & 0 deletions packages/opencode/src/session/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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++
Expand Down
26 changes: 26 additions & 0 deletions packages/opencode/test/telemetry/requested-tool-abort.test.ts
Original file line number Diff line number Diff line change
@@ -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 <x> 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)
})
})
Loading