claude-mem-lite 2.75.0 → 2.77.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/lib/hook-telemetry.mjs +123 -0
- package/mem-cli.mjs +29 -2
- package/package.json +2 -1
- package/scripts/pre-skill-bridge.js +18 -6
- package/scripts/pre-tool-recall.js +28 -7
- package/source-files.mjs +5 -0
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;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// lib/hook-telemetry.mjs — unsampled hook-error log.
|
|
2
|
+
//
|
|
3
|
+
// Distinct from lib/err-sampler.mjs: that one writes 1% of swallowed debugCatch
|
|
4
|
+
// errors into ${dbDir}/errors/ for *production diagnostics* — high cardinality,
|
|
5
|
+
// must be cheap. This one writes 100% of hook script failures into
|
|
6
|
+
// ${runtimeDir}/hook-errors/ for *self-observation* — low cardinality (hooks
|
|
7
|
+
// fail rarely), every event matters because it's the only window into
|
|
8
|
+
// PreToolUse / Skill-bridge / UPS failure modes (DB corruption, schema drift,
|
|
9
|
+
// upstream field rename). Both follow the same JSONL daily-shard layout to
|
|
10
|
+
// keep tooling consistent; the directory split is the contract that says
|
|
11
|
+
// "no sampling here, this is full fidelity".
|
|
12
|
+
//
|
|
13
|
+
// Hook scripts catch *all* errors and exit 0 (must never block Edit/Write).
|
|
14
|
+
// Calling this from inside each catch turns the silent path into a recorded
|
|
15
|
+
// path without changing behavior. All failures inside the recorder itself
|
|
16
|
+
// are swallowed — telemetry must never crash the host hook.
|
|
17
|
+
//
|
|
18
|
+
// Retention: 14 days. GC is lazy on append (cheap stat+unlink loop, only
|
|
19
|
+
// reads the dir, no parse).
|
|
20
|
+
|
|
21
|
+
import { appendFileSync, mkdirSync, existsSync, readdirSync, readFileSync, statSync, unlinkSync } from 'fs';
|
|
22
|
+
import { join } from 'path';
|
|
23
|
+
|
|
24
|
+
const DAY_MS = 86400000;
|
|
25
|
+
const RETENTION_MS = 14 * DAY_MS;
|
|
26
|
+
const HOOK_ERRORS_SUBDIR = 'hook-errors';
|
|
27
|
+
|
|
28
|
+
function today() {
|
|
29
|
+
return new Date(Date.now()).toISOString().slice(0, 10);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function hookErrorsDir(runtimeDir) {
|
|
33
|
+
return join(runtimeDir, HOOK_ERRORS_SUBDIR);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Lazy GC: scan the dir, unlink shards older than RETENTION_MS. Cheap because
|
|
37
|
+
// it never opens files — just statSync + unlinkSync. Called from recordHookError
|
|
38
|
+
// after a successful write, so it amortizes over the failure cadence.
|
|
39
|
+
function pruneOldShards(dir) {
|
|
40
|
+
let entries;
|
|
41
|
+
try { entries = readdirSync(dir); } catch { return; }
|
|
42
|
+
const cutoff = Date.now() - RETENTION_MS;
|
|
43
|
+
for (const f of entries) {
|
|
44
|
+
if (!/^\d{4}-\d{2}-\d{2}\.jsonl$/.test(f)) continue;
|
|
45
|
+
const full = join(dir, f);
|
|
46
|
+
try {
|
|
47
|
+
const st = statSync(full);
|
|
48
|
+
if (st.mtimeMs < cutoff) unlinkSync(full);
|
|
49
|
+
} catch { /* gone or unreadable — skip */ }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Record one hook-script failure to the unsampled JSONL log.
|
|
55
|
+
*
|
|
56
|
+
* @param {string} scope Short label naming the hook script + failure point.
|
|
57
|
+
* Convention: '<script>:<step>' e.g. 'pre-recall:db-open',
|
|
58
|
+
* 'skill-bridge:registry-query'. Truncated to 80 chars.
|
|
59
|
+
* @param {unknown} err Thrown value (Error, string, undefined — all ok).
|
|
60
|
+
* @param {string} runtimeDir Absolute path to ${CLAUDE_MEM_DIR}/runtime/. Caller
|
|
61
|
+
* must resolve this — module avoids importing
|
|
62
|
+
* hook-shared.mjs so it stays usable from the
|
|
63
|
+
* lightweight standalone scripts.
|
|
64
|
+
* @param {object} [ctx] Optional small JSON-safe context (filePath, toolName,
|
|
65
|
+
* sessionId fragment). Stringified + truncated to 240
|
|
66
|
+
* chars to keep shard lines bounded.
|
|
67
|
+
*/
|
|
68
|
+
export function recordHookError(scope, err, runtimeDir, ctx) {
|
|
69
|
+
try {
|
|
70
|
+
if (!runtimeDir || typeof runtimeDir !== 'string') return;
|
|
71
|
+
const dir = hookErrorsDir(runtimeDir);
|
|
72
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
73
|
+
|
|
74
|
+
const line = JSON.stringify({
|
|
75
|
+
ts: new Date().toISOString(),
|
|
76
|
+
scope: String(scope || '').slice(0, 80),
|
|
77
|
+
msg: String(err?.message ?? err ?? '').slice(0, 500),
|
|
78
|
+
stack: typeof err?.stack === 'string' ? err.stack.split('\n').slice(0, 6).join('\n') : undefined,
|
|
79
|
+
ctx: ctx === undefined ? undefined : JSON.stringify(ctx).slice(0, 240),
|
|
80
|
+
}) + '\n';
|
|
81
|
+
|
|
82
|
+
appendFileSync(join(dir, `${today()}.jsonl`), line, { mode: 0o600 });
|
|
83
|
+
// Amortized retention sweep: 14-day window kept clean without a cron.
|
|
84
|
+
pruneOldShards(dir);
|
|
85
|
+
} catch { /* recorder must never throw */ }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Count hook errors with timestamp >= sinceMs. Used by `mem-cli stats` to
|
|
90
|
+
* surface a single self-observation line.
|
|
91
|
+
*
|
|
92
|
+
* Reads every shard (capped at 14 by retention). Each shard is JSONL — parse
|
|
93
|
+
* line-by-line and tolerate malformed lines (treat as zero contribution).
|
|
94
|
+
* Returns a plain count; per-scope breakdown is a follow-up if cardinality
|
|
95
|
+
* grows.
|
|
96
|
+
*/
|
|
97
|
+
export function countRecentHookErrors(runtimeDir, sinceMs) {
|
|
98
|
+
try {
|
|
99
|
+
if (!runtimeDir || typeof runtimeDir !== 'string') return 0;
|
|
100
|
+
const dir = hookErrorsDir(runtimeDir);
|
|
101
|
+
if (!existsSync(dir)) return 0;
|
|
102
|
+
let entries;
|
|
103
|
+
try { entries = readdirSync(dir); } catch { return 0; }
|
|
104
|
+
const cutoffIso = new Date(sinceMs).toISOString();
|
|
105
|
+
let count = 0;
|
|
106
|
+
for (const f of entries) {
|
|
107
|
+
if (!/^\d{4}-\d{2}-\d{2}\.jsonl$/.test(f)) continue;
|
|
108
|
+
let body;
|
|
109
|
+
try { body = readFileSync(join(dir, f), 'utf8'); } catch { continue; }
|
|
110
|
+
for (const line of body.split('\n')) {
|
|
111
|
+
if (!line) continue;
|
|
112
|
+
let parsed;
|
|
113
|
+
try { parsed = JSON.parse(line); } catch { continue; }
|
|
114
|
+
// ISO 8601 lexical ordering matches chronological ordering — no Date parse.
|
|
115
|
+
if (typeof parsed.ts === 'string' && parsed.ts >= cutoffIso) count++;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return count;
|
|
119
|
+
} catch { return 0; }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Test hook — current retention window (14 days, in ms). */
|
|
123
|
+
export const HOOK_ERROR_RETENTION_MS = RETENTION_MS;
|
package/mem-cli.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// No MCP SDK or heavy deps — only imports schema.mjs and utils.mjs
|
|
4
4
|
|
|
5
5
|
import { homedir } from 'os';
|
|
6
|
-
import { ensureDb, DB_PATH, REGISTRY_DB_PATH } from './schema.mjs';
|
|
6
|
+
import { ensureDb, DB_PATH, DB_DIR, REGISTRY_DB_PATH } from './schema.mjs';
|
|
7
7
|
import { sanitizeFtsQuery, relaxFtsQueryToOr, truncate, typeIcon, inferProject, jaccardSimilarity, computeMinHash, estimateJaccardFromMinHash, scrubSecrets, cjkBigrams, isoWeekKey, COMPRESSED_PENDING_PURGE, SESS_BM25, DEFAULT_DECAY_HALF_LIFE_MS, notLowSignalTitleClause } from './utils.mjs';
|
|
8
8
|
import { cjkPrecisionOk } from './nlp.mjs';
|
|
9
9
|
import { extractCjkLikePatterns } from './nlp.mjs';
|
|
@@ -29,6 +29,7 @@ import { readFileSync, existsSync, readdirSync } from 'fs';
|
|
|
29
29
|
// move each cmdXxx into its own cli/<cmd>.mjs; mem-cli.mjs becomes pure dispatch.
|
|
30
30
|
import { parseArgs, out, fail, relativeTime, fmtDateShort, parseIdToken, formatProbeHints } from './cli/common.mjs';
|
|
31
31
|
import { saveObservation } from './lib/save-observation.mjs';
|
|
32
|
+
import { countRecentHookErrors } from './lib/hook-telemetry.mjs';
|
|
32
33
|
import {
|
|
33
34
|
insertDeferred, listOpenWithOrdinal, dropDeferred,
|
|
34
35
|
resolveDeferredIds, closeDeferredItems,
|
|
@@ -1258,6 +1259,12 @@ async function cmdStats(db, args) {
|
|
|
1258
1259
|
`SELECT COUNT(*) as c FROM observations WHERE superseded_at IS NOT NULL AND compressed_into IS NULL ${projectFilter}`
|
|
1259
1260
|
).get(...baseParams);
|
|
1260
1261
|
|
|
1262
|
+
// Hook self-observation: count PreToolUse / Skill-bridge script failures
|
|
1263
|
+
// recorded in the last 24h. Surfaces silent breakage (DB corruption,
|
|
1264
|
+
// CC upstream field rename) that would otherwise stay invisible — the
|
|
1265
|
+
// failure mode that left code-graph's matcher bug undetected for 10 sessions.
|
|
1266
|
+
const hookErrors24h = countRecentHookErrors(join(DB_DIR, 'runtime'), now - 86400000);
|
|
1267
|
+
|
|
1261
1268
|
// Tier distribution (aligned with MCP mem_stats)
|
|
1262
1269
|
const tierCtx = { now, currentProject: project || inferProject(), currentSessionId: '' };
|
|
1263
1270
|
const tdParams = tierSqlParams(tierCtx);
|
|
@@ -1292,6 +1299,7 @@ async function cmdStats(db, args) {
|
|
|
1292
1299
|
noise_ratio: Number(noiseRatio.toFixed(4)),
|
|
1293
1300
|
compressed: compressedCount.c,
|
|
1294
1301
|
superseded_only: supersededOnlyCount.c,
|
|
1302
|
+
hook_errors_24h: hookErrors24h,
|
|
1295
1303
|
},
|
|
1296
1304
|
tier_distribution: {
|
|
1297
1305
|
working: tierMap.working ?? 0,
|
|
@@ -1326,6 +1334,7 @@ async function cmdStats(db, args) {
|
|
|
1326
1334
|
out(` Avg importance: ${(avgImp.v ?? 1).toFixed(2)}`);
|
|
1327
1335
|
out(` Low-value (imp=1, never accessed, >30d): ${lowVal.c} (${(noiseRatio * 100).toFixed(1)}% noise)`);
|
|
1328
1336
|
out(` Compressed: ${compressedCount.c}`);
|
|
1337
|
+
out(` Hook errors (last 24h): ${hookErrors24h}${hookErrors24h > 0 ? ` ← tail ${join(DB_DIR, 'runtime/hook-errors')}` : ''}`);
|
|
1329
1338
|
if (noiseRatio > 0.6) out(' ⚠️ High noise ratio — consider running mem compress');
|
|
1330
1339
|
out('');
|
|
1331
1340
|
// Tier counts only live (uncompressed, non-superseded) observations — surface the
|
|
@@ -2417,11 +2426,29 @@ function cmdCitationStats(db, args) {
|
|
|
2417
2426
|
LIMIT 10
|
|
2418
2427
|
`).all(cutoff);
|
|
2419
2428
|
|
|
2429
|
+
// v34.x: surface pre-v34 data pollution. applyCitationDecay bumps cited_count
|
|
2430
|
+
// and decay_seen_count atomically (same UPDATE statement), so the invariant
|
|
2431
|
+
// cited_count <= decay_seen_count holds for every resolution this codepath
|
|
2432
|
+
// performs. Yet a small set of obs violate it — these are pre-v34 rows
|
|
2433
|
+
// where a backfill seeded cited_count without populating decay_seen_count.
|
|
2434
|
+
// Without this note, those rows make per-project cite_pct >100% with no
|
|
2435
|
+
// explanation. Cite rate stays unbiased for obs created after this commit.
|
|
2436
|
+
const pollutedRows = db.prepare(`
|
|
2437
|
+
SELECT COUNT(*) AS n FROM observations
|
|
2438
|
+
WHERE cited_count > decay_seen_count
|
|
2439
|
+
AND COALESCE(compressed_into, 0) = 0
|
|
2440
|
+
AND superseded_at IS NULL
|
|
2441
|
+
`).get();
|
|
2442
|
+
const dataPollutionNote = pollutedRows.n > 0
|
|
2443
|
+
? `${pollutedRows.n} obs have cited_count > decay_seen_count (pre-v34 backfill — invariant holds for new data).`
|
|
2444
|
+
: null;
|
|
2445
|
+
|
|
2420
2446
|
if (json) {
|
|
2421
|
-
out(JSON.stringify({ window_days: days, per_project: perProject, decay_queue: decayQueue, promoted, demoted }, null, 2));
|
|
2447
|
+
out(JSON.stringify({ window_days: days, per_project: perProject, decay_queue: decayQueue, promoted, demoted, data_pollution_note: dataPollutionNote }, null, 2));
|
|
2422
2448
|
return;
|
|
2423
2449
|
}
|
|
2424
2450
|
|
|
2451
|
+
if (dataPollutionNote) out(`Note: ${dataPollutionNote}\n`);
|
|
2425
2452
|
out(`Cite rate by project (last ${days}d, cited / decay-resolutions):`);
|
|
2426
2453
|
for (const r of perProject) {
|
|
2427
2454
|
const rate = r.resolved > 0 ? (r.cited * 100 / r.resolved).toFixed(1) + '%' : '—';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.77.0",
|
|
4
4
|
"description": "Lightweight persistent memory system for Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "npm@10.9.2",
|
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
"lib/summary-extractor.mjs",
|
|
61
61
|
"lib/id-routing.mjs",
|
|
62
62
|
"lib/err-sampler.mjs",
|
|
63
|
+
"lib/hook-telemetry.mjs",
|
|
63
64
|
"lib/metrics.mjs",
|
|
64
65
|
"lib/binding-probe.mjs",
|
|
65
66
|
"lib/mem-override.mjs",
|
|
@@ -6,7 +6,11 @@
|
|
|
6
6
|
import { existsSync, readFileSync } from 'fs';
|
|
7
7
|
import { join, resolve, sep } from 'path';
|
|
8
8
|
import { homedir } from 'os';
|
|
9
|
+
import { recordHookError } from '../lib/hook-telemetry.mjs';
|
|
9
10
|
|
|
11
|
+
// CLAUDE_MEM_DIR mirrors pre-tool-recall.js — one env var sandboxes everything.
|
|
12
|
+
const DATA_DIR = process.env.CLAUDE_MEM_DIR || join(homedir(), '.claude-mem-lite');
|
|
13
|
+
const RUNTIME_DIR = process.env.CLAUDE_MEM_RUNTIME_DIR || join(DATA_DIR, 'runtime');
|
|
10
14
|
const REGISTRY_DB_PATH = join(homedir(), '.claude-mem-lite', 'resource-registry.db');
|
|
11
15
|
const MANAGED_BASE = join(homedir(), '.claude-mem-lite');
|
|
12
16
|
const MANAGED_MARKER = '/.claude-mem-lite/managed/';
|
|
@@ -24,7 +28,10 @@ try {
|
|
|
24
28
|
try {
|
|
25
29
|
const event = JSON.parse(input);
|
|
26
30
|
skillName = event.tool_input?.skill;
|
|
27
|
-
} catch
|
|
31
|
+
} catch (e) {
|
|
32
|
+
recordHookError('skill-bridge:json', e, RUNTIME_DIR, { inputLen: input.length });
|
|
33
|
+
process.exit(0);
|
|
34
|
+
}
|
|
28
35
|
|
|
29
36
|
if (!skillName || typeof skillName !== 'string') process.exit(0);
|
|
30
37
|
|
|
@@ -37,7 +44,10 @@ try {
|
|
|
37
44
|
try {
|
|
38
45
|
db = new Database(REGISTRY_DB_PATH, { readonly: true });
|
|
39
46
|
db.pragma('busy_timeout = 1000');
|
|
40
|
-
} catch
|
|
47
|
+
} catch (e) {
|
|
48
|
+
recordHookError('skill-bridge:db-open', e, RUNTIME_DIR);
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
41
51
|
|
|
42
52
|
try {
|
|
43
53
|
// Query: find by name or invocation_name, ONLY if managed path
|
|
@@ -85,11 +95,13 @@ try {
|
|
|
85
95
|
additionalContext,
|
|
86
96
|
},
|
|
87
97
|
}));
|
|
88
|
-
} catch {
|
|
89
|
-
// Silent failure — never block Skill tool
|
|
98
|
+
} catch (e) {
|
|
99
|
+
// Silent failure — never block Skill tool, but record for self-observation.
|
|
100
|
+
recordHookError('skill-bridge:query', e, RUNTIME_DIR, { skillName });
|
|
90
101
|
} finally {
|
|
91
102
|
try { db.close(); } catch {}
|
|
92
103
|
}
|
|
93
|
-
} catch {
|
|
94
|
-
// Top-level catch — exit 0 no matter what
|
|
104
|
+
} catch (e) {
|
|
105
|
+
// Top-level catch — exit 0 no matter what, but record what slipped past.
|
|
106
|
+
try { recordHookError('skill-bridge:top', e, RUNTIME_DIR); } catch {}
|
|
95
107
|
}
|
|
@@ -8,6 +8,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
|
8
8
|
import { basename, join } from 'path';
|
|
9
9
|
import { homedir } from 'os';
|
|
10
10
|
import { buildNotLowSignalSql } from '../lib/low-signal-patterns.mjs';
|
|
11
|
+
import { recordHookError } from '../lib/hook-telemetry.mjs';
|
|
11
12
|
|
|
12
13
|
// CLAUDE_MEM_DIR matches schema.mjs / main CLI — one env var sandboxes the
|
|
13
14
|
// whole system. CLAUDE_MEM_DB_PATH / CLAUDE_MEM_RUNTIME_DIR remain as
|
|
@@ -87,9 +88,24 @@ try {
|
|
|
87
88
|
filePath = event.tool_input?.file_path;
|
|
88
89
|
sessionId = event.session_id || null;
|
|
89
90
|
toolName = event.tool_name || null;
|
|
90
|
-
} catch
|
|
91
|
+
} catch (e) {
|
|
92
|
+
recordHookError('pre-recall:json', e, RUNTIME_DIR, { inputLen: input.length });
|
|
93
|
+
process.exit(0);
|
|
94
|
+
}
|
|
91
95
|
|
|
92
|
-
|
|
96
|
+
// Upstream-shape probe: hook ran but neither field nor input shape matches the
|
|
97
|
+
// contract we encode (event.tool_input.file_path, event.tool_name in
|
|
98
|
+
// Edit|Write|NotebookEdit|Read). Distinguishes "Claude Code renamed the field"
|
|
99
|
+
// from "event genuinely has no file_path" — without this trace, a CC upstream
|
|
100
|
+
// rename silently zeroes injection like code-graph's matcher bug.
|
|
101
|
+
if (!filePath) {
|
|
102
|
+
if (toolName && !['Edit', 'Write', 'NotebookEdit', 'Read'].includes(toolName)) {
|
|
103
|
+
recordHookError('pre-recall:unknown-tool', new Error(`tool_name=${toolName}`), RUNTIME_DIR, { toolName });
|
|
104
|
+
} else if (!toolName) {
|
|
105
|
+
recordHookError('pre-recall:no-toolname', new Error('event missing tool_name'), RUNTIME_DIR);
|
|
106
|
+
}
|
|
107
|
+
process.exit(0);
|
|
108
|
+
}
|
|
93
109
|
|
|
94
110
|
// v2.34.6 Gap 3: Read-side recall with asymmetric quiet-mode. Reads have
|
|
95
111
|
// lower per-event information value than Edits (passive observation, may
|
|
@@ -117,7 +133,10 @@ try {
|
|
|
117
133
|
try {
|
|
118
134
|
db = new Database(DB_PATH, { readonly: true });
|
|
119
135
|
db.pragma('busy_timeout = 1000');
|
|
120
|
-
} catch
|
|
136
|
+
} catch (e) {
|
|
137
|
+
recordHookError('pre-recall:db-open', e, RUNTIME_DIR);
|
|
138
|
+
process.exit(0);
|
|
139
|
+
}
|
|
121
140
|
|
|
122
141
|
try {
|
|
123
142
|
const project = inferProject();
|
|
@@ -258,11 +277,13 @@ try {
|
|
|
258
277
|
// the per-filePath invariant that underpins Read→Edit dedup.
|
|
259
278
|
cooldown[filePath] = now;
|
|
260
279
|
writeCooldown(cooldownPath, cooldown, isSessionScoped);
|
|
261
|
-
} catch {
|
|
262
|
-
// Silent failure — never block editing
|
|
280
|
+
} catch (e) {
|
|
281
|
+
// Silent failure — never block editing, but record for self-observation.
|
|
282
|
+
recordHookError('pre-recall:query', e, RUNTIME_DIR, { filePath });
|
|
263
283
|
} finally {
|
|
264
284
|
try { db.close(); } catch {}
|
|
265
285
|
}
|
|
266
|
-
} catch {
|
|
267
|
-
// Top-level catch — exit 0 no matter what
|
|
286
|
+
} catch (e) {
|
|
287
|
+
// Top-level catch — exit 0 no matter what, but record what slipped past.
|
|
288
|
+
try { recordHookError('pre-recall:top', e, RUNTIME_DIR); } catch {}
|
|
268
289
|
}
|
package/source-files.mjs
CHANGED
|
@@ -43,6 +43,11 @@ export const SOURCE_FILES = [
|
|
|
43
43
|
'lib/summary-extractor.mjs',
|
|
44
44
|
'lib/id-routing.mjs',
|
|
45
45
|
'lib/err-sampler.mjs',
|
|
46
|
+
// v2.76.x: unsampled hook-script failure log. Imported by
|
|
47
|
+
// scripts/pre-tool-recall.js + scripts/pre-skill-bridge.js (recorder)
|
|
48
|
+
// and mem-cli.mjs (countRecentHookErrors for `stats`). Missing from
|
|
49
|
+
// manifest → tarball ships hooks that ERR_MODULE_NOT_FOUND on every fire.
|
|
50
|
+
'lib/hook-telemetry.mjs',
|
|
46
51
|
'lib/metrics.mjs',
|
|
47
52
|
// v2.71.x: better-sqlite3 ABI probe + auto-rebuild. Shared by install.mjs
|
|
48
53
|
// (post-`npm install` verify) and scripts/launch.mjs (pre-server-launch
|