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.
- package/CHANGELOG-v1.1.0.md +227 -0
- package/CHANGELOG.md +23 -0
- package/README.md +82 -0
- package/README_CN.md +82 -0
- package/index.ts +106 -11
- package/openclaw.plugin.json +69 -1
- package/package.json +1 -1
- package/src/access-tracker.ts +13 -3
- package/src/decay-engine.ts +227 -0
- package/src/extraction-prompts.ts +205 -0
- package/src/llm-client.ts +92 -0
- package/src/memory-categories.ts +69 -0
- package/src/retriever.ts +152 -4
- package/src/smart-extractor.ts +524 -0
- package/src/tier-manager.ts +189 -0
|
@@ -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
|
|
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 =
|
|
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(
|
|
1041
|
+
return new MemoryRetriever(
|
|
1042
|
+
store,
|
|
1043
|
+
embedder,
|
|
1044
|
+
fullConfig,
|
|
1045
|
+
lifecycle?.decayEngine,
|
|
1046
|
+
lifecycle?.tierManager,
|
|
1047
|
+
);
|
|
900
1048
|
}
|