claude-mem-lite 3.9.0 → 3.10.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 +5 -1
- package/lib/citation-tracker.mjs +26 -14
- package/lib/search-core.mjs +19 -2
- package/mem-cli.mjs +43 -4
- package/package.json +1 -1
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "claude-mem-lite",
|
|
13
|
-
"version": "3.
|
|
13
|
+
"version": "3.10.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.10.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
|
@@ -553,7 +553,11 @@ async function handleStop() {
|
|
|
553
553
|
// Union closed by extractAllInjected — one integration point so the
|
|
554
554
|
// contract test in tests/citation-tracker-userprompt.test.mjs covers it.
|
|
555
555
|
try {
|
|
556
|
-
|
|
556
|
+
// mainOnly: the injected denominator must use the same thread
|
|
557
|
+
// filter as citedMain (the numerator, below) — an obs injected only
|
|
558
|
+
// inside a subagent (sidechain) would otherwise enter the denominator
|
|
559
|
+
// but never the numerator and streak-demote despite being used there.
|
|
560
|
+
const injected = extractAllInjected(transcriptPath, { mainOnly: true });
|
|
557
561
|
// P5 ①: cite-back signals — observations whose warned file the agent
|
|
558
562
|
// edited this session. Union into injected so they're resolved (they
|
|
559
563
|
// were injected via pre-tool-recall) and, below, into cited so the
|
package/lib/citation-tracker.mjs
CHANGED
|
@@ -182,8 +182,15 @@ function normalizeHookCommand(command) {
|
|
|
182
182
|
*
|
|
183
183
|
* @param {string|null|undefined} transcriptPath
|
|
184
184
|
* @param {(ctx: {command: string, text: string}) => void} fn
|
|
185
|
+
* @param {object} [opts]
|
|
186
|
+
* @param {boolean} [opts.mainOnly=false] If true, skip attachments on sidechain
|
|
187
|
+
* (subagent) transcript records. Mirrors extractCitationsFromTranscript's
|
|
188
|
+
* mainOnly so the citation-decay injected DENOMINATOR uses the same thread
|
|
189
|
+
* filter as the cited NUMERATOR — an obs injected only inside a subagent must
|
|
190
|
+
* not enter the denominator, else it streak-demotes despite being used there.
|
|
185
191
|
*/
|
|
186
|
-
function eachHookAttachment(transcriptPath, fn) {
|
|
192
|
+
function eachHookAttachment(transcriptPath, fn, opts = {}) {
|
|
193
|
+
const { mainOnly = false } = opts;
|
|
187
194
|
if (!transcriptPath || !existsSync(transcriptPath)) return;
|
|
188
195
|
let raw;
|
|
189
196
|
try { raw = readFileSync(transcriptPath, 'utf8'); } catch { return; }
|
|
@@ -192,6 +199,7 @@ function eachHookAttachment(transcriptPath, fn) {
|
|
|
192
199
|
let entry;
|
|
193
200
|
try { entry = JSON.parse(line); } catch { continue; }
|
|
194
201
|
if (entry.type !== 'attachment') continue;
|
|
202
|
+
if (mainOnly && entry.isSidechain === true) continue;
|
|
195
203
|
const att = entry.attachment;
|
|
196
204
|
if (!att || att.type !== 'hook_success') continue;
|
|
197
205
|
const stdout = att.stdout || '';
|
|
@@ -217,14 +225,14 @@ function eachHookAttachment(transcriptPath, fn) {
|
|
|
217
225
|
* @param {string|null|undefined} transcriptPath
|
|
218
226
|
* @returns {Set<number>} unique injected IDs (empty set on missing path/file)
|
|
219
227
|
*/
|
|
220
|
-
export function extractInjectedFromPreToolUse(transcriptPath) {
|
|
228
|
+
export function extractInjectedFromPreToolUse(transcriptPath, opts = {}) {
|
|
221
229
|
const ids = new Set();
|
|
222
230
|
eachHookAttachment(transcriptPath, ({ command, text }) => {
|
|
223
231
|
if (!command.includes('pre-tool-recall')) return;
|
|
224
232
|
INJECTED_RE.lastIndex = 0;
|
|
225
233
|
let m;
|
|
226
234
|
while ((m = INJECTED_RE.exec(text))) addObsId(ids, m[1]);
|
|
227
|
-
});
|
|
235
|
+
}, opts);
|
|
228
236
|
return ids;
|
|
229
237
|
}
|
|
230
238
|
|
|
@@ -251,7 +259,7 @@ const UPS_COMMAND_SUFFIX = 'hook.mjs user-prompt';
|
|
|
251
259
|
* @param {string|null|undefined} transcriptPath
|
|
252
260
|
* @returns {Set<number>}
|
|
253
261
|
*/
|
|
254
|
-
export function extractInjectedFromUserPromptSubmit(transcriptPath) {
|
|
262
|
+
export function extractInjectedFromUserPromptSubmit(transcriptPath, opts = {}) {
|
|
255
263
|
const ids = new Set();
|
|
256
264
|
eachHookAttachment(transcriptPath, ({ command, text }) => {
|
|
257
265
|
if (!command.includes(UPS_COMMAND_SUFFIX)) return;
|
|
@@ -265,7 +273,7 @@ export function extractInjectedFromUserPromptSubmit(transcriptPath) {
|
|
|
265
273
|
if (matches.length === 0) continue;
|
|
266
274
|
addObsId(ids, matches[matches.length - 1][1]);
|
|
267
275
|
}
|
|
268
|
-
});
|
|
276
|
+
}, opts);
|
|
269
277
|
return ids;
|
|
270
278
|
}
|
|
271
279
|
|
|
@@ -280,7 +288,7 @@ export function extractInjectedFromUserPromptSubmit(transcriptPath) {
|
|
|
280
288
|
* @param {string|null|undefined} transcriptPath
|
|
281
289
|
* @returns {Set<number>}
|
|
282
290
|
*/
|
|
283
|
-
export function extractInjectedFromErrorRecall(transcriptPath) {
|
|
291
|
+
export function extractInjectedFromErrorRecall(transcriptPath, opts = {}) {
|
|
284
292
|
const ids = new Set();
|
|
285
293
|
eachHookAttachment(transcriptPath, ({ command, text }) => {
|
|
286
294
|
if (!command.includes('post-tool-use')) return;
|
|
@@ -290,7 +298,7 @@ export function extractInjectedFromErrorRecall(transcriptPath) {
|
|
|
290
298
|
INJECTED_RE.lastIndex = 0;
|
|
291
299
|
let m;
|
|
292
300
|
while ((m = INJECTED_RE.exec(text))) addObsId(ids, m[1]);
|
|
293
|
-
});
|
|
301
|
+
}, opts);
|
|
294
302
|
return ids;
|
|
295
303
|
}
|
|
296
304
|
|
|
@@ -311,7 +319,7 @@ const FYI_LINE_ID_RE = /^#(\d{1,7})\s/;
|
|
|
311
319
|
* @param {string|null|undefined} transcriptPath
|
|
312
320
|
* @returns {Set<number>}
|
|
313
321
|
*/
|
|
314
|
-
export function extractInjectedFromFyi(transcriptPath) {
|
|
322
|
+
export function extractInjectedFromFyi(transcriptPath, opts = {}) {
|
|
315
323
|
const ids = new Set();
|
|
316
324
|
eachHookAttachment(transcriptPath, ({ command, text }) => {
|
|
317
325
|
if (!command.includes('user-prompt-search')) return;
|
|
@@ -320,7 +328,7 @@ export function extractInjectedFromFyi(transcriptPath) {
|
|
|
320
328
|
const m = FYI_LINE_ID_RE.exec(fyiLine);
|
|
321
329
|
if (m) addObsId(ids, m[1]);
|
|
322
330
|
}
|
|
323
|
-
});
|
|
331
|
+
}, opts);
|
|
324
332
|
return ids;
|
|
325
333
|
}
|
|
326
334
|
|
|
@@ -330,14 +338,18 @@ export function extractInjectedFromFyi(transcriptPath) {
|
|
|
330
338
|
* user-prompt-search FYI block. Single integration point the Stop handler calls.
|
|
331
339
|
*
|
|
332
340
|
* @param {string|null|undefined} transcriptPath
|
|
341
|
+
* @param {object} [opts]
|
|
342
|
+
* @param {boolean} [opts.mainOnly=false] Skip sidechain-injected IDs. The
|
|
343
|
+
* citation-decay caller passes true so the injected denominator matches the
|
|
344
|
+
* mainOnly cited numerator; the P4 access-bump caller omits it (broader).
|
|
333
345
|
* @returns {Set<number>}
|
|
334
346
|
*/
|
|
335
|
-
export function extractAllInjected(transcriptPath) {
|
|
347
|
+
export function extractAllInjected(transcriptPath, opts = {}) {
|
|
336
348
|
return new Set([
|
|
337
|
-
...extractInjectedFromPreToolUse(transcriptPath),
|
|
338
|
-
...extractInjectedFromUserPromptSubmit(transcriptPath),
|
|
339
|
-
...extractInjectedFromErrorRecall(transcriptPath),
|
|
340
|
-
...extractInjectedFromFyi(transcriptPath),
|
|
349
|
+
...extractInjectedFromPreToolUse(transcriptPath, opts),
|
|
350
|
+
...extractInjectedFromUserPromptSubmit(transcriptPath, opts),
|
|
351
|
+
...extractInjectedFromErrorRecall(transcriptPath, opts),
|
|
352
|
+
...extractInjectedFromFyi(transcriptPath, opts),
|
|
341
353
|
]);
|
|
342
354
|
}
|
|
343
355
|
|
package/lib/search-core.mjs
CHANGED
|
@@ -59,9 +59,26 @@ export function parseDateBounds(fromRaw, toRaw) {
|
|
|
59
59
|
* per-source SQL double-applied it and gapped/overlapped pages, because the
|
|
60
60
|
* obs hybrid path (AND→OR fallback / vector / concept stages) re-adds rows the
|
|
61
61
|
* SQL OFFSET already skipped (#8217/#8638).
|
|
62
|
+
*
|
|
63
|
+
* D#30 re-audit (reopened): perSourceLimit MUST be offset-independent. The old
|
|
64
|
+
* `offset + limit + 10` term grew the pool for deeper pages, and because RRF
|
|
65
|
+
* fusion of FTS + vector ranks is candidate-pool-sensitive (an item present in
|
|
66
|
+
* BOTH lists outranks one in a single list), a larger pool RE-RANKS the prefix —
|
|
67
|
+
* so page(offset=0) and page(offset=N) sliced DIFFERENT orderings and overlapped
|
|
68
|
+
* /gapped on the real (vector-populated) DB. #8642 missed this because its guard
|
|
69
|
+
* test seeds no vectors (FTS-only is prefix-stable). The fix makes the pool a
|
|
70
|
+
* function of `limit` only:
|
|
71
|
+
* - MIN_FUSION_POOL floor (= default limit 20 × the 3× buffer): every limit ≤ 20
|
|
72
|
+
* fuses ONE 60-candidate pool, so same-limit --offset pages are disjoint/stable
|
|
73
|
+
* and top-N is limit-stable (top-5 ⊂ top-10 ⊂ top-20). limit=20 offset=0 stays
|
|
74
|
+
* byte-identical to before (60) → no change on the benchmarked single-page path.
|
|
75
|
+
* - limit > 20 keeps limit*3 (the over-fetch buffer the fallback stages need).
|
|
76
|
+
* Trade-off: pages beyond the pool now return empty instead of the (overlapping,
|
|
77
|
+
* wrong) rows the offset-scaling used to surface — stability over deep reach.
|
|
62
78
|
*/
|
|
63
|
-
export
|
|
64
|
-
|
|
79
|
+
export const MIN_FUSION_POOL = 60;
|
|
80
|
+
export function computePerSourceWindow(limit, offset) { // eslint-disable-line no-unused-vars
|
|
81
|
+
return { perSourceLimit: Math.max(limit * 3, MIN_FUSION_POOL), perSourceOffset: 0 };
|
|
65
82
|
}
|
|
66
83
|
|
|
67
84
|
/** obs-side total query: when the AND→OR fallback fired, count the OR set. */
|
package/mem-cli.mjs
CHANGED
|
@@ -133,7 +133,15 @@ async function cmdSearch(db, args, { llm } = {}) {
|
|
|
133
133
|
// --deep proceeds even when the literal query sanitizes to nothing — its LLM
|
|
134
134
|
// rewrite may still produce searchable variants (F3, parity with server.mjs).
|
|
135
135
|
if (!ftsQuery && deepMode === 'normal') {
|
|
136
|
-
|
|
136
|
+
// A query that sanitizes to an empty FTS expression (only operators/punctuation/
|
|
137
|
+
// sub-min-length tokens) is a zero-result search, not a malformed one. In --json
|
|
138
|
+
// mode emit the same empty envelope as the no-match path below so programmatic
|
|
139
|
+
// consumers always get parseable stdout (the human path keeps the stderr hint).
|
|
140
|
+
if (jsonOutput) {
|
|
141
|
+
out(JSON.stringify({ query, total: 0, returned: 0, offset, limit, deep: false, results: [] }));
|
|
142
|
+
} else {
|
|
143
|
+
fail(`[mem] No valid search terms in "${query}"`);
|
|
144
|
+
}
|
|
137
145
|
return;
|
|
138
146
|
}
|
|
139
147
|
// --deep ignores --or: each variant runs AND + the engine's built-in
|
|
@@ -607,7 +615,20 @@ function cmdTimeline(db, args) {
|
|
|
607
615
|
if (flags.anchor !== undefined && flags.anchor !== true) {
|
|
608
616
|
const resolved = resolveAnchorToken(db, flags.anchor, { project });
|
|
609
617
|
if (!resolved.ok) {
|
|
610
|
-
|
|
618
|
+
// --json must always emit a parseable envelope. An explicit-but-missing anchor is
|
|
619
|
+
// a direct-lookup miss (like `get` on a bad id) → anchor:null + error code, rc=1.
|
|
620
|
+
if (jsonOutput) {
|
|
621
|
+
process.exitCode = 1;
|
|
622
|
+
out(JSON.stringify({
|
|
623
|
+
anchor: null,
|
|
624
|
+
anchor_note: formatAnchorError(resolved.error, 'mcp'),
|
|
625
|
+
before: [],
|
|
626
|
+
after: [],
|
|
627
|
+
error: resolved.error.code || 'anchor_resolution_failed',
|
|
628
|
+
}));
|
|
629
|
+
} else {
|
|
630
|
+
fail(formatAnchorError(resolved.error, 'cli'));
|
|
631
|
+
}
|
|
611
632
|
return;
|
|
612
633
|
}
|
|
613
634
|
anchorId = resolved.anchorId;
|
|
@@ -663,7 +684,20 @@ function cmdTimeline(db, args) {
|
|
|
663
684
|
// Window fetch (access-count bump + project auto-scope) shared with MCP.
|
|
664
685
|
const win = fetchTimelineWindow(db, anchorId, { before, after, project });
|
|
665
686
|
if (!win) {
|
|
666
|
-
|
|
687
|
+
// Anchor resolved to a real id but the window fetch found no row (e.g. project
|
|
688
|
+
// mismatch). Same --json contract as the resolution-miss path above.
|
|
689
|
+
if (jsonOutput) {
|
|
690
|
+
process.exitCode = 1;
|
|
691
|
+
out(JSON.stringify({
|
|
692
|
+
anchor: null,
|
|
693
|
+
anchor_note: `Observation #${anchorId} not found.`,
|
|
694
|
+
before: [],
|
|
695
|
+
after: [],
|
|
696
|
+
error: 'id-not-found',
|
|
697
|
+
}));
|
|
698
|
+
} else {
|
|
699
|
+
fail(`[mem] Observation #${anchorId} not found`);
|
|
700
|
+
}
|
|
667
701
|
return;
|
|
668
702
|
}
|
|
669
703
|
const { anchor, beforeRows, afterRows } = win;
|
|
@@ -1405,8 +1439,11 @@ function cmdUpdate(db, args) {
|
|
|
1405
1439
|
`Prompts and sessions are append-only.`);
|
|
1406
1440
|
return;
|
|
1407
1441
|
}
|
|
1442
|
+
// Strict parseIdToken gate (aligned with cmdDelete): a bare parseInt fallback
|
|
1443
|
+
// truncated "3.9" → 3 and silently UPDATE'd the WRONG row #3 (no preview, no
|
|
1444
|
+
// --confirm). Require an exact obs-id token; non-matching input → usage error.
|
|
1408
1445
|
const parsed = raw ? parseIdToken(raw) : null;
|
|
1409
|
-
const id = parsed && parsed.source === null ? parsed.id :
|
|
1446
|
+
const id = parsed && parsed.source === null ? parsed.id : NaN;
|
|
1410
1447
|
if (!id || isNaN(id)) {
|
|
1411
1448
|
fail('[mem] Usage: claude-mem-lite update <id> [--title T] [--type T] [--importance N] [--lesson T] [--narrative T] [--concepts T]');
|
|
1412
1449
|
return;
|
|
@@ -2331,6 +2368,8 @@ Commands:
|
|
|
2331
2368
|
--project P Filter by project
|
|
2332
2369
|
drop <D#N|ordinal>[,...] Drop one or more deferred items (no fix needed)
|
|
2333
2370
|
--reason "..." Required audit trail
|
|
2371
|
+
--project P Project for ordinal resolution (default: current; must
|
|
2372
|
+
match the "defer list --project P" you read ordinals from)
|
|
2334
2373
|
|
|
2335
2374
|
delete <id1,id2,...> Delete observations by ID
|
|
2336
2375
|
--confirm Execute deletion (preview by default)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.10.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",
|