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
14 changes: 14 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,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: {
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 @@ -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++
Expand Down
3 changes: 3 additions & 0 deletions packages/opencode/src/tool/tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -165,6 +166,8 @@ export type InferDef<T> =
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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
})
})
Loading