@mnemoai/core 1.1.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.
Files changed (49) hide show
  1. package/index.ts +3395 -0
  2. package/openclaw.plugin.json +815 -0
  3. package/package.json +59 -0
  4. package/src/access-tracker.ts +341 -0
  5. package/src/adapters/README.md +78 -0
  6. package/src/adapters/chroma.ts +206 -0
  7. package/src/adapters/lancedb.ts +237 -0
  8. package/src/adapters/pgvector.ts +218 -0
  9. package/src/adapters/qdrant.ts +191 -0
  10. package/src/adaptive-retrieval.ts +90 -0
  11. package/src/audit-log.ts +238 -0
  12. package/src/chunker.ts +254 -0
  13. package/src/config.ts +271 -0
  14. package/src/decay-engine.ts +238 -0
  15. package/src/embedder.ts +735 -0
  16. package/src/extraction-prompts.ts +339 -0
  17. package/src/license.ts +258 -0
  18. package/src/llm-client.ts +125 -0
  19. package/src/mcp-server.ts +415 -0
  20. package/src/memory-categories.ts +71 -0
  21. package/src/memory-upgrader.ts +388 -0
  22. package/src/migrate.ts +364 -0
  23. package/src/mnemo.ts +142 -0
  24. package/src/noise-filter.ts +97 -0
  25. package/src/noise-prototypes.ts +164 -0
  26. package/src/observability.ts +81 -0
  27. package/src/query-tracker.ts +57 -0
  28. package/src/reflection-event-store.ts +98 -0
  29. package/src/reflection-item-store.ts +112 -0
  30. package/src/reflection-mapped-metadata.ts +84 -0
  31. package/src/reflection-metadata.ts +23 -0
  32. package/src/reflection-ranking.ts +33 -0
  33. package/src/reflection-retry.ts +181 -0
  34. package/src/reflection-slices.ts +265 -0
  35. package/src/reflection-store.ts +602 -0
  36. package/src/resonance-state.ts +85 -0
  37. package/src/retriever.ts +1510 -0
  38. package/src/scopes.ts +375 -0
  39. package/src/self-improvement-files.ts +143 -0
  40. package/src/semantic-gate.ts +121 -0
  41. package/src/session-recovery.ts +138 -0
  42. package/src/smart-extractor.ts +923 -0
  43. package/src/smart-metadata.ts +561 -0
  44. package/src/storage-adapter.ts +153 -0
  45. package/src/store.ts +1330 -0
  46. package/src/tier-manager.ts +189 -0
  47. package/src/tools.ts +1292 -0
  48. package/src/wal-recovery.ts +172 -0
  49. package/test/core.test.mjs +301 -0
@@ -0,0 +1,602 @@
1
+ // SPDX-License-Identifier: LicenseRef-Mnemo-Pro
2
+ import type { MemoryEntry, MemorySearchResult } from "./store.js";
3
+ import {
4
+ extractReflectionSliceItems,
5
+ extractReflectionSlices,
6
+ sanitizeReflectionSliceLines,
7
+ type ReflectionSlices,
8
+ } from "./reflection-slices.js";
9
+ import { parseReflectionMetadata } from "./reflection-metadata.js";
10
+ import { buildReflectionEventPayload, createReflectionEventId } from "./reflection-event-store.js";
11
+ import {
12
+ buildReflectionItemPayloads,
13
+ getReflectionItemDecayDefaults,
14
+ REFLECTION_DERIVED_DECAY_K,
15
+ REFLECTION_DERIVED_DECAY_MIDPOINT_DAYS,
16
+ REFLECTION_INVARIANT_DECAY_K,
17
+ REFLECTION_INVARIANT_DECAY_MIDPOINT_DAYS,
18
+ } from "./reflection-item-store.js";
19
+ import { getReflectionMappedDecayDefaults, type ReflectionMappedKind } from "./reflection-mapped-metadata.js";
20
+ import { computeReflectionScore, normalizeReflectionLineForAggregation } from "./reflection-ranking.js";
21
+
22
+ export const REFLECTION_DERIVE_LOGISTIC_MIDPOINT_DAYS = 3;
23
+ export const REFLECTION_DERIVE_LOGISTIC_K = 1.2;
24
+ export const REFLECTION_DERIVE_FALLBACK_BASE_WEIGHT = 0.35;
25
+
26
+ export const DEFAULT_REFLECTION_DERIVED_MAX_AGE_MS = 14 * 24 * 60 * 60 * 1000;
27
+ export const DEFAULT_REFLECTION_MAPPED_MAX_AGE_MS = 60 * 24 * 60 * 60 * 1000;
28
+
29
+ type ReflectionStoreKind = "event" | "item-invariant" | "item-derived" | "combined-legacy";
30
+
31
+ type ReflectionErrorSignalLike = {
32
+ signatureHash: string;
33
+ };
34
+
35
+ interface ReflectionStorePayload {
36
+ text: string;
37
+ metadata: Record<string, unknown>;
38
+ kind: ReflectionStoreKind;
39
+ }
40
+
41
+ interface BuildReflectionStorePayloadsParams {
42
+ reflectionText: string;
43
+ sessionKey: string;
44
+ sessionId: string;
45
+ agentId: string;
46
+ command: string;
47
+ scope: string;
48
+ toolErrorSignals: ReflectionErrorSignalLike[];
49
+ runAt: number;
50
+ usedFallback: boolean;
51
+ eventId?: string;
52
+ sourceReflectionPath?: string;
53
+ writeLegacyCombined?: boolean;
54
+ }
55
+
56
+ export function buildReflectionStorePayloads(params: BuildReflectionStorePayloadsParams): {
57
+ eventId: string;
58
+ slices: ReflectionSlices;
59
+ payloads: ReflectionStorePayload[];
60
+ } {
61
+ const slices = extractReflectionSlices(params.reflectionText);
62
+ const eventId = params.eventId || createReflectionEventId({
63
+ runAt: params.runAt,
64
+ sessionKey: params.sessionKey,
65
+ sessionId: params.sessionId,
66
+ agentId: params.agentId,
67
+ command: params.command,
68
+ });
69
+
70
+ const payloads: ReflectionStorePayload[] = [
71
+ buildReflectionEventPayload({
72
+ eventId,
73
+ scope: params.scope,
74
+ sessionKey: params.sessionKey,
75
+ sessionId: params.sessionId,
76
+ agentId: params.agentId,
77
+ command: params.command,
78
+ toolErrorSignals: params.toolErrorSignals,
79
+ runAt: params.runAt,
80
+ usedFallback: params.usedFallback,
81
+ sourceReflectionPath: params.sourceReflectionPath,
82
+ }),
83
+ ];
84
+
85
+ const itemPayloads = buildReflectionItemPayloads({
86
+ items: extractReflectionSliceItems(params.reflectionText),
87
+ eventId,
88
+ agentId: params.agentId,
89
+ sessionKey: params.sessionKey,
90
+ sessionId: params.sessionId,
91
+ runAt: params.runAt,
92
+ usedFallback: params.usedFallback,
93
+ toolErrorSignals: params.toolErrorSignals,
94
+ sourceReflectionPath: params.sourceReflectionPath,
95
+ });
96
+ payloads.push(...itemPayloads);
97
+
98
+ if (params.writeLegacyCombined !== false && (slices.invariants.length > 0 || slices.derived.length > 0)) {
99
+ payloads.push(buildLegacyCombinedPayload({
100
+ slices,
101
+ scope: params.scope,
102
+ sessionKey: params.sessionKey,
103
+ sessionId: params.sessionId,
104
+ agentId: params.agentId,
105
+ command: params.command,
106
+ toolErrorSignals: params.toolErrorSignals,
107
+ runAt: params.runAt,
108
+ usedFallback: params.usedFallback,
109
+ sourceReflectionPath: params.sourceReflectionPath,
110
+ }));
111
+ }
112
+
113
+ return { eventId, slices, payloads };
114
+ }
115
+
116
+ function buildLegacyCombinedPayload(params: {
117
+ slices: ReflectionSlices;
118
+ sessionKey: string;
119
+ sessionId: string;
120
+ agentId: string;
121
+ command: string;
122
+ scope: string;
123
+ toolErrorSignals: ReflectionErrorSignalLike[];
124
+ runAt: number;
125
+ usedFallback: boolean;
126
+ sourceReflectionPath?: string;
127
+ }): ReflectionStorePayload {
128
+ const dateYmd = new Date(params.runAt).toISOString().split("T")[0];
129
+ const deriveQuality = computeDerivedLineQuality(params.slices.derived.length);
130
+ const deriveBaseWeight = params.usedFallback ? REFLECTION_DERIVE_FALLBACK_BASE_WEIGHT : 1;
131
+
132
+ return {
133
+ kind: "combined-legacy",
134
+ text: [
135
+ `reflection · ${params.scope} · ${dateYmd}`,
136
+ `Session Reflection (${new Date(params.runAt).toISOString()})`,
137
+ `Session Key: ${params.sessionKey}`,
138
+ `Session ID: ${params.sessionId}`,
139
+ "",
140
+ "Invariants:",
141
+ ...(params.slices.invariants.length > 0 ? params.slices.invariants.map((x) => `- ${x}`) : ["- (none captured)"]),
142
+ "",
143
+ "Derived:",
144
+ ...(params.slices.derived.length > 0 ? params.slices.derived.map((x) => `- ${x}`) : ["- (none captured)"]),
145
+ ].join("\n"),
146
+ metadata: {
147
+ type: "memory-reflection",
148
+ stage: "reflect-store",
149
+ reflectionVersion: 3,
150
+ sessionKey: params.sessionKey,
151
+ sessionId: params.sessionId,
152
+ agentId: params.agentId,
153
+ command: params.command,
154
+ storedAt: params.runAt,
155
+ invariants: params.slices.invariants,
156
+ derived: params.slices.derived,
157
+ usedFallback: params.usedFallback,
158
+ errorSignals: params.toolErrorSignals.map((s) => s.signatureHash),
159
+ decayModel: "logistic",
160
+ decayMidpointDays: REFLECTION_DERIVE_LOGISTIC_MIDPOINT_DAYS,
161
+ decayK: REFLECTION_DERIVE_LOGISTIC_K,
162
+ deriveBaseWeight,
163
+ deriveQuality,
164
+ deriveSource: params.usedFallback ? "fallback" : "normal",
165
+ ...(params.sourceReflectionPath ? { sourceReflectionPath: params.sourceReflectionPath } : {}),
166
+ },
167
+ };
168
+ }
169
+
170
+ interface ReflectionStoreDeps {
171
+ embedPassage: (text: string) => Promise<number[]>;
172
+ vectorSearch: (
173
+ vector: number[],
174
+ limit?: number,
175
+ minScore?: number,
176
+ scopeFilter?: string[]
177
+ ) => Promise<MemorySearchResult[]>;
178
+ store: (entry: Omit<MemoryEntry, "id" | "timestamp">) => Promise<MemoryEntry>;
179
+ }
180
+
181
+ interface StoreReflectionToLanceDBParams extends BuildReflectionStorePayloadsParams, ReflectionStoreDeps {
182
+ dedupeThreshold?: number;
183
+ }
184
+
185
+ export async function storeReflectionToLanceDB(params: StoreReflectionToLanceDBParams): Promise<{
186
+ stored: boolean;
187
+ eventId: string;
188
+ slices: ReflectionSlices;
189
+ storedKinds: ReflectionStoreKind[];
190
+ }> {
191
+ const { eventId, slices, payloads } = buildReflectionStorePayloads(params);
192
+ const storedKinds: ReflectionStoreKind[] = [];
193
+ const dedupeThreshold = Number.isFinite(params.dedupeThreshold) ? Number(params.dedupeThreshold) : 0.97;
194
+
195
+ for (const payload of payloads) {
196
+ const vector = await params.embedPassage(payload.text);
197
+
198
+ if (payload.kind === "combined-legacy") {
199
+ const existing = await params.vectorSearch(vector, 1, 0.1, [params.scope]);
200
+ if (existing.length > 0 && existing[0].score > dedupeThreshold) {
201
+ continue;
202
+ }
203
+ }
204
+
205
+ await params.store({
206
+ text: payload.text,
207
+ vector,
208
+ category: "reflection",
209
+ scope: params.scope,
210
+ importance: resolveReflectionImportance(payload.kind),
211
+ metadata: JSON.stringify(payload.metadata),
212
+ });
213
+ storedKinds.push(payload.kind);
214
+ }
215
+
216
+ return { stored: storedKinds.length > 0, eventId, slices, storedKinds };
217
+ }
218
+
219
+ function resolveReflectionImportance(kind: ReflectionStoreKind): number {
220
+ if (kind === "event") return 0.55;
221
+ if (kind === "item-invariant") return 0.82;
222
+ if (kind === "item-derived") return 0.78;
223
+ return 0.75;
224
+ }
225
+
226
+ export interface LoadReflectionSlicesParams {
227
+ entries: MemoryEntry[];
228
+ agentId: string;
229
+ now?: number;
230
+ deriveMaxAgeMs?: number;
231
+ invariantMaxAgeMs?: number;
232
+ }
233
+
234
+ export function loadAgentReflectionSlicesFromEntries(params: LoadReflectionSlicesParams): {
235
+ invariants: string[];
236
+ derived: string[];
237
+ } {
238
+ const now = Number.isFinite(params.now) ? Number(params.now) : Date.now();
239
+ const deriveMaxAgeMs = Number.isFinite(params.deriveMaxAgeMs)
240
+ ? Math.max(0, Number(params.deriveMaxAgeMs))
241
+ : DEFAULT_REFLECTION_DERIVED_MAX_AGE_MS;
242
+ const invariantMaxAgeMs = Number.isFinite(params.invariantMaxAgeMs)
243
+ ? Math.max(0, Number(params.invariantMaxAgeMs))
244
+ : undefined;
245
+
246
+ const reflectionRows = params.entries
247
+ .map((entry) => ({ entry, metadata: parseReflectionMetadata(entry.metadata) }))
248
+ .filter(({ metadata }) => isReflectionMetadataType(metadata.type) && isOwnedByAgent(metadata, params.agentId))
249
+ .sort((a, b) => b.entry.timestamp - a.entry.timestamp)
250
+ .slice(0, 160);
251
+
252
+ const itemRows = reflectionRows.filter(({ metadata }) => metadata.type === "memory-reflection-item");
253
+ const legacyRows = reflectionRows.filter(({ metadata }) => metadata.type === "memory-reflection");
254
+
255
+ const invariantCandidates = buildInvariantCandidates(itemRows, legacyRows);
256
+ const derivedCandidates = buildDerivedCandidates(itemRows, legacyRows);
257
+
258
+ const invariants = rankReflectionLines(invariantCandidates, {
259
+ now,
260
+ maxAgeMs: invariantMaxAgeMs,
261
+ limit: 8,
262
+ });
263
+
264
+ const derived = rankReflectionLines(derivedCandidates, {
265
+ now,
266
+ maxAgeMs: deriveMaxAgeMs,
267
+ limit: 10,
268
+ });
269
+
270
+ return { invariants, derived };
271
+ }
272
+
273
+ type WeightedLineCandidate = {
274
+ line: string;
275
+ timestamp: number;
276
+ midpointDays: number;
277
+ k: number;
278
+ baseWeight: number;
279
+ quality: number;
280
+ usedFallback: boolean;
281
+ };
282
+
283
+ function buildInvariantCandidates(
284
+ itemRows: Array<{ entry: MemoryEntry; metadata: Record<string, unknown> }>,
285
+ legacyRows: Array<{ entry: MemoryEntry; metadata: Record<string, unknown> }>
286
+ ): WeightedLineCandidate[] {
287
+ const itemCandidates = itemRows
288
+ .filter(({ metadata }) => metadata.itemKind === "invariant")
289
+ .flatMap(({ entry, metadata }) => {
290
+ const lines = sanitizeReflectionSliceLines([entry.text]);
291
+ if (lines.length === 0) return [];
292
+
293
+ const defaults = getReflectionItemDecayDefaults("invariant");
294
+ const timestamp = metadataTimestamp(metadata, entry.timestamp);
295
+ return lines.map((line) => ({
296
+ line,
297
+ timestamp,
298
+ midpointDays: readPositiveNumber(metadata.decayMidpointDays, defaults.midpointDays),
299
+ k: readPositiveNumber(metadata.decayK, defaults.k),
300
+ baseWeight: readPositiveNumber(metadata.baseWeight, defaults.baseWeight),
301
+ quality: readClampedNumber(metadata.quality, defaults.quality, 0.2, 1),
302
+ usedFallback: metadata.usedFallback === true,
303
+ }));
304
+ });
305
+
306
+ if (itemCandidates.length > 0) return itemCandidates;
307
+
308
+ return legacyRows.flatMap(({ entry, metadata }) => {
309
+ const defaults = getReflectionItemDecayDefaults("invariant");
310
+ const timestamp = metadataTimestamp(metadata, entry.timestamp);
311
+ const lines = sanitizeReflectionSliceLines(toStringArray(metadata.invariants));
312
+ return lines.map((line) => ({
313
+ line,
314
+ timestamp,
315
+ midpointDays: defaults.midpointDays,
316
+ k: defaults.k,
317
+ baseWeight: defaults.baseWeight,
318
+ quality: defaults.quality,
319
+ usedFallback: metadata.usedFallback === true,
320
+ }));
321
+ });
322
+ }
323
+
324
+ function buildDerivedCandidates(
325
+ itemRows: Array<{ entry: MemoryEntry; metadata: Record<string, unknown> }>,
326
+ legacyRows: Array<{ entry: MemoryEntry; metadata: Record<string, unknown> }>
327
+ ): WeightedLineCandidate[] {
328
+ const itemCandidates = itemRows
329
+ .filter(({ metadata }) => metadata.itemKind === "derived")
330
+ .flatMap(({ entry, metadata }) => {
331
+ const lines = sanitizeReflectionSliceLines([entry.text]);
332
+ if (lines.length === 0) return [];
333
+
334
+ const defaults = getReflectionItemDecayDefaults("derived");
335
+ const timestamp = metadataTimestamp(metadata, entry.timestamp);
336
+ return lines.map((line) => ({
337
+ line,
338
+ timestamp,
339
+ midpointDays: readPositiveNumber(metadata.decayMidpointDays, defaults.midpointDays),
340
+ k: readPositiveNumber(metadata.decayK, defaults.k),
341
+ baseWeight: readPositiveNumber(metadata.baseWeight, defaults.baseWeight),
342
+ quality: readClampedNumber(metadata.quality, defaults.quality, 0.2, 1),
343
+ usedFallback: metadata.usedFallback === true,
344
+ }));
345
+ });
346
+
347
+ if (itemCandidates.length > 0) return itemCandidates;
348
+
349
+ return legacyRows.flatMap(({ entry, metadata }) => {
350
+ const timestamp = metadataTimestamp(metadata, entry.timestamp);
351
+ const lines = sanitizeReflectionSliceLines(toStringArray(metadata.derived));
352
+ if (lines.length === 0) return [];
353
+
354
+ const defaults = {
355
+ midpointDays: REFLECTION_DERIVE_LOGISTIC_MIDPOINT_DAYS,
356
+ k: REFLECTION_DERIVE_LOGISTIC_K,
357
+ baseWeight: resolveLegacyDeriveBaseWeight(metadata),
358
+ quality: computeDerivedLineQuality(lines.length),
359
+ };
360
+
361
+ return lines.map((line) => ({
362
+ line,
363
+ timestamp,
364
+ midpointDays: readPositiveNumber(metadata.decayMidpointDays, defaults.midpointDays),
365
+ k: readPositiveNumber(metadata.decayK, defaults.k),
366
+ baseWeight: readPositiveNumber(metadata.deriveBaseWeight, defaults.baseWeight),
367
+ quality: readClampedNumber(metadata.deriveQuality, defaults.quality, 0.2, 1),
368
+ usedFallback: metadata.usedFallback === true,
369
+ }));
370
+ });
371
+ }
372
+
373
+ function rankReflectionLines(
374
+ candidates: WeightedLineCandidate[],
375
+ options: { now: number; maxAgeMs?: number; limit: number }
376
+ ): string[] {
377
+ type WeightedLine = { line: string; score: number; latestTs: number };
378
+ const lineScores = new Map<string, WeightedLine>();
379
+
380
+ for (const candidate of candidates) {
381
+ const timestamp = Number.isFinite(candidate.timestamp) ? candidate.timestamp : options.now;
382
+ if (Number.isFinite(options.maxAgeMs) && options.maxAgeMs! >= 0 && options.now - timestamp > options.maxAgeMs!) {
383
+ continue;
384
+ }
385
+
386
+ const ageDays = Math.max(0, (options.now - timestamp) / 86_400_000);
387
+ const score = computeReflectionScore({
388
+ ageDays,
389
+ midpointDays: candidate.midpointDays,
390
+ k: candidate.k,
391
+ baseWeight: candidate.baseWeight,
392
+ quality: candidate.quality,
393
+ usedFallback: candidate.usedFallback,
394
+ });
395
+ if (!Number.isFinite(score) || score <= 0) continue;
396
+
397
+ const key = normalizeReflectionLineForAggregation(candidate.line);
398
+ if (!key) continue;
399
+
400
+ const current = lineScores.get(key);
401
+ if (!current) {
402
+ lineScores.set(key, { line: candidate.line, score, latestTs: timestamp });
403
+ continue;
404
+ }
405
+
406
+ current.score += score;
407
+ if (timestamp > current.latestTs) {
408
+ current.latestTs = timestamp;
409
+ current.line = candidate.line;
410
+ }
411
+ }
412
+
413
+ return [...lineScores.values()]
414
+ .sort((a, b) => {
415
+ if (b.score !== a.score) return b.score - a.score;
416
+ if (b.latestTs !== a.latestTs) return b.latestTs - a.latestTs;
417
+ return a.line.localeCompare(b.line);
418
+ })
419
+ .slice(0, options.limit)
420
+ .map((item) => item.line);
421
+ }
422
+
423
+ function isReflectionMetadataType(type: unknown): boolean {
424
+ return type === "memory-reflection-item" || type === "memory-reflection";
425
+ }
426
+
427
+ function isOwnedByAgent(metadata: Record<string, unknown>, agentId: string): boolean {
428
+ const owner = typeof metadata.agentId === "string" ? metadata.agentId.trim() : "";
429
+ if (!owner) return true;
430
+ return owner === agentId || owner === "main";
431
+ }
432
+
433
+ function toStringArray(value: unknown): string[] {
434
+ if (!Array.isArray(value)) return [];
435
+ return value
436
+ .map((item) => String(item).trim())
437
+ .filter(Boolean);
438
+ }
439
+
440
+ function metadataTimestamp(metadata: Record<string, unknown>, fallbackTs: number): number {
441
+ const storedAt = Number(metadata.storedAt);
442
+ if (Number.isFinite(storedAt) && storedAt > 0) return storedAt;
443
+ return Number.isFinite(fallbackTs) ? fallbackTs : Date.now();
444
+ }
445
+
446
+ function readPositiveNumber(value: unknown, fallback: number): number {
447
+ const num = Number(value);
448
+ if (!Number.isFinite(num) || num <= 0) return fallback;
449
+ return num;
450
+ }
451
+
452
+ function readClampedNumber(value: unknown, fallback: number, min: number, max: number): number {
453
+ const num = Number(value);
454
+ const resolved = Number.isFinite(num) ? num : fallback;
455
+ return Math.max(min, Math.min(max, resolved));
456
+ }
457
+
458
+ export function computeDerivedLineQuality(nonPlaceholderLineCount: number): number {
459
+ const n = Number.isFinite(nonPlaceholderLineCount) ? Math.max(0, Math.floor(nonPlaceholderLineCount)) : 0;
460
+ if (n <= 0) return 0.2;
461
+ return Math.min(1, 0.55 + Math.min(6, n) * 0.075);
462
+ }
463
+
464
+ function resolveLegacyDeriveBaseWeight(metadata: Record<string, unknown>): number {
465
+ const explicit = Number(metadata.deriveBaseWeight);
466
+ if (Number.isFinite(explicit) && explicit > 0) {
467
+ return Math.max(0.1, Math.min(1.2, explicit));
468
+ }
469
+ if (metadata.usedFallback === true) {
470
+ return REFLECTION_DERIVE_FALLBACK_BASE_WEIGHT;
471
+ }
472
+ return 1;
473
+ }
474
+
475
+ export interface LoadReflectionMappedRowsParams {
476
+ entries: MemoryEntry[];
477
+ agentId: string;
478
+ now?: number;
479
+ maxAgeMs?: number;
480
+ maxPerKind?: number;
481
+ }
482
+
483
+ export interface ReflectionMappedSlices {
484
+ userModel: string[];
485
+ agentModel: string[];
486
+ lesson: string[];
487
+ decision: string[];
488
+ }
489
+
490
+ export function loadReflectionMappedRowsFromEntries(params: LoadReflectionMappedRowsParams): ReflectionMappedSlices {
491
+ const now = Number.isFinite(params.now) ? Number(params.now) : Date.now();
492
+ const maxAgeMs = Number.isFinite(params.maxAgeMs)
493
+ ? Math.max(0, Number(params.maxAgeMs))
494
+ : DEFAULT_REFLECTION_MAPPED_MAX_AGE_MS;
495
+ const maxPerKind = Number.isFinite(params.maxPerKind) ? Math.max(1, Math.floor(Number(params.maxPerKind))) : 10;
496
+
497
+ type WeightedMapped = {
498
+ text: string;
499
+ mappedKind: ReflectionMappedKind;
500
+ timestamp: number;
501
+ midpointDays: number;
502
+ k: number;
503
+ baseWeight: number;
504
+ quality: number;
505
+ usedFallback: boolean;
506
+ };
507
+
508
+ const weighted: WeightedMapped[] = params.entries
509
+ .map((entry) => ({ entry, metadata: parseReflectionMetadata(entry.metadata) }))
510
+ .filter(({ metadata }) => metadata.type === "memory-reflection-mapped" && isOwnedByAgent(metadata, params.agentId))
511
+ .flatMap(({ entry, metadata }) => {
512
+ const mappedKind = parseMappedKind(metadata.mappedKind);
513
+ if (!mappedKind) return [];
514
+
515
+ const lines = sanitizeReflectionSliceLines([entry.text]);
516
+ if (lines.length === 0) return [];
517
+
518
+ const defaults = getReflectionMappedDecayDefaults(mappedKind);
519
+ const timestamp = metadataTimestamp(metadata, entry.timestamp);
520
+
521
+ return lines.map((line) => ({
522
+ text: line,
523
+ mappedKind,
524
+ timestamp,
525
+ midpointDays: readPositiveNumber(metadata.decayMidpointDays, defaults.midpointDays),
526
+ k: readPositiveNumber(metadata.decayK, defaults.k),
527
+ baseWeight: readPositiveNumber(metadata.baseWeight, defaults.baseWeight),
528
+ quality: readClampedNumber(metadata.quality, defaults.quality, 0.2, 1),
529
+ usedFallback: metadata.usedFallback === true,
530
+ }));
531
+ });
532
+
533
+ const grouped = new Map<string, { text: string; score: number; latestTs: number; kind: ReflectionMappedKind }>();
534
+
535
+ for (const item of weighted) {
536
+ if (now - item.timestamp > maxAgeMs) continue;
537
+ const ageDays = Math.max(0, (now - item.timestamp) / 86_400_000);
538
+ const score = computeReflectionScore({
539
+ ageDays,
540
+ midpointDays: item.midpointDays,
541
+ k: item.k,
542
+ baseWeight: item.baseWeight,
543
+ quality: item.quality,
544
+ usedFallback: item.usedFallback,
545
+ });
546
+ if (!Number.isFinite(score) || score <= 0) continue;
547
+
548
+ const normalized = normalizeReflectionLineForAggregation(item.text);
549
+ if (!normalized) continue;
550
+
551
+ const key = `${item.mappedKind}::${normalized}`;
552
+ const current = grouped.get(key);
553
+ if (!current) {
554
+ grouped.set(key, { text: item.text, score, latestTs: item.timestamp, kind: item.mappedKind });
555
+ continue;
556
+ }
557
+
558
+ current.score += score;
559
+ if (item.timestamp > current.latestTs) {
560
+ current.latestTs = item.timestamp;
561
+ current.text = item.text;
562
+ }
563
+ }
564
+
565
+ const sortedByKind = (kind: ReflectionMappedKind) => [...grouped.values()]
566
+ .filter((row) => row.kind === kind)
567
+ .sort((a, b) => {
568
+ if (b.score !== a.score) return b.score - a.score;
569
+ if (b.latestTs !== a.latestTs) return b.latestTs - a.latestTs;
570
+ return a.text.localeCompare(b.text);
571
+ })
572
+ .slice(0, maxPerKind)
573
+ .map((row) => row.text);
574
+
575
+ return {
576
+ userModel: sortedByKind("user-model"),
577
+ agentModel: sortedByKind("agent-model"),
578
+ lesson: sortedByKind("lesson"),
579
+ decision: sortedByKind("decision"),
580
+ };
581
+ }
582
+
583
+ function parseMappedKind(value: unknown): ReflectionMappedKind | null {
584
+ if (value === "user-model" || value === "agent-model" || value === "lesson" || value === "decision") {
585
+ return value;
586
+ }
587
+ return null;
588
+ }
589
+
590
+ export function getReflectionDerivedDecayDefaults(): { midpointDays: number; k: number } {
591
+ return {
592
+ midpointDays: REFLECTION_DERIVED_DECAY_MIDPOINT_DAYS,
593
+ k: REFLECTION_DERIVED_DECAY_K,
594
+ };
595
+ }
596
+
597
+ export function getReflectionInvariantDecayDefaults(): { midpointDays: number; k: number } {
598
+ return {
599
+ midpointDays: REFLECTION_INVARIANT_DECAY_MIDPOINT_DAYS,
600
+ k: REFLECTION_INVARIANT_DECAY_K,
601
+ };
602
+ }
@@ -0,0 +1,85 @@
1
+ // SPDX-License-Identifier: MIT
2
+ /**
3
+ * Adaptive Resonance Threshold
4
+ *
5
+ * Maintains a sliding window of recent auto-recall top-1 cosine scores
6
+ * to compute an adaptive resonance gate threshold (P25 of the window).
7
+ * Persisted to ~/.openclaw/memory/resonance-state.json.
8
+ */
9
+
10
+ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
11
+ import { homedir } from "node:os";
12
+ import { join, dirname } from "node:path";
13
+
14
+ const STATE_PATH = join(homedir(), ".openclaw", "memory", "resonance-state.json");
15
+ const WINDOW_SIZE = 100;
16
+ const COLD_START_MIN = 20;
17
+ const DEFAULT_THRESHOLD = 0.45;
18
+ const THRESHOLD_FLOOR = 0.30;
19
+ const THRESHOLD_CEILING = 0.60;
20
+
21
+ interface ResonanceStateData {
22
+ scores: number[];
23
+ }
24
+
25
+ let cached: ResonanceStateData | null = null;
26
+
27
+ function loadState(): ResonanceStateData {
28
+ if (cached) return cached;
29
+ try {
30
+ const raw = readFileSync(STATE_PATH, "utf8");
31
+ const parsed = JSON.parse(raw);
32
+ if (parsed && Array.isArray(parsed.scores)) {
33
+ cached = { scores: parsed.scores.filter((s: unknown) => typeof s === "number" && Number.isFinite(s as number)).slice(-WINDOW_SIZE) };
34
+ return cached;
35
+ }
36
+ } catch {
37
+ // file doesn't exist or is corrupted — start fresh
38
+ }
39
+ cached = { scores: [] };
40
+ return cached;
41
+ }
42
+
43
+ function saveState(state: ResonanceStateData): void {
44
+ try {
45
+ mkdirSync(dirname(STATE_PATH), { recursive: true });
46
+ writeFileSync(STATE_PATH, JSON.stringify(state), "utf8");
47
+ } catch {
48
+ // best-effort persistence
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Get the adaptive resonance threshold.
54
+ * - Cold start (< 20 samples): returns default 0.45
55
+ * - Otherwise: P25 of sliding window, clamped to [0.30, 0.60]
56
+ */
57
+ export function getAdaptiveThreshold(): number {
58
+ const state = loadState();
59
+ if (state.scores.length < COLD_START_MIN) {
60
+ return DEFAULT_THRESHOLD;
61
+ }
62
+
63
+ // Compute P25 (25th percentile)
64
+ const sorted = [...state.scores].sort((a, b) => a - b);
65
+ const idx = Math.floor(sorted.length * 0.25);
66
+ const p25 = sorted[idx];
67
+
68
+ return Math.min(THRESHOLD_CEILING, Math.max(THRESHOLD_FLOOR, p25));
69
+ }
70
+
71
+ /**
72
+ * Record a top-1 cosine score from an auto-recall probe.
73
+ * Appends to the sliding window and persists.
74
+ */
75
+ export function recordResonanceScore(score: number): void {
76
+ if (!Number.isFinite(score)) return;
77
+ const state = loadState();
78
+ state.scores.push(score);
79
+ // Trim to window size
80
+ if (state.scores.length > WINDOW_SIZE) {
81
+ state.scores = state.scores.slice(-WINDOW_SIZE);
82
+ }
83
+ cached = state;
84
+ saveState(state);
85
+ }