claude-mem-lite 3.96.0 → 3.97.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.
Files changed (143) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/adopt-cli.mjs +125 -51
  4. package/bash-utils.mjs +105 -29
  5. package/claudemd.mjs +63 -19
  6. package/cli/activity.mjs +36 -18
  7. package/cli/common.mjs +101 -18
  8. package/cli/doctor.mjs +29 -9
  9. package/cli.mjs +69 -10
  10. package/deep-search.mjs +63 -20
  11. package/format-utils.mjs +15 -7
  12. package/haiku-client.mjs +189 -111
  13. package/hash-utils.mjs +15 -6
  14. package/hook-context.mjs +135 -46
  15. package/hook-episode.mjs +76 -23
  16. package/hook-handoff.mjs +216 -84
  17. package/hook-llm.mjs +476 -216
  18. package/hook-memory.mjs +106 -43
  19. package/hook-optimize.mjs +272 -109
  20. package/hook-precompact.mjs +1 -1
  21. package/hook-semaphore.mjs +46 -12
  22. package/hook-shared.mjs +101 -131
  23. package/hook-update.mjs +286 -88
  24. package/hook.mjs +1207 -617
  25. package/install-metadata.mjs +246 -125
  26. package/install.mjs +1327 -917
  27. package/lib/activity.mjs +52 -29
  28. package/lib/atomic-write.mjs +23 -4
  29. package/lib/binding-probe.mjs +40 -38
  30. package/lib/browse-core.mjs +22 -8
  31. package/lib/citation-tracker.mjs +189 -79
  32. package/lib/cite-back-hint.mjs +37 -19
  33. package/lib/cite-recall-path.mjs +3 -1
  34. package/lib/cli-flags.mjs +4 -2
  35. package/lib/cli-project.mjs +8 -5
  36. package/lib/compress-core.mjs +34 -9
  37. package/lib/cooldown-path.mjs +3 -1
  38. package/lib/db-backup.mjs +33 -11
  39. package/lib/deferred-work.mjs +73 -32
  40. package/lib/delete-core.mjs +25 -9
  41. package/lib/doctor-benchmark.mjs +5 -1
  42. package/lib/doctor-drift.mjs +25 -8
  43. package/lib/edge-attribution.mjs +23 -9
  44. package/lib/env-number.mjs +7 -8
  45. package/lib/err-sampler.mjs +22 -11
  46. package/lib/error-recall-core.mjs +15 -17
  47. package/lib/events-injection.mjs +25 -8
  48. package/lib/export-columns.mjs +49 -10
  49. package/lib/fast-summary.mjs +22 -9
  50. package/lib/file-edge-match.mjs +4 -2
  51. package/lib/file-intel.mjs +38 -13
  52. package/lib/frontmatter.mjs +22 -5
  53. package/lib/get-core.mjs +78 -11
  54. package/lib/handoff-constants.mjs +18 -0
  55. package/lib/hook-stdin.mjs +10 -3
  56. package/lib/hook-stdout.mjs +21 -7
  57. package/lib/hook-telemetry.mjs +44 -17
  58. package/lib/id-routing.mjs +11 -6
  59. package/lib/import-jsonl.mjs +92 -41
  60. package/lib/inject-search-core.mjs +5 -2
  61. package/lib/injected-ids.mjs +19 -12
  62. package/lib/install-shape.mjs +47 -23
  63. package/lib/keyctx-marker.mjs +3 -1
  64. package/lib/lesson-bridge.mjs +8 -5
  65. package/lib/llm-call.mjs +76 -0
  66. package/lib/llm-provider-probe.mjs +24 -12
  67. package/lib/low-signal-patterns.mjs +25 -22
  68. package/lib/maintain-core.mjs +221 -92
  69. package/lib/mem-override.mjs +4 -2
  70. package/lib/metrics.mjs +30 -25
  71. package/lib/native-binding-hint.mjs +29 -8
  72. package/lib/observation-write.mjs +74 -19
  73. package/lib/patha-exclude-meter.mjs +10 -6
  74. package/lib/persist-reminder.mjs +22 -10
  75. package/lib/plan-reader.mjs +2 -2
  76. package/lib/private-strip.mjs +3 -3
  77. package/lib/proc-lock.mjs +15 -3
  78. package/lib/proxy-fetch.mjs +49 -13
  79. package/lib/quiet-scope.mjs +42 -0
  80. package/lib/recall-core.mjs +19 -7
  81. package/lib/recent-core.mjs +18 -5
  82. package/lib/registry-core.mjs +12 -6
  83. package/lib/release-digest.mjs +4 -1
  84. package/lib/relevance-floor.mjs +5 -2
  85. package/lib/reread-guard.mjs +16 -8
  86. package/lib/resolve-data-dir.mjs +1 -1
  87. package/lib/rrf.mjs +4 -1
  88. package/lib/save-enrich.mjs +36 -18
  89. package/lib/save-observation.mjs +78 -35
  90. package/lib/scrub-record.mjs +20 -6
  91. package/lib/search-core.mjs +456 -123
  92. package/lib/shard-gc.mjs +50 -0
  93. package/lib/startup-dashboard.mjs +20 -9
  94. package/lib/stats-core.mjs +101 -35
  95. package/lib/stats-quality.mjs +56 -20
  96. package/lib/summary-extractor.mjs +2 -2
  97. package/lib/task-imperative.mjs +12 -4
  98. package/lib/timeline-core.mjs +88 -28
  99. package/lib/tmp-fixture-sweep.mjs +32 -9
  100. package/lib/transcript-scan.mjs +20 -4
  101. package/lib/upgrade-banner.mjs +11 -7
  102. package/mem-cli.mjs +1314 -506
  103. package/memdir.mjs +53 -21
  104. package/nlp.mjs +68 -38
  105. package/npm-shrinkwrap.json +2 -2
  106. package/package.json +9 -2
  107. package/plugin-cache-guard.mjs +31 -15
  108. package/project-utils.mjs +44 -20
  109. package/registry-enricher.mjs +26 -6
  110. package/registry-github.mjs +4 -2
  111. package/registry-importer.mjs +151 -40
  112. package/registry-recommend.mjs +189 -96
  113. package/registry-retriever.mjs +229 -70
  114. package/registry-scanner.mjs +11 -6
  115. package/registry.mjs +163 -66
  116. package/rerank.mjs +18 -4
  117. package/resource-discovery.mjs +14 -4
  118. package/schema.mjs +258 -99
  119. package/scoring-sql.mjs +21 -10
  120. package/scripts/binding-probe-cli.mjs +17 -12
  121. package/scripts/hook-launcher.mjs +148 -31
  122. package/scripts/launch-preflight.mjs +5 -7
  123. package/scripts/launch.mjs +21 -9
  124. package/scripts/post-tool-recall.js +21 -5
  125. package/scripts/pre-agent-inject.js +51 -13
  126. package/scripts/pre-skill-bridge.js +22 -7
  127. package/scripts/pre-tool-recall.js +157 -77
  128. package/scripts/prompt-search-utils.mjs +70 -25
  129. package/scripts/setup.sh +16 -1
  130. package/scripts/user-prompt-search.js +238 -97
  131. package/search-engine.mjs +329 -96
  132. package/search-scoring.mjs +75 -33
  133. package/secret-scrub.mjs +48 -12
  134. package/server/fts-check.mjs +12 -9
  135. package/server.mjs +674 -243
  136. package/skip-tools.mjs +19 -8
  137. package/source-files.mjs +54 -21
  138. package/stop-words.mjs +152 -19
  139. package/synonyms.mjs +579 -164
  140. package/tfidf.mjs +130 -40
  141. package/tier.mjs +1 -1
  142. package/tool-schemas.mjs +381 -131
  143. package/utils.mjs +184 -36
package/hook-memory.mjs CHANGED
@@ -1,7 +1,18 @@
1
1
  // claude-mem-lite — Semantic Memory Injection
2
2
  // Search past observations for relevant memories to inject as context at user-prompt time.
3
3
 
4
- import { relaxFtsQueryToOr, debugCatch, truncate, OBS_BM25, notLowSignalTitleClause, noisePenaltyClause, tokenizeHandoff, HANDOFF_STOP_WORDS, extractCjkKeywords, neutralizeContextDelimiters } from './utils.mjs';
4
+ import {
5
+ relaxFtsQueryToOr,
6
+ debugCatch,
7
+ truncate,
8
+ OBS_BM25,
9
+ notLowSignalTitleClause,
10
+ noisePenaltyClause,
11
+ tokenizeHandoff,
12
+ HANDOFF_STOP_WORDS,
13
+ extractCjkKeywords,
14
+ neutralizeContextDelimiters,
15
+ } from './utils.mjs';
5
16
  import { upsFtsQuery } from './lib/ups-query.mjs';
6
17
  import { citeFactorJs, TYPE_QUALITY, TYPE_QUALITY_DEFAULT } from './scoring-sql.mjs';
7
18
  import { liveObsFilterSql } from './lib/inject-search-core.mjs';
@@ -160,10 +171,14 @@ function getCrossProjectBoost() {
160
171
  }
161
172
  function extractQueryTerms(text) {
162
173
  if (!text) return [];
163
- const ascii = tokenizeHandoff(text).filter(t => !HANDOFF_STOP_WORDS.has(t));
174
+ const ascii = tokenizeHandoff(text).filter((t) => !HANDOFF_STOP_WORDS.has(t));
164
175
  let cjk = [];
165
- try { cjk = extractCjkKeywords(text) || []; } catch { /* CJK extraction best-effort */ }
166
- return [...new Set([...ascii, ...cjk.map(t => String(t).toLowerCase())])];
176
+ try {
177
+ cjk = extractCjkKeywords(text) || [];
178
+ } catch {
179
+ /* CJK extraction best-effort */
180
+ }
181
+ return [...new Set([...ascii, ...cjk.map((t) => String(t).toLowerCase())])];
167
182
  }
168
183
  // v2.41: hay spans every FTS column whose BM25 weight is >=5 in OBS_BM25
169
184
  // (title=10, subtitle=5, narrative=5, lesson_learned=8). Pre-v2.41 was only
@@ -176,7 +191,8 @@ const COVERAGE_NARRATIVE_PREFIX = 400;
176
191
  function candidateCoverage(row, queryTerms) {
177
192
  if (queryTerms.length === 0) return 1.0;
178
193
  const narrativeHead = (row.narrative || '').slice(0, COVERAGE_NARRATIVE_PREFIX);
179
- const hay = `${row.title || ''} ${row.subtitle || ''} ${row.lesson_learned || ''} ${narrativeHead}`.toLowerCase();
194
+ const hay =
195
+ `${row.title || ''} ${row.subtitle || ''} ${row.lesson_learned || ''} ${narrativeHead}`.toLowerCase();
180
196
  let hits = 0;
181
197
  for (const t of queryTerms) {
182
198
  if (/[^ -~]/.test(t)) {
@@ -191,7 +207,6 @@ function candidateCoverage(row, queryTerms) {
191
207
  return hits / queryTerms.length;
192
208
  }
193
209
 
194
-
195
210
  // P1: stale-obs verify-before-use threshold. An injected obs older than this
196
211
  // AND carrying file paths is flagged so Claude is reminded to grep/Read the
197
212
  // referenced code before applying the lesson — code may have moved or been
@@ -217,15 +232,19 @@ export function formatMemoryLine(obs) {
217
232
  // citation-decay denominator (its promote/demote loop was silently dead).
218
233
  const lessonTag = obs.lesson_learned ? ` | Lesson: ${truncate(obs.lesson_learned, 200)}` : '';
219
234
  let staleHint = '';
220
- if (typeof obs.created_at_epoch === 'number'
221
- && Date.now() - obs.created_at_epoch > STALE_OBS_THRESHOLD_MS
222
- && hasFilePaths(obs.files_modified)) {
235
+ if (
236
+ typeof obs.created_at_epoch === 'number' &&
237
+ Date.now() - obs.created_at_epoch > STALE_OBS_THRESHOLD_MS &&
238
+ hasFilePaths(obs.files_modified)
239
+ ) {
223
240
  staleHint = ' [verify-before-use]';
224
241
  }
225
242
  // Defang any literal block-delimiter tag in title/lesson so it can't prematurely close
226
243
  // the <memory-context> block this line is injected into (parity with hook-context's
227
244
  // <claude-mem-context> defense).
228
- return neutralizeContextDelimiters(`- [${obs.type}] ${truncate(obs.title, 80)}${lessonTag} (#${obs.id})${staleHint}`);
245
+ return neutralizeContextDelimiters(
246
+ `- [${obs.type}] ${truncate(obs.title, 80)}${lessonTag} (#${obs.id})${staleHint}`,
247
+ );
229
248
  }
230
249
 
231
250
  function hasFilePaths(filesModified) {
@@ -268,7 +287,13 @@ function hasFilePaths(filesModified) {
268
287
  * BOTH arms to see one store state, which is achieved by ordering (arm B first, and
269
288
  * it writes nothing) rather than by isolation.
270
289
  */
271
- export function searchRelevantMemories(db, userPrompt, project, excludeIds = [], { counterfactual = false } = {}) {
290
+ export function searchRelevantMemories(
291
+ db,
292
+ userPrompt,
293
+ project,
294
+ excludeIds = [],
295
+ { counterfactual = false } = {},
296
+ ) {
272
297
  // Min-length guard is English-centric: 5 chars ≈ one short English word. A CJK
273
298
  // query is meaningful at 2 chars (状态/架构) and most real Chinese queries are
274
299
  // 2-4 chars (状态管理, 召回率, 熔断降级) — the bare `.length < 5` silently
@@ -290,7 +315,10 @@ export function searchRelevantMemories(db, userPrompt, project, excludeIds = [],
290
315
  // v2.41 metrics: record timing + candidate/filter/return counts per call.
291
316
  // Gated by CLAUDE_MEM_METRICS=1 — no-op when disabled (zero hot-path cost).
292
317
  const _t0 = Date.now();
293
- let _candidates = 0, _aboveThreshold = 0, _returned = 0, _orFired = false;
318
+ let _candidates = 0,
319
+ _aboveThreshold = 0,
320
+ _returned = 0,
321
+ _orFired = false;
294
322
  const _emit = () => {
295
323
  if (counterfactual) return;
296
324
  try {
@@ -302,7 +330,9 @@ export function searchRelevantMemories(db, userPrompt, project, excludeIds = [],
302
330
  returned: _returned,
303
331
  orFallback: _orFired,
304
332
  });
305
- } catch { /* metric record must not crash the caller */ }
333
+ } catch {
334
+ /* metric record must not crash the caller */
335
+ }
306
336
  };
307
337
 
308
338
  try {
@@ -350,7 +380,7 @@ export function searchRelevantMemories(db, userPrompt, project, excludeIds = [],
350
380
  // Count original search terms (AND-separated groups), not expanded synonym tokens.
351
381
  const queryTokenCount = ftsQuery.includes(' AND ')
352
382
  ? ftsQuery.split(' AND ').length
353
- : ftsQuery.split(/\s+/).filter(t => t && !t.startsWith('(') && !t.endsWith(')')).length;
383
+ : ftsQuery.split(/\s+/).filter((t) => t && !t.startsWith('(') && !t.endsWith(')')).length;
354
384
  // CJK-dominant queries bypass the token-count gate: a single CJK word becomes
355
385
  // 2-N overlapping bigrams (优化召回率 → 优化/召回/回率), inflating
356
386
  // queryTokenCount past the gate, so the AND-too-strict query never gets the OR
@@ -369,8 +399,12 @@ export function searchRelevantMemories(db, userPrompt, project, excludeIds = [],
369
399
  // (The two bare catches further down, around the per-row access bumps, are
370
400
  // deliberately left bare: they are write-path and per-row, so logging them would
371
401
  // flood the debug stream on the same corruption this one reports once.)
372
- try { rows = selectStmt.all(orQuery, project, cutoff); usedOrFallback = true; }
373
- catch (e) { debugCatch(e, 'injectMemory:orFallback'); }
402
+ try {
403
+ rows = selectStmt.all(orQuery, project, cutoff);
404
+ usedOrFallback = true;
405
+ } catch (e) {
406
+ debugCatch(e, 'injectMemory:orFallback');
407
+ }
374
408
  }
375
409
  }
376
410
 
@@ -403,11 +437,17 @@ export function searchRelevantMemories(db, userPrompt, project, excludeIds = [],
403
437
  if (orQuery && (queryIsCjkDominant || queryTokenCount <= orFallbackMaxTokens)) {
404
438
  // Same reasoning as the same-project OR fallback above: a fault here silently
405
439
  // drops the cross-project half of the injection.
406
- try { crossRows = crossStmt.all(orQuery, project, cutoff); crossUsedOr = true; }
407
- catch (e) { debugCatch(e, 'injectMemory:crossOrFallback'); }
440
+ try {
441
+ crossRows = crossStmt.all(orQuery, project, cutoff);
442
+ crossUsedOr = true;
443
+ } catch (e) {
444
+ debugCatch(e, 'injectMemory:crossOrFallback');
445
+ }
408
446
  }
409
447
  }
410
- } catch (e) { debugCatch(e, 'crossProjectSearch'); }
448
+ } catch (e) {
449
+ debugCatch(e, 'crossProjectSearch');
450
+ }
411
451
 
412
452
  // Merge and score: same-project full weight, cross-project (default 0.7x).
413
453
  // v2.41: cross-project penalty is env-overridable via MEM_CROSS_PROJECT_BOOST
@@ -419,40 +459,48 @@ export function searchRelevantMemories(db, userPrompt, project, excludeIds = [],
419
459
  // OR-fallback results get 0.4x penalty — they matched individual words, not the full intent
420
460
  // v26 P0: noise_penalty (from SQL) shrinks high-inject/low-cite rows.
421
461
  const crossPenalty = getCrossProjectBoost();
422
- const allRows = [...rows.map(r => ({ ...r, _or: usedOrFallback })), ...crossRows.map(r => ({ ...r, _or: crossUsedOr }))];
462
+ const allRows = [
463
+ ...rows.map((r) => ({ ...r, _or: usedOrFallback })),
464
+ ...crossRows.map((r) => ({ ...r, _or: crossUsedOr })),
465
+ ];
423
466
  const scored = allRows
424
- .filter(r => !excludeSet.has(r.id))
425
- .map(r => {
467
+ .filter((r) => !excludeSet.has(r.id))
468
+ .map((r) => {
426
469
  const crossProjectPenalty = r.project === project ? 1.0 : crossPenalty;
427
470
  const orFallbackPenalty = r._or ? 0.4 : 1.0;
428
471
  const noisePenalty = typeof r.noise_penalty === 'number' ? r.noise_penalty : 1.0;
429
472
  const citeFactor = citeFactorJs(r);
430
473
  return {
431
474
  ...r,
432
- score: Math.abs(r.relevance)
433
- * (TYPE_QUALITY[r.type] || TYPE_QUALITY_DEFAULT)
434
- * (r.lesson_learned ? 1.5 : 1.0)
435
- * (r.importance >= 2 ? 1.0 : 0.6)
436
- * crossProjectPenalty
437
- * orFallbackPenalty
438
- * noisePenalty
439
- * citeFactor,
475
+ score:
476
+ Math.abs(r.relevance) *
477
+ (TYPE_QUALITY[r.type] || TYPE_QUALITY_DEFAULT) *
478
+ (r.lesson_learned ? 1.5 : 1.0) *
479
+ (r.importance >= 2 ? 1.0 : 0.6) *
480
+ crossProjectPenalty *
481
+ orFallbackPenalty *
482
+ noisePenalty *
483
+ citeFactor,
440
484
  };
441
485
  })
442
486
  .sort((a, b) => b.score - a.score);
443
487
 
444
488
  // Adaptive threshold: scales with corpus size to filter noise.
445
489
  // Each result must individually exceed the threshold (not just the top one).
446
- const obsCount = db.prepare(
447
- `SELECT COUNT(*) as c FROM observations WHERE project = ? AND ${liveObsFilterSql('')}`,
448
- ).get(project)?.c || 0;
490
+ const obsCount =
491
+ db
492
+ .prepare(`SELECT COUNT(*) as c FROM observations WHERE project = ? AND ${liveObsFilterSql('')}`)
493
+ .get(project)?.c || 0;
449
494
  const { TINY, SMALL, MEDIUM, LARGE } = BM25_THRESHOLD;
450
495
  const threshold = obsCount < 5 ? TINY : obsCount < 100 ? SMALL : obsCount < 500 ? MEDIUM : LARGE;
451
496
  _candidates = scored.length;
452
497
  _orFired = usedOrFallback || crossUsedOr;
453
- const aboveThreshold = scored.filter(r => r.score >= threshold);
498
+ const aboveThreshold = scored.filter((r) => r.score >= threshold);
454
499
  _aboveThreshold = aboveThreshold.length;
455
- if (aboveThreshold.length === 0) { _emit(); return []; }
500
+ if (aboveThreshold.length === 0) {
501
+ _emit();
502
+ return [];
503
+ }
456
504
 
457
505
  // v27: term-coverage filter — drop candidates whose title+lesson_learned
458
506
  // covers <threshold of the query's significant terms. Skipped for
@@ -463,8 +511,13 @@ export function searchRelevantMemories(db, userPrompt, project, excludeIds = [],
463
511
  if (coverageThreshold > 0) {
464
512
  const queryTerms = extractQueryTerms(userPrompt);
465
513
  if (queryTerms.length >= COVERAGE_MIN_QUERY_TERMS) {
466
- coverageFiltered = aboveThreshold.filter(r => candidateCoverage(r, queryTerms) >= coverageThreshold);
467
- if (coverageFiltered.length === 0) { _emit(); return []; }
514
+ coverageFiltered = aboveThreshold.filter(
515
+ (r) => candidateCoverage(r, queryTerms) >= coverageThreshold,
516
+ );
517
+ if (coverageFiltered.length === 0) {
518
+ _emit();
519
+ return [];
520
+ }
468
521
  }
469
522
  }
470
523
 
@@ -499,10 +552,12 @@ export function searchRelevantMemories(db, userPrompt, project, excludeIds = [],
499
552
  if (!counterfactual) {
500
553
  const now = Date.now();
501
554
  const bumpStmt = db.prepare(
502
- 'UPDATE observations SET injection_count = COALESCE(injection_count, 0) + 1, last_injected_at = ? WHERE id = ?'
555
+ 'UPDATE observations SET injection_count = COALESCE(injection_count, 0) + 1, last_injected_at = ? WHERE id = ?',
503
556
  );
504
557
  for (const r of result) {
505
- try { bumpStmt.run(now, r.id); } catch {}
558
+ try {
559
+ bumpStmt.run(now, r.id);
560
+ } catch {}
506
561
  }
507
562
  }
508
563
 
@@ -613,7 +668,9 @@ export function rankImperativeCandidates(db, userPrompt, project, excludeIds = [
613
668
  // guaranteed one. There are zero such collisions live, so it changes no behaviour here;
614
669
  // it makes the guarantee hold on corpora nobody has seen.
615
670
  try {
616
- rows = db.prepare(`
671
+ rows = db
672
+ .prepare(
673
+ `
617
674
  SELECT id, title, lesson_learned, importance
618
675
  FROM observations
619
676
  WHERE project = ?
@@ -625,12 +682,18 @@ export function rankImperativeCandidates(db, userPrompt, project, excludeIds = [
625
682
  AND (? IS NULL OR created_at_epoch <= ?)
626
683
  ORDER BY importance DESC, created_at_epoch DESC, id DESC
627
684
  LIMIT ${IMPERATIVE_POOL_BACKSTOP}
628
- `).all(project, epochTo, epochTo);
629
- } catch { return []; }
685
+ `,
686
+ )
687
+ .all(project, epochTo, epochTo);
688
+ } catch {
689
+ return [];
690
+ }
630
691
  const out = [];
631
692
  for (const r of rows) {
632
693
  if (exclude.has(r.id)) continue;
633
- const overlap = extractIdents(`${r.lesson_learned} ${r.title || ''}`).filter((id) => promptIdents.has(id)).length;
694
+ const overlap = extractIdents(`${r.lesson_learned} ${r.title || ''}`).filter((id) =>
695
+ promptIdents.has(id),
696
+ ).length;
634
697
  if (overlap === 0) continue;
635
698
  const score = (r.importance || 2) * overlap;
636
699
  out.push({ id: r.id, lesson_learned: r.lesson_learned, importance: r.importance || 2, overlap, score });