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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/hook-episode.mjs +66 -0
- package/hook-memory.mjs +60 -2
- package/lib/citation-tracker.mjs +119 -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.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.
|
|
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
|
|
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 = [];
|
package/lib/citation-tracker.mjs
CHANGED
|
@@ -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
|
-
*
|
|
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 — 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(
|
|
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
|
|
688
|
-
*
|
|
689
|
-
*
|
|
690
|
-
*
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
694
|
-
*
|
|
695
|
-
*
|
|
696
|
-
*
|
|
697
|
-
*
|
|
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
|
-
|
|
709
|
-
|
|
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
|
|
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.82.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.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.
|
|
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",
|