claude-mem-lite 3.66.0 → 3.66.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.
@@ -10,7 +10,7 @@
10
10
  "plugins": [
11
11
  {
12
12
  "name": "claude-mem-lite",
13
- "version": "3.66.0",
13
+ "version": "3.66.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.66.0",
3
+ "version": "3.66.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/hook-shared.mjs CHANGED
@@ -148,12 +148,6 @@ export const GC_PROJECT_MARKER_PREFIXES = Object.freeze([
148
148
  'cite-recall-', // last session's cite-recall snapshot (nudge input)
149
149
  '.skill-cooldown-', // suggestion throttle timestamp
150
150
  '.skill-reco-cooldown-', // recommendation throttle timestamp
151
- // These two have NO writer and NO reader left in the tree (verified by grep,
152
- // 2026-08-16) — they are version-keyed one-time markers from retired code
153
- // paths (live dir holds `.mcp-dedup-v2.10`, `.residue-warned-v2.55`). Nothing
154
- // recreates them, so sweeping them is a one-shot cleanup, not a policy.
155
- '.mcp-dedup-',
156
- '.residue-warned-',
157
151
  ]);
158
152
 
159
153
  // Records of a completed side effect — never age out. `ep-`/`ep-flush-`/
@@ -164,8 +158,45 @@ export const GC_PRESERVED_MARKER_PREFIXES = Object.freeze([
164
158
  '.auto-adopt-',
165
159
  '.deferred-block-migrated-',
166
160
  '.legacy-claude-md-cleaned-',
161
+ // v3.66.1: these two shipped in the GC list for one release and had to come
162
+ // out. Both are version-keyed one-shot migration sentinels written by
163
+ // scripts/setup.sh, and their gate is `! -f <marker>` — deleting one re-runs
164
+ // its migration. `.mcp-dedup-v2.78` gates a block that removes
165
+ // mcpServers.mem / mcpServers["mem-lite"] from the user's ~/.claude.json with
166
+ // a raw writeFileSync (no tmp+rename, no backup), which the repo's own test
167
+ // documents as intentionally one-shot: "If a user later runs `claude mcp add
168
+ // mem ...` themselves, the gate intentionally lets it stand." A 30-day sweep
169
+ // turned that into a recurring purge of a config file we do not own.
170
+ // The mtime never refreshes (the gate skips the block once the file exists),
171
+ // so every install older than 30 days would have lost it on the first
172
+ // SessionStart after upgrading.
173
+ //
174
+ // Why it was missed: the search for writers used `grep --include=*.mjs
175
+ // --include=*.js`, and the writer is a SHELL script. `sentinelPrefixesFromShell`
176
+ // below now derives this class from scripts/*.sh instead of from memory.
177
+ '.mcp-dedup-',
178
+ '.residue-warned-',
167
179
  ]);
168
180
 
181
+ /**
182
+ * Marker-name prefixes that scripts/*.sh treats as one-shot sentinels, derived
183
+ * from the shell source rather than restated here. `tests/runtime-marker-gc`
184
+ * asserts none of them is GC-able: a shell-written sentinel is invisible to a
185
+ * JS-only grep, which is exactly how `.mcp-dedup-` reached the GC list.
186
+ *
187
+ * @param {string} shellSource concatenated contents of scripts/*.sh
188
+ * @returns {string[]} prefixes like `.mcp-dedup-`
189
+ */
190
+ export function sentinelPrefixesFromShell(shellSource) {
191
+ const out = new Set();
192
+ // Matches `"$DATA_DIR/runtime/.mcp-dedup-v2.78"` and friends: a dotfile under
193
+ // runtime/ whose name carries a version-ish suffix.
194
+ for (const m of String(shellSource || '').matchAll(/runtime\/(\.[a-z0-9-]*?-)v?[0-9][0-9.]*/gi)) {
195
+ out.add(m[1]);
196
+ }
197
+ return [...out];
198
+ }
199
+
169
200
  /**
170
201
  * Sweep per-project runtime markers older than `ageMs`. fs-only, best-effort,
171
202
  * never throws. Returns the number of files removed.
package/hook.mjs CHANGED
@@ -54,6 +54,7 @@ import { snapshotDb } from './lib/db-backup.mjs';
54
54
  import {
55
55
  extractCitationsFromTranscript,
56
56
  extractAllInjected,
57
+ extractInjectedFromKeyContext,
57
58
  bumpCitationAccess,
58
59
  computeCiteRecall,
59
60
  applyCitationDecay,
@@ -740,19 +741,26 @@ async function handleStop() {
740
741
  // filter as citedMain (the numerator, below) — an obs injected only
741
742
  // inside a subagent (sidechain) would otherwise enter the denominator
742
743
  // but never the numerator and streak-demote despite being used there.
743
- // runtimeDir + project enable the 5th (Key Context) face — see
744
- // extractInjectedFromKeyContext: it is marker-derived, because the
745
- // SessionStart block leaves no hook attachment to parse.
746
- const injected = extractAllInjected(transcriptPath, {
747
- mainOnly: true, runtimeDir: RUNTIME_DIR, project, sessionId: ccSessionId,
748
- });
744
+ const injected = extractAllInjected(transcriptPath, { mainOnly: true });
749
745
  // P5 ①: cite-back signals — observations whose warned file the agent
750
746
  // edited this session. Union into injected so they're resolved (they
751
747
  // were injected via pre-tool-recall) and, below, into cited so the
752
748
  // edit promotes them even without a literal #NN in text.
753
749
  const citeBackIds = extractCiteBackSignals(transcriptPath);
754
750
  for (const id of citeBackIds) injected.add(id);
755
- if (injected.size > 0) {
751
+ // D#124, promotion-only (v3.66.1): the SessionStart Key Context block
752
+ // leaves no hook attachment, so its ids come from the per-session
753
+ // marker. They are added to the decay set ONLY where they were
754
+ // actually cited (below), never as bare denominator: the block
755
+ // re-renders the same fixed top-10 unconditionally, so an uncited
756
+ // render says nothing about relevance — and since keyObs gates on
757
+ // `importance >= 2`, one demotion evicts the common importance-2 row
758
+ // from Key Context for good. v3.66.0 fed them in as denominator and
759
+ // that made the block eat its own contents.
760
+ const keyCtxIds = extractInjectedFromKeyContext({
761
+ runtimeDir: RUNTIME_DIR, project, sessionId: ccSessionId,
762
+ });
763
+ if (injected.size > 0 || keyCtxIds.size > 0) {
756
764
  // Text-floor gate: skip decay on tool-only Stops. Without this,
757
765
  // a turn that ends on tool_use locks every injected obs as
758
766
  // uncited (last_decided_session_id set), so a later turn that
@@ -765,6 +773,10 @@ async function handleStop() {
765
773
  } else {
766
774
  const citedMain = extractCitationsFromTranscript(transcriptPath, { mainOnly: true });
767
775
  for (const id of citeBackIds) citedMain.add(id);
776
+ // The promotion-only half: a Key Context row the agent actually
777
+ // cited joins the decay set (and takes the promote branch); one
778
+ // it ignored is never entered, so it cannot streak or demote.
779
+ for (const id of keyCtxIds) if (citedMain.has(id)) injected.add(id);
768
780
  // D#60: the idempotency key must be the CC session UUID, NOT the
769
781
  // project-scoped memory sessionId — concurrent same-project CC
770
782
  // sessions share the latter, so the second session's decay pass
@@ -356,8 +356,20 @@ export function extractInjectedFromFyi(transcriptPath, opts = {}) {
356
356
  * sections never appear).
357
357
  *
358
358
  * Session-gated: a marker whose recorded session differs from the caller's is
359
- * another window's render and must not enter this session's denominator. No
360
- * coordinates → empty set, so transcript-only callers keep their old behaviour.
359
+ * another window's render and must not be attributed to this session.
360
+ *
361
+ * PROMOTION-ONLY (v3.66.1). Deliberately NOT part of extractAllInjected: the
362
+ * other four faces are query-conditioned — a row appears there because it
363
+ * MATCHED something, so its absence from the cited set is evidence it was not
364
+ * useful. A Key Context render is unconditional and re-renders the same fixed
365
+ * top-10 every session, so an uncited render is evidence of nothing but elapsed
366
+ * time. Feeding these ids into the decay DENOMINATOR made the block consume
367
+ * itself: keyObs gates on `importance >= 2` (hook-context.mjs), and a demotion
368
+ * takes the common importance-2 row to 1, dropping it out of Key Context
369
+ * permanently after 3 uncited sessions — each departure promoting the next row
370
+ * into the same grinder. Callers must therefore intersect with the cited set
371
+ * (see handleStop) so a CITED Key Context row is credited while an uncited one
372
+ * is left alone.
361
373
  *
362
374
  * @param {object} [ctx]
363
375
  * @param {string} [ctx.runtimeDir]
@@ -378,21 +390,22 @@ export function extractInjectedFromKeyContext({ runtimeDir, project, sessionId =
378
390
  }
379
391
 
380
392
  /**
381
- * Union of every injection surface's IDs for a transcript: pre-tool-recall +
382
- * UserPromptSubmit `<memory-context>` + PostToolUse error-recall + the
383
- * user-prompt-search FYI block + the SessionStart Key Context block. Single
384
- * integration point the Stop handler calls.
393
+ * Union of the QUERY-CONDITIONED injection surfaces for a transcript:
394
+ * pre-tool-recall + UserPromptSubmit `<memory-context>` + PostToolUse
395
+ * error-recall + the user-prompt-search FYI block. Single integration point the
396
+ * Stop handler calls for the decay DENOMINATOR.
397
+ *
398
+ * Key Context is intentionally absent (v3.66.1 — it was unioned here for one
399
+ * release): every face above appears because a row matched something, so an
400
+ * uncited appearance carries relevance information. An unconditional
401
+ * SessionStart render does not. Callers wanting the Key Context ids ask
402
+ * `extractInjectedFromKeyContext` directly and use them promotion-only.
385
403
  *
386
404
  * @param {string|null|undefined} transcriptPath
387
405
  * @param {object} [opts]
388
406
  * @param {boolean} [opts.mainOnly=false] Skip sidechain-injected IDs. The
389
407
  * citation-decay caller passes true so the injected denominator matches the
390
408
  * mainOnly cited numerator; the P4 access-bump caller omits it (broader).
391
- * @param {string} [opts.runtimeDir] With `project`, enables the Key Context
392
- * face. Omitted by callers that only have a transcript path (computeCiteRecall
393
- * over an arbitrary file), which then see the four transcript-derived faces.
394
- * @param {string} [opts.project]
395
- * @param {string|null} [opts.sessionId]
396
409
  * @returns {Set<number>}
397
410
  */
398
411
  export function extractAllInjected(transcriptPath, opts = {}) {
@@ -401,9 +414,6 @@ export function extractAllInjected(transcriptPath, opts = {}) {
401
414
  ...extractInjectedFromUserPromptSubmit(transcriptPath, opts),
402
415
  ...extractInjectedFromErrorRecall(transcriptPath, opts),
403
416
  ...extractInjectedFromFyi(transcriptPath, opts),
404
- // Marker-derived, not transcript-derived: no-op unless the caller passes
405
- // runtimeDir + project (see extractInjectedFromKeyContext).
406
- ...extractInjectedFromKeyContext(opts),
407
417
  ]);
408
418
  }
409
419
 
@@ -42,21 +42,24 @@ export function recordKeyContextInjection(db, { runtimeDir, project, sessionId =
42
42
  if (Number.isInteger(id) && id > 0 && id < 1e7) clean.push(id);
43
43
  }
44
44
 
45
- let bumped = 0;
46
- if (db && clean.length > 0) {
47
- try {
48
- const now = Date.now();
49
- // Mirrors hook-memory.mjs's UPS bump verbatim so the two surfaces feed the
50
- // same counter with the same semantics. Per-row try/catch for FTS trigger
51
- // safety (project_non_obvious.md).
52
- const stmt = db.prepare(
53
- 'UPDATE observations SET injection_count = COALESCE(injection_count, 0) + 1, last_injected_at = ? WHERE id = ?'
54
- );
55
- for (const id of clean) {
56
- try { stmt.run(now, id); bumped++; } catch { /* single-row failure must not drop the rest */ }
57
- }
58
- } catch (e) { debugCatch(e, 'keyctx-bump'); }
59
- }
45
+ // NO injection_count bump. v3.66.0 added one here and it had to be reverted in
46
+ // v3.66.1: injection_count is not a neutral counter. scoring-sql.mjs states the
47
+ // invariant — "bumped ONLY on UserPromptSubmit / hook-memory auto-inject" —
48
+ // because noisePenaltyClause reads it as a NOISE signal: a row scores x0.5 once
49
+ // injection_count >= 4 (and > access_count * 3), x0.2 at >= 8. Nothing bumps
50
+ // access_count for a rendered row (bumpCitationAccess fires on CITED ids only),
51
+ // so the counter crosses those thresholds purely as a function of elapsed
52
+ // sessions, deprioritising the highest-importance rows — the exact rows Key
53
+ // Context renders in mem_search, UPS ranking and injectionRelevanceSql.
54
+ //
55
+ // The UPS bump the reverted code claimed to "mirror verbatim" is
56
+ // query-conditioned: a row is counted only when it MATCHED a query, so the
57
+ // counter means "auto-injected and never useful". A Key Context render is
58
+ // unconditional and hits the same fixed row set every session, so it would have
59
+ // measured nothing but time. D#124's requirement is decay reachability, which
60
+ // the extractor face delivers on its own — decay reads decay_seen_count /
61
+ // uncited_streak / cited_count, never injection_count.
62
+ const bumped = 0;
60
63
 
61
64
  let written = false;
62
65
  try {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "3.66.0",
3
+ "version": "3.66.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "claude-mem-lite",
9
- "version": "3.66.0",
9
+ "version": "3.66.1",
10
10
  "dependencies": {
11
11
  "@modelcontextprotocol/sdk": "^1.26.0",
12
12
  "better-sqlite3": "^12.6.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "3.66.0",
3
+ "version": "3.66.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",