From 7b2176f66c6514283fbd49f99528c5c2f7a375ff Mon Sep 17 00:00:00 2001 From: "altimate-factory[bot]" Date: Wed, 4 Nov 2026 18:50:01 +0000 Subject: [PATCH] fix: automated fix in packages/opencode/src/altimate/telemetry Factory lane: altimate_code. Agent-Authored-By: altimate-factory --- .../src/altimate/telemetry/error-detail.ts | 11 +++++++ packages/opencode/src/session/prompt.ts | 15 ++++++---- .../permission-rejection-fingerprint.test.ts | 30 +++++++++++++++++++ 3 files changed, 50 insertions(+), 6 deletions(-) create mode 100644 packages/opencode/test/telemetry/permission-rejection-fingerprint.test.ts diff --git a/packages/opencode/src/altimate/telemetry/error-detail.ts b/packages/opencode/src/altimate/telemetry/error-detail.ts index 3d2ef5e78d..5b18409df5 100644 --- a/packages/opencode/src/altimate/telemetry/error-detail.ts +++ b/packages/opencode/src/altimate/telemetry/error-detail.ts @@ -400,6 +400,17 @@ export function describeToolError(raw: string) { } } +/** + * True when the tool error is the user declining its permission prompt (with or without + * feedback). That is the user's choice, not a tool failure; rule-based denials and every + * other permission error still count. + */ +export function isUserPermissionRejection(raw: string): boolean { + return /^(?:[\w.]+: )?The user rejected permission to use this specific tool call(?:\.|\s+with the following feedback: )/.test( + raw, + ) +} + export function buildErrorFingerprintEvent( sessionID: string, err: { diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index bd832d27ab..31f44f9f31 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -2295,12 +2295,15 @@ export namespace SessionPrompt { lastToolWasError = true const errorMsg = part.state.status === "error" && typeof part.state.error === "string" ? part.state.error : "unknown" - pendingError = { - toolName: part.tool, - toolCategory, - errorClass: Telemetry.classifyError(errorMsg), - ...ErrorDetail.describeToolError(errorMsg), - } + // a declined permission prompt is the user's choice, not a tool failure to fingerprint + pendingError = ErrorDetail.isUserPermissionRejection(errorMsg) + ? null + : { + toolName: part.tool, + toolCategory, + errorClass: Telemetry.classifyError(errorMsg), + ...ErrorDetail.describeToolError(errorMsg), + } } else { if (lastToolWasError && pendingError) { errorRecoveryCount++ diff --git a/packages/opencode/test/telemetry/permission-rejection-fingerprint.test.ts b/packages/opencode/test/telemetry/permission-rejection-fingerprint.test.ts new file mode 100644 index 0000000000..8bf0e2be94 --- /dev/null +++ b/packages/opencode/test/telemetry/permission-rejection-fingerprint.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, test } from "bun:test" +import { PermissionV1 } from "@opencode-ai/core/v1/permission" +import { PermissionNext } from "../../src/permission/next" +import { ErrorDetail } from "../../src/altimate/telemetry/error-detail" + +const { isUserPermissionRejection } = ErrorDetail + +// The processor stores `error.toString()` on the tool part; prompt.ts fingerprints that string. +describe("user permission rejections are not fingerprinted as tool errors", () => { + test("rejected and corrected prompts are user rejections", () => { + expect(isUserPermissionRejection(new PermissionNext.RejectedError().toString())).toBe(true) + expect(isUserPermissionRejection(new PermissionNext.CorrectedError("use dbt instead").toString())).toBe(true) + expect(isUserPermissionRejection(String(new PermissionV1.RejectedError()))).toBe(true) + expect(isUserPermissionRejection(String(new PermissionV1.CorrectedError({ feedback: "no" })))).toBe(true) + }) + + test("rule denials and other permission failures are still reported", () => { + expect(isUserPermissionRejection(new PermissionNext.DeniedError([]).toString())).toBe(false) + expect(isUserPermissionRejection(String(new PermissionV1.DeniedError({ ruleset: [] })))).toBe(false) + expect(isUserPermissionRejection("Error: EACCES: permission denied, open '/usr/lib/driver'")).toBe(false) + expect(isUserPermissionRejection("pip failed: The user rejected permission to use this specific tool call.")).toBe( + false, + ) + }) + + test("prompt.ts skips the fingerprint only for user rejections", async () => { + const src = await Bun.file(new URL("../../src/session/prompt.ts", import.meta.url)).text() + expect(src).toContain("pendingError = ErrorDetail.isUserPermissionRejection(errorMsg)") + }) +})