Repository navigation
fix(pixel): a Playground refusal that trips the run fuse still admits the write it asked for, once - #6766
Open
Lightheartdevs wants to merge 3 commits into
Open
fix(pixel): a Playground refusal that trips the run fuse still admits the write it asked for, once#6766Lightheartdevs wants to merge 3 commits into
Lightheartdevs wants to merge 3 commits into
Conversation
…tripped The Playground router's refusals before the first project file (the run-nothing-before-the-first-write gate and the project-path refusal) run nothing and name their remedy: a write that binds the project. They stay charged failures, so four of them in a row still trip the run-progress consecutive-failure fuse, which then refused the very write they asked for (fleet photo-renamer on build bfb62a2: call 7 blocked, run ended PARTIAL). The router now marks those two refusals correctable by kind. When such a refusal's charge trips the global consecutive fuse, and neither the total cap nor both lanes are reached, the budget leaves one corrected attempt pending, at most once per kind per run. The first tool call after the fuse spends it: only a write the router will bind as the new project (bindsNewProject), sent before any tool boundary delivered the finalization instruction, re-opens the response. Its counters are kept, so that write's failure stops the response at once and only its success resets the consecutive count. Any other first call gets exactly the stop it gets today. The round cap clears a pending correction; the total cap, lanes, external stops, tool-call caps and wall clock are unchanged. While a correction pends, the tripping refusal's transcript copy keeps its text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds the Tool Search route to the same remedy write, an owner cancel after the fuse (routing off, so nothing is admitted and the stop is as on main), and a lane-classified refusal while the global count stands at the fuse after an admission (never granted). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Moves pendingCorrection and correctFuse after beginModelRound, so a change that adds a method after laneExhausted (#6748) merges without a conflict. No behavior change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR relaxes the consecutive-failure fuse in one narrow case
The run-progress fuse (
RUN_PROGRESS_LIMITS.consecutiveFailures: 4) still trips at exactly the same failure as before. What changes is what happens next, in one case only:In that case, that one call runs. Every other first call gets exactly the stop it gets on main.
Why: the router refuses commands until the first project file exists, and the refusal says so ("Not run: write the first project file before running commands"). Those refusals are charged as failures, and they still are. But once four of them trip the fuse, the fuse refuses the write the refusals asked for. The model follows the instruction and is stopped for following it.
Evidence
Fleet sweep
claude-open-prompts, build bfb62a2, prompt 07 "write a python script that renames all photos in a folder by date taken, and test it". Qwen3.6-35B-A3B GGUF on a Windows WSL target, session b121a8f7 (fixturetests/fixtures/photo-renamer-bfb62a2b.json). Graded PARTIAL: 8 calls, 7 failures, no file written.python3 --version && python3 -c "from PIL …"python3 --versionwrite Playground/photo-renamer/rename_photos.py(5853 chars)Replayed against main's router, call 7 is accepted as written and binds
Playground/photo-renamer. Only the fuse blocked it.Change
plugin/playground-projects.mjscorrectable: <kind>:'playground-first-write': the NOT_RUN gate for exec and apply_patch;'playground-project-path': the write refused for a bare, generic or unusable path.bindsNewProject({state, tool, params, root, existingPaths})is a pure check. It is true only when the router would reserve and bind a new project for this call, which means all of:tool_call(Tool Search);Playground/<non-generic name>/…, optionally under/workspace/or the configured root;playground/<name>/…or<name>/<file>, which the router also binds, are not admitted.plugin/run-progress-budget.mjsobserveResult({..., correctableRefusal})charges the failure as always. A correction pends only when all of these hold:pendingCorrectionexposes the pending kind.correctFuse(remedy)is called with the first tool call after the trip. It always spends the pending correction. Whenremedyis true it re-opens the response and keeps every counter:beginModelRound()clears a pending correction when the round cap fires, andstop()clears it too.RUN_PROGRESS_LIMITSis unchanged.plugin/progress-finalization.mjs:disarm()returns the phase frompendingtoidle.pendingmeans no tool boundary has delivered the instruction yet, so the model never saw the stop.disarm()does nothing in any other phase.plugin/tool-loop-guard.mjsdecideToolCall, while a correction pends,admitRefusalRemedydecides the first call. It admits the call only when all of these hold:pending;tool_search_code:child;playgroundRoutingActive(state));bindsNewProjectis true.correctFuse(true)anddisarm(), and the call then goes through every normal check, routing included. Anything else callscorrectFuse(false), and main's exhausted branch runs unchanged.after_tool_callortool_result_persist). Only{block, blockReason}reaches the runtime.Why "binds", then "succeeds": the router reserves the project folder and saves the binding in
before_tool_call, so whether the write binds is known before it runs, and admission requires it. The write's own result then decides the rest. A success resets the consecutive count, so the ordinary fuse applies from zero. A failure is charged on top of the tripped count and stops the response immediately.Bounds kept
unavailable.prepareclears the marker.Overlap with #6749 (
claude/budget-one-corrected-attempt, open, conflicting with main)Same idea. Both PRs deal with a failure that ran or measured nothing and whose result already gives the model its correction, and give it one corrected attempt. All four
RUN_PROGRESS_LIMITSstay unchanged in both.Different mechanism:
The router's remedy can be identified before it runs (a write the router will bind), so the stricter form is possible here. An inspection's correction can only be judged by its result, which is why #6749 needs the deferral.
Naming: the vocabulary is reused (correctable, corrected attempt), but the parameter is
correctableRefusal(a kind string), so it does not collide with #6749's booleancorrectable/corrected. The grant is once per kind, not a per-run count likeRUN_PROGRESS_CORRECTED_ATTEMPTS.Composition: in
observeResult's failure branch, run #6749's deferral andsettle()first, then this PR's trip-time recording. At both call sites, pass both parameters.Tests
tests/refusal_remedy_fuse.test.mjsadds 14 tests. The guard tests replay real hook order: model call start and end,before_tool_call, thenafter_tool_callandtool_result_persist. The replays run in both result-hook orders where that matters.The recorded trace (calls 1–8):
Playground/photo-renamer. The run continues: call 8 revises the file, andpython3 rename_photos.py --helpgets the project cwd. No abort happens.tool_call {id:'openclaw:core:write'}) is admitted the same way.A model that keeps ignoring refusals is still fused at the same bounds:
PROGRESS_FINALIZATION_INSTRUCTION. The late write then getsRUN_PROGRESS_STOP_REASONand one abort. This is main's sequence, byte for byte.Playground/<file>;playground/spelling, or a path outside the workspace;mkdiror read;tool_search_code:child.A refusal followed by a different failing tool still counts:
lstrip the fuse with no correction.No double grant (budget unit tests):
stop()leave no correction.Property test. 400 seeded random traces compare a budget that gets correctable kinds with one that does not:
Other unit tests: the router marks exactly the two refusals,
bindsNewProjectagrees with the router's actual binding over 12 path forms (direct and wrapped), anddisarm()works only frompending.Red check. On main 5afd46c with only
bindsNewProjectadded (so the file imports), 10 of 14 tests fail, all as expected. The 4 that pass are the "stop exactly as on main" tests: ignore, forfeited turn, owner cancel, and other first calls.Mutation checks. Each of these 14 mutations fails at least one test:
pendingphase check, the nested-ID check or the routing-active check;disarm()or the transcript-copy exception;correctableto the runtime;Full Linux pixel-agent suite (
node --test tests/*.test.mjson a Linux host from agit archiveof each ref):The skip count (38) is identical on all three.
Conflicts with open PRs (
git merge-treeagainst this head)tool-loop-guard.mjs. Keep both: this PR addsbindsNewProjectto the playground-projects import, and fix(pixel): let the turn continue after an in-project rm -rf is refused #6722 adds its recursive-delete-scope import below it.run-progress-budget.mjsand 2 intool-loop-guard.mjs, atobserveResultand its two call sites. Composition is described above.vendor/pixel.bundle), fix(pixel): answer after an unchanged file and check preview bytes when a result is saved #6732;/<name>/<file>routing; its rooted spelling is not an admitted remedy here);Not covered
🤖 Generated with Claude Code