open-memex 0.7.0 → 0.7.1-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -16,6 +16,8 @@ via `prepublishOnly` — Node refuses `--experimental-strip-types` for files und
16
16
 
17
17
  `src/store/db.ts` picks the backend at runtime by sniffing `globalThis.Bun`. Both backends share the same surface (`new Database(path)`, `.exec`, `.prepare().run/all/get`, `.close`). Any DB code you write must stay on that common subset — do not import `better-sqlite3` or `bun:sqlite` directly outside `db.ts`. The Node driver is pinned to the `better-sqlite3` 13 N-API line (D72): the pre-13 `node::ObjectWrap` build aborts intermittently on Node 24.19+ when a prepared statement is finalized by GC.
18
18
 
19
+ D74 — the Node floor is **22.14**, not 22.6. `better-sqlite3` 13 is compiled against Node-API 10, which Node gained in 22.14.0; below that floor `require()` succeeds and `new Database()` segfaults the process with no diagnostic (measured: Node 22.12.0 dies with `0xC0000005` on win32-x64, Node 22.23.3 / 24.20.0 are fine; upstream WiseLibs/better-sqlite3#1514). The gate lives in `src/runtime-floor.ts` (pure, unit-tested) and is enforced in `loadDatabase` (`src/store/db.ts`) so every entry point fails with an actionable message instead of a silent crash; `doctor` reports the floor plus a live driver probe run in a child process (a segfault cannot be caught in-process, so the probe must be spawned, and it must resolve the driver from this package's directory, not the caller's cwd). `engines.node` is `>=22.14.0`. Never lower it back to the `--experimental-strip-types` floor of 22.6 — that number is a different constraint and predates the driver bump.
20
+
19
21
  Tool logic is host-agnostic and lives in `src/tools/ops.ts` (plain functions + shared zod arg shapes + `TOOL_DESCRIPTIONS`). `src/tools/memory.ts` (opencode) and `src/mcp.ts` are thin adapters — when adding or changing a tool, change `ops.ts` once and both hosts pick it up.
20
22
 
21
23
  **Plugin code must not runtime-import a host SDK package** (D58, extended by D60): `@opencode-ai/plugin` (v1) and `@opencode/plugin` (v2) are devDependencies, and neither host resolves them for plugins loaded from a global npm install — the import fails and the host silently skips the plugin (no error, no tools). Type-only imports are fine. `src/tools/memory.ts` carries a local stand-in for the v1 SDK's `tool()` (a runtime identity), and `src/opencode-v2.ts` duck-types the v2 context, for exactly this reason. The plugin entry is a dual export (`{ id, setup, server }`, D60): v1 ≥1.18.29 calls `server`, opencode 2 calls `setup`; both adapt `src/plugin-core.ts`.
package/CHANGELOG.md CHANGED
@@ -9,6 +9,15 @@ the `@alpha` tag are development snapshots and are not listed individually.
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## The Node floor is 22.14, because the driver says so (D74)
13
+
14
+ - **Fixed: every command could die with no output on Node older than 22.14.** `better-sqlite3` 13 (the D72 N-API line) is compiled against Node-API 10, which Node only gained in **22.14.0**. Below that, `require()` succeeds and the first `new Database()` segfaults the process - no message, no stack, exit 139 on macOS/Linux and `0xC0000005` on Windows. Reproduced on win32-x64 with Node 22.12.0; Node 22.23.3 and 24.20.0 are fine, and Node 20 can never work with v13 (upstream [WiseLibs/better-sqlite3#1514](https://github.com/WiseLibs/better-sqlite3/issues/1514), open). open-memex now refuses to load the driver below the floor and says exactly what to do (upgrade Node, or pin `better-sqlite3@^12.11.1`) instead of dying.
15
+ - **`engines.node` corrected to `>=22.14.0`.** It claimed `>=22.6`, which was the `--experimental-strip-types` floor - a different constraint that predates the driver bump, and wrong for the driver.
16
+ - **`open-memex doctor` gained a `sqlite driver` check** that loads the driver and opens an in-memory database in a **child process** (a segfault cannot be caught in-process) and reports the exit code or signal. It resolves the driver from this package's own directory, so it works when `doctor` runs with cwd set to a user project.
17
+ - Both READMEs gained a troubleshooting entry for the crash signature (`exit 139` / `0xC0000005` / "Segmentation fault"). No protocol, schema, or data change.
18
+
19
+ ## [0.7.0] - 2026-10-04
20
+
12
21
  ## One thing you said is one memory (D73)
13
22
 
14
23
  Saying "remember ." used to save the same statement up to three times: the keyword hook stored your sentence verbatim with no agent in the loop, then the agent - which could see the chat but not the store - saved its own paraphrase beside it, and `memory_add`'s near-duplicate check scored the two too far apart (0.21-0.38 against a 0.80 bar) to notice. `open-memex audit` reported zero near-duplicates while three copies sat in the folder, and search returned all of them.
package/README.md CHANGED
@@ -53,7 +53,7 @@ depth once you're up and running.
53
53
 
54
54
  ## Quick start
55
55
 
56
- You need **Node.js ≥ 22.6** (check with `node -v`). Then:
56
+ You need **Node.js ≥ 22.14** (check with `node -v`). Then:
57
57
 
58
58
  ```sh
59
59
  npm install -g open-memex
@@ -163,7 +163,9 @@ no one remembers to write down.
163
163
 
164
164
  ### Requirements
165
165
 
166
- - **Node.js ≥ 22.6** (`open-memex doctor` verifies this for you)
166
+ - **Node.js ≥ 22.14** (`open-memex doctor` verifies this for you). The floor is
167
+ the SQLite driver's: `better-sqlite3` 13 is built against Node-API 10, which
168
+ Node gained in 22.14.0 — older Node segfaults on the first database open.
167
169
 
168
170
  ### Install the CLI
169
171
 
@@ -805,6 +807,18 @@ walks through the mental model end to end.
805
807
 
806
808
  ## Troubleshooting
807
809
 
810
+ **Every command dies with no output, or the process crashes (`exit 139`,
811
+ `0xC0000005`, "Segmentation fault").**
812
+ Your Node is older than **22.14** and the bundled SQLite driver cannot load on
813
+ it. `better-sqlite3` 13 is compiled against Node-API 10, which Node only gained
814
+ in 22.14.0; on anything older `require()` succeeds and then the first database
815
+ open segfaults the process with no diagnostic. open-memex now refuses to load
816
+ the driver and says so, but anything already crashing was almost certainly this.
817
+ Check with `node -v`, then either upgrade Node (`nvm install 22.14 && nvm use
818
+ 22.14`, or any current 22.x/24.x) or pin the driver down with `npm install
819
+ better-sqlite3@^12.11.1`. `open-memex doctor` reports both the Node floor and a
820
+ live driver probe. Known upstream: WiseLibs/better-sqlite3#1514.
821
+
808
822
  **`open-memex` is not recognized / command not found.**
809
823
  A global `npm install -g` puts the `open-memex` launcher in npm's global bin
810
824
  folder. If your terminal can't find it, that folder isn't on your `PATH`:
package/README.zh-CN.md CHANGED
@@ -42,7 +42,7 @@
42
42
 
43
43
  ## 快速开始
44
44
 
45
- 需要 **Node.js ≥ 22.6**(用 `node -v` 检查)。然后:
45
+ 需要 **Node.js ≥ 22.14**(用 `node -v` 检查)。然后:
46
46
 
47
47
  ```sh
48
48
  npm install -g open-memex
@@ -118,7 +118,7 @@ open-memex 用两种方式接编辑器:原生的 **opencode 插件**,和标
118
118
 
119
119
  ### 环境要求
120
120
 
121
- - **Node.js ≥ 22.6**(`open-memex doctor` 会替你核查)
121
+ - **Node.js ≥ 22.14**(`open-memex doctor` 会替你核查)。这个下限来自 SQLite 驱动:`better-sqlite3` 13 是按 Node-API 10 编译的,而 Node 直到 22.14.0 才支持它——更老的 Node 在第一次打开数据库时会直接段错误崩溃。
122
122
 
123
123
  ### 安装 CLI
124
124
 
@@ -561,6 +561,9 @@ npx -y open-memex mcp # 无需安装
561
561
 
562
562
  ## 故障排查
563
563
 
564
+ **每条命令都毫无输出地死掉,或进程直接崩溃(`exit 139`、`0xC0000005`、"Segmentation fault")。**
565
+ 你的 Node 比 **22.14** 老,内置的 SQLite 驱动在它上面加载不了。`better-sqlite3` 13 是按 Node-API 10 编译的,而 Node 直到 22.14.0 才支持;在更老的版本上 `require()` 会成功,然后第一次打开数据库就把进程段错误掉,没有任何诊断信息。open-memex 现在会拒绝加载驱动并明确告诉你,但如果之前就是毫无输出一路崩溃,基本可以确定是这个原因。用 `node -v` 确认后,二选一:升级 Node(`nvm install 22.14 && nvm use 22.14`,或任意当前的 22.x/24.x),或者把驱动降回去:`npm install better-sqlite3@^12.11.1`。`open-memex doctor` 会同时报告 Node 下限和一次真实的驱动探测。上游已知问题:WiseLibs/better-sqlite3#1514。
566
+
564
567
  **`open-memex` 无法识别 / 命令找不到。**
565
568
  全局 `npm install -g` 把 `open-memex` 启动器放在 npm 的全局 bin 文件夹里。终端找不到命令,就是这个文件夹不在 PATH 上:
566
569
 
package/dist/.build-stamp CHANGED
@@ -1 +1 @@
1
- c84b05d4afadebb4346f31a95ca00c4075a700269fc5019453799d0eeeb8b09e
1
+ e9cc36b1db649d899fdd7298be01622709e4f6d8d4e583764c294bd009cfd0e6
package/dist/doctor.js CHANGED
@@ -10,6 +10,7 @@ import { fileURLToPath } from "node:url";
10
10
  import { loadConfig, configSource, DEFAULT_CONFIG, stripJsonComments } from "./config.js";
11
11
  import { paths, homeOverride } from "./paths.js";
12
12
  import { resolveCwdScope } from "./scope.js";
13
+ import { MIN_NODE_VERSION, nativeDriverFloorMessage, nodeSupportsNativeDriver, } from "./runtime-floor.js";
13
14
  import { db, backendName } from "./store/db.js";
14
15
  import { LEGACY_PACKAGE_NAME, opencodeGlobalConfigPath, userMcpConfigPath } from "./init.js";
15
16
  // D64: the MCP server exposes eleven tools; checking only the five the
@@ -29,12 +30,69 @@ const EXPECTED_TOOLS = [
29
30
  ];
30
31
  function nodeCheck() {
31
32
  const [major, minor] = process.versions.node.split(".").map(Number);
32
- const ok = major > 22 || (major === 22 && minor >= 6);
33
- return {
34
- name: "node",
35
- ok,
36
- detail: `v${process.versions.node} (need >= 22.6 for --experimental-strip-types)`,
37
- };
33
+ // Two independent floors, and the higher one wins: 22.6 for
34
+ // --experimental-strip-types, 22.14 for the native SQLite driver (D74).
35
+ const stripTypesOk = major > 22 || (major === 22 && minor >= 6);
36
+ const driverOk = nodeSupportsNativeDriver(process.versions.node);
37
+ const ok = stripTypesOk && driverOk;
38
+ let detail = `v${process.versions.node}`;
39
+ if (!stripTypesOk) {
40
+ detail += ` (need >= 22.6 for --experimental-strip-types)`;
41
+ }
42
+ else if (!driverOk) {
43
+ detail += ` (need >= ${MIN_NODE_VERSION}: better-sqlite3 13 is built against Node-API 10 and segfaults below it)`;
44
+ }
45
+ else {
46
+ detail += " (>= 22.14: ok for type stripping and the SQLite driver)";
47
+ }
48
+ return { name: "node", ok, detail };
49
+ }
50
+ /**
51
+ * D74: the native driver used to fail by segfaulting — invisible to any
52
+ * in-process try/catch. Now that `loadDatabase` refuses below the floor, the
53
+ * load is safe to attempt here, so a broken driver reports itself here instead
54
+ * of killing the first command the user runs.
55
+ */
56
+ function driverCheck() {
57
+ if (typeof globalThis.Bun !== "undefined") {
58
+ return { name: "sqlite driver", ok: true, detail: "bun:sqlite (built-in, no native module)" };
59
+ }
60
+ // Resolve the driver from THIS package's directory, not from the caller's
61
+ // cwd: `doctor` routinely runs with cwd set to a user project, where a bare
62
+ // `require("better-sqlite3")` would not find our node_modules at all.
63
+ const pkgRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
64
+ try {
65
+ const probe = execFileSync(process.execPath, [
66
+ "-e",
67
+ "const p=process.argv[1];const D=require(require.resolve('better-sqlite3',{paths:[p]}));const db=new D(':memory:');db.exec('create table t(a)');db.close();process.stdout.write('ok')",
68
+ pkgRoot,
69
+ ], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 30_000 });
70
+ return {
71
+ name: "sqlite driver",
72
+ ok: probe.includes("ok"),
73
+ detail: probe.includes("ok")
74
+ ? "better-sqlite3 loaded and opened an in-memory database"
75
+ : `probe returned ${JSON.stringify(probe)}`,
76
+ };
77
+ }
78
+ catch (err) {
79
+ const e = err;
80
+ // A native crash arrives as a huge unsigned status (0xC0000005 on Windows,
81
+ // 139 via SIGSEGV elsewhere). Print both forms - the hex is the one people
82
+ // recognize from Event Viewer or a shell's `exit=139`.
83
+ const how = e.signal
84
+ ? `signal ${e.signal}`
85
+ : typeof e.status === "number" && e.status > 255
86
+ ? `exit code ${e.status} (0x${(e.status >>> 0).toString(16).toUpperCase()})`
87
+ : `exit code ${e.status}`;
88
+ return {
89
+ name: "sqlite driver",
90
+ ok: false,
91
+ detail: `better-sqlite3 could not open a database (${how}). ` +
92
+ (e.stderr ? String(e.stderr).trim().split("\n").slice(-2).join(" ") + " " : "") +
93
+ nativeDriverFloorMessage(),
94
+ };
95
+ }
38
96
  }
39
97
  function configCheck() {
40
98
  try {
@@ -514,7 +572,7 @@ function vscodeMcpCheck() {
514
572
  }
515
573
  export async function runDoctor() {
516
574
  console.log("open-memex doctor");
517
- const checks = [nodeCheck(), configCheck(), scopeCheck(), storageCheck(), lockingCheck(), opencodePluginCheck(), vscodeMcpCheck()];
575
+ const checks = [nodeCheck(), configCheck(), driverCheck(), scopeCheck(), storageCheck(), lockingCheck(), opencodePluginCheck(), vscodeMcpCheck()];
518
576
  checks.push(await mcpCheck());
519
577
  checks.push(legacyCheck());
520
578
  let allOk = true;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Runtime floor for the native SQLite driver (D74).
3
+ *
4
+ * `better-sqlite3` 13 is built against Node-API 10, and Node only gained
5
+ * Node-API 10 in **22.14.0**. On an older Node the module registers and then
6
+ * dies with a segmentation fault inside `new Database(...)` — no message, no
7
+ * stack, exit 139 on macOS/Linux and `0xC0000005` on Windows. Upstream:
8
+ * WiseLibs/better-sqlite3#1514 (open; reproduces on win32-x64 and
9
+ * darwin-arm64, Node 20 and 22; `v12.11.1` is unaffected). Node 20 can never
10
+ * work with v13 — Node-API 10 never landed on that line — and it is EoL.
11
+ *
12
+ * So the floor is not "whatever `engines` claimed" (it said >=22.6, which was
13
+ * the `--experimental-strip-types` floor, not the driver's) but the exact
14
+ * Node-API 10 boundary. Below it we refuse loudly instead of segfaulting.
15
+ *
16
+ * Pure module — no imports, no db — so the comparison is unit-testable.
17
+ */
18
+ export const MIN_NODE_VERSION = "22.14.0";
19
+ export const NODE_FLOOR = { major: 22, minor: 14, patch: 0 };
20
+ /** Parse `major.minor.patch`, ignoring any prerelease suffix (`23.1.0-nightly`). */
21
+ export function parseNodeVersion(version) {
22
+ const m = /^v?(\d+)\.(\d+)\.(\d+)/.exec(version.trim());
23
+ if (!m)
24
+ return null;
25
+ return { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3]) };
26
+ }
27
+ /** True when `version` is at or above the Node-API 10 floor. */
28
+ export function nodeSupportsNativeDriver(version) {
29
+ const v = parseNodeVersion(version);
30
+ if (!v)
31
+ return false;
32
+ const f = NODE_FLOOR;
33
+ if (v.major !== f.major)
34
+ return v.major > f.major;
35
+ if (v.minor !== f.minor)
36
+ return v.minor > f.minor;
37
+ return v.patch >= f.patch;
38
+ }
39
+ /** The actionable message used by the driver guard and `doctor`. */
40
+ export function nativeDriverFloorMessage(version = process.versions.node) {
41
+ return (`better-sqlite3 13 needs Node >= ${MIN_NODE_VERSION} (it is built against ` +
42
+ `Node-API 10), but this runtime is v${version}. On older Node the driver ` +
43
+ `segfaults on the first database open, which is why open-memex refuses to ` +
44
+ `load it. Upgrade Node (nvm install ${MIN_NODE_VERSION.split(".").slice(0, 2).join(".")} ` +
45
+ `&& nvm use ${MIN_NODE_VERSION.split(".").slice(0, 2).join(".")}), or pin the driver ` +
46
+ `down with: npm install better-sqlite3@^12.11.1`);
47
+ }
package/dist/store/db.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { paths } from "../paths.js";
3
+ import { nativeDriverFloorMessage, nodeSupportsNativeDriver, } from "../runtime-floor.js";
3
4
  // Runtime-agnostic SQLite:
4
5
  // - Inside opencode (Bun runtime): bun:sqlite (built-in, no native module load)
5
6
  // - Under Node CLI: better-sqlite3
@@ -11,6 +12,13 @@ function loadDatabase() {
11
12
  // bun:sqlite is a built-in module; only resolvable under the Bun runtime.
12
13
  return require("bun:sqlite").Database;
13
14
  }
15
+ // D74: better-sqlite3 13 is built against Node-API 10, which Node gained in
16
+ // 22.14.0. Below that floor `require` succeeds and `new Database()` then
17
+ // segfaults the process with no diagnostic — so refuse before we can crash
18
+ // and say what to do instead (upstream: WiseLibs/better-sqlite3#1514).
19
+ if (!nodeSupportsNativeDriver(process.versions.node)) {
20
+ throw new Error(nativeDriverFloorMessage());
21
+ }
14
22
  return require("better-sqlite3");
15
23
  }
16
24
  let _db = null;
package/docs/V2-DESIGN.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # OpenMemex — Design Document (protocol v0.2)
2
2
 
3
- **Status:** FROZEN — protocol v0.2 (2026-09-26). Decisions D1–D73 are settled; open questions are tracked at the end of this document.
3
+ **Status:** FROZEN — protocol v0.2 (2026-09-26). Decisions D1–D74 are settled; open questions are tracked at the end of this document.
4
4
  **Author:** Stone, with 小沐
5
5
  **Changelog vs v1:** incorporates round-3 review from Perplexity, Grok, Gemini, ChatGPT, DeepSeek.
6
6
  Key changes: Design Principles section; `role` separated from `type`; two iron rules;
@@ -1469,6 +1469,36 @@ requirement: personal data never touches third-party services). Benchmarks to tr
1469
1469
  entry back before deleting. The HTML rendering of the same data layer
1470
1470
  follows as a separate change; auto-draft stays gated on the unlock
1471
1471
  conditions in the design doc.
1472
+ - **D74 — the Node floor is 22.14, because the driver sets it, not us.** D72
1473
+ moved the Node driver to `better-sqlite3` 13 (the N-API rewrite) to stop the
1474
+ Node 24.19+ exit-path abort. That upgrade carried an unstated requirement:
1475
+ v13 is compiled against **Node-API 10**, and Node only gained Node-API 10 in
1476
+ **22.14.0**. Below that floor `require("better-sqlite3")` succeeds and then
1477
+ `new Database()` segfaults the process — no message, no stack, exit 139 on
1478
+ macOS/Linux, `0xC0000005` on Windows. Measured on win32-x64: Node 22.12.0
1479
+ dies on the first open, Node 22.23.3 and 24.20.0 work; Node 20 can never work
1480
+ with v13, since Node-API 10 never landed on that line (EoL regardless). This
1481
+ is upstream and still open — WiseLibs/better-sqlite3#1514, plus #1516 for the
1482
+ separate `npm ci` gyp problem on Windows, where `gypfile: false` does not stop
1483
+ npm from shelling out to node-gyp. `engines.node` had been `>=22.6`, which was
1484
+ the `--experimental-strip-types` floor — a real but *different* constraint
1485
+ that predated the driver bump; leaving it would have shipped a package whose
1486
+ declared support window crashes on its own dependency. Three changes:
1487
+ (1) `engines.node` is `>=22.14.0`, and the two floors are documented as two
1488
+ floors rather than merged into one number; (2) `loadDatabase` refuses below
1489
+ the new floor and throws the actionable message (upgrade Node, or pin
1490
+ `better-sqlite3@^12.11.1` — v12 is unaffected), because a crash no in-process
1491
+ `try`/`catch` can report is the worst possible failure mode for a CLI;
1492
+ (3) `doctor` reports the floor *and* probes the driver in a **child process**,
1493
+ since a segfault cannot be caught in-process — the probe resolves the driver
1494
+ from this package's own directory, because `doctor` routinely runs with cwd
1495
+ set to a user project where a bare `require` finds nothing. The version
1496
+ comparison lives in `src/runtime-floor.ts`, pure and unit-tested, because the
1497
+ boundary is a fact to be tested (22.13.9 below, 22.14.0 exactly at, 22.23.3
1498
+ and 24.20.0 above) rather than a constant to be trusted. Deliberately NOT
1499
+ done: pinning back to v12 (that re-opens D72's Node 24 abort), or attempting
1500
+ to fix either upstream bug here.
1501
+
1472
1502
  - **D73 — one user statement, one memory: the keyword hook hands off to the
1473
1503
  agent.** The keyword hook and the agent are two writers for one user turn,
1474
1504
  and only one of them knows the sentence is already stored. Observed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-memex",
3
- "version": "0.7.0",
3
+ "version": "0.7.1-alpha.1",
4
4
  "description": "Local-first memory layer and protocol for AI coding agents. Markdown source of truth, SQLite FTS5 index, zero cloud.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -38,6 +38,6 @@
38
38
  "open-memex": "dist/cli.js"
39
39
  },
40
40
  "engines": {
41
- "node": ">=22.6"
41
+ "node": ">=22.14.0"
42
42
  }
43
43
  }
@@ -9,6 +9,12 @@ import { resolveProjectScope, resolveCwdScope, pickScopeRoot, PERSONAL_SCOPE } f
9
9
  import { cjkIndexText, cjkQueryExpr, hasCjk } from "../src/retrieve/cjk.ts";
10
10
  import { toFtsQuery } from "../src/retrieve/query.ts";
11
11
  import { contentHash, similarity, NEAR_DUP_THRESHOLD } from "../src/store/lifecycle.ts";
12
+ import {
13
+ MIN_NODE_VERSION,
14
+ nativeDriverFloorMessage,
15
+ nodeSupportsNativeDriver,
16
+ parseNodeVersion,
17
+ } from "../src/runtime-floor.ts";
12
18
  import {
13
19
  TURN_ECHO_THRESHOLD,
14
20
  TURN_ECHO_WINDOW_MS,
@@ -608,6 +614,31 @@ console.log("== env var names (D62) ==");
608
614
  }
609
615
  }
610
616
 
617
+ console.log("== runtime floor (native driver, D74) ==");
618
+ // better-sqlite3 13 is built against Node-API 10, which Node gained in 22.14.0.
619
+ // Below that floor `require` succeeds and `new Database()` segfaults the
620
+ // process with no diagnostic, so the version gate is the only thing standing
621
+ // between an old Node and a silent crash. The 22.12.0 / 22.23.3 boundary is
622
+ // the measured one (win32-x64, WiseLibs/better-sqlite3#1514).
623
+ ok("floor is 22.14.0", MIN_NODE_VERSION === "22.14.0");
624
+ ok("22.12.0 is below the floor (segfaults)", !nodeSupportsNativeDriver("22.12.0"));
625
+ ok("22.13.9 is below the floor", !nodeSupportsNativeDriver("22.13.9"));
626
+ ok("22.14.0 is exactly the floor", nodeSupportsNativeDriver("22.14.0"));
627
+ ok("22.14.1 is above", nodeSupportsNativeDriver("22.14.1"));
628
+ ok("22.23.3 is above (measured working)", nodeSupportsNativeDriver("22.23.3"));
629
+ ok("24.20.0 is above (measured working)", nodeSupportsNativeDriver("24.20.0"));
630
+ ok("25.x is above", nodeSupportsNativeDriver("25.3.0"));
631
+ ok("20.x is below (Node-API 10 never landed on 20)", !nodeSupportsNativeDriver("20.19.6"));
632
+ ok("v-prefix tolerated", nodeSupportsNativeDriver("v22.14.0"));
633
+ ok("prerelease suffix tolerated", nodeSupportsNativeDriver("23.1.0-nightly20260101"));
634
+ ok("garbage is below, not a crash", !nodeSupportsNativeDriver("not-a-version"));
635
+ ok("parses major/minor/patch", JSON.stringify(parseNodeVersion("22.14.3")) === '{"major":22,"minor":14,"patch":3}');
636
+ const floorMsg = nativeDriverFloorMessage("22.12.0");
637
+ ok("message names the floor", floorMsg.includes("22.14.0"));
638
+ ok("message names the running version", floorMsg.includes("22.12.0"));
639
+ ok("message offers a fix (upgrade or downgrade)", /Upgrade Node/.test(floorMsg) && /better-sqlite3@\^12/.test(floorMsg));
640
+ ok("message explains the segfault", /segfaults/.test(floorMsg));
641
+
611
642
  console.log("== lifecycle pure: contentHash / similarity ==");
612
643
  ok("hash deterministic + whitespace-insensitive", contentHash("hello world\n") === contentHash("hello world"));
613
644
  ok("hash differs on content", contentHash("hello world") !== contentHash("hello mars"));
package/src/doctor.ts CHANGED
@@ -10,6 +10,11 @@ import { fileURLToPath } from "node:url";
10
10
  import { loadConfig, configSource, DEFAULT_CONFIG, stripJsonComments } from "./config.ts";
11
11
  import { paths, homeOverride } from "./paths.ts";
12
12
  import { resolveCwdScope } from "./scope.ts";
13
+ import {
14
+ MIN_NODE_VERSION,
15
+ nativeDriverFloorMessage,
16
+ nodeSupportsNativeDriver,
17
+ } from "./runtime-floor.ts";
13
18
  import { db, backendName } from "./store/db.ts";
14
19
  import { LEGACY_PACKAGE_NAME, opencodeGlobalConfigPath, userMcpConfigPath } from "./init.ts";
15
20
 
@@ -37,12 +42,72 @@ const EXPECTED_TOOLS = [
37
42
 
38
43
  function nodeCheck(): Check {
39
44
  const [major, minor] = process.versions.node.split(".").map(Number);
40
- const ok = major > 22 || (major === 22 && minor >= 6);
41
- return {
42
- name: "node",
43
- ok,
44
- detail: `v${process.versions.node} (need >= 22.6 for --experimental-strip-types)`,
45
- };
45
+ // Two independent floors, and the higher one wins: 22.6 for
46
+ // --experimental-strip-types, 22.14 for the native SQLite driver (D74).
47
+ const stripTypesOk = major > 22 || (major === 22 && minor >= 6);
48
+ const driverOk = nodeSupportsNativeDriver(process.versions.node);
49
+ const ok = stripTypesOk && driverOk;
50
+ let detail = `v${process.versions.node}`;
51
+ if (!stripTypesOk) {
52
+ detail += ` (need >= 22.6 for --experimental-strip-types)`;
53
+ } else if (!driverOk) {
54
+ detail += ` (need >= ${MIN_NODE_VERSION}: better-sqlite3 13 is built against Node-API 10 and segfaults below it)`;
55
+ } else {
56
+ detail += " (>= 22.14: ok for type stripping and the SQLite driver)";
57
+ }
58
+ return { name: "node", ok, detail };
59
+ }
60
+
61
+ /**
62
+ * D74: the native driver used to fail by segfaulting — invisible to any
63
+ * in-process try/catch. Now that `loadDatabase` refuses below the floor, the
64
+ * load is safe to attempt here, so a broken driver reports itself here instead
65
+ * of killing the first command the user runs.
66
+ */
67
+ function driverCheck(): Check {
68
+ if (typeof (globalThis as { Bun?: unknown }).Bun !== "undefined") {
69
+ return { name: "sqlite driver", ok: true, detail: "bun:sqlite (built-in, no native module)" };
70
+ }
71
+ // Resolve the driver from THIS package's directory, not from the caller's
72
+ // cwd: `doctor` routinely runs with cwd set to a user project, where a bare
73
+ // `require("better-sqlite3")` would not find our node_modules at all.
74
+ const pkgRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
75
+ try {
76
+ const probe = execFileSync(
77
+ process.execPath,
78
+ [
79
+ "-e",
80
+ "const p=process.argv[1];const D=require(require.resolve('better-sqlite3',{paths:[p]}));const db=new D(':memory:');db.exec('create table t(a)');db.close();process.stdout.write('ok')",
81
+ pkgRoot,
82
+ ],
83
+ { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 30_000 },
84
+ );
85
+ return {
86
+ name: "sqlite driver",
87
+ ok: probe.includes("ok"),
88
+ detail: probe.includes("ok")
89
+ ? "better-sqlite3 loaded and opened an in-memory database"
90
+ : `probe returned ${JSON.stringify(probe)}`,
91
+ };
92
+ } catch (err) {
93
+ const e = err as { status?: number | null; signal?: string | null; stderr?: string };
94
+ // A native crash arrives as a huge unsigned status (0xC0000005 on Windows,
95
+ // 139 via SIGSEGV elsewhere). Print both forms - the hex is the one people
96
+ // recognize from Event Viewer or a shell's `exit=139`.
97
+ const how = e.signal
98
+ ? `signal ${e.signal}`
99
+ : typeof e.status === "number" && e.status > 255
100
+ ? `exit code ${e.status} (0x${(e.status >>> 0).toString(16).toUpperCase()})`
101
+ : `exit code ${e.status}`;
102
+ return {
103
+ name: "sqlite driver",
104
+ ok: false,
105
+ detail:
106
+ `better-sqlite3 could not open a database (${how}). ` +
107
+ (e.stderr ? String(e.stderr).trim().split("\n").slice(-2).join(" ") + " " : "") +
108
+ nativeDriverFloorMessage(),
109
+ };
110
+ }
46
111
  }
47
112
 
48
113
  function configCheck(): Check {
@@ -540,7 +605,7 @@ function vscodeMcpCheck(): Check {
540
605
 
541
606
  export async function runDoctor(): Promise<boolean> {
542
607
  console.log("open-memex doctor");
543
- const checks: Check[] = [nodeCheck(), configCheck(), scopeCheck(), storageCheck(), lockingCheck(), opencodePluginCheck(), vscodeMcpCheck()];
608
+ const checks: Check[] = [nodeCheck(), configCheck(), driverCheck(), scopeCheck(), storageCheck(), lockingCheck(), opencodePluginCheck(), vscodeMcpCheck()];
544
609
  checks.push(await mcpCheck());
545
610
  checks.push(legacyCheck());
546
611
  let allOk = true;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Runtime floor for the native SQLite driver (D74).
3
+ *
4
+ * `better-sqlite3` 13 is built against Node-API 10, and Node only gained
5
+ * Node-API 10 in **22.14.0**. On an older Node the module registers and then
6
+ * dies with a segmentation fault inside `new Database(...)` — no message, no
7
+ * stack, exit 139 on macOS/Linux and `0xC0000005` on Windows. Upstream:
8
+ * WiseLibs/better-sqlite3#1514 (open; reproduces on win32-x64 and
9
+ * darwin-arm64, Node 20 and 22; `v12.11.1` is unaffected). Node 20 can never
10
+ * work with v13 — Node-API 10 never landed on that line — and it is EoL.
11
+ *
12
+ * So the floor is not "whatever `engines` claimed" (it said >=22.6, which was
13
+ * the `--experimental-strip-types` floor, not the driver's) but the exact
14
+ * Node-API 10 boundary. Below it we refuse loudly instead of segfaulting.
15
+ *
16
+ * Pure module — no imports, no db — so the comparison is unit-testable.
17
+ */
18
+
19
+ export const MIN_NODE_VERSION = "22.14.0";
20
+
21
+ export interface NodeFloor {
22
+ major: number;
23
+ minor: number;
24
+ patch: number;
25
+ }
26
+
27
+ export const NODE_FLOOR: NodeFloor = { major: 22, minor: 14, patch: 0 };
28
+
29
+ /** Parse `major.minor.patch`, ignoring any prerelease suffix (`23.1.0-nightly`). */
30
+ export function parseNodeVersion(version: string): NodeFloor | null {
31
+ const m = /^v?(\d+)\.(\d+)\.(\d+)/.exec(version.trim());
32
+ if (!m) return null;
33
+ return { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3]) };
34
+ }
35
+
36
+ /** True when `version` is at or above the Node-API 10 floor. */
37
+ export function nodeSupportsNativeDriver(version: string): boolean {
38
+ const v = parseNodeVersion(version);
39
+ if (!v) return false;
40
+ const f = NODE_FLOOR;
41
+ if (v.major !== f.major) return v.major > f.major;
42
+ if (v.minor !== f.minor) return v.minor > f.minor;
43
+ return v.patch >= f.patch;
44
+ }
45
+
46
+ /** The actionable message used by the driver guard and `doctor`. */
47
+ export function nativeDriverFloorMessage(version: string = process.versions.node): string {
48
+ return (
49
+ `better-sqlite3 13 needs Node >= ${MIN_NODE_VERSION} (it is built against ` +
50
+ `Node-API 10), but this runtime is v${version}. On older Node the driver ` +
51
+ `segfaults on the first database open, which is why open-memex refuses to ` +
52
+ `load it. Upgrade Node (nvm install ${MIN_NODE_VERSION.split(".").slice(0, 2).join(".")} ` +
53
+ `&& nvm use ${MIN_NODE_VERSION.split(".").slice(0, 2).join(".")}), or pin the driver ` +
54
+ `down with: npm install better-sqlite3@^12.11.1`
55
+ );
56
+ }
package/src/store/db.ts CHANGED
@@ -1,5 +1,10 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { paths } from "../paths.ts";
3
+ import {
4
+ MIN_NODE_VERSION,
5
+ nativeDriverFloorMessage,
6
+ nodeSupportsNativeDriver,
7
+ } from "../runtime-floor.ts";
3
8
 
4
9
  // Runtime-agnostic SQLite:
5
10
  // - Inside opencode (Bun runtime): bun:sqlite (built-in, no native module load)
@@ -18,6 +23,13 @@ function loadDatabase(): AnyDatabaseCtor {
18
23
  // bun:sqlite is a built-in module; only resolvable under the Bun runtime.
19
24
  return require("bun:sqlite").Database as AnyDatabaseCtor;
20
25
  }
26
+ // D74: better-sqlite3 13 is built against Node-API 10, which Node gained in
27
+ // 22.14.0. Below that floor `require` succeeds and `new Database()` then
28
+ // segfaults the process with no diagnostic — so refuse before we can crash
29
+ // and say what to do instead (upstream: WiseLibs/better-sqlite3#1514).
30
+ if (!nodeSupportsNativeDriver(process.versions.node)) {
31
+ throw new Error(nativeDriverFloorMessage());
32
+ }
21
33
  return require("better-sqlite3") as AnyDatabaseCtor;
22
34
  }
23
35