What happens
_worst_case (grapharc/planner/admission.py:653-663) sums each proposed node's cost once. LangGraph runs a node once per super-step in which it is triggered, so an unequal-arm join re-runs the join node. The planner controls the topology, so the multiplier is planner-controlled.
admission.py:25-29 states the promise this breaks: "An over-budget proposal is therefore refused before its first node exists, not after the run notices it overspent."
Repro
Chained unequal-arm diamonds, require_acyclic=True (the default), all admitted:
stages=1 nodes=4 worst_case_iters=4 actual=5 1.2x
stages=4 nodes=16 worst_case_iters=16 actual=44 2.8x
stages=12 nodes=48 worst_case_iters=48 actual=324 6.8x
End to end with Budget(max_iterations=4) — exactly the admitted worst case:
stop: budget_exhausted | max_iterations reached (4/4)
round 1: admitted worst_iters=4 remaining=4 executed=False
node bodies that actually ran before the run died: ['l', 's', 'm', 'o']
Observed: every node body executes, then the round is discarded as executed=False.
Expected: refusal before the first node exists — or an estimate that accounts for re-triggered joins.
The same arithmetic applies to tokens and seconds, so this is not only an iteration-counting curiosity: a plan can be admitted against a token ceiling it will exceed.
Why it matters
Admission's entire value is that it decides before anything runs. Here the side effects happen and only the bookkeeping is refused — the worst outcome of both designs: the work is done, the budget is blown, and the round is thrown away so the planner cannot even build on it.
Note docs/architecture-review.md:306-307 records an under-estimate for cyclic proposals ("by the loop factor"). This is the default acyclic path, so the existing note does not cover it — which is probably why it has gone unnoticed.
What to consider
- Count triggers, not nodes: compute the worst-case number of super-steps a node can be triggered in (a node with k predecessors on unequal-length paths can run up to k times) and multiply its cost accordingly.
- Or make the meter authoritative and admit against a bound that is provably an over-estimate, documenting the direction of the error — an over-estimate refuses too much, which is the safe direction; an under-estimate spends money it promised not to.
- Whatever the fix, a test with a chained-diamond topology asserting
worst_case >= actual would keep it honest.
Acceptance criteria
For a chained unequal-arm diamond, the admitted worst case is greater than or equal to the iterations actually consumed; a plan that would exceed the remaining budget is refused before any node body runs.
What happens
_worst_case(grapharc/planner/admission.py:653-663) sums each proposed node's cost once. LangGraph runs a node once per super-step in which it is triggered, so an unequal-arm join re-runs the join node. The planner controls the topology, so the multiplier is planner-controlled.admission.py:25-29states the promise this breaks: "An over-budget proposal is therefore refused before its first node exists, not after the run notices it overspent."Repro
Chained unequal-arm diamonds,
require_acyclic=True(the default), all admitted:End to end with
Budget(max_iterations=4)— exactly the admitted worst case:Observed: every node body executes, then the round is discarded as
executed=False.Expected: refusal before the first node exists — or an estimate that accounts for re-triggered joins.
The same arithmetic applies to
tokensandseconds, so this is not only an iteration-counting curiosity: a plan can be admitted against a token ceiling it will exceed.Why it matters
Admission's entire value is that it decides before anything runs. Here the side effects happen and only the bookkeeping is refused — the worst outcome of both designs: the work is done, the budget is blown, and the round is thrown away so the planner cannot even build on it.
Note
docs/architecture-review.md:306-307records an under-estimate for cyclic proposals ("by the loop factor"). This is the default acyclic path, so the existing note does not cover it — which is probably why it has gone unnoticed.What to consider
worst_case >= actualwould keep it honest.Acceptance criteria
For a chained unequal-arm diamond, the admitted worst case is greater than or equal to the iterations actually consumed; a plan that would exceed the remaining budget is refused before any node body runs.