claude-mem-lite 2.34.4 → 2.34.6
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/hooks/hooks.json
CHANGED
package/install.mjs
CHANGED
|
@@ -489,7 +489,10 @@ async function install() {
|
|
|
489
489
|
};
|
|
490
490
|
|
|
491
491
|
const memPreToolRecall = {
|
|
492
|
-
|
|
492
|
+
// v2.34.6: Read added to cover planning-Read (pre-Edit exploration).
|
|
493
|
+
// Read-path uses a tighter filter (lesson_learned required, top-1,
|
|
494
|
+
// 120-char truncation, silent-on-empty) — see scripts/pre-tool-recall.js.
|
|
495
|
+
matcher: 'Edit|Write|NotebookEdit|Read',
|
|
493
496
|
hooks: [
|
|
494
497
|
{
|
|
495
498
|
type: 'command',
|
package/package.json
CHANGED
|
@@ -91,14 +91,23 @@ try {
|
|
|
91
91
|
// Parse event
|
|
92
92
|
let filePath;
|
|
93
93
|
let sessionId;
|
|
94
|
+
let toolName;
|
|
94
95
|
try {
|
|
95
96
|
const event = JSON.parse(input);
|
|
96
97
|
filePath = event.tool_input?.file_path;
|
|
97
98
|
sessionId = event.session_id || null;
|
|
99
|
+
toolName = event.tool_name || null;
|
|
98
100
|
} catch { process.exit(0); }
|
|
99
101
|
|
|
100
102
|
if (!filePath) process.exit(0);
|
|
101
103
|
|
|
104
|
+
// v2.34.6 Gap 3: Read-side recall with asymmetric quiet-mode. Reads have
|
|
105
|
+
// lower per-event information value than Edits (passive observation, may
|
|
106
|
+
// not lead to action), so inject less per Read. Cooldown is shared with
|
|
107
|
+
// Edit via per-filePath session state — Read→Edit in the same session is
|
|
108
|
+
// not double-injected. See CHANGELOG v2.34.6 for the data behind 120/1/no-nudge.
|
|
109
|
+
const isRead = toolName === 'Read';
|
|
110
|
+
|
|
102
111
|
// v2.33.1: session-scoped cooldown. Within one session, same file recalls
|
|
103
112
|
// once; cross-session, each session gets fresh nudges. Legacy 5-min global
|
|
104
113
|
// cooldown only applies when no session_id is present.
|
|
@@ -135,6 +144,18 @@ try {
|
|
|
135
144
|
// Priority: 1) observations with lesson_learned (most actionable for preventing repeat bugs)
|
|
136
145
|
// 2) bugfix/decision types with importance>=2 (contextual history)
|
|
137
146
|
// Skip pure change/discovery without lessons — they add noise without actionable value.
|
|
147
|
+
//
|
|
148
|
+
// v2.34.6: Read tightens the filter to require lesson_learned (drops type-OR
|
|
149
|
+
// fallback — decision/bugfix WITHOUT lesson add context noise to passive Reads
|
|
150
|
+
// where the agent isn't committed to a change). Edit/Write keep the wider
|
|
151
|
+
// filter for decision-point context.
|
|
152
|
+
const typeFallback = isRead
|
|
153
|
+
? 'AND o.lesson_learned IS NOT NULL AND o.lesson_learned != \'\''
|
|
154
|
+
: `AND (
|
|
155
|
+
(o.lesson_learned IS NOT NULL AND o.lesson_learned != '')
|
|
156
|
+
OR o.type IN ('bugfix', 'decision')
|
|
157
|
+
)`;
|
|
158
|
+
const obsLimit = isRead ? 1 : 2;
|
|
138
159
|
const rows = db.prepare(`
|
|
139
160
|
SELECT DISTINCT o.id, o.type, o.title, o.lesson_learned
|
|
140
161
|
FROM observations o
|
|
@@ -145,14 +166,11 @@ try {
|
|
|
145
166
|
AND o.superseded_at IS NULL
|
|
146
167
|
AND o.created_at_epoch > ?
|
|
147
168
|
AND (of2.filename = ? OR of2.filename LIKE ? ESCAPE '\\')
|
|
148
|
-
|
|
149
|
-
(o.lesson_learned IS NOT NULL AND o.lesson_learned != '')
|
|
150
|
-
OR o.type IN ('bugfix', 'decision')
|
|
151
|
-
)
|
|
169
|
+
${typeFallback}
|
|
152
170
|
ORDER BY
|
|
153
171
|
CASE WHEN o.lesson_learned IS NOT NULL AND o.lesson_learned != '' THEN 0 ELSE 1 END,
|
|
154
172
|
o.created_at_epoch DESC
|
|
155
|
-
LIMIT
|
|
173
|
+
LIMIT ${obsLimit}
|
|
156
174
|
`).all(project, cutoff, filePath, likePattern);
|
|
157
175
|
|
|
158
176
|
// T9: also query the `events` table — after T9, bugfix/lesson/decision/etc.
|
|
@@ -163,6 +181,11 @@ try {
|
|
|
163
181
|
// matching "myfoo.mjs".
|
|
164
182
|
const fnameEscaped = fname.replace(/%/g, '\\%').replace(/_/g, '\\_');
|
|
165
183
|
const filePathEscaped = filePath.replace(/%/g, '\\%').replace(/_/g, '\\_');
|
|
184
|
+
// v2.34.6: Read also tightens the events query — only rows with a non-empty
|
|
185
|
+
// body (= lesson equivalent). Edit path keeps the wider net since the agent
|
|
186
|
+
// is about to change the file and benefits from any contextual signal.
|
|
187
|
+
const eventsBodyFilter = isRead ? "AND body IS NOT NULL AND body != ''" : '';
|
|
188
|
+
const eventsLimit = isRead ? 1 : 2;
|
|
166
189
|
let eventRows = [];
|
|
167
190
|
try {
|
|
168
191
|
eventRows = db.prepare(`
|
|
@@ -173,25 +196,29 @@ try {
|
|
|
173
196
|
AND superseded_at_epoch IS NULL
|
|
174
197
|
AND created_at_epoch > ?
|
|
175
198
|
AND (file_paths LIKE ? ESCAPE '\\' OR file_paths LIKE ? ESCAPE '\\')
|
|
199
|
+
${eventsBodyFilter}
|
|
176
200
|
ORDER BY created_at_epoch DESC
|
|
177
|
-
LIMIT
|
|
201
|
+
LIMIT ${eventsLimit}
|
|
178
202
|
`).all(project, cutoff, `%"${fnameEscaped}"%`, `%"${filePathEscaped}"%`);
|
|
179
203
|
} catch { /* events table may not exist on pre-v2.31 DBs — silent */ }
|
|
180
204
|
|
|
181
|
-
// Merge: observations first (they carry richer lesson_learned), then events
|
|
182
|
-
//
|
|
183
|
-
const
|
|
205
|
+
// Merge: observations first (they carry richer lesson_learned), then events.
|
|
206
|
+
// Edit/Write caps at 3 total; Read caps at 1 (single most-actionable hit).
|
|
207
|
+
const mergeCap = isRead ? 1 : 3;
|
|
208
|
+
const allRows = [...rows, ...eventRows].slice(0, mergeCap);
|
|
184
209
|
|
|
185
210
|
// v2.31 T2: emit JSON with hookSpecificOutput.additionalContext so the message
|
|
186
211
|
// reliably renders across CC variants (sdscc drops plain-text stdout from PreToolUse).
|
|
187
212
|
// suppressOutput:true hides it from transcript mode per CC hook docs.
|
|
188
213
|
const lines = [];
|
|
214
|
+
// v2.34.6: Read mode uses 120-char truncation (Edit mode keeps the 240-char
|
|
215
|
+
// cap from R3-UX). Rationale: Read is a one-shot nudge with 1 lesson max;
|
|
216
|
+
// Edit is a 3-lesson decision-support injection where the fuller lesson tail
|
|
217
|
+
// carries the actionable "Fix:" guidance — short enough per-lesson at 240,
|
|
218
|
+
// but the total payload is bounded by the 3-row limit and the cooldown.
|
|
219
|
+
const LESSON_MAX = isRead ? 120 : 240;
|
|
189
220
|
if (allRows.length > 0) {
|
|
190
221
|
lines.push(`[mem] Lessons for ${fname}:`);
|
|
191
|
-
// R3-UX: raised from 120 → 240 after measuring 97% of lessons exceed 120 chars
|
|
192
|
-
// (p50=218, avg=247). Previous limit truncated the actionable "Fix:" tail in 80%
|
|
193
|
-
// of lessons containing it. 3 × 240 ≈ 180 tokens/Edit — negligible context cost.
|
|
194
|
-
const LESSON_MAX = 240;
|
|
195
222
|
for (const r of allRows) {
|
|
196
223
|
if (r.lesson_learned) {
|
|
197
224
|
const lesson = r.lesson_learned.length > LESSON_MAX
|
|
@@ -205,22 +232,29 @@ try {
|
|
|
205
232
|
lines.push(` #${r.id} [${r.type}] ${title}`);
|
|
206
233
|
}
|
|
207
234
|
}
|
|
208
|
-
} else {
|
|
209
|
-
// R-4:
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
//
|
|
235
|
+
} else if (!isRead) {
|
|
236
|
+
// R-4: Edit/Write empty → short backfill reminder. Two goals: (1) Claude
|
|
237
|
+
// sees that the system actually ran, (2) Claude is nudged to save a lesson
|
|
238
|
+
// after a non-obvious bug. Reminder is one line to keep per-Edit cost low.
|
|
239
|
+
//
|
|
240
|
+
// v2.34.6: Read does NOT emit this nudge. Read is passive — the agent
|
|
241
|
+
// isn't necessarily about to solve anything, so /lesson prompts are noise.
|
|
242
|
+
// Empty Reads exit silently, saving ~60 tokens × (every empty-file Read).
|
|
213
243
|
lines.push(`[mem] No prior lessons for ${fname} — if you solve a non-obvious bug here, run: /lesson --file ${fname} "<root cause + fix>"`);
|
|
214
244
|
}
|
|
215
245
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
246
|
+
if (lines.length > 0) {
|
|
247
|
+
process.stdout.write(JSON.stringify({
|
|
248
|
+
suppressOutput: true,
|
|
249
|
+
hookSpecificOutput: {
|
|
250
|
+
hookEventName: 'PreToolUse',
|
|
251
|
+
additionalContext: lines.join('\n'),
|
|
252
|
+
},
|
|
253
|
+
}));
|
|
254
|
+
}
|
|
255
|
+
// Cooldown applies on ALL branches (including silent-Read) so subsequent
|
|
256
|
+
// calls on the same file in the same session don't re-query — preserving
|
|
257
|
+
// the per-filePath invariant that underpins Read→Edit dedup.
|
|
224
258
|
cooldown[filePath] = now;
|
|
225
259
|
writeCooldown(cooldownPath, cooldown, isSessionScoped);
|
|
226
260
|
} catch {
|
|
@@ -163,6 +163,43 @@ function searchByFile(db, files, project, limit) {
|
|
|
163
163
|
});
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
// v2.34.5 Gap 1: prompts-table fallback. When observations-based paths
|
|
167
|
+
// (FTS / file-recall / sigRows / recent) all return empty, scan the user's
|
|
168
|
+
// own past prompts — meta/UX/"did we discuss this" questions often match
|
|
169
|
+
// prior prompts even when no observation was saved. Uses a simpler BM25
|
|
170
|
+
// ranking with no scoring multipliers and no top-|rel| gate (prompts are
|
|
171
|
+
// sparser and more surface-form than observations; the gate would rarely
|
|
172
|
+
// fire and mostly kill real hits).
|
|
173
|
+
function searchByUserPrompts(db, queryText, project, limit) {
|
|
174
|
+
const ftsQuery = sanitizeFtsQuery(queryText);
|
|
175
|
+
if (!ftsQuery) return [];
|
|
176
|
+
|
|
177
|
+
const cutoff = Date.now() - LOOKBACK_MS;
|
|
178
|
+
const sql = `
|
|
179
|
+
SELECT up.id, up.prompt_text, up.created_at_epoch,
|
|
180
|
+
bm25(user_prompts_fts) as relevance
|
|
181
|
+
FROM user_prompts_fts
|
|
182
|
+
JOIN user_prompts up ON up.id = user_prompts_fts.rowid
|
|
183
|
+
JOIN sdk_sessions s ON s.content_session_id = up.content_session_id
|
|
184
|
+
WHERE user_prompts_fts MATCH ?
|
|
185
|
+
AND s.project = ?
|
|
186
|
+
AND up.created_at_epoch > ?
|
|
187
|
+
ORDER BY relevance
|
|
188
|
+
LIMIT ?
|
|
189
|
+
`;
|
|
190
|
+
|
|
191
|
+
let rows = db.prepare(sql).all(ftsQuery, project, cutoff, limit);
|
|
192
|
+
|
|
193
|
+
if (rows.length === 0) {
|
|
194
|
+
const orQuery = relaxFtsQueryToOr(ftsQuery);
|
|
195
|
+
if (orQuery) {
|
|
196
|
+
try { rows = db.prepare(sql).all(orQuery, project, cutoff, limit); } catch {}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return rows;
|
|
201
|
+
}
|
|
202
|
+
|
|
166
203
|
function searchRecent(db, project, limit) {
|
|
167
204
|
const cutoff = Date.now() - LOOKBACK_MS;
|
|
168
205
|
// R1: exclude LOW_SIGNAL degraded titles from "recent" recall intent
|
|
@@ -225,6 +262,20 @@ function formatResults(rows) {
|
|
|
225
262
|
return lines.join('\n');
|
|
226
263
|
}
|
|
227
264
|
|
|
265
|
+
// v2.34.5 Gap 1: distinct header signals to Claude that these are prior
|
|
266
|
+
// *user questions*, not codebase lessons — helps the reader interpret the
|
|
267
|
+
// row correctly (surface-form match, not a saved insight). Truncate to 80
|
|
268
|
+
// chars (slightly longer than obs titles because prompts carry more context).
|
|
269
|
+
function formatPromptResults(rows) {
|
|
270
|
+
if (!rows || rows.length === 0) return null;
|
|
271
|
+
const lines = ['[mem] Past similar questions:'];
|
|
272
|
+
for (const r of rows) {
|
|
273
|
+
const text = truncate((r.prompt_text || '').replace(/\s+/g, ' '), 80);
|
|
274
|
+
lines.push(`P#${r.id} 💬 ${text}`);
|
|
275
|
+
}
|
|
276
|
+
return lines.join('\n');
|
|
277
|
+
}
|
|
278
|
+
|
|
228
279
|
// ─── Registry Skill Pointer (T4 v2.31) ─────────────────────────────────────
|
|
229
280
|
// Formerly "auto-load": we used to read the full SKILL.md body (up to 16KB)
|
|
230
281
|
// and inject it into stdout on keyword match. Now we only emit a short
|
|
@@ -376,10 +427,24 @@ async function main() {
|
|
|
376
427
|
rows = [...sigRows, ...rows.filter(r => !sigIds.has(r.id))].slice(0, MAX_RESULTS);
|
|
377
428
|
}
|
|
378
429
|
|
|
379
|
-
|
|
430
|
+
// v2.34.5 Gap 1: if observations-based search drew a blank, try the
|
|
431
|
+
// user_prompts corpus. Only fires when `rows` is empty (obs hits
|
|
432
|
+
// suppress the fallback to avoid noise). Namespace prompt IDs with
|
|
433
|
+
// a "P" prefix so shouldSkipByDedup's Set comparison doesn't collide
|
|
434
|
+
// with future observation IDs.
|
|
435
|
+
let promptRows = [];
|
|
436
|
+
if (rows.length === 0) {
|
|
437
|
+
promptRows = searchByUserPrompts(db, promptText, project, 3);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const candidateIds = rows.length > 0
|
|
441
|
+
? rows.map(r => r.id)
|
|
442
|
+
: promptRows.map(r => `P${r.id}`);
|
|
380
443
|
const dedupSkip = shouldSkipByDedup(candidateIds, INJECTED_IDS_FILE);
|
|
381
444
|
|
|
382
|
-
const output = !dedupSkip
|
|
445
|
+
const output = !dedupSkip
|
|
446
|
+
? (rows.length > 0 ? formatResults(rows) : formatPromptResults(promptRows))
|
|
447
|
+
: null;
|
|
383
448
|
if (output) {
|
|
384
449
|
process.stdout.write(output + '\n');
|
|
385
450
|
// Write injected IDs for dedup with hook.mjs handleUserPrompt + self-dedup
|