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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/hook.mjs +10 -16
- package/lib/cite-back-hint.mjs +98 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
package/lib/cite-back-hint.mjs
CHANGED
|
@@ -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;
|