open-memex 0.7.0 → 0.7.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`.
@@ -170,9 +172,9 @@ Context injection happens exactly once per session in `experimental.chat.system.
170
172
 
171
173
  ## Design constraints — read the frozen design first
172
174
 
173
- `docs/V2-DESIGN.md` is the frozen protocol v0.2 (decisions settled through D73; open questions tracked at the end of the doc). Per its §12:
175
+ `docs/V2-DESIGN.md` is the frozen protocol v0.2 (decisions settled through D74; open questions tracked at the end of the doc). Per its §12:
174
176
  AGENTS.md answers "how should AI work here"; the design doc answers "why is it
175
- built this way" (principles, iron rules, the append-only decision log, currently D1–D73). Before changing
177
+ built this way" (principles, iron rules, the append-only decision log, currently D1–D74). Before changing
176
178
  architecture, scope semantics, lifecycle, or the protocol surface (frontmatter
177
179
  schema, MCP tools, CLI contract), read the relevant design section — the decision
178
180
  log records what was already considered and rejected.
package/CHANGELOG.md CHANGED
@@ -9,6 +9,17 @@ the `@alpha` tag are development snapshots and are not listed individually.
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [0.7.1] - 2026-10-04
13
+
14
+ ## The Node floor is 22.14, because the driver says so (D74)
15
+
16
+ - **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.
17
+ - **`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.
18
+ - **`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.
19
+ - Both READMEs gained a troubleshooting entry for the crash signature (`exit 139` / `0xC0000005` / "Segmentation fault"). No protocol, schema, or data change.
20
+
21
+ ## [0.7.0] - 2026-10-04
22
+
12
23
  ## One thing you said is one memory (D73)
13
24
 
14
25
  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`:
@@ -915,6 +929,14 @@ the Copilot instructions. With no `--client` it cleans up every detected
915
929
  editor; `--global` limits the cleanup to user-level wiring. Your memories are
916
930
  never touched.
917
931
 
932
+ **I upgraded Node, or switched versions with nvm. Do I need to reinstall?**
933
+ The package itself doesn't care: its SQLite driver is a Node-API prebuild, so
934
+ it loads on any supported Node (≥ 22.14) with no recompiling, and your
935
+ memories live outside the install. But version managers (nvm and friends)
936
+ keep a separate global package folder per Node version, so after a switch
937
+ `open-memex` may simply be "not found". Run `npm install -g open-memex` once
938
+ under the new Node, then `open-memex doctor` to confirm the driver loads.
939
+
918
940
  ## Project status
919
941
 
920
942
  open-memex is stable and in daily use; the current stable line is published on
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
 
@@ -607,6 +610,9 @@ Windows 上已加载的 DLL 是锁定的:如果 open-memex 的 MCP 服务器
607
610
  **VS Code——`init` 跑一次还是每个项目跑?**
608
611
  跑一次就够。直接 `open-memex init` 会自动检测 VS Code 并把 MCP 服务器条目写进 VS Code 的用户级 `mcp.json`(Windows 在 `%APPDATA%/Code/User/mcp.json`,macOS 在 `~/Library/Application Support/Code/User/mcp.json`,Linux 在 `~/.config/Code/User/mcp.json`),服务器在每个项目都会启动。项目里有项目级 `.vscode/mcp.json` 时仍以它优先,而该条目保留 `cwd=${workspaceFolder}`,所以项目作用域按窗口各自解析。如果你的 用户级 `mcp.json` 里有注释(VS Code 接受 JSONC),`init` 不动它,会打印确切片段让你手动添加。空文件会被当作空白,直接写入。
609
612
 
613
+ **我升级了 Node,或者用 nvm 切换了版本,需要重装 open-memex 吗?**
614
+ 包本身不用:它的 SQLite 驱动是 Node-API 预编译的,在任何受支持的 Node(≥ 22.14)上都能直接加载、不需要重新编译;你的记忆也不在安装目录里。但 nvm 这类版本管理器会给每个 Node 版本留一份独立的全局包目录,切换之后 `open-memex` 可能就"找不到"了。在新 Node 下跑一次 `npm install -g open-memex`,再用 `open-memex doctor` 确认驱动能正常加载。
615
+
610
616
  **怎么移除编辑器接线?**
611
617
  `open-memex uninstall` 撤销 `init`:移除 MCP 服务器条目、opencode 插件行、Agent Skill 目录,以及 Copilot 指令里的 open-memex 段落。不带 `--client` 时清理所有检测到的编辑器;`--global` 把清理限制在用户级接线。你的记忆永不被动。
612
618
 
package/dist/.build-stamp CHANGED
@@ -1 +1 @@
1
- c84b05d4afadebb4346f31a95ca00c4075a700269fc5019453799d0eeeb8b09e
1
+ f8c82ea850a33c28a396d33675b9d22b1b99cf19cef5e828252fb0400b1154e6
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",
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