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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/hook-episode.mjs +66 -0
- package/lib/citation-tracker.mjs +105 -21
- package/mem-cli.mjs +5 -3
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "claude-mem-lite",
|
|
13
|
-
"version": "3.
|
|
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.
|
|
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}}
|
package/lib/citation-tracker.mjs
CHANGED
|
@@ -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
|
-
*
|
|
551
|
-
*
|
|
552
|
-
*
|
|
553
|
-
*
|
|
554
|
-
* framing under-performs, the penalty lands on the
|
|
555
|
-
* selects high-value ones) rather than on the
|
|
556
|
-
*
|
|
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(
|
|
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
|
|
688
|
-
*
|
|
689
|
-
*
|
|
690
|
-
*
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
694
|
-
*
|
|
695
|
-
*
|
|
696
|
-
*
|
|
697
|
-
*
|
|
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
|
-
|
|
709
|
-
|
|
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
|
|
2655
|
-
//
|
|
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)
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "3.
|
|
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.
|
|
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.
|
|
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",
|