claude-mem-lite 3.47.0 → 3.49.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/server.mjs CHANGED
@@ -12,19 +12,21 @@ import { reRankWithContext, autoBoostIfNeeded, runIdleCleanup, buildServerInstru
12
12
  import { searchObservationsHybrid } from './search-engine.mjs';
13
13
  import { deepSearch, resolveDeepMode, shouldEscalateToDeep, autoDeepLlmReady } from './deep-search.mjs';
14
14
  import { selectCompressionCandidates, groupByProjectWeek, compressGroup } from './lib/compress-core.mjs';
15
- import { buildNotLowSignalSql } from './lib/low-signal-patterns.mjs';
16
15
  import { resolveAnchorToken, formatAnchorError, resolveQueryAnchor, fetchRecentTimeline, fetchTimelineWindow } from './lib/timeline-core.mjs';
17
16
  import { buildSearchFtsQuery, parseDateBounds, parseDuration, coreRunSearchPipeline } from './lib/search-core.mjs';
18
17
  import {
19
18
  cleanupBroken, decayAndMarkIdle, boostAccessed, demotePinned, mergeDuplicates,
20
19
  recoverOrphanedChildren, recoverBuriedLessons, sweepDeferredWorkOrphans,
21
20
  purgeStale, purgeStalePreview, findDuplicates, maintenanceStats, rebuildVectors, vacuum,
22
- recoverChildrenOf, hardDeleteCandidateCount,
21
+ hardDeleteCandidateCount,
23
22
  OP_CAP, STALE_AGE_MS,
24
23
  } from './lib/maintain-core.mjs';
25
24
  import { snapshotDb } from './lib/db-backup.mjs';
25
+ import { deleteObservations } from './lib/delete-core.mjs';
26
26
  import { effectiveQuiet, RUNTIME_DIR } from './hook-shared.mjs';
27
27
  import { TIER_CASE_SQL, tierSqlParams } from './tier.mjs';
28
+ import { computeStatsFeed } from './lib/stats-core.mjs';
29
+ import { buildLessonNudge } from './lib/save-nudge.mjs';
28
30
  import { formatObsFieldValue } from './cli/common.mjs';
29
31
  import { memSearchSchema, memRecentSchema, memTimelineSchema, memGetSchema, memDeleteSchema, memSaveSchema, memStatsSchema, memCompressSchema, memMaintainSchema, memOptimizeSchema, memUpdateSchema, memExportSchema, memRecallSchema, memFtsCheckSchema, memRegistrySchema, memBrowseSchema, memUseSchema, memDeferSchema, memDeferListSchema, memDeferDropSchema, tools as TOOL_DEFS } from './tool-schemas.mjs';
30
32
 
@@ -45,7 +47,6 @@ import { probeOtherSources as probeIdSources, bucketIdTokens } from './lib/id-ro
45
47
  import { saveObservation } from './lib/save-observation.mjs';
46
48
  import { rebuildObservationDerived } from './lib/observation-write.mjs';
47
49
  import { EXPORT_COLUMNS_SQL } from './lib/export-columns.mjs';
48
- import { computeNoiseGauge } from './lib/stats-quality.mjs';
49
50
  import { recallByFile } from './lib/recall-core.mjs';
50
51
  import { AUTO_MERGE_THRESHOLD } from './lib/dedup-constants.mjs';
51
52
  import {
@@ -680,49 +681,14 @@ server.registerTool(
680
681
  return { content: [{ type: 'text', text: lines.join('\n') }] };
681
682
  }
682
683
 
683
- // Snapshot before the irreversible hard-delete so a wrong-id delete has a pre-image,
684
- // matching the CLI delete + maintain purge/cleanup paths (audit MED-2). Best-effort
685
- // (never throws, skips :memory:). Must run OUTSIDE the transaction below (VACUUM).
686
- snapshotDb(db, { tag: 'pre-delete' });
687
-
688
- // Wrap cleanup + deletion in a transaction for consistency
689
- const deletedIds = new Set(args.ids);
690
- const deleteTx = db.transaction(() => {
691
- // Clean up stale references in other observations' related_ids
692
- // Use LIKE filter to avoid O(N) full-table scan — only fetch rows that may reference deleted IDs.
693
- // NOTE: LIKE %id% has false positives (e.g. %1% matches [10], [21]). This is intentional —
694
- // the LIKE is a coarse pre-filter; the JSON parse + Set.has below is the precise filter.
695
- // Acceptable because observation count per user is typically <10K.
696
- const likeConditions = args.ids.map(() => `related_ids LIKE ?`).join(' OR ');
697
- const likeParams = args.ids.map(id => `%${id}%`);
698
- const referencing = db.prepare(`
699
- SELECT id, related_ids FROM observations
700
- WHERE related_ids IS NOT NULL AND related_ids != '[]'
701
- AND (${likeConditions})
702
- `).all(...likeParams);
703
- for (const r of referencing) {
704
- let ids;
705
- try { ids = JSON.parse(r.related_ids); } catch (e) { debugCatch(e, 'deleteRelatedIds'); continue; }
706
- if (!Array.isArray(ids) || !ids.every(id => Number.isInteger(id))) continue;
707
- const filtered = ids.filter(id => !deletedIds.has(id));
708
- if (filtered.length !== ids.length) {
709
- db.prepare('UPDATE observations SET related_ids = ? WHERE id = ?').run(JSON.stringify(filtered), r.id);
710
- }
711
- }
712
- // Resurface rows merged/compressed INTO the doomed keepers before deleting, else
713
- // they dangle behind a now-missing parent (compressed_into has no FK) — invisible
714
- // to every COALESCE(compressed_into,0)=0 view and unrecoverable. Mirrors the CLI
715
- // delete path + the maintain hard-delete guard (recoverChildrenOf).
716
- const recovered = recoverChildrenOf(db, args.ids);
717
- // Execute deletion (FTS5 cleanup handled by observations_ad trigger)
718
- const deleted = db.prepare(`DELETE FROM observations WHERE id IN (${placeholders})`).run(...args.ids);
719
- return { changes: deleted.changes, recovered };
720
- });
721
- const result = deleteTx();
684
+ // Full delete orchestration (snapshot + related_ids cleanup + child recovery +
685
+ // delete transaction) lives in lib/delete-core.mjs single source of truth shared
686
+ // with the CLI `delete` path (was inlined + kept in sync by parity comments).
687
+ const result = deleteObservations(db, args.ids);
722
688
 
723
689
  const missing = args.ids.filter(id => !rows.some(r => r.id === id));
724
- const msg = [`Deleted ${result.changes} observation(s).`];
725
- if (result.recovered > 0) msg.push(`Recovered ${result.recovered} merged/compressed child observation(s) to live.`);
690
+ const msg = [`Deleted ${result.deleted} observation(s).`];
691
+ if (result.recoveredChildren > 0) msg.push(`Recovered ${result.recoveredChildren} merged/compressed child observation(s) to live.`);
726
692
  if (missing.length > 0) msg.push(`Note: ID(s) ${missing.join(', ')} not found.`);
727
693
  return { content: [{ type: 'text', text: msg.join(' ') }] };
728
694
  })
@@ -784,7 +750,8 @@ server.registerTool(
784
750
  const supersededNote = result.supersededIds && result.supersededIds.length > 0
785
751
  ? ` Superseded: ${result.supersededIds.map(i => `#${i}`).join(', ')}.`
786
752
  : '';
787
- return { content: [{ type: 'text', text: `Saved as observation #${result.id} [${result.type}] in project "${project}".${lessonNote}${closedNote}${supersededNote}` }] };
753
+ const nudge = buildLessonNudge({ type: result.type, id: result.id, lessonCaptured: result.lessonCaptured, surface: 'mcp' });
754
+ return { content: [{ type: 'text', text: `Saved as observation #${result.id} [${result.type}] in project "${project}".${lessonNote}${closedNote}${supersededNote}${nudge}` }] };
788
755
  })
789
756
  );
790
757
 
@@ -882,97 +849,11 @@ server.registerTool(
882
849
  return { content: [{ type: 'text', text: formatQualityReport(data) }] };
883
850
  }
884
851
 
885
- const cutoff = Date.now() - days * 86400000;
886
- const projectFilter = args.project ? 'AND project = ?' : '';
887
- const baseParams = args.project ? [args.project] : [];
888
-
889
- // Total counts
890
- const obsTotal = db.prepare(`SELECT COUNT(*) as c FROM observations WHERE 1=1 ${projectFilter}`).get(...baseParams);
891
- const sessTotal = db.prepare(`SELECT COUNT(*) as c FROM session_summaries WHERE 1=1 ${projectFilter}`).get(...baseParams);
892
- const promptTotal = args.project
893
- ? db.prepare(`SELECT COUNT(*) as c FROM user_prompts p JOIN sdk_sessions s ON p.content_session_id = s.content_session_id WHERE s.project = ?`).get(args.project)
894
- : db.prepare(`SELECT COUNT(*) as c FROM user_prompts`).get();
895
-
896
- // Recent counts
897
- const obsRecent = db.prepare(`SELECT COUNT(*) as c FROM observations WHERE created_at_epoch >= ? ${projectFilter}`).get(cutoff, ...baseParams);
898
- const sessRecent = db.prepare(`SELECT COUNT(*) as c FROM session_summaries WHERE created_at_epoch >= ? ${projectFilter}`).get(cutoff, ...baseParams);
899
-
900
- // Type distribution (recent)
901
- const types = db.prepare(`
902
- SELECT type, COUNT(*) as c FROM observations
903
- WHERE created_at_epoch >= ? ${projectFilter}
904
- GROUP BY type ORDER BY c DESC
905
- `).all(cutoff, ...baseParams);
906
-
907
- // Projects (global view — skipped when filtering by single project)
908
- const projects = args.project ? [] : db.prepare(`
909
- SELECT project, COUNT(*) as c FROM observations
910
- GROUP BY project ORDER BY c DESC
911
- LIMIT 20
912
- `).all();
913
-
914
- // Daily activity (last 7 days)
915
- const daily = db.prepare(`
916
- SELECT date(created_at) as day, COUNT(*) as c FROM observations
917
- WHERE created_at_epoch >= ? ${projectFilter}
918
- GROUP BY day ORDER BY day DESC
919
- LIMIT 7
920
- `).all(Date.now() - 7 * 86400000, ...baseParams);
921
-
922
- // Health metrics
923
- const tokenEst = db.prepare(`
924
- SELECT SUM(LENGTH(COALESCE(title,'')) + LENGTH(COALESCE(narrative,'')) + LENGTH(COALESCE(text,''))) / 4 as t
925
- FROM observations WHERE 1=1 ${projectFilter}
926
- `).get(...baseParams);
927
-
928
- const avgImp = db.prepare(`
929
- SELECT AVG(COALESCE(importance,1)) as v FROM observations WHERE 1=1 ${projectFilter}
930
- `).get(...baseParams);
931
-
932
- const thirtyDaysAgo = Date.now() - 30 * 86400000;
933
- // v3.23 noise-gauge de-blinding (mirrors mem-cli cmdStats): `<= 1` makes the
934
- // imp=0 dormant population visible (decay floor + LLM low-signal filter push
935
- // ~half the live corpus to 0); injection_count=0 mirrors decay's NEVER-INJECTED
936
- // guard so injected-but-decayed pinned noise isn't miscounted as "never used".
937
- const lowVal = db.prepare(`
938
- SELECT COUNT(*) as c FROM observations
939
- WHERE COALESCE(importance,1) <= 1 AND COALESCE(access_count,0) = 0
940
- AND COALESCE(injection_count,0) = 0
941
- AND COALESCE(compressed_into, 0) = 0
942
- AND created_at_epoch < ? ${projectFilter}
943
- `).get(thirtyDaysAgo, ...baseParams);
944
-
945
- // Low-signal-title population (template / tool-log titles the read-side filter
946
- // already excludes). The imp=1 "Low-value" metric can't see these, so the
947
- // gauge under-reports real noise without it. See lib/low-signal-patterns.mjs.
948
- const lowSignalTitle = db.prepare(`
949
- SELECT COUNT(*) as c FROM observations
950
- WHERE NOT ${buildNotLowSignalSql()}
951
- AND COALESCE(compressed_into, 0) = 0 ${projectFilter}
952
- `).get(...baseParams);
953
- // F7: both noise numerators exclude compressed rows → divide by the LIVE count, not
954
- // obsTotal (all rows), so a compress-heavy store isn't reported cleaner than it is.
955
- const liveTotal = db.prepare(
956
- `SELECT COUNT(*) as c FROM observations WHERE COALESCE(compressed_into, 0) = 0 ${projectFilter}`
957
- ).get(...baseParams);
958
- const { noiseRatio, lowSignalRatio } = computeNoiseGauge({ liveTotal: liveTotal.c, lowValCount: lowVal.c, lowSignalCount: lowSignalTitle.c });
959
- const compressedCount = db.prepare(`
960
- SELECT COUNT(*) as c FROM observations WHERE compressed_into IS NOT NULL ${projectFilter}
961
- `).get(...baseParams);
962
- const supersededOnlyCount = db.prepare(`
963
- SELECT COUNT(*) as c FROM observations WHERE superseded_at IS NOT NULL AND compressed_into IS NULL ${projectFilter}
964
- `).get(...baseParams);
965
-
966
- // Tier distribution
967
- const tierCtx = { now: Date.now(), currentProject: args.project || inferProject(), currentSessionId: '' };
968
- const tdParams = tierSqlParams(tierCtx);
969
- const tierDist = db.prepare(`
970
- SELECT tier, COUNT(*) as c FROM (
971
- SELECT ${TIER_CASE_SQL} as tier FROM observations
972
- WHERE COALESCE(compressed_into, 0) = 0 AND superseded_at IS NULL ${projectFilter}
973
- ) GROUP BY tier ORDER BY tier
974
- `).all(...tdParams, ...baseParams);
975
- const tierMap = Object.fromEntries(tierDist.map(r => [r.tier, r.c]));
852
+ const {
853
+ obsTotal, sessTotal, promptTotal, obsRecent, sessRecent,
854
+ types, projects, daily, tokenEst, avgImp, lowVal, lowSignalTitle,
855
+ noiseRatio, lowSignalRatio, compressedCount, supersededOnlyCount, tierMap,
856
+ } = computeStatsFeed(db, { project: args.project || null, days });
976
857
 
977
858
  const lines = [
978
859
  `Memory Statistics${args.project ? ` (project: ${args.project})` : ''}:`,
package/source-files.mjs CHANGED
@@ -152,6 +152,29 @@ export const SOURCE_FILES = [
152
152
  // server.mjs, and hook.mjs before their destructive purge/cleanup — missing it
153
153
  // would crash maintain on auto-update with an unresolved import.
154
154
  'lib/db-backup.mjs',
155
+ // HIGH-1 events-injection: surfaces the `events` canonical store into the passive
156
+ // injection surfaces. Statically imported by hook.mjs (UserPromptSubmit) and
157
+ // hook-context.mjs (SessionStart) — missing it from the manifest would break both
158
+ // hooks on auto-update.
159
+ 'lib/events-injection.mjs',
160
+ // Shared delete orchestration (snapshot + related_ids cleanup + child recovery
161
+ // + delete txn). Statically imported by server.mjs (mem_delete) and mem-cli.mjs
162
+ // (cmdDelete) — extracted to kill the byte-duplicated twin. Missing it from the
163
+ // manifest would leave both delete surfaces unsigned/broken on auto-update.
164
+ 'lib/delete-core.mjs',
165
+ // Shared primary stats feed (audit 2026-07-17 MED-4) — statically imported by
166
+ // server.mjs (mem_stats) and mem-cli.mjs (cmdStats); killed the ~80-line
167
+ // byte-duplicated twin. Missing it breaks both stats surfaces on auto-update.
168
+ 'lib/stats-core.mjs',
169
+ // Observation `type` vocabulary single source (audit 2026-07-17 MED-3) —
170
+ // statically imported by tool-schemas.mjs, mem-cli.mjs, hook-llm.mjs,
171
+ // hook-optimize.mjs, lib/activity.mjs. Missing it breaks every save/validate
172
+ // path on auto-update.
173
+ 'lib/obs-types.mjs',
174
+ // Save-time lesson nudge (audit 2026-07-17 P4) — statically imported by server.mjs
175
+ // (mem_save) and mem-cli.mjs (cmdSave). Missing it breaks both save surfaces on
176
+ // auto-update.
177
+ 'lib/save-nudge.mjs',
155
178
  // P10 dedup/merge threshold constants — single source of truth for the Jaccard
156
179
  // dedup/merge cutoffs. Statically imported by hook.mjs, hook-llm.mjs,
157
180
  // hook-optimize.mjs, mem-cli.mjs, server.mjs, and the save/maintain cores;
package/tool-schemas.mjs CHANGED
@@ -3,8 +3,9 @@
3
3
 
4
4
  import { z } from 'zod';
5
5
  import { CLI_INVOKE } from './cli-path.mjs';
6
+ import { OBS_TYPES } from './lib/obs-types.mjs';
6
7
 
7
- export const OBS_TYPE_ENUM = z.enum(['decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change']);
8
+ export const OBS_TYPE_ENUM = z.enum([...OBS_TYPES]);
8
9
 
9
10
  // LLM-friendly coercion: accept string numbers and normalize to proper types
10
11
  const coerceInt = z.preprocess(