Phase D -- Budget hard ceiling¶
Status: Done
Goal¶
Make daemon budget a hard ceiling under concurrency: eliminate material overshoot, record spend on timeout/cancel paths, optionally persist totals across restarts, and clarify the budget_usd=0 unlimited footgun.
Why (problems addressed -- bullet list with severity)¶
- P0: Concurrent budget overshoot --
CostBudgetGuardchecked at phase entry; N concurrent agents can each pass before anyrecord()(tests/adversarial/test_daemon_resilience.py::test_concurrent_goal_generation_overshoot_boundeddocuments PARTIAL gap). - P0: Cycle timeout drops spend --
AgentCycleRunner.run_guarded()abandons goal on timeout without reading partialGoalOutcome/ provider cost (src/hive/daemon/agent_cycle.pylines 56--78). - P1: Budget not persisted --
BudgetTrackerin-memory only (src/hive/daemon/budget.py); restart resets spend. - P1:
budget_usd=0means unlimited -- operators misconfigure kill-switch (src/hive/config.pyDaemonConfig, stability-01 documented but footgun remains). - P2:
GeneratedGoalsoft metering -- generation spend recorded after LLM call; race window beforerecord()(agent_cycle.pylines 501--505).
Related issues bundled¶
| ID | Finding |
|---|---|
| REL-BUDGET-01 | Concurrent overshoot ≤ N × generation cost |
| REL-BUDGET-02 | Timeout path skips _budget.record() |
| REL-BUDGET-03 | No durable budget ledger |
| REL-BUDGET-04 | Zero budget = unlimited semantics |
| REL-BUDGET-05 | Post-call record race |
Current state (files)¶
| Area | Location | Behavior today |
|---|---|---|
| Tracker | src/hive/daemon/budget.py |
Async lock on record(); 0 = unlimited |
| Guard | src/hive/daemon/gates.py CostBudgetGuard |
Phase entry check |
| Kill-switch | src/hive/daemon/loop.py |
_budget_exceeded flag (stability-01) |
| Recording | src/hive/daemon/agent_cycle.py |
After pursuit/generation completes |
| Tests | tests/adversarial/test_daemon_resilience.py |
Bounded overshoot test (xfail/document) |
| REST | src/hive/server/routes/system.py |
GET /budget |
Proposed changes (numbered)¶
- Reservation model (preferred):
- Add
BudgetTracker.reserve(estimate_usd, estimate_tokens) -> Reservation | Nonebefore LLM call; commit actual on completion; release on failure. - Estimates: configurable defaults per phase (
daemon.budget_reserve_usd_generation,daemon.budget_reserve_usd_pursuit) or last-moving-average. -
Guard checks
available = budget - spent - reserved. -
Serialize spend-critical sections (fallback if reservation too heavy):
-
Global asyncio lock around goal generation + pursuit LLM invocations only (not whole cycle).
-
Timeout / cancel spend:
- On
asyncio.wait_fortimeout, capture partial cost from provider/adapter if available; alwaysrecord()best-effort. -
Revisit timeout -->
abandon_goalpolicy (coordinate Phase B/C): budget fix should not lose spend even if goal continues. -
Persist budget (optional, config-gated):
- Store
spent_usd,spent_tokensin.hive/budget.jsonor SQLite row; load on daemon start. -
hive budget resetCLI +POST /budget/reset. -
Operator clarity for zero = unlimited:
hive daemon/GET /budgetmust print"unlimited (budget_usd=0)"prominently.-
Optional validation warning in
hive config validatewhen both limits zero anddaemon.warn_unlimited_budget: true(default true in docs template only). -
Tests:
- Replace/document
test_concurrent_goal_generation_overshoot_boundedwith strict ceiling test under reservation. - Add timeout spend test with mock provider returning partial usage.
Non-goals¶
- Per-agent budget caps (profile
max_cost_usdis Phase B runtime concern). - Billing integration / invoices.
- Changing fail-closed guard policy (stability-01 done).
Risks / rollback¶
| Risk | Mitigation |
|---|---|
| Over-reservation stalls agents | Conservative estimates + release on all exit paths |
| Persist file corruption | Atomic write; rebuild from logs optional |
| Stricter budget breaks dev workflows | Document budget_usd: 0; template uses non-zero in production example |
Rollback: daemon.budget_mode: record_only disables reservation.
Acceptance criteria (testable)¶
- [x] With
budget_usd=0.10and 4 concurrent agents, totalspent_usd≤0.10 + epsilon(epsilon documented as estimate slack only). - [x] Cycle timeout after mock LLM call still increments
spent_usd/spent_tokens. - [x]
GET /budgetshowsunlimitedwhen limits are zero. - [x] Optional: restart daemon preserves spend when persistence enabled.
Suggested implementation order¶
- Reservation API + unit tests in
tests/test_budget.py. - Wire pursuit + generation in
agent_cycle.py. - Timeout partial record.
- Persistence + CLI (optional sub-PR).
- Adversarial strict test + docs (
docs/guide/daemon-mode.md,docs/getting-started/cli-quickstart.md).
Estimate¶
M (2--4 days; +1 day if persistence included).
Dependencies (prior phases)¶
- Phase B -- pursuit/generation exit paths must report cost consistently before reservation commit logic.