Skip to content

Spend Dashboard Codex publish delayed for hours — stale scanSinceKey reseeds lookback queue on every scan #3411

Description

@kesslerio

TL;DR

The Usage & Spend dashboard took ~5 hours to publish Codex spend for one of my two Codex accounts because the lookback queue reseeded on every scan pass. Root cause: a stale scanSinceKey in the persisted lookback state triggered activeLookbackStateWasReset on every scan, which reseeded the entire queue (3,367 files) even though all files were already parsed and priced. The queue eventually drained fully between two scan boundaries and the dashboard published, but the recovery should take one scan pass, not five hours.

Bug Type

behavior bug

Release Blocker

  • No

Steps to Reproduce

  1. Have a large Codex session corpus (3,879 files, ~3,500 in ambient ~/.codex/sessions, rest in CodexBar-managed homes). Two Codex accounts configured.
  2. Let CodexBar build its cost cache. scan_metadata reaches completedFiles: 3879 / totalFiles: 3879.
  3. Observe lookback_state.pendingFilePaths drain to zero during background passes (queue shrinks ~250-500 files per ~15 min pass).
  4. Trigger a new scan (open Settings → Usage & Spend, or wait for the next background cycle). The scan re-enumerates and re-enqueues the entire lookback set. pendingFilePaths jumps back to ~3,367.
  5. catchUpPending stays true for hours. The dashboard shows "Spend unavailable" for the affected account row until a drain pass happens to finish the whole queue between scan boundaries.

Expected Behavior

Once all files are complete and the lookback queue is empty, catchUpPending should flip to false and the dashboard should publish Codex spend on the next refresh. The queue should not re-enqueue files that were already processed and marked complete. A scan-window change should not reseed already-completed files.

Actual Behavior

The queue reseeded on every scan pass for ~5 hours before one drain pass completed between scan boundaries.

Timeline from a single session (all times UTC):

Time pending completed trigger
04:42:48 0 3879/3879 background pass, queue drained
04:46:03 3,367 3878/3879 pane opened, scan re-enqueued entire lookback set
04:46:36 3,262 3878/3879 draining again
04:48:14 307 3878/3879 continued draining

Root cause: scanSinceKey mismatch triggers lookback state reset on every scan

CostUsageScanner.swift:5616 checks activeLookbackStateWasReset by comparing the cached lookback state's scanSinceKey and rootPaths against the freshly computed values:

let activeLookbackStateWasReset = cache.codexActiveLookbackState.map {
    $0.scanSinceKey != activeLookbackState.scanSinceKey
        || $0.rootPaths != activeLookbackState.rootPaths
} ?? true

When activeLookbackStateWasReset is true, shouldSeedBoundedQueue becomes true (line 5780), which calls seedOrExtendCodexActiveLookbackQueue and reseeds the entire pending queue from all known files.

The dashboard uses a 365-day scan window (SpendDashboardController.swift:168-170: activityDays = 365, scanDays = activityDays). So the fresh scanSinceKey is 2025-09-03 (365 days back from today). But the persisted lookback_state.scanSinceDay was stuck at 2026-06-05:

scan_metadata.scanSinceDay:     2025-09-03   (365-day window)
lookback_state.scanSinceDay:   2026-06-05   (stale, ~90-day window)

Every scan saw the scanSinceKey mismatch, activeLookbackStateWasReset = true, shouldSeedBoundedQueue = true, entire queue reseeded. The stale scanSinceDay couldn't update because the scanner only persists the new lookback state at the end of a completed scan, and the scan couldn't complete because the reseeded queue was too large to drain in one bounded pass.

The lookback_state.scanSinceDay of 2026-06-05 corresponds to a ~90-day window, likely the original scan window before the dashboard switched to 365 days. When the window widened, the cached lookback state's scanSinceKey became stale. The codexActiveLookbackState function (line 2775) returns the cached state when scanSinceKey matches, but when it doesn't match, it creates a fresh state with retainedPendingFilePaths (line 2787) and the new scanSinceKey. The fresh state is used in-memory but never persisted until a scan completes.

Resolution observed

After ~5 hours, one drain pass finished the entire queue before the next scan boundary. The scan completed, the fresh lookback state persisted (the lookback_state row is now gone entirely), and the publish gate opened:

lookback_state row: gone (0 rows)
catchUpPending: false
completedFiles: 3839 / 3839

The dashboard now renders the previously blocked account row. The bug is not a permanent deadlock on every corpus, but a race: the reseed loop continues until a drain pass happens to complete the full queue between scan boundaries. On a large corpus (3,879 files) with ~15-minute background cadence, that took roughly 5 hours.

Suggested fix direction

A scanSinceKey-only mismatch should not reseed already-completed files. When the window widens, the scanner should enqueue only files whose content days fall in the newly added range (between the old scanSinceKey and the new one), not re-enqueue all 3,879 files. Alternatively, the lookback state's scanSinceKey could be updated in-place when a window change is detected, without requiring a full scan completion. Either approach collapses the 5-hour recovery into a single scan pass.

Cost data was computed the whole time

The dashboard's JSON export (Usage & Spend > export) shows the affected account's data was fully computed while the UI showed "Spend unavailable":

{
  "displayName": "Codex · #2",
  "id": "codex:subs@kessler.io",
  "sourceKind": "native",
  "totalCost": 4256.232196680002,
  "totalTokens": 14660778502
}

The cost engine correctly attributed and priced all sessions. The "Spend unavailable" state was purely the publish gate. The model breakdown was also computed (gpt-5.6-sol, gpt-5.6-luna, gpt-5.6-terra, gpt-5.5, gpt-5.4).

Version

0.56.4 (build 135)

Environment

macOS 26.6.2 (25G83), Apple Silicon. Two Codex accounts (one team account managed home, one personal account managed home). 3,879 indexed session files.

Configuration Details

Two Codex accounts configured via CodexBar managed homes. Sessions routed through a wrapper that sets CODEX_HOME per account (not auth_profile). Ambient ~/.codex/sessions has ~3,500 files; managed homes have a small number of recently routed files. Both managed homes were re-authed with fresh codex login (previous tokens had expired).

Logs

Redacted log excerpts (current rotation, ~/Library/Logs/CodexBar/CodexBar.log):

[2026-09-03T22:18:38Z] [WARNING] Spend Dashboard Codex cost catch-up stopped because all pending account caches stalled
[2026-09-03T23:08:32Z] [WARNING] Spend Dashboard Codex cost catch-up stopped because all pending account caches stalled
[2026-09-04T02:45:50Z] [WARNING] Spend Dashboard Codex cost catch-up stopped because all pending account caches stalled
[2026-09-04T03:06:01Z] [WARNING] Spend Dashboard Codex cost catch-up stopped because all pending account caches stalled
[2026-09-04T04:42:57Z] [INFO]    cost usage success provider=codex duration=8.79s today=$0.00 historyDays=90 windowCost=$10,615.66

Screenshots

Dashboard overview (30d) after the publish gate opened, showing Codex · #2 at $4,256.23 · 15B tokens in "By subscription" and the model breakdown:

Usage & Spend dashboard 30d

Session breakdown, showing per-session rows for Codex · #2 all rendering with individual costs:

Usage & Spend sessions

Note: the banner still reads "Local estimated history · Refreshing" with "stale data · Updated 14 minutes ago", 71 KB / 71 KB. This may clear on the next successful refresh; I'll report back if it persists.

Impact and Severity

  • Affected: users with large Codex session corpora and multiple accounts
  • Severity: blocks workflow. The Usage & Spend dashboard is the primary spend surface, and one account row shows "Spend unavailable" for hours
  • Frequency: every time the scan window changes on a large corpus (window widen, or possibly the daily rolling-window shift)
  • Consequence: cannot see per-account Codex spend in the dashboard for hours; must rely on the menu bar aggregate which doesn't break down by account

Additional Information

Related: #3071 (closed, fix in #3075), #3316 (open, restart loop variant).

This is distinct from #3316 in two ways:

  1. Re-enumeration across scan boundaries: my queue drains to zero but a new scan repopulates it. Spend Dashboard no-progress pause auto-restarts, loops at 100% CPU #3316 reports 25 already-complete paths that never drain. My corpus shows the queue can reach nil (the Drain processed zero-row files from Codex cost catch-up #3075 drain fix partially works), but the next scan re-enqueues everything.
  2. Corpus size: 3,879 files vs 537. The re-enumeration scales with corpus size, which may explain why the drain fix works on smaller corpora but stalls here.

What I tried that didn't help (before understanding the root cause):

  • Deleting the lookback_state row: triggered a full cold rebuild that re-enqueued the entire lookback set (3,367 files). Made it worse.
  • Waiting for the drain: the queue drains between scans but repopulates on the next pass.
  • Keeping the Usage & Spend pane open (accelerated 10s cadence per fix(spend): clear final-file stall for local estimated history #3114): triggers more frequent scans, which trigger more re-enumerations.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Normal priority bug or improvement with limited blast radius.clawsweeper:needs-maintainer-reviewClawSweeper marked this issue as needing maintainer review before automation.clawsweeper:no-new-fix-prClawSweeper does not recommend queueing a new automated fix PR for this issue.impact:ux-frictionUser-facing flow adds avoidable confusion or support burden without fully blocking progress.issue-rating: 🦪 silver shellfishThin issue quality; more reproduction proof or environment detail is needed.

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions