claude-mem-lite 3.38.1 → 3.38.3
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-memory.mjs +30 -13
- package/lib/citation-tracker.mjs +43 -0
- package/mem-cli.mjs +4 -4
- package/package.json +1 -1
- package/scripts/user-prompt-search.js +28 -5
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "claude-mem-lite",
|
|
13
|
-
"version": "3.38.
|
|
13
|
+
"version": "3.38.3",
|
|
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.38.
|
|
3
|
+
"version": "3.38.3",
|
|
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-memory.mjs
CHANGED
|
@@ -408,17 +408,19 @@ export function recallForFile(db, filePath, project) {
|
|
|
408
408
|
}
|
|
409
409
|
|
|
410
410
|
/**
|
|
411
|
-
* Phase-2 task-imperative
|
|
412
|
-
*
|
|
413
|
-
*
|
|
414
|
-
* with the prompt, top-1) — deliberately independent of searchRelevantMemories' coverage/
|
|
411
|
+
* Phase-2 task-imperative ranking (spec 2026-06-29 §4.1): score every candidate lesson
|
|
412
|
+
* relevant to THIS prompt (importance>=2 + non-empty lesson + identifier overlap with the
|
|
413
|
+
* prompt), sorted best-first — deliberately independent of searchRelevantMemories' coverage/
|
|
415
414
|
* BM25 filters so a high-value lesson is not dropped by the context-list scoring.
|
|
416
|
-
*
|
|
415
|
+
* `epochTo` (optional) restricts the pool to observations created at/before that epoch —
|
|
416
|
+
* for offline point-in-time replay (benchmark use); a no-op at every production call site,
|
|
417
|
+
* which never passes it.
|
|
418
|
+
* @returns {Array<{id:number, lesson_learned:string, importance:number, overlap:number, score:number}>}
|
|
417
419
|
*/
|
|
418
|
-
export function
|
|
419
|
-
if (!db || !userPrompt) return
|
|
420
|
+
export function rankImperativeCandidates(db, userPrompt, project, excludeIds = [], { epochTo = null } = {}) {
|
|
421
|
+
if (!db || !userPrompt) return [];
|
|
420
422
|
const promptIdents = new Set(extractIdents(userPrompt));
|
|
421
|
-
if (promptIdents.size === 0) return
|
|
423
|
+
if (promptIdents.size === 0) return []; // no symbol anchor → no imperative (precision-first)
|
|
422
424
|
const exclude = new Set(excludeIds);
|
|
423
425
|
let rows;
|
|
424
426
|
try {
|
|
@@ -432,19 +434,34 @@ export function selectImperativeLesson(db, userPrompt, project, excludeIds = [])
|
|
|
432
434
|
AND lesson_learned IS NOT NULL
|
|
433
435
|
AND TRIM(lesson_learned) != ''
|
|
434
436
|
AND LOWER(TRIM(lesson_learned)) != 'none'
|
|
437
|
+
AND (? IS NULL OR created_at_epoch <= ?)
|
|
435
438
|
ORDER BY importance DESC, created_at_epoch DESC
|
|
436
439
|
LIMIT 50
|
|
437
|
-
`).all(project);
|
|
438
|
-
} catch { return
|
|
439
|
-
|
|
440
|
+
`).all(project, epochTo, epochTo);
|
|
441
|
+
} catch { return []; }
|
|
442
|
+
const out = [];
|
|
440
443
|
for (const r of rows) {
|
|
441
444
|
if (exclude.has(r.id)) continue;
|
|
442
445
|
const overlap = extractIdents(`${r.lesson_learned} ${r.title || ''}`).filter((id) => promptIdents.has(id)).length;
|
|
443
446
|
if (overlap === 0) continue;
|
|
444
447
|
const score = (r.importance || 2) * overlap;
|
|
445
|
-
|
|
448
|
+
out.push({ id: r.id, lesson_learned: r.lesson_learned, importance: r.importance || 2, overlap, score });
|
|
446
449
|
}
|
|
447
|
-
|
|
450
|
+
// Array.prototype.sort is spec-guaranteed stable (ES2019+) — ties keep the pool's
|
|
451
|
+
// ORDER BY importance DESC, created_at_epoch DESC relative order, matching the old
|
|
452
|
+
// loop's strict `score > bestScore` (first-max-wins) tie-break exactly.
|
|
453
|
+
out.sort((a, b) => b.score - a.score);
|
|
454
|
+
return out;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Phase-2 task-imperative selection: the single highest-value lesson relevant to THIS
|
|
459
|
+
* prompt — top-1 of rankImperativeCandidates, mapped to the pre-existing return shape.
|
|
460
|
+
* @returns {{id:number, lesson_learned:string}|null}
|
|
461
|
+
*/
|
|
462
|
+
export function selectImperativeLesson(db, userPrompt, project, excludeIds = []) {
|
|
463
|
+
const top = rankImperativeCandidates(db, userPrompt, project, excludeIds)[0];
|
|
464
|
+
return top ? { id: top.id, lesson_learned: top.lesson_learned } : null;
|
|
448
465
|
}
|
|
449
466
|
|
|
450
467
|
// P0 (2026-07-03): compose the subagent-dispatch injection. Given a PreToolUse
|
package/lib/citation-tracker.mjs
CHANGED
|
@@ -377,8 +377,51 @@ export function extractAllInjected(transcriptPath, opts = {}) {
|
|
|
377
377
|
* @param {string} transcriptPath
|
|
378
378
|
* @returns {{injected: number, cited: number, recalled: number, ratio: number}}
|
|
379
379
|
*/
|
|
380
|
+
// pre-agent-inject.js (CLAUDE_MEM_SUBAGENT_INJECT) APPENDS formatSubagentContext to a
|
|
381
|
+
// dispatched subagent's task prompt via PreToolUse updatedInput — a PROMPT-embedded
|
|
382
|
+
// injection, NOT a hook attachment. extractAllInjected (the attachment path) misses it,
|
|
383
|
+
// so the sidechain instrument read a false 0 ("subagents memory-blind") even while the
|
|
384
|
+
// dispatch surface was live. The block is `[Project memory — surfaced by your operator's
|
|
385
|
+
// claude-mem-lite ...]` followed by a ` #NN — <lesson>.` tag line.
|
|
386
|
+
const SUBAGENT_INJECT_MARKER = /surfaced by your operator's claude-mem-lite/;
|
|
387
|
+
// Row-anchored to the `#NN — ` tag so a #NN quoted inside the lesson body does NOT enter
|
|
388
|
+
// the injected set — same discipline as INJECTED_ROW_RE for the attachment surfaces.
|
|
389
|
+
const SUBAGENT_INJECT_ID_RE = /^\s{0,4}#(\d{1,7})\s+—/;
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Extract observation ids injected into a subagent's PROMPT by pre-agent-inject.js
|
|
393
|
+
* (formatSubagentContext). Only the `#NN — ` tag line counts; a body cross-reference is
|
|
394
|
+
* ignored. Returns numeric ids.
|
|
395
|
+
* @param {string|null|undefined} transcriptPath
|
|
396
|
+
* @returns {Set<number>}
|
|
397
|
+
*/
|
|
398
|
+
export function extractInjectedFromSubagentPrompt(transcriptPath) {
|
|
399
|
+
const ids = new Set();
|
|
400
|
+
let raw;
|
|
401
|
+
try { raw = readFileSync(transcriptPath, 'utf8'); } catch { return ids; }
|
|
402
|
+
for (const line of raw.split('\n')) {
|
|
403
|
+
if (!line) continue;
|
|
404
|
+
let entry;
|
|
405
|
+
try { entry = JSON.parse(line); } catch { continue; }
|
|
406
|
+
const c = entry.message?.content;
|
|
407
|
+
const text = typeof c === 'string'
|
|
408
|
+
? c
|
|
409
|
+
: Array.isArray(c) ? c.map((x) => (typeof x === 'string' ? x : x?.text || '')).join('\n') : '';
|
|
410
|
+
if (!text || !SUBAGENT_INJECT_MARKER.test(text)) continue;
|
|
411
|
+
for (const bl of text.split('\n')) {
|
|
412
|
+
const m = SUBAGENT_INJECT_ID_RE.exec(bl);
|
|
413
|
+
if (m) addObsId(ids, m[1]);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return ids;
|
|
417
|
+
}
|
|
418
|
+
|
|
380
419
|
export function computeThreadCiteRecall(transcriptPath) {
|
|
381
420
|
const injected = extractAllInjected(transcriptPath);
|
|
421
|
+
// Subagent files carry NO hook-attachment injection; pre-agent-inject.js injects into
|
|
422
|
+
// the PROMPT (updatedInput). Fold that in so sidechain recall isn't a false 0. On a
|
|
423
|
+
// main transcript this marker is absent → no-op.
|
|
424
|
+
for (const id of extractInjectedFromSubagentPrompt(transcriptPath)) injected.add(id);
|
|
382
425
|
const cited = extractCitationsFromTranscript(transcriptPath);
|
|
383
426
|
let recalled = 0;
|
|
384
427
|
for (const id of injected) if (cited.has(id)) recalled++;
|
package/mem-cli.mjs
CHANGED
|
@@ -2349,10 +2349,10 @@ function _reportSidechainCiteRecall({ days, json }) {
|
|
|
2349
2349
|
out(` main ${pct(mainRate).padStart(6)} recalled ${main.recalled} / injected ${main.injected} (${main.files} transcript(s))`);
|
|
2350
2350
|
out(` sidechain ${pct(sideRate).padStart(6)} recalled ${sidechain.recalled} / injected ${sidechain.injected} (${sidechain.files} subagent file(s), ${sidechain.withInjections} with injections)`);
|
|
2351
2351
|
if (sidechain.files > 0 && sidechain.injected === 0) {
|
|
2352
|
-
out(' → subagent transcripts exist but
|
|
2353
|
-
out(' hooks do NOT fire inside subagents
|
|
2354
|
-
out('
|
|
2355
|
-
out('
|
|
2352
|
+
out(' → subagent transcripts exist but none carried a detectable memory injection in');
|
|
2353
|
+
out(' this window. claude-mem-lite hooks do NOT fire inside subagents; the dispatch-');
|
|
2354
|
+
out(' time surface (pre-agent-inject.js, CLAUDE_MEM_SUBAGENT_INJECT) prompt-injects');
|
|
2355
|
+
out(' a lesson when enabled — a 0 here means it was off or selected none for these.');
|
|
2356
2356
|
} else if (sidechain.files === 0) {
|
|
2357
2357
|
out(' → no subagent transcripts in window.');
|
|
2358
2358
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "3.38.
|
|
3
|
+
"version": "3.38.3",
|
|
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",
|
|
@@ -9,6 +9,7 @@ import { citeFactorClause } from '../scoring-sql.mjs';
|
|
|
9
9
|
import { cjkPrecisionOk } from '../nlp.mjs';
|
|
10
10
|
import { writeFileSync, readFileSync, existsSync, renameSync } from 'fs';
|
|
11
11
|
import { join, sep } from 'path';
|
|
12
|
+
import { pathToFileURL } from 'url';
|
|
12
13
|
import Database from 'better-sqlite3';
|
|
13
14
|
import { shouldSkip, computeEffectiveLen, detectIntent, shouldSkipByDedup, extractFiles, extractErrorSignature, DEDUP_STALE_MS, matchRegistrySkillName, detectMemOverride } from './prompt-search-utils.mjs';
|
|
14
15
|
import { recommendSkill } from '../registry-recommend.mjs';
|
|
@@ -265,14 +266,15 @@ export function rowMatchesIdentifier(row, idsLower) {
|
|
|
265
266
|
// Each row includes `bm25_raw` (pre-multiplier bm25 magnitude) alongside the
|
|
266
267
|
// composite `relevance`, so callers can distinguish raw-match strength from
|
|
267
268
|
// importance/type/decay inflation.
|
|
268
|
-
export function searchByFts(db, queryText, project, limit, typeFilter
|
|
269
|
+
export function searchByFts(db, queryText, project, limit, typeFilter,
|
|
270
|
+
{ nowT = Date.now(), epochTo = null } = {}) {
|
|
269
271
|
const ftsQuery = sanitizeFtsQuery(queryText);
|
|
270
272
|
if (!ftsQuery) return { rows: [], mode: null };
|
|
271
273
|
|
|
272
|
-
const cutoff =
|
|
274
|
+
const cutoff = nowT - LOOKBACK_MS;
|
|
273
275
|
|
|
274
276
|
const typeClause = typeFilter ? 'AND o.type = ?' : '';
|
|
275
|
-
const now =
|
|
277
|
+
const now = nowT;
|
|
276
278
|
// R1: notLowSignalTitleClause() excludes hook-llm degraded titles
|
|
277
279
|
// ("Modified X", "Worked on X", "Reviewed N files:", raw error logs).
|
|
278
280
|
// v26 P0: noise penalty shrinks relevance magnitude for obs with high
|
|
@@ -298,6 +300,7 @@ export function searchByFts(db, queryText, project, limit, typeFilter) {
|
|
|
298
300
|
AND o.project = ?
|
|
299
301
|
AND o.importance >= 1
|
|
300
302
|
AND o.created_at_epoch > ?
|
|
303
|
+
AND (? IS NULL OR o.created_at_epoch <= ?)
|
|
301
304
|
AND COALESCE(o.compressed_into, 0) = 0
|
|
302
305
|
AND o.superseded_at IS NULL
|
|
303
306
|
AND ${notLowSignalTitleClause('o')}
|
|
@@ -306,7 +309,7 @@ export function searchByFts(db, queryText, project, limit, typeFilter) {
|
|
|
306
309
|
LIMIT ?
|
|
307
310
|
`;
|
|
308
311
|
|
|
309
|
-
const params = [now, ftsQuery, project, cutoff];
|
|
312
|
+
const params = [now, ftsQuery, project, cutoff, epochTo, epochTo];
|
|
310
313
|
if (typeFilter) params.push(typeFilter);
|
|
311
314
|
params.push(limit);
|
|
312
315
|
|
|
@@ -821,4 +824,24 @@ async function main() {
|
|
|
821
824
|
// every sibling hook script upholds). Deliberately NOT `.finally(process.exit(0))` —
|
|
822
825
|
// this hook detaches a background `claude -p` search and a forced exit would kill it;
|
|
823
826
|
// letting the loop drain naturally exits 0 once the detached child is unref'd.
|
|
824
|
-
|
|
827
|
+
// Import guard (mirrors benchmark/longmemeval.mjs): only auto-run when this file is
|
|
828
|
+
// executed directly as the UserPromptSubmit hook, not when a benchmark/test harness
|
|
829
|
+
// imports it to call searchByFts() offline (see tests/adoption-searchbyfts-snapshot.test.mjs).
|
|
830
|
+
//
|
|
831
|
+
// Exported as a pure predicate (side-effect-free) so a regression test can
|
|
832
|
+
// exercise it directly without triggering main(). hook-launcher.mjs's
|
|
833
|
+
// self-heal retry (runEntry({ bustCache: true })) re-imports this entry with
|
|
834
|
+
// a cache-buster query appended (`?t=<ts>`) so Node's ESM cache doesn't
|
|
835
|
+
// return the earlier ERR_MODULE_NOT_FOUND rejection, while process.argv[1]
|
|
836
|
+
// stays query-less — a strict `===` here evaluated false on that retry,
|
|
837
|
+
// silently skipping main() so the healed hook fire did no memory search.
|
|
838
|
+
// Stripping the query before comparing fixes that without weakening the
|
|
839
|
+
// normal-launcher / execFileSync direct-invocation checks (both still match
|
|
840
|
+
// exactly, query or no query).
|
|
841
|
+
export function isDirectInvocation(metaUrl, argv1) {
|
|
842
|
+
if (!argv1) return false;
|
|
843
|
+
return metaUrl.split('?')[0] === pathToFileURL(argv1).href;
|
|
844
|
+
}
|
|
845
|
+
if (isDirectInvocation(import.meta.url, process.argv[1])) {
|
|
846
|
+
main().catch(() => {});
|
|
847
|
+
}
|