Skip to content

Defer a host-guided inspection failure that would stop the run for exactly one corrected attempt, with every hard bound kept - #6749

Open
Lightheartdevs wants to merge 2 commits into
mainfrom
claude/budget-one-corrected-attempt
Open

Lightheartdevs wants to merge 2 commits into
mainfrom
claude/budget-one-corrected-attempt

Conversation

@Lightheartdevs

@Lightheartdevs Lightheartdevs commented Sep 26, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

The run-progress budget charges every failed inspection the same way. That includes results that measured nothing and already tell the model how to fix the check:

A website-only task has no progress lane, because workspaceExtensionIsolated makes toolProgressLane return undefined. These failures therefore hit the global fuse of 4 consecutive failures. So the result that hands the model its fix can be the one that ends the run, before the model ever sends the fix.

Evidence from fleet task ods-main-qualification-20260924-r049 on tower2. website_create_artifact_preview_interaction failed on merged main d4a61f3 (round 107) on laptop, strixy and mac-mini, while all three towers passed:

Scope: this PR alone does not fix the r107 website regression

Replayed through the real guard hooks, this PR rescues none of the recorded failing sessions:

In those runs, the failing results did not hand the model a usable fix. The invalid_request text is generic and sends the model to tool_describe through tool_call, and no_match names no candidate locator. Those texts are changed in sibling PRs, not here:

This PR is the budget half. When such a result arrives at the fuse, the run stays alive for the one attempt that uses it. The classifier covers exactly the categories #6748 makes ready-to-send, plus #6740's INCOMPLETE. No model-facing string changes here.

Change

plugin/run-progress-budget.mjs, observeResult({..., correctable, corrected}). Both flags are optional and default to false, so every existing caller behaves exactly as before.

A correctable failure waits only when its consecutive (or lane) charge would stop the run or the lane, i.e. that counter is at 3. Two more conditions apply:

  • no other charge is already waiting;
  • fewer than RUN_PROGRESS_CORRECTED_ATTEMPTS (2) charges have waited in this run.

Below the fuse, a correctable failure is charged at once and spends no allowance.

While a charge waits:

  • The total cap counts the failure at once and is never held back.
  • The next failure of any tool charges its own counter and then the waiting charge.
  • The next non-discovery success charges the waiting charge first, unless corrected is set: a passing inspection in the same lane (or globally) forgives it. So an unrelated read or a running-process receipt stops the run one result after main.
  • Discovery results and free corrections neither charge nor forgive the waiting charge, as on main between failures. Model rounds keep advancing, so the unchanged 8-rounds-without-progress cap bounds them.

The terminal expression is unchanged. It moved into a settle() helper.

plugin/preview-interaction-assurance.mjs

  • correctableInspectionFailure(result) requires isError: true and details.kind === INSPECTION_KIND, plus one of:
    • invalid_request with status 'failed';
    • status 'incomplete' with transition_untested;
    • status 'failed' with no top-level errorCode, no pageErrors, an empty blockedRequests, and a last step of no_match, selector_not_unique or invalid_selector.
  • passedInspection(result) is true only for an inspection receipt that is not isError, has status 'passed' and has no page errors.

plugin/tool-loop-guard.mjs

  • after_tool_call:
    • It passes correctable only for a pending run bound as the inspection tool in this run.
    • It passes corrected for a non-failed direct result or validated Tool Search envelope whose inspection passed.
    • Nested tool_search_code: IDs are never observed, as before.
  • recordPreviewInspectionResult(toolCallId, params, result) keeps the tool's own result on the exact pending call: direct, or the pending tool_call parent of a Tool Search child. It uses the same binding as previewInspectionTransition, now shared as boundPreviewInspectionRuns.
  • tool_result_persist classifies the recorded result when there is one, else the persisted copy. It also counts a Tool Search inspection whose failure is inside the envelope (persisted isError: false).

plugin/index.js wraps the registered inspection tool's execute to call recordPreviewInspectionResult with the result it returns. The tool itself is unchanged.

Why the tool result is recorded

OpenClaw 2026.6.33 caps persisted details above 8 KiB (MAX_PERSISTED_TOOL_RESULT_DETAILS_BYTES) before tool_result_persist runs. after_tool_call is dispatched fire-and-forget and can run after persistence.

In that order, a capped receipt could not be classified and was charged at once, while the other order deferred it. The tool returns before either hook runs, so its recorded result makes both orders account the same way. Without a record, for example a tool not registered through index.js, the old fail-closed behavior remains.

Deviations from the fix spec

  1. The limit is its own export, RUN_PROGRESS_CORRECTED_ATTEMPTS = 2, not a fifth key in RUN_PROGRESS_LIMITS. The existing test "an external stop is sticky and changes no limit" deep-equals that object to the four limits.
  2. Allowances are spent only at the fuse, and only a passing inspection forgives. These are review fixes. The spec's wording deferred every correctable failure and forgave on any success. That spent both allowances on deferrals that could change nothing, and it let an unrelated success keep a run alive.
  3. One existing test gained an assertion. In inspection_transition_coverage.test.mjs, "the registered inspection tool asks the run guard about exactly its own call" runs the index.js registration block against a fake guard. The fake now also has recordPreviewInspectionResult, and the test asserts that it receives exactly ('call-1', args, result). Its existing assertions are unchanged.
  4. The persist path counts a Tool Search inspection failure inside the envelope. Otherwise, with persistence first, after_tool_call could no longer bind the inspection and charged it normally, and strixy stopped at Rename to Android Framework — update all references #11. With persistence first, the transcript copy of a stopping Tool Search inspection now gets the stop text too. The after-first order already did this on main (strixy entry 27). The model's live context is unaffected.

Safety invariants

  • RUN_PROGRESS_LIMITS is unchanged: consecutive 4, total 12, 8 rounds without progress, 2 identical successes. Every failure counts toward the total of 12 at once, and the total cap is never deferred.
  • At most one charge waits at a time, and at most 2 per run.
  • Without a passing inspection, a run stops exactly one non-discovery result after it would have stopped on main. The property test checks this at every step against a reference copy of the previous budget. Discovery and free corrections in between are bounded by the unchanged rounds cap.
  • visibility_mismatch, click_failed, unstable, page errors, blocked requests, unavailable, timeout, cancelled, output_limit, blocked receipts and every non-inspection tool are never correctable. A pass with page errors never forgives.
  • Stops stay sticky. The seenCalls dedupe and its cap are unchanged. Nested tool_search_code: IDs and unbound calls get no allowance.
  • No model-facing string changed. No proof, finalization or delivery path is touched: a deferral changes only when the budget stops a run, and it never marks an inspection passed or a site verified. The recorded result is used only for budget classification.

Tests

  • tests/run_progress_budget.test.mjs: 12 new tests.
    • Classifier tables for correctableInspectionFailure and passedInspection.
    • INCOMPLETE at consecutive 3 waits for exactly one attempt:
      • a pass resets consecutive to 0;
      • a failure, an unrelated read or a running-process receipt stops the run.
    • Below the fuse, correctable failures spend nothing:
      • the C S C S F F F INCOMPLETE shape keeps the run going;
      • a streak of correctable failures gets exactly one more attempt than the fuse, whichever consecutive count (0 to 3) it starts from.
    • Two correctable failures in a row at the fuse are charged together.
    • A third deferral is not granted.
    • The total cap still stops at 12, including a correctable failure 12 at consecutive 3.
    • Discovery and free corrections neither charge nor forgive; after them, a failure or an unrelated success stops the run and a passing inspection does not.
    • A lane charge is forgiven only by a passing inspection in the same lane. An unrelated success in either lane, a pass in the other lane, or a failure in the other lane exhausts it, and the other lane remains.
    • Measured failures are never deferred.
    • Duplicate receipts are ignored, and the stop stays sticky.
    • A seeded property test over 600 random traces against a reference copy of the previous budget:
      • with correctable omitted (random corrected), the budget is identical at every step;
      • with random flags, it never stops the run or a lane earlier;
      • without a passing correction, a difference opens only at a correctable failure and closes at the next non-discovery, non-duplicate result.
      • The run exercised 89 deferrals and 17 rescues by a passing inspection.
  • tests/inspection_budget_replay.test.mjs (fixture tests/inspection-budget-replay-round106-107.json, unchanged). Real before_tool_call/after_tool_call/tool_result_persist hooks, both orders:
  • Replay fidelity. With main d4a61f3's budget and guard (plus only the classifier), the same harness reproduces the recorded stops: strixy Rename to Android Framework — update all references #11, mac-mini 12, laptop 40.
  • Red check against the previous head 92110ff, with its budget and guard plus only passedInspection: 14 of 57 tests in the three changed files fail, and all of those failures are expected:
    • the new semantic tests: below-fuse allowance, one-result wait, lane forgiveness, discovery, mac Fix execute permissions on 6 shell scripts #13, the unrelated-read counterfactual and the property test;
    • the tests that call the new recordPreviewInspectionResult API.
  • Mutation checks:
    • Dropping the fuse condition fails the below-fuse test, both mac replays and the property test.
    • Letting any success forgive fails 6 tests.
    • Ignoring the recorded result in persistence fails both capped-copy tests.
    • Removing the index.js recorder call fails the registration test.
  • Full Linux suite (remote-js-test.sh, pixel-agent JS, tower2):
    • branch 3d65f00: # tests 3165 # pass 3127 # fail 0
    • previous head 92110ff: 3159 / 3121 / 0
    • main d4a61f3: 3138 / 3100 / 0
    • The skip count (38) is identical.
  • Local Windows runs of the 22 related test files pass, except the known Windows-only "exec cancellation control creates exact owner-private markers", which passes on Linux.

🤖 Generated with Claude Code

Lightheartdevs and others added 2 commits September 25, 2026 22:21
A failed preview inspection whose result already hands the model its
correction was charged like any other failure, so it could be the
result that ends the run before the correction was ever sent. On merged
main d4a61f3 this failed the website journey on strixy (round 107: the
second pre-execution rejection was the fourth consecutive failure) and
mac-mini (round 106: four locators that matched nothing).

The run-progress budget now lets the consecutive (or lane) charge of a
correctable failure wait for exactly one next result. The next failure
of any tool charges both; only a success that resets the same counter
forgives it; discovery and free corrections do neither. At most one
charge waits, at most two per run (RUN_PROGRESS_CORRECTED_ATTEMPTS),
and every failure still counts toward the total cap of 12 at once. The
four limits in RUN_PROGRESS_LIMITS are unchanged.

correctableInspectionFailure accepts only inspection results that
measured nothing: invalid_request, INCOMPLETE transition_untested, or a
failed receipt with no top-level error, page errors or blocked requests
whose last step is no_match, selector_not_unique or invalid_selector.
The guard passes it only for an outer call bound as an inspection by
before_tool_call. tool_result_persist classifies the same receipt the
same way, including a Tool Search inspection whose failure is inside
the envelope, so either hook order gives the same accounting.

Replays of the recorded runs through the real guard: strixy stops at
#12 (main: #11), mac-mini still at call 12, the laptop still at call 40
on the total cap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review fixes for the run-progress deferral:

- A correctable inspection failure waits only when its consecutive (or
  lane) charge would stop the run or lane. Below the fuse it is charged
  at once and spends no allowance, so early correctable failures no
  longer use up both allowances before the failure main stops on.
- Only a passing inspection (passedInspection: steps passed, no page
  errors) forgives the waiting charge. The next failure or any other
  non-discovery success charges it, so without a passing correction the
  run stops exactly one result after main would have.
- The inspection tool's own result is recorded on its pending call when
  it returns (index.js wrapper, recordPreviewInspectionResult), and
  tool_result_persist classifies that result instead of a copy whose
  details OpenClaw capped at 8 KiB. Both hook orders now account alike.

Replays: strixy r107 still stops at #12; mac-mini r106 now runs its next
inspection (#13), which is INCOMPLETE and stops the run there; laptop
r107 still stops at #40 on the total cap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant