Skip to content

admission: worst_case under-counts iterations for acyclic plans, so an admitted round runs every node and is then discarded #71

Description

@Shashankss1205

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinghelp wantedExtra attention is needed

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions