memory-lancedb-pro 1.0.26 → 1.1.0-beta.2

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.
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Memory Categories — 6-category classification system
3
+ * Ported from epro-memory / OpenViking
4
+ *
5
+ * UserMemory: profile, preferences, entities, events
6
+ * AgentMemory: cases, patterns
7
+ */
8
+
9
+ export const MEMORY_CATEGORIES = [
10
+ "profile",
11
+ "preferences",
12
+ "entities",
13
+ "events",
14
+ "cases",
15
+ "patterns",
16
+ ] as const;
17
+
18
+ export type MemoryCategory = (typeof MEMORY_CATEGORIES)[number];
19
+
20
+ /** Categories that always merge (skip dedup entirely). */
21
+ export const ALWAYS_MERGE_CATEGORIES = new Set<MemoryCategory>(["profile"]);
22
+
23
+ /** Categories that support MERGE decision from LLM dedup. */
24
+ export const MERGE_SUPPORTED_CATEGORIES = new Set<MemoryCategory>([
25
+ "preferences",
26
+ "entities",
27
+ "patterns",
28
+ ]);
29
+
30
+ /** Categories that are append-only (CREATE or SKIP only, no MERGE). */
31
+ export const APPEND_ONLY_CATEGORIES = new Set<MemoryCategory>([
32
+ "events",
33
+ "cases",
34
+ ]);
35
+
36
+ /** Memory tier levels for lifecycle management. */
37
+ export type MemoryTier = "core" | "working" | "peripheral";
38
+
39
+ /** A candidate memory extracted from conversation by LLM. */
40
+ export type CandidateMemory = {
41
+ category: MemoryCategory;
42
+ abstract: string; // L0: one-sentence index
43
+ overview: string; // L1: structured markdown summary
44
+ content: string; // L2: full narrative
45
+ };
46
+
47
+ /** Dedup decision from LLM. */
48
+ export type DedupDecision = "create" | "merge" | "skip";
49
+
50
+ export type DedupResult = {
51
+ decision: DedupDecision;
52
+ reason: string;
53
+ matchId?: string; // ID of existing memory to merge with
54
+ };
55
+
56
+ export type ExtractionStats = {
57
+ created: number;
58
+ merged: number;
59
+ skipped: number;
60
+ };
61
+
62
+ /** Validate and normalize a category string. */
63
+ export function normalizeCategory(raw: string): MemoryCategory | null {
64
+ const lower = raw.toLowerCase().trim();
65
+ if ((MEMORY_CATEGORIES as readonly string[]).includes(lower)) {
66
+ return lower as MemoryCategory;
67
+ }
68
+ return null;
69
+ }
package/src/retriever.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * Combines vector search + BM25 full-text search with RRF fusion
4
4
  */
5
5
 
6
- import type { MemoryStore, MemorySearchResult } from "./store.js";
6
+ import type { MemoryEntry, MemoryStore, MemorySearchResult } from "./store.js";
7
7
  import type { Embedder } from "./embedder.js";
8
8
  import { filterNoise } from "./noise-filter.js";
9
9
  import {
@@ -12,6 +12,11 @@ import {
12
12
  computeEffectiveHalfLife,
13
13
  } from "./access-tracker.js";
14
14
 
15
+ // Smart lifecycle scoring (decay + tier)
16
+ import type { DecayEngine, DecayableMemory } from "./decay-engine.js";
17
+ import type { TierManager } from "./tier-manager.js";
18
+ import type { MemoryTier } from "./memory-categories.js";
19
+
15
20
  // ============================================================================
16
21
  // Types & Configuration
17
22
  // ============================================================================
@@ -127,6 +132,54 @@ function clamp01(value: number, fallback: number): number {
127
132
  return Math.min(1, Math.max(0, value));
128
133
  }
129
134
 
135
+ function parseJsonObject(metadata?: string): Record<string, unknown> {
136
+ if (!metadata || typeof metadata !== "string") return {};
137
+ try {
138
+ const obj = JSON.parse(metadata);
139
+ return (obj && typeof obj === "object") ? (obj as Record<string, unknown>) : {};
140
+ } catch {
141
+ return {};
142
+ }
143
+ }
144
+
145
+ function parseMemoryTier(raw: unknown, fallback: MemoryTier = "working"): MemoryTier {
146
+ const v = typeof raw === "string" ? raw.toLowerCase().trim() : "";
147
+ if (v === "core" || v === "working" || v === "peripheral") return v;
148
+ return fallback;
149
+ }
150
+
151
+ function parseNumber(raw: unknown, fallback: number): number {
152
+ const n = typeof raw === "number" ? raw : Number(raw);
153
+ return Number.isFinite(n) ? n : fallback;
154
+ }
155
+
156
+ function getDecayableFromEntry(entry: MemoryEntry): { memory: DecayableMemory; meta: Record<string, unknown> } {
157
+ const meta = parseJsonObject(entry.metadata);
158
+
159
+ // Support both snake_case and camelCase keys for interoperability.
160
+ const accessCount = parseNumber(meta.access_count ?? meta.accessCount, 0);
161
+ const createdAt = parseNumber(meta.created_at ?? meta.createdAt, entry.timestamp);
162
+ const lastAccessedAt = parseNumber(
163
+ meta.last_accessed_at ?? meta.lastAccessedAt,
164
+ createdAt,
165
+ );
166
+ const confidence = clamp01(parseNumber(meta.confidence, 0.7), 0.7);
167
+ const tier = parseMemoryTier(meta.tier, "working");
168
+
169
+ return {
170
+ memory: {
171
+ id: entry.id,
172
+ importance: clamp01(entry.importance, 0.5),
173
+ confidence,
174
+ tier,
175
+ accessCount: Math.max(0, Math.floor(accessCount)),
176
+ createdAt,
177
+ lastAccessedAt,
178
+ },
179
+ meta,
180
+ };
181
+ }
182
+
130
183
  // ============================================================================
131
184
  // Rerank Provider Adapters
132
185
  // ============================================================================
@@ -286,6 +339,8 @@ export class MemoryRetriever {
286
339
  private store: MemoryStore,
287
340
  private embedder: Embedder,
288
341
  private config: RetrievalConfig = DEFAULT_RETRIEVAL_CONFIG,
342
+ private decayEngine?: DecayEngine,
343
+ private tierManager?: TierManager,
289
344
  ) {}
290
345
 
291
346
  setAccessTracker(tracker: AccessTracker): void {
@@ -354,7 +409,8 @@ export class MemoryRetriever {
354
409
  const weighted = this.applyImportanceWeight(boosted);
355
410
  const lengthNormalized = this.applyLengthNormalization(weighted);
356
411
  const timeDecayed = this.applyTimeDecay(lengthNormalized);
357
- const hardFiltered = timeDecayed.filter(
412
+ const lifecycleBoosted = this.applyLifecycleBoost(timeDecayed);
413
+ const hardFiltered = lifecycleBoosted.filter(
358
414
  (r) => r.score >= this.config.hardMinScore,
359
415
  );
360
416
  const denoised = this.config.filterNoise
@@ -422,8 +478,11 @@ export class MemoryRetriever {
422
478
  // Apply time decay (penalize stale entries)
423
479
  const timeDecayed = this.applyTimeDecay(lengthNormalized);
424
480
 
481
+ // Apply lifecycle-aware decay/tier boost
482
+ const lifecycleBoosted = this.applyLifecycleBoost(timeDecayed);
483
+
425
484
  // Hard minimum score cutoff (post all scoring stages)
426
- const hardFiltered = timeDecayed.filter(
485
+ const hardFiltered = lifecycleBoosted.filter(
427
486
  (r) => r.score >= this.config.hardMinScore,
428
487
  );
429
488
 
@@ -801,6 +860,83 @@ export class MemoryRetriever {
801
860
  return decayed.sort((a, b) => b.score - a.score);
802
861
  }
803
862
 
863
+ /**
864
+ * Apply lifecycle-aware score adjustment (decay + tier floors).
865
+ *
866
+ * This is intentionally lightweight:
867
+ * - reads tier/access metadata (if any)
868
+ * - multiplies scores by max(tierFloor, decayComposite)
869
+ */
870
+ private applyLifecycleBoost(results: RetrievalResult[]): RetrievalResult[] {
871
+ if (!this.decayEngine) return results;
872
+
873
+ const now = Date.now();
874
+ const pairs = results.map(r => {
875
+ const { memory } = getDecayableFromEntry(r.entry);
876
+ return { r, memory };
877
+ });
878
+
879
+ const scored = pairs.map(p => ({ memory: p.memory, score: p.r.score }));
880
+ this.decayEngine.applySearchBoost(scored, now);
881
+
882
+ const boosted = pairs.map((p, i) => ({ ...p.r, score: scored[i].score }));
883
+ return boosted.sort((a, b) => b.score - a.score);
884
+ }
885
+
886
+ /**
887
+ * Record access stats (access_count, last_accessed_at) and apply tier
888
+ * promotion/demotion for a small number of top results.
889
+ *
890
+ * Note: this writes back to LanceDB via delete+readd; keep it bounded.
891
+ */
892
+ private async recordAccessAndMaybeTransition(results: RetrievalResult[]): Promise<void> {
893
+ if (!this.decayEngine && !this.tierManager) return;
894
+
895
+ const now = Date.now();
896
+ const toUpdate = results.slice(0, 3);
897
+
898
+ for (const r of toUpdate) {
899
+ const { memory, meta } = getDecayableFromEntry(r.entry);
900
+
901
+ // Update access stats in-memory first
902
+ const nextAccess = memory.accessCount + 1;
903
+ meta.access_count = nextAccess;
904
+ meta.last_accessed_at = now;
905
+ if (meta.created_at === undefined && meta.createdAt === undefined) {
906
+ meta.created_at = memory.createdAt;
907
+ }
908
+ if (meta.tier === undefined) {
909
+ meta.tier = memory.tier;
910
+ }
911
+ if (meta.confidence === undefined) {
912
+ meta.confidence = memory.confidence;
913
+ }
914
+
915
+ const updatedMemory: DecayableMemory = {
916
+ ...memory,
917
+ accessCount: nextAccess,
918
+ lastAccessedAt: now,
919
+ };
920
+
921
+ // Tier transition (optional)
922
+ if (this.decayEngine && this.tierManager) {
923
+ const ds = this.decayEngine.score(updatedMemory, now);
924
+ const transition = this.tierManager.evaluate(updatedMemory, ds, now);
925
+ if (transition) {
926
+ meta.tier = transition.toTier;
927
+ }
928
+ }
929
+
930
+ try {
931
+ await this.store.update(r.entry.id, {
932
+ metadata: JSON.stringify(meta),
933
+ });
934
+ } catch {
935
+ // best-effort: ignore
936
+ }
937
+ }
938
+ }
939
+
804
940
  /**
805
941
  * MMR-inspired diversity filter: greedily select results that are both
806
942
  * relevant (high score) and diverse (low similarity to already-selected).
@@ -890,11 +1026,23 @@ export class MemoryRetriever {
890
1026
  // Factory Function
891
1027
  // ============================================================================
892
1028
 
1029
+ export interface RetrieverLifecycleOptions {
1030
+ decayEngine?: DecayEngine;
1031
+ tierManager?: TierManager;
1032
+ }
1033
+
893
1034
  export function createRetriever(
894
1035
  store: MemoryStore,
895
1036
  embedder: Embedder,
896
1037
  config?: Partial<RetrievalConfig>,
1038
+ lifecycle?: RetrieverLifecycleOptions,
897
1039
  ): MemoryRetriever {
898
1040
  const fullConfig = { ...DEFAULT_RETRIEVAL_CONFIG, ...config };
899
- return new MemoryRetriever(store, embedder, fullConfig);
1041
+ return new MemoryRetriever(
1042
+ store,
1043
+ embedder,
1044
+ fullConfig,
1045
+ lifecycle?.decayEngine,
1046
+ lifecycle?.tierManager,
1047
+ );
900
1048
  }