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 +4 -2
- package/CHANGELOG.md +11 -0
- package/README.md +24 -2
- package/README.zh-CN.md +8 -2
- package/dist/.build-stamp +1 -1
- package/dist/doctor.js +65 -7
- package/dist/runtime-floor.js +47 -0
- package/dist/store/db.js +8 -0
- package/docs/V2-DESIGN.md +31 -1
- package/package.json +2 -2
- package/scripts/smoke-pure.ts +31 -0
- package/src/doctor.ts +72 -7
- package/src/runtime-floor.ts +56 -0
- package/src/store/db.ts +12 -0
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
|
|
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–
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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–
|
|
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.
|
|
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.
|
|
41
|
+
"node": ">=22.14.0"
|
|
42
42
|
}
|
|
43
43
|
}
|
package/scripts/smoke-pure.ts
CHANGED
|
@@ -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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|