Repository navigation
Keep a record's audit history readable after it is evicted - #3504
Merged
Merged
Conversation
Contributor
There was a problem hiding this comment.
Code Review
This pull request enhances historical record reconstruction (getRecordAtTime) and per-record history retrieval (getHistoryOfRecord) to correctly span across record evictions and re-inserts. It introduces log scanning as a fallback when a predecessor link chain is broken due to eviction, optimizes exact-key lookups to avoid scanning, and adds comprehensive unit tests validating these behaviors. There are no review comments, and we have no further feedback to provide.
kriszyp
force-pushed
the
fix/record-history-after-eviction
branch
from
October 24, 2026 05:08
47ab189 to
be8d9dd
Compare
This was referenced Oct 25, 2026
Open
kriszyp
force-pushed
the
fix/record-history-after-eviction
branch
from
October 28, 2026 17:02
be8d9dd to
debcf98
Compare
This was referenced Nov 6, 2026
getHistoryOfRecord returned [] for an evicted record and dropped everything before a re-insert that came more than the 100ms walk window after the eviction, since the re-insert has no previousVersion link to follow. It now scans the retained log for an evicted record, and scans below the walked range when the walk ends at an unlinked write of a record that can have been evicted. getRecordAtTime now starts from the newest put/patch/delete at or before a timestamp that is not an audit key, instead of returning null. Refs #2730 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Dispatch-Task: harper-record-history-after-eviction Co-Authored-By: GPT-5 Codex <noreply@openai.com> Dispatch-Task: pr-maint-0611fabc36c24b9ab57c8473c3566a23
- Scan below the walk only when its oldest entry has no predecessor link, not when it links to a pruned entry in the same window. - Start a non-key timestamp from the exact entry only when it is a state change or links to one; an unlinked eviction marker reaches no state. - Make the crdt mock return another record's entry at a key, as LMDB does. Refs #2730 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Dispatch-Task: harper-record-history-after-eviction Co-Authored-By: GPT-5 Codex <noreply@openai.com> Dispatch-Task: pr-maint-0611fabc36c24b9ab57c8473c3566a23
… start reconstructAtTimestamp now searches back in doubling windows, the first holding only the timestamp, so an exact-key miss (RocksDB's exact lookup misses compound ids) costs one narrow range instead of a scan from key 1. Corrects the documented gap: a re-insert without an expiry after read-path expiry of a per-record expiresAt on a table that cannot evict is also not bridged. Refs #2730 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Dispatch-Task: harper-record-history-after-eviction Co-Authored-By: GPT-5 Codex <noreply@openai.com> Dispatch-Task: pr-maint-0611fabc36c24b9ab57c8473c3566a23
Also makes the exact-key test count any non-exact range, and drops two comments that restated code. Refs #2730 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Dispatch-Task: harper-record-history-after-eviction Co-Authored-By: GPT-5 Codex <noreply@openai.com> Dispatch-Task: pr-maint-0611fabc36c24b9ab57c8473c3566a23
Refs #2730 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Dispatch-Task: harper-record-history-after-eviction Co-Authored-By: GPT-5 Codex <noreply@openai.com> Dispatch-Task: pr-maint-0611fabc36c24b9ab57c8473c3566a23
Resolve complete same-origin frames for exact timestamp reads, predecessor walks and reversal. Preserve the shared keyed lookup contract and cover real put/patch, multiple-patch, later-link and replacement histories after eviction. Dispatch-Task: pr-maint-0611fabc36c24b9ab57c8473c3566a23 Co-Authored-By: GPT-5 Codex <noreply@openai.com>
Read RocksDB frames in one pass and retain single-entry LMDB lookups. Add real-store delete/recreate and put/delete boundary regressions. Dispatch-Task: pr-maint-0611fabc36c24b9ab57c8473c3566a23 Co-Authored-By: GPT-5 Codex <noreply@openai.com>
Dispatch-Task: pr-maint-0611fabc36c24b9ab57c8473c3566a23 Co-Authored-By: GPT-5 Codex <noreply@openai.com>
Dispatch-Task: pr-maint-0611fabc36c24b9ab57c8473c3566a23 Co-Authored-By: GPT-5 Codex <noreply@openai.com>
Dispatch-Task: pr-maint-0611fabc36c24b9ab57c8473c3566a23 Co-Authored-By: GPT-5 Codex <noreply@openai.com>
Dispatch-Task: pr-maint-0611fabc36c24b9ab57c8473c3566a23 Co-Authored-By: GPT-5 Codex <noreply@openai.com>
Keep control-frame origin selection consistent and use plain rebuilding after an observed head changes. Measure the remaining evicted and bulk-patch reader costs. Dispatch-Task: pr-maint-0611fabc36c24b9ab57c8473c3566a23 Co-Authored-By: GPT-5 Codex <noreply@openai.com>
Dispatch-Task: pr-maint-0611fabc36c24b9ab57c8473c3566a23 Co-Authored-By: GPT-5 Codex <noreply@openai.com>
Resolve requested-origin frames before accepting another origin's candidate, and choose by audit key rather than append order. Extend native regressions with interleaved control frames and a late-appended older put. Dispatch-Task: pr-maint-0611fabc36c24b9ab57c8473c3566a23 Co-Authored-By: GPT-5 Codex <noreply@openai.com>
This was referenced Nov 12, 2026
kriszyp
force-pushed
the
fix/record-history-after-eviction
branch
from
November 12, 2026 00:02
debcf98 to
a4b0e86
Compare
Keep the reviewed runtime unchanged and avoid expanding this scoped history fix into a shared-note rewrite. Dispatch-Task: pr-maint-0611fabc36c24b9ab57c8473c3566a23 Co-Authored-By: GPT-5 Codex <noreply@openai.com>
kriszyp
marked this pull request as ready for review
November 12, 2026 05:49
This was referenced Nov 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
⊙ Problem
Eviction removes the primary entry that anchored retained history. Per-record audit reads then return
[], and reinsertion can hide older segments (#2730). Complete transaction-frame reconstruction fixes stale multi-mutation values but repeatedly decodes large frames. Long patch listings also rebuild the same prefixes.💡 Solution
Recover absent/eligible unlinked history from retained audit entries. Reconstruct complete matching-origin frames and reuse copied entries and ordinary patch images within each history call. Unsafe chains, corruption and budget exhaustion use the plain rebuild. Search-back stops at the lower retained-log/floor boundary. Writes and stored formats stay unchanged. This covers #4063's eligible live-listing goal; that draft is untouched.
⚖️ Alternatives
🤔 Keep retained history despite its scan cost?
Retain it. Per-id scans can decode all retained history synchronously. The owner accepted the measured listing latency and froze optimization. Residual CPU and the reinsertion gap are tracked in #4286 and #4285.
Reader design and native-filtering alternative
🤔 Why keep the reader-local design?
Framing-Verdict: better-alternative-exists— resolved by adopting detached bytes and conservative plain fallback. An observed head change abandons reuse after a confirmed wrong-value regression. Image stamps, refusal caches and LRU eviction remain deferred.🤔 Native filtering could simplify frame reads
Native record/origin filtering could remove JavaScript scans and copies. It needs a binding/release and does not supply missing predecessor anchors or remove prefix replay. The reader repair delivers measured gains; #4286 tracks remaining work.
🔧 Changes
How retained values and per-call reuse work
historyReader.ts:339
A new write can change the plain oracle for an older patch. When the existing eligibility check observes a changed head, the call abandons reuse; it does not validate an old carried image against the new head.
Product and architecture tour
Retained values after eviction
Which history survives losing the primary entry?
One bounded reader per call
When can a listing reuse work?
Full values without persistent caches
Complete matching-origin frames remain the reconstruction unit.
flowchart TD A[History entry] --> B{Safe bounded reuse?} B -->|yes| C[Copied frame or patch image] B -->|no| D[Plain audit reconstruction] C --> E[Full value] D --> E E --> F[Release context when call ends]✅ Verification
Performance: Latest single samples: 50,000-row histories 169/155/163 ms; 2,880 live patches 198 ms. Evicted listing: 34.7 s; 1,000-row bulk-patch live/stream/replay: 2.83/6.24/2.90 s. #4286 records costs and the reproducer; accepted medians and main controls are below.
8ee0988changes documentation only, so the scenario remains applicable.Executed checks and scope limits
Benchmark controls and limits
The scoped source uses RocksDB 2.14.0 and Node 26.2.0. Latest single-sample wall/CPU ms and native reads: first-row169/258/24; middle155/239/24; last163/273/24;2,880 live patches198/278/2,887. Earlier accepted three-sample medians were196/293,202/233,169/212 and182/234 respectively. Same-dependency main3b9072d0f one-sample controls:316/319,1,959/2,339,3,673/4,103 ms with1,259reads each;2,880patches132,537/154,990 ms with24,891,850reads. The historical92.64 ms pre-frame baseline used older dependencies and is not claimed achieved.
Evicted2,880 patches:34,655 ms wall/41,044 CPU,5,765 native reads (one sample). This low read count hides repeated copied-frame restoration. At1,000 rows, live bulk-patch delivery including commit:2,829/3,965 ms,1,000ranges; table history:6,237/8,555 ms,2,001ranges; replay:2,901/3,961 ms,1,002ranges. These are not50,000-row measurements.
Earlier50,000-filler-row fallback probes, one sample each: exact never-cached invalidate109/176 ms wall/CPU (37ranges); missing id132/245 ms (1range); eligible first write102/171 ms (2ranges); non-exact miss120/172 ms (37ranges). Bounding search windows does not bound retained-entry decoding. A floor of1 retains conservative scan behavior. Broader stress/worker/restart coverage is tracked in #4287.
Closing measurements (one sample each,50,000 filler rows): raised-floor nearby read128.840ms wall/239.141CPU versus2.192/8.122 in pre-bound ancestor3d3a165. Both return the correct value; the new prefix scan is tracked by #4293. Clean memo misses disable reuse; image admission can rebuild an already-computed value. Their counters and timings are in #4286.
Documentation-only correction
8ee0988restores DESIGN.md and resources/DESIGN.md exactly to the reviewed base, preserving the accurate reverse-walk rationale. No runtime/test changes. The base-aware design check and formatting pass; the unchanged base resources note is reported only as a warning. This avoids expanding the history repair into an unrelated shared-note rewrite. The original closing review covers runtime; a subsequent docs-only delta covers this correction. No promotion signal.The first LMDB attempt exited 139 in native
mdb_cursor_close→CursorWrap::close, matching the stack family tracked by #828. The exact-head rerun passed without code changes (completed 2026-11-12 00:46:17 UTC). The preceding PR head and reviewed main also passed that job. The native cause remains unknown; no repair or proven baseline classification is claimed. The unchanged expiry file also passes locally: 10 passing / 4 engine skips. This PR does not change that file or test commands; it adds history-reader coverage.CI is settled against three integration failures that also fail on reviewed main
be77be5: 1/6 Bun, 6/6 Bun and 6/6 uWS. Lint, formatting, Windows unit tests and the LMDB rerun pass on8ee0988. No further code or review iteration was needed.All seven remaining closing findings are filed: raised-floor scan #4293; clean miss and cap duplicate work #4286; shared-log-age test fragility #4287; benchmark header and unused limits export (reverse-boundary rationale restored by the documentation correction) #4294. Existing correctness deferrals are #4282, #4283 and #4284. These are tracked limitations, not repaired behavior. The owner's scope cutoff leaves runtime unchanged; the receipt still flags framing-recheck and closing-full obligations. This stays draft without a promotion signal.
Reproduce after building:
HISTORY_BENCH_REPETITIONS=1 npx mocha unitTests/resources/historyReader.bench.js. The fixture checks returned values and measures real native calls, wall time and process CPU. Unit shards exclude this manual benchmark.🤖 Generated by OpenAI Codex; posted via @kriszyp.
Release target: v5.4 — existing milestone retained.
Related PRs: #4063 supersedes (eligible live-listing goal), #3494 overlaps (history filtering), #3578 overlaps (merged reconstruction), #2155 overlaps (expiry flags), #3137 overlaps (audit fan-out), #4235 overlaps (replay handoff), #4270 overlaps (audit floor), #4250 overlaps (TTL flags), #3414 overlaps (reverse history), #3469 overlaps (replacement replay), #3503 overlaps (reload replay), #3356 overlaps (origin checkpoints), 23 others independent
Related-PR discovery was partial.
Complexity: complicated
Deferred findings
97353c0b742e5169major performance —resources/crdt.ts— Exact unlinked-control search-back scans retained history — filed Reduce synchronous audit-history work outside eligible live listings #42863e9b36fc44ba80eemajor performance —resources/crdt.ts— Full-frame read amplification outside live-record listings — filed Reduce synchronous audit-history work outside eligible live listings #4286382e07e5cff92254major performance —resources/Table.ts— Evicted many-patch histories rebuild quadratically — filed Reduce synchronous audit-history work outside eligible live listings #4286c09ed64a6c60d9e5major performance —resources/Table.ts— Eligible unlinked history requests scan retained logs — filed Reduce synchronous audit-history work outside eligible live listings #4286de3a49e291ef1de9major performance —resources/Table.ts— Never-evicted eligible records may scan retained logs — filed Reduce synchronous audit-history work outside eligible live listings #428654c06b9b6dd6207fmajor performance —resources/Table.ts— Missing-id requests scan retained logs per id — filed Reduce synchronous audit-history work outside eligible live listings #4286a0f6cfc90d9c1414major performance —resources/crdt.ts— Nonexact timestamp misses synchronously search earlier history — filed Reduce synchronous audit-history work outside eligible live listings #4286012f52dec67a6e61major data-integrity —resources/crdt.ts— Pruned predecessor above cutoff reports false absence — filed Historical reconstruction reports false absence at a pruned predecessor #4283bf66cc0c57d88414major correctness —resources/RocksTransactionLogStore.ts— Shared compound-id reference lookups compare object identity — filed Audit reference lookup misses equivalent compound record ids #4282f0999a0163b8335aminor correctness —resources/Table.ts— Non-expiring reinsertion on non-evicting tables omits older history — filed Plain reinsertion on a non-evicting table hides earlier retained history #4285🤔 defer this PR's major performance finding
97353c0b742e5169(Exact unlinked-control search-back scans retained history) to #4286 instead of fixing it here?🤔 defer this PR's major performance finding
3e9b36fc44ba80ee(Full-frame read amplification outside live-record listings) to #4286 instead of fixing it here?🤔 defer this PR's major performance finding
382e07e5cff92254(Evicted many-patch histories rebuild quadratically) to #4286 instead of fixing it here?🤔 defer this PR's major performance finding
c09ed64a6c60d9e5(Eligible unlinked history requests scan retained logs) to #4286 instead of fixing it here?🤔 defer this PR's major performance finding
de3a49e291ef1de9(Never-evicted eligible records may scan retained logs) to #4286 instead of fixing it here?🤔 defer this PR's major performance finding
54c06b9b6dd6207f(Missing-id requests scan retained logs per id) to #4286 instead of fixing it here?🤔 defer this PR's major performance finding
a0f6cfc90d9c1414(Nonexact timestamp misses synchronously search earlier history) to #4286 instead of fixing it here?Origin — the dispatch brief this PR was written from
Record history after eviction, beyond what #3214/#3414 fixed. (D1, #2730) getHistoryOfRecord (resources/Table.ts ~8791-8795) returns [] when primaryStore.getEntry(id) is missing. (D2) After a re-insert, RecordEncoder.ts (~1116) stamps previousVersion from the absent entry, so the audit chain is unlinked; the walk (~8800-8845) only advances through previousVersion < start, so earlier history is dropped; also the loop's
countis a const 0, making its count < 1000 bound inert. (D3) crdt.ts (~280, ~318) calls reconstructForward with { txnLogKey: timestamp } and reconstructForward does an exact-key auditStore.get, so any timestamp that is not an exact audit key returns null. Let the per-record walk start without a primary entry and bridge an unlinked boundary using the record's prior audit entries (evict-type entries can serve as a bridge point); in reconstructForward start from the latest audit entry for (table, record) at or before the timestamp; fix the inert count bound.Acceptance
Dual-engine tests: evicted record's getHistoryOfRecord returns its audited history; history spans an evict + re-insert; time-travel read at a non-exact timestamp reconstructs the right value. Shown failing on origin/main.
Dispatch: task
harper-record-history-after-eviction· queued by finding-triage/wave-39 · ran by claude/opus/xhigh · worker kzyp-xps-1Review-Coverage: authored=codex; ran=cursor-composer,gemini,cursor-grok,claude; late=cursor-grok:adjudicated; adjudicated=domain; blocked=claude(fallback)(out-of-budget); declined=muse; rounds=11; full=6 @ 8ee0988
Review-Attention: deep ~75m (critical: Table.ts, auditStore.ts +1; decisions: documentation-scope, do-less-alternative, perf-cost, deferred-97353c0b, deferred-3e9b36fc, deferred-382e07e5, deferred-c09ed64a, deferred-de3a49e2, deferred-54c06b9b, deferred-a0f6cfc9) @ 8ee0988