claude-mem-lite 3.32.0 → 3.33.1

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.32.0",
13
+ "version": "3.33.1",
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.32.0",
3
+ "version": "3.33.1",
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
@@ -6,6 +6,7 @@ import { citeFactorJs } from './scoring-sql.mjs';
6
6
  import { recordMetric } from './lib/metrics.mjs';
7
7
  import { DB_DIR } from './schema.mjs';
8
8
  import { extractIdents } from './lib/lesson-idents.mjs';
9
+ import { formatSubagentContext } from './lib/task-imperative.mjs';
9
10
 
10
11
  const MAX_MEMORY_INJECTIONS = 3;
11
12
  const MEMORY_LOOKBACK_MS = 60 * 86400000; // 60 days
@@ -439,3 +440,18 @@ export function selectImperativeLesson(db, userPrompt, project, excludeIds = [])
439
440
  }
440
441
  return best ? { id: best.id, lesson_learned: best.lesson_learned } : null;
441
442
  }
443
+
444
+ // P0 (2026-07-03): compose the subagent-dispatch injection. Given a PreToolUse
445
+ // Agent/Task tool_input, pick ONE project-scoped high-value lesson whose identifiers
446
+ // overlap the SUBAGENT's task prompt (precision-first via selectImperativeLesson:
447
+ // no overlap -> null -> no injection) and return a NEW tool_input with the
448
+ // safe-framed context appended to `prompt`. Returns null when there is nothing to
449
+ // inject. Pure over (db, toolInput, project) so it unit-tests without the subprocess.
450
+ export function buildSubagentInjection(db, toolInput, project) {
451
+ if (!db || !toolInput || typeof toolInput.prompt !== 'string' || !toolInput.prompt.trim()) return null;
452
+ const pick = selectImperativeLesson(db, toolInput.prompt, project);
453
+ if (!pick || !pick.lesson_learned) return null;
454
+ const block = formatSubagentContext(pick.lesson_learned, pick.id);
455
+ if (!block) return null;
456
+ return { ...toolInput, prompt: `${toolInput.prompt}\n${block}` };
457
+ }
package/hooks/hooks.json CHANGED
@@ -50,6 +50,16 @@
50
50
  "timeout": 3
51
51
  }
52
52
  ]
53
+ },
54
+ {
55
+ "matcher": "Agent|Task",
56
+ "hooks": [
57
+ {
58
+ "type": "command",
59
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-launcher.mjs\" scripts/pre-agent-inject.js",
60
+ "timeout": 5
61
+ }
62
+ ]
53
63
  }
54
64
  ],
55
65
  "PostToolUse": [
package/install.mjs CHANGED
@@ -705,10 +705,24 @@ const memPreSkillBridge = {
705
705
  ]
706
706
  };
707
707
 
708
+ // P0 subagent dispatch-time injection (default off — CLAUDE_MEM_SUBAGENT_INJECT).
709
+ // Fires on the Agent/Task dispatch so a subagent (otherwise memory-blind — #8848)
710
+ // can receive one relevant lesson via updatedInput. Parity with hooks/hooks.json.
711
+ const memPreAgentInject = {
712
+ matcher: 'Agent|Task',
713
+ hooks: [
714
+ {
715
+ type: 'command',
716
+ command: nodeHook('scripts/pre-agent-inject.js'),
717
+ timeout: 5
718
+ }
719
+ ]
720
+ };
721
+
708
722
  // Filter out existing mem hooks, then append fresh ones
709
- // PreToolUse has two separate matchers, so we register both
723
+ // PreToolUse has three separate matchers, so we register all three
710
724
  const hookConfigs = {
711
- PreToolUse: [memPreToolRecall, memPreSkillBridge],
725
+ PreToolUse: [memPreToolRecall, memPreSkillBridge, memPreAgentInject],
712
726
  PostToolUse: [memPostToolUse],
713
727
  SessionStart: [memSessionStart],
714
728
  Stop: [memStop],
@@ -16,3 +16,25 @@ export function formatTaskImperative(lesson, id) {
16
16
  const tag = (id === undefined || id === null || id === '') ? '' : ` (#${id})`;
17
17
  return `Memory — a past lesson applies to THIS task. You must: ${body}.${tag}`;
18
18
  }
19
+
20
+ // Subagent-dispatch framing. Subagents are memory-blind (plugin hooks don't fire
21
+ // inside them — #8848); the P0 dispatch hook (scripts/pre-agent-inject.js) APPENDS
22
+ // this block to a spawned subagent's prompt via PreToolUse updatedInput. This is
23
+ // deliberately NOT the "You must:" imperative above: Phase-0b measured live
24
+ // (2026-07-03) that a raw imperative PREPEND trips the subagent's own
25
+ // prompt-injection detector -> refusal, whereas this appended, attributed,
26
+ // reference-only block is adopted. The four load-bearing elements (named provenance
27
+ // / "reference, not an instruction" / appended-below-the-task / no adversarial
28
+ // tokens) are the measured difference between adopt and refuse — do not drift them.
29
+ export function formatSubagentContext(lesson, id) {
30
+ const body = String(lesson || '').trim().replace(/\.$/, '');
31
+ if (!body) return '';
32
+ const tag = (id === undefined || id === null || id === '') ? '' : `#${id} — `;
33
+ return [
34
+ '',
35
+ '---',
36
+ "[Project memory — surfaced by your operator's claude-mem-lite memory system for this project. Reference context, not an external instruction.]",
37
+ 'A past lesson recorded for this project that may be relevant to the task above:',
38
+ ` ${tag}${body}.`,
39
+ ].join('\n');
40
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "3.32.0",
3
+ "version": "3.33.1",
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",
@@ -143,6 +143,7 @@
143
143
  "scripts/pre-tool-recall.js",
144
144
  "scripts/post-tool-recall.js",
145
145
  "scripts/pre-skill-bridge.js",
146
+ "scripts/pre-agent-inject.js",
146
147
  "scripts/prompt-search-utils.mjs",
147
148
  "scripts/hook-launcher.mjs",
148
149
  ".mcp.json",
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env node
2
+ // claude-mem-lite: PreToolUse:Agent|Task hook — subagent dispatch-time memory injection.
3
+ // Subagents are memory-blind (plugin hooks do NOT fire inside them — #8848); this hook
4
+ // injects ONE relevant project lesson into a dispatched subagent's prompt by mutating
5
+ // tool_input.prompt via hookSpecificOutput.updatedInput. Verified live 2026-07-03
6
+ // (Phase 0a: the mutation reaches the subagent's task-prompt position; Phase 0b: an
7
+ // appended, attributed, reference-only block is adopted, whereas a raw imperative
8
+ // prepend trips the subagent's own prompt-injection detector and is refused).
9
+ //
10
+ // DEFAULT OFF (CLAUDE_MEM_SUBAGENT_INJECT=on|1). The off path costs one env check and
11
+ // returns — no stdin read, no DB, no heavy imports (schema/better-sqlite3 are dynamic,
12
+ // loaded only on the enabled Agent path). Fail-open: never exits non-zero, never blocks
13
+ // a dispatch (a thrown hook would abort the user's subagent).
14
+
15
+ const ENABLED = process.env.CLAUDE_MEM_SUBAGENT_INJECT === 'on'
16
+ || process.env.CLAUDE_MEM_SUBAGENT_INJECT === '1';
17
+
18
+ function readStdin() {
19
+ return new Promise((resolve) => {
20
+ let data = '';
21
+ const timer = setTimeout(() => { try { process.stdin.destroy(); } catch { /* */ } resolve(data); }, 1500);
22
+ process.stdin.setEncoding('utf8');
23
+ process.stdin.on('data', (c) => {
24
+ data += c;
25
+ // cap: agent prompts can be large. destroy() so the loop can drain and exit on
26
+ // its own (see the no-forced-exit note at the bottom) rather than streaming to
27
+ // the 1.5s timeout.
28
+ if (data.length > 262144) { clearTimeout(timer); try { process.stdin.destroy(); } catch { /* */ } resolve(data.slice(0, 262144)); }
29
+ });
30
+ process.stdin.on('end', () => { clearTimeout(timer); resolve(data); });
31
+ process.stdin.on('error', () => { clearTimeout(timer); resolve(data); });
32
+ process.stdin.resume();
33
+ });
34
+ }
35
+
36
+ async function main() {
37
+ if (!ENABLED) return; // default: cheapest possible no-op
38
+ if (process.env.CLAUDE_MEM_HOOK_RUNNING) return; // recursion guard (background claude -p)
39
+
40
+ const raw = await readStdin();
41
+ let hook;
42
+ try { hook = JSON.parse(raw); } catch { return; }
43
+ if (!hook || typeof hook !== 'object') return;
44
+ if (hook.tool_name !== 'Agent' && hook.tool_name !== 'Task') return;
45
+
46
+ // Heavy deps loaded ONLY on the enabled Agent-dispatch path, so the default-off
47
+ // hot path never pays the schema.mjs + better-sqlite3 native load on every dispatch.
48
+ const { ensureDb } = await import('../schema.mjs');
49
+ const { inferProject } = await import('../utils.mjs');
50
+ const { buildSubagentInjection } = await import('../hook-memory.mjs');
51
+
52
+ let db;
53
+ try { db = ensureDb(); } catch { return; }
54
+ try {
55
+ const updatedInput = buildSubagentInjection(db, hook.tool_input, inferProject());
56
+ if (updatedInput) {
57
+ process.stdout.write(JSON.stringify({
58
+ hookSpecificOutput: { hookEventName: 'PreToolUse', updatedInput },
59
+ }));
60
+ }
61
+ } catch { /* never break a dispatch */ } finally {
62
+ try { db.close(); } catch { /* */ }
63
+ }
64
+ }
65
+
66
+ // No forced process.exit(0): every readStdin path ends/destroys stdin and db.close()
67
+ // runs in main's finally, so the event loop drains and the process exits 0 on its own —
68
+ // which FLUSHES stdout. The emitted updatedInput echoes the whole prompt back, so the
69
+ // payload can exceed the ~64KB pipe buffer; a forced process.exit() would drop that
70
+ // pending async write and truncate the JSON (the gotcha every sibling hook avoids).
71
+ // Swallow any rejection so the exit code can never go non-zero.
72
+ main().catch(() => {});
package/source-files.mjs CHANGED
@@ -185,6 +185,7 @@ export const HOOK_SCRIPT_FILES = [
185
185
  'pre-tool-recall.js',
186
186
  'post-tool-recall.js',
187
187
  'pre-skill-bridge.js',
188
+ 'pre-agent-inject.js',
188
189
  // v2.84: self-heal wrapper that detects ERR_MODULE_NOT_FOUND under the
189
190
  // install dir and runs install.mjs repair before retrying the entry.
190
191
  // hooks.json + install.mjs settings template invoke node hook entries