Skip to content

Bound Codex cost cache persistence size (#2637) - #2646

Merged
steipete merged 25 commits into
steipete:mainfrom
Yuxin-Qiao:codex/bound-cost-cache-2637
Aug 5, 2026
Merged

Bound Codex cost cache persistence size (#2637)#2646
steipete merged 25 commits into
steipete:mainfrom
Yuxin-Qiao:codex/bound-cost-cache-2637

Conversation

@Yuxin-Qiao

@Yuxin-Qiao Yuxin-Qiao commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Bound the persisted Codex cost-usage cache so the artifact (and the in-memory decode) cannot grow without limit as the local session corpus accumulates.
  • CostUsageCacheIO.save prunes per-file entries whose usage days fall outside the active requested scan window when the cache exceeds an entry count (25,000), the previous artifact exceeded the byte budget (256 MiB), or the candidate artifact encoded in this refresh exceeds the byte budget, mirroring the scanner's drop semantics including day-aggregate subtraction, and narrows persisted coverage after removals.
  • Pruning preserves only genuinely resumable entries (incomplete scans, resume state, buffered fork retries) and out-of-window fork parents referenced by in-window children; completed indexed sessions are eligible for pruning.
  • CostUsageCacheIO.load refuses artifacts above 1 GiB so a pathological cache is rebuilt by the existing bounded scanner instead of being decoded in one shot.
  • Regenerated the Codex parser hash (parser sources changed), which rotates the cache producer key once.

Real local-corpus proof

Ran the fixed CLI against the real local Codex corpus (~290 MB session files) with the cache seeded to 600,273 entries / 80.6 MB (simulating all-time accumulation):

  • Run 1 (loads oversized cache, prunes on save): wall 135s, max RSS 3.39 GB -> wrote 269 entries / 5.9 MB.
  • Run 2 (loads pruned artifact): wall 21s, max RSS 213 MB.

Peak memory drops ~16x and wall time ~6x once the artifact is bounded. Details in the comment thread.

Why

Issue #2637 reports the main process growing past 7 GiB. The reporter's allocation capture showed MALLOC_LARGE dominating during JSONDecoder.decode on the cost-usage-scan queue. The cache keeps one CostUsageFileUsage entry per scanned session file (with rows, turn IDs, token snapshots) and retains out-of-window entries forever, so an all-time corpus can grow the single JSON artifact to multiple gigabytes; every scan then decodes and re-encodes the whole document.

Commands run

  • swift build passed; make check passed (0 lint violations)
  • swift test --filter CostUsageCacheTests|CostUsageFetcherTests — 32 tests passed (current revision)
  • swift test --filter 'CostUsage|CodexLocalProjectUsage|CodexSubagentAccounting' — 398 tests passed

Kept open for reporter validation on a 0.47.0+ build (re-run vmmap -summary + sample on cold launch).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dd2fa7dffd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


let outOfWindowKeys = cache.files.keys.filter { key in
guard let usage = cache.files[key] else { return false }
return !usage.touchesCodexScanWindow(sinceKey: sinceKey, untilKey: untilKey)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve resumable out-of-window fork parents

When the cache is over budget, this removes every file with no usage day in the current window, including an incomplete old parent rollout that was discovered while resolving an in-window fork. Production scans read at most 256 MiB per file per refresh and rely on the cached offset to resume; pruning that entry on every save makes a larger parent restart from byte zero indefinitely, so the child baseline never resolves and catch-up cannot complete. Exclude incomplete files and active fork dependencies from this pruning.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 2fb4d4c: pruning now preserves entries that are still resuming (incomplete scans, resume state, in-progress scan ids, buffered fork retries) and out-of-window fork parents referenced by in-window children; the token-snapshot stripping step was removed entirely, so incremental-resume state is never invalidated.

for (path, var usage) in cache.files {
let newestDay = usage.days.keys.max()
guard let newestDay, newestDay < cutoffKey else { continue }
usage.codexTokenSnapshots = nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Force full rereads after stripping token snapshots

For an older cached parent rollout that is later appended, clearing its snapshots does not invalidate parsedBytes, size, or the incremental-resume state. The next scan therefore reads only the appended tail and rebuilds codexTokenSnapshots from that tail, yet marks the resulting index as covering the whole file; a child fork whose cutoff predates the append then receives a missing or incorrect inherited baseline and can overcount replayed tokens. Retain these snapshots or invalidate the cached parse so the next change forces a full reread.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 2fb4d4c: pruning now preserves entries that are still resuming (incomplete scans, resume state, in-progress scan ids, buffered fork retries) and out-of-window fork parents referenced by in-window children; the token-snapshot stripping step was removed entirely, so incremental-resume state is never invalidated.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2fb4d4ca6b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if usage.touchesCodexScanWindow(sinceKey: sinceKey, untilKey: untilKey) { return false }
if usage.codexScanComplete == false { return false }
if usage.codexJSONLResumeState != nil { return false }
if usage.codexScanFileId != nil { return false }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Allow completed indexed sessions to be pruned

Every current Codex normalization path assigns codexScanFileId to completed entries as well as partial ones (CostUsageScanner+CacheHelpers.swift:1270-1272 and 1427-1429), so this unconditional check preserves virtually every session produced by the current parser. Once the cache exceeds either budget, completed out-of-window files therefore remain persisted and the memory-growth regression this change is intended to fix continues; restrict this exemption to genuinely incomplete/resumable entries.

Useful? React with 👍 / 👎.

let neededParentSessionIDs = Set(cache.files.values.compactMap(\.forkedFromId))
let outOfWindowKeys = cache.files.keys.filter { key in
guard let usage = cache.files[key] else { return false }
if usage.touchesCodexScanWindow(sinceKey: sinceKey, untilKey: untilKey) { return false }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Prune against the requested window instead of the retained union

On normal rolling-window refreshes, CostUsageScanner.swift:4646-4655 keeps scanSinceKey at the minimum historical start and scanUntilKey at the maximum end. Testing entries against those retained bounds means a session that was once requested remains “in window” forever, even after it falls outside the current report, so a long-running installation still accumulates entries without limit. Pass the actual requested range into persistence pruning and adjust the cache coverage bounds when older entries are discarded.

Useful? React with 👍 / 👎.

#expect(loaded.days["2026-04-11"] == nil)
}

func `save preserves out-of-window entries that are still resuming`() throws {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Register the resumable-entry regression as a test

This newly added Swift Testing function lacks @Test, so swift test silently ignores it; the reported 15-test run therefore does not exercise the preservation behavior this case was added to cover. Add the attribute so future pruning changes are actually checked.

AGENTS.md reference: AGENTS.md:L5-L5

Useful? React with 👍 / 👎.

@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P1 Urgent regression or broken agent/channel workflow affecting real users now. merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. merge-risk: 🚨 availability 🚨 Merging this PR could cause crashes, hangs, restart loops, stalls, or process outages. labels Aug 4, 2026
@clawsweeper

clawsweeper Bot commented Aug 4, 2026

Copy link
Copy Markdown

Codex review: needs changes before merge. Reviewed August 4, 2026, 11:40 PM ET / August 5, 2026, 03:40 UTC.

ClawSweeper review

What this changes

Bounds the persisted Codex cost-usage cache and adds recovery-state compaction and regression tests to prevent unbounded memory use during local-session scans.

Regression provenance

Possible regression — probable (reproduction; failure trace; reviewed change). No predecessor PR is attributed.

Merge readiness

⚠️ Needs maintainer review before merge - 4 items remain

This PR remains necessary, but one cache-recovery defect can replace a previously complete report with partial data after a repeated budget trim.

Priority: P1
Reviewed head: 9fdc1a70088a49f05850f61c7580f68e6bfdac0a

Review scores

Measure Result What it means
Overall readiness 🦐 gold shrimp (3/6) Strong real-corpus proof and substantial regression coverage support the approach, but the remaining persisted-report defect blocks merge.
Proof confidence 🦞 diamond lobster (5/6) Sufficient (terminal): The contributor supplied redacted after-fix CLI measurements on a real local Codex corpus showing cache compaction and improved second-run memory and duration.
Patch quality 🦐 gold shrimp (3/6) 1 actionable review finding remain.

Verification

Check Result Evidence
Real behavior Verified Sufficient (terminal): The contributor supplied redacted after-fix CLI measurements on a real local Codex corpus showing cache compaction and improved second-run memory and duration.
Evidence reviewed 5 items PR is not already on main: The PR head is not an ancestor of current main; its merge base is the PR base commit, so current main does not contain this cache-bounding implementation.
Complete report is overwritten: The final hard-cap path rebuilds and assigns a previous report from the current pre-strip cache, even when that cache is already in a partial catch-up pass.
Persisted report is returned on later launches: When catch-up is pending, the scanner returns the persisted previous report, so overwriting it can expose partial totals after a cold restart.
Findings 1 actionable finding [P2] Preserve the complete report across repeated cache trims
Security None None.

How this fits together

CodexBar scans local Codex session files into a persisted usage cache that supplies background refreshes and menu cost reports. The cache is loaded before scanning and saved afterward, so its size and recovery state affect memory use, refresh time, and displayed totals.

flowchart LR
A[Local Codex sessions] --> B[Usage scanner]
B --> C[In-memory usage cache]
C --> D{Within cache budget?}
D -- Yes --> E[Persisted cache]
D -- No --> F[Compact and mark catch-up]
F --> E
E --> G[Next refresh and menu report]
Loading

Before merge

  • Preserve the complete report across repeated cache trims (P2) - When catch-up is already partial, cache.codexPreviousReport can hold the last complete report, but this replaces it with one rebuilt from preStripCache. After restart the pending-catch-up path returns that partial replacement, so users can see reduced totals until scanning completes. Preserve an existing report that covers the requested window, and add a repeated-trim restart regression. This is a late finding on code unchanged since the prior review head.
  • Resolve merge risk (P1) - A repeated trim during an already partial catch-up can persist a partial report and serve incomplete cost totals after restart until catch-up completes.
  • Resolve merge risk (P1) - The new hard-cap recovery path can discard an oversized artifact and trigger a bounded rebuild; its persisted-state behavior needs the focused regression below before merge.
  • Complete next step (P2) - A narrow, mechanical persisted-report repair and focused regression test can resolve the only remaining blocking finding.

Findings

  • [P2] Preserve the complete report across repeated cache trims — Sources/CodexBarCore/Vendored/CostUsage/CostUsageCache.swift:505-508
Agent review details

Security

None.

Review metrics

Metric Value Why it matters
Patch size production +639/-13; tests +1,200 across 4 files The large test addition is proportionate to a complex persisted-cache recovery change.

Root-cause cluster

Relationship: fixed_by_candidate
Canonical: #2637
Summary: This PR is the candidate repair for the reported CodexBar memory-growth problem.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Merge-risk options

Maintainer options:

  1. Preserve the prior complete report (recommended)
    Keep the existing matching previous report during a later trim and add a restart regression test before merging.
Copy recommended automerge instruction
@clawsweeper automerge

Special instructions:
Preserve an existing matching complete previous report during subsequent Codex cache trims, and add a regression test for partial catch-up followed by another trim and cold restart.

Technical review

Best possible solution:

Retain an existing matching complete previous report during later trims unless a replacement is proven to cover at least the same report window, with a regression test covering partial catch-up, another trim, and restart.

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

Yes, at source level. The linked production report and the contributor’s real-corpus run establish the cache path, while the remaining partial-report case follows directly from the persisted catch-up control flow.

Is this the best way to solve the issue?

No, not yet. The bounded cache design is appropriate, but it must preserve an already complete catch-up report rather than replacing it with a report built from a partial scan.

Full review comments:

  • [P2] Preserve the complete report across repeated cache trims — Sources/CodexBarCore/Vendored/CostUsage/CostUsageCache.swift:505-508
    When catch-up is already partial, cache.codexPreviousReport can hold the last complete report, but this replaces it with one rebuilt from preStripCache. After restart the pending-catch-up path returns that partial replacement, so users can see reduced totals until scanning completes. Preserve an existing report that covers the requested window, and add a repeated-trim restart regression. This is a late finding on code unchanged since the prior review head.
    Confidence: 0.94
    Late finding: first raised on code an earlier review cycle already covered.

Overall correctness: patch is incorrect
Overall confidence: 0.94

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against 9cc993f310cb.

Labels

Label justifications:

  • P1: The linked report describes multi-gigabyte growth in normal CodexBar background use.
  • merge-risk: 🚨 session-state: The patch rewrites persisted report, discovery, resume, fork, and catch-up state.
  • merge-risk: 🚨 availability: The hard-cap path can rebuild discarded cache state and currently risks serving partial totals after restart.
  • rating: 🦐 gold shrimp: Overall readiness is 🦐 gold shrimp; proof is 🦞 diamond lobster and patch quality is 🦐 gold shrimp.
  • status: ⏳ waiting on author: ClawSweeper has contributor-facing work open and is waiting for author action. Sufficient (terminal): The contributor supplied redacted after-fix CLI measurements on a real local Codex corpus showing cache compaction and improved second-run memory and duration.
  • proof: sufficient: Contributor real behavior proof is sufficient. The contributor supplied redacted after-fix CLI measurements on a real local Codex corpus showing cache compaction and improved second-run memory and duration.

Evidence

Acceptance criteria:

  • [P1] swift test --filter CostUsageCacheTests.
  • [P1] make check.

What I checked:

Likely related people:

  • steipete: Added the branch’s loader/save contract and integrated main-line cache work into this PR branch. (role: recent area contributor; confidence: high; commits: 1f104be21407, a0c7b2d88a40, 959b466fae77; files: Sources/CodexBarCore/Vendored/CostUsage/CostUsageCache.swift)
  • Xu Xiang: Current main contains the adjacent Codex fork catch-up fix, which is an ancestor of this PR head. (role: recent cache recovery contributor; confidence: medium; commits: 4f99e6aba8c3; files: Sources/CodexBarCore/Vendored/CostUsage/CostUsageCache.swift)

Rank-up moves

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

  • Preserve the existing complete catch-up report during subsequent trims.
  • Add a focused partial-catch-up, trim, and cold-restart regression test.

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 (38 earlier review cycles; latest 8 shown)
  • reviewed 2026-08-04T22:52:20.426Z sha 71e2743 :: needs changes before merge. :: [P1] Share the discovery-ID capacity between both lists
  • reviewed 2026-08-04T23:26:27.301Z sha 71e2743 :: needs changes before merge. :: [P1] Share the discovery-ID capacity between both lists
  • reviewed 2026-08-05T01:31:31.017Z sha 71e2743 :: needs changes before merge. :: [P1] Share the discovery-ID capacity between both lists
  • reviewed 2026-08-05T02:37:38.920Z sha 1f104be :: needs changes before merge. :: [P1] Preserve pending recursive lookback roots | [P1] Share the discovery-ID capacity between both lists
  • reviewed 2026-08-05T02:49:46.059Z sha af49fd9 :: needs changes before merge. :: [P1] Preserve pending recursive lookback roots | [P1] Share discovery-ID capacity across both lists
  • reviewed 2026-08-05T03:12:50.069Z sha 3970a96 :: needs changes before merge. :: [P1] Drain migrated legacy roots into the scan
  • reviewed 2026-08-05T03:20:31.309Z sha 959b466 :: needs changes before merge. :: [P1] Drain migrated legacy roots into ordinary scanning
  • reviewed 2026-08-05T03:36:16.135Z sha 9fdc1a7 :: needs maintainer review before merge. :: none

@Yuxin-Qiao

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review - all three findings are addressed in 88aa138:

  1. Completed indexed sessions are now prunable: the codexScanFileId exemption was removed; only genuinely resumable state (codexScanComplete == false, resume state, buffered fork retries) and out-of-window fork parents referenced by in-window children are preserved.
  2. Pruning now uses the active requested window: CostUsageCacheIO.save accepts requestedScanWindow and the scanner passes the current CostUsageDayRange; persisted scanSinceKey/scanUntilKey are narrowed after removals so the historically widened retained union cannot keep entries in-window forever.
  3. The resumable-entry regression test is registered with @Test (both new tests now run; suite is 31 tests).

Real-behavior proof is being collected: I will run the fixed build's CodexBarCLI cost --provider codex against the local corpus and post the redacted cache-size/duration output.

@clawsweeper

clawsweeper Bot commented Aug 4, 2026

Copy link
Copy Markdown

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: when the review finishes, ClawSweeper will create the durable review comment if needed or update the existing comment in place.

Re-review progress:

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 88aa138a29

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +180 to +181
let overBudget = cache.files.count > maxCacheEntries
|| (previousArtifactBytes ?? 0) > Int64(maxCacheBytes)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Check the new artifact against the byte budget

When the existing cache is below 256 MiB but one refresh adds enough rows or token snapshots to cross that limit, this condition remains false as long as there are at most 25,000 entries because it checks only previousArtifactBytes. A production refresh can admit up to 512 MiB of new session data, so save can write a 256 MiB–1 GiB document that the next load will decode, recreating the memory spike this change is intended to prevent. Base pruning on the candidate artifact being written, or otherwise trigger it before the write when the current cache crosses the byte budget.

Useful? React with 👍 / 👎.

@clawsweeper clawsweeper Bot removed the merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. label Aug 4, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7883c72aa5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

|| (previousArtifactBytes ?? 0) > Int64(maxCacheBytes)
guard overBudget else { return false }

let neededParentSessionIDs = Set(cache.files.values.compactMap(\.forkedFromId))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Exempt parents only for children that survive pruning

The revised dependency calculation includes forkedFromId from every cached child, including out-of-window children removed by this same pass. In a fork-heavy stale corpus, pruning can therefore delete the children while retaining tens of thousands of now-unreferenced parents, leaving the resulting artifact above both budgets; because pruned is then true, the candidate-size fallback at line 158 is skipped, and an artifact above the 1 GiB load limit can be written and discarded on the next refresh. Build the dependency set from in-window/resumable children that will actually remain, or recompute it after candidate removal.

Useful? React with 👍 / 👎.

Comment on lines +203 to +205
let outOfWindowKeys = cache.files.keys.filter { key in
guard let usage = cache.files[key] else { return false }
if usage.touchesCodexScanWindow(sinceKey: sinceKey, untilKey: untilKey) { return false }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Retain zero-day files from the active scan window

When the cache is over budget, a completed session in the currently requested partition that produces no usage rows—such as a suppressed subagent or idless copied prefix—fails touchesCodexScanWindow because days is empty and is therefore removed. The next refresh rediscovers and fully parses that same file; with enough zero-day files, every bounded pass can spend its budget reparsing them and repeatedly defer usage-bearing files. Pass the active scan paths into pruning or otherwise distinguish active zero-day entries from stale out-of-window entries.

Useful? React with 👍 / 👎.

Comment on lines +118 to +121
private static func decodeCache(at url: URL, maxBytes: Int) -> CostUsageCache? {
let fileSize = (try? FileManager.default.attributesOfItem(atPath: url.path)[.size] as? NSNumber)?
.int64Value ?? 0
guard fileSize <= maxBytes else { return nil }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Apply the hard load cap only to bounded providers

The size guard is shared by every CostUsageCacheIO.load call, but the inspected Claude path in loadClaudeDaily uses an unbounded recursive scanner and save prunes only when provider == .codex. Once a Claude or Vertex cache exceeds 1 GiB, each load now returns an empty cache, the provider performs a full corpus rebuild, and then writes another oversized artifact that will be rejected again on the next refresh. Either scope this refusal to Codex or add equivalent bounded persistence/rebuild behavior for the other providers.

Useful? React with 👍 / 👎.

@Yuxin-Qiao

Copy link
Copy Markdown
Contributor Author

Real local-corpus proof (redacted)

Ran the fixed CLI (CodexBarCLI cost --provider codex --refresh) against the real local Codex corpus (~290 MB of session files). To simulate an all-time accumulation, I first seeded the persisted cache with 600,000 out-of-window historical file entries (80.6 MB artifact), then ran the same command twice: once loading the oversized cache, once loading the pruned artifact it wrote.

Seed:  600,273 entries / 80.6 MB artifact (real 273 in-window entries + 600k synthetic 2026-01-02 entries)

Run 1 (loads oversized cache, then prunes on save):
  wall 135s, max RSS 3.39 GB, peak footprint 3.77 GB
  -> wrote 269 entries / 5.9 MB artifact (in-window only)

Run 2 (loads the pruned artifact):
  wall 21s, max RSS 213 MB, peak footprint 160 MB

Same machine, same corpus, same command: peak memory drops ~16x and wall time ~6x once the artifact is bounded, and the oversized artifact is never left on disk for the next refresh to decode. Cache and session paths were not touched beyond the cost-usage cache file; private session IDs/paths omitted.

@clawsweeper clawsweeper Bot added proof: sufficient Contributor real behavior proof is sufficient. rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. and removed rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Aug 4, 2026
@Yuxin-Qiao

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review - all three findings are addressed in 2ad965d:

  1. Load cap scoped to Codex (P1): CostUsageCacheIO.load now applies the 1 GiB refusal only for .codex, which is the provider with bounded persistence pruning on save; Claude/Vertex keep loading oversized artifacts as before, avoiding a rebuild loop.
  2. Parents protected only when referenced by surviving entries (P1): the pruning pass now computes protected parent session IDs from the entries that remain after candidate filtering, so a stale child that is removed no longer keeps its stale parent alive, and the candidate-size fallback can still run.
  3. Active zero-day sessions retained (P2): an entry whose file modification time falls inside the requested scan window is treated as in-window even when it has produced no usage rows yet, so an active session started today is not dropped and rediscovered on every refresh.

Added focused regression tests for all three (suite is now 35 tests), make check clean. Real local-corpus proof is already in the thread and PR body.

@clawsweeper

clawsweeper Bot commented Aug 4, 2026

Copy link
Copy Markdown

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: when the review finishes, ClawSweeper will create the durable review comment if needed or update the existing comment in place.

Re-review progress:

@clawsweeper clawsweeper Bot added rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. labels Aug 4, 2026
steipete and others added 3 commits August 4, 2026 20:12
Resolves the CodexParserHash conflict by regenerating from the merged
parser sources (74cc412adaee297d) rather than picking a side. Keeps
main's compatibleCodexProducerKeys entry for the shipped p843ca061
hash (steipete#2648): legacy caches now load compatibly and are bounded by the
new load cap and save-side pruning instead of a forced rebuild.
Parser hash regenerated from the fully merged sources (e6cd0b92f60a0380).
@Yuxin-Qiao

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review - the P1 from the 03:20 review is addressed in 9fdc1a7:

clearActiveLookbackForBudget no longer migrates legacy recursive lookback roots into the discovery directory queue (which only fork-parent lookup consumes). It now leaves legacyRecursivePendingRootPaths untouched in the active-lookback state and only moves pending file paths into the discovery file queue, so recently modified archived sessions remain discoverable by the ordinary scan path. Regression test updated to assert the legacy roots stay in the lookback state and the discovery directory queue stays empty.

Rebased onto the current head (including @steipete's load-cap commit and the main merges). Suite is 50 tests, make check clean.

@clawsweeper

clawsweeper Bot commented Aug 5, 2026

Copy link
Copy Markdown

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: when the review finishes, ClawSweeper will create the durable review comment if needed or update the existing comment in place.

Re-review progress:

@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 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 Aug 5, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9fdc1a7008

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +424 to +426
cache.codexScanCatchUpPending = true
cache.lastScanUnixMs = 0
cache.codexPreviousReport = previousReport

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve existing catch-up report when trimming again

When a refresh is already in catch-up, cache.codexPreviousReport may be the last complete user-visible report, but this save-time trim always replaces it with a report rebuilt from preTrimCache. If that refresh is still partial (for example after a force/incompatible rebuild or a prior save-time trim) and the cache remains over budget, the rebuilt report can be missing deferred files; after restart, codexPreviousReport will serve that partial data until catch-up finishes. Prefer the existing previous report when present, or only overwrite it with a report known to cover at least the same window.

Useful? React with 👍 / 👎.

@clawsweeper clawsweeper Bot added 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. and removed 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. labels Aug 5, 2026
@steipete
steipete merged commit 90c6fae into steipete:main Aug 5, 2026
1 check passed
@Yuxin-Qiao

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review - the P2 from the 03:40 review is addressed in c6004b3:

Both trim paths now only create a catch-up previous report when the cache does not already carry one, so a repeated budget trim during an in-progress catch-up pass can no longer overwrite a complete report with partial data. The catch-up report is only computed when it will actually be stored (avoids the wasted build). Regression test: save preserves an existing complete report across repeated trims (seeds a pending catch-up state with a complete report and asserts it is retained verbatim).

Suite is 52 tests, make check clean.

@clawsweeper

clawsweeper Bot commented Aug 5, 2026

Copy link
Copy Markdown

🦞🧹
ClawSweeper could not start a re-review for this item.

Reason: re-review requires an open issue or PR.

@steipete

steipete commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Landed. Verification before merge (independent maintainer-agent review, distrust-first):

  • Diagnosis matches the allocation evidence on CodexBar 0.46.0 main process grows past 7 GiB and requires daily restart #2637 (vmmap: MALLOC_LARGE under JSONDecoder on com.steipete.codexbar.cost-usage-scan): the cache kept one entry per session file forever and re-decoded/re-encoded the whole document every scan.
  • Pruning verified to mirror the scanner's own drop semantics (day-aggregate subtraction, scan-window matching, fork-parent protection, resume/buffered-line preservation), with the layered enforcement ladder (out-of-window prune → oldest-first trim with catch-up marking → detail stripping → discovery compaction) and coverage narrowing so stale entries can't masquerade as in-window later.
  • Defect found and fixed (1f104be21): the load-refusal cap was 1 GiB against a 256 MiB save budget, so a 256 MiB–1 GiB legacy artifact still got one-shot decoded on upgrade (~10x expansion — the exact jetsam scenario, made one-time instead of recurring). Load cap lowered to 320 MiB with rebuild fallback, and save now removes an artifact its own loader would refuse instead of persisting it. Two regression tests added; parser hash regenerated.
  • Reconciled with fix: keep Codex fork catch-up progress across appends #2648's append-resume validators after both landed: interaction verified benign (detail stripping zeroes parsedBytes, deliberately failing the resume validators and forcing a bounded full re-read). Producer-key compatibility means existing caches are bounded by pruning rather than force-rebuilt.
  • Local: 424 tests in 27 suites pass (incl. fix: keep Codex fork catch-up progress across appends #2648's suite and the new regression tests); make check — 0 violations. CI green at merge.

This closes the traced allocation path for #2637. Suggested closing condition there: reporter validation on the next build (cold-launch vmmap -summary + sample, as requested in-thread).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c6004b3fa0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}
var seen = Set(discovery?.filePaths ?? [])
for path in pendingPaths where !seen.contains(path) {
discovery?.filePaths.append(path)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep active lookback paths in the scan queue

Fresh evidence versus the earlier lookback comment is that this now copies pending lookback paths only into codexSessionDiscovery.filePaths before clearing codexActiveLookbackState.pendingFilePaths. When the byte cap trips with older recently modified sessions queued, the normal refresh path still only appends active-lookback pendingFilePaths to the scan list (CostUsageScanner.swift:4539-4543); discovery filePaths are consumed only during fork-parent lookup, so any queued session that is not needed as a parent disappears from ordinary scanning and its current-window usage stays missing until a force rescan. Keep these paths in the active lookback queue or also feed them into the normal scan list.

Useful? React with 👍 / 👎.

@Yuxin-Qiao

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

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

Labels

merge-risk: 🚨 availability 🚨 Merging this PR could cause crashes, hangs, restart loops, stalls, or process outages. merge-risk: 🚨 session-state 🚨 Merging this PR could lose, corrupt, stale, or mis-associate session or agent state. P1 Urgent regression or broken agent/channel workflow affecting real users now. 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.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants