Refuse to load node:sqlite on known-buggy Node 22.x patch versions (#265)
Some checks are pending
CI / semgrep (push) Waiting to run

Reported in #264 as a V8 CHECK abort with `Check failed: (location_) != nullptr`
inside `node::sqlite::StatementSync::ColumnToValue`. The crash happens when
SQLite returns a TEXT column whose bytes V8's String::NewFromUtf8 rejects
(invalid UTF-8 — common for Cursor's stored chat text where multi-byte chars
are truncated at streaming boundaries). Node 22.x prior to 22.20 does not
check the resulting MaybeLocal<String> for empty before dereferencing,
aborting the whole process with a trace trap.

A try/catch in JS can't recover — the abort runs in the C++ extension before
the V8 exception handler. So we refuse to load node:sqlite at all when we
detect a buggy Node version, surface a clear "upgrade Node" diagnostic, and
let the rest of the CLI run with the file-based providers (Claude, Codex,
Copilot, Gemini, etc.) instead of taking the whole tool down.

- engines.node bumped to >=22.20 so npm warns at install time
- src/sqlite.ts: checkBuggyNodeVersion() detects Node 22.x < 22.20 and routes
  through the existing isSqliteAvailable() / loadError diagnostic path
This commit is contained in:
Resham Joshi 2026-05-07 09:48:57 -07:00 committed by GitHub
parent 492bb5a5ec
commit 04aeda71b6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 35 additions and 1 deletions

View file

@ -30,7 +30,7 @@
"developer-tools"
],
"engines": {
"node": ">=22"
"node": ">=22.20"
},
"author": "AgentSeal <hello@agentseal.org>",
"license": "MIT",

View file

@ -23,6 +23,31 @@ let DatabaseSync: DatabaseSyncCtor | null = null
let loadAttempted = false
let loadError: string | null = null
/// Minimum Node 22.x patch version that contains the node:sqlite UTF-8 fix.
/// Older 22.x lines crash with `Check failed: (location_) != nullptr` when a
/// SQLite TEXT column returns bytes that V8's String::NewFromUtf8 rejects —
/// commonly the case for Cursor's text blobs (truncated multi-byte chars at
/// streaming boundaries) and OpenCode message text (rich tooling output).
/// Track of issue: https://github.com/getagentseal/codeburn/issues/264
/// Track of upstream: https://github.com/nodejs/node — fix landed in 22.x via
/// later patches; stable on Node 24+.
const MIN_NODE_22_PATCH = 20
function checkBuggyNodeVersion(): string | null {
const match = /^v(\d+)\.(\d+)\.(\d+)/.exec(process.version)
if (!match) return null
const major = parseInt(match[1]!, 10)
const minor = parseInt(match[2]!, 10)
if (major === 22 && minor < MIN_NODE_22_PATCH) {
return (
`codeburn: Node ${process.version} ships an older node:sqlite that crashes on ` +
`non-UTF-8 bytes in Cursor/OpenCode session text. Upgrade to Node 22.${MIN_NODE_22_PATCH}+ ` +
`or 24+ to avoid the V8 fatal error. (https://nodejs.org)`
)
}
return null
}
/// Lazily imports `node:sqlite`. On Node 22/23 it emits an ExperimentalWarning the first
/// time the module is loaded; we silence that specific warning once so dashboards aren't
/// preceded by a scary stderr line every run. Any other warnings (including future
@ -31,6 +56,15 @@ function loadDriver(): boolean {
if (loadAttempted) return DatabaseSync !== null
loadAttempted = true
// Refuse to load on a Node version known to crash mid-query. Treating the
// SQLite providers as unavailable is much friendlier than letting the user
// hit a V8 CHECK abort that takes down the whole CLI.
const versionWarning = checkBuggyNodeVersion()
if (versionWarning !== null) {
loadError = versionWarning
return false
}
const origEmit = process.emit.bind(process)
let restored = false
const restore = () => {