Shared derived-index runtime for native backends - #2567
Conversation
There was a problem hiding this comment.
Code Review
This pull request implements Stage 1 of the derived-index runtime, establishing a lock-elected, cursor-based delivery system of committed RocksDB log mutations to derived-index backends. It introduces the DerivedIndexRuntime and DerivedIndexRunner to coordinate aggregate log iteration, transaction assembly, authoritative record resolution, and a robust rebuild phase. Additionally, it adds host-storage primitives via RocksDerivedIndexStorage, local-only cache eviction markers in Table.ts, and an opt-in writer backpressure lag policy. Comprehensive unit tests, benchmarks, and integration tests are also included to verify correctness and performance. No review comments were provided for this pull request, so there is no additional feedback to address.
|
Reviewed; no blockers found. |
Adds to the shared derived-index runtime (#2489) what a backend with milliseconds-per-mutation apply cost and an msync barrier needs: - a coalesced last-write-wins `records` view beside `transactions` - identity-first bounded collection with partial chunks for oversized transactions and no cursor publication mid-transaction; per-registration turn, chunk, cadence and rebuild options - a runtime-scheduled durability cadence (age, thresholds, shutdown) through an optional backend `flush(reason)` - rebuild as a runtime phase on the conservative log boundary with capped backoff, a shared attempt budget and an observable `unavailable` end state - shutdown-before-unlock handoff, an owner-epoch fence for backends, and a new epoch per rebuild attempt - sequence-locked shared readiness readable on every worker Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P9BNWi77MsouBBexxHpT65
- a fault found mid-drain can no longer publish `ready` over `rebuilding` - resolution checks the chunk byte and time bounds after every key and carries the remainder, so the turn budget covers both phases - reload markers committed before a rebuild's capture are covered by its scan - an owner acquiring on shared `needs-rebuild`/`rebuilding` rebuilds - `stop()`/unregister reject after a failed backend shutdown; the held lock is revivable through `requestRebuild`; one `shutdown` per epoch - `requestRebuild` from a non-owner travels through a shared request word; a request during a rebuild no longer dangles - promise-returning `reset`/`flush` are awaited or fail closed Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P9BNWi77MsouBBexxHpT65
- a held lock is revived under the same epoch and re-quiesced before any successor epoch or reset - stop()/unregister return one cached promise, wait for every backend, and release table registrations only after the backend settled - a rebuild consumes the shared rebuild request at start and on success; a non-owner never publishes readiness - flush rejections are generation-fenced; the age timer re-arms while accepted work is not durable - discarded log iterators are closed; scan tombstones are skipped - rebuild proven against a real audited RocksDB table Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P9BNWi77MsouBBexxHpT65
- a failed unregister shutdown stays in the runtime-wide stop() wait - a shared rebuild request reaches an owner parked on backpressure or backoff - an inherited exhausted budget spends no further attempt; a backend that cannot rebuild parks on a condemned generation - reload suppression uses the wall clock transaction timestamps use - an unindexable reason carries the error class and status, not its message Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P9BNWi77MsouBBexxHpT65
- the readiness buffer carries a notify callback so a non-owning worker's requestRebuild wakes the owner directly - readiness views are cached per buffer; epoch minting sits inside the acquisition error boundary - a two-log rebuild with an empty log proves the boundary omits it safely Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P9BNWi77MsouBBexxHpT65
…y store Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P9BNWi77MsouBBexxHpT65
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P9BNWi77MsouBBexxHpT65
- a release waits for an in-flight reset before quiescing the epoch - a stopped runner holding the lock after a failed shutdown is revivable through the runtime's requestRebuild - a latched unavailable status clears once a peer revived the index - the reload-suppression bound is shared with the next owner - shared readiness reasons never carry backend error messages; live tombstones resolve to absent; unflushed counters reset with the cursor Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P9BNWi77MsouBBexxHpT65
…capabilities Adopted from the planning recheck: a backend that queues declares `queued: true` and registration rejects it unless it implements attach, flush and shutdown. Also guards the idle-release cursor read and the cleanup hooks, and keeps a failed shutdown's message out of the shared readiness reason. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P9BNWi77MsouBBexxHpT65
…al store - latestSeen is recorded at collection and cleared with the cursor; stalledMilliseconds reports time parked on backpressure or the ceiling - the per-key wall-time check samples every 16 records - the real-RocksDB test drives a second runtime's rebuild request through the native shared buffer - documents the wall-clock-step residual of reload suppression Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P9BNWi77MsouBBexxHpT65
…by iteration Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P9BNWi77MsouBBexxHpT65
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P9BNWi77MsouBBexxHpT65
…le advance Item 7 as ruled (option b): a registration's maxLagMilliseconds makes the owner publish a shared lag word; every worker's runner registers an admission check for the index's tables and Table.update()/delete() throw a retryable 503 DerivedIndexLagError while it is set. Replication apply and cache fills are never gated. Also from the adjudicated doc round: `ready` and the retry budget settle on the first durable advance (a queued backend under sustained ingest never idles); a chunk the projection rejects entirely fails closed; the latched unavailable check reads one word. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P9BNWi77MsouBBexxHpT65
…ch-up Adopted from the lag policy's planning gate: the admission check moves to _writeUpdate/_writeDelete, where create(), loadAsInstance:false writes and held-lock saves converge, bypassing replication apply and replay. Lag is the longest of cursor distance, time parked, and time since catch-up was last proven, sampled on a lag timer while parked; a trip survives handoff until the successor proves catch-up. The rebuild scan skips Harper-internal symbol-keyed store entries. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P9BNWi77MsouBBexxHpT65
…ld circuit - the all-unindexable circuit covers every rebuild scan chunk and the whole scan, with a floor no larger than the chunk bound - an index that becomes unavailable admits writes again - the unproven-catch-up clock restarts on discard, so a rebuild does not fabricate lag from ownership age - a throw from tryLock is retried instead of parking the runner - shared-memory views re-fetch until the binding hands back shared memory - admission checks are stored in arrays and walked without allocation; rebuiltRecords counts only indexed records Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P9BNWi77MsouBBexxHpT65
Adopted from the second planning recheck: the capability flag is `asynchronous: true` — any effect that survives a method return — rather than a queued apply; a synchronous backend returning a promise from flush fails closed; release awaits the shutdown flush and any in-flight reset before quiescing and unlocking; reset carries a stated crash-safety obligation. Shared-memory views are cached and refreshed only off the write path until the memory is shared; a registration that throws part-way cancels its readiness subscription. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P9BNWi77MsouBBexxHpT65
- the fence, epoch mint and owner publishes re-fetch their views on every use until the binding hands back shared memory; the hot admission read at most every 100 ms - _writeInvalidate/_writeRelocate are gated like the other staging methods - the lag policy is suspended during a rebuild; a rebuild re-offers a deferred chunk after one flush age so a dropped wake cannot park it - a release before any epoch was minted skips quiescence Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P9BNWi77MsouBBexxHpT65
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P9BNWi77MsouBBexxHpT65
- an exhausted-budget validate no longer opens an iterator after release - fence, admission and readiness reads re-fetch at most every 100 ms while the buffer is private; the mint and owner publishes still re-fetch each use - collection samples the clock every 16 entries - a present entry that decodes to null reaches the projection again - invalidate/relocate reuse the transaction they already resolve - an undeclared asynchronous shutdown flush is not awaited; tables stay registered while a failed shutdown holds the lock Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P9BNWi77MsouBBexxHpT65
… once Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P9BNWi77MsouBBexxHpT65
Fetch shared-memory views once: rocksdb-js wraps one process-wide native allocation per key in a new external ArrayBuffer on every call and never returns a SharedArrayBuffer, so the re-fetch-until-shared machinery was re-fetching forever. Charge the rebuild scan's turn budget for filtered entries and yield on an empty chunk. Remove the all-unindexable circuit, which turned a bulk write of attribute-less records into an unavailable index; skip, count and warn once per streak instead. Hold the epoch's quiescence under an undeclared asynchronous flush. Arm one lag timer. Add the admission-cost bench case. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P9BNWi77MsouBBexxHpT65
A caught-up owner idling through its grace period kept adding its idle time to the lag measure, tripped the lag word after one budget, and then released with the word set, so a node whose only writes hit those tables stayed at 503 with nothing left to prove catch-up. The clock now starts when work is offered and stops when durable equals offered. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P9BNWi77MsouBBexxHpT65
Arm one guarded retry timer while tryLock keeps throwing, document that a condemnation lives in process memory beside the reset crash-safety caveat, prove the shared readiness record from a real worker thread through the binding alone, and prune narrating comments. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P9BNWi77MsouBBexxHpT65
A condemnation now also lands in the root store under the index's marker key, so a restart before the rebuild's reset has invalidated the cursor rebuilds instead of trusting it; the marker clears at the first durable ready. The admission bypass keys on transaction.sourceApply and isReplay as well as isNotification, so a canonical-source apply is never shed. Acquisition stays suppressed while the lock-retry timer is armed. Expose quiescence age, document partial-chunk visibility and the awaited stop(), and add a real-table put benchmark for the guard. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P9BNWi77MsouBBexxHpT65
A marker that cannot be written makes the index unavailable with no reset issued, one that cannot be read counts as present, and every acquisition reads it so the rebuild that follows clears it. Quiescence age counts from the start of release, the bench proves its stub reaches the staging layer, and the marker keyspace is exercised on the real store. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P9BNWi77MsouBBexxHpT65
Proving catch-up only at an idle pass tripped the lag word for a backend keeping up under sustained ingest, whose log is never exhausted. The third term is now the age of the oldest accepted work not yet durable, which every durable advance shrinks. An unreadable condemnation marker no longer pre-sets the local flag, and a refused write is retried on the next acquisition before anything else is trusted. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P9BNWi77MsouBBexxHpT65
Lag now also measures time since the oldest commit the runner may not have read yet, cleared whenever a drain reaches the end of the log, so a reader too slow to keep up cannot hide behind a caught-up backend. A condemnation the root store refuses stays a shared needs-rebuild with the lock released, and whichever runner acquires next retries the write before any reset without spending a rebuild attempt. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P9BNWi77MsouBBexxHpT65
…rained The hysteresis clear now needs both a durable advance and the end of the log reached since acquisition, so a successor's first barrier cannot clear a trip it inherited with the backlog. A backend that cannot rebuild no longer enters the rebuild path on a refused marker, and the unread clock is not read when the policy is off. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P9BNWi77MsouBBexxHpT65
…sted Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P9BNWi77MsouBBexxHpT65
A reader that never quite empties a steadily fed log was charged the age of its first unread commit; the term is now the smaller of that and the distance between the clock and the newest entry read. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P9BNWi77MsouBBexxHpT65
The lag latch is process-shared memory that only an owning runner clears. Three paths gave up ownership while it was still set and scheduled nothing that could clear it, so every worker kept rejecting writes to the index's tables with a retryable 503 indefinitely: - `#needsRebuild()` when the backend has no `reset` or the runtime has no `scanRecords`. Nothing will ever rebuild, so nothing will ever catch up. - `#acquired()` inheriting that same shared `needs-rebuild` state. - `#deferForCondemnation()`, whose retry needs a wake, and wakes come from commits -- the very writes being shed. This is the rule `#becomeUnavailable()` and the failed-shutdown hold already followed; these three were the paths that missed it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
One backend contract. No shipped or planned backend is synchronous (the HNSW plane and Tantivy both queue and barrier), so the `asynchronous` discriminant, the registration branch that validated hooks only for declared-asynchronous backends, the check that a "synchronous" flush did not return a promise, and the quiescence path for that undeclared promise are gone. `attach`, `flush` and `shutdown` are required of every backend; a fake that completes inside `deliver()` implements them trivially. Shared readiness is plain words, not a sequence lock. Nothing in production read the free-form reason string the seqlock existed to publish; the shared record now carries a `DerivedIndexReadinessReason` code in one Int32 word, the owner-epoch counter lives in the same buffer instead of a second `getUserSharedBuffer` key, and a read is four `Atomics.load`s with no spin. The full message stays in the owner's local status and log. This removes the one construct in the runtime that was novel with respect to how Harper already uses `Atomics` over rocksdb-js shared buffers (primary-key allocation, blob holds, HNSW node ids). The rebuild anchors at the committed tail, not the oldest retained entry. rocksdb-js advances `lastCommittedPosition` only to the earliest still-uncommitted write (`TransactionLogStore::commitFinished`, `uncommittedTransactionPositions.front()`), so a committed read is a contiguous physical prefix and nothing committed after the capture can sit behind the tail. The whole-retained-log replay after every rebuild, the capture-time reload-marker suppression, its `Date.now()` comparison against transaction timestamps, the shared reload word and the backward-clock residual all go with it: a reload marker is met exactly once. The lag latch clears on entering a rebuild (no durable cursor to guard) and on every park the runtime cannot leave on its own, not only on `unavailable`. `partial` is dropped from `DerivedIndexTransaction`: nothing consumed it and a backend cannot act on it; the withheld cursor already says what is certified. The per-log cursor checks in `#reconcileDurableCursor` that could not fail after the whole-vector match are gone; the repeat-detection set stays, since transaction timestamps are unique per log but not physically monotone. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…nning rocksdb-js reports `oldestSequenceNumber: 0` with `fileCount: 0` for a log that has no files yet, so the `=== 1` retention test rejected every brand-new database: the first runner found no durable cursor, started a rebuild, and `#captureBoundary` failed the attempt with "retains no committed transaction and has lost its beginning" — on every attempt, until the budget parked the index unavailable. Found by restacking the native HNSW index on the runtime and defining a fresh table. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`docs/derived-index-runtime-stage-1.md` described a PR and its stages. The durable design — invariants, ownership, cursor semantics, offered versus durable progress, the backend contract, bounded delivery, cadence, the committed-tail rebuild, handoff fencing, shared readiness and the lag policy — is now a section of the root DESIGN.md, where agents read design. Decision records, alternatives and measurements move to the pull request. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
4cad607 to
b2f0e49
Compare
Prettier expects a blank line between top-level sections; the manual merge of the chooseOperation and derived-index-runtime sections dropped it in both spots. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The storage surface from #2535 existed to back Tantivy's KvDirectory with a Harper-owned column family. The HNSW plane keeps its graph in its own mmap file, and #2568's benchmark shows the hosted path failing its own gates (26 GB through ~108k JS read callbacks for 500 searches; a root-wide flush per publication) while Tantivy's native MmapDirectory passes them, so Tantivy will persist the same way. The runtime contract never depended on it: the cursor is backend-owned and read from whatever the backend made durable. Removes the adapter, its tests, and the derived-index column-family open/close/drop hooks in databases.ts. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The shared post-commit derived-index runtime for RocksDB tables: lock-elected delivery of committed transaction-log mutations to a native index backend, exact cursor resume, rebuild from primary records, cross-worker readiness, and opt-in writer backpressure. This PR carries the whole stack — #2533 (runtime foundation), #2535 and the native-backend additions — so one review covers the derived index end to end and one merge lands it. #2430 (native HNSW) is rebuilt on this branch and is the first consumer; nothing here has a production caller until it lands.
The durable design is the § Derived-index runtime section of the root
DESIGN.md(invariants, ownership, cursor semantics, backend contract, bounded delivery, cadence, rebuild, fencing, shared readiness, lag policy). This description holds what is transient: scope, decisions, measurements, verification.What is here
Runtime (
resources/derivedIndexRuntime.ts). OneDerivedIndexRuntimeper database wakes on the root store'scommittedevent; each registered backend has its own process-wide lock, runner, cursor vector (log name → completed transaction timestamp) and readiness. The elected runner reads committed entries from every physical log, collects identities per transaction, resolves each changed record once against the primary store after its last occurrence, projects only the registered attributes, and delivers bounded batches. Resume proves every saved cursor withexactStart; a boundary the log cannot resolve, a corrupt frame, a removed log or areloadmarker condemns the generation and rebuilds. Nothing on the commit path is added beyond the log entry that already exists and a local-only eviction marker for registered caching tables.Backend contract. One interface:
attach(host)(the owner-epoch fence),deliver(batch)(accept/defer/fail),flush(reason)(barrier request),shutdown(epoch)(quiescence),getDurableCursor(),onStateChange(), and optionalreset(epoch).batch.recordsis the last-write-wins view over distinct keys;batch.throughis the cursor vector the batch completes and is withheld until an oversized transaction's closing chunk. Every hook is required — a backend that completes insidedeliver()implements them trivially — because work that survives a method return is what an ownership handoff has to fence.Rebuild.
rebuildingis published, the previous epoch is quiesced,reset(newEpoch)runs, the committed tail of every log is captured, primary records are scanned and delivered in bounded chunks, the final chunk carries the tail asthrough, and replay resumes after it. Bounded retry with backoff;unavailableafter the budget, honoured by peers.Shared readiness. State, reason code, attempt count, rebuild request, lag flag and the owner-epoch counter in one
getUserSharedBufferallocation per index, read with plainAtomics.loads on any worker (readDerivedIndexReadiness). Same dependency as primary-key allocation, blob holds and HNSW node ids already carry.Lag policy (opt-in,
maxLagMilliseconds). The owner measures cursor distance, time parked, unread-log age and undurable-work age; past the budget every worker rejects local user writes to the index's tables with a retryable 503 (DERIVED_INDEX_LAGGING) at the staging layer (_writeUpdate/_writeDelete/_writeInvalidate/_writeRelocate), never canonical-source applies, replay or replication notifications. Cleared with hysteresis once the owner proves catch-up, and on every park the runtime cannot leave on its own.Simplifications made in review
The design was cut down before asking for review, each item verified against source rather than inferred:
flushreturned no promise, and a quiescence path for that undeclared promise. No shipped or planned backend is synchronous.DerivedIndexReadinessReasoncode, the epoch counter moved into the same buffer, and the 512-byte record became 32 bytes with no spin. This removed the only construct in the runtime that was novel with respect to Harper's existing use ofAtomicsover rocksdb-js shared buffers.lastCommittedPositiononly to the earliest still-uncommitted write (TransactionLogStore::commitFinished,uncommittedTransactionPositions.front()), so a committed read is a contiguous physical prefix and nothing committed after the capture can sit behind the tail. The whole-retention-window replay after every rebuild, the capture-time reload-marker suppression, itsDate.now()comparison against transaction timestamps, the shared reload word and the backward-clock residual are gone; a reload marker is met exactly once.unavailable: a backend withoutreset, a runtime withoutscanRecords, a condemnation marker that could not be written, and entering a rebuild (no cursor to guard; readers act onrebuilding). Before this, three paths released ownership with the latch set and nothing scheduled to clear it, so writes stayed 503 indefinitely; one of them needed a commit wake to retry, and commits were what was being shed.partialdropped fromDerivedIndexTransaction(nothing consumed it; the withheld cursor already says what is certified), and the per-log cursor checks that could not fail after the whole-vector match removed.RocksDerivedIndexStorage(from Add Harper RocksDB storage for derived indexes #2535) removed. It existed to back Tantivy'sKvDirectorywith a Harper-owned column family. The HNSW plane persists in its own mmap file, and Prepare the Harper full-text benchmark for native storage #2568's benchmark shows the hosted path failing its own gates (~26 GB through ~108k JS read callbacks for 500 searches; a root-wide flush per publication) while Tantivy's nativeMmapDirectorypasses, so Tantivy will persist the same way. The runtime never depended on it: the cursor is backend-owned. The derived-index column-family open/close/drop hooks indatabases.tsgo with it.oldestSequenceNumber: 0withfileCount: 0for it; the=== 1test read every brand-new database as a retention gap and failed every rebuild attempt. Found by restacking Native HNSW index: file-primary mmap graph as a backend on the shared derived-index runtime #2430 and defining a fresh table — the first real consumer exercising the runtime.Decisions and alternatives
aftercommitstream or a permanent worker-0 drainer. Same-threadaftercommitdispatch has no durable cursor for logs shared by all workers and retains every audit object in a transaction; a fixed drainer pins all indexes to one worker. A lock-elected runner per backend gives each index one serial stream, its own cursor, and parallelism across workers.transactionBroadcast's subscription registry was considered as the base and rejected: its cursors are live-subscription state, its lifecycle is customer-facing, and its aggregate results omit the log identity a cursor vector needs.Measurements
Synthetic native-cost bench (
derivedIndexRuntime.bench.js, 350 µs/apply, 5 ms barrier): 1,000 occurrences over 50 keys in one window — 1,000 applies / 359 ms throughtransactions, 50 applies / 24 ms throughrecords; queued application held event-loop delay to 6.8 ms max versus 963 ms inline at the same ~2,800 mutations/s. At 1,500 arrivals/s, a 100 ms / 512-mutation cadence delivered 1,492/s with p99 write→durable 116 ms and 30 barriers in 3 s; the defaults (1 s / 4096) give p50 1.27 s and 4 barriers. On the native HNSW backend (#2430's ingest bench, 384-d): 265 native applies for 1,000 commits over 50 keys, foreground put 0.09 ms, apply 0.365 ms, barrier ~9 ms.Verification
derivedIndexRuntime,derivedIndexRuntimeNativeBackend,derivedIndexRuntimeRocks,derivedIndexRegistry): 81 passing. The native-backend suite covers coalescing, oversized transactions, deferral, cadence, rebuild/retry/exhaustion, handoff with an apply scheduled and with a flush pending, shutdown failure holding the lock, condemnation across a simulated restart, lag trip/clear/hysteresis, the three terminal-park cases (each proven red without the fix), tail-anchored rebuilds across several logs, and a real worker thread reading the owner's publication through the binding.vectorIndexPlane.test.js22 passing against@harperfast/hnsw0.2.1 — two workers into one native file, a committed write replayed after its worker dies, reload markers, a cursor outside retention, an empty index answering no results, a populated index that lost its file answering 503.npm run build,tsc --noEmit,lint:required, prettier: clean.Refs #2489. Supersedes #2533 and #2535 (whose storage adapter is dropped here, see above).
🤖 Generated with Claude Code