claude-mem-lite 3.37.0 → 3.38.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/lib/citation-tracker.mjs +38 -9
- package/mem-cli.mjs +21 -3
- package/package.json +1 -1
- package/schema.mjs +16 -2
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "claude-mem-lite",
|
|
13
|
-
"version": "3.
|
|
13
|
+
"version": "3.38.1",
|
|
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.38.1",
|
|
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/lib/citation-tracker.mjs
CHANGED
|
@@ -571,18 +571,24 @@ export function applyCitationDecay(db, project, injectedIds, citedIds, sessionId
|
|
|
571
571
|
// superseded_at IS NULL: mirror computeCitationAdoption + the 4 injection SELECTs so a
|
|
572
572
|
// row superseded mid-session (injected live, then auto-dedup supersedes it before this
|
|
573
573
|
// decay resolves) is not decayed/streaked/mutated — defense-in-depth parity.
|
|
574
|
-
'SELECT id, importance, uncited_streak, last_decided_session_id FROM observations WHERE id = ? AND project = ? AND superseded_at IS NULL'
|
|
574
|
+
'SELECT id, importance, uncited_streak, last_decided_session_id, last_cited_session_id FROM observations WHERE id = ? AND project = ? AND superseded_at IS NULL'
|
|
575
575
|
);
|
|
576
576
|
// decay_seen_count (v34) bumps on every resolution branch — gives
|
|
577
577
|
// citation-stats a denominator that's same-source as cited_count, so the
|
|
578
578
|
// ratio actually means "cite-rate" instead of mixing decay + UserPromptSubmit.
|
|
579
|
+
// Promote also stamps last_cited_session_id (v41 promote-idempotency key) so a
|
|
580
|
+
// same-session re-fire is a no-op. decay_seen_count is bumped by a PARAM, not a
|
|
581
|
+
// literal +1: a FIRST resolution counts the obs into the denominator (1); a
|
|
582
|
+
// cross-turn LATE upgrade of an already-resolved obs must NOT re-count it (0), else
|
|
583
|
+
// cite-rate reads N/2 for a single injected-then-cited obs instead of N/1.
|
|
579
584
|
const updatePromote = db.prepare(`
|
|
580
585
|
UPDATE observations
|
|
581
586
|
SET importance = MIN(?, importance + 1),
|
|
582
587
|
cited_count = cited_count + 1,
|
|
583
588
|
uncited_streak = 0,
|
|
584
589
|
last_decided_session_id = ?,
|
|
585
|
-
|
|
590
|
+
last_cited_session_id = ?,
|
|
591
|
+
decay_seen_count = decay_seen_count + ?
|
|
586
592
|
WHERE id = ?
|
|
587
593
|
`);
|
|
588
594
|
const updateStreakOnly = db.prepare(`
|
|
@@ -619,12 +625,28 @@ export function applyCitationDecay(db, project, injectedIds, citedIds, sessionId
|
|
|
619
625
|
for (const id of injected) {
|
|
620
626
|
const row = selectStmt.get(id, project);
|
|
621
627
|
if (!row) continue; // cross-project or deleted
|
|
622
|
-
|
|
623
|
-
|
|
628
|
+
const decidedThisSession = row.last_decided_session_id === sessionId;
|
|
629
|
+
|
|
624
630
|
if (cited.has(id)) {
|
|
625
|
-
|
|
631
|
+
// Promote path — idempotent on last_cited_session_id, NOT last_decided. This is
|
|
632
|
+
// what lets a citation in a LATER turn upgrade an obs this session already
|
|
633
|
+
// resolved as uncited: the contract is "cite NEXT time you produce user-visible
|
|
634
|
+
// text," which may be several turns after injection. A promote resets
|
|
635
|
+
// uncited_streak and lifts importance, so a same-session demotion that fired at
|
|
636
|
+
// an earlier turn is naturally undone. Re-firing after the promote is a no-op.
|
|
637
|
+
if (row.last_cited_session_id === sessionId) continue; // already promoted this session
|
|
638
|
+
// touched/decay_seen only on the FIRST resolution — a late upgrade re-decides an
|
|
639
|
+
// obs already in the injected denominator, so re-counting it would inflate both
|
|
640
|
+
// decay_seen_count and the funnel's injected_n (cite-rate would read N/2, not N/1).
|
|
641
|
+
const firstResolution = !decidedThisSession;
|
|
642
|
+
updatePromote.run(IMPORTANCE_CAP, sessionId, sessionId, firstResolution ? 1 : 0, id);
|
|
626
643
|
promoted++;
|
|
644
|
+
if (firstResolution) touched++;
|
|
627
645
|
} else {
|
|
646
|
+
// Uncited path — idempotent on last_decided_session_id (unchanged): don't
|
|
647
|
+
// re-streak an obs already resolved this session.
|
|
648
|
+
if (decidedThisSession) continue;
|
|
649
|
+
touched++;
|
|
628
650
|
const nextStreak = (row.uncited_streak || 0) + 1;
|
|
629
651
|
// Demote only when the streak is up AND the project has demonstrably
|
|
630
652
|
// adopted citations. A non-adopting project advances the streak (idempotent
|
|
@@ -649,9 +671,11 @@ export function applyCitationDecay(db, project, injectedIds, citedIds, sessionId
|
|
|
649
671
|
* R1 — persist one accumulating per-session row of the invocation→cite funnel.
|
|
650
672
|
* Fed by applyCitationDecay's return: `injectedDelta` = obs RESOLVED this Stop
|
|
651
673
|
* (touched), `citedDelta` = obs CITED this Stop (promoted). Idempotent against
|
|
652
|
-
* Stop multi-fire by construction — a re-
|
|
653
|
-
*
|
|
654
|
-
*
|
|
674
|
+
* Stop multi-fire by construction — a pure re-fire re-resolves nothing (touched=0
|
|
675
|
+
* AND promoted=0), so the (0,0) gate below skips it. A later turn that resolves NEW
|
|
676
|
+
* injections (touched>0) OR lands a cross-turn LATE citation (touched=0, promoted>0)
|
|
677
|
+
* accumulates onto the same (project, session) row — the numerator must accrue even
|
|
678
|
+
* when the denominator doesn't, so the gate skips only when BOTH deltas are 0.
|
|
655
679
|
*
|
|
656
680
|
* Unlike the per-obs cited_count/decay_seen_count counters (lifetime-cumulative,
|
|
657
681
|
* session breakdown lost), this preserves the per-session series that
|
|
@@ -667,8 +691,13 @@ export function applyCitationDecay(db, project, injectedIds, citedIds, sessionId
|
|
|
667
691
|
export function recordCitationFunnel(db, project, sessionId, injectedDelta, citedDelta) {
|
|
668
692
|
if (!db || !project || !sessionId) return;
|
|
669
693
|
const inj = Number(injectedDelta) || 0;
|
|
670
|
-
if (inj <= 0) return; // nothing resolved this run → no row noise
|
|
671
694
|
const cited = Math.max(0, Number(citedDelta) || 0);
|
|
695
|
+
// Skip only when BOTH deltas are 0: a pure cross-turn late upgrade contributes
|
|
696
|
+
// injectedDelta=0 (the obs was already counted at its first resolution) but
|
|
697
|
+
// citedDelta>0, and that numerator must still fold onto the existing session row
|
|
698
|
+
// (a late upgrade always has a prior first-resolution row). Pre-v41 the `inj<=0`
|
|
699
|
+
// gate silently dropped it, under-counting cites in the funnel.
|
|
700
|
+
if (inj <= 0 && cited <= 0) return; // nothing resolved this run → no row noise
|
|
672
701
|
try {
|
|
673
702
|
db.prepare(`
|
|
674
703
|
INSERT INTO citation_log (project, memory_session_id, resolved_at, injected_n, cited_n)
|
package/mem-cli.mjs
CHANGED
|
@@ -2443,16 +2443,34 @@ function cmdCitationStats(db, args) {
|
|
|
2443
2443
|
// as the per-project cite rate above; funnel.prior/delta_pt show the direction.
|
|
2444
2444
|
const funnel = computeCitationFunnelTrend(db, { days });
|
|
2445
2445
|
|
|
2446
|
+
// Survivorship-honesty: the per-project rate (cited_count/decay_seen_count over
|
|
2447
|
+
// SURVIVING in-window obs) is doubly biased — GC drops uncited obs from the
|
|
2448
|
+
// denominator and the window excludes older obs — so it overstates adoption (a
|
|
2449
|
+
// project can read 91% while its true lifetime inject→cite is ~6%). citation_log
|
|
2450
|
+
// rows persist across GC, so their per-project sum is the honest historical rate.
|
|
2451
|
+
// Attach both to each row so the text + JSON surface the biased and honest numbers.
|
|
2452
|
+
const funnelByProject = new Map(
|
|
2453
|
+
db.prepare(`SELECT project, COALESCE(SUM(injected_n), 0) inj, COALESCE(SUM(cited_n), 0) cit
|
|
2454
|
+
FROM citation_log GROUP BY project`).all().map(r => [r.project, r])
|
|
2455
|
+
);
|
|
2456
|
+
for (const r of perProject) {
|
|
2457
|
+
const f = funnelByProject.get(r.project);
|
|
2458
|
+
r.funnel_injected = f ? f.inj : 0;
|
|
2459
|
+
r.funnel_cited = f ? f.cit : 0;
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2446
2462
|
if (json) {
|
|
2447
2463
|
out(JSON.stringify({ window_days: days, per_project: perProject, decay_queue: decayQueue, promoted, demoted, data_pollution_note: dataPollutionNote, funnel }, null, 2));
|
|
2448
2464
|
return;
|
|
2449
2465
|
}
|
|
2450
2466
|
|
|
2451
2467
|
if (dataPollutionNote) out(`Note: ${dataPollutionNote}\n`);
|
|
2452
|
-
out(`Cite rate by project (last ${days}d
|
|
2468
|
+
out(`Cite rate by project (last ${days}d):`);
|
|
2469
|
+
out(` funnel = lifetime injected→cited (citation_log, GC-durable = honest) · surviving = cited/decay over in-window non-GC'd obs (survivorship-biased, reads high):`);
|
|
2453
2470
|
for (const r of perProject) {
|
|
2454
|
-
const
|
|
2455
|
-
|
|
2471
|
+
const funnelRate = r.funnel_injected > 0 ? (r.funnel_cited * 100 / r.funnel_injected).toFixed(1) + '%' : '—';
|
|
2472
|
+
const survRate = r.resolved > 0 ? (r.cited * 100 / r.resolved).toFixed(1) + '%' : '—';
|
|
2473
|
+
out(` ${r.project.padEnd(30)} funnel ${String(funnelRate).padStart(6)} (${r.funnel_cited}/${r.funnel_injected}) · surviving ${String(survRate).padStart(6)} (${r.cited}/${r.resolved}) · at_risk:${r.at_risk}`);
|
|
2456
2474
|
}
|
|
2457
2475
|
out('');
|
|
2458
2476
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.38.1",
|
|
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
|
@@ -98,13 +98,19 @@ export const CODE_DIR = join(homedir(), '.claude-mem-lite');
|
|
|
98
98
|
// (Haiku summary enrichment lost every session). Pure index reheal (no data migration, no
|
|
99
99
|
// column drop); idempotent. New behavior via the forced pass; LATEST_MIGRATION_COLUMN
|
|
100
100
|
// unchanged (no new column) — same pattern as v35/v36/v38/v39.
|
|
101
|
-
|
|
101
|
+
// v41 (cross-turn late-citation): adds observations.last_cited_session_id (additive,
|
|
102
|
+
// nullable) — the promote idempotency key, split from last_decided_session_id so a
|
|
103
|
+
// citation landing in a LATER turn of the same session can still upgrade a
|
|
104
|
+
// previously-uncited obs (see applyCitationDecay). REAL new column, so unlike v38-v40
|
|
105
|
+
// this DOES advance LATEST_MIGRATION_COLUMN (→ observations.last_cited_session_id);
|
|
106
|
+
// existing DBs reach the ALTER because version 40 != 41 falls through the fast-path.
|
|
107
|
+
export const CURRENT_SCHEMA_VERSION = 41;
|
|
102
108
|
|
|
103
109
|
// Sentinel column for the LATEST migration set. The fast-path uses this to
|
|
104
110
|
// self-heal half-migrated DBs — schema_version bumped but column ALTERs rolled
|
|
105
111
|
// back (observed once in dev during v2.74.0). Update both the column AND
|
|
106
112
|
// (if needed) the table when adding a new migration batch.
|
|
107
|
-
const LATEST_MIGRATION_COLUMN = { table: '
|
|
113
|
+
const LATEST_MIGRATION_COLUMN = { table: 'observations', column: 'last_cited_session_id' };
|
|
108
114
|
|
|
109
115
|
function hasLatestMigrationColumn(db) {
|
|
110
116
|
try {
|
|
@@ -264,6 +270,14 @@ const MIGRATIONS = [
|
|
|
264
270
|
// legacy rows + non-CC/no-stdin invocations read back NULL and the handoff falls
|
|
265
271
|
// back to its legacy unfiltered query.
|
|
266
272
|
'ALTER TABLE user_prompts ADD COLUMN cc_session_id TEXT DEFAULT NULL',
|
|
273
|
+
// v41 (cross-turn late-citation): promote-idempotency key, split from
|
|
274
|
+
// last_decided_session_id. applyCitationDecay guards the cited/promote branch on
|
|
275
|
+
// THIS column so a citation in a LATER turn of the same session can still upgrade a
|
|
276
|
+
// previously-uncited obs; the uncited/streak branch stays guarded on
|
|
277
|
+
// last_decided_session_id (so it never double-streaks within a session). Nullable:
|
|
278
|
+
// legacy rows read NULL and behave exactly as before until their first same-session
|
|
279
|
+
// late cite.
|
|
280
|
+
'ALTER TABLE observations ADD COLUMN last_cited_session_id TEXT DEFAULT NULL',
|
|
267
281
|
];
|
|
268
282
|
|
|
269
283
|
/**
|