Skip to content

Persist Codex priority-turn scan cursor across relaunches - #3130

Merged
steipete merged 3 commits into
steipete:mainfrom
olddonkey:perf/persist-codex-priority-turn-cursor
Aug 21, 2026
Merged

Persist Codex priority-turn scan cursor across relaunches#3130
steipete merged 3 commits into
steipete:mainfrom
olddonkey:perf/persist-codex-priority-turn-cursor

Conversation

@olddonkey

@olddonkey olddonkey commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Summary

After every relaunch, CodexBar's first Codex refresh re-scans the whole codex CLI trace database. This PR makes the priority-turn scan cursor durable across launches so a relaunch resumes incrementally instead of re-scanning.

Root cause (profiled). CostUsageScanner.codexPriorityTurns runs three leading-wildcard feedback_log_body LIKE '%…%' predicates over <codexHome>/logs_2.sqlite. Leading % defeats every index, so it is a full body-column scan — 1.4 GB / 130k rows on the reference machine (release CodexBarCLI, sample(1): pread + strcspn/patternCompare under codexPriorityTurns). With a cold OS page cache that is ~2 minutes of I/O-bound wall time (~20 s CPU); with a warm page cache it is ~2.5–3 s of CPU. The function already scans incrementally (rowid > lastRowID) through a process-global memo, but that memo was never persisted, and app refreshes always inspect priority turns (bypassScannerDebouncerefreshMinIntervalSeconds = 0), so every process launch paid the scan again.

Measured (isolated cache root, same real data, same machine, codexbar cost --provider codex --days 30; M1/M2 = a fresh process after the 60 s refresh interval, i.e. the relaunch path):

binary cold scan within interval (cached) relaunch M1 relaunch M2
main (f74117a) 8.73 s / 7.46 s CPU 0.82 s 5.86 s / 5.46 s CPU 5.37 s / 5.00 s CPU
this PR 8.61 s / 7.71 s CPU 0.84 s 3.33 s / 3.08 s CPU 2.85 s / 2.63 s CPU

Token and cost totals are identical between the two binaries at every step; the cached fast path is unchanged. The saved ~2.5 s CPU per relaunch is the LIKE scan with a warm page cache; the cold-page-cache case (~2 min I/O wait) is removed for the same reason but was not re-measured (no purge without sudo).

What changes

  • Persist the memo cursor (lastRowID, sqlite file identity, coverage epoch, accumulated turns) as an optional turnsCursor inside the existing StoredPriorityState payload in the codex cost cache, and seed the in-memory memo from it before the cold path runs.
  • All existing invalidation rules apply unchanged to a seeded cursor: file identity (inode) change, rowid regression, and a requested window that expands earlier than the cursor's coverage each force a full rescan. forceRescan drops the memo instead of seeding it.
  • The cursor is excluded from the identical-content save comparison (its lastRowID advances on nearly every refresh because the codex CLI appends trace rows continuously) and is written as a metadata-only update on the skip path, so the cheap no-op save stays cheap.
  • StoredPriorityState decodes the cursor leniently (try? decodeIfPresent): a malformed cursor yields nil and one cold trace scan, never a dropped turnKeys (which would trigger cache-wide reprocessing).
  • The persisted cursor is validated with a content anchor (rowid the accumulation ended on + SHA-256 of that row's ts\nbody), captured with one primary-key lookup and checked before resuming; a missing row or digest mismatch forces a full rescan, so a recreated logs_2.sqlite that reuses an inode cannot resume from a stale cursor (review feedback). The inode check remains as a cheap pre-check.
  • CodexParserHash is regenerated (hashed scanner sources changed). The previous hash 2d17f4981b78d07f is added to compatiblePredecessorParserHashes — parsing and the persisted row shape are unchanged — so existing cost-usage.sqlite stores are adopted on upgrade, not rebuilt; an old priority payload without a cursor loads with codexPriorityTurnsCursor == nil and intact turnKeys.

Out of scope (deliberately unchanged): the LIKE predicates / query plans / parsers, pricing, the JSONL corpus scan and its 2 s budget, catch-up policy. No new dependencies.

Tests

New CostUsageScannerCodexPriorityCursorTests (9 cases): relaunch reuses the persisted cursor and scans only appended rows (a mutated old row must not reappear); inode change still full-scans (with maxRowID == lastRowID so only identity can trigger it); window expanded earlier still full-scans; old payload without a cursor cold-scans; live memo wins over a stale seed; stale-cursor re-accumulation is idempotent (turns, source maps, insertion order, no duplicates); identical-content skip still persists an advanced cursor with zero file writes; malformed cursor still loads turn keys; force rescan drops the cursor and cold-scans; replaced database with a reused inode (anchor mismatch → full rescan, matching anchor → incremental); deleted anchor row → full rescan; payload without anchor fields → cold scan. Plus CostUsageStoreTests: predecessor-hash adoption of a cursorless payload without rebuild.

Note: the skip-path test uses the repo's process-global saveCycleCheckpointForTesting seam like other store tests; it is safe under the repo runner (swift test --no-parallel) and can interfere only in ad-hoc parallel swift test runs.

Verification

  • make check: parser hash current (3c984b655688593f), 0/1974 files require formatting, swiftlint 0 violations, 0 serious in 1973 files.
  • Full suite make test: green on c529ac35c (tree cc21c5df; exit 0, 1116 s, ~9.2k tests) and again on 51845ab7d after the content-anchor follow-up (exit 0, 852 s, 9231 tests; all cursor-suite cases incl. the reused-inode / deleted-anchor / missing-anchor-fields cases executed inside the gate) — exit 0, 1116.6 s, ~9.2k tests passed across all shards; all 9 CostUsageScannerCodexPriorityCursorTests cases and the new predecessor-adoption case executed inside the gate.
  • Baseline make test on main (f74117a): green (1174 s).

Memory note

The ~400 MB steady RSS some users see is a separate story (GUI framework residency + transient multi-provider scan buffers + malloc fragmentation; the CLI runs this same scan in ~107 MB). This PR targets the CPU burst, not the resident-memory number.


Implemented by grok-4.6 (xhigh) via implementation-loop; reviewed hunk by hunk plus an independent deep review; one iterate round (write-amplification and lenient-decode fixes) and two follow-ups (parser hash regeneration, predecessor adoption).

🤖 Generated with Claude Code

@clawsweeper

clawsweeper Bot commented Aug 21, 2026

Copy link
Copy Markdown

🦞👀
ClawSweeper picked this up.

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

olddonkey added a commit to olddonkey/CodexBar that referenced this pull request Aug 21, 2026
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@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: 3d780b8a94

ℹ️ 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 databasePath: String
var coverageSinceEpoch: Int64
var lastRowID: Int64
var fileIdentity: UInt64?

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 Avoid persisting an inode as the database identity

If logs_2.sqlite is deleted and recreated between launches and the filesystem reuses its inode, this persisted identity still matches; when the replacement has already reached maxRowID >= lastRowID, the validation at lines 266–269 accepts the old cursor and skips every replacement row through lastRowID, retaining stale priority classifications. Inodes are only unique while a file exists, so the durable cursor needs a replacement-resistant database generation or equivalent validation before resuming.

Useful? React with 👍 / 👎.

olddonkey and others added 2 commits August 21, 2026 13:40
After every relaunch, CodexBar's first Codex refresh re-scans the whole codex
CLI trace DB. Profiling (release CodexBarCLI, real ~/.codex, sample(1)) pinned
it on CostUsageScanner.codexPriorityTurns: three leading-wildcard
`feedback_log_body LIKE '%...%'` predicates over `logs_2.sqlite` (1.4 GB /
130k rows on the reference machine) — a full body-column scan. With a cold OS
page cache that is ~2 minutes of I/O-bound wall time (~20 s CPU); with a warm
page cache it is ~2.5-3 s of CPU on every relaunch. The function already scans
incrementally (`rowid > lastRowID`) through a process-global memo, but that
memo was never persisted, and app refreshes always inspect priority turns
(`bypassScannerDebounce` → `refreshMinIntervalSeconds = 0`), so every process
launch paid the cold scan again.

Persist the memo cursor (lastRowID, sqlite file identity, coverage epoch,
accumulated turns) in the codex cost cache metadata next to the existing
StoredPriorityState, and seed the memo from it before the cold path runs.
All existing invalidation rules still apply to a seeded cursor (file identity
change, rowid regression, window expanded earlier than coverage), and
`forceRescan` drops the memo instead of seeding it.

Two steady-state guards: the cursor is excluded from the identical-content
save comparison (its lastRowID advances on nearly every refresh) and is
written as a metadata-only update on the skip path, so the cheap no-op save
stays cheap; and the cursor decodes leniently so a malformed cursor can never
take `turnKeys` down with it (which would trigger cache-wide reprocessing).

CodexParserHash is regenerated because hashed scanner sources changed; the
previous hash (2d17f4981b78d07f) is added to compatiblePredecessorParserHashes
since parsing and the persisted row shape are unchanged, so existing
cost-usage.sqlite stores are adopted on upgrade instead of rebuilt (an old
priority payload without a cursor simply yields one cold trace-DB scan).

Measured (isolated cache root, same real data, same machine): the first
refresh after a relaunch drops from ~5.4-5.9 s / ~5.0-5.5 s CPU (main) to
~2.9-3.3 s / ~2.6-3.1 s CPU (this change); token and cost totals are identical
at every step; the within-interval cached fast path is unchanged (~0.8 s).

Tests: 9 new cases in CostUsageScannerCodexPriorityCursorTests covering
relaunch reuse with incremental-only scanning, inode change, window
expansion, old payload compatibility, live-memo-wins seeding, stale-cursor
re-accumulation idempotency, skip-path cursor persistence, malformed cursor
decode, and force rescan; plus a predecessor-adoption case in
CostUsageStoreTests for a cursorless payload.

Implemented by grok-4.6 (xhigh) via implementation-loop; reviewed hunk by
hunk plus an independent deep review; one iterate round.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@olddonkey
olddonkey force-pushed the perf/persist-codex-priority-turn-cursor branch from c1b9d25 to a37fa55 Compare August 21, 2026 20:40
@clawsweeper clawsweeper Bot added 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. 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 Aug 21, 2026
@clawsweeper

clawsweeper Bot commented Aug 21, 2026

Copy link
Copy Markdown

Codex review: needs maintainer review before merge. Reviewed August 21, 2026, 5:33 PM ET / 21:33 UTC.

ClawSweeper review

What this changes

This PR persists Codex trace-scan progress in the local cost cache so relaunches scan only newly appended SQLite rows instead of the full trace database.

Merge readiness

⚠️ Ready for maintainer review - 2 items remain

Keep open for normal merge review: the prior inode-reuse concern is addressed with content-anchor validation, and no blocking defect was found in the updated patch.

Priority: P2
Reviewed head: 51845ab7d185521222c372fa501b66fc643f3710

Review scores

Measure Result What it means
Overall readiness 🐚 platinum hermit (4/6) The patch is focused, upgrade-aware, and backed by real performance evidence plus extensive targeted coverage.
Proof confidence 🦞 diamond lobster (5/6) Sufficient (terminal): The PR body provides before-and-after CLI measurements against a real Codex trace database and reports repeated full-suite runs after the anchor fix.
Patch quality 🐚 platinum hermit (4/6) No actionable review findings were identified.

Verification

Check Result Evidence
Real behavior Verified Sufficient (terminal): The PR body provides before-and-after CLI measurements against a real Codex trace database and reports repeated full-suite runs after the anchor fix.
Evidence reviewed 6 items Cursor restore and invalidation: The refresh plan restores a cursor only for a matching database path, drops it for forced rescans, and passes the resolved database URL into the priority-turn scan.
Replacement validation: Before incremental reuse, the scanner rejects a cursor when the database identity, row range, requested coverage, or anchored terminal row content differs.
Upgrade compatibility: The persisted payload decodes its optional cursor leniently while preserving existing turn keys, and the store accepts the prior parser hash without rebuilding compatible caches.
Findings None None.
Security None None.

Live Verification

Command: swift test --filter CostUsageScannerCodexPriorityCursorTests

Result: FAIL (failed) — execution before step 1 run: sh -lc pnpm install --ignore-scripts --frozen-lockfile failed: ! Corepack is about to download https://registry.npmjs.org/pnpm/-/pnpm-11.22.0.tgz

sh -lc pnpm install --ignore-scripts --frozen-lockfile failed: ! Corepack is about to download https://registry.npmjs.org/pnpm/-/pnpm-11.22.0.tgz

Assertions:

  • FAIL expect_output: Test run with

How this fits together

CodexBar scans the local Codex CLI trace database to derive priority-turn cost metadata. This change saves that scanner state in the cost cache and validates it before restoring it on a later launch.

flowchart LR
A[Codex trace SQLite database] --> B[Priority-turn scanner]
B --> C[Cursor validation]
C --> D[Usage summary]
D --> E[Local cost cache]
E --> F[Next-launch restore]
Loading

Before merge

  • Resolve merge risk (P1) - This extends persisted cost-cache metadata; the cursorless-payload and predecessor-hash compatibility coverage should remain green on the final merge head.
  • Complete next step (P2) - No discrete repair remains; this PR needs ordinary maintainer review and completion of its platform checks.
Agent review details

Security

None.

Review metrics

Metric Value Why it matters
Code and test scope production +292, tests +767, changelog +1 The sizable focused regression suite covers a durable cache-state change and its upgrade paths.

Merge-risk options

Maintainer options:

  1. Land with cache compatibility coverage (recommended)
    Merge once the pending platform checks confirm that existing cursorless cost caches adopt without rebuilding and invalid cursors cold-scan safely.

Technical review

Best possible solution:

Merge the durable cursor with its conservative invalidation and cache-adoption coverage once the pending platform checks complete.

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

Yes—current source shows each eligible refresh scans the trace database, and the PR supplies a concrete real-database CLI benchmark for the relaunch path.

Is this the best way to solve the issue?

Yes—the durable cursor reuses the existing incremental scanner while retaining forced-rescan, coverage, identity, and content-anchor invalidation paths.

AGENTS.md: found and applied where relevant.

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

Labels

Label changes:

  • add rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🦞 diamond lobster and patch quality is 🐚 platinum hermit.
  • add status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): The PR body provides before-and-after CLI measurements against a real Codex trace database and reports repeated full-suite runs after the anchor fix.
  • remove status: ⏳ waiting on author: Current PR status label is status: 👀 ready for maintainer look.
  • remove rating: 🦐 gold shrimp: Current PR rating is rating: 🐚 platinum hermit, so this older rating label is no longer current.

Label justifications:

  • P2: This is a bounded performance and cache-correctness improvement for Codex cost refreshes.
  • merge-risk: 🚨 compatibility: The PR writes and restores persisted local cache metadata across upgrades and relaunches.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🦞 diamond lobster and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): The PR body provides before-and-after CLI measurements against a real Codex trace database and reports repeated full-suite runs after the anchor fix.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body provides before-and-after CLI measurements against a real Codex trace database and reports repeated full-suite runs after the anchor fix.

Evidence

What I checked:

Likely related people:

  • pickaxe: Introduced the incremental priority-turn memo that this persistence path extends. (role: introduced incremental scanner behavior; confidence: high; commits: 71b93e5f8727; files: Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+CodexPriority.swift)
  • Anri Lombard: Authored the recent priority trace-accounting correction in the same scanner surface. (role: recent priority-accounting contributor; confidence: high; commits: 0296845fb36e; files: Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+CodexPriority.swift)
  • Peter Steinberger: Recent history shows substantial maintenance of the cost-cache save and upgrade paths affected here. (role: recent cache-persistence contributor; confidence: high; commits: 6bf0dc4aafd8, c735150bef29; files: Sources/CodexBarCore/Vendored/CostUsage/CostUsageStore+CodexCache.swift)

Rank-up moves

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

  • Allow the pending platform checks to complete on this head.

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 (1 earlier review cycle)
  • reviewed 2026-08-21T20:45:26.963Z sha a37fa55 :: needs changes before merge. :: [P2] Use a replacement-safe database generation

Review feedback on steipete#3130: the persisted cursor's `fileIdentity` is only the
sqlite inode. If `logs_2.sqlite` is deleted and recreated between launches
and the filesystem reuses the inode, and the replacement already has
`maxRowID >= lastRowID`, the inode check accepts the stale cursor and the
`rowid > lastRowID` query skips every replacement row up to lastRowID while
stale priority classifications are retained. In-process that window was
short; with persistence it is arbitrarily long.

Add a content anchor to both the in-memory memo and the persisted cursor:
the rowid the accumulation ended on plus the SHA-256 of that row's
`"<ts>\n<feedback_log_body>"`. Capture it with one primary-key lookup after
each successful accumulation (if the lookup fails the turns are still
returned but nothing is memoized or persisted), and validate it before
resuming: a missing row or a digest mismatch forces a full rescan. The inode
check stays as a cheap pre-check. A missing anchor row can also be Codex
pruning old rows in place; the conservative full rescan is intended there.
SHA-256 uses the repo's CryptoKit/swift-crypto pattern so the Linux CLI
still builds.

Tests: replaced database with a reused inode (anchor mismatch -> full
rescan; matching anchor -> incremental control), deleted anchor row ->
full rescan, payload without anchor fields -> nil cursor / cold scan; the
relaunch test now also asserts the anchor survives and advances.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@olddonkey

Copy link
Copy Markdown
Contributor Author

Addressed the P2 from the Codex review (and ClawSweeper's merge-risk item): the persisted cursor no longer trusts the inode as its only identity.

What changed (commit on this branch): both the in-memory memo and the persisted cursor now carry a content anchor — the rowid the accumulation ended on plus the SHA-256 of that row's "<ts>\n<feedback_log_body>". It is captured with one primary-key lookup after each successful accumulation (if that lookup fails nothing is memoized/persisted), and validated before resuming: a missing anchor row or a digest mismatch forces a full rescan. The inode check stays as a cheap pre-check. This also closes the same (shorter) window the in-process memo had. A missing anchor row can also be Codex pruning old rows in place; the conservative full rescan is intended there. SHA-256 uses the repo's CryptoKit/swift-crypto pattern so the Linux CLI still builds.

Tests added: replaced database with a reused inode (stale anchor → full rescan; matching anchor → incremental control, with a mutated old row that must not reappear), deleted anchor row → full rescan, payload without anchor fields → nil cursor / cold scan; the relaunch test now also asserts the anchor survives and advances. CodexParserHash regenerated accordingly (the unreleased intermediate hash is not listed as a predecessor; 2d17f4981b78d07f from main still is).

The "Live Verification FAIL" in the ClawSweeper report is its own environment failing at pnpm install before running anything; not related to this change.

@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 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 Aug 21, 2026
@steipete
steipete merged commit df18670 into steipete:main Aug 21, 2026
9 checks passed
steipete added a commit that referenced this pull request Aug 21, 2026
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. proof: sufficient Contributor real behavior proof is sufficient. 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.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants