claude-mem-lite 3.59.0 → 3.60.0
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/cli.mjs +1 -1
- package/install.mjs +68 -1
- package/lib/activity.mjs +4 -2
- package/lib/binding-probe.mjs +91 -3
- package/lib/hook-telemetry.mjs +17 -0
- package/lib/low-signal-patterns.mjs +18 -2
- package/lib/native-binding-hint.mjs +64 -8
- package/lib/stats-quality.mjs +6 -4
- package/mem-cli.mjs +34 -0
- package/memdir.mjs +45 -8
- package/npm-shrinkwrap.json +24 -24
- package/package.json +1 -1
- package/scoring-sql.mjs +10 -1
- package/scripts/hook-launcher.mjs +99 -3
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "claude-mem-lite",
|
|
13
|
-
"version": "3.
|
|
13
|
+
"version": "3.60.0",
|
|
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.
|
|
3
|
+
"version": "3.60.0",
|
|
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/cli.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
const CLI_COMMANDS = new Set(['search', 'recent', 'recall', 'get', 'timeline', 'save', 'stats', 'context', 'browse', 'citation-stats', 'delete', 'update', 'export', 'restore', 'compress', 'maintain', 'optimize', 'fts-check', 'registry', 'import', 'import-jsonl', 'enrich', 'activity', 'adopt', 'unadopt', 'memdir-audit', 'defer', 'help']);
|
|
3
|
-
const INSTALL_COMMANDS = new Set(['install', 'uninstall', 'status', 'doctor', 'cleanup', 'cleanup-hooks', 'self-update', 'repair', 'release']);
|
|
3
|
+
const INSTALL_COMMANDS = new Set(['install', 'uninstall', 'status', 'doctor', 'cleanup', 'cleanup-hooks', 'self-update', 'repair', 'rebuild-binding', 'release']);
|
|
4
4
|
|
|
5
5
|
const cmd = process.argv[2];
|
|
6
6
|
|
package/install.mjs
CHANGED
|
@@ -40,7 +40,8 @@ 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 } from './lib/binding-probe.mjs';
|
|
43
|
+
import { probeBetterSqlite3Binding, ensureBetterSqlite3Working, NATIVE_BINDING_REBUILD_CMD } from './lib/binding-probe.mjs';
|
|
44
|
+
import { clearNativeBindingBreakage, readNativeBindingBreakage } from './lib/native-binding-hint.mjs';
|
|
44
45
|
import { sweepStaleTestFixtures } from './lib/tmp-fixture-sweep.mjs';
|
|
45
46
|
import { acquireLock } from './lib/proc-lock.mjs';
|
|
46
47
|
import { atomicWriteFileSync } from './lib/atomic-write.mjs';
|
|
@@ -1447,6 +1448,23 @@ async function doctor() {
|
|
|
1447
1448
|
ok('Hook self-heal: no recent silent hook breakage');
|
|
1448
1449
|
}
|
|
1449
1450
|
|
|
1451
|
+
// Native DB binding. Two signals, because they answer different questions:
|
|
1452
|
+
// the marker says "hooks have been failing" (possibly for days, since the hint
|
|
1453
|
+
// is 6h-rate-limited stderr nobody reads), the live probe says "is it broken
|
|
1454
|
+
// right now". A Node upgrade breaks every DB-touching path at once, so this is
|
|
1455
|
+
// the single highest-value line in doctor when it fires.
|
|
1456
|
+
const breakage = readNativeBindingBreakage(join(MEM_DATA_DIR, 'runtime'));
|
|
1457
|
+
const bindingProbe = await probeBetterSqlite3Binding(bindingHostDir());
|
|
1458
|
+
if (!bindingProbe.ok) {
|
|
1459
|
+
fail(`Native DB binding: unusable (${String(bindingProbe.error).split('\n')[0]}) — run \`node ${join(PROJECT_DIR, 'cli.mjs')} rebuild-binding\``);
|
|
1460
|
+
issues++;
|
|
1461
|
+
} else if (breakage) {
|
|
1462
|
+
const ageH = Math.round((Date.now() - (breakage.ts || 0)) / 3600000);
|
|
1463
|
+
dwarn(`Native DB binding: healthy now, but a fire failed ~${ageH}h ago (${breakage.reason || 'unknown'}) — stale marker clears on the next successful rebuild-binding`);
|
|
1464
|
+
} else {
|
|
1465
|
+
ok(`Native DB binding: loadable on Node ${process.version}`);
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1450
1468
|
// Plugin/hook lifecycle state
|
|
1451
1469
|
const settings = readSettings();
|
|
1452
1470
|
const hasHooks = hasMemHooksConfigured(settings);
|
|
@@ -2129,6 +2147,51 @@ function regenerateLockfile() {
|
|
|
2129
2147
|
|
|
2130
2148
|
// ─── Main ───────────────────────────────────────────────────────────────────
|
|
2131
2149
|
|
|
2150
|
+
// An install can own MORE THAN ONE better-sqlite3 tree (dev repo, ~/.claude-mem-lite,
|
|
2151
|
+
// the plugin cache), each with its own .node — and only the one the RUNNING code
|
|
2152
|
+
// resolves matters, i.e. the one next to this file. Rebuilding the wrong tree
|
|
2153
|
+
// reports success while every hook keeps failing. Fall back to INSTALL_DIR when
|
|
2154
|
+
// this file sits in a source-only layout with no deps of its own.
|
|
2155
|
+
function bindingHostDir() {
|
|
2156
|
+
return existsSync(join(PROJECT_DIR, 'node_modules', 'better-sqlite3')) ? PROJECT_DIR : INSTALL_DIR;
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
// Local, network-free repair for an unusable native DB binding — the Node-upgrade
|
|
2160
|
+
// fault (ABI 127 → 137) that `repair` is the wrong size for: repair re-downloads
|
|
2161
|
+
// and signature-verifies a whole GitHub release and fails closed offline, while
|
|
2162
|
+
// this recompiles one module in place. Named in the hook hint, run unattended by
|
|
2163
|
+
// scripts/hook-launcher.mjs at session-start, and usable by hand.
|
|
2164
|
+
//
|
|
2165
|
+
// Takes the same install.lock as the install write phase and launch.mjs's rebuild:
|
|
2166
|
+
// two concurrent rebuilds can clobber the .node mid-compile. A live peer → report
|
|
2167
|
+
// and exit 0 (it is doing this very work), never race it.
|
|
2168
|
+
async function rebuildBinding() {
|
|
2169
|
+
const host = bindingHostDir();
|
|
2170
|
+
const release = acquireLock(join(MEM_DATA_DIR, 'runtime', 'install.lock'));
|
|
2171
|
+
if (!release) {
|
|
2172
|
+
// NOT exit 0: skipping is not healing. Callers key their state on the exit
|
|
2173
|
+
// code — a false success would let the launcher drop its cooldown and the
|
|
2174
|
+
// CLI re-exec into the same broken binding.
|
|
2175
|
+
console.error('[install] Another install/repair is in progress — it owns the rebuild; skipping.');
|
|
2176
|
+
process.exitCode = 1;
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
2179
|
+
try {
|
|
2180
|
+
const verify = await ensureBetterSqlite3Working(host);
|
|
2181
|
+
if (verify.ok) {
|
|
2182
|
+
ok(`better-sqlite3 binding ${verify.action} for Node ${process.version} (${host})`);
|
|
2183
|
+
// The fault is gone → drop the marker so session-start stops retrying.
|
|
2184
|
+
clearNativeBindingBreakage(join(MEM_DATA_DIR, 'runtime'));
|
|
2185
|
+
} else {
|
|
2186
|
+
fail(`better-sqlite3 binding still unusable: ${verify.error}`);
|
|
2187
|
+
log(`Try manually: cd ${host} && ${NATIVE_BINDING_REBUILD_CMD}`);
|
|
2188
|
+
process.exitCode = 1;
|
|
2189
|
+
}
|
|
2190
|
+
} finally {
|
|
2191
|
+
release();
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2132
2195
|
// Cross-process gate around the install write phase. repair() is intentionally
|
|
2133
2196
|
// NOT locked here: it spawns `install.mjs install` as a child, which takes this
|
|
2134
2197
|
// lock — locking the parent too would deadlock. A live peer (another session's
|
|
@@ -2173,6 +2236,9 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
2173
2236
|
case 'repair':
|
|
2174
2237
|
await repair();
|
|
2175
2238
|
break;
|
|
2239
|
+
case 'rebuild-binding':
|
|
2240
|
+
await rebuildBinding();
|
|
2241
|
+
break;
|
|
2176
2242
|
case 'release':
|
|
2177
2243
|
syncVersions();
|
|
2178
2244
|
if (!flags.has('--no-lock')) regenerateLockfile();
|
|
@@ -2203,6 +2269,7 @@ Usage:
|
|
|
2203
2269
|
node install.mjs cleanup-hooks Remove only claude-mem-lite hooks from settings.json
|
|
2204
2270
|
node install.mjs self-update Check for and install updates
|
|
2205
2271
|
node install.mjs repair Recover a broken install: download latest tarball, re-run install
|
|
2272
|
+
node install.mjs rebuild-binding Recompile better-sqlite3 for the running Node (fixes "NODE_MODULE_VERSION" after a Node upgrade)
|
|
2206
2273
|
node install.mjs release Sync versions (plugin/marketplace/CLAUDE.md) + regen lockfile via npm@10.9.2 (use --no-lock to skip lock regen)
|
|
2207
2274
|
|
|
2208
2275
|
npx claude-mem-lite Install via npx (one-liner)
|
package/lib/activity.mjs
CHANGED
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
import { sanitizeFtsQuery } from '../utils.mjs';
|
|
8
8
|
import { scrubRecord } from './scrub-record.mjs';
|
|
9
9
|
import { saveObservation } from './save-observation.mjs';
|
|
10
|
-
|
|
10
|
+
// Pure title-only builder: this query runs on the EVENTS table, which has no
|
|
11
|
+
// lesson_learned column — the lesson-escape variant would be a SQL error here.
|
|
12
|
+
import { buildNotLowSignalSql } from './low-signal-patterns.mjs';
|
|
11
13
|
import { OBS_TYPE_SET } from './obs-types.mjs';
|
|
12
14
|
|
|
13
15
|
// Observation types (mirrors the observations.type enum) — events carry a wider
|
|
@@ -150,7 +152,7 @@ export function promoteInsightEvents(db, { project = null, minImportance = 2, ex
|
|
|
150
152
|
FROM events
|
|
151
153
|
WHERE body IS NOT NULL AND TRIM(body) != '' AND importance >= ?
|
|
152
154
|
AND superseded_at_epoch IS NULL
|
|
153
|
-
AND ${
|
|
155
|
+
AND ${buildNotLowSignalSql('')}
|
|
154
156
|
${projClause}
|
|
155
157
|
ORDER BY created_at_epoch DESC LIMIT ?
|
|
156
158
|
`;
|
package/lib/binding-probe.mjs
CHANGED
|
@@ -6,10 +6,51 @@
|
|
|
6
6
|
// Node v24), and the presence of node_modules/better-sqlite3/ on disk is not
|
|
7
7
|
// sufficient — the binding can be present-but-stale after a Node upgrade.
|
|
8
8
|
|
|
9
|
-
import { execSync } from 'node:child_process';
|
|
9
|
+
import { execSync, spawnSync } from 'node:child_process';
|
|
10
10
|
import { createRequire } from 'node:module';
|
|
11
11
|
import { join } from 'node:path';
|
|
12
12
|
|
|
13
|
+
// npm >= 12 blocks lifecycle scripts by default, so a plain `npm rebuild` exits 0
|
|
14
|
+
// WITHOUT compiling — see the rebuild() comment below. Single home for the one
|
|
15
|
+
// command that actually works, so hints, docs and heal paths cannot drift apart.
|
|
16
|
+
export const NATIVE_BINDING_REBUILD_CMD = 'npm rebuild better-sqlite3 --dangerously-allow-all-scripts';
|
|
17
|
+
|
|
18
|
+
// Set on a re-exec'd child so one failed heal cannot fork-bomb the CLI.
|
|
19
|
+
export const BINDING_HEAL_GUARD_ENV = 'CLAUDE_MEM_BINDING_HEALED';
|
|
20
|
+
|
|
21
|
+
// The native-binding fault family, in the four shapes it actually reaches callers:
|
|
22
|
+
// • ERR_DLOPEN_FAILED — Node's code for a failed dlopen (ABI mismatch)
|
|
23
|
+
// • NODE_MODULE_VERSION N vs M — the ABI text itself (some throws carry no code)
|
|
24
|
+
// • Could not locate the bindings file — build/Release missing or never compiled
|
|
25
|
+
// • Module did not self-register — the .node was REPLACED under a process that
|
|
26
|
+
// already dlopen'd the old one; only a fresh process recovers (hence the
|
|
27
|
+
// re-exec in healAndReexec, not an in-process retry)
|
|
28
|
+
// Deliberately NARROW: a rebuild cannot fix DB corruption or a missing data dir,
|
|
29
|
+
// and misclassifying those would burn a 30s npm run on every fire.
|
|
30
|
+
const NATIVE_BINDING_PATTERNS = [
|
|
31
|
+
/NODE_MODULE_VERSION/,
|
|
32
|
+
/Could not locate the bindings file/i,
|
|
33
|
+
/did not self-register/i,
|
|
34
|
+
/invalid ELF header/i,
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* True when `err` means "the better-sqlite3 native binding is unusable and a
|
|
39
|
+
* rebuild is the right repair".
|
|
40
|
+
*
|
|
41
|
+
* @param {unknown} err
|
|
42
|
+
* @returns {boolean}
|
|
43
|
+
*/
|
|
44
|
+
export function isNativeBindingError(err) {
|
|
45
|
+
if (!err) return false;
|
|
46
|
+
if (err.code === 'ERR_DLOPEN_FAILED') return true;
|
|
47
|
+
// `err ?? ''` covers a thrown STRING: recordHookError accepts any thrown value
|
|
48
|
+
// and already normalizes that shape for its log, so the classifier must not
|
|
49
|
+
// silently read undefined and miss it.
|
|
50
|
+
const msg = String(err.message ?? err ?? '');
|
|
51
|
+
return NATIVE_BINDING_PATTERNS.some((re) => re.test(msg));
|
|
52
|
+
}
|
|
53
|
+
|
|
13
54
|
/**
|
|
14
55
|
* Probe better-sqlite3's native binding by importing it from `installDir`'s
|
|
15
56
|
* node_modules and opening an in-memory DB. Returns {ok, error?}.
|
|
@@ -43,7 +84,11 @@ export async function probeBetterSqlite3Binding(installDir) {
|
|
|
43
84
|
*/
|
|
44
85
|
export async function ensureBetterSqlite3Working(installDir, deps = {}) {
|
|
45
86
|
const probe = deps.probe || (() => probeBetterSqlite3Binding(installDir));
|
|
46
|
-
|
|
87
|
+
// Bounded by default: a node-gyp fallback that stalls (no compiler, a hung
|
|
88
|
+
// registry fetch) must not hang the caller forever — the CLI blocks a user at
|
|
89
|
+
// the terminal, and scripts/setup.sh passes an even tighter 20s cap because it
|
|
90
|
+
// runs under a hook timeout. Callers needing a different budget inject `exec`.
|
|
91
|
+
const exec = deps.exec || ((cmd, opts) => execSync(cmd, { timeout: 240_000, ...opts }));
|
|
47
92
|
const rebuild = deps.rebuild || (async () => {
|
|
48
93
|
// npm >= 12 blocks install/lifecycle scripts by default (the `allow-scripts`
|
|
49
94
|
// allowlist ships empty). better-sqlite3's install step
|
|
@@ -57,7 +102,7 @@ export async function ensureBetterSqlite3Working(installDir, deps = {}) {
|
|
|
57
102
|
// npm has no such gate and treats the unknown flag as an ignored config; if
|
|
58
103
|
// it instead errors on the flag, fall back to the plain rebuild.
|
|
59
104
|
try {
|
|
60
|
-
exec(
|
|
105
|
+
exec(NATIVE_BINDING_REBUILD_CMD, { cwd: installDir, stdio: 'pipe' });
|
|
61
106
|
} catch {
|
|
62
107
|
exec('npm rebuild better-sqlite3', { cwd: installDir, stdio: 'pipe' });
|
|
63
108
|
}
|
|
@@ -77,3 +122,46 @@ export async function ensureBetterSqlite3Working(installDir, deps = {}) {
|
|
|
77
122
|
|
|
78
123
|
return { ok: false, error: second.error || first.error };
|
|
79
124
|
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Foreground heal for a user-invoked process (the CLI): rebuild the binding,
|
|
128
|
+
* then RE-EXEC this process with its original argv and return the child's exit
|
|
129
|
+
* code. The re-exec is not a convenience — better-sqlite3 dlopen's its .node
|
|
130
|
+
* lazily and caches the handle, so a process that has already hit the stale
|
|
131
|
+
* binary cannot use the fresh one: retrying in-process fails with "Module did
|
|
132
|
+
* not self-register" (observed 2026-08-13 while healing this exact fault).
|
|
133
|
+
*
|
|
134
|
+
* Refuses to act when the guard env is already set, so a heal that does not
|
|
135
|
+
* actually fix the binding cannot spawn an unbounded chain of children.
|
|
136
|
+
*
|
|
137
|
+
* @param {{installDir?: string, argv?: string[], env?: Record<string,string|undefined>, ensure?: () => Promise<{ok: boolean, action?: string, error?: string}>, reexec?: (argv: string[], env: Record<string,string|undefined>) => number, log?: (msg: string) => void}} opts
|
|
138
|
+
* @returns {Promise<{healed: true, exitCode: number} | {healed: false, reason: string, error?: string}>}
|
|
139
|
+
*/
|
|
140
|
+
export async function healAndReexec(opts) {
|
|
141
|
+
const {
|
|
142
|
+
installDir,
|
|
143
|
+
argv = process.argv,
|
|
144
|
+
env = process.env,
|
|
145
|
+
log = () => {},
|
|
146
|
+
} = opts;
|
|
147
|
+
const ensure = opts.ensure || (() => ensureBetterSqlite3Working(installDir));
|
|
148
|
+
const reexec = opts.reexec || ((childArgv, childEnv) => {
|
|
149
|
+
const r = spawnSync(childArgv[0], childArgv.slice(1), { stdio: 'inherit', env: childEnv });
|
|
150
|
+
return typeof r.status === 'number' ? r.status : 1;
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
if (env[BINDING_HEAL_GUARD_ENV]) return { healed: false, reason: 'already-attempted' };
|
|
154
|
+
|
|
155
|
+
log(`native DB binding unusable — rebuilding for this Node (${process.version})…`);
|
|
156
|
+
let verify;
|
|
157
|
+
try {
|
|
158
|
+
verify = await ensure();
|
|
159
|
+
} catch (e) {
|
|
160
|
+
return { healed: false, reason: 'rebuild-failed', error: e.message };
|
|
161
|
+
}
|
|
162
|
+
if (!verify.ok) return { healed: false, reason: 'rebuild-failed', error: verify.error };
|
|
163
|
+
|
|
164
|
+
log('binding rebuilt — retrying');
|
|
165
|
+
const exitCode = reexec(argv, { ...env, [BINDING_HEAL_GUARD_ENV]: '1' });
|
|
166
|
+
return { healed: true, exitCode };
|
|
167
|
+
}
|
package/lib/hook-telemetry.mjs
CHANGED
|
@@ -20,6 +20,11 @@
|
|
|
20
20
|
|
|
21
21
|
import { appendFileSync, mkdirSync, existsSync, readdirSync, readFileSync, statSync, unlinkSync } from 'fs';
|
|
22
22
|
import { join } from 'path';
|
|
23
|
+
// Both are pure-`node:`-static modules (better-sqlite3 is only createRequire'd
|
|
24
|
+
// lazily inside binding-probe's functions), so this keeps the "usable from the
|
|
25
|
+
// lightweight standalone scripts" property stated above.
|
|
26
|
+
import { isNativeBindingError } from './binding-probe.mjs';
|
|
27
|
+
import { recordNativeBindingBreakage } from './native-binding-hint.mjs';
|
|
23
28
|
|
|
24
29
|
const DAY_MS = 86400000;
|
|
25
30
|
const RETENTION_MS = 14 * DAY_MS;
|
|
@@ -82,6 +87,18 @@ export function recordHookError(scope, err, runtimeDir, ctx) {
|
|
|
82
87
|
appendFileSync(join(dir, `${today()}.jsonl`), line, { mode: 0o600 });
|
|
83
88
|
// Amortized retention sweep: 14-day window kept clean without a cron.
|
|
84
89
|
pruneOldShards(dir);
|
|
90
|
+
|
|
91
|
+
// Every hook script funnels its failures through here — including the
|
|
92
|
+
// STANDALONE ones (scripts/pre-tool-recall.js, scripts/pre-skill-bridge.js)
|
|
93
|
+
// that never import hook.mjs and so never reach its dispatch catch. That gap
|
|
94
|
+
// is why the 2026-08-13 outage stayed invisible: 78 of that day's 79 entries
|
|
95
|
+
// were `pre-recall:db-open`, i.e. the ONE path whose errors nothing but this
|
|
96
|
+
// log could see. Flagging the native-binding family here — rather than at
|
|
97
|
+
// each call site — is what makes the session-start heal fire no matter which
|
|
98
|
+
// script hits the stale binding first.
|
|
99
|
+
if (isNativeBindingError(err)) {
|
|
100
|
+
recordNativeBindingBreakage(runtimeDir, { reason: String(err?.message ?? err ?? ''), event: String(scope || '') });
|
|
101
|
+
}
|
|
85
102
|
} catch { /* recorder must never throw */ }
|
|
86
103
|
}
|
|
87
104
|
|
|
@@ -50,13 +50,29 @@ export function buildLowSignalRegex() {
|
|
|
50
50
|
* Build the SQL NOT LIKE clause chain, optionally prefixed with a table alias.
|
|
51
51
|
* Output is a single parenthesized AND-chain — safe to combine with other AND/OR.
|
|
52
52
|
*
|
|
53
|
+
* `lessonEscape` (2026-07-24 audit P1, D#11): retrieval consumers on the
|
|
54
|
+
* observations table pass `{ lessonEscape: true }` to admit rows whose title
|
|
55
|
+
* matches a LOW_SIGNAL pattern but which carry a real lesson_learned — the
|
|
56
|
+
* read-side counterpart of the isNoiseObservation/capNoiseImportance write-side
|
|
57
|
+
* signal escapes. Without it, a substantive obs titled "npm pack drops …" is
|
|
58
|
+
* unsearchable forever. Escape is lesson-only by design: an importance>=2
|
|
59
|
+
* escape would resurrect pre-v2.47 Haiku-inflated noise on legacy DBs.
|
|
60
|
+
* Default stays title-only because two consumers query the events table
|
|
61
|
+
* (no lesson_learned column) and two are noise-title METRICS, not filters.
|
|
62
|
+
*
|
|
53
63
|
* @param {string} [alias=''] Table alias (e.g. 'o') — empty for unqualified.
|
|
64
|
+
* @param {object} [opts]
|
|
65
|
+
* @param {boolean} [opts.lessonEscape=false] Admit rows with non-empty, non-'none' lesson_learned.
|
|
54
66
|
* @returns {string} SQL boolean expression
|
|
55
67
|
*/
|
|
56
|
-
export function buildNotLowSignalSql(alias = '') {
|
|
68
|
+
export function buildNotLowSignalSql(alias = '', { lessonEscape = false } = {}) {
|
|
57
69
|
const p = alias ? `${alias}.` : '';
|
|
58
70
|
const clauses = LOW_SIGNAL_PATTERNS.map(({ like }) => `${p}title NOT LIKE '${like}'`);
|
|
59
|
-
|
|
71
|
+
const chain = '(\n ' + clauses.join('\n AND ') + '\n )';
|
|
72
|
+
if (!lessonEscape) return chain;
|
|
73
|
+
// Mirrors the write-side lesson test: String(lesson).trim().toLowerCase() not in ('', 'none').
|
|
74
|
+
const escape = `(${p}lesson_learned IS NOT NULL AND LOWER(TRIM(${p}lesson_learned)) NOT IN ('', 'none'))`;
|
|
75
|
+
return `(${chain} OR ${escape})`;
|
|
60
76
|
}
|
|
61
77
|
|
|
62
78
|
// Cached singleton — isNoiseObservation is called once per observation insert.
|
|
@@ -16,20 +16,33 @@
|
|
|
16
16
|
// run `npm rebuild` itself (2–5s timeout + concurrent-fire races).
|
|
17
17
|
//
|
|
18
18
|
// Pure node: imports + injectable now/runtimeDir so it unit-tests without the
|
|
19
|
-
// hook dependency graph (no schema.mjs / better-sqlite3 import).
|
|
19
|
+
// hook dependency graph (no schema.mjs / better-sqlite3 import). The one non-node:
|
|
20
|
+
// import — lib/binding-probe.mjs for the shared fault classifier — keeps that
|
|
21
|
+
// property: it only `createRequire`s better-sqlite3 lazily inside its functions,
|
|
22
|
+
// so importing it never dlopen's the very binding this module reports on.
|
|
20
23
|
|
|
21
24
|
import { join } from 'node:path';
|
|
22
|
-
import { readFileSync, writeFileSync, mkdirSync, renameSync } from 'node:fs';
|
|
25
|
+
import { readFileSync, writeFileSync, mkdirSync, renameSync, unlinkSync } from 'node:fs';
|
|
23
26
|
import { fileURLToPath } from 'node:url';
|
|
27
|
+
import { isNativeBindingError } from './binding-probe.mjs';
|
|
24
28
|
|
|
25
29
|
export const NATIVE_BINDING_HINT_COOLDOWN_MS = 6 * 60 * 60 * 1000; // 6h
|
|
26
30
|
const MARKER_NAME = 'native-binding-hint-last';
|
|
27
31
|
|
|
28
|
-
//
|
|
32
|
+
// Breakage marker read by scripts/hook-launcher.mjs at session-start to trigger
|
|
33
|
+
// the unattended rebuild. The hint alone was NOT enough: it goes to hook stderr,
|
|
34
|
+
// is rate-limited to once per 6h, and in the field (2026-08-13) a Node 22 → 24
|
|
35
|
+
// upgrade left the binding stale for 4 days across 79 failed fires because the
|
|
36
|
+
// only healer was the MCP-server launch path, which those sessions never ran.
|
|
37
|
+
export const NATIVE_BINDING_BROKEN_MARKER = 'native-binding-broken';
|
|
38
|
+
|
|
39
|
+
// Resolvable invocation of the bundled CLI's LOCAL binding repair. Absolute via
|
|
29
40
|
// import.meta.url (cli.mjs is one dir up from lib/) so it works on a plugin-only
|
|
30
|
-
// install, where bare `claude-mem-lite` is not on PATH.
|
|
31
|
-
//
|
|
32
|
-
|
|
41
|
+
// install, where bare `claude-mem-lite` is not on PATH. `rebuild-binding`, not
|
|
42
|
+
// `repair`: repair re-downloads and Ed25519-verifies a whole GitHub release and
|
|
43
|
+
// fails closed offline — the wrong (often impossible) tool for recompiling one
|
|
44
|
+
// native module against the running Node. (review #3)
|
|
45
|
+
const CLI_REBUILD_BINDING = `node ${fileURLToPath(new URL('../cli.mjs', import.meta.url))} rebuild-binding`;
|
|
33
46
|
|
|
34
47
|
// Stable-ish identity of a fault so DISTINCT failures get DISTINCT cooldown
|
|
35
48
|
// windows: the same fault → same key (suppressed within the window), a different
|
|
@@ -93,13 +106,56 @@ export function nativeBindingHintDue(runtimeDir, now = Date.now(), cooldownMs =
|
|
|
93
106
|
*/
|
|
94
107
|
export function formatHookError(err, event, { now = Date.now(), runtimeDir } = {}) {
|
|
95
108
|
const ts = new Date(now).toISOString();
|
|
96
|
-
if (err
|
|
109
|
+
if (isNativeBindingError(err)) {
|
|
110
|
+
// Record BEFORE the cooldown check: the hint is cosmetic and rate-limited,
|
|
111
|
+
// the marker is the heal trigger. Gating the marker on the hint would mean a
|
|
112
|
+
// silenced hint also silences the repair — exactly the 4-day outage shape.
|
|
113
|
+
if (runtimeDir) recordNativeBindingBreakage(runtimeDir, { reason: err.message, event, now });
|
|
97
114
|
// Key the cooldown on the fault identity so a DISTINCT native failure within
|
|
98
115
|
// the window still surfaces (a second ABI mismatch after a partial rebuild, a
|
|
99
116
|
// corrupt .node) instead of being silenced by a prior, different DLOPEN. (#8/#15)
|
|
100
117
|
if (runtimeDir && !nativeBindingHintDue(runtimeDir, now, NATIVE_BINDING_HINT_COOLDOWN_MS, errKey(err.message))) return null;
|
|
101
118
|
return `[claude-mem-lite] [${ts}] [WARN] ${event}: native DB binding can't load ` +
|
|
102
|
-
`(likely a Node version change) — auto-heals
|
|
119
|
+
`(likely a Node version change) — auto-heals at the next session start, or run now: ${CLI_REBUILD_BINDING}`;
|
|
103
120
|
}
|
|
104
121
|
return `[claude-mem-lite] [${ts}] [ERROR] ${event}: ${err && err.message}`;
|
|
105
122
|
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Record the "native binding is unusable" state for the launcher's session-start
|
|
126
|
+
* heal. Overwrites: the newest fault is the one worth repairing. Best-effort —
|
|
127
|
+
* a hook must never fail because a marker could not be written.
|
|
128
|
+
*
|
|
129
|
+
* @param {string} runtimeDir
|
|
130
|
+
* @param {{reason?: string, event?: string, now?: number}} [opts]
|
|
131
|
+
*/
|
|
132
|
+
export function recordNativeBindingBreakage(runtimeDir, { reason = '', event = '', now = Date.now() } = {}) {
|
|
133
|
+
try {
|
|
134
|
+
mkdirSync(runtimeDir, { recursive: true });
|
|
135
|
+
const marker = join(runtimeDir, NATIVE_BINDING_BROKEN_MARKER);
|
|
136
|
+
const tmp = `${marker}.tmp-${process.pid}`;
|
|
137
|
+
// First line only: the ABI error is multi-line and the marker is read by the
|
|
138
|
+
// launcher (pure node:, no parser beyond JSON.parse) and by `doctor`.
|
|
139
|
+
writeFileSync(tmp, JSON.stringify({ reason: String(reason).split('\n')[0], event, ts: now }));
|
|
140
|
+
renameSync(tmp, marker);
|
|
141
|
+
} catch { /* best-effort */ }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* @param {string} runtimeDir
|
|
146
|
+
* @returns {{reason?: string, event?: string, ts?: number} | null} null when
|
|
147
|
+
* absent, unreadable or torn — a garbage marker must never throw into a hook.
|
|
148
|
+
*/
|
|
149
|
+
export function readNativeBindingBreakage(runtimeDir) {
|
|
150
|
+
try {
|
|
151
|
+
const parsed = JSON.parse(readFileSync(join(runtimeDir, NATIVE_BINDING_BROKEN_MARKER), 'utf8'));
|
|
152
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
153
|
+
} catch {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Idempotent. @param {string} runtimeDir */
|
|
159
|
+
export function clearNativeBindingBreakage(runtimeDir) {
|
|
160
|
+
try { unlinkSync(join(runtimeDir, NATIVE_BINDING_BROKEN_MARKER)); } catch { /* already gone */ }
|
|
161
|
+
}
|
package/lib/stats-quality.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Splits pure data aggregation from text rendering so MCP handlers don't
|
|
4
4
|
// collide with CLI's `out()` stdout-write pattern.
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { buildNotLowSignalSql } from './low-signal-patterns.mjs';
|
|
7
7
|
import { truncate } from '../format-utils.mjs';
|
|
8
8
|
import { COMPRESSED_PENDING_PURGE } from '../utils.mjs';
|
|
9
9
|
|
|
@@ -27,9 +27,11 @@ export function computeQualityStats(db, { project, days }) {
|
|
|
27
27
|
const baseParams = project ? [project] : [];
|
|
28
28
|
const cutoff = Date.now() - days * 86400000;
|
|
29
29
|
|
|
30
|
-
// LOW_SIGNAL match = NOT notLowSignal.
|
|
31
|
-
//
|
|
32
|
-
|
|
30
|
+
// LOW_SIGNAL match = NOT notLowSignal. Pure title-only builder: this is a
|
|
31
|
+
// METRIC counting pattern-titled rows ("Low-signal titles" in stats output),
|
|
32
|
+
// not a retrieval filter — the lesson-escape variant would silently exclude
|
|
33
|
+
// lesson-bearing rows from the count and understate title degradation.
|
|
34
|
+
const lowSignalIsMatchExpr = `NOT ${buildNotLowSignalSql('')}`;
|
|
33
35
|
|
|
34
36
|
// Narrative-text proxy for bugfix investigations that never landed a fix.
|
|
35
37
|
const unresolvedNarrativeExpr = `(
|
package/mem-cli.mjs
CHANGED
|
@@ -35,11 +35,14 @@ import { auditMemdir, memdirPath } from './memdir.mjs';
|
|
|
35
35
|
import { aggregateProjectCiteRecall } from './lib/citation-tracker.mjs';
|
|
36
36
|
import { probeOtherSources as probeIdSources, bucketIdTokens, splitDeferredTokens } from './lib/id-routing.mjs';
|
|
37
37
|
import { join, sep, dirname } from 'path';
|
|
38
|
+
import { spawnSync } from 'child_process';
|
|
38
39
|
import { readFileSync, existsSync, readdirSync } from 'fs';
|
|
39
40
|
|
|
40
41
|
// v2.41: shared CLI helpers extracted to cli/common.mjs. Keep this file as the
|
|
41
42
|
// router + remaining-command bodies during the incremental split. Future work:
|
|
42
43
|
// move each cmdXxx into its own cli/<cmd>.mjs; mem-cli.mjs becomes pure dispatch.
|
|
44
|
+
import { isNativeBindingError, healAndReexec } from './lib/binding-probe.mjs';
|
|
45
|
+
import { CLI_PATH, CLI_INVOKE } from './cli-path.mjs';
|
|
43
46
|
import { parseArgs, out, fail, relativeTime, fmtDateShort, parseIdToken, formatProbeHints, rejectBareStringFlags, resolvePositionalAlias, suggestUnknownFlags, OBS_TIME_FIELDS, formatObsFieldValue } from './cli/common.mjs';
|
|
44
47
|
import { saveObservation } from './lib/save-observation.mjs';
|
|
45
48
|
import { rebuildObservationDerived, normalizeScope, insertObservationVector } from './lib/observation-write.mjs';
|
|
@@ -3204,6 +3207,37 @@ export async function run(argv) {
|
|
|
3204
3207
|
// previously threw here with no auto-repair until the next MCP start.
|
|
3205
3208
|
db = ensureDbWithWalRecovery({ warn: (m) => process.stderr.write(`[mem] ${m}\n`) });
|
|
3206
3209
|
} catch (e) {
|
|
3210
|
+
// A Node upgrade leaves better_sqlite3.node compiled for the old ABI, and
|
|
3211
|
+
// every DB-touching path fails at once. Pre-v3.60 this printed the raw
|
|
3212
|
+
// multi-line NODE_MODULE_VERSION error with no repair named, and the only
|
|
3213
|
+
// healer was an MCP server start the user might never perform — the shape of
|
|
3214
|
+
// the 4-day outage on 2026-08-13. Heal in place, then RE-EXEC: this process
|
|
3215
|
+
// has already dlopen'd the stale binary, so it cannot use the new one.
|
|
3216
|
+
if (isNativeBindingError(e)) {
|
|
3217
|
+
const healed = await healAndReexec({
|
|
3218
|
+
// Delegate the actual rebuild to `cli.mjs rebuild-binding` so there is
|
|
3219
|
+
// ONE healer: it takes install.lock, resolves which node_modules tree
|
|
3220
|
+
// the running code uses, and clears the hooks' breakage marker.
|
|
3221
|
+
ensure: async () => {
|
|
3222
|
+
// Child stdout is DISCARDED, not inherited: install.mjs logs progress
|
|
3223
|
+
// to stdout, and this CLI's stdout is a data channel (`search --json`
|
|
3224
|
+
// is piped into jq). Progress still reaches the user via stderr.
|
|
3225
|
+
const r = spawnSync(process.execPath, [CLI_PATH, 'rebuild-binding'], {
|
|
3226
|
+
stdio: ['ignore', 'ignore', 'inherit'],
|
|
3227
|
+
timeout: 300_000,
|
|
3228
|
+
});
|
|
3229
|
+
return r.status === 0
|
|
3230
|
+
? { ok: true, action: 'rebuilt' }
|
|
3231
|
+
: { ok: false, error: `rebuild-binding exited ${r.status ?? 'on signal'}` };
|
|
3232
|
+
},
|
|
3233
|
+
log: (m) => process.stderr.write(`[mem] ${m}\n`),
|
|
3234
|
+
});
|
|
3235
|
+
if (healed.healed) { process.exitCode = healed.exitCode; return; }
|
|
3236
|
+
out(`[mem] Error: native DB binding unusable on Node ${process.version}${healed.error ? ` — ${healed.error}` : ''}`);
|
|
3237
|
+
out(`[mem] Fix: ${CLI_INVOKE} rebuild-binding`);
|
|
3238
|
+
process.exitCode = 1;
|
|
3239
|
+
return;
|
|
3240
|
+
}
|
|
3207
3241
|
out(`[mem] Error: Cannot open database: ${e.message}`);
|
|
3208
3242
|
out(`[mem] DB path: ${DB_PATH}`);
|
|
3209
3243
|
process.exitCode = 1;
|
package/memdir.mjs
CHANGED
|
@@ -304,9 +304,30 @@ export function removePluginDoc(memdir, slug) {
|
|
|
304
304
|
// noise.
|
|
305
305
|
|
|
306
306
|
const AUDIT_FILE_RE = /^(feedback|project)_[A-Za-z0-9_-]+\.md$/;
|
|
307
|
+
// Legacy skip prefixes: user_*/reference_* have no Why/How requirement. A known
|
|
308
|
+
// filename prefix wins over frontmatter type (a user_*.md stays excluded even if
|
|
309
|
+
// its frontmatter says feedback).
|
|
310
|
+
const SKIP_FILE_RE = /^(user|reference)_[A-Za-z0-9_-]+\.md$/;
|
|
307
311
|
const WHY_RE = /^\s*\*\*Why:\*\*/m;
|
|
308
312
|
const HOW_RE = /^\s*\*\*How to apply:\*\*/m;
|
|
309
313
|
|
|
314
|
+
/**
|
|
315
|
+
* Extract the memory type from a file's YAML frontmatter (2026-07-24 audit P2).
|
|
316
|
+
* The current CC harness writes kebab-case filenames (ship-runbook.md) and puts
|
|
317
|
+
* the type in frontmatter — either top-level `type: X` or nested under
|
|
318
|
+
* `metadata:` as ` type: X`. Anchored `^\s*type:` cannot match `node_type:`.
|
|
319
|
+
*
|
|
320
|
+
* @param {string} raw Full file content
|
|
321
|
+
* @returns {string|null} Lowercased type, or null when absent/no frontmatter
|
|
322
|
+
*/
|
|
323
|
+
function frontmatterType(raw) {
|
|
324
|
+
if (!raw.startsWith('---\n') && !raw.startsWith('---\r\n')) return null;
|
|
325
|
+
const m = raw.match(/^---\r?\n([\s\S]*?)\r?\n---(\r?\n|$)/);
|
|
326
|
+
if (!m) return null;
|
|
327
|
+
const t = m[1].match(/^\s*type:\s*([A-Za-z]+)\s*$/m);
|
|
328
|
+
return t ? t[1].toLowerCase() : null;
|
|
329
|
+
}
|
|
330
|
+
|
|
310
331
|
/**
|
|
311
332
|
* Strip the leading YAML frontmatter block (between `---` fences) so audit
|
|
312
333
|
* checks run only against body content. Returns input unchanged if no
|
|
@@ -339,13 +360,29 @@ export function auditMemdir(memdir) {
|
|
|
339
360
|
let entries;
|
|
340
361
|
try { entries = readdirSync(memdir); } catch { return result; }
|
|
341
362
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
363
|
+
// Two selection paths (2026-07-24 audit P2):
|
|
364
|
+
// 1. legacy filename prefix — feedback_*/project_* audited, user_*/reference_* skipped
|
|
365
|
+
// 2. kebab-case (current harness) — frontmatter type ∈ {feedback, project}
|
|
366
|
+
// Path 2 needs the file content; read once and reuse for the body check.
|
|
367
|
+
const candidates = entries
|
|
368
|
+
.filter(n => n.endsWith('.md') && n !== 'MEMORY.md' && !n.startsWith('.'))
|
|
369
|
+
.sort();
|
|
370
|
+
let total = 0;
|
|
371
|
+
for (const name of candidates) {
|
|
372
|
+
const legacyAudit = AUDIT_FILE_RE.test(name);
|
|
373
|
+
if (!legacyAudit && SKIP_FILE_RE.test(name)) continue;
|
|
374
|
+
|
|
375
|
+
let raw = null;
|
|
376
|
+
try { raw = readFileSync(join(memdir, name), 'utf8'); } catch { /* unreadable */ }
|
|
377
|
+
|
|
378
|
+
if (!legacyAudit) {
|
|
379
|
+
// Frontmatter decides; unreadable or untyped files carry no Why/How contract.
|
|
380
|
+
const type = raw === null ? null : frontmatterType(raw);
|
|
381
|
+
if (type !== 'feedback' && type !== 'project') continue;
|
|
382
|
+
}
|
|
383
|
+
// Legacy-prefixed unreadable file keeps the old behavior: counted as missingBoth.
|
|
384
|
+
const body = raw === null ? '' : stripFrontmatter(raw);
|
|
385
|
+
total += 1;
|
|
349
386
|
|
|
350
387
|
const hasWhy = WHY_RE.test(body);
|
|
351
388
|
const hasHow = HOW_RE.test(body);
|
|
@@ -354,6 +391,6 @@ export function auditMemdir(memdir) {
|
|
|
354
391
|
else if (!hasWhy) result.missingWhy.push(name);
|
|
355
392
|
else result.missingHowToApply.push(name);
|
|
356
393
|
}
|
|
357
|
-
result.total =
|
|
394
|
+
result.total = total;
|
|
358
395
|
return result;
|
|
359
396
|
}
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.60.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "claude-mem-lite",
|
|
9
|
-
"version": "3.
|
|
9
|
+
"version": "3.60.0",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
12
12
|
"better-sqlite3": "^12.6.2",
|
|
@@ -1570,22 +1570,6 @@
|
|
|
1570
1570
|
}
|
|
1571
1571
|
}
|
|
1572
1572
|
},
|
|
1573
|
-
"node_modules/ajv/node_modules/fast-uri": {
|
|
1574
|
-
"version": "3.1.4",
|
|
1575
|
-
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz",
|
|
1576
|
-
"integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==",
|
|
1577
|
-
"funding": [
|
|
1578
|
-
{
|
|
1579
|
-
"type": "github",
|
|
1580
|
-
"url": "https://github.com/sponsors/fastify"
|
|
1581
|
-
},
|
|
1582
|
-
{
|
|
1583
|
-
"type": "opencollective",
|
|
1584
|
-
"url": "https://opencollective.com/fastify"
|
|
1585
|
-
}
|
|
1586
|
-
],
|
|
1587
|
-
"license": "BSD-3-Clause"
|
|
1588
|
-
},
|
|
1589
1573
|
"node_modules/assertion-error": {
|
|
1590
1574
|
"version": "2.0.1",
|
|
1591
1575
|
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
|
@@ -2401,6 +2385,22 @@
|
|
|
2401
2385
|
"dev": true,
|
|
2402
2386
|
"license": "MIT"
|
|
2403
2387
|
},
|
|
2388
|
+
"node_modules/fast-uri": {
|
|
2389
|
+
"version": "3.1.5",
|
|
2390
|
+
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz",
|
|
2391
|
+
"integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==",
|
|
2392
|
+
"funding": [
|
|
2393
|
+
{
|
|
2394
|
+
"type": "github",
|
|
2395
|
+
"url": "https://github.com/sponsors/fastify"
|
|
2396
|
+
},
|
|
2397
|
+
{
|
|
2398
|
+
"type": "opencollective",
|
|
2399
|
+
"url": "https://opencollective.com/fastify"
|
|
2400
|
+
}
|
|
2401
|
+
],
|
|
2402
|
+
"license": "BSD-3-Clause"
|
|
2403
|
+
},
|
|
2404
2404
|
"node_modules/fd-package-json": {
|
|
2405
2405
|
"version": "2.0.0",
|
|
2406
2406
|
"resolved": "https://registry.npmjs.org/fd-package-json/-/fd-package-json-2.0.0.tgz",
|
|
@@ -2687,9 +2687,9 @@
|
|
|
2687
2687
|
}
|
|
2688
2688
|
},
|
|
2689
2689
|
"node_modules/hono": {
|
|
2690
|
-
"version": "4.
|
|
2691
|
-
"resolved": "https://registry.npmjs.org/hono/-/hono-4.
|
|
2692
|
-
"integrity": "sha512-
|
|
2690
|
+
"version": "4.13.2",
|
|
2691
|
+
"resolved": "https://registry.npmjs.org/hono/-/hono-4.13.2.tgz",
|
|
2692
|
+
"integrity": "sha512-JydRilDRkYBQMt9qR9U92mXxmbGqsqSn/IKOrh4e7/gEbn+0zSr8igTu0obwJoNGN4sez28DIql7FBHWydoJpA==",
|
|
2693
2693
|
"license": "MIT",
|
|
2694
2694
|
"engines": {
|
|
2695
2695
|
"node": ">=16.9.0"
|
|
@@ -2791,9 +2791,9 @@
|
|
|
2791
2791
|
"license": "ISC"
|
|
2792
2792
|
},
|
|
2793
2793
|
"node_modules/ip-address": {
|
|
2794
|
-
"version": "10.
|
|
2795
|
-
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.
|
|
2796
|
-
"integrity": "sha512
|
|
2794
|
+
"version": "10.5.0",
|
|
2795
|
+
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.5.0.tgz",
|
|
2796
|
+
"integrity": "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==",
|
|
2797
2797
|
"license": "MIT",
|
|
2798
2798
|
"engines": {
|
|
2799
2799
|
"node": ">= 12"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.60.0",
|
|
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",
|
package/scoring-sql.mjs
CHANGED
|
@@ -152,8 +152,17 @@ export function noisePenaltyClause(alias = 'o') {
|
|
|
152
152
|
// The SQL path (this), the regex path (utils.mjs::LOW_SIGNAL_TITLE), and the
|
|
153
153
|
// pre-tool-recall.js inline SQL now all derive from one authoritative
|
|
154
154
|
// pattern list. Previously hand-mirrored with "keep in sync" comments.
|
|
155
|
+
//
|
|
156
|
+
// lessonEscape (2026-07-24 audit P1, D#11): every consumer of THIS clause is an
|
|
157
|
+
// observations-table retrieval surface (search, recall, error-recall, context/
|
|
158
|
+
// handoff/UPS injection, optimize candidates), so all get the read-side lesson
|
|
159
|
+
// escape — a low-signal TITLE no longer hides a row with a real lesson_learned.
|
|
160
|
+
// Consumers that must stay title-only import buildNotLowSignalSql directly:
|
|
161
|
+
// events-table queries (lib/activity.mjs, pre-tool-recall.js events fallback —
|
|
162
|
+
// no lesson_learned column) and noise-title metrics (lib/stats-core.mjs,
|
|
163
|
+
// lib/stats-quality.mjs — they COUNT pattern-titled rows, not filter them).
|
|
155
164
|
export function notLowSignalTitleClause(alias = 'o') {
|
|
156
|
-
return buildNotLowSignalSql(alias);
|
|
165
|
+
return buildNotLowSignalSql(alias, { lessonEscape: true });
|
|
157
166
|
}
|
|
158
167
|
|
|
159
168
|
// ─── Cite-history factor (A1, v2.83) ────────────────────────────────────────
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
// install.
|
|
26
26
|
|
|
27
27
|
import { existsSync, mkdirSync, writeFileSync, statSync, unlinkSync, readFileSync } from 'node:fs';
|
|
28
|
-
import { spawnSync } from 'node:child_process';
|
|
28
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
29
29
|
import { dirname, join, isAbsolute } from 'node:path';
|
|
30
30
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
31
31
|
import { homedir } from 'node:os';
|
|
@@ -47,6 +47,34 @@ const HEAL_COOLDOWN_MS = 6 * 60 * 60 * 1000;
|
|
|
47
47
|
// the intentional silence (no stack trace per fire) stays detectable. (#4/#8)
|
|
48
48
|
const BROKEN_MARKER = join(RUNTIME_DIR, 'hook-launcher-broken');
|
|
49
49
|
|
|
50
|
+
// ── Native-binding (ABI) self-heal ──────────────────────────────────────────
|
|
51
|
+
// A stale better_sqlite3.node after a Node upgrade does NOT throw at import time
|
|
52
|
+
// — better-sqlite3 dlopen's it lazily at the first `new Database()`, deep inside
|
|
53
|
+
// the hook script, whose own catch swallows it. So it never reaches the
|
|
54
|
+
// ERR_MODULE_NOT_FOUND path below.
|
|
55
|
+
//
|
|
56
|
+
// scripts/setup.sh has probed + rebuilt the binding at SessionStart since v3.58,
|
|
57
|
+
// but ONLY on plugin-manifest installs: hooks/hooks.json registers setup.sh,
|
|
58
|
+
// while an install.mjs-managed settings.json does NOT — it wires the launcher
|
|
59
|
+
// alone. On that install shape nothing healed. Field result (2026-08-13): 4 days
|
|
60
|
+
// with a dead memory system, 79 failed fires in one day.
|
|
61
|
+
//
|
|
62
|
+
// The hook scripts now drop a marker on every such fire (via
|
|
63
|
+
// lib/hook-telemetry.mjs and lib/native-binding-hint.mjs); this heals from it at
|
|
64
|
+
// SESSION-START only — never on the per-tool hot path, where an npm run would
|
|
65
|
+
// stall the user's edit.
|
|
66
|
+
// Marker dir mirrors the standalone hook scripts (pre-tool-recall /
|
|
67
|
+
// pre-skill-bridge), which honor CLAUDE_MEM_RUNTIME_DIR — they write 78 of every
|
|
68
|
+
// 79 of these markers, so reading a different dir would mean never healing.
|
|
69
|
+
const NB_RUNTIME_DIR = process.env.CLAUDE_MEM_RUNTIME_DIR || RUNTIME_DIR;
|
|
70
|
+
const NB_BROKEN_MARKER = join(NB_RUNTIME_DIR, 'native-binding-broken');
|
|
71
|
+
const NB_HEAL_MARKER = join(NB_RUNTIME_DIR, 'native-binding-lastheal');
|
|
72
|
+
// Literal, not imported: the pure-`node:` charter above forbids importing lib/
|
|
73
|
+
// here (this file must survive a broken install). Kept in sync with
|
|
74
|
+
// lib/binding-probe.mjs::NATIVE_BINDING_REBUILD_CMD, which is the single home
|
|
75
|
+
// everywhere the charter allows an import.
|
|
76
|
+
const NB_MANUAL_CMD = 'npm rebuild better-sqlite3 --dangerously-allow-all-scripts';
|
|
77
|
+
|
|
50
78
|
// Resolvable invocation of the bundled CLI's repair path. Absolute via
|
|
51
79
|
// INSTALL_DIR (import.meta.url) so it works on a plugin-only install, where
|
|
52
80
|
// bare `claude-mem-lite` is not on PATH and ~/.claude-mem-lite/ holds no source.
|
|
@@ -248,6 +276,62 @@ async function attemptHeal(reason) {
|
|
|
248
276
|
// falls through to the normal entry import. The dynamic import keeps this
|
|
249
277
|
// launcher's pure-`node:` static-import charter intact (it must survive a broken
|
|
250
278
|
// install even if hook-update.mjs is unimportable).
|
|
279
|
+
// → the function this describes is trySyncDataDirFromCache(), below.
|
|
280
|
+
|
|
281
|
+
// Rebuild the native binding when a prior fire recorded it as unusable.
|
|
282
|
+
//
|
|
283
|
+
// DETACHED, never awaited. This hook runs under a 15s Claude Code cap
|
|
284
|
+
// (hooks/hooks.json) while a rebuild can take far longer — prebuild-install has
|
|
285
|
+
// to fetch, and a node-gyp fallback is minutes. Waiting would trade a broken
|
|
286
|
+
// binding for a SIGKILL'd session-start (no memory context at all) plus a
|
|
287
|
+
// half-written .node, the exact hazard scripts/setup.sh's 20s exec cap documents.
|
|
288
|
+
// Detaching costs one fire: the rebuild lands within seconds and the NEXT hook
|
|
289
|
+
// fire — usually the same session's first PreToolUse — is already healthy.
|
|
290
|
+
// stdio is fully ignored: install.mjs logs to STDOUT, and SessionStart stdout is
|
|
291
|
+
// a JSON envelope Claude Code parses, so inheriting it corrupts the fire.
|
|
292
|
+
//
|
|
293
|
+
// Bounded by its own 6h cooldown so an unfixable case (no prebuild for this Node,
|
|
294
|
+
// no compiler, offline) does not re-spawn npm every session. The cooldown is
|
|
295
|
+
// dropped once the binding is confirmed healthy, so a LATER unrelated break heals
|
|
296
|
+
// immediately instead of waiting out a stale window.
|
|
297
|
+
// Best-effort throughout — a heal failure must never stop the hook fire.
|
|
298
|
+
function healNativeBindingIfBroken() {
|
|
299
|
+
try {
|
|
300
|
+
if (!existsSync(NB_BROKEN_MARKER)) {
|
|
301
|
+
// Healthy (or already healed by the child) → reset the cooldown.
|
|
302
|
+
try { unlinkSync(NB_HEAL_MARKER); } catch { /* nothing to reset */ }
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
try {
|
|
306
|
+
if (Date.now() - statSync(NB_HEAL_MARKER).mtimeMs < HEAL_COOLDOWN_MS) return;
|
|
307
|
+
} catch { /* no marker → not on cooldown */ }
|
|
308
|
+
try {
|
|
309
|
+
mkdirSync(NB_RUNTIME_DIR, { recursive: true });
|
|
310
|
+
writeFileSync(NB_HEAL_MARKER, String(Date.now()));
|
|
311
|
+
} catch { /* best-effort */ }
|
|
312
|
+
|
|
313
|
+
const installer = join(INSTALL_DIR, 'install.mjs');
|
|
314
|
+
if (!existsSync(installer)) {
|
|
315
|
+
process.stderr.write(
|
|
316
|
+
`[claude-mem-lite] native DB binding unusable and install.mjs is missing — run: cd "${INSTALL_DIR}" && ${NB_MANUAL_CMD}\n`,
|
|
317
|
+
);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
// The CHILD clears the breakage marker, and only on a verified-good rebuild
|
|
321
|
+
// (install.mjs::rebuildBinding). Clearing it here would mean a rebuild that
|
|
322
|
+
// silently did nothing — lock contention, a no-op npm — still reads as
|
|
323
|
+
// "healed", dropping the cooldown and re-spawning npm on every session.
|
|
324
|
+
process.stderr.write(
|
|
325
|
+
'[claude-mem-lite] native DB binding unusable (Node version change?) — rebuilding in the background\n',
|
|
326
|
+
);
|
|
327
|
+
const child = spawn(process.execPath, [installer, 'rebuild-binding'], {
|
|
328
|
+
detached: true,
|
|
329
|
+
stdio: 'ignore',
|
|
330
|
+
});
|
|
331
|
+
child.unref();
|
|
332
|
+
} catch { /* best-effort — never block the hook fire */ }
|
|
333
|
+
}
|
|
334
|
+
|
|
251
335
|
async function trySyncDataDirFromCache() {
|
|
252
336
|
try {
|
|
253
337
|
const { syncDataDirFromCache } = await import(
|
|
@@ -257,15 +341,27 @@ async function trySyncDataDirFromCache() {
|
|
|
257
341
|
} catch { /* best-effort — proceed to the normal entry regardless */ }
|
|
258
342
|
}
|
|
259
343
|
|
|
260
|
-
|
|
344
|
+
const IS_SESSION_START = rest.includes('session-start');
|
|
345
|
+
|
|
346
|
+
if (IS_SESSION_START) {
|
|
347
|
+
// Before the entry: this process has not dlopen'd better-sqlite3 yet, so the
|
|
348
|
+
// freshly built .node is picked up by the very fire that follows. (After a
|
|
349
|
+
// failed dlopen, only a NEW process can load the replacement — the module
|
|
350
|
+
// handle is cached and an in-process retry dies with "did not self-register".)
|
|
351
|
+
healNativeBindingIfBroken();
|
|
261
352
|
await trySyncDataDirFromCache();
|
|
262
353
|
}
|
|
263
354
|
|
|
355
|
+
|
|
264
356
|
try {
|
|
265
357
|
await runEntry();
|
|
266
358
|
// A clean session-start fire confirms the install is healthy → clear any stale
|
|
267
359
|
// breakage marker. Gated to session-start so the per-tool hot path pays nothing.
|
|
268
|
-
if (
|
|
360
|
+
if (IS_SESSION_START) clearBreakage();
|
|
361
|
+
// After the entry too: the fire that DISCOVERS the breakage is the one that
|
|
362
|
+
// records it, so a pre-entry-only check would leave the whole session dead and
|
|
363
|
+
// heal one session late.
|
|
364
|
+
if (IS_SESSION_START) healNativeBindingIfBroken();
|
|
269
365
|
} catch (e) {
|
|
270
366
|
if (!isLocalModuleErr(e)) throw e;
|
|
271
367
|
const reason = describeFailure(e);
|