From 1636c798c06dae686a5a44786b7054fda566f503 Mon Sep 17 00:00:00 2001 From: "altimate-factory[bot]" Date: Sun, 8 Nov 2026 16:51:35 +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 | 12 +++++++++ packages/opencode/src/session/prompt.ts | 2 ++ .../test/telemetry/user-rejection.test.ts | 27 +++++++++++++++++++ 3 files changed, 41 insertions(+) create mode 100644 packages/opencode/test/telemetry/user-rejection.test.ts diff --git a/packages/opencode/src/altimate/telemetry/error-detail.ts b/packages/opencode/src/altimate/telemetry/error-detail.ts index 3d2ef5e78d..8b1effe62b 100644 --- a/packages/opencode/src/altimate/telemetry/error-detail.ts +++ b/packages/opencode/src/altimate/telemetry/error-detail.ts @@ -400,6 +400,18 @@ export function describeToolError(raw: string) { } } +// Messages of PermissionNext.RejectedError and CorrectedError, which the processor stores as `error.toString()`. +const USER_REJECTED = "Error: The user rejected permission to use this specific tool call." +const USER_CORRECTED = "Error: The user rejected permission to use this specific tool call with the following feedback: " + +/** + * True only when the user answered a permission prompt with "reject": an expected decision, not a tool failure. + * Config-rule denials and every other permission failure still return false and keep being reported. + */ +export function isUserRejection(raw: string): boolean { + return raw === USER_REJECTED || raw.startsWith(USER_CORRECTED) +} + export function buildErrorFingerprintEvent( sessionID: string, err: { diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 11a455551d..6cf73bc0e9 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -2289,6 +2289,8 @@ export namespace SessionPrompt { // altimate_change end if (toolChain.length < 50) toolChain.push(part.tool) const isError = part.state?.status === "error" + // A user rejecting a permission prompt is their decision, not a tool failure: no fingerprint, no recovery link + if (part.state.status === "error" && typeof part.state.error === "string" && ErrorDetail.isUserRejection(part.state.error)) continue if (isError) { toolErrorCount++ // Flush previous unrecovered error before recording new one diff --git a/packages/opencode/test/telemetry/user-rejection.test.ts b/packages/opencode/test/telemetry/user-rejection.test.ts new file mode 100644 index 0000000000..291bfa9ea9 --- /dev/null +++ b/packages/opencode/test/telemetry/user-rejection.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, test } from "bun:test" +import { ErrorDetail } from "../../src/altimate/telemetry/error-detail" +import { PermissionNext } from "../../src/permission/next" + +// The processor stores a failed tool call's error as `error.toString()`; these are the strings prompt.ts sees. +const rejected = new PermissionNext.RejectedError().toString() +const corrected = new PermissionNext.CorrectedError("use grep instead").toString() +const denied = new PermissionNext.DeniedError([{ permission: "glob", pattern: "*", action: "deny" }]).toString() + +describe("ErrorDetail.isUserRejection", () => { + test("is the reported glob tool_error fingerprint", () => { + expect(ErrorDetail.templateErrorMessage(rejected)).toBe( + "error the user rejected permission to use this tool call", + ) + }) + + test("matches a user rejecting a permission prompt, with or without feedback", () => { + expect(ErrorDetail.isUserRejection(rejected)).toBe(true) + expect(ErrorDetail.isUserRejection(corrected)).toBe(true) + }) + + test("keeps reporting config denials and other permission failures", () => { + expect(ErrorDetail.isUserRejection(denied)).toBe(false) + expect(ErrorDetail.isUserRejection("Error: EACCES: permission denied, scandir '/root'")).toBe(false) + expect(ErrorDetail.isUserRejection("Error: unknown")).toBe(false) + }) +})