Skip to content

[Bug]: tool_decision always reports reject with empty source and tool_name=unknown #122

Description

@naylrush

Description

When a user answers a permission prompt, tool_decision reports the same result no matter what
they chose: decision: "reject", source: "", tool_name: "unknown", tool_type: "unknown".
Approvals and denials are indistinguishable, so no accept/reject ratio can be derived from
OpenCode telemetry.

handlePermissionReplied reads response from the event payload:

const { permissionID, sessionID, response } = e.properties
const pending = ctx.pendingPermissions.get(permissionID)
const decision = response === "allow" || response === "allowAlways" ? "accept" : "reject"

The field name is correct — @opencode-ai/sdk 1.18.7 declares
EventPermissionReplied.properties as { sessionID: string; permissionID: string; response: string }.
But response arrives undefined, so the strict comparison is false and the code falls through
to reject unconditionally. Separately pendingPermissions never holds the entry, so
pending?.title ?? "unknown" yields unknown — the preceding permission.updated seems not to
reach the plugin, or carries a different id.

This is not a recent regression: the same logic is unchanged from 1.2.2 through 1.5.1.

I suspect the empty response originates in the core rather than in this plugin, since the plugin
reads exactly what the SDK type declares. Neither of these core issues matches the symptom, but
they are the closest I found: anomalyco/opencode#28037 (closed — plugin permission replies
dropped via separate memoMaps) and anomalyco/opencode#34853 (open — serializing prompt settlement
around event publication). Happy to re-test a patched build, or to refile upstream if that is the
right place.

Steps to reproduce

  1. Configure the plugin with any OTLP collector endpoint.
  2. Run opencode INTERACTIVELY. Headless opencode run auto-approves and emits no
    tool_decision at all, so the bug is invisible there.
  3. Ask it to run a command that raises a permission prompt, e.g.
    "run in bash: rm -rf /tmp/nonexistent-test-dir". In my case the prompt raised was
    external_directory for /tmp/*, not the bash command itself.
  4. DENY the prompt.
  5. Repeat the same request and APPROVE the prompt.
  6. Inspect the collector output and compare the two tool_decision events.

Expected behavior

The denied prompt emits decision: "reject" and the approved one emits decision: "accept",
with source carrying the actual reply (allow / allowAlways / reject) and tool_name
carrying the permission title rather than unknown.

Actual behavior

Both events are identical — the approval is indistinguishable from the denial:

{
  "event.name": "tool_decision",
  "decision": "reject",
  "source": "",
  "tool_name": "unknown",
  "tool_type": "unknown",
  "agent.name": "build",
  "agent.type": "primary"
}

source is not an empty string: it arrives as an OTLP attribute with an empty value object,
i.e. the underlying value is undefined.

Aggregated over three weeks of real usage this yields 100% reject across every tool_decision
row, which reads as "users reject everything" unless you know it is an artifact.

Plugin version

1.5.1 (also reproduced on 1.4.0)

Configuration

{
  "$schema": "https://opencode.ai/config.json",
  "plugin": [
    ["@devtheops/opencode-plugin-otel", {
      "enabled": true,
      "endpoint": "http://127.0.0.1:4317",
      "protocol": "grpc",
      "resourceAttributes": "service.namespace=ai-agent,repo=test",
      "spanAttributes": "service.namespace=ai-agent,repo=test",
      "disabledTraces": ["session", "llm", "tool"]
    }]
  ]
}

Environment: opencode 1.18.20 (Homebrew, opencode-ai npm tarball), @opencode-ai/sdk 1.18.7
bundled in the plugin, macOS 15 arm64, collector otelcol-contrib 0.157.0 over OTLP/gRPC.

Relevant log output

Core log for the same session, showing the prompt actually being raised (note that the bash
command itself was auto-allowed and only the directory access was asked about):

level=INFO message=evaluated permission=external_directory pattern=/tmp/* action.permission=external_directory action.pattern=* action.action=ask
level=INFO message=asking id=per_051eb5da0001BZX7321ZVVAePu permission=external_directory patterns=["/tmp/*"]
level=INFO message=evaluated permission=external_directory pattern=/tmp/* action.permission=external_directory action.pattern=* action.action=ask
level=INFO message=asking id=per_051eb77580019Gz7g07LC8kOGB permission=external_directory patterns=["/tmp/*"]
level=INFO message=evaluated permission=bash pattern="rm -rf /tmp/nonexistent-test-dir" action.permission=* action.action=allow action.pattern=*

Both prompts above were answered — one denied, one approved — and both produced the identical
reject event shown in "Actual behavior".

Checklist

  • I have searched existing issues to ensure this hasn't been reported
  • I have redacted any sensitive information (API keys, tokens, etc.)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions