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
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 thetoolsmap 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 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
toolsmap. 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-messagetoolsmap. That includes agents whose permissions denyedit/write(explore, reviewer, verifier, gate-runner), and none of their first messages carryedit: false. Agent and session permissions live on the agent config andsession.permission, which is exactly whatresolveToolPermissionDeniedreads. They are not written intomessage.info.tools.So for a session whose agent denies
ctx_reduce: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.callable: trueagain.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.
resolveToolPermissionDeniedis async while the freeze path is sync, so the read likely has to happen atsession.created(or on the first pass), with the freeze waiting for it the same way the provisionalfrozen: falseverdict already defers system-prompt-hash persistence. A mid-session permission change would stay log-only as it is today.Verification status
da0cf178aopencode.dbon one host (OpenCode 1.x)