claude-mem-lite 2.35.0 → 2.36.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-llm.mjs +68 -1
- package/hook.mjs +18 -0
- package/lib/citation-tracker.mjs +82 -0
- package/lib/low-signal-patterns.mjs +79 -0
- package/package.json +2 -1
- package/source-files.mjs +1 -0
package/hook-llm.mjs
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
sessionFile, getSessionId, openDb, callLLM, sleep,
|
|
17
17
|
} from './hook-shared.mjs';
|
|
18
18
|
import { EVENT_TYPES, saveEvent } from './lib/activity.mjs';
|
|
19
|
+
import { isNoiseObservation } from './lib/low-signal-patterns.mjs';
|
|
19
20
|
|
|
20
21
|
// T9: memdir-incompatible types live in the `events` table, not `observations`.
|
|
21
22
|
// Set lookup is O(1) — authoritative source is lib/activity.mjs::EVENT_TYPES.
|
|
@@ -69,6 +70,14 @@ export function saveObservation(obs, projectOverride, sessionIdOverride, externa
|
|
|
69
70
|
VALUES (?, ?, ?, ?, ?, 'active')
|
|
70
71
|
`).run(sessionId, sessionId, project, now.toISOString(), now.getTime());
|
|
71
72
|
|
|
73
|
+
// P0: write-side noise block — LOW_SIGNAL title with no recoverable signal
|
|
74
|
+
// (no lesson, importance<2, empty facts, thin narrative) is dropped before
|
|
75
|
+
// dedup/MinHash/vector work. Opt-out: CLAUDE_MEM_KEEP_LOW_SIGNAL=1.
|
|
76
|
+
if (isNoiseObservation(obs)) {
|
|
77
|
+
debugLog('saveObservation', `dropped noise: ${truncate(obs.title || '', 60)}`);
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
72
81
|
// Three-tier dedup — returns null (not throw) for dedup hits
|
|
73
82
|
// Tier 1 (fast): 5-min Jaccard on titles
|
|
74
83
|
const fiveMinAgo = now.getTime() - DEDUP_WINDOW_MS;
|
|
@@ -464,6 +473,38 @@ export function buildImmediateObservation(episode) {
|
|
|
464
473
|
};
|
|
465
474
|
}
|
|
466
475
|
|
|
476
|
+
// ─── Lesson retry prompt (P3) ───────────────────────────────────────────────
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Build a lesson-focused retry prompt after Haiku's first pass for
|
|
480
|
+
* bugfix/decision returned null/empty/'none'. Narrow ask: one non-obvious
|
|
481
|
+
* insight a future session would benefit from — either root cause (bugfix)
|
|
482
|
+
* or tradeoff (decision).
|
|
483
|
+
*
|
|
484
|
+
* @param {object} episode
|
|
485
|
+
* @param {object} firstPass — parsed first-pass response (title, type, narrative)
|
|
486
|
+
* @returns {string} prompt
|
|
487
|
+
*/
|
|
488
|
+
export function buildLessonRetryPrompt(episode, firstPass) {
|
|
489
|
+
const actionList = episode.entries.map((e, i) =>
|
|
490
|
+
`${i + 1}. [${e.tool}] ${e.desc}${e.isError ? ' (ERROR)' : ''}`
|
|
491
|
+
).join('\n');
|
|
492
|
+
const typeHint = firstPass.type === 'bugfix'
|
|
493
|
+
? 'For this bugfix: what was the root cause + how to spot it next time? Example: "FTS5 trigger fires on any UPDATE — wrap access_count writes in try/catch."'
|
|
494
|
+
: 'For this decision: what tradeoff was made + why? Example: "Chose single-source module over schema column because 1 drift point, not 4."';
|
|
495
|
+
return `A ${firstPass.type} episode just completed. First-pass title: "${firstPass.title || 'untitled'}".
|
|
496
|
+
|
|
497
|
+
Actions:
|
|
498
|
+
${actionList}
|
|
499
|
+
|
|
500
|
+
${typeHint}
|
|
501
|
+
|
|
502
|
+
If the work was purely mechanical with no insight worth remembering, reply {"lesson":"none"}.
|
|
503
|
+
Otherwise reply in 12-280 chars.
|
|
504
|
+
|
|
505
|
+
Reply ONLY valid JSON, no markdown fences: {"lesson":"..."}`;
|
|
506
|
+
}
|
|
507
|
+
|
|
467
508
|
// ─── Background: LLM Episode Extraction (Tier 2 F) ──────────────────────────
|
|
468
509
|
|
|
469
510
|
export async function handleLLMEpisode() {
|
|
@@ -506,6 +547,7 @@ Action: ${e.desc}
|
|
|
506
547
|
Error: ${e.isError ? 'yes' : 'no'}
|
|
507
548
|
|
|
508
549
|
JSON: {"type":"decision|bugfix|feature|refactor|discovery|change","title":"concise ≤80 char description","narrative":"what changed, why, and outcome (2-3 sentences)","concepts":["kw1","kw2"],"facts":["fact1","fact2"],"importance":1,"lesson_learned":"non-obvious insight or 'none' if routine","search_aliases":["alt query 1","alt query 2"]}
|
|
550
|
+
type: pick by strongest signal. decision = explicit tradeoff / "chose X over Y because Z" / rejected an approach (e.g. "Rejected schema migration — single-source module + sync test instead"; "Heterogeneous hook events → heterogeneous context budgets"). bugfix = prior-failing path fixed with a named root cause. feature = new user-visible capability. refactor = behavior unchanged but structure improved. discovery = learned how a system works (read-heavy, no writes). change = routine edit with no new principle (default if unsure and nothing else fits).
|
|
509
551
|
Facts: each MUST be (1) atomic—one claim, (2) self-contained—no pronouns, include file/function name, (3) specific—"refreshToken() in auth.ts:45 uses 1h TTL" not "handles tokens"
|
|
510
552
|
importance: Be strict — default to 1. 0=pure browsing with zero learning value. 1=routine file edits, standard changes, normal workflow (MOST episodes). 2=notable ONLY if it reveals something non-obvious: error fix with discovered root cause, architectural decision with explicit tradeoff, config change with unexpected side effects. 3=critical: breaking change affecting users, security vulnerability fix, data migration. Ask yourself: "would a future session benefit from knowing this?" — if not, it's importance=1.
|
|
511
553
|
lesson_learned: REQUIRED field. State what was learned that isn't obvious from reading the code. Examples: "FTS5 porter stemmer doesn't tokenize CJK — need bigram workaround", "vitest --reporter=verbose hangs on large test suites, use default reporter". If purely routine with nothing learned, write "none" (not null).
|
|
@@ -523,6 +565,7 @@ Actions (${episode.entries.length} total):
|
|
|
523
565
|
${actionList}
|
|
524
566
|
|
|
525
567
|
JSON: {"type":"decision|bugfix|feature|refactor|discovery|change","title":"coherent ≤80 char summary","narrative":"what was done, why, and outcome (3-5 sentences)","concepts":["keyword1","keyword2"],"facts":["specific fact 1","specific fact 2"],"importance":1,"lesson_learned":"non-obvious insight or 'none' if routine","search_aliases":["alt query 1","alt query 2"]}
|
|
568
|
+
type: pick by strongest signal. decision = explicit tradeoff / "chose X over Y because Z" / rejected an approach (e.g. "Rejected schema migration — single-source module + sync test instead"; "Heterogeneous hook events → heterogeneous context budgets"). bugfix = prior-failing path fixed with a named root cause. feature = new user-visible capability. refactor = behavior unchanged but structure improved. discovery = learned how a system works (read-heavy, no writes). change = routine edit with no new principle (default if unsure and nothing else fits).
|
|
526
569
|
Facts: each MUST be (1) atomic—one claim, (2) self-contained—no pronouns, include file/function name, (3) specific—"refreshToken() in auth.ts:45 uses 1h TTL" not "handles tokens"
|
|
527
570
|
importance: Be strict — default to 1. 0=pure browsing with zero learning value. 1=routine file edits, standard changes, normal workflow (MOST episodes). 2=notable ONLY if it reveals something non-obvious: error fix with discovered root cause, architectural decision with explicit tradeoff, config change with unexpected side effects. 3=critical: breaking change affecting users, security vulnerability fix, data migration. Ask yourself: "would a future session benefit from knowing this?" — if not, it's importance=1.
|
|
528
571
|
lesson_learned: REQUIRED field. State what was learned that isn't obvious from reading the code. Examples: "FTS5 porter stemmer doesn't tokenize CJK — need bigram workaround", "vitest --reporter=verbose hangs on large test suites, use default reporter". If purely routine with nothing learned, write "none" (not null).
|
|
@@ -570,7 +613,31 @@ search_aliases: 2-6 alternative search terms someone might use to find this memo
|
|
|
570
613
|
const rawLesson = typeof parsed.lesson_learned === 'string' ? parsed.lesson_learned.trim() : '';
|
|
571
614
|
const lowSignalLesson = new Set(['none', '', 'n/a', 'null', 'todo', 'tbd', 'na', '-', 'nothing', 'nil']);
|
|
572
615
|
const isLessonLowSignal = lowSignalLesson.has(rawLesson.toLowerCase()) || rawLesson.length < 12;
|
|
573
|
-
|
|
616
|
+
let lessonLearned = isLessonLowSignal ? null : rawLesson.slice(0, 500);
|
|
617
|
+
|
|
618
|
+
// P3: for bugfix/decision, retry once with a lesson-focused prompt.
|
|
619
|
+
// These types have the highest reuse value (~72.7% hit-rate vs change
|
|
620
|
+
// ~16.5%), and Haiku's first pass writes NULL ~70% of the time for
|
|
621
|
+
// curated observations. Retry budget: 1 extra callLLM per bugfix/decision
|
|
622
|
+
// episode. Opt-out: CLAUDE_MEM_NO_LESSON_RETRY=1.
|
|
623
|
+
if (isLessonLowSignal &&
|
|
624
|
+
(parsed.type === 'bugfix' || parsed.type === 'decision') &&
|
|
625
|
+
!process.env.CLAUDE_MEM_NO_LESSON_RETRY) {
|
|
626
|
+
try {
|
|
627
|
+
const retryPrompt = buildLessonRetryPrompt(episode, parsed);
|
|
628
|
+
const retryRaw = callLLM(retryPrompt, 10000);
|
|
629
|
+
if (retryRaw) {
|
|
630
|
+
const retry = parseJsonFromLLM(retryRaw);
|
|
631
|
+
const retryLesson = typeof retry?.lesson === 'string' ? retry.lesson.trim() : '';
|
|
632
|
+
const retryIsLow = lowSignalLesson.has(retryLesson.toLowerCase()) || retryLesson.length < 12;
|
|
633
|
+
if (!retryIsLow) {
|
|
634
|
+
lessonLearned = retryLesson.slice(0, 500);
|
|
635
|
+
debugLog('DEBUG', 'llm-episode', `lesson-retry: recovered ${retryLesson.length}-char lesson for ${parsed.type}`);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
} catch (e) { debugCatch(e, 'lesson-retry'); }
|
|
639
|
+
}
|
|
640
|
+
|
|
574
641
|
const searchAliases = Array.isArray(parsed.search_aliases)
|
|
575
642
|
? parsed.search_aliases.slice(0, 6).join(' ')
|
|
576
643
|
: null;
|
package/hook.mjs
CHANGED
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
spawnBackground,
|
|
43
43
|
} from './hook-shared.mjs';
|
|
44
44
|
import { handleLLMEpisode, handleLLMSummary, saveObservation, buildImmediateObservation } from './hook-llm.mjs';
|
|
45
|
+
import { extractCitationsFromTranscript, bumpCitationAccess } from './lib/citation-tracker.mjs';
|
|
45
46
|
import { searchRelevantMemories } from './hook-memory.mjs';
|
|
46
47
|
import { buildAndSaveHandoff, detectContinuationIntent, renderHandoffInjection, extractUnfinishedSummary } from './hook-handoff.mjs';
|
|
47
48
|
import { checkForUpdate } from './hook-update.mjs';
|
|
@@ -344,12 +345,16 @@ async function handleStop() {
|
|
|
344
345
|
// This is the stable CC identifier — the mem plugin's file-based getSessionId()
|
|
345
346
|
// collides across parallel sessions for the same project (see docs/bug.txt).
|
|
346
347
|
let ccSessionId = null;
|
|
348
|
+
let transcriptPath = null;
|
|
347
349
|
try {
|
|
348
350
|
const raw = await readStdin();
|
|
349
351
|
const hookData = JSON.parse(raw.text);
|
|
350
352
|
if (typeof hookData?.session_id === 'string' && hookData.session_id.length > 0) {
|
|
351
353
|
ccSessionId = hookData.session_id;
|
|
352
354
|
}
|
|
355
|
+
if (typeof hookData?.transcript_path === 'string' && hookData.transcript_path.length > 0) {
|
|
356
|
+
transcriptPath = hookData.transcript_path;
|
|
357
|
+
}
|
|
353
358
|
} catch { /* stdin unavailable — fall back to local session id */ }
|
|
354
359
|
|
|
355
360
|
// Capture session info BEFORE cleanup. All DB lookups use the mem-internal id
|
|
@@ -448,6 +453,19 @@ async function handleStop() {
|
|
|
448
453
|
}
|
|
449
454
|
}
|
|
450
455
|
} catch (e) { debugCatch(e, 'handleStop-fast-summary'); }
|
|
456
|
+
|
|
457
|
+
// P4: scan transcript for `#NN` observation citations in assistant text
|
|
458
|
+
// and bump access_count for matched rows. Closes the loop on the "cite #NN"
|
|
459
|
+
// contract — before P4 this was a one-way obligation with no feedback.
|
|
460
|
+
try {
|
|
461
|
+
if (transcriptPath && !process.env.CLAUDE_MEM_NO_CITATION_TRACK) {
|
|
462
|
+
const ids = extractCitationsFromTranscript(transcriptPath);
|
|
463
|
+
if (ids.size > 0) {
|
|
464
|
+
const n = bumpCitationAccess(db, ids, project);
|
|
465
|
+
debugLog('DEBUG', 'handleStop', `citations: ${ids.size} ids scanned, ${n} obs bumped`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
} catch (e) { debugCatch(e, 'handleStop-citation-track'); }
|
|
451
469
|
} finally {
|
|
452
470
|
db.close();
|
|
453
471
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Citation tracker (P4): scan Claude Code transcript for `#NN` observation-id
|
|
2
|
+
// citations in assistant text, then bulk-increment access_count for matched rows.
|
|
3
|
+
//
|
|
4
|
+
// Closes the loop on the CLAUDE.md "cite #NN" contract — before P4, citations
|
|
5
|
+
// were a one-way obligation with no measurable feedback. Now each honored
|
|
6
|
+
// citation bumps access_count, making contract compliance observable via
|
|
7
|
+
// mem_stats and preventing cited lessons from decaying into dead memory.
|
|
8
|
+
//
|
|
9
|
+
// FTS5 caveat (project_non_obvious.md): observations_au trigger fires on any
|
|
10
|
+
// column UPDATE including access_count. Per-row UPDATEs wrapped in try-catch
|
|
11
|
+
// to prevent SQLITE_CORRUPT_VTAB cascades from stopping the whole scan.
|
|
12
|
+
|
|
13
|
+
import { readFileSync, existsSync } from 'fs';
|
|
14
|
+
import { debugCatch } from '../utils.mjs';
|
|
15
|
+
|
|
16
|
+
// `#123` / `#45678` at a word boundary — matches the CLAUDE.md cite pattern.
|
|
17
|
+
// Bounded to 1-7 digits to skip URL fragments, markdown anchors, etc.
|
|
18
|
+
const CITATION_RE = /#(\d{1,7})\b/g;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Parse a Claude Code transcript .jsonl and extract unique observation IDs
|
|
22
|
+
* cited inside assistant text blocks.
|
|
23
|
+
*
|
|
24
|
+
* @param {string} transcriptPath Path to transcript file (.jsonl)
|
|
25
|
+
* @returns {Set<number>} unique IDs referenced as `#NN` in assistant text
|
|
26
|
+
*/
|
|
27
|
+
export function extractCitationsFromTranscript(transcriptPath) {
|
|
28
|
+
const ids = new Set();
|
|
29
|
+
if (!transcriptPath || !existsSync(transcriptPath)) return ids;
|
|
30
|
+
let raw;
|
|
31
|
+
try { raw = readFileSync(transcriptPath, 'utf8'); } catch { return ids; }
|
|
32
|
+
for (const line of raw.split('\n')) {
|
|
33
|
+
if (!line.trim()) continue;
|
|
34
|
+
let entry;
|
|
35
|
+
try { entry = JSON.parse(line); } catch { continue; }
|
|
36
|
+
// Claude Code transcript: one JSON per line with type='assistant' | 'user' | ...
|
|
37
|
+
if (entry.type !== 'assistant' || !entry.message) continue;
|
|
38
|
+
const content = entry.message.content;
|
|
39
|
+
if (!Array.isArray(content)) continue;
|
|
40
|
+
for (const block of content) {
|
|
41
|
+
if (block.type !== 'text' || typeof block.text !== 'string') continue;
|
|
42
|
+
CITATION_RE.lastIndex = 0;
|
|
43
|
+
let m;
|
|
44
|
+
while ((m = CITATION_RE.exec(block.text))) {
|
|
45
|
+
const id = Number(m[1]);
|
|
46
|
+
if (Number.isInteger(id) && id > 0 && id < 1e7) ids.add(id);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return ids;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Increment `access_count` (and `last_accessed_at`) for each cited observation
|
|
55
|
+
* that belongs to `project`. Returns the count of successful increments.
|
|
56
|
+
*
|
|
57
|
+
* Per-row UPDATE in try-catch so a single FTS-corrupted row can't abort the
|
|
58
|
+
* scan. Cross-project IDs are silently ignored by the WHERE clause.
|
|
59
|
+
*
|
|
60
|
+
* @param {import('better-sqlite3').Database} db
|
|
61
|
+
* @param {Iterable<number>} ids
|
|
62
|
+
* @param {string} project
|
|
63
|
+
* @returns {number} count of rows incremented
|
|
64
|
+
*/
|
|
65
|
+
export function bumpCitationAccess(db, ids, project) {
|
|
66
|
+
if (!db || !ids || !project) return 0;
|
|
67
|
+
const idList = Array.isArray(ids) ? ids : [...ids];
|
|
68
|
+
if (idList.length === 0) return 0;
|
|
69
|
+
const stmt = db.prepare(`
|
|
70
|
+
UPDATE observations SET access_count = access_count + 1, last_accessed_at = ?
|
|
71
|
+
WHERE id = ? AND project = ?
|
|
72
|
+
`);
|
|
73
|
+
const now = Date.now();
|
|
74
|
+
let n = 0;
|
|
75
|
+
for (const id of idList) {
|
|
76
|
+
try {
|
|
77
|
+
const result = stmt.run(now, id, project);
|
|
78
|
+
if (result.changes > 0) n++;
|
|
79
|
+
} catch (e) { debugCatch(e, `bumpCitationAccess-id-${id}`); }
|
|
80
|
+
}
|
|
81
|
+
return n;
|
|
82
|
+
}
|
|
@@ -58,3 +58,82 @@ export function buildNotLowSignalSql(alias = '') {
|
|
|
58
58
|
const clauses = LOW_SIGNAL_PATTERNS.map(({ like }) => `${p}title NOT LIKE '${like}'`);
|
|
59
59
|
return '(\n ' + clauses.join('\n AND ') + '\n )';
|
|
60
60
|
}
|
|
61
|
+
|
|
62
|
+
// Cached singleton — isNoiseObservation is called once per observation insert.
|
|
63
|
+
const _LOW_SIG_RE = buildLowSignalRegex();
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Detect narrative that is raw tool-output passthrough, not human/LLM prose (P2).
|
|
67
|
+
*
|
|
68
|
+
* `buildImmediateObservation` constructs narrative as
|
|
69
|
+
* `episode.entries.map(e => e.desc).join('; ')` where each desc is
|
|
70
|
+
* "cmd → stdout/stderr" from `scripts/post-tool-use.sh`. Such narratives
|
|
71
|
+
* have characteristic fingerprints (arrows, stack traces, diffs, test
|
|
72
|
+
* failure banners, absent sentence prose) that Haiku/user-written narratives
|
|
73
|
+
* don't. This check treats passthrough narratives as zero-signal for the
|
|
74
|
+
* purposes of isNoiseObservation.
|
|
75
|
+
*
|
|
76
|
+
* @param {string} narrative
|
|
77
|
+
* @returns {boolean} true = raw tool output, not substantive narrative
|
|
78
|
+
*/
|
|
79
|
+
function _isLikelyToolOutputPassthrough(narrative) {
|
|
80
|
+
if (!narrative || narrative.length < 80) return false;
|
|
81
|
+
// post-tool-use.sh formats entries as "cmd → output"; presence of " → " in
|
|
82
|
+
// a long narrative is near-diagnostic of raw entry-desc passthrough.
|
|
83
|
+
if (/ → /.test(narrative)) return true;
|
|
84
|
+
// Stack-trace fingerprints that never appear in curated narratives.
|
|
85
|
+
if (/\n\s+at .+:\d+:\d+/.test(narrative)) return true;
|
|
86
|
+
if (/node:internal\//.test(narrative)) return true;
|
|
87
|
+
// Raw diff output.
|
|
88
|
+
if (/(^|\n)diff --git |(^|\n)@@ -\d/.test(narrative)) return true;
|
|
89
|
+
// Test-runner failure banners.
|
|
90
|
+
if (/(^|\n)\s*FAIL\s+|AssertionError|TypeError: |SyntaxError: /.test(narrative)) return true;
|
|
91
|
+
// Absent sentence prose + multi-"; " is the buildImmediateObservation join signature.
|
|
92
|
+
const hasSentenceBreaks = /\. [A-Z]/.test(narrative);
|
|
93
|
+
const semiJoins = (narrative.match(/; /g) || []).length;
|
|
94
|
+
if (!hasSentenceBreaks && semiJoins >= 2) return true;
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Write-side noise filter (P0/P2). Returns true when an observation has a
|
|
100
|
+
* LOW_SIGNAL title AND no recoverable downstream signal — caller should skip
|
|
101
|
+
* insertion.
|
|
102
|
+
*
|
|
103
|
+
* Contract: a low-signal title is kept if ANY of these carry signal:
|
|
104
|
+
* - lesson_learned set and not 'none'
|
|
105
|
+
* - importance >= 2
|
|
106
|
+
* - facts has >=1 non-empty string
|
|
107
|
+
* - narrative >= 40 chars AND not raw stderr / tool-output passthrough (P2)
|
|
108
|
+
*
|
|
109
|
+
* Opt-out: env `CLAUDE_MEM_KEEP_LOW_SIGNAL=1` disables filter (preserves
|
|
110
|
+
* pre-v2.36 behavior — every observation is inserted regardless of signal).
|
|
111
|
+
*
|
|
112
|
+
* @param {object} obs Observation shape: { title, facts, narrative, lessonLearned|lesson_learned, importance }
|
|
113
|
+
* @param {object} [env=process.env] Environment (injected for testability)
|
|
114
|
+
* @returns {boolean} true = noise, caller should drop
|
|
115
|
+
*/
|
|
116
|
+
export function isNoiseObservation(obs, env = process.env) {
|
|
117
|
+
if (env && env.CLAUDE_MEM_KEEP_LOW_SIGNAL === '1') return false;
|
|
118
|
+
const title = (obs && obs.title) || '';
|
|
119
|
+
if (!_LOW_SIG_RE.test(title)) return false;
|
|
120
|
+
|
|
121
|
+
const lesson = obs.lessonLearned ?? obs.lesson_learned;
|
|
122
|
+
if (lesson && String(lesson).trim() && String(lesson).trim().toLowerCase() !== 'none') return false;
|
|
123
|
+
|
|
124
|
+
if ((obs.importance ?? 1) >= 2) return false;
|
|
125
|
+
|
|
126
|
+
if (Array.isArray(obs.facts) &&
|
|
127
|
+
obs.facts.filter(f => typeof f === 'string' && f.trim().length > 0).length >= 1) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const narrative = (obs.narrative || '').trim();
|
|
132
|
+
if (narrative.length >= 40 &&
|
|
133
|
+
!/^Error[: ]/i.test(narrative) &&
|
|
134
|
+
!_isLikelyToolOutputPassthrough(narrative)) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return true;
|
|
139
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.36.0",
|
|
4
4
|
"description": "Lightweight persistent memory system for Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"lib/doctor-drift.mjs",
|
|
51
51
|
"lib/stats-quality.mjs",
|
|
52
52
|
"lib/low-signal-patterns.mjs",
|
|
53
|
+
"lib/citation-tracker.mjs",
|
|
53
54
|
"registry.mjs",
|
|
54
55
|
"registry-retriever.mjs",
|
|
55
56
|
"registry-indexer.mjs",
|
package/source-files.mjs
CHANGED