claude-mem-lite 3.38.2 → 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.
@@ -10,7 +10,7 @@
10
10
  "plugins": [
11
11
  {
12
12
  "name": "claude-mem-lite",
13
- "version": "3.38.2",
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.2",
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 selection (spec 2026-06-29 §4.1): the single highest-value
412
- * lesson relevant to THIS prompt, for delivery at the task-prompt position under the
413
- * imperative template. Own gate (importance>=2 + non-empty lesson + identifier overlap
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
- * @returns {{id:number, lesson_learned:string}|null}
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 selectImperativeLesson(db, userPrompt, project, excludeIds = []) {
419
- if (!db || !userPrompt) return null;
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 null; // no symbol anchor → no imperative (precision-first)
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 null; }
439
- let best = null, bestScore = 0;
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
- if (score > bestScore) { bestScore = score; best = r; }
448
+ out.push({ id: r.id, lesson_learned: r.lesson_learned, importance: r.importance || 2, overlap, score });
446
449
  }
447
- return best ? { id: best.id, lesson_learned: best.lesson_learned } : null;
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "3.38.2",
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 = Date.now() - LOOKBACK_MS;
274
+ const cutoff = nowT - LOOKBACK_MS;
273
275
 
274
276
  const typeClause = typeFilter ? 'AND o.type = ?' : '';
275
- const now = Date.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
- main().catch(() => {});
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
+ }