claude-mem-lite 3.80.0 → 3.82.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.82.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.82.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}}
package/hook-memory.mjs CHANGED
@@ -412,6 +412,57 @@ export function searchRelevantMemories(db, userPrompt, project, excludeIds = [])
412
412
  // injection query is guarded by the subprocess cases in
413
413
  // tests/pre-tool-recall.test.mjs. Do not reintroduce an in-process twin here.
414
414
 
415
+ /**
416
+ * Upper bound on the imperative candidate pool. This is an OOM/latency BACKSTOP, not a
417
+ * ranking gate — read that literally before changing it (D#172, authorised 2026-08-25).
418
+ *
419
+ * The bound is applied in SQL, i.e. BEFORE the identifier-overlap filter below, so
420
+ * whatever it is set to is a hard REACHABILITY bound: a lesson outside the window cannot
421
+ * be picked however well it matches the prompt. At its original value of 50 that made
422
+ * this face reachable only from the 50 newest `importance >= 2` rows, and in five
423
+ * projects on this machine the importance=3 population ALONE exceeds 50 — so every
424
+ * importance=2 lesson in those projects was structurally unreachable, and a
425
+ * citation-decay demotion 3->2 EVICTED a row from the pool instead of down-ranking it.
426
+ * That eviction loop is the risk D#172 was filed on; raising the bound above any
427
+ * plausible per-project population is what closes it, because a 3->2 demotion then only
428
+ * changes the row's score multiplier, which is what the decay design intends.
429
+ *
430
+ * COUNT THE POPULATION WITH THE POOL'S OWN FILTER. Those figures are
431
+ * `liveObsFilterSql` + the `importance >= 2` + non-empty-lesson gates, i.e. what the query
432
+ * below can actually return — 327 / 121 / 56 / 53 / 51 for projects--mem, code-graph-mcp,
433
+ * ubuntu-sec, daagu, agentsmd. The first version of this note published the RAW
434
+ * importance=3 counts instead (365 / 131 / 62 / 69 / 51), which include superseded and
435
+ * compressed rows the pool can never see and overstated one project by a third; the
436
+ * pre-tag review caught it, and caught that those wrong numbers had replaced correct ones
437
+ * in lib/citation-tracker.mjs. Re-measure with `node benchmark/imperative-pool-replay.mjs
438
+ * --population`, never with a bare `SELECT ... WHERE importance = 3`.
439
+ *
440
+ * 3->2 IS NOW A DOWN-RANK; 2->1 IS STILL AN EVICTION. The pool gate is
441
+ * `COALESCE(importance, 1) >= 2`, so a row demoted to the IMPORTANCE_FLOOR of 1 leaves
442
+ * this face's reach until some other face cites it back up. Widening the bound is also
443
+ * what first makes importance=2 rows reachable here (56 of projects--mem's 383 eligible),
444
+ * so it creates the injections that can walk one down to 1. Measured exposure: of the
445
+ * picks the widening newly surfaces, one is importance=2 — `score = importance x overlap`
446
+ * keeps importance=3 rows ahead nearly always — so this is a known small edge, not a
447
+ * closed loop.
448
+ *
449
+ * MEASURED, and reproducible: `node benchmark/imperative-pool-replay.mjs`. Over 373 real
450
+ * user prompts replayed against their OWN project's live corpus (85 produced a candidate
451
+ * at all), the 50-row bound destroyed 7 picks outright (8.2%) and changed the top-1 in 3
452
+ * of 78 (3.8%). Small n, so the load-bearing argument is not that one: the wide pool is a
453
+ * SUPERSET of the narrow one, so its top-1 score is always >= the narrow one's and a
454
+ * stable sort keeps the incumbent on a tie — a different pick therefore always means a
455
+ * strictly higher score under this face's own objective. That harness attacks the claim on
456
+ * every prompt and exits non-zero on a counterexample; it currently finds none.
457
+ *
458
+ * COST, from the same harness against projects--mem (383 eligible rows under the shipped
459
+ * predicate): 0.44 ms/prompt at 50, 1.50 ms/prompt at 5000, on a UserPromptSubmit path
460
+ * budgeted in seconds. A synthetic 8000-row pool measured 8.3 ms/prompt, so the bound is
461
+ * doing real work at the top of its range and should not be raised casually. It is ~13x
462
+ * the largest eligible population on this machine.
463
+ */
464
+ export const IMPERATIVE_POOL_BACKSTOP = 5000;
465
+
415
466
  /**
416
467
  * Phase-2 task-imperative ranking (spec 2026-06-29 §4.1): score every candidate lesson
417
468
  * relevant to THIS prompt (importance>=2 + non-empty lesson + identifier overlap with the
@@ -428,6 +479,13 @@ export function rankImperativeCandidates(db, userPrompt, project, excludeIds = [
428
479
  if (promptIdents.size === 0) return []; // no symbol anchor → no imperative (precision-first)
429
480
  const exclude = new Set(excludeIds);
430
481
  let rows;
482
+ // The ORDER BY ends in `id DESC` to make it a TOTAL order. Without that tiebreaker two
483
+ // rows sharing (importance, created_at_epoch) may come back in either relative order,
484
+ // and SQLite is free to use a bounded top-N sorter at a small LIMIT and a full sort at a
485
+ // large one — so "the narrow pool is a prefix of the wide pool", which the v3.82.0
486
+ // widening argument rests on, was an empirical property of this corpus rather than a
487
+ // guaranteed one. There are zero such collisions live, so it changes no behaviour here;
488
+ // it makes the guarantee hold on corpora nobody has seen.
431
489
  try {
432
490
  rows = db.prepare(`
433
491
  SELECT id, title, lesson_learned, importance
@@ -439,8 +497,8 @@ export function rankImperativeCandidates(db, userPrompt, project, excludeIds = [
439
497
  AND TRIM(lesson_learned) != ''
440
498
  AND LOWER(TRIM(lesson_learned)) != 'none'
441
499
  AND (? IS NULL OR created_at_epoch <= ?)
442
- ORDER BY importance DESC, created_at_epoch DESC
443
- LIMIT 50
500
+ ORDER BY importance DESC, created_at_epoch DESC, id DESC
501
+ LIMIT ${IMPERATIVE_POOL_BACKSTOP}
444
502
  `).all(project, epochTo, epochTo);
445
503
  } catch { return []; }
446
504
  const out = [];
@@ -547,15 +547,72 @@ 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 — CLOSED 2026-08-25, in rankImperativeCandidates rather than here.
590
+ * All 22 marginal rows are importance=3, and the pool took `LIMIT 50` ordered by
591
+ * importance DESC, so in the five largest projects (projects--mem 327, code-graph-mcp
592
+ * 121, ubuntu-sec 56, daagu 53, agentsmd 51 — counted with the pool's OWN
593
+ * `liveObsFilterSql`, which is the only count that means anything here) the imp=3
594
+ * population alone exceeded the limit: a demotion 3->2 EVICTED a lesson from this face's
595
+ * candidate pool rather than down-ranking it — a feedback loop the four original
596
+ * denominator faces do not have, because they select on FTS relevance.
597
+ *
598
+ * Measuring it turned up a bigger fact than the one filed. The limit sits in SQL,
599
+ * BEFORE the identifier-overlap filter, so it was never a ranking bound at all: the
600
+ * face could only ever pick from the 50 newest `importance >= 2` rows, which made every
601
+ * importance=2 lesson in those five projects unreachable no matter how well it matched.
602
+ * Replaying 373 real prompts against their own project's corpus
603
+ * (`benchmark/imperative-pool-replay.mjs`), the cap destroyed 7 of 85 picks outright and
604
+ * changed the top-1 in 3 of 78. The bound is now IMPERATIVE_POOL_BACKSTOP = 5000,
605
+ * documented there as an OOM backstop and not a relevance gate.
606
+ *
607
+ * A 3->2 demotion is a down-rank again. A 2->1 demotion is still an EVICTION, because
608
+ * the pool gate is `>= 2` and IMPORTANCE_FLOOR is 1 — and widening is what first makes
609
+ * importance=2 rows reachable by this face at all, so it creates the injections that can
610
+ * walk one there. Measured exposure is one row; see the constant's docblock. `subagent`
611
+ * shares that pool and inherits both halves. Still open in D#172: admitting `subagent`
612
+ * to the denominator, which is a separate decision needing the receiver-attributed cites
613
+ * merged asymmetrically.
557
614
  */
558
- const DECAY_EXCLUDED_SURFACES = new Set(['task_imperative']);
615
+ const DECAY_EXCLUDED_SURFACES = new Set();
559
616
 
560
617
  /** @type {ReadonlyArray<string>} faces that DO feed the decay denominator. */
561
618
  export const DECAY_DENOMINATOR_SURFACES = ATTACHMENT_SURFACES.filter((f) => !DECAY_EXCLUDED_SURFACES.has(f));
@@ -578,6 +635,30 @@ export const DECAY_DENOMINATOR_SURFACES = ATTACHMENT_SURFACES.filter((f) => !DEC
578
635
  * subagent-only injection uncited by construction. Metered first (this is the
579
636
  * whole point of D#152: the face's cite-rate is unknown), decided second.
580
637
  *
638
+ * D#164 read it (2026-08-25, 30 live sessions): 25.0% (12/48) on the house
639
+ * id-level caliber — above fyi (10.9%) and error_recall (6.2%), both of which
640
+ * ARE in the denominator, so "it performs badly" is not available as a reason
641
+ * to keep it out. The two costs that ARE measured: (1) admitting it means
642
+ * feeding its receiver-attributed cites alongside `citedMain`, which would flip
643
+ * 3 main-face (session,id) pairs from demote to promote on a cite the main thread
644
+ * never made — 3 of the 1064 such pairs inside the 30 subagent-bearing sessions
645
+ * sampled (0.28%), or 3 of 1935 (0.16%) if you widen to every subagent-bearing
646
+ * session in the corpus; state which denominator you mean, the phrase "main-face
647
+ * ids" alone does not pin it; (2) the 33 rows it would newly add cite at 15.2% and are
648
+ * 94% importance=3. That second cost was the LIMIT-50 eviction loop described under
649
+ * `task_imperative` above, shared because both faces select through
650
+ * rankImperativeCandidates — CLOSED 2026-08-25 by raising that pool's bound out of
651
+ * relevance range, so it is no longer an argument against admitting this face. Three
652
+ * of those 33 would still demote over that corpus, as down-ranks rather than evictions.
653
+ *
654
+ * Its sibling was admitted on 2026-08-25; this one deliberately was NOT, and the
655
+ * difference is not the rate. task_imperative needed one line and no change to
656
+ * `citedMain`; this face needs the receiver-attributed cites merged in
657
+ * asymmetrically, and its numerator only became trustworthy on 2026-08-25 (see
658
+ * collectSubagentSurface — it credited cross-agent citations until then). Letting
659
+ * one release separate them also means the eviction loop they share is observed
660
+ * on one face before it acts on two. Tracked in D#172.
661
+ *
581
662
  * A member here that the Stop path stops feeding becomes an all-zero face, not
582
663
  * a silently-demoting one — which is the failure mode worth keeping.
583
664
  * @type {ReadonlyArray<string>}
@@ -684,20 +765,32 @@ export function findSubagentTranscripts(transcriptPath) {
684
765
  * also why the Stop path records this face in its own recordCitationSurfaces
685
766
  * call: one call carries one `cited` set for every face in it.
686
767
  *
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.
768
+ * UNIT — read this before reading the rate it produces (D#164 settled the first
769
+ * half of it; the second half is still a live caveat).
770
+ *
771
+ * `injected` is unioned by id across the session's sidechain files, which is the
772
+ * house caliber: every other face also counts an obs once per session no matter
773
+ * how many times it was injected. `cited` is NOT unioned an id is credited only
774
+ * when the agent that RECEIVED it is the agent that cited it. This face is the
775
+ * only one where injection and citation can land in different contexts, so the
776
+ * union form silently counted "agent A was handed it, agent B mentioned it" as
777
+ * adoption. Measured over 30 live sessions before the attribution was added:
778
+ * 13/48 unioned vs 12/48 receiver-attributed.
779
+ *
780
+ * What is still biased HIGH: an id handed to three agents and cited by one counts
781
+ * as one full hit rather than a third, because the id-level denominator collapses
782
+ * the three dispatches into one. On the same corpus that is 48 ids over 82
783
+ * (dispatch, id) PAIRS — not 82 files: the sessions hold ~268 sidechain transcripts and
784
+ * most carry no injection — i.e. 25.0% id-level against 14.6% per-dispatch. Opportunity-level
785
+ * accounting would need a denominator shape citation_surface_log does not have
786
+ * (its key is (project, session, surface)), and changing that would put this face
787
+ * on a different ruler from the other six — so the id-level number is the one
788
+ * that is comparable to pretool/ups/fyi, and the per-dispatch number is the one
789
+ * to quote when asking "does a dispatched agent use what it was handed".
698
790
  *
699
791
  * @param {string|null|undefined} transcriptPath main-thread transcript (.jsonl)
700
792
  * @returns {{injected: Set<number>, cited: Set<number>, files: number}}
793
+ * `cited` is always a subset of `injected`.
701
794
  */
702
795
  export function collectSubagentSurface(transcriptPath) {
703
796
  const injected = new Set();
@@ -705,8 +798,13 @@ export function collectSubagentSurface(transcriptPath) {
705
798
  let files = 0;
706
799
  for (const p of findSubagentTranscripts(transcriptPath)) {
707
800
  files++;
708
- for (const id of extractInjectedFromSubagentPrompt(p)) injected.add(id);
709
- for (const id of extractCitationsFromTranscript(p)) cited.add(id);
801
+ // Per-file intersection, not two unions: the pairing is the whole point.
802
+ const seen = extractInjectedFromSubagentPrompt(p);
803
+ const said = extractCitationsFromTranscript(p);
804
+ for (const id of seen) {
805
+ injected.add(id);
806
+ if (said.has(id)) cited.add(id);
807
+ }
710
808
  }
711
809
  return { injected, cited, files };
712
810
  }
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.82.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.82.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.82.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",