Skip to content

Migrate burn plans rolling-window usage to archive (#91)#131

Open
willwashburn wants to merge 1 commit intomainfrom
feat/plans-from-archive-91
Open

Migrate burn plans rolling-window usage to archive (#91)#131
willwashburn wants to merge 1 commit intomainfrom
feat/plans-from-archive-91

Conversation

@willwashburn
Copy link
Copy Markdown
Member

@willwashburn willwashburn commented Apr 26, 2026

Summary

  • 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.
  • New helper planUsageFromArchive in @relayburn/analyze lives alongside computePlanUsage and reuses costForTurn's source-aware reasoning override, so Codex output_tokens is not double-billed against usage.reasoning.
  • --no-archive (or RELAYBURN_ARCHIVE=0) keeps the in-memory queryAll() 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)
  • Analyze-layer parity: planUsageFromArchive matches computePlanUsage byte-for-byte on the parity fixture (packages/analyze/src/plan-usage.test.ts).
  • Reset-day boundary: turn at exact cycleEnd ISO falls in the next cycle (half-open [start, end)).
  • Provider matchers: claude (claude-code + anthropic-api), cursor (no-op short-circuit), custom (every source) all match the in-memory path.
  • Multi-source / multi-model GROUP BY aggregates correctly (claude-sonnet-4-5 + gpt-5-mini mixed).
  • Codex reasoning override prevents double-billing output_tokens against usage.reasoning.
  • CLI surface: runPlans text and --json output identical between archive and --no-archive paths for single-plan and multi-plan fixtures (packages/cli/src/commands/plans.test.ts).
  • RELAYBURN_ARCHIVE=0 env 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


Open in Devin Review

`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>
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

View 5 additional findings in Devin Review.

Open in Devin Review

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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Migrate burn plans rolling-window usage to archive query

1 participant