claude-mem-lite 3.16.2 → 3.16.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.16.2",
13
+ "version": "3.16.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.16.2",
3
+ "version": "3.16.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/adopt-content.mjs CHANGED
@@ -43,6 +43,8 @@ PreToolUse hooks already run \`mem_recall\` for past lessons before Read/Edit/Wr
43
43
  | Deferring to a future session | \`mem_defer({title, priority:1|2|3, detail})\`; when fixed, add \`closes_deferred=[N]\` to \`mem_save\` |
44
44
  | Looking up past work / history | \`mem_search "keywords"\` · \`mem_recent\` · \`mem_timeline\` |
45
45
 
46
+ Path cost is round-trips, not milliseconds: the PreToolUse hook above already recalls (0 calls) — prefer it. For an explicit query, if these \`mem_*\` tools are deferred behind ToolSearch this session, the Bash CLI (exact path in the detail doc) is one call vs two (ToolSearch + call).
47
+
46
48
  Full tool + CLI tables, citation/decay rules, and save discipline → \`.claude/plugin_claude_mem_lite.md\``;
47
49
  }
48
50
 
@@ -77,6 +79,16 @@ PreToolUse hook 在你 Read / Edit / Write 文件前已自动 \`mem_recall\` 该
77
79
  \`mem_search\` / \`mem_recent\` / \`mem_recall\` / \`mem_get\` / \`mem_save\` / \`mem_timeline\` +
78
80
  \`mem_defer\` / \`mem_defer_list\` / \`mem_defer_drop\`。
79
81
 
82
+ ### 选 MCP 还是 CLI:按 round-trip,不是执行毫秒
83
+
84
+ 真正的开销是模型往返次数,不是工具执行——暖 MCP 调用 ~25ms、CLI 冷启 ~90ms,在一次推理(秒级)面前都是噪声。按往返次数选路:
85
+
86
+ 1. **被动 hook(0 往返)**:上面的 PreToolUse recall 已自动跑,最快,优先采纳,别重复调。
87
+ 2. **CLI via Bash(1 往返)**:工具多的会话里 \`mem_*\` 会被 defer 到 ToolSearch 后面——这时一次 MCP 调用 = ToolSearch + call = **2 往返**,而 Bash 跑一条 CLI 只 **1 往返**。派出去的子 agent 通常也拿不到 \`mem_*\` 工具,CLI 是它唯一的 1-往返路径。用下面「CLI 速查」表里的命令。
88
+ 3. **MCP 直调(已加载时 1 往返)**:\`mem_*\` 已在上下文里(未被 defer)就直接调,暖进程执行最快、省掉 ToolSearch。
89
+
90
+ 一句话:能让 hook 代劳就别调;要显式查,若得先 ToolSearch 才能用 \`mem_*\`,改跑 CLI。
91
+
80
92
  | 时机 | 工具 | 关键参数 |
81
93
  |------|------|----------|
82
94
  | Edit / Write 前 | \`mem_recall\` | \`file="<路径>"\`(hook 通常已代劳) |
package/format-utils.mjs CHANGED
@@ -24,6 +24,32 @@ export function truncate(str, max = 80) {
24
24
  return str.slice(0, end) + '\u2026';
25
25
  }
26
26
 
27
+ /**
28
+ * Render the PostToolUse error-recall hint block (hook.mjs::triggerErrorRecall).
29
+ * The single most-relevant hit (rows[0]) that carries a lesson_learned gets its
30
+ * lesson INLINED, so the agent can act with zero follow-up round-trips: the old
31
+ * "pointer + mem_get for details" form cost a deferred mem_get (2 model turns in
32
+ * tool-heavy sessions, where mem_* is gated behind ToolSearch) at the exact
33
+ * moment a fix is needed. Later rows stay as #ID pointers to keep the injected
34
+ * payload bounded (one body, not three). Upstream noise gating (low-signal title
35
+ * exclusion) is the SELECT's job (see triggerErrorRecall).
36
+ * @param {Array<{id:number,type:string,title:string,lesson_learned?:string}>} rows
37
+ * @returns {string} stdout block (trailing newline) or '' when there are no rows
38
+ */
39
+ export function formatErrorRecallHints(rows) {
40
+ if (!rows || rows.length === 0) return '';
41
+ const lines = rows.map((r, i) => {
42
+ const head = ` #${r.id} [${r.type}] ${truncate(r.title, 60)}`;
43
+ // Inline the lesson body for the single most-relevant hit only (bounded payload).
44
+ if (i === 0 && typeof r.lesson_learned === 'string' && r.lesson_learned.trim()) {
45
+ return `${head} \u2014 ${truncate(r.lesson_learned.trim(), 200)}`;
46
+ }
47
+ return head;
48
+ });
49
+ const ids = rows.map(r => r.id).join(',');
50
+ return `[claude-mem-lite] Related memories found for this error:\n${lines.join('\n')}\n \u2192 Use mem_get(ids=[${ids}]) for details.\n`;
51
+ }
52
+
27
53
  /**
28
54
  * Map observation type to its display emoji icon.
29
55
  * @param {string} type Observation type (decision, bugfix, feature, etc.)
package/hook.mjs CHANGED
@@ -26,7 +26,7 @@ import {
26
26
  truncate, inferProject, detectBashSignificance,
27
27
  extractErrorKeywords, extractFilePaths, isRelatedToEpisode,
28
28
  makeEntryDesc, scrubSecrets, stripPrivate, EDIT_TOOLS, debugCatch, debugLog,
29
- COMPRESSED_AUTO, COMPRESSED_PENDING_PURGE, OBS_BM25,
29
+ COMPRESSED_AUTO, COMPRESSED_PENDING_PURGE, OBS_BM25, notLowSignalTitleClause, formatErrorRecallHints,
30
30
  } from './utils.mjs';
31
31
  import {
32
32
  readEpisodeRaw, episodeFile,
@@ -363,19 +363,18 @@ function triggerErrorRecall(db, toolInput, response) {
363
363
 
364
364
  const nowR = Date.now();
365
365
  const rows = db.prepare(`
366
- SELECT o.id, o.type, o.title
366
+ SELECT o.id, o.type, o.title, o.lesson_learned
367
367
  FROM observations_fts
368
368
  JOIN observations o ON observations_fts.rowid = o.id
369
369
  WHERE observations_fts MATCH ? AND o.project = ?
370
+ AND ${notLowSignalTitleClause('o')}
370
371
  ORDER BY ${OBS_BM25}
371
372
  * (1.0 + EXP(-0.693 * (? - o.created_at_epoch) / 1209600000.0))
372
373
  LIMIT 3
373
374
  `).all(ftsQuery, project, nowR);
374
375
 
375
- if (rows.length > 0) {
376
- const hints = rows.map(r => ` #${r.id} [${r.type}] ${truncate(r.title, 60)}`).join('\n');
377
- process.stdout.write(`[claude-mem-lite] Related memories found for this error:\n${hints}\n → Use mem_get(ids=[${rows.map(r => r.id).join(',')}]) for details.\n`);
378
- }
376
+ const out = formatErrorRecallHints(rows);
377
+ if (out) process.stdout.write(out);
379
378
  } catch (e) { debugCatch(e, 'triggerErrorRecall'); }
380
379
  }
381
380
 
@@ -10,7 +10,8 @@
10
10
  // column UPDATE including access_count. Per-row UPDATEs wrapped in try-catch
11
11
  // to prevent SQLITE_CORRUPT_VTAB cascades from stopping the whole scan.
12
12
 
13
- import { readFileSync, existsSync } from 'fs';
13
+ import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
14
+ import { join } from 'path';
14
15
  import { debugCatch } from '../utils.mjs';
15
16
 
16
17
  // `#123` / `#45678` at a word boundary — matches the CLAUDE.md cite pattern.
@@ -155,6 +156,11 @@ export function bumpCitationAccess(db, ids, project) {
155
156
  // Bounded type list mirrors observations.type CHECK + the events table's allowed
156
157
  // event_type values these surfaces can emit.
157
158
  const INJECTED_RE = /#(\d{1,7})\s+\[(bugfix|decision|change|discovery|feature|refactor|lesson)\]/g;
159
+ // Line-anchored variant: a genuine injected ROW begins (after its short indent) with
160
+ // `#NN [type]`. pre-tool-recall AND error-recall inline a lesson_learned body into the
161
+ // row; a body that quotes another obs ("same as #1234 [decision]") must NOT count as
162
+ // injected: that id would pollute the citation-decay denominator and falsely demote.
163
+ const INJECTED_ROW_RE = new RegExp('^\\s{0,6}' + INJECTED_RE.source);
158
164
 
159
165
  // Add a numeric obs id to `set` if it parses to a sane in-range positive int.
160
166
  function addObsId(set, raw) {
@@ -229,9 +235,10 @@ export function extractInjectedFromPreToolUse(transcriptPath, opts = {}) {
229
235
  const ids = new Set();
230
236
  eachHookAttachment(transcriptPath, ({ command, text }) => {
231
237
  if (!command.includes('pre-tool-recall')) return;
232
- INJECTED_RE.lastIndex = 0;
233
- let m;
234
- while ((m = INJECTED_RE.exec(text))) addObsId(ids, m[1]);
238
+ for (const line of text.split('\n')) {
239
+ const m = INJECTED_ROW_RE.exec(line);
240
+ if (m) addObsId(ids, m[1]);
241
+ }
235
242
  }, opts);
236
243
  return ids;
237
244
  }
@@ -293,11 +300,14 @@ export function extractInjectedFromErrorRecall(transcriptPath, opts = {}) {
293
300
  eachHookAttachment(transcriptPath, ({ command, text }) => {
294
301
  if (!command.includes('post-tool-use')) return;
295
302
  if (!text.includes('Related memories found for this error')) return;
296
- // INJECTED_RE requires `#NN [type]`, so the trailing
297
- // `→ Use mem_get(ids=[7933,8455])` line (bare numbers) is not matched.
298
- INJECTED_RE.lastIndex = 0;
299
- let m;
300
- while ((m = INJECTED_RE.exec(text))) addObsId(ids, m[1]);
303
+ // Per-line anchored: match only a row that STARTS with `#NN [type]` (after its
304
+ // indent), NOT every such token in the block. The inlined lesson body (v3.16.x)
305
+ // can quote another obs id, which must not enter the injected set; the trailing
306
+ // `Use mem_get(ids=[...])` line (bare numbers) is excluded too.
307
+ for (const line of text.split('\n')) {
308
+ const m = INJECTED_ROW_RE.exec(line);
309
+ if (m) addObsId(ids, m[1]);
310
+ }
301
311
  }, opts);
302
312
  return ids;
303
313
  }
@@ -353,6 +363,72 @@ export function extractAllInjected(transcriptPath, opts = {}) {
353
363
  ]);
354
364
  }
355
365
 
366
+ /**
367
+ * Cite-recall over ONE transcript file, using the SAME methodology as the
368
+ * citation-decay loop: injected = extractAllInjected (OUR hook injections only),
369
+ * cited = #NN in assistant text, ratio = |injected ∩ cited| / |injected|.
370
+ *
371
+ * Thread is keyed by FILE LOCATION, not the isSidechain field. Claude Code writes
372
+ * each subagent's turns to a SEPARATE file (<session>/subagents/agent-*.jsonl),
373
+ * NOT inline in the parent transcript (verified empirically: 0 isSidechain records
374
+ * across 60 parent transcripts; the subagent files carry isSidechain=true). So a
375
+ * whole subagent file IS the sidechain — aggregateProjectCiteRecall splits by path.
376
+ *
377
+ * @param {string} transcriptPath
378
+ * @returns {{injected: number, cited: number, recalled: number, ratio: number}}
379
+ */
380
+ export function computeThreadCiteRecall(transcriptPath) {
381
+ const injected = extractAllInjected(transcriptPath);
382
+ const cited = extractCitationsFromTranscript(transcriptPath);
383
+ let recalled = 0;
384
+ for (const id of injected) if (cited.has(id)) recalled++;
385
+ return {
386
+ injected: injected.size,
387
+ cited: cited.size,
388
+ recalled,
389
+ ratio: injected.size > 0 ? recalled / injected.size : 0,
390
+ };
391
+ }
392
+
393
+ /**
394
+ * Aggregate cite-recall across a project's transcripts, split MAIN vs SIDECHAIN
395
+ * by file location. `txDir` = ~/.claude/projects/<encoded>/. Main = the top-level
396
+ * <session>.jsonl files; sidechain = every <session>/subagents/agent-*.jsonl.
397
+ * Descends ONE level into the literal `subagents` subdir only — no unbounded
398
+ * recursion. mtime-gated by `cutoff` (epoch ms; 0 = no window).
399
+ *
400
+ * @param {string} txDir
401
+ * @param {{cutoff?: number}} [opts]
402
+ * @returns {{main: {injected:number,recalled:number,files:number}, sidechain: {injected:number,recalled:number,files:number,withInjections:number}}}
403
+ */
404
+ export function aggregateProjectCiteRecall(txDir, { cutoff = 0 } = {}) {
405
+ const main = { injected: 0, recalled: 0, files: 0 };
406
+ const sidechain = { injected: 0, recalled: 0, files: 0, withInjections: 0 };
407
+ const within = (p) => { try { return statSync(p).mtimeMs >= cutoff; } catch { return false; } };
408
+ let entries;
409
+ try { entries = readdirSync(txDir, { withFileTypes: true }); } catch { return { main, sidechain }; }
410
+ for (const ent of entries) {
411
+ const full = join(txDir, ent.name);
412
+ if (ent.isFile() && ent.name.endsWith('.jsonl')) {
413
+ if (!within(full)) continue;
414
+ const r = computeThreadCiteRecall(full);
415
+ main.injected += r.injected; main.recalled += r.recalled; main.files++;
416
+ } else if (ent.isDirectory()) {
417
+ let subFiles;
418
+ try { subFiles = readdirSync(join(full, 'subagents')); } catch { continue; }
419
+ for (const sf of subFiles) {
420
+ if (!sf.endsWith('.jsonl')) continue;
421
+ const sp = join(full, 'subagents', sf);
422
+ if (!within(sp)) continue;
423
+ const r = computeThreadCiteRecall(sp);
424
+ sidechain.injected += r.injected; sidechain.recalled += r.recalled; sidechain.files++;
425
+ if (r.injected > 0) sidechain.withInjections++;
426
+ }
427
+ }
428
+ }
429
+ return { main, sidechain };
430
+ }
431
+
356
432
  /**
357
433
  * True iff the transcript contains at least one non-whitespace text block from
358
434
  * a main-thread assistant turn. Gates the citation-decay loop so a tool-only
package/mem-cli.mjs CHANGED
@@ -26,8 +26,9 @@ import { buildSessionContextLines } from './hook-context.mjs';
26
26
  import { cmdAdopt, cmdUnadopt } from './adopt-cli.mjs';
27
27
  import { parseIntFlag, isNumericToken } from './lib/cli-flags.mjs';
28
28
  import { auditMemdir, memdirPath } from './memdir.mjs';
29
+ import { aggregateProjectCiteRecall } from './lib/citation-tracker.mjs';
29
30
  import { probeOtherSources as probeIdSources, bucketIdTokens } from './lib/id-routing.mjs';
30
- import { join, sep } from 'path';
31
+ import { join, sep, dirname } from 'path';
31
32
  import { readFileSync, existsSync, readdirSync } from 'fs';
32
33
 
33
34
  // v2.41: shared CLI helpers extracted to cli/common.mjs. Keep this file as the
@@ -2210,20 +2211,61 @@ function cmdMemdirAudit(args) {
2210
2211
  if (nonCompliant > 0) process.exitCode = 1;
2211
2212
  }
2212
2213
 
2214
+ // `citation-stats --sidechain`: subagent (sidechain) cite-recall, the blind spot the
2215
+ // main decay loop excludes (it runs mainOnly). aggregateProjectCiteRecall scans THIS
2216
+ // project's transcripts: top-level <session>.jsonl = main, and
2217
+ // <session>/subagents/agent-*.jsonl = sidechain (descends ONE level into the literal
2218
+ // subagents/ dir only, no unbounded recursion). Same methodology, so comparable.
2219
+ function _reportSidechainCiteRecall({ days, json }) {
2220
+ const cutoff = Date.now() - days * 86400 * 1000;
2221
+ // memdir = ~/.claude/projects/<encoded>/memory; transcripts are its siblings.
2222
+ const txDir = dirname(memdirPath(process.cwd()));
2223
+ const { main, sidechain } = aggregateProjectCiteRecall(txDir, { cutoff });
2224
+ const rate = b => (b.injected > 0 ? (100 * b.recalled / b.injected) : null);
2225
+ const sideRate = rate(sidechain), mainRate = rate(main);
2226
+
2227
+ if (json) {
2228
+ out(JSON.stringify({
2229
+ window_days: days,
2230
+ main: { ...main, rate: mainRate },
2231
+ sidechain: { ...sidechain, rate: sideRate },
2232
+ }));
2233
+ return;
2234
+ }
2235
+
2236
+ const pct = r => (r === null ? '—' : `${r.toFixed(1)}%`);
2237
+ out(`Sidechain (subagent) cite-recall — last ${days}d:`);
2238
+ out(` main ${pct(mainRate).padStart(6)} recalled ${main.recalled} / injected ${main.injected} (${main.files} transcript(s))`);
2239
+ out(` sidechain ${pct(sideRate).padStart(6)} recalled ${sidechain.recalled} / injected ${sidechain.injected} (${sidechain.files} subagent file(s), ${sidechain.withInjections} with injections)`);
2240
+ if (sidechain.files > 0 && sidechain.injected === 0) {
2241
+ out(' → subagent transcripts exist but received ZERO memory injections: claude-mem-lite');
2242
+ out(' hooks do NOT fire inside subagents (no PreToolUse/PostToolUse recall, no');
2243
+ out(' SessionStart block, no mem_* tools). Subagents are memory-blind — giving them');
2244
+ out(' memory needs a NEW surface (inject at Agent/Task dispatch), not deepening.');
2245
+ } else if (sidechain.files === 0) {
2246
+ out(' → no subagent transcripts in window.');
2247
+ }
2248
+ }
2249
+
2213
2250
  /**
2214
2251
  * `citation-stats` — visualize the citation-decay feedback loop:
2215
2252
  * per-project cite rate + active decay queue + recently promoted.
2216
2253
  * Read-only over observations.
2217
2254
  *
2218
2255
  * Flags:
2219
- * --json machine-readable output
2220
- * --days N project cite-rate window (default 7)
2256
+ * --json machine-readable output
2257
+ * --days N project cite-rate window (default 7)
2258
+ * --sidechain subagent (sidechain) cite-recall vs main — the decay-loop blind spot
2221
2259
  */
2222
2260
  function cmdCitationStats(db, args) {
2223
2261
  const { flags } = parseArgs(args);
2224
2262
  const json = flags.json === true || flags.json === 'true';
2225
2263
  const days = parseIntFlag(flags.days, { name: '--days', defaultValue: 7, max: 365 });
2226
2264
 
2265
+ if (flags.sidechain === true || flags.sidechain === 'true') {
2266
+ return _reportSidechainCiteRecall({ days, json });
2267
+ }
2268
+
2227
2269
  const cutoff = Date.now() - days * 86400 * 1000;
2228
2270
  const perProject = db.prepare(`
2229
2271
  SELECT project,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "3.16.2",
3
+ "version": "3.16.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",
@@ -15,9 +15,14 @@ import { CLI_INVOKE } from './cli-path.mjs';
15
15
  // Decision-rules sections; keeps the irreducible CLI/MCP tool list. Intended
16
16
  // for users who adopted invited-memory (MEMORY.md sentinel carries the same
17
17
  // triggers at higher authority). Default false preserves v2.31.2 behavior.
18
+ // The CLI-vs-MCP round-trip routing rule lives in BASE (not VERBOSE) on purpose:
19
+ // it must reach adopted (quiet) projects too, where tool-heavy sessions defer the
20
+ // mem_* tools behind ToolSearch (CLI via Bash = 1 model round-trip; ToolSearch +
21
+ // call = 2). Execution latency is NOT the lever — warm MCP (~25ms) actually beats
22
+ // CLI cold-start (~90ms); both are noise against one model turn. Round-trips are.
18
23
 
19
24
  const INSTRUCTIONS_BASE = [
20
- 'Long-term memory across sessions. Hooks auto-inject context; CLI preferred for explicit queries.',
25
+ 'Long-term memory across sessions. Hooks auto-inject context (0 round-trips) prefer adopting that over any call. For an explicit query, pick the path with fewer model round-trips (CLI vs MCP below).',
21
26
  '',
22
27
  `CLI (via Bash) — invoke as \`${CLI_INVOKE} <cmd>\` (resolves on any install shape; the bare \`claude-mem-lite\` shorthand works only after an optional global \`npm i -g claude-mem-lite\`):`,
23
28
  ` ${CLI_INVOKE} search "query" — FTS5 full-text search`,
@@ -27,7 +32,7 @@ const INSTRUCTIONS_BASE = [
27
32
  ` ${CLI_INVOKE} get 42,43 — full details by ID`,
28
33
  ` ${CLI_INVOKE} timeline --anchor 42 — chronological context`,
29
34
  '',
30
- 'MCP tools: mem_search, mem_recent, mem_save, mem_get, mem_recall, mem_timeline for programmatic access (always availableno PATH/CLI install needed).',
35
+ 'MCP tools: mem_search, mem_recent, mem_save, mem_get, mem_recall, mem_timeline. If already loaded, call directly (warm server, fastest path). In tool-heavy sessions these are deferred behind ToolSearch if using one would cost a ToolSearch load first, run the Bash CLI above instead: one call, not two. Neither needs a PATH/CLI install.',
31
36
  'mem_save: Save non-obvious insights (bugfix lessons, architecture decisions).',
32
37
  'Search tips: short keywords (2-3 words), filter with obs_type when relevant.',
33
38
  ];
package/utils.mjs CHANGED
@@ -14,7 +14,7 @@ export { cjkBigrams, extractCjkSynonymTokens, extractCjkKeywords, extractCjkLike
14
14
  export { resolveProject, _resetProjectCache } from './project-utils.mjs';
15
15
  export { scrubSecrets, SECRET_PATTERNS } from './secret-scrub.mjs';
16
16
  export { stripPrivate } from './lib/private-strip.mjs';
17
- export { truncate, typeIcon, fmtDate, fmtTime, isoWeekKey } from './format-utils.mjs';
17
+ export { truncate, typeIcon, fmtDate, fmtTime, isoWeekKey, formatErrorRecallHints } from './format-utils.mjs';
18
18
  export { computeMinHash, estimateJaccardFromMinHash, jaccardSimilarity } from './hash-utils.mjs';
19
19
  export { detectBashSignificance, extractErrorKeywords, extractFilePaths, stripTestSuffix } from './bash-utils.mjs';
20
20