Skip to content

Bug: Deferred large files can starve indefinitely under newest-first scan priority #3203

Description

@IchenDEV

Summary

Deferred oversized Codex session files can permanently stall at their first-pass progress when the user's daily session volume consistently consumes the full per-refresh byte budget. The bounded scan mechanism introduced in #2452 correctly defers large files and persists resumable progress, but the newest-first priority order combined with high daily throughput creates a starvation condition where deferred files never receive additional budget.

Environment

  • CodexBar: 0.55.0 (build 129)
  • macOS: 27
  • Codex CLI model: gpt-5.6-sol (Ultra mode, frequent sub-agent spawning)
  • Daily session volume: ~133 files/day, ~1 GB/day
  • Total session corpus: ~979 files across ~/.codex/sessions/ and ~/.codex/archived_sessions/

Observed behavior

Three files have scan_complete = 0 and have not advanced beyond their first-pass progress:

File Size Parsed Progress Stuck since
rollout-2026-08-19T19-05-32-*.jsonl 1,143 MB 256 MB 22.4% 7 days
rollout-2026-07-14T20-16-33-*.jsonl 28 MB 0.5 MB 1.8% 42 days
rollout-2026-08-26T12-45-09-*.jsonl 3 MB 0.8 MB 26.2% today (in progress)

The 1.1 GB file is stuck at exactly 268,435,456 bytes = 2^28 = maxFileBytes (256 MiB), confirming the first pass consumed the per-file budget and stopped. Subsequent refreshes have not advanced it despite updated_at_ms showing recent attempts.

The 28 MB file at 0.5 MB parsed for 42 days is especially notable — it is well under maxFileBytes and should complete in a single pass, but the global budget is exhausted by higher-priority (newer) files before it is reached.

Root cause analysis

The starvation occurs due to the interaction of three design choices in CostUsageScanner:

  1. Per-file budget (maxFileBytes = 268,435,456): each file reads at most 256 MiB per refresh.
  2. Global budget (maxBytesPerRefresh = 536,870,912): all file I/O is capped at 512 MiB per refresh.
  3. Newest-first ordering (preferNewestCodexSessionsFirst): files sorted by mtime descending.

With ~133 new session files/day (~1 GB), the global 512 MiB budget is consumed by fresh/recently-modified files on every refresh. Deferred files from older dates are queued but never reached:

Each refresh:
  1. Enumerate files → sort by mtime desc (newest first)
  2. Process new/changed files → consumes ~256-512 MiB of global budget
  3. Reach deferred large file → global budget exhausted → skip
  4. Repeat indefinitely

Suggested fix directions

  1. Reserve a deferred-file budget slice. Split maxBytesPerRefresh into e.g. 384 MiB for new/changed files + 128 MiB reserved for deferred-in-progress files. Deferred files with persisted progress > 0 should be served from the reserved slice regardless of priority order. This guarantees forward progress on at least one deferred file per refresh.

  2. Age-based priority boost. Track how many refreshes a deferred file has been skipped. After N skips (e.g. 10), promote it above newest-first ordering for one pass. This prevents indefinite starvation while preserving the general newest-first preference.

  3. Small-file fast path. Files under maxFileBytes (like the 28 MB file stuck at 1.8%) should bypass the deferred queue entirely and be processed in a single pass, since they cannot exceed the per-file budget. The current code appears to defer them when the global budget is exhausted, even though completing them would cost minimal budget.

Direction 1 is the most straightforward and guarantees bounded completion time for any corpus size.

Diagnostic data

-- From cost-usage.sqlite
SELECT path, size, parsed_bytes, scan_complete, 
       datetime(updated_at_ms/1000, 'unixepoch') as last_update
FROM files WHERE scan_complete = 0;
-- The 1.1 GB file: parsed_bytes = 268435456 = exactly maxFileBytes
-- The 28 MB file: parsed_bytes = 524288, stuck since 2026-07-15
-- Both files have been attempted (updated_at_ms is recent for the 1.1GB one)

File characteristics of the 1.1 GB file:

  • 34,338 lines total
  • Average line: 48.3 KB
  • Max line: 3.3 MB (3 lines > 1 MB in first 200 lines)
  • Likely an Ultra-mode session with embedded patches/images

Related issues

Regression test sketch

@Test
func deferredFileReceivesBudgetAfterNewerFilesExhaustGlobalBudget() async throws {
    // 1. Create a large (> maxFileBytes) session file with an old mtime.
    // 2. Create enough newer session files to exhaust maxBytesPerRefresh.
    // 3. Run one refresh → large file should parse first 256 MiB slice.
    // 4. Run a second refresh → large file should advance beyond 256 MiB
    //    (currently fails: progress stays at 268435456).
}

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:fix-shape-clearClawSweeper found a clear likely implementation shape for this issue.clawsweeper:queueable-fixClawSweeper marked this issue as an existing queue_fix_pr work candidate.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:otherThis issue has meaningful maintainer-visible impact outside the owned taxonomy.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.no-staleExempts this issue from stale automation.

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions