Refuse a better-sqlite3 build that aborts Node 24 when it frees a statement (#719) - #720
Merged
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Iris gate — 1 of 2 tripped
|
| Trace | Basis | Rules | Evidence |
|---|---|---|---|
b194dd4244a741022ecb0f7c95176831 |
detector_veto |
no_pii | no_pii: AWS Access Key (output 45–65) |
| Verdict basis | Traces |
|---|---|
detector_veto |
2 |
clean |
1 |
Unjudged questions: task_completed (3), tool_use_correct (3) — a trace that did not carry what a rule needs.
tests/fixtures/ci-gate/traces.ndjson · 3 evaluated · dataset release-gate: 2 in the gate · exit 1 · what the bases mean
Iris gate — 1 stored, nothing tripped
|
| Verdict basis | Traces |
|---|---|
clean |
1 |
Unjudged questions: task_completed (1), tool_use_correct (1) — a trace that did not carry what a rule needs.
tests/fixtures/ci-gate/clean.ndjson · 1 evaluated · exit 0 · what the bases mean
irparent
force-pushed
the
fix/native-statement-teardown
branch
from
September 28, 2026 19:02
d8043c8 to
57c2866
Compare
irparent
force-pushed
the
fix/native-statement-teardown
branch
from
September 28, 2026 19:03
57c2866 to
d0f6304
Compare
irparent
force-pushed
the
fix/native-statement-teardown
branch
from
September 28, 2026 19:53
d0f6304 to
a0673e5
Compare
…tement (#719) Node 24.19.0 changed the header-only node::ObjectWrap: its destructor now calls RemoveEnvironmentCleanupHook, and no 24.x runtime so far has the global hook list that makes that safe during a garbage collection (nodejs/node#65446). A better-sqlite3 compiled against that header, as npm does when the prebuilt download fails, aborts the process the first time V8 collects one of its statements, open or closed. The prebuilt 12.11.1 binaries predate the header and are safe. The driver seam now reads the binary better-sqlite3 would load before loading it. When it carries the call and the runtime lacks the fix, Iris holds the store with node:sqlite and says why; IRIS_SQLITE_DRIVER=native refuses and names npm rebuild better-sqlite3. The end of stdin now shuts a stdio server down in order, stopping an in-flight search-index build instead of running it for a client that left. A new CI job compiles the addon from source on Node 24 (Linux, macOS, Windows) and Node 22 (Linux, macOS), and requires the abort to happen exactly when Iris predicts it, and six stdio sessions of the real server to end cleanly. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
irparent
force-pushed
the
fix/native-statement-teardown
branch
from
September 28, 2026 22:03
a0673e5 to
8c2237a
Compare
irparent
added a commit
that referenced
this pull request
Sep 28, 2026
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
irparent
added a commit
that referenced
this pull request
Sep 28, 2026
…where the job's binary would abort Node (#720) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
irparent
added a commit
that referenced
this pull request
Sep 28, 2026
…695 erased in steps; checkpoints off the event loop (#727) * perf: the MCP connection opens before retention runs, and nothing in the background holds the event loop (#695) - The boot retention sweep starts after the transport connects and deletes in steps of about 50 ms, each its own transaction. A large sweep deletes with FTS5 secure-delete off and then merges the index to one segment in steps; the merge owed is recorded in the same transaction and finished by the next start if the server closes first. - A retired search index is erased a bounded number of rows per step (defensive mode off for those DELETEs only), with a one-statement fallback for a connection that refuses the writes. - WAL checkpoints run on a worker thread with its own connection; the server's connection checkpoints by itself until it is up and if it fails. - The index build logs when it starts and finishes; health reports search state and progress as a share; --self-test reports the configured database's index. - A stall guard (tests/stall) runs alone in CI on both drivers. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * perf: sweep steps turn automerge off; a refused worker checkpoint is not retried on the event loop; changelog and truthbase - A merge-mode sweep step turns FTS5 automerge off with secure-delete and restores both, so no segment merge lands inside a delete step; the owed merge does that work in its own steps. - checkpoint() with the worker running no longer falls back to a TRUNCATE on the server's connection, which waited for readers inside the event loop; it is best effort, as before. - CHANGELOG [Unreleased]: the sweep after connect, #695, checkpoints on a worker, health/self-test search status, with measured numbers. - .claims.json recaptured. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * claims: test counts after merging main Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * claims: test counts after merging main Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * checkpoint worker: a TRUNCATE never waits for a reader, so delete_trace does not wait for a search (#716); the adapter retries while the reader reads Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * tests: FTS5 integrity-check on both indexes after the stepped erase; defensive mode restored when the work throws, on both drivers Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * claims: test counts Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * claims: test counts after merging main Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * stall guard: one retry per case, for the runner's own rare stalls; a stall the code causes repeats Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * delete_trace's erasure stays on the server's connection, as #716 has it; sweeps and purge use the checkpoint worker A delete's log is a few pages, and its answer must not wait behind the worker's periodic checkpoint: on a loaded runner a delete during a search took 122 ms through the worker. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * stall guard: read the main thread's CPU time per gap, not the wall clock, so a runner's descheduling is not read as a statement holding the loop On the same commit the node job saw 296 and 414 ms wall-clock gaps in steps that held the thread for under 100 ms. The code before the steps still fails: 2,125 ms and 280 ms of this thread's CPU at 10,000 traces. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * claims: test counts after merging main Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * checkpoint worker: #716's lifecycle; started at the first write, replaced after a crash, never tried again after a failed start, and left to end on its own at close The thread used to start at open and be terminated right after its connection closed. A macOS run died once with SIGBUS in the search worker suite, where each store now had two native threads; the search worker's pattern never terminates a thread that is ending by itself. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * claims: test counts Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * claims: test counts after merging main Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * tests: the defensive-mode test on better-sqlite3 asserts the refusal where the job's binary would abort Node (#720) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This branch was successfully deployed
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.
Closes #719.
The crash
test (24, node)in run 36453179190 aborted inside the spawned server:Assertion failed: (env) != nullptrinnode::RemoveEnvironmentCleanupHook, called from better-sqlite3'sStatement::~Statementduring a garbage collection, on Node 24.21.0.node::ObjectWrap: the destructor now removes an environment cleanup hook. It did not backport the runtime's global hook list, which lets that removal work during a garbage collection. See nodejs/node#65446. The fix (nodejs/node#65943) is onv24.x-stagingand in no release up to 24.21.0. The same abort is reported at WiseLibs/better-sqlite3#1515.npm citook 1m24s against 4s in the sibling jobs: the prebuild download failed and node-gyp compiled the addon against 24.21.0's headers. A user whose download fails gets the same binary.Characterised in a node:24.21.0 container, better-sqlite3 12.11.1
db.close()Finalizing statements before
close()does not help, and a statement cache that eventually drops its statements does not either. Any Database, Statement or iterator object that V8 collects triggers it, including the in-memory probe the seam opens. The one fix inside Iris is to not load that binary on a runtime that aborts on it.Upgrading to 13.x (N-API, not affected) is not safe yet: #308 records spawned-process crashes with 13.0.3.
Changes
src/storage/driver.ts: before loading better-sqlite3,nativeAbortsOnCollectfinds thebetter_sqlite3.nodethatrequire('better-sqlite3')would load, the same way itsbindingsloader does, and looks for theRemoveEnvironmentCleanupHookimport. No better-sqlite3 source calls it, so its presence means the file was compiled against the new header.runtimeKeepsAddonHookscovers 26.4.0+ and no 24.x yet. When the binary carries the call and the runtime lacks the list, the default falls back tonode:sqlitewith one warning and areason.IRIS_SQLITE_DRIVER=nativerefuses and namesnpm rebuild better-sqlite3. The scan reads about 2 MB once per process (4 ms measured).src/index.ts: in stdio mode with no HTTP server, the end of stdin runs the same shutdown as SIGINT/SIGTERM. Shutdown is idempotent. Measured on a store with 80,000 unindexed traces: the process used to exit 1.5 s after stdin closed, still building the search index. Now it exits in 32 ms with "Shutdown complete". With the dashboard up, stdin ending does not stop the process, as before.native addon built from source (os, Node): Node 24 on ubuntu, macOS and Windows, and Node 22 (the control: its headers never changed) on ubuntu and macOS. Windows × Node 22 is excluded because the node-gyp in Node 22's npm 10 cannot identify the runner's Visual Studio 18 (unknown version "undefined" found at "C:\Program Files\Microsoft Visual Studio\18\Enterprise"), so nothing compiles there and npm drops the optional module. The job installs withnpm_config_build_from_source=true, checks that the addon was compiled on the runner (and on Node 24, that the call is in it), then runs:tests/integration/native-addon-collect.test.ts: runs the real binary in a child under collection and requiresaborted === predicted. On the ordinary test jobs the prebuilt binary survives and is predicted to. When a Node 24 release ships the fix, this test fails with the message to add the version.tests/integration/native-teardown-stdio.test.ts: 6 real server processes over stdio, 25 rounds each of log_trace, evaluate_output and a search, then stdin closed. Each must exit 0 with no signal and no native assertion, log "Shutdown complete", and use the driver Iris predicts. The server runs with--max-semi-space-size=1so V8 collects inside each short session. With the check turned off, on the compiled binary in the container, this test failed 5 runs of 5 (the server died mid-session). Without the small young generation, the server did not abort in any of 5 runs. That flag is what makes the test catch the bug every time instead of occasionally.tests/unit/storage/driver.test.ts: the version table, marked and unmarked binaries, missing files, the fallback and the refusal, and a check that the inspected file is the onerequireloads.--self-test, which must name the driver and the reason.docs/architecture.md,docs/api-reference.md: the fallback now also covers this build.Checks
claims:check,claims:check-hardcoded,llms:check,mcp-json:check,changelog:check,version:checkandproof -- --checkall pass..claims.jsontotalCombined 4,182.aborted (exit SIGABRT); Iris predicted abort.🤖 Generated with Claude Code