claude-mem-lite 2.28.2 → 2.30.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/hook.mjs CHANGED
@@ -5,13 +5,13 @@
5
5
  // Background workers (slow): llm-episode, llm-summary
6
6
 
7
7
  import { randomUUID } from 'crypto';
8
- import { join, basename } from 'path';
8
+ import { join } from 'path';
9
9
  import { readFileSync, writeFileSync, unlinkSync, readdirSync, renameSync, statSync } from 'fs';
10
10
  import { homedir } from 'os';
11
11
  import {
12
- truncate, typeIcon, inferProject, detectBashSignificance,
12
+ truncate, inferProject, detectBashSignificance,
13
13
  extractErrorKeywords, extractFilePaths, isRelatedToEpisode,
14
- makeEntryDesc, scrubSecrets, EDIT_TOOLS, debugCatch, debugLog, fmtTime,
14
+ makeEntryDesc, scrubSecrets, EDIT_TOOLS, debugCatch, debugLog,
15
15
  COMPRESSED_AUTO, COMPRESSED_PENDING_PURGE, isoWeekKey, OBS_BM25,
16
16
  } from './utils.mjs';
17
17
  import {
@@ -20,10 +20,10 @@ import {
20
20
  createEpisode, addFileToEpisode,
21
21
  writePendingEntry, mergePendingEntries, episodeHasSignificantContent,
22
22
  } from './hook-episode.mjs';
23
- import { selectWithTokenBudget, updateClaudeMd, buildSummaryLines } from './hook-context.mjs';
23
+ import { cleanupClaudeMdLegacyBlock, buildSessionContextLines } from './hook-context.mjs';
24
24
  import {
25
25
  RUNTIME_DIR, EPISODE_BUFFER_SIZE, EPISODE_TIME_GAP_MS,
26
- SESSION_EXPIRY_MS, STALE_SESSION_MS, STALE_LOCK_MS, FALLBACK_OBS_WINDOW_MS,
26
+ SESSION_EXPIRY_MS, STALE_SESSION_MS, STALE_LOCK_MS,
27
27
  sessionFile, getSessionId, createSessionId, openDb,
28
28
  spawnBackground,
29
29
  } from './hook-shared.mjs';
@@ -31,13 +31,14 @@ import { handleLLMEpisode, handleLLMSummary, saveObservation, buildImmediateObse
31
31
  import { searchRelevantMemories } from './hook-memory.mjs';
32
32
  import { buildAndSaveHandoff, detectContinuationIntent, renderHandoffInjection, extractUnfinishedSummary } from './hook-handoff.mjs';
33
33
  import { checkForUpdate } from './hook-update.mjs';
34
+ import { handleLLMOptimize } from './hook-optimize.mjs';
34
35
  import { SKIP_TOOLS, SKIP_PREFIXES } from './skip-tools.mjs';
35
36
  import { getVocabulary } from './tfidf.mjs';
36
37
 
37
38
  // Prevent recursive hooks from background claude -p calls
38
39
  // Background workers (llm-episode, llm-summary) are exempt — they're ours
39
40
  const event = process.argv[2];
40
- const BG_EVENTS = new Set(['llm-episode', 'llm-summary', 'auto-compress']);
41
+ const BG_EVENTS = new Set(['llm-episode', 'llm-summary', 'auto-compress', 'llm-optimize']);
41
42
 
42
43
  // Respect Claude Code plugin disable state even when legacy settings.json hooks remain.
43
44
  // install.mjs writes direct hooks into ~/.claude/settings.json, so disabling the plugin
@@ -122,6 +123,22 @@ function flushEpisode(episode) {
122
123
 
123
124
  if (isSignificant) {
124
125
  spawnBackground('llm-episode', flushFile);
126
+
127
+ // P3: Auto-save hint — detect error→fix pattern (error entry followed by Edit/Write)
128
+ // and nudge Claude to save the lesson for future recall
129
+ try {
130
+ const entries = episode.entries || [];
131
+ const hasError = entries.some(e => e.isError);
132
+ const hasEdit = entries.some(e => EDIT_TOOLS.has(e.tool));
133
+ if (hasError && hasEdit && entries.length >= 3) {
134
+ const editFiles = entries.filter(e => EDIT_TOOLS.has(e.tool)).flatMap(e => e.files || []);
135
+ const uniqueFiles = [...new Set(editFiles)].slice(0, 3);
136
+ const filesHint = uniqueFiles.length > 0 ? ` (files: ${uniqueFiles.join(', ')})` : '';
137
+ process.stdout.write(
138
+ `[mem] 💡 Error→fix pattern detected${filesHint}. Consider: mem_save(type="bugfix", lesson_learned="root cause & fix")\n`,
139
+ );
140
+ }
141
+ } catch { /* never block on hint */ }
125
142
  } else {
126
143
  try { unlinkSync(flushFile); } catch {}
127
144
  }
@@ -538,7 +555,8 @@ async function handleSessionStart() {
538
555
  // Mark maintenance as done (24h gate) — even though compression runs in background
539
556
  writeFileSync(maintainFile, JSON.stringify({ epoch: Date.now() }));
540
557
  // Weekly summary grouping runs in background to avoid blocking SessionStart
541
- spawnBackground('auto-compress');
558
+ if (!process.env.CLAUDE_MEM_SKIP_COMPRESS) spawnBackground('auto-compress');
559
+ if (!process.env.CLAUDE_MEM_SKIP_OPTIMIZE) spawnBackground('llm-optimize');
542
560
  } catch (e) { debugCatch(e, 'auto-maintain'); }
543
561
  }
544
562
 
@@ -627,28 +645,6 @@ async function handleSessionStart() {
627
645
  }
628
646
  } catch {}
629
647
 
630
- // Token-budgeted observation selection (replaces flat LIMIT 15)
631
- const selected = selectWithTokenBudget(db, project, 2000);
632
- const observations = selected.observations;
633
-
634
- // Fallback: recent across all projects with tiered windows (M7: local variable for clarity)
635
- let fallbackObs = [];
636
- if (observations.length < 3) {
637
- const fbOneDayAgo = Date.now() - STALE_SESSION_MS;
638
- const fbSevenDaysAgo = Date.now() - FALLBACK_OBS_WINDOW_MS;
639
- fallbackObs = db.prepare(`
640
- SELECT id, type, title, project, created_at
641
- FROM observations
642
- WHERE COALESCE(compressed_into, 0) = 0
643
- AND (
644
- (created_at_epoch > ? AND importance >= 1)
645
- OR (created_at_epoch > ? AND importance >= 2)
646
- )
647
- ORDER BY created_at_epoch DESC
648
- LIMIT 5
649
- `).all(fbOneDayAgo, fbSevenDaysAgo);
650
- }
651
-
652
648
  // Fallback fast summary: if a recently completed session has no summary yet
653
649
  // (e.g. /exit → fast restart before Haiku finishes), build one synchronously.
654
650
  // Skipped when prevSessionId is set (already handled above).
@@ -690,114 +686,19 @@ async function handleSessionStart() {
690
686
  } catch (e) { debugCatch(e, 'session-start-exit-fast-summary'); }
691
687
  }
692
688
 
693
- // Latest session summary
694
- const latestSummary = db.prepare(`
695
- SELECT request, completed, next_steps, remaining_items, lessons, key_decisions, created_at
696
- FROM session_summaries
697
- WHERE project = ?
698
- ORDER BY created_at_epoch DESC
699
- LIMIT 1
700
- `).get(project);
701
-
702
- // Build summary lines (shared by stdout and CLAUDE.md)
703
- const summaryLines = buildSummaryLines(latestSummary);
704
-
705
- // Key context: top high-importance observations for CLAUDE.md persistence
706
- // Split into "File Lessons" (actionable, has lesson + file) and "Key Context" (informational)
707
- const keyObs = db.prepare(`
708
- SELECT o.id, o.type, o.title, o.lesson_learned, o.files_modified FROM observations o
709
- WHERE o.project = ? AND COALESCE(o.compressed_into, 0) = 0
710
- AND o.superseded_at IS NULL
711
- AND COALESCE(o.importance, 1) >= 2
712
- ORDER BY o.created_at_epoch DESC LIMIT 10
713
- `).all(project);
714
-
715
- if (keyObs.length > 0) {
716
- const fileLessons = [];
717
- const keyContext = [];
718
-
719
- for (const o of keyObs) {
720
- const clean = (o.title || '(untitled)')
721
- .replace(/ → (?:ERROR: )?\{".*$/, '')
722
- .replace(/ → (?:ERROR: )?\{[^}]*\.{3}$/, '');
723
- const hasLesson = o.lesson_learned && o.lesson_learned.trim();
724
- const hasFiles = o.files_modified && o.files_modified !== '[]';
725
-
726
- if (hasLesson && hasFiles) {
727
- try {
728
- const files = JSON.parse(o.files_modified);
729
- const fname = basename(Array.isArray(files) && files.length > 0 ? files[0] : '');
730
- if (fname) {
731
- fileLessons.push(`- ${fname}: ${truncate(o.lesson_learned, 100)} (#${o.id})`);
732
- continue;
733
- }
734
- } catch {}
735
- }
736
- const lesson = hasLesson ? ` — ${truncate(o.lesson_learned, 60)}` : '';
737
- keyContext.push(`- [${o.type || 'discovery'}] ${truncate(clean, 80)} (#${o.id})${lesson}`);
738
- }
739
-
740
- if (fileLessons.length > 0) {
741
- summaryLines.push('### File Lessons');
742
- summaryLines.push(...fileLessons.slice(0, 5));
743
- summaryLines.push('');
744
- }
745
- if (keyContext.length > 0) {
746
- summaryLines.push('### Key Context');
747
- summaryLines.push(...keyContext.slice(0, 5));
748
- summaryLines.push('');
749
- }
750
- } else if (!latestSummary) {
751
- // Fallback: no summary AND no key observations — show recent activity
752
- const recentObs = (observations.length >= 3 ? observations : fallbackObs).slice(0, 3);
753
- if (recentObs.length > 0) {
754
- summaryLines.push('### Recent Activity');
755
- for (const o of recentObs) {
756
- summaryLines.push(`- ${truncate(o.title || '(untitled)', 80)}`);
757
- }
758
- summaryLines.push('');
759
- }
760
- }
761
-
762
- // Working state from /clear handoff (persisted to both stdout and CLAUDE.md)
763
- const handoffLines = [];
764
- if (prevClearHandoff) {
765
- handoffLines.push('### Working State (from /clear)');
766
- if (prevClearHandoff.working_on) handoffLines.push(`- Working on: ${truncate(prevClearHandoff.working_on, 200)}`);
767
- if (prevClearHandoff.unfinished) {
768
- const pendingSummary = extractUnfinishedSummary(prevClearHandoff.unfinished);
769
- if (pendingSummary) handoffLines.push(`- Unfinished: ${truncate(pendingSummary, 200)}`);
770
- }
771
- if (prevClearHandoff.key_files) {
772
- try {
773
- const files = JSON.parse(prevClearHandoff.key_files);
774
- if (files.length > 0) handoffLines.push(`- Key files: ${files.map(f => basename(f)).join(', ')}`);
775
- } catch {}
776
- }
777
- handoffLines.push('');
778
- }
779
-
780
- // Build observations table (stdout only — not persisted to CLAUDE.md)
781
- const obsLines = [];
782
- const obsToShow = observations.length >= 3 ? observations : fallbackObs;
783
- if (obsToShow.length > 0) {
784
- const today = now.toISOString().slice(0, 10);
785
- obsLines.push(`### Recent (${today})`);
786
- obsLines.push('');
787
- obsLines.push('| ID | Time | T | Title |');
788
- obsLines.push('|----|------|---|-------|');
789
- for (const o of obsToShow) {
790
- const proj = o.project ? ` (${o.project})` : '';
791
- obsLines.push(`| #${o.id} | ${fmtTime(o.created_at)} | ${typeIcon(o.type)} | ${truncate(o.title || '(untitled)', 60)}${proj} |`);
792
- }
793
- }
689
+ // Build the full context body via shared helper (also used by `mem-cli context`).
690
+ // Queries session_summaries, key observations, clear handoff, and the
691
+ // token-budgeted observation pool directly from the DB.
692
+ const fullContext = buildSessionContextLines(db, project, now);
794
693
 
795
- // Stdout: full context (summary + handoff state + observations table)
796
- const fullContext = [...summaryLines, ...handoffLines, ...obsLines].join('\n');
694
+ // Stdout is the sole context-delivery channel. The SessionStart hook output
695
+ // is injected as a <system-reminder> at session start, giving Claude the
696
+ // full summary + handoff state + observations table fresh from the DB.
797
697
  process.stdout.write(`<claude-mem-context>\n${fullContext}\n</claude-mem-context>\n`);
798
698
 
799
- // CLAUDE.md: slim (summary + handoff state observations already in stdout)
800
- updateClaudeMd([...summaryLines, ...handoffLines].join('\n'));
699
+ // One-time migration: remove any stale <claude-mem-context> block left in
700
+ // CLAUDE.md by pre-v2.30 installs. Idempotent no-op afterwards.
701
+ cleanupClaudeMdLegacyBlock();
801
702
 
802
703
  // Pre-load TF-IDF vocabulary cache for this session (from DB, ~1ms)
803
704
  try { getVocabulary(db); } catch (e) { debugCatch(e, 'session-start-vocab'); }
@@ -1060,6 +961,7 @@ try {
1060
961
  case 'llm-episode': await handleLLMEpisode(); break;
1061
962
  case 'llm-summary': await handleLLMSummary(); break;
1062
963
  case 'auto-compress': handleAutoCompress(); break;
964
+ case 'llm-optimize': await handleLLMOptimize(); break;
1063
965
  }
1064
966
  } catch (err) {
1065
967
  // Always log fatal errors (ungated) with structured format
package/install.mjs CHANGED
@@ -204,7 +204,7 @@ async function install() {
204
204
  const SOURCE_FILES = [
205
205
  'cli.mjs', 'server.mjs', 'server-internals.mjs', 'tool-schemas.mjs',
206
206
  'hook.mjs', 'hook-shared.mjs', 'hook-llm.mjs', 'hook-memory.mjs', 'skip-tools.mjs',
207
- 'hook-semaphore.mjs', 'hook-episode.mjs', 'hook-context.mjs', 'hook-handoff.mjs', 'hook-update.mjs',
207
+ 'hook-semaphore.mjs', 'hook-episode.mjs', 'hook-context.mjs', 'hook-handoff.mjs', 'hook-update.mjs', 'hook-optimize.mjs',
208
208
  'haiku-client.mjs', 'utils.mjs', 'schema.mjs', 'package.json', 'package-lock.json', 'skill.md',
209
209
  'registry.mjs', 'registry-scanner.mjs', 'registry-indexer.mjs',
210
210
  'registry-retriever.mjs', 'resource-discovery.mjs',