Skip to content

ctx_reduce availability ignores OpenCode agent/session permissions, so a session that can't call ctx_reduce still gets §N§ prefixes and ctx_reduce nudges #519

Description

@iceteaSA

Refs are against master da0cf178a; the resolver is unchanged from v0.42.6.

The gate

The §N§ prefix, Channel 1, Channel 2, and the reduce guidance all gate on one verdict, resolveToolAvailabilityFromMessages (ctx-reduce-availability.ts:130-159). It is derived only from the tools map on the session's first user message (verdictFromToolsMap(message.info.tools, toolName) ?? true, :150), fails open to callable, and is then frozen.

The file says the live permission rules are deliberately excluded:

The ctx_reduce verdict intentionally remains frozen even when OpenCode's live permission rules deny the tool. (:23-28)

The live read (resolveToolPermissionDenied, :336) only feeds a log on cache-busting passes: "ctx_reduce permission is denied by OpenCode; frozen guidance remains until session restart" (transform-postprocess-phase.ts:1416-1434).

Why that leaves the deny path uncovered

The freeze rationale is sound: a verdict that flips mid-session would bust the prefix. But the design assumes a permission deny shows up in the first-message tools map. On OpenCode it doesn't. Measured on a live host over the last 14 days: 894 subagent sessions, 0 with a non-empty first-user-message tools map. That includes agents whose permissions deny edit/write (explore, reviewer, verifier, gate-runner), and none of their first messages carry edit: false. Agent and session permissions live on the agent config and session.permission, which is exactly what resolveToolPermissionDenied reads. They are not written into message.info.tools.

So for a session whose agent denies ctx_reduce:

  • OpenCode removes the tool, and the model cannot call it.
  • MC resolves callable: true (empty map means no signal, which fails open), freezes it, and keeps adding §N§ prefixes, the reduce guidance, and Channel 1/2 "make a ctx_reduce pass" nudges. That is the cargo-cult case the resolver's header says it exists to prevent.
  • The log's "until session restart" doesn't hold for this case. A restart, or a fresh session, re-derives from the same empty map and lands on callable: true again.

The first-message map only covers an explicit per-prompt tools map, and OpenCode's task tool isn't sending one here (0/894).

Suggested shape

Fold the permission read into the one-time freeze instead of keeping it log-only. The agent and session permissions are fixed at spawn, so evaluating them once when the verdict freezes keeps the no-flap property. resolveToolPermissionDenied is async while the freeze path is sync, so the read likely has to happen at session.created (or on the first pass), with the freeze waiting for it the same way the provisional frozen: false verdict already defers system-prompt-hash persistence. A mid-session permission change would stay log-only as it is today.

Verification status

  • Resolver and log path: read at source on master da0cf178a
  • 894/894 empty first-message tools maps: measured in opencode.db on one host (OpenCode 1.x)
  • That a denied session receives nudges: inferred from the two facts above, not reproduced with a deny config

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions