Design: docs/supervision.md. Companion to #183. Revised after critical review — the first draft was wrong in three ways, recorded below.
Why
Nothing in the platform enforces a spend cap. ai_usage (migration 0048) records cost_micros faithfully, but it is a ledger, not a control — it tells you what a runaway cost you, after it cost you. The only limits in the codebase are req/min rate limits, which bound request frequency, not spend.
Two levels of hierarchy in code are safe because a human wrote them. N levels by configuration are not. Fan-out 5 at 3 levels is 125 leaf runs from one delegation, each an LLM call.
Three corrections to the original draft
1. Depth bounds almost nothing. The first draft leaned on depth as a primary control. It is not: a supervisor at depth 1 can re-delegate sequentially a thousand times without ever increasing depth. Depth caps tree height; it does not cap work. Keep it as a cheap per-path guard, but cost is the real control, plus a separate delegation-count guard (a cheap model can spin thousands of iterations before a cost budget bites).
2. "Carries a budget, decremented at every hop" is wrong under fan-out. That describes a value copied down each path — so five siblings each receive the full allowance and the tree total becomes allowance × fanout^depth, i.e. unbounded in exactly the quantity we are trying to bound. It must be a shared pool scoped to the root delegation, drawn from atomically (UPDATE … SET remaining = remaining - ? WHERE remaining >= ?), not a per-path copy.
3. Check-then-go overshoots under concurrency. Cost is only known after a run. An optimistic "is there budget left?" check lets N concurrent branches all pass simultaneously and overshoot by N × run_cost. This is the classic overbooking problem. Correct pattern: reserve a bounded maximum before starting, settle the actual on completion, refund the remainder.
Scope correction: budget every autonomous entry point, not just delegation
Supervision makes this acute but did not create it. Today every unattended path starts work with no budget:
Budgeting only delegation plugs one hole in a boat. This should be one primitive covering all four, with delegation as the first consumer.
Retries and replay must draw from the same pool. The 0058 outbox retries up to 5 times with backoff, and each retry re-runs the action — so a persistently failing consumer can cost 5× before dead-lettering. Manual replay is currently unlimited. Both must check and draw.
Two pools, different purposes
Natural chokepoint for both: the BYOK/Workers-AI call path that already writes ai_usage.
Scope
- Root-scoped budget pool: remaining cost (micros) + remaining delegations + max depth.
- Atomic reserve-and-settle at the model-call chokepoint; refund unused reservation on completion.
- Admission check at every autonomous entry point (cron, webhook, delivery, delegation) and on retry/replay.
- Per-user/period backstop in addition to the per-tree pool — a tree budget alone does not stop a thousand small runaway trees.
- Exhaustion is a distinct, visible outcome recording where it stopped ("budget exhausted at depth 3"), and must preserve the partial work as resumable once the human raises the limit. Exhaustion must not destroy what was already paid for.
Acceptance
- Total spend across an entire delegation tree is bounded by the root pool, regardless of fan-out.
- Concurrent siblings cannot collectively overshoot the pool.
- A supervisor cannot raise its own budget by delegating (the pool only ever decreases).
- Sequential re-delegation at constant depth is bounded by the cost/count budget.
- Retries and replays draw from the pool.
- Exhaustion surfaces its own status and the work resumes after a raise.
- Tests: fan-out overshoot, concurrent draw, sequential re-delegation, retry drain, resume-after-raise.
Note
Build this with #183. Threading a budget through an existing handoff path is a retrofit touching every call site; adding it while the path is being defined is nearly free.
Design:
docs/supervision.md. Companion to #183. Revised after critical review — the first draft was wrong in three ways, recorded below.Why
Nothing in the platform enforces a spend cap.
ai_usage(migration 0048) recordscost_microsfaithfully, but it is a ledger, not a control — it tells you what a runaway cost you, after it cost you. The only limits in the codebase are req/min rate limits, which bound request frequency, not spend.Two levels of hierarchy in code are safe because a human wrote them. N levels by configuration are not. Fan-out 5 at 3 levels is 125 leaf runs from one delegation, each an LLM call.
Three corrections to the original draft
1. Depth bounds almost nothing. The first draft leaned on depth as a primary control. It is not: a supervisor at depth 1 can re-delegate sequentially a thousand times without ever increasing depth. Depth caps tree height; it does not cap work. Keep it as a cheap per-path guard, but cost is the real control, plus a separate delegation-count guard (a cheap model can spin thousands of iterations before a cost budget bites).
2. "Carries a budget, decremented at every hop" is wrong under fan-out. That describes a value copied down each path — so five siblings each receive the full allowance and the tree total becomes
allowance × fanout^depth, i.e. unbounded in exactly the quantity we are trying to bound. It must be a shared pool scoped to the root delegation, drawn from atomically (UPDATE … SET remaining = remaining - ? WHERE remaining >= ?), not a per-path copy.3. Check-then-go overshoots under concurrency. Cost is only known after a run. An optimistic "is there budget left?" check lets N concurrent branches all pass simultaneously and overshoot by
N × run_cost. This is the classic overbooking problem. Correct pattern: reserve a bounded maximum before starting, settle the actual on completion, refund the remainder.Scope correction: budget every autonomous entry point, not just delegation
Supervision makes this acute but did not create it. Today every unattended path starts work with no budget:
Budgeting only delegation plugs one hole in a boat. This should be one primitive covering all four, with delegation as the first consumer.
Retries and replay must draw from the same pool. The 0058 outbox retries up to 5 times with backoff, and each retry re-runs the action — so a persistently failing consumer can cost 5× before dead-lettering. Manual replay is currently unlimited. Both must check and draw.
Two pools, different purposes
Natural chokepoint for both: the BYOK/Workers-AI call path that already writes
ai_usage.Scope
Acceptance
Note
Build this with #183. Threading a budget through an existing handoff path is a retrofit touching every call site; adding it while the path is being defined is nearly free.