From 23b450b143c07a83f99bba3bceecfdcc60e71886 Mon Sep 17 00:00:00 2001 From: "altimate-factory[bot]" Date: Mon, 19 Oct 2026 02:09:38 +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 | 14 ++++++++++ packages/opencode/src/session/prompt.ts | 2 ++ packages/opencode/src/tool/tool.ts | 3 +++ .../user-rejection-not-tool-error.test.ts | 26 +++++++++++++++++++ 4 files changed, 45 insertions(+) create mode 100644 packages/opencode/test/telemetry/user-rejection-not-tool-error.test.ts diff --git a/packages/opencode/src/altimate/telemetry/error-detail.ts b/packages/opencode/src/altimate/telemetry/error-detail.ts index dc761dd3dc..29a00ae2eb 100644 --- a/packages/opencode/src/altimate/telemetry/error-detail.ts +++ b/packages/opencode/src/altimate/telemetry/error-detail.ts @@ -400,6 +400,20 @@ export function describeToolError(raw: string) { } } +// Exact messages of PermissionNext/PermissionV1 RejectedError and CorrectedError and of +// Question.RejectedError, optionally behind the "Name: " prefix that toString() adds. +const USER_REJECTION = + /^(?:\w+: )?(?:The user rejected permission to use this specific tool call(?:\.$| with the following feedback: )|The user dismissed this question$)/ + +/** + * True when a tool "error" is the user declining a permission prompt or dismissing a + * question. That is a user decision, not a tool failure, so it is kept out of tool error + * telemetry. Config-rule denials and every other permission failure still count. + */ +export function isUserRejection(raw: string): boolean { + return USER_REJECTION.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 1c137dc78d..fc251f61f8 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -2172,6 +2172,8 @@ export namespace SessionPrompt { lastToolName = part.tool // altimate_change end if (toolChain.length < 50) toolChain.push(part.tool) + // A declined permission prompt is the user's decision: neither a tool error nor a recovery. + if (part.state?.status === "error" && ErrorDetail.isUserRejection(String(part.state.error))) continue const isError = part.state?.status === "error" if (isError) { toolErrorCount++ diff --git a/packages/opencode/src/tool/tool.ts b/packages/opencode/src/tool/tool.ts index c7fa9f7c35..989e0b4c87 100644 --- a/packages/opencode/src/tool/tool.ts +++ b/packages/opencode/src/tool/tool.ts @@ -8,6 +8,7 @@ import { Agent } from "@/agent/agent" // altimate_change start — telemetry instrumentation + legacy zod-tool adapter import z from "zod" import { Telemetry } from "../altimate/telemetry" +import { ErrorDetail } from "../altimate/telemetry/error-detail" // altimate_change // altimate_change start — debug mode tool tracing import { traceToolCall } from "@/altimate/debug/mode" // altimate_change end @@ -165,6 +166,8 @@ export type InferDef = function altimateTrackError(id: string, args: unknown, ctx: Context, startTime: number, error: unknown) { try { const errorMsg = error instanceof Error ? error.message : String(error) + // The user declining a permission prompt is not a tool failure. + if (ErrorDetail.isUserRejection(errorMsg)) return const maskedErrorMsg = Telemetry.maskString(errorMsg).slice(0, 500) Telemetry.track({ type: "tool_call", diff --git a/packages/opencode/test/telemetry/user-rejection-not-tool-error.test.ts b/packages/opencode/test/telemetry/user-rejection-not-tool-error.test.ts new file mode 100644 index 0000000000..e60c8cf84c --- /dev/null +++ b/packages/opencode/test/telemetry/user-rejection-not-tool-error.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, test } from "bun:test" +import { PermissionNext } from "../../src/permission/next" +import { Question } from "../../src/question" +import { ErrorDetail } from "../../src/altimate/telemetry/error-detail" + +// Telemetry 55e58557cee2275d: declined bash permission prompts were reported as +// error_fingerprint tool errors with error_class "permission". +describe("isUserRejection", () => { + test.each([ + ["permission rejected (stored part error)", new PermissionNext.RejectedError().toString()], + ["permission rejected (message)", new PermissionNext.RejectedError().message], + ["permission corrected", new PermissionNext.CorrectedError("use dbt run instead").toString()], + ["question dismissed", new Question.RejectedError().message], + ])("excludes %s", (_, raw) => { + expect(ErrorDetail.isUserRejection(raw)).toBe(true) + }) + + test.each([ + ["config rule denial", new PermissionNext.DeniedError([]).toString()], + ["filesystem permission denied", "Error: EACCES: permission denied, open '/etc/x'"], + ["warehouse permission", "Insufficient privileges: permission denied for table orders"], + ["rejection text embedded later", "bash failed: The user rejected permission to use this specific tool call."], + ])("still reports %s", (_, raw) => { + expect(ErrorDetail.isUserRejection(raw)).toBe(false) + }) +})