claude-mem-lite 2.54.0 → 2.58.2

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.
@@ -4,7 +4,7 @@
4
4
  // Lightweight: only imports schema.mjs and utils.mjs, no MCP SDK
5
5
 
6
6
  import { ensureDb, DB_DIR, REGISTRY_DB_PATH } from '../schema.mjs';
7
- import { sanitizeFtsQuery, relaxFtsQueryToOr, truncate, typeIcon, inferProject, OBS_BM25, TYPE_DECAY_CASE, TYPE_QUALITY_CASE, notLowSignalTitleClause, noisePenaltyClause } from '../utils.mjs';
7
+ import { sanitizeFtsQuery, relaxFtsQueryToOr, truncate, typeIcon, inferProject, OBS_BM25, TYPE_DECAY_CASE, TYPE_QUALITY_CASE, notLowSignalTitleClause, noisePenaltyClause, stripPrivate } from '../utils.mjs';
8
8
  import { cjkPrecisionOk } from '../nlp.mjs';
9
9
  import { writeFileSync, readFileSync, existsSync, renameSync } from 'fs';
10
10
  import { join } from 'path';
@@ -14,9 +14,24 @@ import { shouldSkip, computeEffectiveLen, detectIntent, shouldSkipByDedup, extra
14
14
  // ─── Constants ──────────────────────────────────────────────────────────────
15
15
 
16
16
  const INJECTED_IDS_FILE = join(DB_DIR, 'runtime', `.claude-mem-injected-${inferProject()}`);
17
- const MAX_RESULTS = 5;
17
+ // Per-prompt UPS cap. Cut from 5 → 3 after the 2026-05-09 per-hook recall
18
+ // scan (#8255): UPS contributed 74% of silent injected IDs (131/177) at 26%
19
+ // recall, vs PreToolUse:Read at 94% recall on a tighter file-keyed set.
20
+ // Hypothesis: fewer candidates → each one more relevant → cite-rate up.
21
+ // useRecent intent path is unaffected (it uses intent.limit=5 directly,
22
+ // gated by explicit "before/previously/记得" prompts where breadth is the
23
+ // point). Env override for projects that want broader recall or to A/B.
24
+ const MAX_RESULTS = Number(process.env.CLAUDE_MEM_UPS_MAX_RESULTS || 3);
18
25
  const LOOKBACK_MS = 60 * 86400000; // 60 days
19
26
 
27
+ // v2.56.x: Past-similar-questions fallback row cap. Cut from 3 → 1 after
28
+ // 30d transcript scan (#8062 follow-up, 2026-05-09) showed UPS prompt-fallback
29
+ // path contributing ~24% of session injection budget with near-zero cite-recall.
30
+ // Unlike the obs FTS path (TOP_REL_FLOOR + BM25 gates), prompt-fallback has no
31
+ // quality gate — only BM25 ordering — so additional rows inflate noise without
32
+ // improving signal. Env-overridable for projects that want broader prompt recall.
33
+ const PROMPT_FALLBACK_LIMIT = Number(process.env.CLAUDE_MEM_UPS_PROMPT_FALLBACK_LIMIT || 1);
34
+
20
35
  // T3 (v2.31): per-row BM25 magnitude floor. OBS_BM25 (in scoring-sql.mjs)
21
36
  // returns the raw bm25() value — negative, smaller = better. Multiplied by
22
37
  // decay × type-quality × (0.5+0.5·importance), sign stays negative. We
@@ -104,6 +119,82 @@ function isFollowUpSession() {
104
119
  } catch { return false; }
105
120
  }
106
121
 
122
+ // ─── Explicit-signal gate (v2.57.x) ─────────────────────────────────────────
123
+ //
124
+ // Upstream gate that decides whether the FTS / prompt-fallback paths run at
125
+ // all. Per cite-recall baseline 2026-04-22 → 2026-05-09 (29 sessions),
126
+ // UserPromptSubmit injection cite-recall = 25.8% (132/178 silent injections)
127
+ // vs PreToolUse:Read/Edit at 94.1/94.2%. The gap is the always-search policy
128
+ // burning tokens on prompts the model never refers back to.
129
+ //
130
+ // Retreat: only inject when the prompt carries a signal that names something
131
+ // concrete. Four orthogonal channels:
132
+ // (1) error-signature — extractErrorSignature() typed exception match
133
+ // (2) file-reference — extractFiles() basename.ext or path separator
134
+ // (3) detected intent — detectIntent() catches recall words ("记得", "之前",
135
+ // "previously") + actionable keywords (bugfix/test/
136
+ // decision/refactor/perf/schema/implement/...)
137
+ // (4) tech identifier — CamelCase / snake_case / ALL_CAPS_CONST /
138
+ // kebab-case (≥3 segments). Conservative — drops
139
+ // single-lowercase-word identifiers ("mem", "fix")
140
+ // since those are 99% prose noise.
141
+ //
142
+ // "No signal" prompts ("does this work?", "how is it going") return no
143
+ // injection. PreToolUse file-keyed hook is independent (94% recall track,
144
+ // fires on Edit/Read/Write file paths) — not affected.
145
+ //
146
+ // Env override: CLAUDE_MEM_UPS_REQUIRE_SIGNAL=0 restores always-search.
147
+ // Default ON.
148
+ //
149
+ // Note for OR-fallback gate (#8144) interaction: this gate is upstream of
150
+ // score-quality gates (OR_TOP_BM25_FLOOR / TOP_REL_FLOOR). They compose:
151
+ // presence-gate decides whether to search at all; score-gate trims the
152
+ // returned set. Orthogonal layers — turning REQUIRE_SIGNAL off restores
153
+ // the previous behavior where score-gates alone control noise.
154
+ //
155
+ // Regex post-review (Important #1): bare-acronym ALL_CAPS arm `[A-Z]{2,}…`
156
+ // false-positived on common English prose (IBM, NPM, THE, BSD, ASCII).
157
+ // camelCase arm `[a-z][a-z0-9]*[A-Z]…` false-positived on iOS, eBay.
158
+ // Five-arm tightening:
159
+ // • snake_case — requires `_` between lowercase tokens
160
+ // • CONST_CASE — requires `_` between uppercase tokens (catches
161
+ // MAX_RESULTS, CLAUDE_MEM_DIR, OBS_BM25)
162
+ // • ACRONYM_w_digit — bare 2+-cap run with at least one digit (catches
163
+ // FTS5, MD5, HTML5, OAUTH2, HTTP2; rejects IBM/NPM/
164
+ // THE/BSD/ASCII which never carry digits in prose)
165
+ // • camelCase — requires ≥2 lowercase before the first cap
166
+ // (excludes iOS, eBay; allows getUserById, parseJsonFromLLM)
167
+ // • kebab-case — ≥3 segments (pre-tool-use; excludes "easy-to-use")
168
+ // Bare digitless acronyms (URL, JWT, JSON, HTTP) no longer match — they
169
+ // typically appear alongside intent keywords or files anyway, so the gate
170
+ // catches the prompt via those channels rather than the identifier itself.
171
+ const TECH_IDENTIFIER_RE = /\b(?:[a-z][a-z0-9]*_[a-z0-9_]+|[A-Z][A-Z0-9]*_[A-Z0-9_]+|[A-Z]{2,}[0-9][A-Z0-9_]*|[a-z]{2,}[A-Z][a-zA-Z0-9]+|[a-z]+(?:-[a-z]+){2,})\b/;
172
+
173
+ // CJK presence channel (Important #2): bilingual users (project memory
174
+ // `feedback_*` calls this out explicitly) ask CJK questions that may carry
175
+ // genuine debug intent without containing an English identifier. CJK is
176
+ // information-dense — an 8-effective-unit prompt rarely encodes "how is it
177
+ // going"-style noise. Threshold mirrors shouldSkip's CJK floor.
178
+ const CJK_CHAR_RE = /[一-鿿぀-ヿ]/;
179
+ const CJK_MIN_EFFECTIVE_LEN = 8;
180
+
181
+ const REQUIRE_EXPLICIT_SIGNAL = process.env.CLAUDE_MEM_UPS_REQUIRE_SIGNAL !== '0';
182
+
183
+ export function hasExplicitSignal(text, { errSig, files, intent } = {}) {
184
+ if (!text) return false;
185
+ if (errSig) return true;
186
+ if (Array.isArray(files) && files.length > 0) return true;
187
+ if (intent) return true;
188
+ // Recompute path — fires only when the caller passes `text` alone (test
189
+ // entry point); production caller in main() always pre-computes all three.
190
+ if (errSig === undefined && extractErrorSignature(text)) return true;
191
+ if (files === undefined && extractFiles(text).length > 0) return true;
192
+ if (intent === undefined && detectIntent(text)) return true;
193
+ if (TECH_IDENTIFIER_RE.test(text)) return true;
194
+ if (CJK_CHAR_RE.test(text) && computeEffectiveLen(text) >= CJK_MIN_EFFECTIVE_LEN) return true;
195
+ return false;
196
+ }
197
+
107
198
  // ─── DB Query Functions ─────────────────────────────────────────────────────
108
199
 
109
200
  // Returns { rows, mode } where mode is 'AND' (initial pass), 'OR' (fallback
@@ -385,11 +476,17 @@ async function main() {
385
476
  let hookData;
386
477
  try { hookData = JSON.parse(raw); } catch { return; }
387
478
 
388
- const promptText = hookData.prompt || hookData.user_prompt;
389
- if (!promptText || typeof promptText !== 'string') return;
479
+ const rawPrompt = hookData.prompt || hookData.user_prompt;
480
+ if (!rawPrompt || typeof rawPrompt !== 'string') return;
390
481
 
391
- // Skip internal protocol messages
392
- if (promptText.startsWith('<task-notification>')) return;
482
+ // Skip internal protocol messages (check on raw text — protocol sentinel
483
+ // would never legitimately be wrapped in <private>).
484
+ if (rawPrompt.startsWith('<task-notification>')) return;
485
+
486
+ // Strip <private>...</private> blocks before length gates and FTS query
487
+ // construction — private content must not pad effective length nor leak
488
+ // into the FTS MATCH query terms. Mirrors hook.mjs handleUserPrompt.
489
+ const promptText = stripPrivate(rawPrompt);
393
490
 
394
491
  // Skip short/confirmation/slash-command/simple-op prompts
395
492
  if (shouldSkip(promptText)) return;
@@ -426,12 +523,25 @@ async function main() {
426
523
  )
427
524
  : [];
428
525
 
526
+ // v2.57.x explicit-signal gate. Compute files once for both the gate and
527
+ // the file-recall path below — extractFiles is regex over the prompt,
528
+ // safe to call eagerly. errSig + intent already computed above.
529
+ const filesForGate = extractFiles(promptText);
530
+ const signalPresent = hasExplicitSignal(promptText, {
531
+ errSig, files: filesForGate, intent,
532
+ });
533
+
429
534
  if (intent?.useRecent) {
430
535
  // Recall intent: show recent observations
431
536
  rows = searchRecent(db, project, intent.limit);
537
+ } else if (REQUIRE_EXPLICIT_SIGNAL && !signalPresent) {
538
+ // No explicit signal — skip FTS pipeline + prompt-fallback. sigRows
539
+ // is already empty (errSig was null else signalPresent would be true).
540
+ // Registry skill pointer below remains unaffected (its own name match).
541
+ rows = [];
432
542
  } else {
433
543
  // FTS search: use the prompt as query, optionally type-filtered
434
- const files = extractFiles(promptText);
544
+ const files = filesForGate;
435
545
  let ftsResult = searchByFts(db, promptText, project, intent?.limit || MAX_RESULTS, intent?.type || null);
436
546
  // Fallback: if typed search returned nothing, retry without type filter
437
547
  if (ftsResult.rows.length === 0 && intent?.type) {
@@ -497,9 +607,14 @@ async function main() {
497
607
  // suppress the fallback to avoid noise). Namespace prompt IDs with
498
608
  // a "P" prefix so shouldSkipByDedup's Set comparison doesn't collide
499
609
  // with future observation IDs.
610
+ //
611
+ // v2.57.x: also gated by signalPresent. The prompt-fallback path has
612
+ // no quality gate (only BM25 ordering — see PROMPT_FALLBACK_LIMIT
613
+ // rationale at top), so injecting it on no-signal prompts is the
614
+ // single highest-noise UPS path. Restored when REQUIRE_SIGNAL=0.
500
615
  let promptRows = [];
501
- if (rows.length === 0) {
502
- promptRows = searchByUserPrompts(db, promptText, project, 3);
616
+ if (rows.length === 0 && (!REQUIRE_EXPLICIT_SIGNAL || signalPresent)) {
617
+ promptRows = searchByUserPrompts(db, promptText, project, PROMPT_FALLBACK_LIMIT);
503
618
  }
504
619
 
505
620
  const candidateIds = rows.length > 0
package/source-files.mjs CHANGED
@@ -37,6 +37,7 @@ export const SOURCE_FILES = [
37
37
  'lib/doctor-drift.mjs',
38
38
  'lib/stats-quality.mjs',
39
39
  'lib/low-signal-patterns.mjs',
40
+ 'lib/private-strip.mjs',
40
41
  'lib/citation-tracker.mjs',
41
42
  'lib/summary-extractor.mjs',
42
43
  'lib/id-routing.mjs',
@@ -53,3 +54,23 @@ export const SOURCE_FILES = [
53
54
  'adopt-content.mjs',
54
55
  'adopt-cli.mjs',
55
56
  ];
57
+
58
+ /**
59
+ * Hook scripts that direct-install (non-plugin) mode must materialize under
60
+ * ~/.claude-mem-lite/scripts/ — settings.json hook commands resolve to these
61
+ * absolute paths. Plugin mode does not consume this directory (it runs scripts
62
+ * from ${CLAUDE_PLUGIN_ROOT} instead).
63
+ *
64
+ * Single source of truth for both install.mjs (initial install) and
65
+ * hook-update.mjs (auto-update): pre-v2.55 hook-update copied the entire
66
+ * scripts/ tree from the GitHub Releases tarball, which silently shipped
67
+ * dev-only files (mock-claude.mjs, extract-repos.mjs, p0-forward-probe.mjs…)
68
+ * to every user's data dir on the first auto-update.
69
+ */
70
+ export const HOOK_SCRIPT_FILES = [
71
+ 'post-tool-use.sh',
72
+ 'user-prompt-search.js',
73
+ 'prompt-search-utils.mjs',
74
+ 'pre-tool-recall.js',
75
+ 'pre-skill-bridge.js',
76
+ ];
package/utils.mjs CHANGED
@@ -13,6 +13,7 @@ export { DECAY_HALF_LIFE_BY_TYPE, DEFAULT_DECAY_HALF_LIFE_MS, OBS_BM25, SESS_BM2
13
13
  export { cjkBigrams, extractCjkSynonymTokens, extractCjkKeywords, extractCjkLikePatterns, SYNONYM_MAP, expandToken, sanitizeFtsQuery, relaxFtsQueryToOr, FTS_STOP_WORDS, CJK_COMPOUNDS } from './nlp.mjs';
14
14
  export { resolveProject, _resetProjectCache } from './project-utils.mjs';
15
15
  export { scrubSecrets, SECRET_PATTERNS } from './secret-scrub.mjs';
16
+ export { stripPrivate } from './lib/private-strip.mjs';
16
17
  export { truncate, typeIcon, fmtDate, fmtTime, isoWeekKey } from './format-utils.mjs';
17
18
  export { computeMinHash, estimateJaccardFromMinHash, jaccardSimilarity } from './hash-utils.mjs';
18
19
  export { detectBashSignificance, extractErrorKeywords, extractFilePaths, stripTestSuffix } from './bash-utils.mjs';