Skip to content

Count drifted Codex ledger mirrors once - #4290

Merged
steipete merged 17 commits into
steipete:mainfrom
gabrielrojasc:bugfix/codex-ledger-mirror-window
Oct 7, 2026
Merged

steipete merged 17 commits into
steipete:mainfrom
gabrielrojasc:bugfix/codex-ledger-mirror-window

Conversation

@gabrielrojasc

@gabrielrojasc gabrielrojasc commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #4289.

Summary

A response's token_usage_record and its token_count mirror are counted twice when Codex's token_count counter has drifted from the thread counter, for example after a resume. This PR pairs them by turn, identical usage, and time or counter offset, while keeping the #4195 protections.

 CodexRequestLedgerState
+  pendingLedgerNearMirror / pendingLegacyNearMirror   // turn + input/cached/output key, timestamp, own counter
+  mirrorCounterOffset                                 // ledger thread total − token_count total of the latest pair
+  nearMirrorsDisabled                                 // bare usage counted while an observation is buffered; exact pairing only

 adjacent pairing (existing pending-mirror path)
   exact timestamped key  ||  thread-total counter alias           (unchanged)
+  || same near key within 5 s                                     (either order)
+  || same near key with offset == mirrorCounterOffset             (ledger first only: token_count after a tool run)
+  known replays never pair, seed, or teach an offset by near key  (exact keys only, as before)
+  the second observation's counter must advance by no more than its usage since that counter's last observation

 pending state cleared by
   task_started, turn_context, any parsed token_count               (unchanged)
+  same-leaf resume session_meta (also clears the offset), counted bare usage

 CostUsageFileUsage.currentCodexParserRevision  8 -> 9               (one bounded reparse; saved prices kept)
 CostUsageStore: "ed735dc27ffa70d9" adopted; its retained report is dropped
  • Kept from Fix resumed Codex usage with request ledger identities #4195. Exact mirrors still include timestamps. Equal request sizes alone still don't establish a mirror: the two-minute test is unchanged, a legacy-first pair never uses the offset, a resume between observations keeps them distinct, and a replayed response never pairs by near key. Bare usage is counted when read, while subagent observations are buffered and processed later, so a bare line read after a buffered subagent observation leaves the rest of that file with exact pairing only. Other files are processed in read order and keep the window.
  • Why the window and the offset. Codex writes both events at completion. In one real history, 89% of drifted pairs are under 5 s apart (median 7 ms). The rest follow a tool run (median 17 s); 99.6% of those keep the previous pair's counter offset. Every adjacent same-usage pair in that history puts the ledger record first.
  • Counter continuity. In the real history above, no counter in any of 62,217 in-window pairs advanced by more than its usage, while 330 pairs changed offset without a resume. So the check rejects only counters that skip ahead, not offset changes.
  • Known limit. When a fork's parent resolves only in a later pass, the scanner reprocesses the buffered ledger records as replays, and replays never pair by near key. A drifted pair that was not already paired before that pass, in either order, then stays two rows, as on main. A cold parse pairs it.
  • Existing caches. Revision 9 reparses files once through the bounded parser upgrade, which drops the stored duplicate rows and keeps the ledger rows' saved pricing.

Evidence

  • Real history (.github/pr-proof/codex-ledger-mirror-window-real-setup.log). The CLI cost path ran over one real Codex history from a copy of a 0.72.0 cache. main and this branch each ran until fully scanned:

    main @ 6a26b2e9b this branch
    ledger rows 66,204 66,206 (a live session appended 2)
    legacy rows in files with ledger rows 40,807 1,714
    30-day tokens 13.56B 8.10B

    No file loses ledger rows or gains legacy rows. A raw-log pass finds 66,336 token_count events beside a ledger record with identical usage, holding 8.21B tokens. The 5.46B removed fits inside that pool.

    Temporary harness (not committed)
    import Foundation
    @testable import CodexBarCore
    import Testing
    @testable import CodexBarCLI
    
    // Temporary, uncommitted proof harness: the CLI's fetch and render path over the real ~/.codex logs with a copy of
    // the live cost cache. Reports tokens and row counts; dollar amounts are redacted.
    @Suite(.serialized, .enabled(if: ProcessInfo.processInfo.environment["CODEXBAR_REAL_PROOF"] == "1"))
    struct ZZMirrorRealSetupProofTests {
        @Test func `real setup duplicate mirror report`() async throws {
            let env = ProcessInfo.processInfo.environment
            let source = try #require(env["CODEXBAR_PROOF_CACHE_SOURCE"].map(URL.init(fileURLWithPath:)))
            let output = try #require(env["CODEXBAR_PROOF_OUTPUT"])
            let cacheRoot = FileManager.default.temporaryDirectory.appendingPathComponent("codexbar-mirror-proof-\(UUID())")
            try FileManager.default.copyItem(at: source, to: cacheRoot)
            defer { try? FileManager.default.removeItem(at: cacheRoot) }
            var calendar = Calendar(identifier: .gregorian)
            calendar.timeZone = try #require(TimeZone(identifier: "America/Santiago"))
            let now = try #require(ISO8601DateFormatter().date(from: "2026-10-05T23:00:00Z"))
            let fetcher = CostUsageFetcher(cacheRoot: cacheRoot, calendar: calendar)
            var snapshot: CostUsageTokenSnapshot?
            var passes = 0
            var stablePasses = 0
            var lastProgress = ""
            var progressLog: [String] = []
            repeat {
                passes += 1
                snapshot = try await fetcher.loadTokenSnapshot(
                    provider: .codex,
                    now: now,
                    codexHomePath: FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(".codex").path,
                    historyDays: 30,
                    allowPricingRefresh: false,
                    refreshPricingInBackground: false,
                    includePiSessions: false,
                    bypassScannerDebounce: true,
                    calendar: calendar)
                let cache = CostUsageStore(cacheRoot: cacheRoot).syncLoadCodexCache(calendar: calendar)
                let files = cache.files.values
                let pending = files.count { $0.hasPendingCodexScanWork || !$0.hasCurrentCodexParser }
                let progress = "tokens=\(snapshot?.last30DaysTokens.map(String.init) ?? "nil") files=\(files.count) "
                    + "pending=\(pending) catchUp=\(cache.codexScanCatchUpPending.map(String.init) ?? "nil") "
                    + "bytes=\(cache.codexScanProcessedBytes ?? -1)/\(cache.codexScanTotalBytes ?? -1) "
                    + "partial=\(snapshot?.historyScanIsPartial == true) "
                    + "coverage=\(snapshot?.historyCoverageIsEstablished == true)"
                progressLog.append("pass \(passes): \(progress)")
                try progressLog.joined(separator: "\n").write(toFile: output + ".progress", atomically: true, encoding: .utf8)
                stablePasses = progress == lastProgress ? stablePasses + 1 : 0
                lastProgress = progress
                // Stop on a complete scan, or after six identical passes once no file has pending work.
                if pending == 0, stablePasses >= 5 { break }
                // Ten identical passes with pending work means the remaining work cannot progress here.
                if stablePasses >= 9 { break }
            } while snapshot?.historyIsFullyScanned != true && passes < 600
            let final = try #require(snapshot)
            let cache = CostUsageStore(cacheRoot: cacheRoot).syncLoadCodexCache(calendar: calendar)
            var ledgerRows = 0
            var legacyRowsInLedgerFiles = 0
            var revisions: [Int: Int] = [:]
            // Local-only per-file dump for diffing two runs; never published.
            let fileLines = cache.files.sorted { $0.key < $1.key }.map { path, usage in
                let rows = usage.codexRows ?? []
                let ledger = rows.count { $0.responseID != nil }
                return "\(path)\t\(rows.count)\t\(ledger)\t\(rows.count - ledger)\t"
                    + "\(rows.reduce(0) { $0 + $1.input + $1.output })"
            }
            try fileLines.joined(separator: "\n").write(toFile: output + ".files", atomically: true, encoding: .utf8)
            for usage in cache.files.values {
                let rows = usage.codexRows ?? []
                revisions[usage.codexParserRevision ?? -1, default: 0] += 1
                guard rows.contains(where: { $0.responseID != nil }) else { continue }
                ledgerRows += rows.count { $0.responseID != nil }
                legacyRowsInLedgerFiles += rows.count { $0.responseID == nil }
            }
            let rendered = CodexBarCLI.renderCostText(provider: .codex, snapshot: final, useColor: false, calendar: calendar)
                .replacingOccurrences(of: #"\$[0-9][0-9,]*(\.[0-9]+)?"#, with: "$•••", options: .regularExpression)
            let lines = [
                rendered,
                "",
                "fetch passes: \(passes)",
                "fully scanned: \(final.historyIsFullyScanned)",
                "last pass: \(lastProgress)",
                "parser revisions: \(revisions.sorted { $0.key < $1.key }.map { "r\($0.key)=\($0.value)" }.joined(separator: " "))",
                "files with ledger rows: ledger rows \(ledgerRows), legacy rows beside them \(legacyRowsInLedgerFiles)",
                "30-day tokens: \(final.last30DaysTokens.map(String.init) ?? "nil")",
            ]
            try (lines.joined(separator: "\n") + "\n").write(toFile: output, atomically: true, encoding: .utf8)
        }
    }
  • Issue repro (CostUsageCodexOffsetMirrorTests on the issue). On main @ 6a26b2e9b, both event orders give rows=2 input=174550. With this branch, the same scenario is covered by adjacent offset counters pair only inside the mirror window.

  • New tests, in CostUsageCodexRequestLedgerTests+MirrorWindow.swift:

    • the window boundary: 2, 4,900, and 5,000 ms pair; 5,001 and 5,100 ms don't
    • the real token_count shape without turn_id, and a task_started between the events (stays distinct)
    • another turn inside the window (stays distinct)
    • far ledger-first pairs with the same offset (pair) and a different offset (stay distinct)
    • a far legacy-first pair with a matching offset (stays distinct)
    • a legacy-only request inside the thread total (stays distinct)
    • a resume or a bare usage line between the events (stays distinct)
    • a replay next to a different equal-size legacy request, both orders (stays distinct)
    • bare usage between buffered subagent observations (stays distinct), and the same file split by a bounded pass
    • an ordinary file split after a bare line: the split and full parses agree
    • a mirror appended after an incremental refresh (pairs)
    • a revision 8 store with a stored duplicate: the reparse removes it and keeps Priority pricing; when the saved rows are unpriced, a bounded reparse and store reopen keep them unpriced
    • equal usage without a known turn, both orders (stays distinct)
    • after an exact pair, a legacy-only and a ledger-only request of equal size 2 ms apart, both orders: the skipped counter keeps them distinct, through the revision 8 reparse too
    • a revision 8 store where a priced and an unpriced request share a saved-pricing key: both stay unpriced, unbounded and bounded with reopen
  • Mutation checks. With near pairing disabled, the window, refresh, and revision 8 tests fail. With the offset rule, resume clear, and bare-usage clear disabled, the far same-offset case and all four separator cases fail. Removing either replay guard fails the replay test; never setting the bare-usage flag, always setting it, or dropping it from saved state each fails a bounded-pass or empty-fragment test.

Commands run:

make check
make test-fast FILTER='CostUsageCodexRequestLedgerTests|CostUsageStoreTests'
TZ=UTC make test

codexParserRevisionMigrationPricing now returns an empty map when saved unpriced rows remain and no saved price is reusable, so the revision 9 reparse keeps them unpriced. It also returns an empty map when an unpriced and a priced row share a key, using the same lines as #4270, so that part merges to identical code. Whichever merges second keeps both checks.

This PR, #4270, and #4279 each regenerate the Codex parser hash from ed735dc27ffa70d9 and add it to the predecessor lists. Whichever merges later needs a small rebase that regenerates the hash and keeps the merged PR's hash as a predecessor.

Merge Danger

Door: two-way

Reverting restores the stricter pairing. Caches reparsed under revision 9 keep their rows; a revert only stops new pairing.

Blast Radius: Codex costs

Codex token and cost totals go down wherever duplicated mirrors were counted. Every Codex cache reparses its files once, in bounded passes, after upgrading. Other providers are unchanged.

Upgrading an older Codex cache to the request-ledger parser looked up the
replaced legacy row's saved pricing with the ledger event's timestamp. Codex
writes token_usage_record and its token_count mirror a few hundred
milliseconds apart, so the lookup missed and every upgraded request was
marked unpriced, which dropped whole day/model costs to nil.

The parser now records the replaced legacy row's pricing key for each ledger
row, in both event orders and across bounded slices, and source pricing
recovery falls back to that key. Stores written by 0.72.0 are adopted
without a rebuild; ledger rows carrying that release's full markers are
repaired, while authoritative amounts and files with invalidated evidence
keep theirs.
A stored unpriced marker does not record whether it came from the 0.72.0
ledger timestamp mismatch or from deliberately invalidated source evidence,
and invalidated files can contain only ledger rows. Adoption now keeps every
row and marker and only drops the retained report. Caches already upgraded
by 0.72.0 can be rebuilt from session logs with `codexbar cache clear --cost`.
…cting

A saved unpriced request and a saved priced request can share one source
pricing key (identical turn, timestamp, model, and tokens). The upgrade map
skipped the unpriced row but kept the priced row's evidence, so the
legacy-key fallback could price the unknown request. Such keys now
invalidate the file's saved pricing, as other conflicting evidence does.
A replayed response links its later token_count to the original ledger row
for deduplication. That mirror belongs to a different legacy request, so it
must not supply the original row's saved pricing. Donation now requires the
token_count to match the ledger row's own persisted mirror keys, which also
holds across bounded slices.
When a resumed session's token_count totals follow the turn, a replay's
mirror total can equal the original record's thread total and pass the
counter-alias check. A mirror whose exact timestamped key belongs to this
response but not to the ledger row itself is the replay's mirror, so it no
longer supplies the original row's saved pricing.
After a resume, Codex's token_count counter can run behind the thread
counter, so a response's token_count and token_usage_record no longer share
a timestamp or cumulative total and were counted as two requests. Adjacent
observations from the same turn with identical input, cached, and output
tokens at most five seconds apart now reconcile as one request. Parser
revision 9 reparses existing files once to remove stored duplicates while
keeping saved prices, and 0.72.0 stores are adopted without a rebuild.
…daries

A token_count written after a long tool run can follow its ledger record by
more than the pairing window. When the ledger record comes first, it now
pairs if both counters keep the offset learned from the previous pair.
A resumed session or a counted bare usage line between two observations
clears the pending candidates so they stay distinct.
…iring

A replayed ledger record no longer pairs with, or seeds, a different
token_count of equal size inside the mirror window.

A bare usage line is counted when read, while subagent observations are
buffered and processed later. When bare usage follows a buffered subagent
observation, the rest of the file keeps exact pairing only, and the saved
ledger state keeps that rule across bounded passes.
@clawsweeper

clawsweeper Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

🦞👀
ClawSweeper picked this up.

Pull request received. I will update this pull request when review starts.

ClawSweeper review complete

ClawSweeper finished reviewing this revision. The review result is being finalized.

View the workflow run.

@clawsweeper clawsweeper Bot added P2 Normal priority bug or improvement with limited blast radius. merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. proof: sufficient Contributor real behavior proof is sufficient. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. labels Oct 5, 2026
@clawsweeper

clawsweeper Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Codex review: needs real behavior proof before merge. Reviewed October 6, 2026, 8:16 AM ET / 12:16 UTC (Revision 5).

ClawSweeper review

What this changes

The PR reconciles duplicate Codex usage observations using turn and counter continuity, handles deferred fork replay, and reparses older cost caches while preserving historical pricing evidence.

Merge readiness

⛔ Blocked before merge - 3 items remain

This remains a useful fix that current main does not provide. No blocking code defect was found, but the recorded real-history proof explicitly excludes the final implementation and its cache-upgrade corrections.

Priority: P2
Reviewed head: 67c74d5fa6c632ee63f7be77ca12028880766c9a

Review scores

Measure Result What it means
Overall readiness 🦐 gold shrimp (3/6) The patch has substantial regression coverage and no identified blocking defect, but its positive runtime evidence predates material final corrections.
Proof confidence 🦐 gold shrimp (3/6) Needs stronger real behavior proof before merge: The historical macOS run exercises the real CLI fetch/render path over Codex logs and a copied 0.72.0 cache, but the current proof file expressly excludes the final scanner continuity, deferred-fork, and unknown-price corrections. Current tests support fresh parsing and upgrades; observed final-implementation runtime and existing-cache compatibility results remain outstanding. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
Patch quality 🐚 platinum hermit (4/6) No actionable review findings were identified.

Verification

Check Result Evidence
Real behavior Needs proof Needs stronger real behavior proof before merge: The historical macOS run exercises the real CLI fetch/render path over Codex logs and a copied 0.72.0 cache, but the current proof file expressly excludes the final scanner continuity, deferred-fork, and unknown-price corrections. Current tests support fresh parsing and upgrades; observed final-implementation runtime and existing-cache compatibility results remain outstanding. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
Evidence reviewed 10 items Pinned patch scope: Reviewed the introduced delta from the pinned merge base to the exact head: 18 files, including scanner logic, persisted replay evidence, migration safeguards, tests, and documentation. Current-main comparison was used only as behavioral context; no base-only deletion was attributed to this PR.
Still necessary on main: Fetched main still reconciles adjacent observations through exact timestamp or cumulative-counter aliases, without the new near-mirror continuity path. The latest release has parser revision 8. The merged pricing fix at #4270 addresses pricing ownership rather than drifted-mirror accounting.
Release behavior: The supplied latest-release commit retains parser revision 8; revision 9 is introduced by this branch.
Findings None None.
Security None None.

How this fits together

CodexBar scans local Codex session logs into a persistent usage cache. Reconciled request rows feed token totals and cost reports in the CLI and app.

flowchart TD
  A[Codex session logs] --> B[Session and turn ownership]
  B --> C[Mirror reconciliation]
  D[Deferred fork observations] --> C
  E[Existing usage cache] --> F[Bounded upgrade and pricing recovery]
  C --> F
  F --> G[Token and cost reports]
Loading

Before merge

  • Add real behavior proof - Needs stronger real behavior proof before merge: The historical macOS run exercises the real CLI fetch/render path over Codex logs and a copied 0.72.0 cache, but the current proof file expressly excludes the final scanner continuity, deferred-fork, and unknown-price corrections. Current tests support fresh parsing and upgrades; observed final-implementation runtime and existing-cache compatibility results remain outstanding. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
  • Resolve merge risk (P1) - Revision 9 reparses existing cost caches and changes persisted replay and unknown-price evidence; the final implementation still needs an observed upgrade result showing duplicate removal while saved pricing and explicit unknown markers survive bounded scanning and reopen.
  • Complete next step (P2) - Add final-implementation real-setup and cache-upgrade proof before merge; terminal output or redacted logs count, and screenshots or recordings are useful when they show the behavior. Redact credentials, paths, account details, and other private information. Updating the PR body should trigger a fresh review; otherwise ask a maintainer to comment @clawsweeper re-review.
Agent review details

Security

None.

Review metrics

Metric Value Why it matters
Production and test LOC production +349/-368, net −19; tests +1186/-21, net +1165 The introduced scanner correction reduces net production code while substantially expanding reconciliation and upgrade coverage.

Merge-risk options

Maintainer options:

  1. Verify the final cache upgrade (recommended)
    Provide final-implementation production-path evidence from a copied existing cache, showing preserved pricing and unknown markers after bounded repair and reopen.

Technical review

Best possible solution:

Count each owned response once across compaction and deferred replay while retaining historical prices and explicit pricing uncertainty through cache upgrades.

Do we have a high-confidence way to reproduce the issue?

Yes: the linked issue supplies both event orders and failing scanner output, and current main retains the exact matching limitation. This review verified the source path without executing tests.

Is this the best way to solve the issue?

Yes: extending the existing reconciliation path with continuity evidence and persisted replay boundaries is a focused solution; current tests address the earlier pricing and distinct-request failures.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning medium; reviewed against ab32496d3c1f.

Labels

Label changes:

  • add merge-risk: 🚨 compatibility: Existing caches undergo a revision-9 reparse, and final-implementation upgrade evidence remains outstanding.
  • add rating: 🦐 gold shrimp: Overall readiness is 🦐 gold shrimp; proof is 🦐 gold shrimp and patch quality is 🐚 platinum hermit.
  • add status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs stronger real behavior proof before merge: The historical macOS run exercises the real CLI fetch/render path over Codex logs and a copied 0.72.0 cache, but the current proof file expressly excludes the final scanner continuity, deferred-fork, and unknown-price corrections. Current tests support fresh parsing and upgrades; observed final-implementation runtime and existing-cache compatibility results remain outstanding. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
  • remove proof: sufficient: Current real behavior proof status is insufficient, not sufficient.
  • remove status: 👀 ready for maintainer look: Current PR status label is status: 📣 needs proof.
  • remove rating: 🐚 platinum hermit: Current PR rating is rating: 🦐 gold shrimp, so this older rating label is no longer current.

Label justifications:

  • P2: This fixes inflated local Codex token and cost reports with a bounded accounting scope.
  • merge-risk: 🚨 compatibility: Existing caches undergo a revision-9 reparse, and final-implementation upgrade evidence remains outstanding.
  • rating: 🦐 gold shrimp: Overall readiness is 🦐 gold shrimp; proof is 🦐 gold shrimp and patch quality is 🐚 platinum hermit.
  • status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs stronger real behavior proof before merge: The historical macOS run exercises the real CLI fetch/render path over Codex logs and a copied 0.72.0 cache, but the current proof file expressly excludes the final scanner continuity, deferred-fork, and unknown-price corrections. Current tests support fresh parsing and upgrades; observed final-implementation runtime and existing-cache compatibility results remain outstanding. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.

Evidence

What I checked:

Likely related people:

  • steipete: Suggested for follow-up; no historical authorship or introduction is verified. (role: unverified routing candidate; confidence: low)
  • Yuxin-Qiao: Suggested for follow-up; no historical authorship or introduction is verified. (role: unverified routing candidate; confidence: low)
  • gabrielrojasc: Suggested for follow-up; no historical authorship or introduction is verified. (role: unverified routing candidate; confidence: low)

Rank-up moves

Optional improvements that raise the rating; they are not merge blockers.

  • Add redacted final-implementation CLI output or logs over real Codex history and a copied existing cache, demonstrating complete scanning, retained owned requests, and preserved pricing uncertainty through upgrade and reopen.

Rating scale

Score Internal tier Crab rank Meaning
6/6 S 🦀 challenger crab Exceptional readiness
5/6 A 🦞 diamond lobster Very strong readiness
4/6 B 🐚 platinum hermit Good normal PR; ordinary maintainer review
3/6 C 🦐 gold shrimp Useful, but confidence is limited
2/6 D 🦪 silver shellfish Proof or implementation needs work
1/6 F 🧂 unranked krab Not merge-ready
N/A NA 🌊 off-meta tidepool Rating does not apply

Overall follows the weaker of proof and patch quality.
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

Workflow

  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

History

Review history (4 earlier review cycles)
  • reviewed 2026-10-05T18:04:09.970Z sha 5a4d317 :: blocked before merge. :: [P1] Preserve unpriced markers during the revision 9 reparse
  • reviewed 2026-10-05T19:07:34.420Z sha 07c60a6 :: blocked before merge. :: [P1] Reject mixed priced and unpriced keys during the revision-9 upgrade
  • reviewed 2026-10-05T20:34:52.710Z sha c69dd8c :: blocked before merge. :: [P1] Preserve distinct requests when near usage keys collide
  • reviewed 2026-10-05T21:41:01.503Z sha 26ffcaf :: needs maintainer review before merge. :: none

…eparse

A revision 8 file whose saved rows carry unpriced markers and no reusable
saved price returned no migration evidence, so its reparsed rows took current
prices. An empty evidence map now keeps them unpriced through bounded slices
and store reopen.
Without turn evidence, equal request sizes close in time are not evidence of
one request, so such observations keep exact pairing only. Covers the unpriced
revision 8 reparse with a bounded, reopened migration test.
…eparse

Two requests can share a saved-pricing key. When one was saved unpriced,
the key cannot prove which request its price belongs to, so the revision 9
reparse now keeps both unpriced, matching the rule in steipete#4270.
@kcharlan

kcharlan commented Oct 5, 2026

Copy link
Copy Markdown

Second history: same double count on a v0.72.0-based build, but here the counter offset starts at a context compaction. I tested c69dd8c with six synthetic fixtures: five with a compaction, plus one control. The control and three compaction fixtures pass, including a revision-8 cache repair. The other two each count one response twice. In both, the response is the first one after a compaction and its token_count comes more than 5 s after its record. In that position, at c69dd8c, neither the 5 s window nor the learned offset matches. My Oct 1–5 history has one such pair, 5023 ms apart, and my build's cache holds two rows for it. I have not run c69dd8c over my real history.

Build: b957e06b3 (CodexGitCommit in the installed app), which is v0.72.0 (6ba06fe) plus local patches: 59 files changed, 13 of them in Vendored/CostUsage. Codex parser hash: ed735dc27ffa70d9 at v0.72.0, edfc706be64a069e in this build.

Where the offset starts (one real session, numbers only)

Columns: kind, timestamp, usage/last_token_usage input, cached, output, then thread_token_usage/total_token_usage input, output.

record      2026-10-03T23:50:14.749Z  227061 226176   97  15934553 23403
token_count 2026-10-03T23:50:14.751Z  227061 226176   97  15934553 23403
record      2026-10-03T23:52:16.672Z  228673 226944 3942  16163226 27345
compacted   2026-10-03T23:52:16.678Z
token_count 2026-10-03T23:52:16.681Z       0      0    0  15934553 23403
record      2026-10-03T23:52:20.591Z   42221  22784   75  16205447 27420
token_count 2026-10-03T23:52:20.640Z   42221  22784   75  15976774 23478

Real-history counts on the build above

Oct 1–5 (Oct 5 partial), America/New_York days, from my cache. "Ledger rows" are rows with a responseID; "legacy rows" are rows without one. Tokens are input + output.

Day Ledger rows Legacy rows Ledger tokens Legacy tokens
10-01 3963 1781 461953371 203014501
10-02 2273 333 232017725 36468173
10-03 2290 1185 278882947 149382990
10-04 4798 2719 645099967 360631889
10-05 1735 1028 213141280 133802031
  • day_aggregates for these days total 2714394874.
  • Each of the 7046 legacy rows has a ledger row in the same file with identical input/cached/output within 60 s.
  • Of the 31 files holding legacy rows, all 31 have their first compacted line before their first legacy row.

c69dd8c against synthetic compaction fixtures

In the compaction fixtures (every row except the control), r1 pairs at an offset of zero: thread 1000/200/100 against total 1000/200/100. Then comes compaction record r2, with no token_count reporting it. After that, r3's record has thread 6300/300/530 against token_count total 1300/300/130. No token_count carries turn_id.

Fixture Expected c69dd8c
Control: no compaction 2 rows, 1430 2 rows, 1430
Two post-compaction pairs, 49 ms and 15 ms apart 4 rows, 7270 4 rows, 7270
Same, second pair 20.015 s apart 4 rows, 7270 4 rows, 7270
Revision-8 cache seeded with a doubled legacy row (full and bounded reparse) 3 rows, 6830 3 rows, 6830
First post-compaction pair 5023 ms apart 3 rows, 6830 4 rows, 7160 (r1, r2, r3, token_count 300+30)
First post-compaction pair 20.049 s apart 3 rows, 6830 4 rows, 7160 (same rows)

The real 5023 ms pair: record 2026-10-01T14:48:57.555Z, token_count 14:49:02.578Z, usage 46174/22528/98, 46272 tokens.

  • It is the only one of 99 first-after-compacted pairs in my Oct 1–5 history that are more than 5 s apart.
  • The events before it in its file are: record 14:48:54.188Z, compacted 14:48:54.190Z, token_count 0/0/0 at 14:48:54.192Z.
  • My build's cache holds a row with a responseID and a row without one for this pair.
Fixture file (drop into Tests/CodexBarTests)
import Foundation
import Testing
@testable import CodexBarCore

/// Diagnostic repro shaped like one examined real session: after a context compaction there is a
/// token_usage_record for the compaction request and no token_count reporting it. From then on the
/// token_count counter (info.total_token_usage) trails the ledger's thread_token_usage by
/// that request. token_count events here carry no turn_id, as in the examined real session.
@Suite(.serialized)
struct CostUsageCodexCompactionMirrorReproTests {
    private static func tokens(_ v: [Int]) -> [String: Int] {
        ["input_tokens": v[0], "cached_input_tokens": v[1], "output_tokens": v[2], "reasoning_output_tokens": v[3]]
    }

    private static func record(_ id: String, _ ts: String, usage: [Int], thread: [Int]) -> [String: Any] {
        ["type": "token_usage_record", "timestamp": ts, "payload": [
            "thread_id": "repro-thread", "session_id": "repro-thread", "turn_id": "repro-turn",
            "root_turn_id": "repro-turn", "response_id": id,
            "usage": tokens(usage), "turn_token_usage": tokens(thread), "thread_token_usage": tokens(thread),
        ]]
    }

    private static func tokenCount(_ ts: String, last: [Int], total: [Int]) -> [String: Any] {
        ["type": "event_msg", "timestamp": ts, "payload": [
            "type": "token_count", "info": ["last_token_usage": tokens(last), "total_token_usage": tokens(total)],
        ]]
    }

    private static func turnContext(_ ts: String) -> [String: Any] {
        ["type": "turn_context", "timestamp": ts, "payload": ["turn_id": "repro-turn", "model": "gpt-5"]]
    }

    private static func header() -> [[String: Any]] { [
        ["type": "session_meta", "timestamp": "2026-10-03T23:16:40.000Z", "payload": ["id": "repro-thread"]],
        turnContext("2026-10-03T23:17:48.179Z"),
    ] }

    /// r1 paired, then compaction request r2 (ledger only), then the repeated zero-delta token_count.
    private static func prefix() -> [[String: Any]] {
        header() + [
            record("r1", "2026-10-03T23:17:56.136Z", usage: [1000, 200, 100, 0], thread: [1000, 200, 100, 0]),
            tokenCount("2026-10-03T23:17:56.137Z", last: [1000, 200, 100, 0], total: [1000, 200, 100, 0]),
            record("r2", "2026-10-03T23:52:16.672Z", usage: [5000, 0, 400, 0], thread: [6000, 200, 500, 0]),
            ["type": "compacted", "timestamp": "2026-10-03T23:52:16.678Z", "payload": ["message": ""]],
            turnContext("2026-10-03T23:52:16.679Z"),
            tokenCount("2026-10-03T23:52:16.681Z", last: [0, 0, 0, 0], total: [1000, 200, 100, 0]),
        ]
    }

    private static func calendar() throws -> Calendar {
        var calendar = Calendar(identifier: .gregorian)
        calendar.timeZone = try #require(TimeZone(identifier: "America/New_York"))
        return calendar
    }

    private static func run(_ label: String, _ lines: [[String: Any]]) throws -> (rows: Int, tokens: Int) {
        let env = try CostUsageTestEnvironment()
        defer { env.cleanup() }
        let file = env.root.appendingPathComponent("repro.jsonl")
        try env.jsonl(lines).write(to: file, atomically: false, encoding: .utf8)
        let day = try #require(ISO8601DateFormatter().date(from: "2026-10-03T23:00:00Z"))
        let result = try CostUsageScanner.parseCodexFile(
            fileURL: file, range: .init(since: day, until: day, calendar: Self.calendar()))
        let tokens = result.rows.reduce(0) { $0 + $1.input + $1.output }
        print("[compaction-repro] \(label) rows=\(result.rows.count) input+output=\(tokens) " +
            "detail=\(result.rows.map { "\($0.responseID ?? "token_count"):\($0.input)+\($0.output)" })")
        return (result.rows.count, tokens)
    }

    @Test
    func `control without compaction counts each response once`() throws {
        let result = try Self.run("control", Self.header() + [
            Self.record("r1", "2026-10-03T23:17:56.136Z", usage: [1000, 200, 100, 0], thread: [1000, 200, 100, 0]),
            Self.tokenCount("2026-10-03T23:17:56.137Z", last: [1000, 200, 100, 0], total: [1000, 200, 100, 0]),
            Self.record("r3", "2026-10-03T23:52:20.591Z", usage: [300, 100, 30, 0], thread: [1300, 300, 130, 0]),
            Self.tokenCount("2026-10-03T23:52:20.640Z", last: [300, 100, 30, 0], total: [1300, 300, 130, 0]),
        ])
        #expect(result.rows == 2)
        #expect(result.tokens == 1430)
    }

    @Test
    func `responses after a compaction are counted once`() throws {
        let result = try Self.run("near", Self.prefix() + [
            Self.record("r3", "2026-10-03T23:52:20.591Z", usage: [300, 100, 30, 0], thread: [6300, 300, 530, 0]),
            Self.tokenCount("2026-10-03T23:52:20.640Z", last: [300, 100, 30, 0], total: [1300, 300, 130, 0]),
            Self.record("r4", "2026-10-03T23:52:33.856Z", usage: [400, 300, 40, 0], thread: [6700, 600, 570, 0]),
            Self.tokenCount("2026-10-03T23:52:33.871Z", last: [400, 300, 40, 0], total: [1700, 600, 170, 0]),
        ])
        #expect(result.rows == 4)
        #expect(result.tokens == 7270)
    }

    /// First pair after a compaction, written 20 s apart: the offset just changed and the
    /// pair is outside the near window.
    @Test
    func `first response after a compaction is counted once when its mirror is 20 s late`() throws {
        let result = try Self.run("far-first", Self.prefix() + [
            Self.record("r3", "2026-10-03T23:52:20.591Z", usage: [300, 100, 30, 0], thread: [6300, 300, 530, 0]),
            Self.tokenCount("2026-10-03T23:52:40.640Z", last: [300, 100, 30, 0], total: [1300, 300, 130, 0]),
        ])
        #expect(result.rows == 3)
        #expect(result.tokens == 6830)
    }

    /// Same, with the 5,023 ms gap observed in a real history.
    @Test
    func `first response after a compaction is counted once when its mirror is 5023 ms late`() throws {
        let result = try Self.run("far-first-5023ms", Self.prefix() + [
            Self.record("r3", "2026-10-03T23:52:20.591Z", usage: [300, 100, 30, 0], thread: [6300, 300, 530, 0]),
            Self.tokenCount("2026-10-03T23:52:25.614Z", last: [300, 100, 30, 0], total: [1300, 300, 130, 0]),
        ])
        #expect(result.rows == 3)
        #expect(result.tokens == 6830)
    }

    /// After a near pair established the post-compaction offset, a 20 s late mirror pairs by offset.
    @Test
    func `later late mirror after a compaction pairs by the learned offset`() throws {
        let result = try Self.run("far-offset", Self.prefix() + [
            Self.record("r3", "2026-10-03T23:52:20.591Z", usage: [300, 100, 30, 0], thread: [6300, 300, 530, 0]),
            Self.tokenCount("2026-10-03T23:52:20.640Z", last: [300, 100, 30, 0], total: [1300, 300, 130, 0]),
            Self.record("r4", "2026-10-03T23:52:33.856Z", usage: [400, 300, 40, 0], thread: [6700, 600, 570, 0]),
            Self.tokenCount("2026-10-03T23:52:53.871Z", last: [400, 300, 40, 0], total: [1700, 600, 170, 0]),
        ])
        #expect(result.rows == 4)
        #expect(result.tokens == 7270)
    }

    /// Upgrade path: a revision 8 cache that already holds a doubled legacy row is repaired by the
    /// revision 9 reparse (simulates an existing cache written by the unfixed build).
    @Test(arguments: [false, true])
    func `revision 8 cache with a doubled compaction mirror is repaired`(bounded: Bool) throws {
        let env = try CostUsageTestEnvironment()
        defer { env.cleanup() }
        let calendar = try Self.calendar()
        let start = try #require(ISO8601DateFormatter().date(from: "2026-10-03T23:00:00Z"))
        let end = try #require(ISO8601DateFormatter().date(from: "2026-10-03T23:59:00Z"))
        let file = try env.writeCodexSessionFile(
            day: start,
            filename: "rollout-2026-10-03T19-16-40-repro-thread.jsonl",
            contents: env.jsonl(Self.prefix() + [
                Self.record("r3", "2026-10-03T23:52:20.591Z", usage: [300, 100, 30, 0], thread: [6300, 300, 530, 0]),
                Self.tokenCount("2026-10-03T23:52:20.640Z", last: [300, 100, 30, 0], total: [1300, 300, 130, 0]),
            ]))
        var options = CostUsageScanner.Options(
            codexSessionsRoot: env.codexSessionsRoot,
            cacheRoot: env.cacheRoot,
            codexTraceDatabaseURL: env.root.appendingPathComponent("missing-traces.sqlite"),
            calendar: calendar)
        options.refreshMinIntervalSeconds = 0
        func report(_ now: Date) -> CostUsageDailyReport {
            CostUsageScanner.loadDailyReport(provider: .codex, since: start, until: end, now: now, options: options)
        }
        #expect(report(end).summary?.totalTokens == 6830)

        var stored = CostUsageStoreAccess.read(cacheRoot: env.cacheRoot, calendar: calendar)
        var usageFile = try #require(stored.files[file.path])
        var rows = try #require(usageFile.codexRows)
        try #require(rows.count == 3)
        let r3 = try #require(rows.first { $0.responseID == "r3" })
        rows.append(CostUsageScanner.CodexUsageRow(
            day: r3.day, model: r3.model, rawModel: r3.rawModel, turnID: r3.turnID, eventIndex: 99,
            timestampUnixMs: (r3.timestampUnixMs ?? 0) + 49,
            input: r3.input, cached: r3.cached, output: r3.output, reasoning: r3.reasoning))
        usageFile.codexRows = rows
        usageFile.codexParserRevision = 8
        stored.files[file.path] = usageFile
        _ = CostUsageStoreAccess.replace(cacheRoot: env.cacheRoot, cache: stored, calendar: calendar)
        let seeded = CostUsageStoreAccess.read(cacheRoot: env.cacheRoot, calendar: calendar).files[file.path]
        #expect(seeded?.codexRows?.count == 4)
        if bounded {
            options.maxCodexScanBytesPerRefresh = usageFile.size / 2
        }

        var migrated: CostUsageFileUsage?
        for pass in 1...20 {
            _ = report(end.addingTimeInterval(Double(pass)))
            migrated = CostUsageStore(cacheRoot: env.cacheRoot).syncLoadCodexCache(calendar: calendar)
                .files[file.path]
            if migrated?.hasCurrentCodexParser == true, migrated?.codexScanComplete == true { break }
        }
        let migratedRows = try #require(migrated?.codexRows)
        print("[compaction-repro] upgrade bounded=\(bounded) rows=\(migratedRows.count) " +
            "total=\(String(describing: report(end.addingTimeInterval(30)).summary?.totalTokens))")
        #expect(migrated?.hasCurrentCodexParser == true)
        #expect(migratedRows.count == 3)
        #expect(report(end.addingTimeInterval(30)).summary?.totalTokens == 6830)
    }
}

Run with swift test --filter "CostUsageCodexCompactionMirrorReproTests|CostUsageCodexRequestLedgerTests". I have a ledger with the command and output behind each number here and can attach it.

A near or offset pair is one request only if the second observation's
counter advanced by no more than its own usage since that counter's
previous observation. A larger advance shows another request in between,
so the two stay distinct, as on main, including through the revision 9
reparse.
@clawsweeper clawsweeper Bot added rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. labels Oct 5, 2026
Carry saved pricing from the original legacy observation through bounded
request-ledger upgrades. Track pricing ownership separately from response
deduplication so later same-counter replays cannot donate their prices.

Keep compatible cache rows and unknown-price markers. Replace the production
cache forwarding factory with direct value initialization, preserving defaults;
the convenience factory now belongs to synthetic test fixtures.

Validation: 251 focused tests in 26 suites passed, including bounded/unbounded
same-counter replay regressions that failed before the ownership fix. Codex
review found no remaining actionable findings. make check passes.

Co-authored-by: Gabriel Rojas <[email protected]>
Pair adjacent same-turn observations using request usage and counter
continuity, including delayed mirrors after compaction. Preserve original
ledger evidence and bare-usage boundaries in replay buffers, and resolve
fork mirror ownership in log order after verifying the parent baseline.

Keep pending matches across identical copies and recognize owned legacy
replays after counter-history eviction. Preserve independent last-only
requests and explicit unknown-price evidence during cache upgrades.

Consolidate regression fixtures and remove unused accounting wrappers.
Incorporate steipete#4270 without rewriting contributor history. Includes
@kcharlan's delayed-compaction regression.

Fixes steipete#4289

Co-authored-by: Gabriel Rojas <[email protected]>
@clawsweeper clawsweeper Bot added merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. and removed proof: sufficient Contributor real behavior proof is sufficient. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. labels Oct 6, 2026
@steipete
steipete merged commit 72b726c into steipete:main Oct 7, 2026
9 of 11 checks passed
steipete added a commit that referenced this pull request Oct 7, 2026
Merge train for ten Codex cost PRs that each regenerate the parser fingerprint; landed together so the generated hash is regenerated once (final fingerprint 7ff985e81e281a11, predecessor 99d920977063318a from main) and existing caches and checkpoints survive.

#4276 (@luochen211) Fix Codex session tier pricing: durable thread_settings_applied Priority evidence binds to the following turns, with the Fast multipliers applied and the evidence preserved through incremental scans and SQLite reopening; the train also fixes Priority being cleared after the first turn (threadPriority is retained until replaced).
#4279 (@gabrielrojasc, consolidating #4278 by @luochen211) Keep priced Codex subtotals when some requests are unpriced: one unpriced request no longer hides the whole model/day estimate, and a day with an unpriced model no longer reads as fully priced.
#4295 (@Yuxin-Qiao) Keep known daily request counts as lower bounds independently of token and cost completeness.
#4258 (@urda) Price Antigravity gpt-oss-120b at the Google Vertex list price through warm and cold catalog paths.
#4290 (@gabrielrojasc, fixture by @kcharlan) Count drifted Codex ledger mirrors once: counter-continuity matching, retained replay evidence, deferred fork mirror ownership, bounded resumes.
#4305 (@shiquda) ClinePass labeled API-key token accounts through the shared token-account adapter with selected-key isolation.
#4296 (@Yuxin-Qiao) Recover empty Codex cost scans (one bounded recovery per cache and worker when a pass yielded with zero attempts) and clear completed dashboard stalls via a read-only completion check.
#4312 (@steipete) Honor documented long-context pricing thresholds: the models.dev explicit context tier (272,000 for the GPT-6 family) replaces the literal 200,000 read from the legacy field.
#4277 (@luochen211, includes #4286 by @Yuxin-Qiao) Reuse Codex report preparation per refresh (402 reports instead of 802 for a 400-file cached refresh) and reduce repeated component path work in storage scans.
#4291 (@kristofferR) Stream Codex usage rows for reports and baselines without materializing encoded-row arrays.

Fixes #4274
Fixes #4273
Fixes #4289
Fixes #4302
Fixes #4275

Thanks @luochen211, @gabrielrojasc, @kcharlan, @Yuxin-Qiao, @urda, @shiquda and @kristofferR!
@steipete

steipete commented Oct 7, 2026

Copy link
Copy Markdown
Owner

Thanks @gabrielrojasc and @kcharlan! Landed through the merge train #4317 (d919d23) with the contributor history preserved and #4270 incorporated. Drifted and delayed mirrors retain counter continuity, ordered fork replay and explicit unknown-price evidence; the reported fixture counts one row and 87,275 input tokens in either order. Ships in the next release.

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

Labels

merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. P2 Normal priority bug or improvement with limited blast radius. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Codex cost counts a response twice when its token_count counter drifts from the thread counter

3 participants