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

Subscription throughput: a governor that keeps every model subscription's window full, with usage as the forge's efficiency metric #379

Merged opened this in answer to

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

Merged

This pull adds the `usage` ledger, the first run of #341, and a read of it through the API and through MCP. When each agent run ends, the forge writes one row: tokens in and out, the cost the provider reported, wall time, and the outcome (`ok`, `failed`, or `paused`). The row records which subscription, app, agent and issue the run belongs to.

shipped production is serving it deploy 4183511

opened this

This pull adds the usage ledger, the first run of #341, and a read of it through the API and through MCP. When each agent run ends, the forge writes one row: tokens in and out, the cost the provider reported, wall time, and the outcome (ok, failed, or paused). The row records which subscription, app, agent and issue the run belongs to.

Why the previous pull went red, and the fix The check passed; the preview deploy failed at boot with SQLiteError: table usage already exists. When main's share table took migration 36, this branch's ledger moved to 37. But the preview's database had already run the ledger as 36, so on the next deploy it skipped share and ran usage a second time.

Migration 37 now creates usage, and repeats migration 36's share table word for word, both only if they are missing. That does nothing on a database where 36 was share, and fills the gap on the preview's database. A new test in test/usage.test.ts builds such a database and boots it with today's migration list. It fails with the old migration.

What to look at

  • src/core/usage.ts with migration 37: the ledger table and its sums by subscription, app, agent or issue. Each sum is one query.

- A subscription's id is stored as plain text, so removing the account does not erase what it spent. - Writing a run's line a second time replaces it rather than counting the spend twice.

  • src/crew/claude.ts: tokensOf reads Claude Code's result.usage.

- Tokens in = input_tokens + cache_creation_input_tokens + cache_read_input_tokens. - Tokens out = output_tokens.

  • src/crew/runner.ts: a meter adds up the tokens of every model call in a run and remembers the subscription the run was picked onto. The line is written once the run ends. If that write fails, it is logged and the run carries on.

- Main's redactResult now wraps each model call's result, and the meter reads the same redacted result.

  • Reading it: GET /api/v1/repos/:owner/:name/usage?days=1..90 (default 7) and the MCP usage tool both answer from src/actions/usage.ts, so they always agree.

- Only people who can write the repo can read it, the same rule as run cost. - Anyone else gets 403. - A days value outside 1–90 gets 422 invalid_days.

Tests

  • New test/usage.test.ts, in the gate:

- hand-calculated sums; - a real crew run through a fake Claude, where one subscription is refused and the other does the work, with the line written for each run; - the API and MCP access rules; - the migration repair above.

  • I corrected three existing tests that were wrong. The code they test is unchanged:

- test/mcp.test.ts: the tool list didn't include main's own share tool. That file isn't in the gate, so it was already failing on main without anyone seeing it. It now lists share and usage. - test/apps-adversarial.test.ts: the token-expiry test allowed 5 ms between minting a token and checking it. A busy test shard can take longer than that, and it failed that way in two full-gate runs. It now uses 500 ms and checks again after 600 ms, so it tests the same thing without the race. - test/crew-limits.test.ts, test/subscriptions.test.ts: the outcomes they build by hand now include the two new token fields.

  • Before the last fix, the gate plus the MCP and subscription tests ran 552 tests with one failure: that 5 ms test. The fixed file then passed 68/68 on its own. I did not re-run the whole gate after that last edit.

Not done here: planned as linked issues

  • #372: each subscription's 5-hour and weekly windows, plus an operator setting for headroom kept back for people. Waits for this issue.
  • #373: pacing the crew to the remaining window, paused runs labelled with the real reason, and no switching subscription mid-run. Waits for #372.
  • #374: the board's per-subscription window and per-app spend per shipped change. Waits for #372.

Cost is stored in US dollars, as the provider reports it. The forge has no "units" yet, so that conversion is left for the profit-slider work.

Try it: let a crew run finish, then curl -H "authorization: Bearer <token>" $FORGE/api/v1/repos/<owner>/<app>/usage.

🤖 Generated with Claude Code

Closes #341

commented

This pull adds the usage ledger, the first run of #341, and a read of it through the API and through MCP. When each agent run ends, the forge writes one row: tokens in and out, the cost the provider reported, wall time, and the outcome (ok, failed, or paused). The row records which subscription, app, agent and issue the run belongs to.

What to look at

  • src/core/usage.ts with migration 37: the ledger table and its sums by subscription, app, agent or issue. Each sum is one query.

- A subscription's id is stored as plain text, so removing the account does not erase what it spent. - Writing a run's line a second time replaces it rather than counting the spend twice.

  • Migration 37 creates usage, and repeats migration 36's share table word for word, both only if they are missing.

- Before main's share took migration 36, this branch's ledger was 36. The earlier preview's database had already run it under that number. - That database then skipped share and failed to boot with table usage already exists. That failure is what closed #375. - On every other database these statements do nothing; on that one they fill the gap. A test in test/usage.test.ts recreates that database and boots it.

  • src/crew/claude.ts: tokensOf reads Claude Code's result.usage.

- Tokens in = input_tokens + cache_creation_input_tokens + cache_read_input_tokens. - Tokens out = output_tokens.

  • src/crew/runner.ts: a meter adds up the tokens of every model call in a run, after main's redactResult, and remembers the subscription the run was picked onto. The line is written once the run ends. If that write fails, it is logged and the run carries on.
  • Reading it: GET /api/v1/repos/:owner/:name/usage?days=1..90 (default 7) and the MCP usage tool both answer from src/actions/usage.ts, so they always agree.

- Only people who can write the repo can read it, the same rule as run cost. - Anyone else gets 403. - A days value outside 1–90 gets 422 invalid_days.

  • Latest merge with main: the only conflict was the gate's test list in forge.json. It keeps main's new forge-cloudflare test and adds usage. Main added no migration, so 37 is still the next number.

Tests

  • New test/usage.test.ts, in the gate:

- hand-calculated sums; - a real crew run through a fake Claude, where one subscription is refused and the other does the work, with the line written for each run; - the API and MCP access rules; - the migration repair above.

  • I corrected three existing tests that were wrong. The code they test is unchanged:

- test/mcp.test.ts: its list of tools didn't include main's own share tool, so it failed on main. That file isn't in the gate, so nobody saw it. It now lists share and usage. - test/apps-adversarial.test.ts: the token-expiry test allowed only 5 ms between creating a token and checking it, and a busy test shard can take longer. It now uses 500 ms and checks again after 600 ms. - test/crew-limits.test.ts, test/subscriptions.test.ts: the outcomes they build by hand now include the two new token fields.

  • After the merge, the full gate plus test/mcp.test.ts passes: 542 tests, none failing.

Not done here: planned as linked issues

  • #372: each subscription's 5-hour and weekly windows, plus an operator setting for headroom kept back for people. Waits for this issue.
  • #373: pacing the crew to the remaining window, paused runs labelled with the real reason, and no switching subscription mid-run. Waits for #372.
  • #374: the board's per-subscription window and per-app spend per shipped change. Waits for #372.

Cost is stored in US dollars, as the provider reports it. The forge has no "units" yet, so that conversion is left for the profit-slider work.

Try it: let a crew run finish, then curl -H "authorization: Bearer <token>" $FORGE/api/v1/repos/<owner>/<app>/usage.

🤖 Generated with Claude Code

commented

This pull adds the usage ledger, the first run of #341, and a read of it through the API and through MCP. When each agent run ends, the forge writes one row: tokens in and out, the cost the provider reported, wall time, and the outcome (ok, failed, or paused). The row records which subscription, app, agent and issue the run belongs to.

What to look at

  • src/core/usage.ts with migration 37: the ledger table and its sums by subscription, app, agent or issue. Each sum is one query.

- A subscription's id is stored as plain text, so removing the account does not erase what it spent. - Writing a run's line a second time replaces it rather than counting the spend twice.

  • Migration 37 creates usage, and repeats migration 36's share table word for word, both only if they are missing.

- Before main's share took migration 36, this branch's ledger was 36. The earlier preview's database had already run it under that number. - That database then skipped share and failed to boot with table usage already exists. That failure is what closed #375. - On every other database these statements do nothing; on that one they fill the gap. A test in test/usage.test.ts recreates that database and boots it.

  • src/crew/claude.ts: tokensOf reads Claude Code's result.usage.

- Tokens in = input_tokens + cache_creation_input_tokens + cache_read_input_tokens. - Tokens out = output_tokens.

  • src/crew/runner.ts: a meter adds up the tokens of every model call in a run, after main's redactResult, and remembers the subscription the run was picked onto. The line is written once the run ends. If that write fails, it is logged and the run carries on.
  • Reading it: GET /api/v1/repos/:owner/:name/usage?days=1..90 (default 7) and the MCP usage tool both answer from src/actions/usage.ts, so they always agree.

- Only people who can write the repo can read it, the same rule as run cost. - Anyone else gets 403. - A days value outside 1–90 gets 422 invalid_days.

  • Latest merge with main: the only conflict was the gate's test list in forge.json. It keeps main's new cloud and run-secrets tests and adds usage. Main changed no migrations, so 37 is still the next number.

Tests

  • New test/usage.test.ts, in the gate:

- hand-calculated sums; - a real crew run through a fake Claude, where one subscription is refused and the other does the work, with the line written for each run; - the API and MCP access rules; - the migration repair above.

  • I corrected three existing tests that were wrong. The code they test is unchanged:

- test/mcp.test.ts: its list of tools didn't include main's own share tool, so it failed on main. That file isn't in the gate, so nobody saw it. It now lists share and usage. - test/apps-adversarial.test.ts: the token-expiry test allowed only 5 ms between creating a token and checking it, and a busy test shard can take longer. It now uses 500 ms and checks again after 600 ms. - test/crew-limits.test.ts, test/subscriptions.test.ts: the outcomes they build by hand now include the two new token fields.

  • After the merge, the full gate plus test/mcp.test.ts passes: 560 tests, none failing.

Not done here: planned as linked issues

  • #372: each subscription's 5-hour and weekly windows, plus an operator setting for headroom kept back for people. Waits for this issue.
  • #373: pacing the crew to the remaining window, paused runs labelled with the real reason, and no switching subscription mid-run. Waits for #372.
  • #374: the board's per-subscription window and per-app spend per shipped change. Waits for #372.

Cost is stored in US dollars, as the provider reports it. The forge has no "units" yet, so that conversion is left for the profit-slider work.

Try it: let a crew run finish, then curl -H "authorization: Bearer <token>" $FORGE/api/v1/repos/<owner>/<app>/usage.

🤖 Generated with Claude Code

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs