claude-mem-lite 3.5.0 → 3.6.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 +5 -0
- package/lib/citation-tracker.mjs +93 -0
- package/mem-cli.mjs +28 -1
- package/package.json +1 -1
- package/schema.mjs +18 -1
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "claude-mem-lite",
|
|
13
|
-
"version": "3.
|
|
13
|
+
"version": "3.6.0",
|
|
14
14
|
"source": "./",
|
|
15
15
|
"description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark)."
|
|
16
16
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.0",
|
|
4
4
|
"description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark).",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "sdsrss"
|
package/hook.mjs
CHANGED
|
@@ -54,6 +54,7 @@ import {
|
|
|
54
54
|
bumpCitationAccess,
|
|
55
55
|
computeCiteRecall,
|
|
56
56
|
applyCitationDecay,
|
|
57
|
+
recordCitationFunnel,
|
|
57
58
|
hasMainThreadAssistantText,
|
|
58
59
|
} from './lib/citation-tracker.mjs';
|
|
59
60
|
import { extractTailAssistantText, extractStructuredSummary } from './lib/summary-extractor.mjs';
|
|
@@ -572,6 +573,10 @@ async function handleStop() {
|
|
|
572
573
|
for (const id of citeBackIds) citedMain.add(id);
|
|
573
574
|
const r = applyCitationDecay(db, project, injected, citedMain, sessionId);
|
|
574
575
|
debugLog('DEBUG', 'handleStop', `citation-decay: touched=${r.touched} promoted=${r.promoted} demoted=${r.demoted}`);
|
|
576
|
+
// R1: persist this session's invocation→cite funnel row. touched =
|
|
577
|
+
// obs resolved this run (denominator), promoted = obs cited this run
|
|
578
|
+
// (numerator). Idempotent (touched is 0 on re-fire) + best-effort.
|
|
579
|
+
recordCitationFunnel(db, project, sessionId, r.touched, r.promoted);
|
|
575
580
|
}
|
|
576
581
|
}
|
|
577
582
|
} catch (e) { debugCatch(e, 'handleStop-citation-decay'); }
|
package/lib/citation-tracker.mjs
CHANGED
|
@@ -544,3 +544,96 @@ export function applyCitationDecay(db, project, injectedIds, citedIds, sessionId
|
|
|
544
544
|
try { txn(); } catch (e) { debugCatch(e, 'applyCitationDecay-txn'); return empty; }
|
|
545
545
|
return { promoted, demoted, touched };
|
|
546
546
|
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* R1 — persist one accumulating per-session row of the invocation→cite funnel.
|
|
550
|
+
* Fed by applyCitationDecay's return: `injectedDelta` = obs RESOLVED this Stop
|
|
551
|
+
* (touched), `citedDelta` = obs CITED this Stop (promoted). Idempotent against
|
|
552
|
+
* Stop multi-fire by construction — a re-fired Stop re-resolves nothing (touched
|
|
553
|
+
* is 0 for already-decided obs), so the no-op gate below skips it. A later turn
|
|
554
|
+
* that resolves NEW injections accumulates onto the same (project, session) row.
|
|
555
|
+
*
|
|
556
|
+
* Unlike the per-obs cited_count/decay_seen_count counters (lifetime-cumulative,
|
|
557
|
+
* session breakdown lost), this preserves the per-session series that
|
|
558
|
+
* computeCitationFunnelTrend reads back as a trend. Telemetry only — every write
|
|
559
|
+
* is wrapped so a citation_log failure can never break the Stop handler.
|
|
560
|
+
*
|
|
561
|
+
* @param {import('better-sqlite3').Database} db
|
|
562
|
+
* @param {string} project
|
|
563
|
+
* @param {string} sessionId — memory_session_id of the resolved session
|
|
564
|
+
* @param {number} injectedDelta — obs resolved this run (applyCitationDecay.touched)
|
|
565
|
+
* @param {number} citedDelta — obs cited this run (applyCitationDecay.promoted)
|
|
566
|
+
*/
|
|
567
|
+
export function recordCitationFunnel(db, project, sessionId, injectedDelta, citedDelta) {
|
|
568
|
+
if (!db || !project || !sessionId) return;
|
|
569
|
+
const inj = Number(injectedDelta) || 0;
|
|
570
|
+
if (inj <= 0) return; // nothing resolved this run → no row noise
|
|
571
|
+
const cited = Math.max(0, Number(citedDelta) || 0);
|
|
572
|
+
try {
|
|
573
|
+
db.prepare(`
|
|
574
|
+
INSERT INTO citation_log (project, memory_session_id, resolved_at, injected_n, cited_n)
|
|
575
|
+
VALUES (?, ?, ?, ?, ?)
|
|
576
|
+
ON CONFLICT(project, memory_session_id) DO UPDATE SET
|
|
577
|
+
injected_n = injected_n + excluded.injected_n,
|
|
578
|
+
cited_n = cited_n + excluded.cited_n,
|
|
579
|
+
resolved_at = excluded.resolved_at
|
|
580
|
+
`).run(project, sessionId, Date.now(), inj, cited);
|
|
581
|
+
} catch (e) { debugCatch(e, 'recordCitationFunnel'); }
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* R1 — read the per-session invocation→cite funnel as a windowed trend.
|
|
586
|
+
* `window` aggregates [now-days, now]; `prior` aggregates [now-2*days, now-days)
|
|
587
|
+
* so `delta_pt` shows whether invocation effectiveness is rising or falling.
|
|
588
|
+
* `sessions` is the most-recent `limit` rows (per-session rate for the table view).
|
|
589
|
+
*
|
|
590
|
+
* @param {import('better-sqlite3').Database} db
|
|
591
|
+
* @param {{days?: number, limit?: number, project?: string|null}} [opts]
|
|
592
|
+
* @returns {{window_days: number, sessions: Array, window: {injected:number,cited:number,rate:number}, prior: {injected:number,cited:number,rate:number}, delta_pt: number|null}}
|
|
593
|
+
*/
|
|
594
|
+
export function computeCitationFunnelTrend(db, { days = 7, limit = 10, project = null } = {}) {
|
|
595
|
+
const rate = (cited, inj) => (inj > 0 ? cited / inj : 0);
|
|
596
|
+
const empty = {
|
|
597
|
+
window_days: days,
|
|
598
|
+
sessions: [],
|
|
599
|
+
window: { injected: 0, cited: 0, rate: 0 },
|
|
600
|
+
prior: { injected: 0, cited: 0, rate: 0 },
|
|
601
|
+
delta_pt: null,
|
|
602
|
+
};
|
|
603
|
+
if (!db) return empty;
|
|
604
|
+
try {
|
|
605
|
+
const now = Date.now();
|
|
606
|
+
const windowStart = now - days * 86400000;
|
|
607
|
+
const priorStart = now - 2 * days * 86400000;
|
|
608
|
+
const projClause = project ? 'AND project = ?' : '';
|
|
609
|
+
|
|
610
|
+
const sessions = db.prepare(`
|
|
611
|
+
SELECT project, memory_session_id, resolved_at, injected_n, cited_n
|
|
612
|
+
FROM citation_log
|
|
613
|
+
WHERE 1=1 ${projClause}
|
|
614
|
+
ORDER BY resolved_at DESC
|
|
615
|
+
LIMIT ?
|
|
616
|
+
`).all(...(project ? [project, limit] : [limit]))
|
|
617
|
+
.map(r => ({ ...r, rate: rate(r.cited_n, r.injected_n) }));
|
|
618
|
+
|
|
619
|
+
const agg = (fromTs, toTs) => {
|
|
620
|
+
const params = toTs === null ? [fromTs] : [fromTs, toTs];
|
|
621
|
+
if (project) params.push(project);
|
|
622
|
+
const upper = toTs === null ? '' : 'AND resolved_at < ?';
|
|
623
|
+
const row = db.prepare(`
|
|
624
|
+
SELECT COALESCE(SUM(injected_n), 0) AS injected, COALESCE(SUM(cited_n), 0) AS cited
|
|
625
|
+
FROM citation_log
|
|
626
|
+
WHERE resolved_at >= ? ${upper} ${projClause}
|
|
627
|
+
`).get(...params);
|
|
628
|
+
return { injected: row.injected, cited: row.cited, rate: rate(row.cited, row.injected) };
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
const windowAgg = agg(windowStart, null);
|
|
632
|
+
const priorAgg = agg(priorStart, windowStart);
|
|
633
|
+
const delta_pt = priorAgg.injected > 0
|
|
634
|
+
? Number(((windowAgg.rate - priorAgg.rate) * 100).toFixed(1))
|
|
635
|
+
: null;
|
|
636
|
+
|
|
637
|
+
return { window_days: days, sessions, window: windowAgg, prior: priorAgg, delta_pt };
|
|
638
|
+
} catch (e) { debugCatch(e, 'computeCitationFunnelTrend'); return empty; }
|
|
639
|
+
}
|
package/mem-cli.mjs
CHANGED
|
@@ -40,6 +40,7 @@ import { resolveAnchorToken, formatAnchorError, resolveQueryAnchor, fetchRecentT
|
|
|
40
40
|
import { buildSearchFtsQuery, parseDateBounds, computePerSourceWindow, effectiveObsFtsQuery, searchSessionsFts, searchPromptsFts, normalizeCrossSourceScores, applyUserSort, applyTierFilter } from './lib/search-core.mjs';
|
|
41
41
|
import { AUTO_MERGE_THRESHOLD } from './lib/dedup-constants.mjs';
|
|
42
42
|
import { countRecentHookErrors } from './lib/hook-telemetry.mjs';
|
|
43
|
+
import { computeCitationFunnelTrend } from './lib/citation-tracker.mjs';
|
|
43
44
|
import { aggregateMetrics } from './lib/metrics.mjs';
|
|
44
45
|
import {
|
|
45
46
|
insertDeferred, listOpenWithOrdinal, dropDeferred,
|
|
@@ -2313,8 +2314,12 @@ function cmdCitationStats(db, args) {
|
|
|
2313
2314
|
? `${pollutedRows.n} obs have cited_count > decay_seen_count (pre-v34 backfill — invariant holds for new data).`
|
|
2314
2315
|
: null;
|
|
2315
2316
|
|
|
2317
|
+
// R1: per-session invocation→cite funnel trend (citation_log). Same `days` window
|
|
2318
|
+
// as the per-project cite rate above; funnel.prior/delta_pt show the direction.
|
|
2319
|
+
const funnel = computeCitationFunnelTrend(db, { days });
|
|
2320
|
+
|
|
2316
2321
|
if (json) {
|
|
2317
|
-
out(JSON.stringify({ window_days: days, per_project: perProject, decay_queue: decayQueue, promoted, demoted, data_pollution_note: dataPollutionNote }, null, 2));
|
|
2322
|
+
out(JSON.stringify({ window_days: days, per_project: perProject, decay_queue: decayQueue, promoted, demoted, data_pollution_note: dataPollutionNote, funnel }, null, 2));
|
|
2318
2323
|
return;
|
|
2319
2324
|
}
|
|
2320
2325
|
|
|
@@ -2325,6 +2330,28 @@ function cmdCitationStats(db, args) {
|
|
|
2325
2330
|
out(` ${r.project.padEnd(34)} ${String(rate).padStart(6)} cited:${r.cited}/${r.resolved} at_risk:${r.at_risk}`);
|
|
2326
2331
|
}
|
|
2327
2332
|
out('');
|
|
2333
|
+
|
|
2334
|
+
// R1: invocation→cite funnel — per-session trend + window-vs-prior direction.
|
|
2335
|
+
out(`Invocation→cite funnel (recent sessions, injected→cited; rate window ${days}d):`);
|
|
2336
|
+
if (funnel.sessions.length === 0) {
|
|
2337
|
+
out(' (no resolved sessions in window)');
|
|
2338
|
+
} else {
|
|
2339
|
+
for (const s of funnel.sessions) {
|
|
2340
|
+
const day = s.resolved_at ? new Date(s.resolved_at).toISOString().slice(0, 10) : '—'.repeat(10);
|
|
2341
|
+
const pct = (s.rate * 100).toFixed(1) + '%';
|
|
2342
|
+
out(` ${day} ${(s.project || '').padEnd(28)} inj ${String(s.injected_n).padStart(3)} cited ${String(s.cited_n).padStart(3)} ${pct.padStart(6)}`);
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
let trendLine = `window rate ${(funnel.window.rate * 100).toFixed(1)}% cited ${funnel.window.cited}/${funnel.window.injected}`;
|
|
2346
|
+
if (funnel.delta_pt === null) {
|
|
2347
|
+
trendLine += ' (no prior-window data)';
|
|
2348
|
+
} else {
|
|
2349
|
+
const arrow = funnel.delta_pt > 0 ? '↑' : funnel.delta_pt < 0 ? '↓' : '→';
|
|
2350
|
+
const sign = funnel.delta_pt > 0 ? '+' : '';
|
|
2351
|
+
trendLine += ` (prior ${days}d ${(funnel.prior.rate * 100).toFixed(1)}%) ${arrow} ${sign}${funnel.delta_pt}pt`;
|
|
2352
|
+
}
|
|
2353
|
+
out(trendLine);
|
|
2354
|
+
out('');
|
|
2328
2355
|
out('Active decay queue (uncited_streak >= 2, next miss → demote):');
|
|
2329
2356
|
if (decayQueue.length === 0) out(' (none)');
|
|
2330
2357
|
for (const r of decayQueue) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.0",
|
|
4
4
|
"description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "npm@10.9.2",
|
package/schema.mjs
CHANGED
|
@@ -71,7 +71,15 @@ export const CODE_DIR = join(homedir(), '.claude-mem-lite');
|
|
|
71
71
|
// legacy trigger on existing DBs. LATEST_MIGRATION_COLUMN unchanged (no new column).
|
|
72
72
|
// v37 (D#26): adds user_prompts.cc_session_id (additive, nullable). LATEST_MIGRATION_COLUMN
|
|
73
73
|
// MOVES to it so the half-migrated-DB self-heal fast-path covers the new column.
|
|
74
|
-
|
|
74
|
+
// v38 (R1): citation_log table — per-session invocation→cite funnel telemetry. One
|
|
75
|
+
// accumulating row per resolved session (injected_n / cited_n), written by
|
|
76
|
+
// recordCitationFunnel from applyCitationDecay's touched/promoted at Stop. Turns the
|
|
77
|
+
// per-obs cite counters (lifetime-cumulative) into a trendable per-session series so
|
|
78
|
+
// `citation-stats` can answer "is memory invocation effectiveness rising or falling".
|
|
79
|
+
// New TABLE (not a column) reached via CORE_SCHEMA's CREATE TABLE IF NOT EXISTS on the
|
|
80
|
+
// forced migration pass; LATEST_MIGRATION_COLUMN unchanged (no new column) — same
|
|
81
|
+
// pattern as v35/v36.
|
|
82
|
+
export const CURRENT_SCHEMA_VERSION = 38;
|
|
75
83
|
|
|
76
84
|
// Sentinel column for the LATEST migration set. The fast-path uses this to
|
|
77
85
|
// self-heal half-migrated DBs — schema_version bumped but column ALTERs rolled
|
|
@@ -168,6 +176,15 @@ const CORE_SCHEMA = `
|
|
|
168
176
|
created_at_epoch INTEGER,
|
|
169
177
|
PRIMARY KEY (project, type, session_id)
|
|
170
178
|
);
|
|
179
|
+
|
|
180
|
+
CREATE TABLE IF NOT EXISTS citation_log (
|
|
181
|
+
project TEXT NOT NULL,
|
|
182
|
+
memory_session_id TEXT NOT NULL,
|
|
183
|
+
resolved_at INTEGER,
|
|
184
|
+
injected_n INTEGER NOT NULL DEFAULT 0,
|
|
185
|
+
cited_n INTEGER NOT NULL DEFAULT 0,
|
|
186
|
+
PRIMARY KEY (project, memory_session_id)
|
|
187
|
+
);
|
|
171
188
|
`;
|
|
172
189
|
|
|
173
190
|
// Column migrations (idempotent — only swallow "duplicate column" errors)
|