fix: wrap the cost-store save cycle in one transaction so a crash mid-save is all-or-nothing (refs #2760) - #2771
Conversation
|
Codex review: needs changes before merge. Reviewed August 8, 2026, 4:28 PM ET / 20:28 UTC. ClawSweeper reviewWhat this changesThe branch wraps the Codex SQLite cost-cache save cycle in one transaction, adds a subprocess SIGKILL recovery test, and lengthens timeout-test delays. Regression provenancePossible regression — suspected (reviewed change). No predecessor PR is attributed. Merge readiness⛔ Blocked by patch quality or review findings - 7 items remain Keep this owner-authored PR open: its crash-path proof is useful, but both prior transaction-failure blockers remain at the unchanged current head. Priority: P1 Review scores
Verification
How this fits togetherCodex usage scans save per-session data, aggregates, and scan metadata in a local SQLite cache that later feeds app and CLI cost reports. Budget enforcement runs after the main save cycle to retain the cache within its limits. flowchart LR
A[Session scan] --> B[Cost cache save]
B --> C[SQLite transaction]
C --> D[Per-file records]
C --> E[Aggregates and metadata]
E --> F[Budget enforcement]
F --> G[App and CLI reports]
Before merge
Findings
Agent review detailsSecurityNone. Review metrics
Merge-risk optionsMaintainer options:
Copy recommended automerge instructionTechnical reviewBest possible solution: Return and honor transaction-start and transaction-end success, skipping all later save-cycle mutations on failure; add focused BUSY/LOCKED-start and nested-write-failure regression coverage. Do we have a high-confidence way to reproduce the issue? Yes. The subprocess harness establishes the crash path, and a second SQLite writer can force BEGIN IMMEDIATE to fail while focused failure injection can exercise an inner write failure. Is this the best way to solve the issue? No. The enclosing transaction is the right design, but its failed-start and failed-end results must stop all later mutations for the save to be all-or-nothing. Full review comments:
Overall correctness: patch is incorrect AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 4c2b9217af72. LabelsLabel changes:
Label justifications:
EvidenceAcceptance criteria:
What I checked:
Likely related people:
Rating scale
Overall follows the weaker of proof and patch quality. Workflow
HistoryReview history (7 earlier review cycles)
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 13aae64b74
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| // midway can never leave e.g. files upserted while day_aggregates stay stale. | ||
| // Budget enforcement below runs outside: it checkpoints the WAL and vacuums, which | ||
| // SQLite forbids inside an open transaction. | ||
| _ = self.withSaveTransaction(default: false) { |
There was a problem hiding this comment.
Skip budget writes after a failed save transaction
When the enclosing save transaction returns false after BEGIN IMMEDIATE or a nested write fails with a preserved error, this result is discarded and the method still runs budget enforcement and the catch-up metadata write. If the existing store is over the row/byte budget in that failure path, those follow-up writes can prune rows or persist a previousReportPayload derived from the unsaved in-memory cache, so the previous on-disk state is no longer the all-or-nothing rollback state this transaction is meant to preserve. Gate the budget/report phase on the transaction succeeding.
Useful? React with 👍 / 👎.
13aae64 to
3c93eab
Compare
…-save is all-or-nothing (refs #2760)
…isfy older Swift 6 region analysis
… deadline always wins the race on loaded CI runners
9788707 to
ba2350c
Compare
Follow-up to the SQLite cost-store cutover (refs #2760), fixing defect D3 from the adversarial review on #2765:
saveCodexCacheperformed many per-table writes without an enclosing transaction, so a crash mid-save could leavefilesupserted whileday_aggregatesstayed stale until the next scan cycle rewrote them. The old JSON path was atomic via single-file replace; the SQLite path now matches it.What changed
saveCodexCache's mutation phase (removed-file deletes → per-file persists → global day aggregates → metadata/discovery/lookback singletons) now runs inside oneBEGIN IMMEDIATE/COMMITvia a newwithSaveTransactionseam on the store.inTransactionbecomes a no-op wrapper whensqlite3_get_autocommitreports an open transaction, so the existing per-write transactions join the outer one instead of failing with a nestedBEGIN(which would previously have been classified as corruption and nuked the database).withDatabasecalls join the outer connection scope and the first inner failure aborts the rest of the cycle. Without this, a failed statement that auto-rolled-back the transaction would let the remaining writes commit individually in autocommit mode — exactly the partial state the transaction exists to prevent. The original error propagates to the outer scope, which keeps the existing rebuild-vs-preserve classification from fix: keep the cost-usage database on transient SQLite failures, only rebuild on corruption (refs #2760) #2765.PRAGMA wal_checkpoint(TRUNCATE)andincremental_vacuum, which SQLite forbids inside an open transaction. Its destructive path already marks catch-up pending, so interruption there self-heals on the next scan.Kill-mid-save proof
New
CostUsageStoreCrashSafetyTestsspawns a real subprocess (CodexBarCostStoreCrashProbe, a test-only executable target) that seeds a store, then re-saves an updated cache and SIGKILLs itself deterministically inside the save transaction — after the first file's table writes were issued, before aggregates and metadata — via a test-only checkpoint hook. The test then reopens the database from the parent process and asserts the previous state survived byte-for-byte in shape (per-file days, file set, and global day aggregates).Verified the test catches the bug: with the transaction wrapper temporarily removed, it fails showing exactly D3's torn state — one file updated to the new tallies, the removed file already deleted, and
day_aggregatesstill holding the stale totals.Proof
swift test --filter CostUsageStore— 69 tests in 4 suites passed (includes the new crash-safety suite plus the existing store, failure-injection, and cutover suites).make check— clean.