Skip to content

Fix: make the ledger agree with the ring, rotate archive segments, and cache past day files - #1602

Closed
huang195 wants to merge 1 commit into
rossoctl:mainfrom
huang195:fix/cost-reads-1566
Closed

huang195 wants to merge 1 commit into
rossoctl:mainfrom
huang195:fix/cost-reads-1566

Conversation

@huang195

Copy link
Copy Markdown
Member

What

Closes #1566 — the cost/session read paths counted the same traffic by different rules and re-decoded on every poll. All five items from the audit, in one PR.

1. The ledger hid a coverage gap the ring reported

The ledger's priceable rule required a non-zero token count, so a truncated OpenAI-dialect stream — 200, a model, and no usage block, because every counter lives on the final chunk — was invisible on today/7d/month while the ring counted it and named it in UnpricedBy. Nine priced requests plus one of those read "9/9 priced" on today against "9/10" on the 1h window.

Fix: usage.Priceable states the ring's rule once (model recognised; zero usage counts as priceable only when the response succeeded), and the ledger writer calls it instead of restating its own.

2. The error rule was written twice and only one copy was updated

The ledger counted any 400+ or denial as an error — predating the ring's destination_private and upgrade_declined exemptions — so an egress-guard refusal read as a failing proxy on today but not on 1h.

Fix: usage.CountsAsError (the ring's rule, exported) is now called from both halves.

3. Archive segments never rotated on size

A segment closed only on 60-minute idle, the open-writer cap, or delete/clear/rename — never on size — and planPrune skips open segments in both its age and maxBytes loops. A session busy past the idle window kept one segment growing without limit, and the archive's size bound was unenforceable against exactly the sessions that produced the most bytes.

Fix: segments rotate at 128 MiB (WithMaxSegmentBytes to configure). The check runs in the tick after the flush, because a segment's byte count is only real once its zstd encoder has been flushed — a write-time check cannot see the cap cross. The cost is one tick of overshoot.

4. Every ledger query re-decoded every day file

Query opened and fully decoded each day file on every call with no cache. agentop polls today every minute and 7d/month every five; on the 31st, the month span decoded 31 files per poll, and past days almost never change.

Fix: past days are cached in the store, keyed on the file's size and mtime (a changed past day is re-read, not served stale); the current day is never cached. Validation uses Stat, not Open. The "today" decision uses the writer's clock, not the machine's.

5. Comments that contradicted the code

Four comments in core/cost read like notes from the middle of a PR stack: the accumulation wrap called "an open defect" though Counts.Add saturates; MaxPlausibleTokens declared importer-less though the ledger writer imports it; plausibleTokenReport/RefusedTokenRequests declared nonexistent; Dropped() declared caller-less. All corrected — a reader was concluding that sums can still wrap and token bounds are not enforced.

Testing

Every fix is pinned by a test that was red on main and green after:

  • TestRecord_ZeroUsageSuccessIsPriceable / TestRecord_DestinationPrivateIsNotAnError — the ledger/ring parity, asserted through the ledger's own day file.
  • TestWrite_RotatesASegmentAtTheByteCap — rotation at a one-byte cap: two segments on disk, all events readable in sequence order across the boundary.
  • TestQuery_APastDayIsServedWithoutReopeningTheFile — the cache observed through permissions (a chmod'ed past day still answers; the uncached read cannot Open it). Runs vacuously as root.
  • TestQuery_APastDayCacheIsInvalidatedWhenTheFileChanges / TestQuery_TodaysFileIsNeverCached — the cache's honesty both ways.

Full core/cost/..., core/session/..., core/sessionapi/... suites green; go vet and gofmt clean on the touched packages.

Assisted-By: Claude (Anthropic AI) noreply@anthropic.com

…d cache past day files

Five defects from the cost/session read-path audit (rossoctl#1566):

1. Coverage parity: the ledger's priceable rule required a non-zero token
   count, so a truncated OpenAI-dialect stream (200, a model, no usage
   block) hid a coverage gap on today/7d/month that the ring's duration
   windows reported. usage.Priceable is the ring's rule stated once; the
   ledger writer now calls it.

2. Error parity: the ledger counted any 400+/denial as an error, predating
   the ring's destination_private and upgrade_declined exemptions, so a
   guard refusal read as a failing proxy on today but not on 1h. The rule
   is now usage.CountsAsError, called from both.

3. Archive segments never rotated on size: a session busy past the idle
   window kept one segment growing without limit, and planPrune skips open
   segments in both its age and maxBytes loops, so the archive's size bound
   was unenforceable against exactly the busiest sessions. Segments now
   rotate at 128 MiB, checked in the tick after the flush (a segment's byte
   count is only real once its zstd encoder has been flushed).

4. Every ledger query re-decoded every day file in the window: agentop
   polls today each minute and 7d/month each five, so a month span decoded
   31 files per poll and past days almost never change. Past days are now
   cached keyed on (size, mtime); today is never cached.

5. Four comments in core/cost described code that has since changed (the
   accumulation "open defect" that Counts.Add's saturating addInto closed,
   "NO IMPORTER ON THIS BRANCH", "neither exists at this commit", and
   Dropped()'s "no non-test caller") — corrected so a reader stops
   concluding that sums can wrap and token bounds are unenforced.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
@huang195
huang195 requested a review from a team as a code owner October 30, 2026 13:13
@coderabbitai

coderabbitai Bot commented Oct 30, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 47 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: dc79f96e-8f74-4d78-9c11-9deb703d4712
Commits

Reviewing files that changed from the base of the PR and between 20ffb36 and 9ba1fcd.

Files selected for processing: 8
  • core/cost/ledger/daycache_test.go
  • core/cost/ledger/parity_test.go
  • core/cost/ledger/store.go
  • core/cost/ledger/writer.go
  • core/cost/pricing/cost.go
  • core/cost/usage/usage.go
  • core/session/archive/archive.go
  • core/session/archive/rotation_test.go
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@huang195

Copy link
Copy Markdown
Member Author

Closing as a duplicate: #1593 (same issue, same design — ring/ledger parity via shared usage predicates, 128 MiB segment rotation, (size, mtime) day cache) merged while this was being written. Nothing here that #1593 lacks; the conflict is with its own changes. Kept the branch for reference.

@huang195 huang195 closed this Oct 30, 2026
@github-project-automation github-project-automation Bot moved this from New/ToDo to Done in Rossoctl Issue Prioritization Oct 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

bug: Ring and ledger count errors and coverage differently, cost reads slow down over time, and cost comments are stale

1 participant