claude-mem-lite 2.75.0 → 2.76.0
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 +11 -5
- package/lib/citation-tracker.mjs +68 -0
- package/mem-cli.mjs +19 -1
- package/package.json +1 -1
package/hook.mjs
CHANGED
|
@@ -47,7 +47,7 @@ import { handleLLMEpisode, handleLLMSummary, saveObservation, buildImmediateObse
|
|
|
47
47
|
import { scrubRecord } from './lib/scrub-record.mjs';
|
|
48
48
|
import {
|
|
49
49
|
extractCitationsFromTranscript,
|
|
50
|
-
|
|
50
|
+
extractAllInjected,
|
|
51
51
|
bumpCitationAccess,
|
|
52
52
|
computeCiteRecall,
|
|
53
53
|
applyCitationDecay,
|
|
@@ -525,11 +525,17 @@ async function handleStop() {
|
|
|
525
525
|
}
|
|
526
526
|
|
|
527
527
|
// v32 citation-decay: tighter feedback loop on top of P4. Re-scan
|
|
528
|
-
// transcript with main-thread filter, extract injected IDs from
|
|
529
|
-
//
|
|
530
|
-
// applyCitationDecay's contract.
|
|
528
|
+
// transcript with main-thread filter, extract injected IDs from BOTH
|
|
529
|
+
// surfaces (PTR + UserPromptSubmit <memory-context>) via extractAllInjected,
|
|
530
|
+
// then mutate importance/streak per applyCitationDecay's contract.
|
|
531
|
+
// Cheap (file still in OS cache).
|
|
532
|
+
//
|
|
533
|
+
// v34.x: pre-v34 this only saw pre-tool-recall injections, leaving the
|
|
534
|
+
// UPS surface (highest-volume — all decision-type FTS hits) starved.
|
|
535
|
+
// Union closed by extractAllInjected — one integration point so the
|
|
536
|
+
// contract test in tests/citation-tracker-userprompt.test.mjs covers it.
|
|
531
537
|
try {
|
|
532
|
-
const injected =
|
|
538
|
+
const injected = extractAllInjected(transcriptPath);
|
|
533
539
|
if (injected.size > 0) {
|
|
534
540
|
const citedMain = extractCitationsFromTranscript(transcriptPath, { mainOnly: true });
|
|
535
541
|
const r = applyCitationDecay(db, project, injected, citedMain, sessionId);
|
package/lib/citation-tracker.mjs
CHANGED
|
@@ -198,6 +198,74 @@ export function extractInjectedFromPreToolUse(transcriptPath) {
|
|
|
198
198
|
return ids;
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
+
// v34.x: UserPromptSubmit injection extractor. hook.mjs handleUserPrompt emits
|
|
202
|
+
// formatMemoryLine `- [type] title | Lesson: X (#NN)[ [verify-before-use]]`,
|
|
203
|
+
// which INJECTED_RE (anchored on `#NN [type]`) never matched — leaving the
|
|
204
|
+
// highest-volume injection surface invisible to applyCitationDecay. The two
|
|
205
|
+
// extractors are disjoint by design: PTR has `[type]` AFTER `#NN`, UPS has
|
|
206
|
+
// `(#NN)` at end-of-line.
|
|
207
|
+
//
|
|
208
|
+
// Line-scan with `- [` prefix gate so a lesson body containing a back-reference
|
|
209
|
+
// like "see (#999)" doesn't pollute the injected set (would streak-uncite an
|
|
210
|
+
// obs we never actually displayed as a top-level entry).
|
|
211
|
+
const UPS_LINE_PREFIX = '- [';
|
|
212
|
+
const UPS_ID_RE = /\(#(\d{1,7})\)/g;
|
|
213
|
+
const UPS_COMMAND_SUFFIX = 'hook.mjs user-prompt';
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Extract observation IDs injected by the UserPromptSubmit `<memory-context>`
|
|
217
|
+
* block (hook.mjs handleUserPrompt). Disjoint from pre-tool-recall extraction —
|
|
218
|
+
* the Stop handler unions both via extractAllInjected.
|
|
219
|
+
*
|
|
220
|
+
* @param {string|null|undefined} transcriptPath
|
|
221
|
+
* @returns {Set<number>}
|
|
222
|
+
*/
|
|
223
|
+
export function extractInjectedFromUserPromptSubmit(transcriptPath) {
|
|
224
|
+
const ids = new Set();
|
|
225
|
+
if (!transcriptPath || !existsSync(transcriptPath)) return ids;
|
|
226
|
+
let raw;
|
|
227
|
+
try { raw = readFileSync(transcriptPath, 'utf8'); } catch { return ids; }
|
|
228
|
+
for (const line of raw.split('\n')) {
|
|
229
|
+
if (!line.trim()) continue;
|
|
230
|
+
let entry;
|
|
231
|
+
try { entry = JSON.parse(line); } catch { continue; }
|
|
232
|
+
if (entry.type !== 'attachment') continue;
|
|
233
|
+
const att = entry.attachment;
|
|
234
|
+
if (!att || att.type !== 'hook_success') continue;
|
|
235
|
+
// Suffix match — survives plugin-cache vs symlinked-install path differences.
|
|
236
|
+
if (!(att.command || '').includes(UPS_COMMAND_SUFFIX)) continue;
|
|
237
|
+
const stdout = att.stdout || '';
|
|
238
|
+
if (!stdout.includes('<memory-context')) continue;
|
|
239
|
+
for (const memLine of stdout.split('\n')) {
|
|
240
|
+
if (!memLine.startsWith(UPS_LINE_PREFIX)) continue;
|
|
241
|
+
// Take the LAST (#NN) on the line — formatMemoryLine puts the obs id
|
|
242
|
+
// in trailing parens, possibly followed by ` [verify-before-use]`. Any
|
|
243
|
+
// earlier (#NN) refs are inside title/lesson text (per the test that
|
|
244
|
+
// pins "see (#999)" → NOT extracted).
|
|
245
|
+
const matches = [...memLine.matchAll(UPS_ID_RE)];
|
|
246
|
+
if (matches.length === 0) continue;
|
|
247
|
+
const id = Number(matches[matches.length - 1][1]);
|
|
248
|
+
if (Number.isInteger(id) && id > 0 && id < 1e7) ids.add(id);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return ids;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Union of pre-tool-recall + UserPromptSubmit injection IDs for a transcript.
|
|
256
|
+
* Single integration point the Stop handler calls — keeps hook.mjs's wiring
|
|
257
|
+
* a one-liner and gives the contract test something to assert against.
|
|
258
|
+
*
|
|
259
|
+
* @param {string|null|undefined} transcriptPath
|
|
260
|
+
* @returns {Set<number>}
|
|
261
|
+
*/
|
|
262
|
+
export function extractAllInjected(transcriptPath) {
|
|
263
|
+
return new Set([
|
|
264
|
+
...extractInjectedFromPreToolUse(transcriptPath),
|
|
265
|
+
...extractInjectedFromUserPromptSubmit(transcriptPath),
|
|
266
|
+
]);
|
|
267
|
+
}
|
|
268
|
+
|
|
201
269
|
const IMPORTANCE_CAP = 3;
|
|
202
270
|
const IMPORTANCE_FLOOR = 0;
|
|
203
271
|
const UNCITED_STREAK_THRESHOLD = 3;
|
package/mem-cli.mjs
CHANGED
|
@@ -2417,11 +2417,29 @@ function cmdCitationStats(db, args) {
|
|
|
2417
2417
|
LIMIT 10
|
|
2418
2418
|
`).all(cutoff);
|
|
2419
2419
|
|
|
2420
|
+
// v34.x: surface pre-v34 data pollution. applyCitationDecay bumps cited_count
|
|
2421
|
+
// and decay_seen_count atomically (same UPDATE statement), so the invariant
|
|
2422
|
+
// cited_count <= decay_seen_count holds for every resolution this codepath
|
|
2423
|
+
// performs. Yet a small set of obs violate it — these are pre-v34 rows
|
|
2424
|
+
// where a backfill seeded cited_count without populating decay_seen_count.
|
|
2425
|
+
// Without this note, those rows make per-project cite_pct >100% with no
|
|
2426
|
+
// explanation. Cite rate stays unbiased for obs created after this commit.
|
|
2427
|
+
const pollutedRows = db.prepare(`
|
|
2428
|
+
SELECT COUNT(*) AS n FROM observations
|
|
2429
|
+
WHERE cited_count > decay_seen_count
|
|
2430
|
+
AND COALESCE(compressed_into, 0) = 0
|
|
2431
|
+
AND superseded_at IS NULL
|
|
2432
|
+
`).get();
|
|
2433
|
+
const dataPollutionNote = pollutedRows.n > 0
|
|
2434
|
+
? `${pollutedRows.n} obs have cited_count > decay_seen_count (pre-v34 backfill — invariant holds for new data).`
|
|
2435
|
+
: null;
|
|
2436
|
+
|
|
2420
2437
|
if (json) {
|
|
2421
|
-
out(JSON.stringify({ window_days: days, per_project: perProject, decay_queue: decayQueue, promoted, demoted }, null, 2));
|
|
2438
|
+
out(JSON.stringify({ window_days: days, per_project: perProject, decay_queue: decayQueue, promoted, demoted, data_pollution_note: dataPollutionNote }, null, 2));
|
|
2422
2439
|
return;
|
|
2423
2440
|
}
|
|
2424
2441
|
|
|
2442
|
+
if (dataPollutionNote) out(`Note: ${dataPollutionNote}\n`);
|
|
2425
2443
|
out(`Cite rate by project (last ${days}d, cited / decay-resolutions):`);
|
|
2426
2444
|
for (const r of perProject) {
|
|
2427
2445
|
const rate = r.resolved > 0 ? (r.cited * 100 / r.resolved).toFixed(1) + '%' : '—';
|