Migrate burn plans rolling-window usage to archive (#91)#131
Migrate burn plans rolling-window usage to archive (#91)#131willwashburn wants to merge 1 commit intomainfrom
Conversation
`burn plans` (list view) now computes per-plan spend with one `SUM(...) GROUP BY (source, model)` aggregate against the archive's `turns` table instead of walking the full ledger once per plan. Output is byte-identical to the legacy `queryAll()` reduce path on the parity fixture (text and `--json`); reset-day boundaries, `limitedData` flagging, and built-in presets all carry over. The new helper `planUsageFromArchive` lives in `@relayburn/analyze` alongside `computePlanUsage` and reuses `costForTurn`'s source-aware reasoning override, so Codex `output_tokens` is not double-billed against `usage.reasoning`. Pass `--no-archive` (or set `RELAYBURN_ARCHIVE=0`) to opt back into the in-memory reduce path while the migration shakes out — covered by a parity test against the archive path. Tests: parity fixture (analyze layer + CLI surface), reset-day boundary correctness, multi-plan list output (text + `--json`), `RELAYBURN_ARCHIVE` env knob, multi-source / multi-model GROUP BY, Codex reasoning double-bill regression. Refs: closes #91, refs #5, #39, #40, #78. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| const pricing = await loadPricing(); | ||
| // Pull the widest cycle window across plans so we only walk the ledger | ||
| // once. Cheaper than per-plan queryAll for users with several plans. | ||
| const useArchive = opts.useArchive ?? true; |
There was a problem hiding this comment.
🟡 burn limits silently switches to archive path without opt-out mechanism
The statusForPlans default changed from in-memory reduce (pre-PR, no archive path existed) to useArchive: true (packages/cli/src/commands/plans.ts:266). The burn limits command calls statusForPlans(plans) without options at packages/cli/src/commands/limits.ts:484, so it now silently uses the archive path. Unlike burn plans which wires shouldUseArchive(args) (checking --no-archive flag and RELAYBURN_ARCHIVE=0 env var), burn limits has no way for users to opt out. If a user sets RELAYBURN_ARCHIVE=0 expecting the legacy path everywhere, burn limits will still call buildArchive() and query archive.sqlite, producing an inconsistency between the two commands that share the same statusForPlans function. The PR's CLI changelog only mentions "burn plans (list view)" adopting the archive, not burn limits.
Prompt for agents
The default value for useArchive in statusForPlans was changed to true, but the burn limits caller at packages/cli/src/commands/limits.ts:484 calls statusForPlans(plans) without options, silently inheriting the archive path with no way for users to opt out via --no-archive or RELAYBURN_ARCHIVE=0.
Two possible fixes:
1. Change the default back to false so callers must explicitly opt in. Update runList in plans.ts to pass useArchive: shouldUseArchive(args) (which it already does).
2. Have defaultLoadPlanStatuses in limits.ts also respect the RELAYBURN_ARCHIVE env var by reading it and passing useArchive accordingly.
Option 1 is safer because it avoids silently changing behavior for any other current or future callers of statusForPlans.
Was this helpful? React with 👍 or 👎 to provide feedback.
Summary
burn plans(list view) now computes per-plan spend with oneSUM(...) GROUP BY (source, model)aggregate against the archive'sturnstable instead of walking the full ledger once per plan. Output is byte-identical to the legacyqueryAll()reduce path on the parity fixture (text and--json); reset-day boundaries,limitedDataflagging, and built-in presets all carry over.planUsageFromArchivein@relayburn/analyzelives alongsidecomputePlanUsageand reusescostForTurn's source-aware reasoning override, so Codexoutput_tokensis not double-billed againstusage.reasoning.--no-archive(orRELAYBURN_ARCHIVE=0) keeps the in-memoryqueryAll()reduce path available while the migration shakes out — covered by a parity test against the archive path.Test plan
pnpm run test:ts(521 tests pass on a clean rebuild)planUsageFromArchivematchescomputePlanUsagebyte-for-byte on the parity fixture (packages/analyze/src/plan-usage.test.ts).cycleEndISO falls in the next cycle (half-open[start, end)).claude-code+anthropic-api), cursor (no-op short-circuit), custom (every source) all match the in-memory path.claude-sonnet-4-5+gpt-5-minimixed).output_tokensagainstusage.reasoning.runPlanstext and--jsonoutput identical between archive and--no-archivepaths for single-plan and multi-plan fixtures (packages/cli/src/commands/plans.test.ts).RELAYBURN_ARCHIVE=0env knob renders identically to--no-archive.Refs
Closes #91. Refs #5, #39, #40, #78.
Performance acceptance criterion (>=100k turns, 3 plans, <100ms after archive is current) was not benchmarked in CI — the SQL aggregate replaces a full-ledger reduce per plan, so the asymptotic improvement is immediate; happy to add a benchmark in a follow-up if useful.
Generated with Claude Code