claude-mem-lite 3.70.2 → 3.71.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/hook.mjs +7 -2
- package/lib/hook-stdout.mjs +55 -10
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/scripts/binding-probe-cli.mjs +26 -5
- package/scripts/launch.mjs +26 -3
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "claude-mem-lite",
|
|
13
|
-
"version": "3.
|
|
13
|
+
"version": "3.71.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.71.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/hook.mjs
CHANGED
|
@@ -48,7 +48,7 @@ import { handleLLMEpisode, handleLLMSummary, saveObservation, buildImmediateObse
|
|
|
48
48
|
import { scrubRecord } from './lib/scrub-record.mjs';
|
|
49
49
|
import { formatHookError } from './lib/native-binding-hint.mjs';
|
|
50
50
|
import { recordHookError } from './lib/hook-telemetry.mjs';
|
|
51
|
-
import { queueHookContext, flushHookStdout } from './lib/hook-stdout.mjs';
|
|
51
|
+
import { queueHookContext, queueHookSystemMessage, flushHookStdout } from './lib/hook-stdout.mjs';
|
|
52
52
|
import { selectCompressionCandidates, groupByProjectWeek, compressGroup } from './lib/compress-core.mjs';
|
|
53
53
|
import { cleanupBroken, decayAndMarkIdle, boostAccessed, selectFuzzyDedupeIds, hardDeleteCandidateCount, purgeStale, recoverOrphanedChildren, recoverBuriedLessons, sweepDeferredWorkOrphans } from './lib/maintain-core.mjs';
|
|
54
54
|
import { snapshotDb } from './lib/db-backup.mjs';
|
|
@@ -1549,7 +1549,12 @@ async function handleSessionStart() {
|
|
|
1549
1549
|
let updateCheckDue = false;
|
|
1550
1550
|
try {
|
|
1551
1551
|
const banner = getCachedUpdateBanner();
|
|
1552
|
-
|
|
1552
|
+
// The human channel, not additionalContext: "vX available" is a notice for the
|
|
1553
|
+
// USER. Folding it into additionalContext under suppressOutput:true kept its
|
|
1554
|
+
// content and lost its audience. Claude Code renders a command hook's top-level
|
|
1555
|
+
// systemMessage as its own hook_system_message, independent of the context
|
|
1556
|
+
// block — see lib/hook-stdout.mjs for the bundle evidence.
|
|
1557
|
+
if (banner) queueHookSystemMessage(String(banner));
|
|
1553
1558
|
updateCheckDue = isUpdateCheckDue();
|
|
1554
1559
|
} catch (e) { debugCatch(e, 'session-start-update'); }
|
|
1555
1560
|
|
package/lib/hook-stdout.mjs
CHANGED
|
@@ -26,15 +26,17 @@
|
|
|
26
26
|
|
|
27
27
|
let parts = [];
|
|
28
28
|
let queuedEvent = null;
|
|
29
|
+
let systemParts = [];
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* Queue a contribution to this process's single stdout envelope.
|
|
32
33
|
*
|
|
33
34
|
* @param {string} hookEventName Event name for hookSpecificOutput.
|
|
34
35
|
* @param {string} text additionalContext contribution; empty/blank is ignored.
|
|
36
|
+
* @param {{warn?: (msg: string) => void}} [deps]
|
|
35
37
|
* @returns {void}
|
|
36
38
|
*/
|
|
37
|
-
export function queueHookContext(hookEventName, text) {
|
|
39
|
+
export function queueHookContext(hookEventName, text, deps = {}) {
|
|
38
40
|
if (!hookEventName) return;
|
|
39
41
|
const body = String(text ?? '').trim();
|
|
40
42
|
if (!body) return;
|
|
@@ -42,11 +44,46 @@ export function queueHookContext(hookEventName, text) {
|
|
|
42
44
|
// hookSpecificOutput.hookEventName does not match the event it dispatched.
|
|
43
45
|
// In practice one process serves one event; keep the first and drop the
|
|
44
46
|
// stragglers rather than emit an envelope the host rejects outright.
|
|
45
|
-
|
|
47
|
+
//
|
|
48
|
+
// The drop is NOISY on purpose. It is unreachable today (all call sites are
|
|
49
|
+
// event-consistent), but flushEpisode's hookEventName DEFAULTS to 'PostToolUse',
|
|
50
|
+
// so a future caller that omits the argument would both mis-tag its receipt and
|
|
51
|
+
// have it swallowed without a trace. Silently vanishing work is this repo's
|
|
52
|
+
// most-repeated defect class; stderr is safe here because the host never parses it
|
|
53
|
+
// as the envelope.
|
|
54
|
+
if (queuedEvent && queuedEvent !== hookEventName) {
|
|
55
|
+
const warn = deps.warn || ((m) => { try { process.stderr.write(m); } catch { /* never block on a warning */ } });
|
|
56
|
+
warn(`[claude-mem-lite] hook-stdout: dropped a ${hookEventName} contribution — this process `
|
|
57
|
+
+ `already queued ${queuedEvent}, and one envelope carries exactly one hookEventName. `
|
|
58
|
+
+ 'This is a wiring bug: the contribution is lost.\n');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
46
61
|
queuedEvent = hookEventName;
|
|
47
62
|
parts.push(body);
|
|
48
63
|
}
|
|
49
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Queue a line for the HUMAN, not the model.
|
|
67
|
+
*
|
|
68
|
+
* Claude Code renders a command hook's top-level `systemMessage` as its own
|
|
69
|
+
* `hook_system_message` conversation message, independently of
|
|
70
|
+
* `hookSpecificOutput.additionalContext` — verified in the 2.1.234 bundle
|
|
71
|
+
* (`if (G.systemMessage) { … yield { message: yc({ type: "hook_system_message", … }) } }`)
|
|
72
|
+
* and documented there as "Display a message to the user (all hooks)". One envelope
|
|
73
|
+
* can therefore carry context for the model AND a notice for the user.
|
|
74
|
+
*
|
|
75
|
+
* Needed because v3.70.0's merge folded the update banner into additionalContext with
|
|
76
|
+
* `suppressOutput: true`, which kept its content and lost its audience.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} text Notice for the user; empty/blank is ignored.
|
|
79
|
+
* @returns {void}
|
|
80
|
+
*/
|
|
81
|
+
export function queueHookSystemMessage(text) {
|
|
82
|
+
const body = String(text ?? '').trim();
|
|
83
|
+
if (!body) return;
|
|
84
|
+
systemParts.push(body);
|
|
85
|
+
}
|
|
86
|
+
|
|
50
87
|
/**
|
|
51
88
|
* Write the queued contributions as one envelope. Idempotent: a second call
|
|
52
89
|
* with nothing queued writes nothing, so calling it from both the dispatcher
|
|
@@ -56,18 +93,25 @@ export function queueHookContext(hookEventName, text) {
|
|
|
56
93
|
* @returns {boolean} true when an envelope was written.
|
|
57
94
|
*/
|
|
58
95
|
export function flushHookStdout(deps = {}) {
|
|
59
|
-
|
|
96
|
+
const hasContext = queuedEvent && parts.length > 0;
|
|
97
|
+
const hasSystem = systemParts.length > 0;
|
|
98
|
+
if (!hasContext && !hasSystem) return false;
|
|
60
99
|
const write = deps.write || ((s) => process.stdout.write(s));
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
100
|
+
const envelope = { suppressOutput: true };
|
|
101
|
+
if (hasSystem) envelope.systemMessage = systemParts.join('\n');
|
|
102
|
+
// Omitted entirely when there is no model-facing context: Stop's schema REJECTS a
|
|
103
|
+
// hookSpecificOutput block, and an envelope carrying only a user notice must not
|
|
104
|
+
// invent an event name to hang one on.
|
|
105
|
+
if (hasContext) {
|
|
106
|
+
envelope.hookSpecificOutput = {
|
|
64
107
|
hookEventName: queuedEvent,
|
|
65
108
|
additionalContext: parts.join('\n\n'),
|
|
66
|
-
}
|
|
67
|
-
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
68
111
|
parts = [];
|
|
69
112
|
queuedEvent = null;
|
|
70
|
-
|
|
113
|
+
systemParts = [];
|
|
114
|
+
write(JSON.stringify(envelope) + '\n');
|
|
71
115
|
return true;
|
|
72
116
|
}
|
|
73
117
|
|
|
@@ -75,9 +119,10 @@ export function flushHookStdout(deps = {}) {
|
|
|
75
119
|
export function resetHookStdout() {
|
|
76
120
|
parts = [];
|
|
77
121
|
queuedEvent = null;
|
|
122
|
+
systemParts = [];
|
|
78
123
|
}
|
|
79
124
|
|
|
80
125
|
/** Test seam: what is queued right now. */
|
|
81
126
|
export function peekHookStdout() {
|
|
82
|
-
return { hookEventName: queuedEvent, parts: [...parts] };
|
|
127
|
+
return { hookEventName: queuedEvent, parts: [...parts], systemParts: [...systemParts] };
|
|
83
128
|
}
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.71.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.71.0",
|
|
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.
|
|
3
|
+
"version": "3.71.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",
|
|
@@ -32,6 +32,15 @@ const ROOT = process.env.PROBE_ROOT || join(dirname(fileURLToPath(import.meta.ur
|
|
|
32
32
|
// broken flag, and a helperless broken tree is repaired by the hook-launcher
|
|
33
33
|
// path instead. Out of process like every other probe here — loading a stale
|
|
34
34
|
// .node caches a dead module handle for the rest of THIS process.
|
|
35
|
+
// Output-identical twin of lib/binding-probe.mjs::flattenBindingError, kept here
|
|
36
|
+
// because bareProbe runs when lib/ could not be imported. Same 240 cap, same
|
|
37
|
+
// ellipsis, same 'unknown' floor — asserted for parity by the tests.
|
|
38
|
+
function flattenLocal(err, max = 240) {
|
|
39
|
+
const s = String(err ?? '').replace(/\s+/g, ' ').trim();
|
|
40
|
+
if (!s) return 'unknown';
|
|
41
|
+
return s.length > max ? `${s.slice(0, max - 1)}…` : s;
|
|
42
|
+
}
|
|
43
|
+
|
|
35
44
|
function bareProbe(root) {
|
|
36
45
|
const script =
|
|
37
46
|
'try {'
|
|
@@ -49,11 +58,23 @@ function bareProbe(root) {
|
|
|
49
58
|
//
|
|
50
59
|
// Flattening is inlined, NOT lib/binding-probe.mjs::flattenBindingError, because
|
|
51
60
|
// this function is the fallback for a tree where lib/ failed to import — `helpers`
|
|
52
|
-
// is still null on every path that reaches here.
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
61
|
+
// is still null on every path that reaches here.
|
|
62
|
+
//
|
|
63
|
+
// The twin must stay byte-identical in OUTPUT, and it did not: the first draft
|
|
64
|
+
// capped with a bare `.slice(0, 240)` while the shared helper appends an ellipsis,
|
|
65
|
+
// so they already disagreed at the one boundary the duplication exists to protect.
|
|
66
|
+
// A comment is not a guard, and this repo's hand-maintained twins have drifted
|
|
67
|
+
// before. tests/binding-error-diagnosis.test.mjs now drives THIS path in a
|
|
68
|
+
// lib/-less tree and asserts parity with the shared helper.
|
|
69
|
+
// Order matters: flattenLocal floors to the string 'unknown', which is truthy, so
|
|
70
|
+
// `flattenLocal(x) || fallback` would make the fallbacks unreachable and swallow a
|
|
71
|
+
// spawn error or an exit code whenever the child printed nothing. Pick the source
|
|
72
|
+
// FIRST, then flatten it.
|
|
73
|
+
const printed = String(r.stdout || '').trim();
|
|
74
|
+
const spawnErr = r.error && r.error.message;
|
|
75
|
+
const why = printed ? flattenLocal(printed)
|
|
76
|
+
: spawnErr ? flattenLocal(spawnErr)
|
|
77
|
+
: `probe exited ${r.status ?? `on signal ${r.signal}`}`;
|
|
57
78
|
process.stderr.write(`[claude-mem-lite] binding probe: ${why}\n`);
|
|
58
79
|
return false;
|
|
59
80
|
}
|
package/scripts/launch.mjs
CHANGED
|
@@ -20,9 +20,32 @@ if (!existsSync(join(ROOT, 'node_modules', 'better-sqlite3'))) {
|
|
|
20
20
|
});
|
|
21
21
|
process.stderr.write('[claude-mem-lite] Dependencies installed\n');
|
|
22
22
|
} catch (e) {
|
|
23
|
-
// Plugin-cache / multi-user / disk-full installs can fail here
|
|
24
|
-
//
|
|
25
|
-
|
|
23
|
+
// Plugin-cache / multi-user / disk-full installs can fail here, and this is not a
|
|
24
|
+
// rare path: Claude Code materializes each new plugin-cache version WITHOUT
|
|
25
|
+
// node_modules, so the guard above opens on the first MCP launch after every
|
|
26
|
+
// plugin update. Without this catch the user sees a Node stack trace.
|
|
27
|
+
//
|
|
28
|
+
// `.split('\n')[0]` is CORRECT here, unlike the four binding-error sites fixed in
|
|
29
|
+
// v3.70.2, and the difference is the `stdio` above: stderr is **inherit**, so
|
|
30
|
+
// npm's own diagnosis (`npm error code EROFS`, `path …`, `rofs EROFS: read-only
|
|
31
|
+
// file system …`) has already streamed straight to the user's terminal by the time
|
|
32
|
+
// we get here — verified by running this file against an unwritable ROOT. With
|
|
33
|
+
// stderr inherited, execSync's `e.message` holds only "Command failed: <cmd>";
|
|
34
|
+
// there is no captured diagnosis to lose. Do NOT "fix" this by piping stderr to
|
|
35
|
+
// recover it: piping is what made a compiling better-sqlite3 look hung under the
|
|
36
|
+
// 5-min bash timeout (bug audit 2026-05), which is why stderr is inherited.
|
|
37
|
+
//
|
|
38
|
+
// A pre-tag review measured `e.message` under `stdio: 'pipe'`, where stderr IS
|
|
39
|
+
// folded into the message, and concluded this line drops the diagnosis. It does
|
|
40
|
+
// not — the stdio differs. Recorded here because the same wrong conclusion is
|
|
41
|
+
// easy to reach from the code alone.
|
|
42
|
+
//
|
|
43
|
+
// `e.status` not `e.code`: execSync failures carry the exit status on `status`,
|
|
44
|
+
// so the old `|| e.code` rung was dead.
|
|
45
|
+
const detail = e?.message?.split('\n')[0]
|
|
46
|
+
|| (e?.status != null ? `npm exited ${e.status}` : '')
|
|
47
|
+
|| (e?.signal ? `npm killed by ${e.signal}` : '')
|
|
48
|
+
|| 'unknown error';
|
|
26
49
|
process.stderr.write(`[claude-mem-lite] npm install failed in ${ROOT} — ${detail}\n`);
|
|
27
50
|
process.stderr.write(`[claude-mem-lite] Likely cause: read-only directory, disk full, or network blocked.\n`);
|
|
28
51
|
process.stderr.write(`[claude-mem-lite] Repair: cd "${ROOT}" && npm install --omit=dev\n`);
|