Adaptive budget: pace spend to each capacity pool's balance and reset time instead of a fixed daily cap #82

Closed
opened 2026-10-17 23:09:49 +00:00 by teahubxyz · 7 comments
Owner

Why. TIME_DAILY_USD / TIME_MONTHLY_USD are fixed numbers. The factory actually draws on several pools with different shapes: API credits that expire on a date, subscription allowances that refill on a window, Venice credits that are a balance. A fixed daily cap leaves expiring credit unspent on quiet days and starves busy ones. The operator's rule: look at actual usage and reset times, and budget to them. Still deterministic: no model decides whether to spend.

Change (.github/time/time.py, governor section).

  • time/pools.json (committed, operator-maintained at first): one entry per pool — name, kind (expiring | window | balance), balance_usd (or window_usd), resets_at (ISO date for expiring, a cadence such as weekly:mon or hours:5 for window), and which routes book to it (anthropic, venice, codex). The ledger rows gain a pool field from the model's provider.
  • allowance(pool, now): for expiring, (balance − spent_since_reset) / days_to_reset × TIME_PACE (default 1.0; 0.8 leaves headroom); for window, the window's remaining amount; for balance, the fixed daily cap as today. budget_check() checks the pool the call books to, and the daily/monthly caps become the fallback when a pool has no entry.
  • cmd_doctor prints each pool: balance, spent since reset, next reset, today's allowance. The Astra log line reports spend per pool, not one number.
  • Later card: pull balances from provider APIs where one exists; until then the operator updates pools.json and the factory paces to it.

Tests. An expiring pool with $30 left and 10 days to reset allows $3/day and refuses the fourth dollar; the allowance rises after a day passes with no spend; a window pool refuses once the window is spent and allows again after its reset; a route with no pool falls back to TIME_DAILY_USD; the reservation in #75 counts against the pool's allowance, not the global cap.

CHANGELOG line under "### Added".

DONE: PR open with the code above, python3 .github/time/test_time.py green including the new checks, CI green

**Why.** `TIME_DAILY_USD` / `TIME_MONTHLY_USD` are fixed numbers. The factory actually draws on several pools with different shapes: API credits that expire on a date, subscription allowances that refill on a window, Venice credits that are a balance. A fixed daily cap leaves expiring credit unspent on quiet days and starves busy ones. The operator's rule: look at actual usage and reset times, and budget to them. Still deterministic: no model decides whether to spend. **Change (`.github/time/time.py`, governor section).** - `time/pools.json` (committed, operator-maintained at first): one entry per pool — `name`, `kind` (`expiring` | `window` | `balance`), `balance_usd` (or `window_usd`), `resets_at` (ISO date for `expiring`, a cadence such as `weekly:mon` or `hours:5` for `window`), and which routes book to it (`anthropic`, `venice`, `codex`). The ledger rows gain a `pool` field from the model's provider. - `allowance(pool, now)`: for `expiring`, `(balance − spent_since_reset) / days_to_reset × TIME_PACE` (default 1.0; 0.8 leaves headroom); for `window`, the window's remaining amount; for `balance`, the fixed daily cap as today. `budget_check()` checks the pool the call books to, and the daily/monthly caps become the fallback when a pool has no entry. - `cmd_doctor` prints each pool: balance, spent since reset, next reset, today's allowance. The Astra log line reports spend per pool, not one number. - Later card: pull balances from provider APIs where one exists; until then the operator updates `pools.json` and the factory paces to it. **Tests.** An expiring pool with $30 left and 10 days to reset allows $3/day and refuses the fourth dollar; the allowance rises after a day passes with no spend; a window pool refuses once the window is spent and allows again after its reset; a route with no pool falls back to `TIME_DAILY_USD`; the reservation in #75 counts against the pool's allowance, not the global cap. CHANGELOG line under "### Added". DONE: PR open with the code above, `python3 .github/time/test_time.py` green including the new checks, CI green
teahubxyz added the queued label 2026-10-18 01:55:34 +00:00
time added ready and removed queued labels 2026-10-18 16:07:37 +00:00

⏱️ Time: today's spend is $15.55 of the $15.00 cap; carrying on tomorrow. Nothing is lost; this card resumes once there is budget.

⏱️ Time: today's spend is $15.55 of the $15.00 cap; carrying on tomorrow. Nothing is lost; this card resumes once there is budget. <!-- time {"beat":"capped"} -->

⏱️ Time: Claude Sonnet (Anthropic) is working on this now, on branch time/card-82.

⏱️ Time: Claude Sonnet (Anthropic) is working on this now, on branch `time/card-82`. <!-- time {"beat":"dispatched","launched":true,"worker":"anthropic"} -->

⏱️ Time: can't go further on this card: 'command'. I'll carry on once it's added.

⏱️ Time: can't go further on this card: 'command'. I'll carry on once it's added. <!-- time {"beat":"blocked"} -->

⏱️ Time: dispatched 6h ago and no PR closing #82 yet. Check the agent. I'll dispatch again on my own next beat; comment /time retry to force one.

⏱️ Time: dispatched 6h ago and no PR closing #82 yet. Check the agent. I'll dispatch again on my own next beat; comment `/time retry` to force one. <!-- time {"beat":"stuck"} -->

⏱️ Time: Claude Sonnet (Anthropic) is working on this now, on branch time/card-82. Retrying on my own, resuming the branch.

⏱️ Time: Claude Sonnet (Anthropic) is working on this now, on branch `time/card-82`. Retrying on my own, resuming the branch. <!-- time {"beat":"dispatched","launched":true,"worker":"anthropic","auto_retry":true} -->

⏱️ Time: sonnet opened PR #167 (21 steps, claude-sonnet-5-5). Review and landing follow on the next beat.

⏱️ Time: `sonnet` opened PR #167 (21 steps, claude-sonnet-5-5). Review and landing follow on the next beat. <!-- time {"beat":"dispatched","worker":"anthropic","auto_retry":true,"pr":167,"steps":21,"seat":"sonnet","model":"claude-sonnet-5-5"} -->

⏱️ Time: PR #167 is up for this card. haiku reviewed it on the PR.

⏱️ Time: PR #167 is up for this card. `haiku` reviewed it on the PR. <!-- time {"beat":"pr","pr":167,"launched":false,"seat":"haiku","as":"time","review":"venice","seats":["haiku"]} -->
Sign in to join this conversation.
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: teahubxyz/time#82