claude-mem-lite 2.83.0 → 2.83.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": "2.83.0",
13
+ "version": "2.83.1",
14
14
  "source": "./",
15
15
  "description": "Lightweight persistent memory system for Claude Code — FTS5 search, episode batching, error-triggered recall"
16
16
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "2.83.0",
3
+ "version": "2.83.1",
4
4
  "description": "Lightweight persistent memory system for Claude Code — FTS5 search, episode batching, error-triggered recall",
5
5
  "author": {
6
6
  "name": "sdsrss"
package/hook.mjs CHANGED
@@ -61,7 +61,7 @@ import { checkForUpdate } from './hook-update.mjs';
61
61
  import { handleLLMOptimize } from './hook-optimize.mjs';
62
62
  import { silentAutoAdopt, hasAutoAdoptMarker } from './adopt-cli.mjs';
63
63
  import { emitV270UpgradeBanner } from './lib/upgrade-banner.mjs';
64
- import { loadCiteBackForEpisode, buildUnsavedBugfixHint } from './lib/cite-back-hint.mjs';
64
+ import { loadCiteBackForEpisode, buildUnsavedBugfixHint, countUnsavedBugfixShape, buildCiteRecallNudge as libBuildCiteRecallNudge } from './lib/cite-back-hint.mjs';
65
65
  // plugin-cache-guard.mjs loaded dynamically — pre-2.31.2 installs that auto-upgraded
66
66
  // from an older hook-update.mjs SOURCE_FILES (which did not list this module) would
67
67
  // crash on static import. Degrade gracefully to no-op when the module is absent.
@@ -565,7 +565,12 @@ async function handleStop() {
565
565
  // unchanged.
566
566
  try {
567
567
  const stats = computeCiteRecall(transcriptPath);
568
- const payload = { ...stats, project, savedAt: Date.now() };
568
+ // B2 (v2.83.1): also persist the bugfix-shape nudge/save delta so
569
+ // the next SessionStart can surface "N unsaved bugfix-shape edits"
570
+ // alongside cite-recall. Same scan target (transcript already in OS
571
+ // cache); same persistence file; one extra line in buildCiteRecallNudge.
572
+ const bugfixStats = countUnsavedBugfixShape(transcriptPath);
573
+ const payload = { ...stats, ...bugfixStats, project, savedAt: Date.now() };
569
574
  const dest = join(RUNTIME_DIR, `cite-recall-${project.replace(/[^a-zA-Z0-9_.-]/g, '-').slice(0, 64)}.json`);
570
575
  writeFileSync(dest, JSON.stringify(payload), { mode: 0o600 });
571
576
  } catch (e) { debugCatch(e, 'handleStop-cite-recall-persist'); }
@@ -589,21 +594,10 @@ async function handleStop() {
589
594
  // fell below threshold. Empty string = no surface (insufficient signal, recall
590
595
  // already healthy, or feature opted-out via env). Default threshold 0.6,
591
596
  // min injected 5 — both env-overridable for ops tuning + tests.
597
+ // Thin wrapper: lib/cite-back-hint.mjs owns the logic so it stays unit-tested.
598
+ // Passing module-level RUNTIME_DIR keeps the call site identical to pre-v2.83.1.
592
599
  function buildCiteRecallNudge(project) {
593
- if (process.env.CLAUDE_MEM_NO_CITE_NUDGE === '1') return '';
594
- try {
595
- const safe = project.replace(/[^a-zA-Z0-9_.-]/g, '-').slice(0, 64);
596
- const path = join(RUNTIME_DIR, `cite-recall-${safe}.json`);
597
- const raw = readFileSync(path, 'utf8');
598
- const data = JSON.parse(raw);
599
- const threshold = Number(process.env.CLAUDE_MEM_CITE_NUDGE_THRESHOLD) || 0.6;
600
- const minInjected = Number(process.env.CLAUDE_MEM_CITE_NUDGE_MIN_INJECTED) || 5;
601
- if (typeof data.injected !== 'number' || typeof data.ratio !== 'number') return '';
602
- if (data.injected < minInjected) return '';
603
- if (data.ratio >= threshold) return '';
604
- const pct = Math.round(data.ratio * 100);
605
- return `[mem] Last session cite-recall ${pct}% (${data.recalled}/${data.injected}) — when injected lessons (#NN lines) inform your action, cite #NN explicitly so the contract loop stays observable.`;
606
- } catch { return ''; /* no prior file, parse error, or FS error — silent */ }
600
+ return libBuildCiteRecallNudge(project, RUNTIME_DIR);
607
601
  }
608
602
 
609
603
  // GC pre-recall cooldown files older than 24h. Pulled out of pre-tool-recall.js
@@ -12,7 +12,7 @@
12
12
  // Legacy schema (pre-v2.81): { "<path>": <number> } — tolerated, never emits.
13
13
 
14
14
  import { basename, join } from 'path';
15
- import { readFileSync } from 'fs';
15
+ import { readFileSync, existsSync } from 'fs';
16
16
  import { EDIT_TOOLS } from '../utils.mjs';
17
17
 
18
18
  const MAX_FILES = 2;
@@ -103,6 +103,103 @@ function cooldownPathFor(sessionId, runtimeDir) {
103
103
  return join(runtimeDir, `pre-recall-cooldown-${safe}.json`);
104
104
  }
105
105
 
106
+ // ─── countUnsavedBugfixShape (B2, v2.83.1) ──────────────────────────────────
107
+ // At Stop time, count this session's transcript for:
108
+ // • bugfix-shape hint emissions (buildUnsavedBugfixHint fired)
109
+ // • lesson/bugfix save signals (mem_save tool_use with type ∈ {bugfix, lesson}
110
+ // OR Bash `activity save --type lesson|bugfix`)
111
+ //
112
+ // Returns {nudged, saved, unsaved} where unsaved = max(0, nudged - saved).
113
+ // SessionStart surfaces `unsaved` as a follow-on to the cite-recall nudge,
114
+ // turning per-episode hints into cross-session pressure.
115
+ const UNSAVED_BUGFIX_LITERAL = 'Unsaved bugfix-shape';
116
+ const ACTIVITY_SAVE_LESSON_RE = /activity\s+save\s+--type\s+(lesson|bugfix)\b/;
117
+ const MEM_SAVE_TOOL_NAMES = new Set([
118
+ 'mem_save',
119
+ 'mcp__claude_mem_lite__mem_save',
120
+ 'mcp__plugin_claude-mem-lite_mem-lite__mem_save',
121
+ ]);
122
+
123
+ export function countUnsavedBugfixShape(transcriptPath) {
124
+ const empty = { nudged: 0, saved: 0, unsaved: 0 };
125
+ if (!transcriptPath || !existsSync(transcriptPath)) return empty;
126
+ let raw;
127
+ try { raw = readFileSync(transcriptPath, 'utf8'); } catch { return empty; }
128
+
129
+ let nudged = 0;
130
+ let saved = 0;
131
+
132
+ for (const line of raw.split('\n')) {
133
+ if (!line.trim()) continue;
134
+ let entry;
135
+ try { entry = JSON.parse(line); } catch { continue; }
136
+
137
+ // Bugfix-shape hint emissions live in PostToolUse attachment.stdout.
138
+ if (entry.type === 'attachment') {
139
+ const stdout = entry.attachment?.stdout || '';
140
+ if (stdout.includes(UNSAVED_BUGFIX_LITERAL)) nudged++;
141
+ continue;
142
+ }
143
+
144
+ // Save signals live in assistant tool_use blocks.
145
+ if (entry.type === 'assistant' || entry.message?.role === 'assistant') {
146
+ const content = entry.message?.content;
147
+ if (!Array.isArray(content)) continue;
148
+ for (const block of content) {
149
+ if (block.type !== 'tool_use') continue;
150
+ if (MEM_SAVE_TOOL_NAMES.has(block.name)) {
151
+ const t = block.input?.type;
152
+ if (t === 'bugfix' || t === 'lesson') saved++;
153
+ continue;
154
+ }
155
+ if (block.name === 'Bash') {
156
+ const cmd = block.input?.command || '';
157
+ if (ACTIVITY_SAVE_LESSON_RE.test(cmd)) saved++;
158
+ }
159
+ }
160
+ }
161
+ }
162
+
163
+ return { nudged, saved, unsaved: Math.max(0, nudged - saved) };
164
+ }
165
+
166
+ // ─── buildCiteRecallNudge (extracted from hook.mjs for unit-testability) ────
167
+ // Reads `runtime/cite-recall-<project>.json` (written by handleStop) and
168
+ // builds the SessionStart nudge surface. Two independent gates compose:
169
+ // • cite-recall ratio gate: prior session's ratio < threshold (default 0.6)
170
+ // AND injected count >= floor (default 5)
171
+ // • B2 (v2.83.1) unsaved-bugfix gate: `unsaved > 0` (no min-volume floor —
172
+ // the bugfix-shape heuristic already requires ≥3 entries)
173
+ // Either gate can fire independently. Both off → empty string (no surface).
174
+ //
175
+ // Env opt-outs:
176
+ // • CLAUDE_MEM_NO_CITE_NUDGE=1 — disables BOTH gates (full silence)
177
+ // • CLAUDE_MEM_CITE_NUDGE_THRESHOLD — ratio gate threshold (default 0.6)
178
+ // • CLAUDE_MEM_CITE_NUDGE_MIN_INJECTED — ratio gate min-volume (default 5)
179
+ export function buildCiteRecallNudge(project, runtimeDir, env = process.env) {
180
+ if (env.CLAUDE_MEM_NO_CITE_NUDGE === '1') return '';
181
+ try {
182
+ const safe = project.replace(/[^a-zA-Z0-9_.-]/g, '-').slice(0, 64);
183
+ const path = join(runtimeDir, `cite-recall-${safe}.json`);
184
+ const raw = readFileSync(path, 'utf8');
185
+ const data = JSON.parse(raw);
186
+ const threshold = Number(env.CLAUDE_MEM_CITE_NUDGE_THRESHOLD) || 0.6;
187
+ const minInjected = Number(env.CLAUDE_MEM_CITE_NUDGE_MIN_INJECTED) || 5;
188
+ const lines = [];
189
+ if (typeof data.injected === 'number'
190
+ && typeof data.ratio === 'number'
191
+ && data.injected >= minInjected
192
+ && data.ratio < threshold) {
193
+ const pct = Math.round(data.ratio * 100);
194
+ lines.push(`[mem] Last session cite-recall ${pct}% (${data.recalled}/${data.injected}) — when injected lessons (#NN lines) inform your action, cite #NN explicitly so the contract loop stays observable.`);
195
+ }
196
+ if (typeof data.unsaved === 'number' && data.unsaved > 0) {
197
+ lines.push(`[mem] Last session: ${data.unsaved} unsaved bugfix-shape edit(s) — if any was a real fix, save now: /lesson --file <path> "<root cause + fix>"`);
198
+ }
199
+ return lines.join('\n');
200
+ } catch { return ''; }
201
+ }
202
+
106
203
  export function loadCiteBackForEpisode(episode, runtimeDir) {
107
204
  if (!episode || !episode.sessionId || !runtimeDir) return null;
108
205
  let cooldown;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "2.83.0",
3
+ "version": "2.83.1",
4
4
  "description": "Lightweight persistent memory system for Claude Code",
5
5
  "type": "module",
6
6
  "packageManager": "npm@10.9.2",