Public: anyone can read this; the crew can change it.

Subscription standing: each account's 5-hour and weekly windows, used and resetting, read where the provider says and inferred from the usage ledger where it does not #380

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

Each subscription now has a standing for its 5-hour and weekly windows. For each window it reports start, reset time, cap, used, remaining, the share kept for people, and what the crew can still spend. Every figure is labelled `read` (the provider said it) or `inferred` (worked out from the usage ledger). Money is in US dollars at list price, the same unit as `usage.cost_usd`.

shipped production is serving it deploy 4183511

opened this

Each subscription now has a standing for its 5-hour and weekly windows. For each window it reports start, reset time, cap, used, remaining, the share kept for people, and what the crew can still spend. Every figure is labelled read (the provider said it) or inferred (worked out from the usage ledger). Money is in US dollars at list price, the same unit as usage.cost_usd.

  • src/core/standing.ts (new): standing(db, subscriptionId, now) returns plain data. The module comment states the rules:

- A run counts in the window it started in. - When the provider hasn't named a reset, a window opens at the first run after the previous window closed and lasts 5 hours or 7 days. - A weekly reset the provider named repeats every 7 days after it passes, marked inferred. - The cap is what the crew had spent in that window when the provider last called it full. A person's own use isn't in the ledger, so the cap is a minimum. Until the provider has refused once, the cap and remaining are null rather than a guess.

  • Provider word (crew/limits.ts, crew/runner.ts): new limitOf(message, now) reads which window is full and its exact reset. A dated reset, or one called "weekly", is the week; a bare clock time is the 5-hour window. The runner saves it with noteLimit when it rests an account. resumeAt gives the same results as before; it now shares the same parser.
  • Migration 38: a subscription_limit table holding the latest refusal per window, and subscription.headroom_pct, a whole percent from 0 to 90.
  • Headroom setting: Settings → Subscriptions shows one line per window ("~" marks inferred figures, with a note on the page saying so) and a 0–90% field per account. It posts to POST /settings/subscriptions/:id/headroom, which is admin only, origin-checked and validated.
  • Exposure: GET /api/v1/repos/:owner/:name/usage and the MCP usage tool both gain standing[] through readUsage. It lists the accounts granted on the repository or its owner, plus any that paid for runs there. The windows cover each account's use across the whole forge, because that is how the provider meters it.

Tests:

  • test/standing.test.ts checks hand-worked windows: the chain boundary at exactly 5 hours, a window that is full on the provider's word, the cap and the headroom arithmetic, the weekly reset carried forward, and edge cases.
  • test/usage.test.ts checks over HTTP and MCP that the stub's "weekly limit · resets Sep 26 at 9pm" comes back as a full week marked read.
  • test/subscriptions.test.ts covers the headroom form, including refusals for non-admins and cross-origin posts.

bun run check passes (555 tests), and I added standing.test.ts to the check list in forge.json. The related suites outside the check (subscriptions, mcp, settings, counts, api) also pass.

Not done:

  • Claude Code's older epoch-style "usage limit reached|<epoch>" message isn't parsed.
  • The headroom isn't enforced yet: nothing stops a run when the crew's remaining share reaches 0. That belongs to the governor.
  • Standing costs a few queries per account. That's fine for a handful of accounts, but it isn't a grouped query.

To try it: add a subscription under Settings → Subscriptions, set a headroom, run the crew on a granted repository, then curl -H "authorization: Bearer $TOKEN" $FORGE/api/v1/repos/<owner>/<repo>/usage | jq .standing.

🤖 Generated with Claude Code

Closes #372

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs