claude-mem-lite 3.60.0 → 3.60.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/install.mjs +14 -9
- package/lib/binding-probe.mjs +75 -5
- package/npm-shrinkwrap.json +2 -2
- package/package.json +2 -1
- package/scripts/binding-probe-cli.mjs +115 -0
- package/scripts/launch.mjs +6 -2
- package/scripts/setup.sh +32 -59
- package/source-files.mjs +4 -0
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "claude-mem-lite",
|
|
13
|
-
"version": "3.60.
|
|
13
|
+
"version": "3.60.1",
|
|
14
14
|
"source": "./",
|
|
15
15
|
"description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark)."
|
|
16
16
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "3.60.
|
|
3
|
+
"version": "3.60.1",
|
|
4
4
|
"description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark).",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "sdsrss"
|
package/install.mjs
CHANGED
|
@@ -40,7 +40,7 @@ const NPM_INSTALL_CMD = 'npm install --omit=dev --no-audit --no-fund';
|
|
|
40
40
|
import { RESOURCE_METADATA } from './install-metadata.mjs';
|
|
41
41
|
import { scanPluginCacheHookPollution } from './plugin-cache-guard.mjs';
|
|
42
42
|
import { SOURCE_FILES, HOOK_SCRIPT_FILES } from './source-files.mjs';
|
|
43
|
-
import { probeBetterSqlite3Binding, ensureBetterSqlite3Working, NATIVE_BINDING_REBUILD_CMD } from './lib/binding-probe.mjs';
|
|
43
|
+
import { probeBetterSqlite3Binding, probeBindingInFreshProcess, ensureBetterSqlite3Working, NATIVE_BINDING_REBUILD_CMD } from './lib/binding-probe.mjs';
|
|
44
44
|
import { clearNativeBindingBreakage, readNativeBindingBreakage } from './lib/native-binding-hint.mjs';
|
|
45
45
|
import { sweepStaleTestFixtures } from './lib/tmp-fixture-sweep.mjs';
|
|
46
46
|
import { acquireLock } from './lib/proc-lock.mjs';
|
|
@@ -1396,14 +1396,17 @@ async function doctor() {
|
|
|
1396
1396
|
issues++;
|
|
1397
1397
|
}
|
|
1398
1398
|
|
|
1399
|
-
// Dependencies
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1399
|
+
// Dependencies. Out of process: an in-process open of a STALE .node caches a
|
|
1400
|
+
// dead module handle for the rest of doctor and can SIGSEGV on teardown —
|
|
1401
|
+
// truncating the report of the very run the user started because things are
|
|
1402
|
+
// broken. This is also what makes the native-binding check further down
|
|
1403
|
+
// (which reads the same tree) honest rather than answering from a poisoned
|
|
1404
|
+
// process.
|
|
1405
|
+
const depProbe = probeBindingInFreshProcess(bindingHostDir());
|
|
1406
|
+
if (depProbe.ok) {
|
|
1404
1407
|
ok('better-sqlite3: verified (import + open OK)');
|
|
1405
|
-
}
|
|
1406
|
-
fail(`better-sqlite3: import/init failed (${
|
|
1408
|
+
} else {
|
|
1409
|
+
fail(`better-sqlite3: import/init failed (${String(depProbe.error).split('\n')[0]})`);
|
|
1407
1410
|
issues++;
|
|
1408
1411
|
}
|
|
1409
1412
|
|
|
@@ -1454,7 +1457,9 @@ async function doctor() {
|
|
|
1454
1457
|
// right now". A Node upgrade breaks every DB-touching path at once, so this is
|
|
1455
1458
|
// the single highest-value line in doctor when it fires.
|
|
1456
1459
|
const breakage = readNativeBindingBreakage(join(MEM_DATA_DIR, 'runtime'));
|
|
1457
|
-
|
|
1460
|
+
// Reuses the dependency probe above — same tree, same question, and doctor
|
|
1461
|
+
// should not pay for two child spawns to ask it twice.
|
|
1462
|
+
const bindingProbe = depProbe;
|
|
1458
1463
|
if (!bindingProbe.ok) {
|
|
1459
1464
|
fail(`Native DB binding: unusable (${String(bindingProbe.error).split('\n')[0]}) — run \`node ${join(PROJECT_DIR, 'cli.mjs')} rebuild-binding\``);
|
|
1460
1465
|
issues++;
|
package/lib/binding-probe.mjs
CHANGED
|
@@ -70,20 +70,90 @@ export async function probeBetterSqlite3Binding(installDir) {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Probe the binding from a FRESH child process.
|
|
75
|
+
*
|
|
76
|
+
* Why this exists: better-sqlite3 dlopen's its .node lazily and Node caches the
|
|
77
|
+
* module handle process-wide, so a process that has already touched a STALE
|
|
78
|
+
* binary can never load its replacement — the retry dies with "Module did not
|
|
79
|
+
* self-register" (and, under scripts/setup.sh's probe, a segfault on the way
|
|
80
|
+
* out). Verifying a freshly rebuilt binding therefore has to happen somewhere
|
|
81
|
+
* that never saw the old one. Same constraint healAndReexec re-execs for.
|
|
82
|
+
*
|
|
83
|
+
* Synchronous (spawnSync) to match the surrounding execSync rebuild — this runs
|
|
84
|
+
* in installers and hook-adjacent scripts, never on a request path.
|
|
85
|
+
*
|
|
86
|
+
* @param {string} installDir Directory containing node_modules/better-sqlite3
|
|
87
|
+
* @param {{timeoutMs?: number}} [opts]
|
|
88
|
+
* @returns {{ok: true} | {ok: false, error: string}}
|
|
89
|
+
*/
|
|
90
|
+
export function probeBindingInFreshProcess(installDir, { timeoutMs = 30_000 } = {}) {
|
|
91
|
+
// The child catches and prints the MESSAGE on its own stdout. Two reasons it
|
|
92
|
+
// is not just an uncaught throw read off stderr: an uncaught exception's first
|
|
93
|
+
// stderr line is the stack HEADER (`node:internal/modules/cjs/loader:1520`),
|
|
94
|
+
// not the diagnostic — and this string is the highest-value line `doctor`
|
|
95
|
+
// prints — and any node warning emitted before the throw would land on stderr
|
|
96
|
+
// first and hijack it. The child's stdout is captured by spawnSync and never
|
|
97
|
+
// inherited, so writing there cannot reach a hook's JSON envelope.
|
|
98
|
+
// installDir is interpolated as a JSON string literal, so a path containing
|
|
99
|
+
// quotes/backslashes cannot break out of the script.
|
|
100
|
+
const script =
|
|
101
|
+
'try {'
|
|
102
|
+
+ 'const { createRequire } = require("node:module");'
|
|
103
|
+
+ `const D = createRequire(${JSON.stringify(join(installDir, 'package.json'))})("better-sqlite3");`
|
|
104
|
+
+ 'new D(":memory:").close();'
|
|
105
|
+
+ '} catch (e) { process.stdout.write(String((e && e.message) || e)); process.exit(1); }';
|
|
106
|
+
const r = spawnSync(process.execPath, ['-e', script], { stdio: 'pipe', timeout: timeoutMs });
|
|
107
|
+
// BEFORE the status check: spawnSync's `timeout` is SIGTERM-then-wait, not a
|
|
108
|
+
// deadline, so a child that survives the signal can still exit 0 while
|
|
109
|
+
// r.error is ETIMEDOUT. Reading status alone would call that healthy.
|
|
110
|
+
if (r.error) return { ok: false, error: r.error.message };
|
|
111
|
+
if (r.status === 0) return { ok: true };
|
|
112
|
+
// Fallbacks cover the paths where the catch never ran: a native crash (the
|
|
113
|
+
// SIGSEGV above) or a failure to spawn at all — both leave stdout empty.
|
|
114
|
+
const printed = String(r.stdout || '').trim();
|
|
115
|
+
const stderrLine = String(r.stderr || '').split('\n').map((l) => l.trim()).find(Boolean);
|
|
116
|
+
return {
|
|
117
|
+
ok: false,
|
|
118
|
+
error: printed
|
|
119
|
+
|| stderrLine
|
|
120
|
+
|| `binding probe exited ${r.status ?? `on signal ${r.signal}`}`,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
73
124
|
/**
|
|
74
125
|
* Verify better-sqlite3 binding works in `installDir`; if not, run
|
|
75
126
|
* `npm rebuild better-sqlite3` and re-probe. Returns
|
|
76
127
|
* { ok: true, action: 'verified' | 'rebuilt' } on success or
|
|
77
|
-
* { ok: false, error } if rebuild can't fix it. The `probe` and
|
|
78
|
-
* deps are injectable so this can be unit-tested without a real npm
|
|
128
|
+
* { ok: false, error } if rebuild can't fix it. The `probe`, `verify` and
|
|
129
|
+
* `rebuild` deps are injectable so this can be unit-tested without a real npm
|
|
79
130
|
* subprocess.
|
|
80
131
|
*
|
|
81
132
|
* @param {string} installDir Directory containing node_modules/better-sqlite3
|
|
82
|
-
* @param {{probe?: () => Promise<{ok: boolean, error?: string}>, rebuild?: () => Promise<void>, exec?: (cmd: string, opts: object) => void}} [deps]
|
|
133
|
+
* @param {{probe?: () => Promise<{ok: boolean, error?: string}>, verify?: () => Promise<{ok: boolean, error?: string}> | {ok: boolean, error?: string}, rebuild?: () => Promise<void>, exec?: (cmd: string, opts: object) => void}} [deps]
|
|
83
134
|
* @returns {Promise<{ok: true, action: 'verified' | 'rebuilt'} | {ok: false, error: string}>}
|
|
84
135
|
*/
|
|
85
136
|
export async function ensureBetterSqlite3Working(installDir, deps = {}) {
|
|
86
|
-
|
|
137
|
+
// BOTH probes run out of process by default, because a probe must never
|
|
138
|
+
// poison the process that has to act on its answer. Loading a stale .node
|
|
139
|
+
// caches a dead module handle process-wide (and can SIGSEGV on teardown), so
|
|
140
|
+
// an in-process probe→rebuild→re-probe cycle always ends in "Module did not
|
|
141
|
+
// self-register" — reporting a SUCCESSFUL rebuild as a failure. Measured
|
|
142
|
+
// 2026-08-13 on a real ABI 127-under-137 tree: this function returned
|
|
143
|
+
// {ok:false} while the rebuilt .node loaded fine in a fresh process. Every
|
|
144
|
+
// caller keyed state on that lie — setup.sh wrote .deps-broken over a healthy
|
|
145
|
+
// install, install.mjs::rebuildBinding skipped clearNativeBindingBreakage so
|
|
146
|
+
// the launcher re-spawned npm every 6h forever, and `rebuild-binding` exited 1.
|
|
147
|
+
//
|
|
148
|
+
// Isolating only the SECOND probe is not enough: scripts/launch.mjs imports
|
|
149
|
+
// the MCP server into this very process right after a successful rebuild, so a
|
|
150
|
+
// first probe that dlopened the stale binary would hand the server the dead
|
|
151
|
+
// handle. Cost is one ~40ms spawn on a path that already runs npm.
|
|
152
|
+
//
|
|
153
|
+
// `deps.probe` alone still drives BOTH probes: injected-stub tests keep their
|
|
154
|
+
// existing semantics.
|
|
155
|
+
const probe = deps.probe || (() => probeBindingInFreshProcess(installDir));
|
|
156
|
+
const verify = deps.verify || deps.probe || (() => probeBindingInFreshProcess(installDir));
|
|
87
157
|
// Bounded by default: a node-gyp fallback that stalls (no compiler, a hung
|
|
88
158
|
// registry fetch) must not hang the caller forever — the CLI blocks a user at
|
|
89
159
|
// the terminal, and scripts/setup.sh passes an even tighter 20s cap because it
|
|
@@ -117,7 +187,7 @@ export async function ensureBetterSqlite3Working(installDir, deps = {}) {
|
|
|
117
187
|
return { ok: false, error: `rebuild failed: ${e.message}` };
|
|
118
188
|
}
|
|
119
189
|
|
|
120
|
-
const second = await
|
|
190
|
+
const second = await verify();
|
|
121
191
|
if (second.ok) return { ok: true, action: 'rebuilt' };
|
|
122
192
|
|
|
123
193
|
return { ok: false, error: second.error || first.error };
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "3.60.
|
|
3
|
+
"version": "3.60.1",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "claude-mem-lite",
|
|
9
|
-
"version": "3.60.
|
|
9
|
+
"version": "3.60.1",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
12
12
|
"better-sqlite3": "^12.6.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "3.60.
|
|
3
|
+
"version": "3.60.1",
|
|
4
4
|
"description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "npm@10.9.2",
|
|
@@ -158,6 +158,7 @@
|
|
|
158
158
|
"scripts/pre-agent-inject.js",
|
|
159
159
|
"scripts/prompt-search-utils.mjs",
|
|
160
160
|
"scripts/hook-launcher.mjs",
|
|
161
|
+
"scripts/binding-probe-cli.mjs",
|
|
161
162
|
".mcp.json",
|
|
162
163
|
".claude-plugin/plugin.json",
|
|
163
164
|
".claude-plugin/marketplace.json",
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/binding-probe-cli.mjs — SessionStart native-binding probe + bounded heal.
|
|
3
|
+
//
|
|
4
|
+
// Contract with scripts/setup.sh: exit 0 = binding usable NOW, non-zero = not.
|
|
5
|
+
// Everything human-readable goes to stderr — SessionStart stdout is a JSON
|
|
6
|
+
// envelope Claude Code parses, so this must never write there.
|
|
7
|
+
//
|
|
8
|
+
// Extracted (v3.60.1) from an inline `node --input-type=module -e '…'` string
|
|
9
|
+
// inside setup.sh. Two load-bearing reasons, both observed rather than assumed:
|
|
10
|
+
// • That form CRASHED. The -e process SIGSEGV'd during exit after a
|
|
11
|
+
// verified-good rebuild (Node v24.18, ~50% of runs, no fatal-error report
|
|
12
|
+
// produced), so a SUCCESSFUL heal exited 139 and setup.sh wrote
|
|
13
|
+
// .deps-broken over a healthy install. install.mjs::rebuildBinding doing
|
|
14
|
+
// the same work as a normal module never reproduced it.
|
|
15
|
+
// • It could not contain an apostrophe. The script was interpolated into a
|
|
16
|
+
// single-quoted bash string, so a single `don't` in a comment truncated the
|
|
17
|
+
// shell command — a footgun that fired during development of this fix.
|
|
18
|
+
//
|
|
19
|
+
// Rebuilds are bounded to 20s because setup.sh runs under the SessionStart hook
|
|
20
|
+
// cap (hooks/hooks.json timeout 30): letting the hook SIGKILL a mid-flight
|
|
21
|
+
// node-gyp would leave a partial .node with no flag written. On lock-miss or
|
|
22
|
+
// timeout, mark broken and defer to the MCP launch path (no cap, same lock).
|
|
23
|
+
|
|
24
|
+
import { execSync, spawnSync } from 'node:child_process';
|
|
25
|
+
import { dirname, join } from 'node:path';
|
|
26
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
27
|
+
|
|
28
|
+
const ROOT = process.env.PROBE_ROOT || join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
29
|
+
|
|
30
|
+
// Fallback for a half-installed tree where lib/ helpers are absent: a bare
|
|
31
|
+
// probe with no rebuild. A WORKING binding must still be able to clear the
|
|
32
|
+
// broken flag, and a helperless broken tree is repaired by the hook-launcher
|
|
33
|
+
// path instead. Out of process like every other probe here — loading a stale
|
|
34
|
+
// .node caches a dead module handle for the rest of THIS process.
|
|
35
|
+
function bareProbe(root) {
|
|
36
|
+
const script =
|
|
37
|
+
'try {'
|
|
38
|
+
+ 'const { createRequire } = require("node:module");'
|
|
39
|
+
+ `const D = createRequire(${JSON.stringify(join(root, 'package.json'))})("better-sqlite3");`
|
|
40
|
+
+ 'new D(":memory:").close();'
|
|
41
|
+
+ '} catch (e) { process.stdout.write(String((e && e.message) || e)); process.exit(1); }';
|
|
42
|
+
const r = spawnSync(process.execPath, ['-e', script], { stdio: 'pipe', timeout: 8000 });
|
|
43
|
+
if (!r.error && r.status === 0) return true;
|
|
44
|
+
// Say WHY. The inline predecessor printed the cause here; dropping it left the
|
|
45
|
+
// user with setup.sh's generic "binding unusable" and nothing to act on.
|
|
46
|
+
const why = String(r.stdout || '').trim().split('\n')[0]
|
|
47
|
+
|| (r.error && r.error.message)
|
|
48
|
+
|| `probe exited ${r.status ?? `on signal ${r.signal}`}`;
|
|
49
|
+
process.stderr.write(`[claude-mem-lite] binding probe: ${why}\n`);
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let helpers = null;
|
|
54
|
+
try {
|
|
55
|
+
const [probeMod, lockMod, dirMod] = await Promise.all(
|
|
56
|
+
['binding-probe.mjs', 'proc-lock.mjs', 'resolve-data-dir.mjs']
|
|
57
|
+
.map((f) => import(pathToFileURL(join(ROOT, 'lib', f)).href)),
|
|
58
|
+
);
|
|
59
|
+
helpers = { ...probeMod, ...lockMod, ...dirMod };
|
|
60
|
+
} catch {
|
|
61
|
+
process.exit(bareProbe(ROOT) ? 0 : 1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Probe first — read-only, no lock needed. A healthy binding exits here.
|
|
65
|
+
// Bounded like every other step: the whole script runs under the SessionStart
|
|
66
|
+
// hook cap (hooks/hooks.json timeout 30) and an unbounded probe would spend the
|
|
67
|
+
// entire budget before the rebuild it exists to trigger.
|
|
68
|
+
const first = helpers.probeBindingInFreshProcess(ROOT, { timeoutMs: 8000 });
|
|
69
|
+
if (first.ok) process.exit(0);
|
|
70
|
+
|
|
71
|
+
// Broken: rebuild ONLY under the shared install.lock. A second MCP launch or an
|
|
72
|
+
// install.mjs repair rebuilding the same node_modules concurrently can tear the
|
|
73
|
+
// .node mid-compile.
|
|
74
|
+
let lockPath;
|
|
75
|
+
try {
|
|
76
|
+
lockPath = join(helpers.resolveDataDir(process.env.CLAUDE_MEM_DIR), 'runtime', 'install.lock');
|
|
77
|
+
} catch (e) {
|
|
78
|
+
// resolveDataDir THROWS on a non-absolute CLAUDE_MEM_DIR. Unhandled, that
|
|
79
|
+
// prints an 8-line rejection stack onto SessionStart stderr; one line is enough.
|
|
80
|
+
process.stderr.write(`[claude-mem-lite] binding probe: ${e.message}\n`);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
const release = helpers.acquireLock(lockPath);
|
|
84
|
+
if (!release) {
|
|
85
|
+
const firstLine = String(first.error).split('\n')[0];
|
|
86
|
+
process.stderr.write(
|
|
87
|
+
`[claude-mem-lite] binding probe: ${firstLine} (another install/repair in flight — deferring heal)\n`,
|
|
88
|
+
);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let result;
|
|
93
|
+
try {
|
|
94
|
+
result = await helpers.ensureBetterSqlite3Working(ROOT, {
|
|
95
|
+
// Reuse the probe we already paid for. Without this, ensure() spawns its own
|
|
96
|
+
// default first probe — a second identical child on the critical path, under
|
|
97
|
+
// a hook cap, to re-learn what `first` already says.
|
|
98
|
+
probe: () => first,
|
|
99
|
+
exec: (cmd, opts) => execSync(cmd, { ...opts, timeout: 20000 }),
|
|
100
|
+
verify: () => helpers.probeBindingInFreshProcess(ROOT, { timeoutMs: 8000 }),
|
|
101
|
+
});
|
|
102
|
+
} finally {
|
|
103
|
+
// process.exit skips finally blocks — every exit below this point, so the
|
|
104
|
+
// lock is always released.
|
|
105
|
+
release();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!result.ok) {
|
|
109
|
+
process.stderr.write(`[claude-mem-lite] binding probe: ${result.error}\n`);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
if (result.action === 'rebuilt') {
|
|
113
|
+
process.stderr.write('[claude-mem-lite] rebuilt better-sqlite3 binding for current Node ABI\n');
|
|
114
|
+
}
|
|
115
|
+
process.exit(0);
|
package/scripts/launch.mjs
CHANGED
|
@@ -36,7 +36,7 @@ if (!existsSync(join(ROOT, 'node_modules', 'better-sqlite3'))) {
|
|
|
36
36
|
// intact but the .node binary stale → server FATALs with "Could not locate
|
|
37
37
|
// the bindings file" on first DB open. Probe + auto-rebuild before launching.
|
|
38
38
|
try {
|
|
39
|
-
const { ensureBetterSqlite3Working,
|
|
39
|
+
const { ensureBetterSqlite3Working, probeBindingInFreshProcess } = await import('../lib/binding-probe.mjs');
|
|
40
40
|
// The rebuild inside ensureBetterSqlite3Working mutates node_modules — the
|
|
41
41
|
// same write class as install/repair/update, and this was the ONE rebuild
|
|
42
42
|
// path outside the shared install.lock: a second MCP launch or a concurrent
|
|
@@ -56,7 +56,11 @@ try {
|
|
|
56
56
|
if (release) {
|
|
57
57
|
verify = await ensureBetterSqlite3Working(ROOT);
|
|
58
58
|
} else {
|
|
59
|
-
|
|
59
|
+
// Out of process, like the rebuild-capable branch above: this process goes
|
|
60
|
+
// on to import the MCP server, and a stale .node loaded here would leave a
|
|
61
|
+
// dead module handle cached for it. Also keeps the exit(1) guidance below
|
|
62
|
+
// reachable — an in-process load of a stale binding can SIGSEGV instead.
|
|
63
|
+
const probe = probeBindingInFreshProcess(ROOT);
|
|
60
64
|
verify = probe.ok
|
|
61
65
|
? { ok: true, action: 'verified' }
|
|
62
66
|
: { ok: false, error: `${probe.error} (another install/repair holds the lock — not rebuilding concurrently; reconnect with /mcp once it finishes)` };
|
package/scripts/setup.sh
CHANGED
|
@@ -136,65 +136,38 @@ fi
|
|
|
136
136
|
# stat, a new plugin-cache version dir (fresh node_modules) re-probes, and
|
|
137
137
|
# a Node upgrade (new ABI) re-probes. While broken, every SessionStart
|
|
138
138
|
# retries the rebuild until it heals.
|
|
139
|
-
#
|
|
139
|
+
# Delegates to scripts/binding-probe-cli.mjs — a real module file, NOT an inline
|
|
140
|
+
# `node -e` string. That inline form SIGSEGV'd during exit after a verified-good
|
|
141
|
+
# rebuild (Node v24.18, ~50% of runs), so a successful heal returned 139 and this
|
|
142
|
+
# branch recorded .deps-broken over a healthy install; it also could not contain
|
|
143
|
+
# an apostrophe without truncating the shell command. See that file's header.
|
|
144
|
+
# Contract: exit 0 = binding usable now.
|
|
140
145
|
probe_binding() {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
// Probe first — read-only, no lock needed. Healthy binding exits here.
|
|
168
|
-
const first = await helpers.probeBetterSqlite3Binding(root);
|
|
169
|
-
if (first.ok) process.exit(0);
|
|
170
|
-
// Broken: rebuild ONLY under the shared install.lock (a second MCP launch
|
|
171
|
-
// or install.mjs repair rebuilding the same node_modules concurrently can
|
|
172
|
-
// tear the .node), and with the exec bounded to 20s — this script runs
|
|
173
|
-
// under the SessionStart hook cap (hooks.json timeout 30), and letting the
|
|
174
|
-
// hook SIGKILL a mid-flight node-gyp leaves a partial .node with no flag
|
|
175
|
-
// written. On lock-miss or timeout: mark broken and defer the heal to the
|
|
176
|
-
// MCP launch path (no hook cap, same lock).
|
|
177
|
-
const lockPath = join(helpers.resolveDataDir(process.env.CLAUDE_MEM_DIR), "runtime", "install.lock");
|
|
178
|
-
const release = helpers.acquireLock(lockPath);
|
|
179
|
-
if (!release) {
|
|
180
|
-
const firstLine = String(first.error).split("\n")[0];
|
|
181
|
-
process.stderr.write(`[claude-mem-lite] binding probe: ${firstLine} (another install/repair in flight — deferring heal)\n`);
|
|
182
|
-
process.exit(1);
|
|
183
|
-
}
|
|
184
|
-
let r;
|
|
185
|
-
try {
|
|
186
|
-
const { execSync } = await import("node:child_process");
|
|
187
|
-
r = await helpers.ensureBetterSqlite3Working(root, {
|
|
188
|
-
exec: (cmd, opts) => execSync(cmd, { ...opts, timeout: 20000 }),
|
|
189
|
-
});
|
|
190
|
-
} finally {
|
|
191
|
-
// process.exit skips finally blocks — exits live BELOW this so the
|
|
192
|
-
// lock is always released.
|
|
193
|
-
release();
|
|
194
|
-
}
|
|
195
|
-
if (!r.ok) { process.stderr.write(`[claude-mem-lite] binding probe: ${r.error}\n`); process.exit(1); }
|
|
196
|
-
if (r.action === "rebuilt") process.stderr.write("[claude-mem-lite] rebuilt better-sqlite3 binding for current Node ABI\n");
|
|
197
|
-
'
|
|
146
|
+
# Absent on a truncated tree: without this guard node prints a full
|
|
147
|
+
# MODULE_NOT_FOUND stack onto SessionStart stderr on every marker-miss.
|
|
148
|
+
[[ -f "$ROOT/scripts/binding-probe-cli.mjs" ]] || return 1
|
|
149
|
+
# stdout muted: this child runs `npm rebuild`, and SessionStart stdout is a
|
|
150
|
+
# JSON envelope Claude Code parses. Nothing writes there today (verified) —
|
|
151
|
+
# this keeps a future non-piped exec from being able to.
|
|
152
|
+
PROBE_ROOT="$ROOT" node "$ROOT/scripts/binding-probe-cli.mjs" >/dev/null
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
# Ground truth about the binding, independent of how the healer above exited.
|
|
156
|
+
# Belt-and-braces: moving the probe out of the inline `node -e` string removed
|
|
157
|
+
# the observed SIGSEGV, but the healer's exit code is a PROXY for the question
|
|
158
|
+
# that actually matters, and a proxy can lie again (a future crash, a partial
|
|
159
|
+
# state, a kill at the hook cap). So ask the real question in a fresh process:
|
|
160
|
+
# can we open a DB right now? A false .deps-broken here is not cosmetic — it
|
|
161
|
+
# renders a "hooks degraded" banner into the user's session over a healthy
|
|
162
|
+
# install. Costs one ~50ms spawn, and only off the marker fast-path.
|
|
163
|
+
# shellcheck disable=SC2016 # node script single-quoted on purpose; ROOT passed via env, not shell expansion
|
|
164
|
+
binding_usable() {
|
|
165
|
+
VERIFY_ROOT="$ROOT" node -e '
|
|
166
|
+
const { createRequire } = require("node:module");
|
|
167
|
+
const { join } = require("node:path");
|
|
168
|
+
const D = createRequire(join(process.env.VERIFY_ROOT, "package.json"))("better-sqlite3");
|
|
169
|
+
new D(":memory:").close();
|
|
170
|
+
' >/dev/null 2>&1
|
|
198
171
|
}
|
|
199
172
|
|
|
200
173
|
if [[ -d "$ROOT/node_modules/better-sqlite3" ]]; then
|
|
@@ -202,7 +175,7 @@ if [[ -d "$ROOT/node_modules/better-sqlite3" ]]; then
|
|
|
202
175
|
BINDING_MARKER="$ROOT/node_modules/.mem-binding-ok-$NODE_ABI"
|
|
203
176
|
if [[ -f "$BINDING_MARKER" ]]; then
|
|
204
177
|
mark_deps_ok
|
|
205
|
-
elif probe_binding; then
|
|
178
|
+
elif probe_binding || binding_usable; then
|
|
206
179
|
rm -f "$ROOT/node_modules/.mem-binding-ok-"* 2>/dev/null || true
|
|
207
180
|
touch "$BINDING_MARKER" 2>/dev/null || true
|
|
208
181
|
mark_deps_ok
|
package/source-files.mjs
CHANGED
|
@@ -250,6 +250,9 @@ export const HOOK_SCRIPT_FILES = [
|
|
|
250
250
|
// as the MCP server. v3.42 audit HIGH-1.
|
|
251
251
|
// - setup.sh: run on plugin SessionStart via hooks.json. Signed for defense-in-depth
|
|
252
252
|
// (no tarball→executed-path propagation today, but it ships in files[] and is executed).
|
|
253
|
+
// - binding-probe-cli.mjs: setup.sh spawns it on every SessionStart that misses the ABI
|
|
254
|
+
// marker, and it runs `npm rebuild` — an unsigned copy would be arbitrary code executed
|
|
255
|
+
// at session start with a build step attached. Same class as setup.sh itself.
|
|
253
256
|
// These ship via package.json files[] directly, not via HOOK_SCRIPT_FILES' copy path, so
|
|
254
257
|
// listing them here changes ONLY what is signed/verified, not what install materializes.
|
|
255
258
|
// Module-internal (spread into RELEASE_SIGNED_FILES below); not exported — no external
|
|
@@ -258,6 +261,7 @@ const LAUNCHER_SCRIPT_FILES = [
|
|
|
258
261
|
'launch.mjs',
|
|
259
262
|
'launch-preflight.mjs',
|
|
260
263
|
'setup.sh',
|
|
264
|
+
'binding-probe-cli.mjs',
|
|
261
265
|
];
|
|
262
266
|
|
|
263
267
|
// The complete set of files the release signature MUST cover: every runtime .mjs
|