fix(agent): budget exhaustion is a resumable blocker, not a bare failure (#1117) - #1128
Conversation
…ure (#1117) A run that produced a substantially complete implementation — 15 create_file, 8 edit_file, 12 run_command, 6 run_tests — hit its iteration cap while adding extras and told the user, in full: Task execution failed The task flipped to FAILED, no blocker was created, and nothing indicated that most of the deliverable was sitting in the working tree. The cap appeared only in .codeframe/runs/<id>/output.log as "Iteration 45/45". Running out of budget is not an error. It is the same shape as the cost cap, whose existing comment already says it: "the work is not wrong, it needs a human decision, which is what a blocker is for." So exhaustion now takes that path. - New _REASON_ITERATION_BUDGET_EXHAUSTED, in _BLOCKED_REASONS. The loop returns BLOCKED with a blocker instead of FAILED, so the run is resumable and reads differently from a genuine error in `cf tasks list`. - The blocker names the cap that was hit, says partial work is already in the tree (review it before re-running), gives the command to raise the budget, and points at `cf work diagnose`. Kept under _create_text_blocker's 500-char cap so that pointer is not truncated away. - CODEFRAME_MAX_ITERATIONS now exists. Neither it nor a --max-iterations flag did, so the obvious message would have pointed the user at nothing. It is an exact budget that beats the config file and the complexity multiplier, matching the LLM-provider precedence chain; a bad value warns rather than aborting. - _create_text_blocker no longer says "Agent detected a blocker" for stops the runtime made. "Agent detected a blocker: iteration_budget_exhausted" reads as though the agent found a problem in the work, which is the opposite of what happened. Those also get BlockerOrigin "system" rather than "agent". test_error_event_and_stream_close_on_max_iterations asserted the old FAILED outcome. Its fixture workspace has no database, so the blocker write fails and the run degrades to FAILED — it now documents that degradation explicitly, which is worth pinning, and the BLOCKED contract is covered against a real workspace in the new test module.
|
Claude finished @frankbria's task in 4m 43s —— View job Bug-hunting review — ✅ GLM review: no defects foundReviewed the three hunks in Verified:
Out of scope (left to the general review, not defects):
|
ReviewSolid, well-scoped fix. Reusing the cost-cap "blocked, not failed" shape for iteration exhaustion is the right call, and the new
Minor / nit findings
None of these block the change; all are polish. The core behavior change, precedence logic, and test coverage look correct and match the PR description's claims. |
…idated (#1117) Both from PR review. _react_loop and run() still documented "FAILED when max_iterations is reached", which is exactly what this branch changes — the kind of docstring a later reader trusts at face value. The `else: reason = "max_iterations_reached"` default in run() is now unreachable: natural exhaustion returns BLOCKED, and the two remaining FAILED paths (stall with --stall-action fail, loop detection) both set a flag handled by the branches above it. Kept as a defensive default rather than deleted, but relabelled "unknown" so it cannot report a budget stop that did not happen.
|
Both findings fixed. 1. Stale docstrings — correct, and exactly the kind a later reader trusts. 2. Dead Thanks also for independently checking the 439-char blocker length and that |
|
Claude finished @frankbria's task in 3m 42s —— View job Bug-hunting review — ✅ GLM review: no defects foundRe-reviewed the three Verified independently (against the file, not the diff):
Earlier findings, now resolved in
Benign edge cases considered and rejected (not defects):
|
ReviewThis has already been through a solid two-round review (the automated GLM bug-hunt and a prior
I did my own independent pass focused on tracing the change end-to-end rather than re-litigating what's already been reviewed:
Nothing new to flag. The behavior change, precedence logic, and test coverage hold up under an independent trace of the full path from |
|
Claude finished @frankbria's task in 4m 36s —— View job Bug-hunting review — ✅ GLM review: no defects foundThird pass on PR #1128, re-triggered by the Independently verified:
No concrete defect with a reproducible failure scenario. No inline comments posted. |
ReviewNo new commits since my last pass (
One small, non-blocking nit I hadn't raised before: Everything else — the |
Bug-hunting review — ✅ GLM review: no defects foundThird pass, re-triggered by the Independently verified:
No concrete defect with a reproducible failure scenario. |
|
Claude finished @frankbria's task in 3m 6s —— View job Bug-hunting review — ✅ GLM review: no defects foundFourth pass, re-triggered by the Independently verified:
Edge cases considered and rejected (not defects):
No concrete defect with a reproducible failure scenario. No inline comments posted. |
ReviewI re-verified the current state of
One thing worth flagging only for the record, not as a defect (and this is consistent with the 439-char measurement already reported in a prior round): No new issues. This has had unusually thorough review coverage already (logic, error paths, origin mapping, precedence, blocker dedup, CLI surface, docstring/dead-code follow-ups) and my independent read of the current file confirms the implementation matches what was verified in those rounds. |
Closes #1117.
What was wrong
A run that produced a substantially complete implementation — 15
create_file,8
edit_file, 12run_command, 6run_tests— hit its iteration cap whileadding a Dockerfile and example clients, and told the user, in full:
The task flipped to
FAILED, no blocker was created, and nothing indicated thatmost of the deliverable was sitting in the working tree. The cap appeared only in
.codeframe/runs/<id>/output.logasIteration 45/45.The change
Running out of budget is not an error. It is the same shape as the cost cap,
which already blocks rather than fails — and whose existing comment says exactly
why: "the work is not wrong, it needs a human decision, which is what a blocker
is for." Exhaustion now takes that path.
That one change satisfies four of the acceptance criteria at once: it is a
distinct named outcome, it creates a blocker, it is resumable rather than
terminal, and
BLOCKEDreads differently fromFAILEDincf tasks list.The blocker the user actually gets:
CODEFRAME_MAX_ITERATIONShad to be built, not just referencedThe AC asks the message to name "the flag/env var that raises it." Neither
--max-iterationsnorCODEFRAME_MAX_ITERATIONSexisted — the obvious messagewould have pointed the user at nothing. Only
.codeframe/config.yaml'sagent.max_iterationsworked, which means editing a file inside the repo.So the env var is now real: an exact budget that beats both the config file and
the complexity multiplier (matching the LLM-provider precedence chain), with a
bad value warning rather than aborting a Golden Path command. Five tests cover
it, including that it wins over the multiplier — raising only a ceiling the
multiplier then scales away from would have bought nothing.
Also fixed
_create_text_blockerprefixed every blocker with "Agent detected a blocker:".For a runtime-initiated stop that is backwards — "Agent detected a blocker:
iteration_budget_exhausted" reads as though the agent found a problem in the
work. Runtime stops now read "Run stopped: ..." and carry
BlockerOrigin "system"rather than"agent".Acceptance criteria
BLOCKED+iteration_budget_exhaustedcf tasks listcf work diagnose <id>15 new tests;
tests/core/+tests/cli/4343 passed.Judgment calls
FAILEDwith better wording, that's a small revert — but
BLOCKEDis what makes itresumable, and it reuses the cost-cap path rather than inventing a state.
--max-iterationsCLI flag. The env var covers the message's needwithout threading a new parameter through
runtime.execute_agent→ engine →agent. Happy to add the flag on top if you want it discoverable in
--help._create_text_blockertruncatesits context at that length — the
cf work diagnosepointer was being cut offin my first draft.
Known limitations
FAILEDwith no message. That is pre-existing behaviour for every blockerreason, not new here;
test_error_event_and_stream_close_on_max_iterationsnowdocuments that path explicitly.
the partial work is nearly complete or barely started. The message says to
review the tree rather than claiming the work is nearly done.