claude-mem-lite 3.80.0 → 3.81.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.80.0",
13
+ "version": "3.81.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.80.0",
3
+ "version": "3.81.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-episode.mjs CHANGED
@@ -260,6 +260,72 @@ const RESEARCH_ENTRY_THRESHOLD = 8;
260
260
  * short — an edit-driven episode that happens to contain Greps is not evidence that
261
261
  * Grep carries research episodes.
262
262
  *
263
+ * READ THE DENOMINATOR BEFORE READING `grepDecisive: 0`. It is a vacuous zero whenever
264
+ * no episode contains a Grep at all, and that is the state measured on 2026-08-25:
265
+ * across 555 instrumented episodes, `sum(readCount) = sum(grepCount) = 0`, and the `Grep`
266
+ * tool was invoked **0 times in the entire 1111-transcript history on this machine**.
267
+ * So "grepDecisive is 0, therefore Grep is safe to skip" is not an inference this counter
268
+ * supports here — it never had the chance to fire. (The decision it was built for is
269
+ * separately moot on that corpus: skipping a tool nobody calls saves 0ms.) Same shape as
270
+ * the FTS5 `rowid = ? AND fts MATCH ?` trap this repo already paid for — a predicate that
271
+ * cannot return true reports the defect as absent.
272
+ *
273
+ * The same measurement shows rules 4 and `buildImmediateObservation`'s `isReviewPattern`
274
+ * are BOTH dormant, for a reason that has nothing to do with Grep: `readCount` counts
275
+ * `Read || Grep`, and `Read` is filtered out at both layers (scripts/post-tool-use.sh
276
+ * records it to `reads-<project>.txt` and exits; SKIP_TOOLS returns early in Node), so the
277
+ * rule's only remaining input is a tool that is never called. Last observation the review
278
+ * branch produced: 133 days ago, 111 lifetime.
279
+ *
280
+ * D#171 proposed the obvious repair — re-point rule 4 at `episode.filesRead`, populated at
281
+ * hook.mjs:228 before this function runs, the same way rule 3 already reads
282
+ * `episode.files`. MEASURED 2026-08-25: IT DOES NOT WORK TODAY, and the reason is dated,
283
+ * not structural. Both halves matter, and the pre-tag review corrected the first draft of
284
+ * each.
285
+ *
286
+ * Unlike the Grep case the denominator is real: `Read` fires 1861 times across the
287
+ * 1114-transcript history (9.1% of all tool calls; the transcripts hold records spanning
288
+ * 2026-08-13..08-25, so that is a live rate, not a lifetime counter). But `filesRead` is a
289
+ * per-FLUSH slice, not a per-episode total — flushEpisodeWithDb renames and consumes
290
+ * `reads-<project>.txt` on every flush. COMPARE THE TWO RATES ON THE SAME WINDOW: the
291
+ * `episode_significance` metric rows cover three ACTIVE days (08-22 / 08-24 / 08-25; 08-23
292
+ * has no file), 607 flushes; the transcripts touched in that window carry 596 Reads. That
293
+ * is 0.98 Reads per episode. The first draft said 0.8 by dividing a 12-day Read rate by a
294
+ * 3-day flush rate — two windows, one ratio, which is the same shape of error v3.80.0
295
+ * recorded as "reading a lifetime counter as an active rate".
296
+ *
297
+ * At ~1 Read per episode a threshold of 8 is out of reach, and the 90-day sample agrees:
298
+ * `files_read` is non-empty on 34 of 1872 rows (1.8%), p50 1 / p95 5 / max 8, exactly ONE
299
+ * row reaching 8 — and that column is a SUPERSET of `episode.filesRead` (hook-llm.mjs
300
+ * merges searched files in), so the true field is smaller. Two caveats on that sample, both
301
+ * from the review: it only covers SIGNIFICANT episodes (saveEpisodeImmediate is gated on
302
+ * `isSignificant`), which is ~8% of flushes and structurally excludes the population rule 4
303
+ * would change; and 90 days starts AFTER the break below.
304
+ *
305
+ * THE RULE WAS NOT ALWAYS UNREACHABLE — do not write "structurally". Non-empty `files_read`
306
+ * by month, with the count reaching the threshold of 8:
307
+ *
308
+ * 2026-02 35/ 78 44.9% >=8: 3 max 11
309
+ * 2026-03 603/1004 60.1% >=8: 49 max 53
310
+ * 2026-04 314/ 621 50.6% >=8: 28 max 33
311
+ * 2026-05 8/ 129 6.2% >=8: 0
312
+ * 2026-06 5/ 754 0.7% >=8: 0
313
+ * 2026-07 8/ 919 0.9% >=8: 1
314
+ * 2026-08 20/ 193 10.4% >=8: 0
315
+ * lifetime reaching >=8: 81
316
+ *
317
+ * For three consecutive months this field fed the threshold at a real rate. It collapsed in
318
+ * 2026-05 and the cause is NOT identified. That regime break is the single most useful fact
319
+ * here, because it is direct evidence for the conclusion rather than against it: the
320
+ * reachable input is the episode BOUNDARY, not the threshold and not the field, and the
321
+ * boundary demonstrably moved once already.
322
+ *
323
+ * D#171 closed as won't-fix-as-specified: the repair it named does not work at the current
324
+ * cadence, and re-pointing the rule would move the dormancy to a field nobody suspects.
325
+ * Reopening means finding what changed in 2026-05 — a far larger change than the rule, with
326
+ * no evidence its output was worth it (111 lifetime observations, dormant 133 days, nobody
327
+ * noticed). The May break is tracked separately so it is not lost with the closure.
328
+ *
263
329
  * @param {object} episode
264
330
  * @returns {{significant: boolean, rule: 1|2|3|4|null, readCount: number,
265
331
  * grepCount: number, grepDecisive: boolean}}
@@ -547,15 +547,60 @@ export function extractAllInjected(transcriptPath, opts = {}) {
547
547
  * requires each face to be either in the union or listed here — so a face added later
548
548
  * cannot slip out of the denominator by being forgotten, only by being argued for.
549
549
  *
550
- * - `task_imperative` (v3.76): metering it is the point of adding it — D#137/D#150/D#151
551
- * are all blocked on not knowing this face's cite-rate. Widening the denominator at the
552
- * same moment would change what gets demoted in every live install on upgrade, and it
553
- * would do so on the face whose framing is itself the open question: if the imperative
554
- * framing under-performs, the penalty lands on the LESSONS it carried (imperativePick
555
- * selects high-value ones) rather than on the framing. Read the rate first, then decide.
556
- * Removing an entry from this set is the one-line change that widens the denominator.
550
+ * EMPTY since 2026-08-25, by decision rather than by default. `task_imperative` was its
551
+ * only member; the history is kept because the exit criterion is the reusable part.
552
+ *
553
+ * - `task_imperative` (v3.76 -> admitted 2026-08-25): it was excluded on a stated
554
+ * condition — "if the imperative framing under-performs, the penalty lands on the
555
+ * LESSONS it carried (imperativePick selects high-value ones) rather than on the
556
+ * framing" with the instruction to read the rate first. THE RATE WAS READ AND THE
557
+ * CONDITION IS NOT MET. Over the live 1113-transcript corpus: 44.1% (15/34), against
558
+ * pretool 38.3% (521/1362), fyi 10.9%, ups 8.4%, error_recall 6.2%.
559
+ *
560
+ * citation_surface_log alone would NOT have supported this — it held n=8 over 2.1 days,
561
+ * because the FLAG had been parked for weeks while the METER had only run since v3.76.
562
+ * Re-deriving the rate by walking live transcripts with the shipped extractors turned
563
+ * n=8 into n=34. Worth keeping as a habit: when a face's row count looks too small to
564
+ * decide on, check whether the meter is younger than the behaviour before concluding
565
+ * there is no data.
566
+ *
567
+ * Two caveats travel with the number. n=34, so its CI overlaps pretool's: "does not
568
+ * under-perform" is established, "leads" is not. And imperativePick returns a single
569
+ * top-ranked lesson, so this is a top-1 pick measured against faces that inject bulk
570
+ * lists — a confound in this face's favour that no amount of n removes.
571
+ *
572
+ * Measured blast radius, same-corpus one-pass A/B (4-face union vs 5-face union in the
573
+ * SAME walk — never by subtracting two counts taken at different times): 22 new
574
+ * (session,id) rows = +0.90% of the denominator across 17 sessions, 9 of the 22 cited
575
+ * (40.9%).
576
+ *
577
+ * COUNT THE STREAK ON THE RIGHT UNIT. The first version of this note said "zero of them
578
+ * uncited across the >=3 sessions UNCITED_STREAK_THRESHOLD requires, so no demotions" —
579
+ * measured on marginal sessions only, which is NOT the unit applyCitationDecay uses.
580
+ * `uncited_streak` is per-OBSERVATION and is driven by every face at once, so the real
581
+ * question is whether a marginal uncited resolution lands on a row the other four faces
582
+ * have already walked to 2. It does: 13 of the 22 marginal pairs are uncited, and of the
583
+ * 16 distinct observations behind them FIVE sit at uncited_streak = 2 today — four of
584
+ * those (#8847, #8948, #10251, #10527) are ids this flip newly resolves as uncited, i.e.
585
+ * one imperative-only silent session from demotion. #8847 is imp=3 with cited_count=56.
586
+ * The product's own CLI calls that state "Active decay queue (uncited_streak >= 2, next
587
+ * miss -> demote)"; a release note claiming three sessions of margin contradicted it.
588
+ *
589
+ * THE RESIDUAL RISK, unresolved and deliberately shipped: all 22 marginal rows are
590
+ * importance=3, because rankImperativeCandidates orders by importance DESC and takes 50,
591
+ * and in the five largest projects here the imp=3 population alone exceeds that limit
592
+ * (projects--mem 326, code-graph-mcp 121). So a demotion 3->2 EVICTS a lesson from this
593
+ * face's candidate pool rather than merely down-ranking it — a feedback loop the four
594
+ * original denominator faces do not have, because they select on FTS relevance. It is
595
+ * NOT permanent: 3->2 still clears the pool's `>= 2` gate (it only loses the LIMIT 50
596
+ * race), and updatePromote restores importance on the next citation from ANY face, so a
597
+ * row returns the moment something else surfaces and cites it. If lessons start
598
+ * disappearing from imperative picks, this is the first place to look, and the cap
599
+ * belongs in rankImperativeCandidates (raise LIMIT above the imp=3 population, or exempt
600
+ * this face's picks from demotion) rather than back here. Tracked with `subagent` in
601
+ * D#172.
557
602
  */
558
- const DECAY_EXCLUDED_SURFACES = new Set(['task_imperative']);
603
+ const DECAY_EXCLUDED_SURFACES = new Set();
559
604
 
560
605
  /** @type {ReadonlyArray<string>} faces that DO feed the decay denominator. */
561
606
  export const DECAY_DENOMINATOR_SURFACES = ATTACHMENT_SURFACES.filter((f) => !DECAY_EXCLUDED_SURFACES.has(f));
@@ -578,6 +623,28 @@ export const DECAY_DENOMINATOR_SURFACES = ATTACHMENT_SURFACES.filter((f) => !DEC
578
623
  * subagent-only injection uncited by construction. Metered first (this is the
579
624
  * whole point of D#152: the face's cite-rate is unknown), decided second.
580
625
  *
626
+ * D#164 read it (2026-08-25, 30 live sessions): 25.0% (12/48) on the house
627
+ * id-level caliber — above fyi (10.9%) and error_recall (6.2%), both of which
628
+ * ARE in the denominator, so "it performs badly" is not available as a reason
629
+ * to keep it out. The two costs that ARE measured: (1) admitting it means
630
+ * feeding its receiver-attributed cites alongside `citedMain`, which would flip
631
+ * 3 main-face (session,id) pairs from demote to promote on a cite the main thread
632
+ * never made — 3 of the 1064 such pairs inside the 30 subagent-bearing sessions
633
+ * sampled (0.28%), or 3 of 1935 (0.16%) if you widen to every subagent-bearing
634
+ * session in the corpus; state which denominator you mean, the phrase "main-face
635
+ * ids" alone does not pin it; (2) the 33 rows it would newly add cite at 15.2% and are
636
+ * 94% importance=3 — same LIMIT-50 eviction loop described under
637
+ * `task_imperative` above, since both faces share selectImperativeLesson. Three
638
+ * of those 33 would actually demote over that corpus.
639
+ *
640
+ * Its sibling was admitted on 2026-08-25; this one deliberately was NOT, and the
641
+ * difference is not the rate. task_imperative needed one line and no change to
642
+ * `citedMain`; this face needs the receiver-attributed cites merged in
643
+ * asymmetrically, and its numerator only became trustworthy on 2026-08-25 (see
644
+ * collectSubagentSurface — it credited cross-agent citations until then). Letting
645
+ * one release separate them also means the eviction loop they share is observed
646
+ * on one face before it acts on two. Tracked in D#172.
647
+ *
581
648
  * A member here that the Stop path stops feeding becomes an all-zero face, not
582
649
  * a silently-demoting one — which is the failure mode worth keeping.
583
650
  * @type {ReadonlyArray<string>}
@@ -684,20 +751,32 @@ export function findSubagentTranscripts(transcriptPath) {
684
751
  * also why the Stop path records this face in its own recordCitationSurfaces
685
752
  * call: one call carries one `cited` set for every face in it.
686
753
  *
687
- * UNIT — read this before reading the rate it produces. Both sets are unioned
688
- * across ALL of the session's sidechain files, so the resulting rate is
689
- * "fraction of injected ids that appear anywhere in any sidechain of this
690
- * session", NOT per-dispatch adoption. An id handed to agent A and cited by
691
- * agent B counts as a hit, and an id handed to three agents and cited by one
692
- * counts as one full hit rather than a third. Observed in real data (review of
693
- * v3.77.0): 1 of this project's 7 historical hits is exactly that shape. The
694
- * number is therefore biased HIGH against per-dispatch adoption which matters
695
- * because D#152's instruction is to read this rate and then decide whether the
696
- * face enters the decay denominator (D#164). Per-dispatch attribution would
697
- * need per-file accounting, deliberately not built yet.
754
+ * UNIT — read this before reading the rate it produces (D#164 settled the first
755
+ * half of it; the second half is still a live caveat).
756
+ *
757
+ * `injected` is unioned by id across the session's sidechain files, which is the
758
+ * house caliber: every other face also counts an obs once per session no matter
759
+ * how many times it was injected. `cited` is NOT unioned an id is credited only
760
+ * when the agent that RECEIVED it is the agent that cited it. This face is the
761
+ * only one where injection and citation can land in different contexts, so the
762
+ * union form silently counted "agent A was handed it, agent B mentioned it" as
763
+ * adoption. Measured over 30 live sessions before the attribution was added:
764
+ * 13/48 unioned vs 12/48 receiver-attributed.
765
+ *
766
+ * What is still biased HIGH: an id handed to three agents and cited by one counts
767
+ * as one full hit rather than a third, because the id-level denominator collapses
768
+ * the three dispatches into one. On the same corpus that is 48 ids over 82
769
+ * (dispatch, id) PAIRS — not 82 files: the sessions hold ~268 sidechain transcripts and
770
+ * most carry no injection — i.e. 25.0% id-level against 14.6% per-dispatch. Opportunity-level
771
+ * accounting would need a denominator shape citation_surface_log does not have
772
+ * (its key is (project, session, surface)), and changing that would put this face
773
+ * on a different ruler from the other six — so the id-level number is the one
774
+ * that is comparable to pretool/ups/fyi, and the per-dispatch number is the one
775
+ * to quote when asking "does a dispatched agent use what it was handed".
698
776
  *
699
777
  * @param {string|null|undefined} transcriptPath main-thread transcript (.jsonl)
700
778
  * @returns {{injected: Set<number>, cited: Set<number>, files: number}}
779
+ * `cited` is always a subset of `injected`.
701
780
  */
702
781
  export function collectSubagentSurface(transcriptPath) {
703
782
  const injected = new Set();
@@ -705,8 +784,13 @@ export function collectSubagentSurface(transcriptPath) {
705
784
  let files = 0;
706
785
  for (const p of findSubagentTranscripts(transcriptPath)) {
707
786
  files++;
708
- for (const id of extractInjectedFromSubagentPrompt(p)) injected.add(id);
709
- for (const id of extractCitationsFromTranscript(p)) cited.add(id);
787
+ // Per-file intersection, not two unions: the pairing is the whole point.
788
+ const seen = extractInjectedFromSubagentPrompt(p);
789
+ const said = extractCitationsFromTranscript(p);
790
+ for (const id of seen) {
791
+ injected.add(id);
792
+ if (said.has(id)) cited.add(id);
793
+ }
710
794
  }
711
795
  return { injected, cited, files };
712
796
  }
package/mem-cli.mjs CHANGED
@@ -2651,9 +2651,11 @@ function cmdCitationStats(db, args) {
2651
2651
  for (const s of surfaceFunnel.surfaces) {
2652
2652
  const pct = (s.rate * 100).toFixed(1) + '%';
2653
2653
  // Which faces actually move importance is NOT readable from the rates —
2654
- // and an annotated keyctx beside a bare task_imperative reads as "that
2655
- // one does feed decay", which is false. Derived from the exported sets so
2656
- // the note cannot drift from the behaviour it describes.
2654
+ // and an annotated keyctx beside a bare `subagent` reads as "that one
2655
+ // does feed decay", which is false. Derived from the exported sets so
2656
+ // the note cannot drift from the behaviour it describes. (The example
2657
+ // used to name task_imperative; it joined the denominator on 2026-08-25
2658
+ // once its rate was read, leaving `subagent` as the bare non-decay face.)
2657
2659
  const note = s.surface === 'keyctx'
2658
2660
  ? ' (promotion-only: never demotes)'
2659
2661
  : (DECAY_DENOMINATOR_SURFACES.includes(s.surface)
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "3.80.0",
3
+ "version": "3.81.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "claude-mem-lite",
9
- "version": "3.80.0",
9
+ "version": "3.81.0",
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.80.0",
3
+ "version": "3.81.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",