Skip to content
Open
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
13 changes: 13 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,19 @@ export function describeToolError(raw: string) {
}
}

/**
* A running tool closed as interrupted after the session's own abort signal fired
* (Stop/Esc, dispose) is a requested cancellation, already reported as the session's
* "aborted" outcome, not a tool failure. Interruptions the session did not request
* (stream error, stall) are still fingerprinted.
*/
export function isRequestedInterruption(
state: { status: string; metadata?: Record<string, unknown> } | undefined,
sessionAborted: boolean,
): boolean {
return sessionAborted && state?.status === "error" && state.metadata?.interrupted === true
}

export function buildErrorFingerprintEvent(
sessionID: string,
err: {
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 @@ -2194,6 +2194,8 @@ export namespace SessionPrompt {
lastToolName = part.tool
// altimate_change end
if (toolChain.length < 50) toolChain.push(part.tool)
// a tool the user's own abort cut short is not a tool error
if (ErrorDetail.isRequestedInterruption(part.state, abort.aborted)) continue
const isError = part.state?.status === "error"
if (isError) {
toolErrorCount++
Expand Down
41 changes: 41 additions & 0 deletions packages/opencode/test/telemetry/requested-interruption.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { describe, expect, test } from "bun:test"
import fs from "fs"
import path from "path"
import { ErrorDetail } from "../../src/altimate/telemetry/error-detail"

// The processor closes still-running tools with this exact state when a generation ends.
const interrupted = { status: "error", error: "Tool execution aborted", metadata: { interrupted: true } }

describe("isRequestedInterruption", () => {
test("user-requested abort of a running tool is not a tool error", () => {
expect(ErrorDetail.isRequestedInterruption(interrupted, true)).toBe(true)
})

test("interruption the session did not request is still reported", () => {
expect(ErrorDetail.isRequestedInterruption(interrupted, false)).toBe(false)
})

test("ordinary tool errors are still reported after an abort", () => {
expect(ErrorDetail.isRequestedInterruption({ status: "error", metadata: {} }, true)).toBe(false)
expect(ErrorDetail.isRequestedInterruption({ status: "error" }, true)).toBe(false)
expect(ErrorDetail.isRequestedInterruption({ status: "completed", metadata: { interrupted: true } }, true)).toBe(
false,
)
expect(ErrorDetail.isRequestedInterruption(undefined, true)).toBe(false)
})

test("the reported fingerprint is the interrupted-tool error", () => {
expect(ErrorDetail.describeToolError(interrupted.error)).toEqual({
errorHash: "18cc068fbdbecb19",
errorMessage: "tool <x> aborted",
})
})

test("prompt loop skips requested interruptions before recording error fingerprints", () => {
const src = fs.readFileSync(path.join(__dirname, "../../src/session/prompt.ts"), "utf8")
const guard = src.indexOf("ErrorDetail.isRequestedInterruption(part.state, abort.aborted)) continue")
const record = src.indexOf("...ErrorDetail.describeToolError(errorMsg)")
expect(guard).toBeGreaterThan(-1)
expect(record).toBeGreaterThan(guard)
})
})
Loading