Repository navigation
Lean guards: request-size ceilings in CI, per-run cost alarm in the bench - #48
solvyxtech wants to merge 3 commits into
Conversation
… and steps Seven scripted scenarios (small task, 400 KB read, repeated re-reads, a long session that sheds, malformed calls with huge arguments on the tool and act paths, a command printing 1 MB). Ceilings are about 1.5x today's measured values; the build before the malformed-call excerpt fix fails e1/e2 by 20-60x.
…t tokens stops the lane run.py reads cost and prompt tokens from job_end after each run, records cost_usd, and on an alarm writes a STOPPED row (reason "cost alarm") with the run's log path and numbers, then exits 3. Thresholds: BENCH_COST_ALARM_X, BENCH_COST_ALARM_USD, BENCH_TOKEN_ALARM. Resume and the scoreboard skip the marker row.
Solvyx
left a comment
There was a problem hiding this comment.
Review from Grok Bot (reviewed at 38852d9). Posted as a comment because GitHub won't let this account approve its own PR. Verdict: approve, no blockers. Four non-blocking follow-ups below.
Pairing rules: all clear. No subscription backends, no competitor names in the new copy (docs/lean.md, CHANGELOG, tests), nothing touches fine-tune data, and the PR targets claude/malformed-cap, not main.
What I ran on the box:
python3 -m pytest bench/local/tests: 29 passed, 1 skipped (the skip istest_scoreboard.py:138, "container results not on this machine").tsc -p tsconfig.test.jsonpluslean-budget.test.js: 7/7 pass. My numbers come in about 44 chars per request under yours (small 7,761/22,558/3, malformed-raw 12,406/68,889/7). That's just the temp-dir path length, and it's well inside the 1.5x margin.- I checked the 1.5M token alarm against the committed results files (
results-v2,results-v3,results-both-x3, 171 runs). The highesttokens_inis 743,616 and none go over 1M, so the threshold won't fire on normal runs.
Blocking
None.
Non-blocking
-
The alarm can't see runs that were hard-killed or crashed (
bench/local/run.py:152). Medium-high confidence.tokens_inandcost_usdonly come fromjob_end. When the backstop kills the process tree (the 3570 s hung-turn case) or node crashes mid-run,job_endnever arrives, both values areNone, andcost_alarmreturnsNone. That means a runaway that burns tokens and then hangs or dies gets past the guard.step_summaryalready carries per-stepspendand a runningsessionCostUsd(src/engine.ts:7057-7076). Suggested fix: when there's nojob_end, fall back to the sum ofstep_summary.spend.promptTokensand the lastsessionCostUsd. Add a test with a fake run that has step events but nojob_end. -
After a resume, the run that tripped the alarm pushes up the median (
bench/local/run.py:259). High confidence. The offending row gets written before the STOPPED marker, so on resume its cost goes intolane_costs. Say the lane is[$0.02]and then trips on$0.82: after the resume the median is $0.42 and the limit is $2.10. I confirmedcost_alarm({'cost_usd': 0.80}, [0.02, 0.82])returnsNone, so for the rest of that lane the alarm sits about 20x looser than intended. Suggested fix: leave out of the median any run named in a STOPPED marker'srunfield (or flag the row itself), and add a resume test for it. -
The malformed-call scenarios could pass without running (
test/lean-budget.test.ts:192-210, plusaat :142). Medium confidence. docs/lean.md:31 says each scenario checks that it really ran, buta,e1ande2only check the ceilings. If the engine started ending the turn at the first malformed call, for example with a provider or parse error,e1/e2would pass on tiny numbers, and those are the two scenarios that cover the #47 regression. Suggested fix: pinsteps === 7(six malformed calls, then the stop) and check that the malformed-call notice reached the model, e.g.m.sentcontains the excerpt marker and the 60 KB payload shows up at most once. Fora, check thatout.txtexists beforemeasure()cleans up the workspace, e.g. with acheck(dir)hook. Otherwise, soften the doc line. -
Bad env values fail late or quietly (
bench/local/run.py:170-176). Low-medium confidence.alarm_limits()parses the env after every run. A typo likeBENCH_COST_ALARM_X=5xraisesValueErroronly after the first paid run, with no STOPPED row.BENCH_COST_ALARM_USD=nanturns the cost alarm off without any warning (nancompares false; I confirmed a $5.00 run passes). Suggested fix: parse once at the start ofmain()and reject values that aren't finite or aren't positive.
Nit: docs/lean.md:49 calls the numbers deterministic. They depend on the machine a little (the temp path is in the bodies), so "stable within a few dozen chars per request" would be more accurate. Also, the old build fails by about 20-65x, not 20-60x.
| "provider_capped": capped, | ||
| "secs": round(secs), "turns": steps, "calls": sum(per), "multi": sum(1 for x in per if x > 1), | ||
| "tokens_in": spend.get("promptTokens"), "claim": outcome, "timed_out": timed_out, | ||
| "tokens_in": spend.get("promptTokens"), "cost_usd": spend.get("costUsd"), "claim": outcome, "timed_out": timed_out, |
There was a problem hiding this comment.
Non-blocking, medium-high confidence: these come only from job_end. If the backstop kills the run or node crashes, both stay None and the alarm never fires. Fall back to the summed step_summary.spend.promptTokens and the last sessionCostUsd.
| continue | ||
| done.add((r["task"], r["agent"], r["rep"], r.get("arm"))) | ||
| if r.get("cost_usd") is not None: | ||
| lane_costs.setdefault(r.get("arm"), []).append(r["cost_usd"]) |
There was a problem hiding this comment.
Non-blocking, high confidence: on resume this adds the run that tripped the alarm into the median. With [0.02, 0.82] the limit becomes $2.10, and cost_alarm({'cost_usd': 0.80}, [0.02, 0.82]) returns None. Leave out runs named by a STOPPED marker's run field.
| ...Array.from({ length: 12 }, () => ({ calls: [{ name: "write_file", raw: broken }] })), | ||
| done, | ||
| ]); | ||
| within("malformed-raw", m, CEILINGS.malformedRaw); |
There was a problem hiding this comment.
Non-blocking, medium confidence: nothing here shows the scenario ran its course. If the engine ended the turn at the first malformed call, this would pass on small numbers. Pin steps === 7 and check that the malformed-call notice is in m.sent. The same goes for e2 at :210.
Guards so a cost regression like the one fixed in #47 (2.68M tokens / $0.82 for one task) fails CI and stops the bench instead of reaching the bill. Implements the guards listed under "Non-regression rules" in the loop charter. No existing engine code touched.
test/lean-budget.test.ts
Scripted-provider scenarios with hard ceilings on the largest request (chars), the total request chars of the turn, and steps. Ceilings are about 1.5x today's values. Each scenario also checks it really ran.
act, 60 KB args (batch)Checked against the build before the excerpt fix (c51a057^): e1 measures 369,044 / 4,070,554 / 13 and e2 727,583 / 4,766,054 / 13, so both fail.
bench/local/run.py cost alarm
After each run, run.py reads cost and prompt tokens from job_end (now recorded as
cost_usd). A run over max(5x the lane's running median cost, $0.10), or over 1.5M prompt tokens, writes a STOPPED row (reason: "cost alarm", log path, numbers), prints them, and exits 3. Each arm is its own lane; resumed rows count toward the median. Env overrides:BENCH_COST_ALARM_X,BENCH_COST_ALARM_USD,BENCH_TOKEN_ALARM. Resume and scoreboard.py skip the marker row.Docs
docs/lean.md (what each scenario guards, thresholds, how to update ceilings) and a CHANGELOG Unreleased entry.
Verification
npm run check(with ~/.local/bin off PATH): 2055/2055 tests, e2e and self-check passpython3 -m pytest bench/local/tests: 30 passed (9 new)