Budget pre-flight gate (accumulated >= limit) blocks provably zero-cost runs when max_cost_usd=0.0 #150
Labels
No labels
auto/blocked-by-deps
auto/ci-timeout
auto/claimed-implementer
auto/claimed-merge
auto/claimed-reviewer
auto/driver-down
auto/invariant-violation
auto/last-attempt-tier-0
auto/last-attempt-tier-1
auto/last-attempt-tier-2
auto/last-attempt-tier-min
Automation Tracking
auto/needs-conflict-resolution
auto/needs-implementer
auto/postmortem
auto/ready-to-merge
auto/restart-throttled
auto/revert
auto/sentinel
auto/stale-inactivity
auto/unstable
Blocked
Bounty
$100
Bounty
$1000
Bounty
$10000
Bounty
$20
Bounty
$2000
Bounty
$250
Bounty
$50
Bounty
$500
Bounty
$5000
Bounty
$750
MoSCoW
Could have
MoSCoW
Must have
MoSCoW
Should have
Needs Feedback
Points
1
Points
13
Points
2
Points
21
Points
3
Points
34
Points
5
Points
55
Points
8
Points
88
Priority
Backlog
Priority
CI Blocker
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Signed-off: Owner
Signed-off: Scrum Master
Signed-off: Tech Lead
Spike
State
Completed
State
Duplicate
State
In Progress
State
In Review
State
Paused
State
Unverified
State
Verified
State
Wont Do
Type
Automation
Type
Bug
Type
Discussion
Type
Documentation
Type
Epic
Type
Feature
Type
Legendary
Type
Refactor
Type
Support
Type
Task
Type
Testing
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Blocks
Depends on
#77 Epic: LLM Agent Runtime Stabilization — reliability, resource enforcement & correctness hardening
cleveragents/cleveractors-core
#151 feat(executor): let provably zero-cost invocations pass the budget pre-flight
cleveragents/cleveractors-core
Reference
cleveragents/cleveractors-core#150
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Metadata
Commit Message:
feat(executor): let provably zero-cost invocations pass the budget pre-flightBranch:
feature/m3-budget-gate-zero-cost-carveoutBackground and context
The pre-flight budget gate introduced with #76 (and reviewed in PR #80) enforces the caller-supplied
max_cost_usdlimit by raisingExecutionError(kind="cost", reason="budget_exhausted")before an invocation whenaccumulated >= limit. Embedding platforms use this as a hard wallet-bound spending cap: the cleverthis webapp pins cleveractors-core at3744056(v2.1.0) and passesmax_cost_usd = min(platform_cap, payer_wallet.available_balance)— including0.0for a zero/negative-balance wallet, deliberately, so the gate fails fast before any upstream call fires (issue cleveragents/cleveragents-webapp#473).The
>=comparison is intentional (PR #80: exactly-at-limit completes the current node, the pre-flight blocks the next one). But withlimit = 0.0an execution starts ataccumulated = 0, so0 >= 0is true and the gate fires before the first node — for every run, regardless of what the run costs. This blocks runs whose every LLM invocation is provably free: an actor whosetype: llmagents all use a model whose prompt AND completion rates are exactly0.0in the supplied pricing table (a real, growing class — Claude Code-style endpoints on catalogued zero-rate models). A brand-new workspace with a $0 wallet can therefore not run even a free actor.Current behavior
Against cleveractors-core @
3744056(v2.1.0), executing a graph actor whose single LLM agent uses a 0/0-rate model withlimits = {"max_cost_usd": 0.0}and a pricing table containing that model at zero rates raises before the first node:Enforcement points that compare
>=pre-invocation:cleveractors/langgraph/pure_graph.py—PureLangGraph._check_budget_pre_flight(per-node pre-flight)cleveractors/agents/llm.py—LLMAgent._check_budget_before_invoke(tool-loop pre-invoke; pruning passes are already skipped via_should_skip_pruning)For comparison, the post-node checks (
_check_post_node_cost_limit; the single-LLM cost block inruntime_dispatch._execute_llm) use>— so on those code paths a provably-free run with a 0.0 limit succeeds. The graph/LLM-loop paths are the only ones that block0 >= 0, and today the same config can succeed or fail depending only on which executor shape it lands in. (Note the single-LLM path has no pre-flight at all: a positive-rate model with a 0.0 limit fires the upstream invocation and only fails afterwards — inconsistent for the opposite reason.)Expected behavior
A run whose remaining budget is 0 should still be refused when it could spend money, but a provably free invocation should pass the pre-flight. The library already holds everything needed to make that call per-invocation: the pricing table and the agent's (provider, model) resolve the exact rates the invocation accumulates at, so the pre-flight can distinguish "this node cannot move accumulated cost" (
prompt == 0.0andcompletion == 0.0) from "budget exhausted with a payable node". Suggested semantics (illustrative — exact design is the team's call):accumulated >= limitblocking as-is whenever the upcoming invocation's rates are not both exactly 0.0 (fail-fast preserved: no upstream call, no negative wallet debit);>post-node checks unchanged, and ideally give the single-LLM path the same pre-flight semantics as the graph path.A blanket change of
>=to>in the pre-flight is not a fix: it would keep the first payable invocation firing (unbilled, on an empty wallet) and would undo the PR #80 semantics.Acceptance criteria
limits = {"max_cost_usd": 0.0}.limits = {"max_cost_usd": 0.0}still fails fast withExecutionError(kind="cost", reason="budget_exhausted")before any upstream invocation fires._check_budget_before_invoke) has consistent semantics with the per-node pre-flight.Supporting information
>=semantics: Graa review PR #80.estimate_actor_cost_is_zero) that omits themax_cost_usdkey for provably-free runs on empty wallets — deliberately conservative (unverifiable configs keep the 0.0 fail-fast cap). That workaround exists only because the library cannot express "zero-cost run on a zero budget"; once this issue lands in a release, the webapp should bump its pin, drop the platform-side estimate, and let the library gate carry the semantics.big-pickle,glm-5-free, etc. (both catalogued at $0/$0), used by Claude Code-style endpoints.Subtasks
>max_cost_usd check to the non-stream single-LLM path (runtime_dispatch._execute_llm), mirroring the existing post-stream block in_execute_llm_stream, so all executor shapes enforce the budget identicallydocs/adr/(continuing the ADR-2029/ADR-2031 cost/budget design lineage) plus theCHANGELOG.mdentry. (Triage 2026-10-13: this subtask originally said "Updatedocs/index.md(§ execution limits / budget gate) for the new semantics" — corrected:docs/index.mdis the Actor Configuration Standard (§0–§21) and has no execution-limits/budget-gate section; executorlimits/pricingare not part of the actor-config YAML standard and are documented only via the ADRs and the changelog.)Definition of Done
This issue is complete when:
master, reviewed, and merged before this issue is marked done.Triage 2026-10-13 — verified. Reviewed the description against the code at
3744056(v2.1.0, with local HEAD14d2132):>=semantics confirmed at both enforcement points —PureLangGraph._check_budget_pre_flight(per-node,pure_graph.py:615) andLLMAgent._check_budget_before_invoke(tool-loop,agents/llm.py:547); post-node checks use>(_check_post_node_cost_limit, and the single-LLM cost block inruntime_dispatch); pruning passes are already skipped via_should_skip_pruning. All exactly as described, including the PR #80 rationale quoted in the docstrings.0.0 >= 0.0first-node block reproduces exactly as described (_get_max_cost_usd_limitreturns the 0.0 limit when the pricing table is non-empty; the pre-flight then trips before the first node with the identical error message/format)._execute_llm/_execute_llm_streamcheck cost only after the invocation with>, so a positive-rate model on an exhausted budget fires the upstream call first (AC4 is implementable —executor.limits/executor.pricingare available pre-invocation).LLMAgentexposesprovider/model(llm.py:310-311) and the graph can resolve a node's agent pre-execution (node.config.agent);_compute_costraisesmissing_pricing_entryfor missing/incomplete/invalid rate entries, which supports the conservative AC5.>=review), cleveragents-webapp#473 (wallet-cappedmax_cost_usd, always including the key at 0.0) and cleveragents-webapp#531 (the ALLOW ZERO-COST policy decision, recorded 2026-10-12 and confirmed by the project owner; platform-side static zero-cost estimate shipped there as the workaround).One description correction applied (see the subtasks checklist): the subtask "Update
docs/index.md(§ execution limits / budget gate)" referenced a section that does not exist —docs/index.mdis the Actor Configuration Standard (§0–§21) and does not document executorlimits/pricingat all; the working design-record surface for executor-limit semantics is the ADR lineage (ADR-2029/ADR-2031) plusCHANGELOG.md. The subtask now targets those.Per triage: label moved
State/Unverified→State/Verified(Priority/Medium, Type/Feature unchanged), assigned to the active milestone v2.1.0, and the Ref field set to the Metadata branchfeature/m3-budget-gate-zero-cost-carveout.Design decision — carve-out semantics (subtask 1 of 7).
Pre-implementation review raised two questions that the ticket owner resolved out-of-band first (transcribed for the record):
Branch base.
The
.gitea/workflows/ci.ymltriggers reference adevelopbranch (push tomaster/develop, PRs tomaster/develop*), but nodevelopbranch exists on the remote today (verified viagit branch -aand the Forgejo API: onlymaster(protected),master-backup, and feature/bugfix/tdd branches). This issue's Definition of Done and the workflow docs both branch offmaster. Decision: basefeature/m3-budget-gate-zero-cost-carveoutonorigin/masterand open the PR againstmaster.Scope of the single-LLM fix. The issue description attributes a post-invocation cost check to
runtime_dispatch._execute_llm; in fact that block lives only in_execute_llm_stream(the streaming variant), and the non-stream_execute_llmhas nomax_cost_usdenforcement at all today (neither pre-flight nor post-invocation — verified:_execute_llmspansruntime_dispatch.pyand never readsexecutor.limits["max_cost_usd"]/executor.pricing). Decision: fully align — add the post-invocation>check to the non-stream path as well, so all executor shapes enforce the budget identically. Added as a new subtask.Chosen semantics (ADR-2038 lineage)
The carve-out is implemented as a per-invocation pricing look-up at the pre-flight enforcement points, using the same pricing table that already drives
_compute_cost:1. Rate-resolution extracted (pure refactor).
core/exceptions.py._compute_costkeeps its exact validation ladder and error contracts; the lookup part is factored into_resolve_pricing_rates(pricing, provider, model) -> tuple[float, float], and_compute_costis reimplemented on top of it. Rationale: the lifted ladder (missing provider → missing model → missing prompt/completion key → non-float rate →ExecutionError(kind="cost", reason="missing_pricing_entry")) is exactly what the carve-out must evaluate, so implementing the predicate on the same code path guarantees the "not treated as free" rule (AC5) can never drift from the charging rule.2. Provably-free predicate. New
_is_zero_rated_invocation(pricing, provider, model) -> bool(same module — the established circular-import-free home shared by all three cost consumers):Trueiff_resolve_pricing_ratessucceeds and both rates are exactly0.0;Falseon any failure (missing/incomplete/invalid entries — conservative, AC5). Only "provably zero-cost" is carved out; e.g. half-zero entries (prompt 0.0, completion > 0) are NOT free — the invocation can still move accumulated cost via completion tokens.3. Graph per-node pre-flight (
PureLangGraph._check_budget_pre_flight).accumulated >= limitstays the blocking condition, and only after it trips do we resolve the upcoming node's invocation (constant cost for every fresh-budget node — zero extra work on the common path):NodeType.AGENTwith a resolvable agent (viaNodeConfig.agent→Node.agents) that is anLLMAgentinstance. Everything else — TOOL/FUNCTION/ROUTER/SUBGRAPH nodes, tool/chain/composite agents, unresolved agent names, missing pricing — is conservative → block (unchanged behaviour).(provider, model)rates are both exactly0.0in the node-context pricing table, the block is skipped (INFO log) and the node runs. Matches the platform-sideestimate_actor_cost_is_zerosoundness conditions in cleveragents-webapp#531's comments (invalid entries, half-specified pairs, composite graphs → keep the block).4. Tool-loop pre-invoke (
LLMAgent._check_budget_before_invoke). Identical carve-out viacurrent_pricingContextVar +self.provider/self.model. Correct for all five call sites (main, synthesis, synthesis_followup, stuck_model, stuck_model_followup) because every one precedesself._retry_ainvoke→self.chat_model(built fromself.provider/self.model); the pruning model is already forced equal to the agent's model (temporary limitation, issue #65)._should_skip_pruningis intentionally unchanged — it is a spending suppression, not an enforcement gate, and making it carve-out-aware would only add (free) pruning calls.5. Single-LLM paths (
runtime_dispatch._execute_llmand_execute_llm_stream). New shared helper_check_single_llm_budget_pre_flight(executor, provider, model), called before any agent creation or upstream invocation, mirroring the graph pre-flight: absentmax_cost_usd→ no-op; limit set + empty pricing →missing_pricing_entry(same asPureLangGraph._get_max_cost_usd_limit); bool/non-numeric limit → no-op on the pre-flight (the post-invocation ladder still raiseskind="cost", so today's observable behaviour for invalid limits is preserved); the gate trips only when0.0 >= limit(a fresh single-LLM execution starts at accumulated = 0, so only a zero/negative cap can pre-block — exactly the webapp'smin(platform_cap, wallet.available_balance)= 0.0 case). Tripped ⇒ block withExecutionError(kind="cost", reason="budget_exhausted")unless the invocation is zero-rated. In_execute_llm_streamthe pre-flight lives inside the early-validation wrapper whoseexceptpopulates the billing-integritylast_resultplaceholder; in_execute_llmit runs after config +unsafevalidation (preserving today's error precedence).6. Post-invocation checks.
_check_post_node_cost_limitand the_execute_llm_streampost-stream block stay>(PR #80 semantics preserved: exactly-at-limit completes the current node; the next payable node's pre-flight blocks it). The non-stream_execute_llmgains the missing post-invocation block mirroring the stream variant (per the approved scope decision above).Was not chosen (and why):
>=→>: rejected in the issue itself — it would re-allow the first payable invocation on an empty wallet and undo PR #80.limits/pricingremain the only control surface, per the triage note they are not part of the Actor Configuration Standard).Test plan (TDD): Behave scenarios first — graph path (zero-rate run completes with
max_cost_usd=0.0; positive-rate + 0.0 blocked before any node executes; exactly-at-limit completes pays, next payable blocked; zero-rate blocked when the pricing entry is missing/invalid = AC5), tool-loop pre-invoke, single-LLM non-stream (fail-fast + no upstream invocation with positive rates + 0.0 limit, zero-rate completion, new post-invocation>block), single-LLM stream (pre-flight fires before any token is consumed; billing-integritylast_resultpopulated). Robot integration suite exercises the same semantics end-to-end throughcreate_executorwith stub chat models (mirroringrobot/llm_token_budget.robot). No ASV benchmark: the carve-out adds work only after the gate trips (an exceptional path), and the_compute_costrefactor is a pure extraction — no performance-sensitive change to measure.Work is on
feature/m3-budget-gate-zero-cost-carveout(offorigin/master). Progress notes will follow as subtasks land.Implementation notes — subtasks 2–5 landed on
feature/m3-budget-gate-zero-cost-carveout(working tree; commit to follow after all gates pass).TDD trail (subtask 2)
Failing tests were written first and confirmed red against the pre-change tree before touching
src/:0.0 >= 0.0before the first node); payable→free→payable chain at an exactly-hit 0.30 limit (the free middle node was blocked today); zero-rate LLM node followed by a TOOL node (the LLM node was blocked today); probe scenario (AttributeErroruntil the helper exists). Green today and kept as lock-ins: positive-rate fail-fast with the node not executed; missing-model / incomplete-entry / non-numeric-rate / half-zero entries staying blocked; AGENT nodes with a missing or non-LLM agent staying blocked.budget_exhaustedat the first main invoke); "still blocks an exhausted budget when the rates are positive" green lock-in.create_executorwith invocation-counting mock models viapatch(build_chat_model)— 9 red (pre-flight on both execute/execute_stream, the new non-stream post-invocation>card, missing-entry/bool/non-numeric post ladders, empty-table missing_pricing_entry), 2 green lock-ins (zero-rate run/stream completing on a 0.0 wallet; exactly-at-limit completing).reason="budget_exhausted"since its enforcement point moved from post-stream to pre-flight; its comments/docstrings updated (test updated to intentionally-changed behaviour per the obsolete-test policy; all assertions still hold post-change).Counted red:
13 failed + 1 erroredon the touched files, matching the expectation exactly (verified with a pre-implementation run). After implementation: all 246 scenarios in the four touched features pass, and the full suite is green —155 features passed, 3133 scenarios passed, 0 failed(nox -s unit_tests).Implementation (subtasks 3, 4 + the approved non-stream post-check)
core.exceptions._resolve_pricing_ratesnow owns the pricing-table validation ladder;_compute_costis a thin wrapper over it (pure refactor — same errors, same coercion).core.exceptions._is_zero_rated_invocationis the conservative predicate (both rates exactly0.0;Falseon empty table / anymissing_pricing_entryfailure). Rationale: provably-free classification shares the exact code path the gate charges by, so it cannot drift.PureLangGraph._check_budget_pre_flight: unchangedaccumulated >= limitcondition; only when tripped does it consult the newPureLangGraph._upcoming_node_invocation_is_zero_rated(node_name)(AGENT node →NodeConfig.agent→Node.agents→LLMAgent→ rates). One helper serves all four_node_budget_contextcall sites, so execute and execute_stream graph shapes share semantics.LLMAgent._check_budget_before_invoke: same carve-out viacurrent_pricing+self.provider/self.model(correct for all five call sites — each precedesself._retry_ainvokeagainstself.chat_model)._should_skip_pruningdeliberately untouched (spending suppression, not an enforcement gate).runtime_dispatch._check_single_llm_budget_pre_flight: shared helper for both single-LLM variants, mirroring the graph parse ladder (absent → no-op; set + empty table →missing_pricing_entry; bool/non-numeric → deferred to the post ladder). Called in_execute_llmafter config+unsafe validation, before any agent creation; in_execute_llm_streaminside the early-validation wrapper so the billing-integrity<no_llm>placeholder populatesexecutor.last_resulton the raise._execute_llmadditionally gained the post-invocation>block, bool/non-numeric ladder, and the ADR-2029 "non-empty pricing table ⇒ model entry mandatory" validation — mirroring_execute_llm_stream/ the graph post checks byte-for-byte in semantics. This changes behaviour for previously-silent over-budget/unpriced/invalid-limit runs on the non-stream path only (the gap the owner opted to close).>semantics preserved — AC6 still covered by pre-existing "Second LLM node blocked by pre-flight when first node exactly hits limit" and by the new payable→free→payable chain scenario).Documentation (subtask 5)
docs/adr/ADR-2038-budget-gate-zero-cost-carveout.md(D-1 rate-resolution single-source, D-2 graph helper, D-3 tool-loop, D-4 single-LLM pre-flight, D-5 non-stream full enforcement, D-6>preserved; alternatives A-1..A-5 incl. the rejected blanket>flip).CHANGELOG.md[Unreleased] › Added entry with module/BDD/Robot/docs pointers.Multi-level tests
robot/budget_gate_zero_cost.robot+robot/BudgetGateZeroCostLib.py— drives realcreate_executoractors (2-node graph, single-LLM execute & execute_stream, and a tool-loop round on the built-inechotool) with stub chat models; asserts budget_exhausted pre-invocation (chat model never invoked / astream never opened) and zero-rate completions.7 tests, 7 passed(nox -s integration_tests -- robot/budget_gate_zero_cost.robot).Remaining: subtask 6 (version-bump per the release process — analysis in the next comment), then the full nox battery (format/lint/typecheck/security_scan/unit_tests/integration_tests/coverage_report ≥ 97%) before committing.
Coverage work + quality-gate results (subtask 6 analysis included).
Version bump per the release process (subtask 6)
Analysis of the repo's release process before touching
pyproject.toml:v*tags (CONTRIBUTING › CI/CD › Release Process;release.yml). No tag exists in this repository — there has never been a release — andCHANGELOG.mdcontains only an[Unreleased]section.pyproject.tomlalready readsversion = "2.1.0"(set once at commit8fa9e76early in this milestone). Every prior v2.1.0-milestone feature (#101, #111, #121, #123, #148 …) landed its changelog entries under[Unreleased]with no further bump.Decision: changelog entry added under
[Unreleased]; nopyproject.tomlversion change — the carve-out ships in the unreleased v2.1.0 that the milestone will tag (a mid-milestone bump to 2.2.0 would desynchronize the version from the milestone and from every other Unreleased entry). The consumer-side re-pin it unblocks (cleveragents-webapp) pins by commit SHA today, so merging this branch is what they need; when v2.1.0 is tagged they can move to the release. CONCLUSION: subtask satisfied as "changelog entry + release-process evaluation ⇒ no bump warranted until the release is cut."Coverage-scope note (the one ledger worth recording)
The pre-flight relocation moved the enforcement point of two pre-existing single-LLM stream scenarios: the old "zero cost limit" case (0.0 limit + positive rates) used to raise at the post-stream check and must now raise at the pre-flight. Its scenario still passed (same kind/reason), but the post-stream
>raise temporarily lost its coverage. Resolved by splitting the two enforcement points into separate scenarios:>check covered — the stream runs, then the post check raises, with the billing-integrityexecutor.last_resultplaceholder asserted.Per the coverage mandate (target files with the most uncovered lines), two adjacent budget-tool-loop behaviours were also newly locked (previously uncovered lines):
LLMAgent._should_skip_pruning's skip branch: a provably-free invocation on an exhausted budget still has its pruning pass suppressed (ADR-2038 D-3's "deliberately unchanged" semantics — the carve-out does not resurrect extra, even free, LLM calls), with the raw (unpruned) echo payload delivered;_is_zero_rated_invocationmust treat an empty pricing table as not free (AC5) — the graph already guards this withmissing_pricing_entry, the tool-loop ContextVar path now pins it;_rawfallback, undeclared tool break, dispatch-error ToolMessage) — a long-uncovered block inLLMAgent._execute_tool_loopexercised with a realistic raw-OpenAI-format tool-call payload.Quality gates (all on the final tree)
nox -s formatnox -s lintnox -s typechecklangchain_google_genai)nox -s security_scannox -s dead_codenox -s unit_testsnox -s integration_testsrobot/budget_gate_zero_cost.robot)nox -s coverage_reportCoverage accounting vs.
origin/master(measured with a stashed baseline run): master = 96.90% (490 missing lines); this branch = 97.11% (460 missing) — net recovery of 30 previously-uncovered lines, i.e. coverage improved against both the prior state and the 97% standard, with every newly added line covered.No ASV benchmark added — reasoning recorded in ADR-2038 (Follow-up): the carve-out only evaluates after the gate trips (exceptional path) and the rate-ladder extraction is pure refactor; nothing performance-sensitive changed. (CI's benchmark-regression job runs with
continue-on-errorand will compare the noop; a benchmark would measure nothing.)Implementation landed — commit pushed, PR open, review requested.
17b2e1confeature/m3-budget-gate-zero-cost-carveout(offorigin/master, per the branch-base decision above). Single atomic commit: implementation + tests + ADR + changelog. First line matches the Metadata Commit Message exactly; footerISSUES CLOSED: #150.master, with the full description (summary, approach, AC matrix, gate table) and a closing keyword:#151
Type/label).State/In Progress→State/In Review.Forgejo Actions run
#518is executing the 10-jobci.ymlmatrix against head SHA17b2e1c. I will monitor the required checks (status-checkconsolidates lint/typecheck/security/unit_tests/coverage) and amend the branch if anything fails.Final status — CI green, PR ready for review (implementation phase complete).
CI on PR #151 (head
17b2e1c, Actions run #518)All required checks passed:
lint,typecheck,security,quality,build,unit_tests,integration_tests,coverage, and the branch-protection consolidation gatestatus-check— all green.The informational
benchmarkjob reported failure. Verified non-blocking and non-attributable to this change:CONTRIBUTING.md› CI/CD, the benchmark job runs withcontinue-on-error: trueandstatus-check— the single branch-protection gate — passed without waiting for it, exactly as documented.benchmarks/exercises any changed path: none of the ASV suites setlimits/pricingor callcreate_executor; the budget-adjacent suites (token_budget_benchmark,pure_graph_parallel_dispatch_benchmark) time untouched helpers (token-count extraction, pruning-marker parsing). The carve-out only evaluates after a tripped gate — a path no benchmark constructs. The noxfile's own docstring also documents that the regression session exits 1 on any measured 'worsened' signal, which ASV commonly emits from runner noise on contended CI hosts.Noted as follow-up information for the benchmark automation owners — no action required for this issue.
Where this leaves #150
17b2e1c(single atomic commit, first line = Metadata Commit Message,ISSUES CLOSED: #150) onfeature/m3-budget-gate-zero-cost-carveout, pushed.master: description withCloses #150, milestone v2.1.0, label Type/Feature, dependency registered in the correct direction (the PR blocks this issue).State/In Review.format✓,lint✓,typecheck(pyright strict, 0 errors) ✓,security_scan✓,dead_code✓,unit_tests(3136 scenarios) ✓,integration_tests(373 tests) ✓,coverage_report97.1% (up from origin/master's measured 96.90%) ✓.Remaining for the maintainer/reviewer pipeline: one approving review and the merge; then the webapp can re-pin and drop its platform-side
estimate_actor_cost_is_zeroworkaround (tracked on their side).PR #151 retargeted to
develop— feature branch rebased onto latestdevelop(2026-10-20).Per the updated project policy (new features now land on
develop; this ticket's original branch-base decision was recorded when nodevelopbranch existed on the remote), the PR base branch was changed frommastertodevelopand the feature branch was rebased:feature/m3-budget-gate-zero-cost-carveoutreplayed ontoorigin/develop(9631ee6, "docs: Document virtual providers for load balancing") — previous base14d2132(master tip, which was also the merge base with develop).4a9818c(previous head17b2e1c). Commit message unchanged: first line matches the Metadata Commit Message exactly;ISSUES CLOSED: #150footer preserved; single atomic commit, no merge commits.CHANGELOG.md[Unreleased] › Added— develop's newbackends/strategyentry (#155) kept first, then this branch's #150 entry (newest-first ordering preserved).src/cleveractors/agents/llm.pymerged cleanly — develop's virtual-provider wiring (_ensure_chat_model, lazy-init comment) and this branch's carve-out (_check_budget_before_invoke) touch disjoint regions; merged diff vs develop contains only the carve-out.Observation — ADR number collision (no action taken, owner decision needed): develop now carries
docs/adr/ADR-2038-virtual-provider-load-balanced-backend-routing.md(landed 2026-10-19) and this branch carriesdocs/adr/ADR-2038-budget-gate-zero-cost-carveout.md(authored 2026-10-13). After the rebase both files coexist on the branch. No tooling enforces ADR-number uniqueness and there is precedent (ADR-2034-*exists twice), so this is not blocking; renumbering one of the two (convention would suggest this branch's, since develop's file lands first in the rebased history) was deliberately left untouched.Quality gates re-run on the rebased tree (all pass):
nox -s format -- --checknox -s lintnox -s typecheck(pyright strict)nox -s security_scan/dead_code/complexitynox -s unit_testsnox -s integration_testsnox -s coverage_reportnox -s buildcleveractors-2.1.0-py3-none-any.whlGitea updates applied: PR #151 base changed
master→develop(Forgejo now reports base9631ee6; PR mergeable, head4a9818c); PR description extended with a "Base branch — retargeted todevelop" section documenting the rebase, the conflict resolution, and the re-run gate results.Self-QA Implementation Notes (Cycle 1)
Outcome: Approved on cycle 1/5 (no fix cycles run). CI is broken, so all gates were run locally via nox.
Cycle 1
Review findings: 0 critical / 0 major / 3 minor / 2 nits.
_node_budget_context; low risk).ADR-2038-virtual-provider-load-balanced-backend-routing.mdalready on develop; resolve before v2.1.0._is_zero_rated_invocationannotation should acceptdict | None.Fixes applied: None — the review verdict was Approve, so no fix cycle was run.
Remaining Issues
All findings above remain open as non-blocking follow-ups; none blocked approval.
Quality gates (local nox runs): format pass · lint pass · typecheck pass · security_scan pass · dead_code pass · complexity pass · unit_tests pass (156 features / 3,174 scenarios / 0 failed) · integration_tests pass (374/374) · coverage_report pass (97.2%).
Approval posted to PR #151 as a COMMENT review per the self-QA workflow.