claude-mem-lite 3.37.0 → 3.38.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.
@@ -10,7 +10,7 @@
10
10
  "plugins": [
11
11
  {
12
12
  "name": "claude-mem-lite",
13
- "version": "3.37.0",
13
+ "version": "3.38.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.37.0",
3
+ "version": "3.38.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"
@@ -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
- decay_seen_count = decay_seen_count + 1
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
- if (row.last_decided_session_id === sessionId) continue; // idempotent skip
623
- touched++;
628
+ const decidedThisSession = row.last_decided_session_id === sessionId;
629
+
624
630
  if (cited.has(id)) {
625
- updatePromote.run(IMPORTANCE_CAP, sessionId, id);
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-fired Stop re-resolves nothing (touched
653
- * is 0 for already-decided obs), so the no-op gate below skips it. A later turn
654
- * that resolves NEW injections accumulates onto the same (project, session) row.
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "3.37.0",
3
+ "version": "3.38.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
@@ -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
- export const CURRENT_SCHEMA_VERSION = 40;
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: 'user_prompts', column: 'cc_session_id' };
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
  /**