Skip to content

Stop readiness permission loops / 阻止 readiness 权限循环#6073

Merged
SivanCola merged 2 commits into
esengine:main-v2from
SivanCola:fix/readiness-permission-loop
Jul 6, 2026
Merged

Stop readiness permission loops / 阻止 readiness 权限循环#6073
SivanCola merged 2 commits into
esengine:main-v2from
SivanCola:fix/readiness-permission-loop

Conversation

@SivanCola

@SivanCola SivanCola commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Prevent final-readiness recovery from turning permission, plan-mode, hook, or loop-guard blocks into repeated tool-request loops.
  • Extend the existing storm breaker so repeated blocked tool outcomes are counted, not just execution errors.
  • Let final-readiness accept a final blocker report after a loop guard fired, tracked as host state instead of message text.
  • Add a blocked-turn streak detector so rotating tools, reordering batches, or blockers whose text varies per attempt still trip the guard.

Cache-impact: low - Only recovery-turn synthetic guidance and loop-guard result text change; system prompt, tool schemas, tool ordering, provider request serialization, and compaction behavior are unchanged.
Cache-guard: scripts/check-cache-impact.sh metadata gate; go test ./internal/agent -run 'TestFinalReadinessPermissionLoopGuardAllowsBlockedFinal|TestFinalReadinessPermissionLoopGuardAllowsBlockedFinalForBatch|TestStormBreakerEscalatesRepeatedBlockedPermission|TestStormBreakerEscalatesAlternatingBlockedShapes|TestStormBreakerBlockedStreakResetBySuccess|TestFinalReadinessAllowsFinalAfterLoopGuardedToolBlocker|TestFinalReadinessLoopGuardPassSurvivesBookkeeping|TestFinalReadinessLoopGuardPassRevokedByRealProgress|TestFinalReadinessIgnoresLoopGuardQuotedInToolOutput'; go test ./internal/evidence -run 'TestLedgerHasWriteOrCommandSince'; go test ./internal/agent ./internal/permission ./internal/control; go test ./...; git diff --check.

Problem and cause

After a write plus todo_write, final-answer readiness blocks a final response until host-observable receipts prove the work is complete. If the model tries to satisfy that by running bash, a permission denial, plan-mode refusal, or hook block returns a blocked: tool result.

The previous storm breaker ignored blocked outcomes, so each changed command or argument reset loop detection even when every attempt hit the same host refusal. In practice this could surface as repeated bash approval requests while the task never completed. After a loop guard did appear, final-readiness could still ask for receipts that could no longer be produced, causing another retry instead of letting the model report the blocker.

Review of the first iteration found three residual gaps in its "latest tool result contains [loop guard]" detection:

  • The guard text lands on a batch's first result, but readiness only inspected the last tool message, so multi-call blocked batches never unlocked the final blocker report.
  • Any tool output that merely quoted [loop guard] (a grep over this repo, a pasted transcript) unlocked readiness by accident.
  • Following the guard's own advice (ask, todo_write, complete_step) made a newer tool result the latest one, revoking the pass and restarting the retry loop. Separately, a model rotating between two blocked shapes reset the signature detector every turn and never tripped the guard at all.

Fix

  • Include blocked outcomes in the storm signature, keyed by tool name and host response rather than tool arguments. Different bash commands that hit the same permission blocker now trip the loop guard; successes and different host responses remain reset points.
  • Add a blocked-turn streak detector alongside the signature: three consecutive turns in which every call was blocked trip the guard regardless of tool rotation, batch order, or blocker text variance. Only host refusals count — plain errors keep legitimate varied probing untouched.
  • Track the readiness loop-guard pass as host state armed by the storm breaker itself (including the per-call repeat-success guard), not by scanning message text. The pass survives the bookkeeping the guard recommends and is revoked once a successful write or command receipt lands after the guard, since receipts are then obtainable again (new Ledger.Len / Ledger.HasWriteOrCommandSince).
  • Tighten the final-readiness retry instruction so the model prefers complete_step and todo_write from existing receipts and does not run exploratory bash commands just to satisfy readiness.

Tests

  • go test ./internal/agent -run 'TestFinalReadinessPermissionLoopGuardAllowsBlockedFinal|TestFinalReadinessPermissionLoopGuardAllowsBlockedFinalForBatch|TestStormBreakerEscalatesRepeatedBlockedPermission|TestStormBreakerEscalatesAlternatingBlockedShapes|TestStormBreakerBlockedStreakResetBySuccess|TestFinalReadinessAllowsFinalAfterLoopGuardedToolBlocker|TestFinalReadinessLoopGuardPassSurvivesBookkeeping|TestFinalReadinessLoopGuardPassRevokedByRealProgress|TestFinalReadinessIgnoresLoopGuardQuotedInToolOutput'
  • go test ./internal/evidence -run 'TestLedgerHasWriteOrCommandSince'
  • go test ./internal/agent ./internal/permission ./internal/control
  • go test ./...
  • git diff --check

@SivanCola SivanCola requested a review from esengine as a code owner July 6, 2026 07:01
@github-actions github-actions Bot added v2 Go rewrite (1.x) — main-v2 branch, active development agent Core agent loop (internal/agent, internal/control) labels Jul 6, 2026
- Replace the readiness loop-guard text scan with host state armed by the
  storm breaker itself: a guard on any result of a multi-call batch now
  unlocks the final blocker report, and tool output that merely quotes
  "[loop guard]" no longer does.
- Keep the pass across the bookkeeping the guard recommends (ask,
  todo_write, complete_step); revoke it once a successful write or command
  receipt proves receipts are obtainable again.
- Add a blocked-turn streak detector so rotating tools, reordering batches,
  or blockers whose text varies per attempt still trip the guard after
  three consecutive fully-blocked turns.
@SivanCola SivanCola merged commit 909bea3 into esengine:main-v2 Jul 6, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent Core agent loop (internal/agent, internal/control) v2 Go rewrite (1.x) — main-v2 branch, active development

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant