Metadata
| Status | abandoned ‖ paused |
|---|---|
| Created | 2026-07-25T20:57:11.933950303+00:00 |
| Tags | controller, parallelism, dispatch, budget |
| Failure reason | Superseded by the operator-published post-lifecycle synthesis: deterministic convergence is service-owned; no supervisor persona, adaptive outage controller, or composition authority remains. |
Description
Objective
Build the adaptive-parallelism + cost/time-budget controller — the control loop that turns the T2 telemetry signal into the max_agents knob. It replaces the binary "spawn / global-pause" response to rate pressure with a graduated knob, while leaving the existing global-outage breaker (src/commands/service/zero_output.rs) as a hard last-resort floor.
Context: docs/studies/adaptive-parallelism-budget-design.md (the full design — read it; this task implements §4-§7). docs/studies/roadmap-rate-limit-and-distribution.md §1/§4(T4). PREREQUISITES: T1 (the authority fix — the controller writes CoordinatorState.runtime_max_agents, which must survive a flagless reload) and T2 (the telemetry FailureSignal + ProviderHealth.cooled_until_ms the controller thresholds on). This task implements BOTH the rate-only spark (study §11 phase 2) AND the budget model (phase 3) as one cohesive controller, since they share the same module/loop/CLI.
Boundary (study §9 — peer of the supervisor, never cross)
The controller NEVER resets tasks; the supervisor NEVER touches max_agents. The controller writes ONLY CoordinatorState.runtime_max_agents; the existing slots_available = max_agents - alive_count math (src/commands/service/coordinator.rs:4850) does the rest. The one-way composition contract with the supervisor (supervisor reads effective_max_agents to politely delay a reset burst; controller reads the supervisor reset log to discount dumb-failures from its throughput denominator) is WIRED + smoke-tested in T5, but the read-method stubs can live here.
What to build
- New module
src/budget/mod.rs:- The knob:
effective_max_agents ∈ [floor, ceiling]is the ONLY thing the controller writes. - Policy (study §4.2): subtractive-down (rate guardrail: sustained-429 rate ≥10% OR provider-wide quorum =>
E - step_downwherestep_down = max(1, ceil(E/4)); spend guardrail; request-rate guardrail) and additive-up (ALL of: busy-fraction ≥0.95, sustained-429 ≤1%, spend ≤0.8x budget, cooldown elapsed, E < min(ceiling, spend_cap, rpm_cap) =>E + step_upwherestep_up = 1). One cut per control interval, never two. - Hysteresis (study §4.3/§5.1): busy-fraction band (never cut because slots are empty) + cooldown band (
cooldown=90scut-to-grow,cut_lockout=30scut-to-cut). Prove bounded-rate by construction (study §5.1). - Sustained-vs-blip (study §3.1): a single 429 is a blip; ≥
sustain_count(3) rate-limit signals withinW(5 min) OR a quorum across ≥2 distinct agents is sustained. Asymmetry: slow to add, faster to cut. - Cost/time budget model (study §6):
projected_spend_per_hour(E) = E x (c-bar / d-bar_hours);spend_capandrpm_capderived from[budget]config + model-registry pricing + OpenRouter free-tier defaults (50 req/day <10 credits, 1000/day ≥10 credits, 20 RPM). Free models: onlyrpm_capbinds. - Safety envelope (study §7): floor (default 1), ceiling (default = static
[coordinator].max_agents), clamp after every move. Kill-switches:budget.enabled=false/pause=truefreezes; credit-exhausted (402) hard-stop setseffective_max_agents = floor/0until a non-zero balance confirmed; daily-wall (usd_per_day) hard-stop. Human pinwg budget pin <N>(recorded with reason + TTL, default 1h) +unpin. - Composition with the global breaker (study §7.1): the breaker OVERRIDES the controller when it trips; a breaker trip is treated as a forced cut (next eval starts from reduced E, cooldown armed).
- Persisted state:
dir/service/budget_state.json—effective_max_agents,floor,ceiling,last_up_at,last_down_at, trailing window counters, budget,kill_switch,pin,moves_24h. Survives daemon restart (so a restart does not reset hysteresis and immediately re-grow into a still-rate-limiting provider).
- The knob:
- Writes the authority field from T1: the controller computes
effective_max_agentsand writes it toCoordinatorState.runtime_max_agents(which T1 made survive a flagless reload). Reads current dispatch state (alive_count,effective_max_agents) fromsrc/commands/service/coordinator.rs:4815,4850. - Config (
src/config.rs): new[budget]section —enabled,usd_per_hour,usd_per_day, and[[budget.limits]]per-model (model,rpm,usd_per_hour). Additive to existing config — no behavior change when absent (controller stays at staticmax_agents). - Daemon-loop integration (
src/commands/service/mod.rs): a parallelcontrol_interval(default 60s) timer that evaluates the controller, reading the T2 rolling window (NOT the instantaneous state). Decoupled from the coordinator spawn tick (spawning is fast/cheap; parallelism decisions must be slow). - CLI (new
src/commands/budget_cmd.rs, register insrc/commands/mod.rs):wg budget {status, pause, pin <N>, unpin}.statusprints JSON + human (thebudget_state.jsonfields from study §10). - Metrics (study §10):
wg budget statusexposes trailing busy-fraction / sustain-429-rate / spend-usd-per-hour / req-per-min + the move log;wg service status/wg spendalready exist.
File scope
- new
src/budget/mod.rs src/config.rs([budget])src/commands/service/mod.rs(daemon-loopcontrol_intervaltimer; the controller writesCoordinatorState.runtime_max_agents)- new
src/commands/budget_cmd.rs,src/commands/mod.rs
Implement directly. The policy + budget + CLI are one cohesive controller; do not split into separate tasks.
Validation
- Unit test: additive-up requires ALL guards (busy≥0.95 AND 429≤1% AND spend≤0.8x budget AND cooldown elapsed AND E<cap); any one failing blocks the grow.
- Unit test: subtractive-down priority — a sustained-429 rate ≥10% forces a cut EVEN IF busy-fraction is high (rate guardrail wins ties).
-
Unit test: hysteresis — after a down-move, no up-move for
cooldown(90s); no second down-move forcut_lockout(30s). The persisted move log proves no oscillation faster than cooldown. - Unit test: sustained-vs-blip — 1-2 isolated 429s (blip) => no change; ≥3 in the window OR ≥2 distinct agents (quorum) => cut.
-
Unit test: budget derivation —
projected_spend_per_hour(E)andrpm_capcomputed correctly from[budget]+ model pricing; a free model binds only onrpm_cap. -
Unit test: kill-switches — credit-exhausted (402) hard-stop sets
effective_max_agentsto floor/0 and blocks grow until balance confirmed; daily-wall trips on cumulative spend ≥usd_per_day. -
Unit test: the controller writes
CoordinatorState.runtime_max_agentsAND it survives a flagless reload (this is the T1 fix the controller depends on — assert it end-to-end). - Unit test: global-outage breaker overrides the controller; a breaker trip is treated as a forced cut with cooldown armed.
-
Unit test:
dir/service/budget_state.jsonround-trips across a simulated daemon restart (hysteresis not reset). -
wg budget statusprints the trailing window + move log;wg budget pin 3/unpinround-trips with reason + TTL. -
cargo build+cargo testpass;cargo fmt --check+cargo clippyclean. - No model pin; routes through the active zai profile.
Depends on
Required by
Log
- 2026-07-25T20:57:11.922529108+00:00 Task paused
- 2026-07-25T20:57:29.497348896+00:00 Task published
- 2026-07-31T11:40:20.861386597+00:00 Task paused
- 2026-07-31T11:40:21.004860667+00:00 Paused as a reversible safety interlock by reframe-supervisor-as-convergence-reconciler. The deterministic daemon reconciler decision draft explicitly requires an operator to choose clean supersession, budget-only narrowing, or in-place rewrite before any controller implementation proceeds. No supersession/edit has been applied yet.
- 2026-07-31T12:40:34.411796729+00:00 Task abandoned: Superseded by the operator-published post-lifecycle synthesis: deterministic convergence is service-owned; no supervisor persona, adaptive outage controller, or composition authority remains.