@prometheus-ai/memory 0.5.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/README.md +107 -0
- package/dist/types/cli.d.ts +35 -0
- package/dist/types/config.d.ts +77 -0
- package/dist/types/core/aaak.d.ts +55 -0
- package/dist/types/core/annotations.d.ts +75 -0
- package/dist/types/core/banks.d.ts +33 -0
- package/dist/types/core/beam/consolidate.d.ts +32 -0
- package/dist/types/core/beam/helpers.d.ts +76 -0
- package/dist/types/core/beam/index.d.ts +59 -0
- package/dist/types/core/beam/recall.d.ts +32 -0
- package/dist/types/core/beam/schema.d.ts +2 -0
- package/dist/types/core/beam/store.d.ts +35 -0
- package/dist/types/core/beam/types.d.ts +233 -0
- package/dist/types/core/binary-vectors.d.ts +54 -0
- package/dist/types/core/chat-normalize.d.ts +13 -0
- package/dist/types/core/content-sanitizer.d.ts +18 -0
- package/dist/types/core/cost-log.d.ts +13 -0
- package/dist/types/core/embeddings.d.ts +44 -0
- package/dist/types/core/entities.d.ts +7 -0
- package/dist/types/core/episodic-graph.d.ts +89 -0
- package/dist/types/core/extraction/client.d.ts +31 -0
- package/dist/types/core/extraction/diagnostics.d.ts +51 -0
- package/dist/types/core/extraction/prompts.d.ts +2 -0
- package/dist/types/core/extraction.d.ts +6 -0
- package/dist/types/core/index.d.ts +4 -0
- package/dist/types/core/llm-backends.d.ts +21 -0
- package/dist/types/core/local-llm.d.ts +15 -0
- package/dist/types/core/memory.d.ts +160 -0
- package/dist/types/core/migrations/e6-triplestore-split.d.ts +17 -0
- package/dist/types/core/migrations/index.d.ts +1 -0
- package/dist/types/core/mmr.d.ts +8 -0
- package/dist/types/core/orchestrator.d.ts +20 -0
- package/dist/types/core/patterns.d.ts +61 -0
- package/dist/types/core/plugins.d.ts +109 -0
- package/dist/types/core/polyphonic-recall.d.ts +66 -0
- package/dist/types/core/query-cache.d.ts +46 -0
- package/dist/types/core/query-intent.d.ts +20 -0
- package/dist/types/core/recall-diagnostics.d.ts +48 -0
- package/dist/types/core/runtime-options.d.ts +68 -0
- package/dist/types/core/shmr.d.ts +56 -0
- package/dist/types/core/streaming.d.ts +136 -0
- package/dist/types/core/synonyms.d.ts +46 -0
- package/dist/types/core/temporal-parser.d.ts +16 -0
- package/dist/types/core/token-counter.d.ts +8 -0
- package/dist/types/core/triples.d.ts +63 -0
- package/dist/types/core/typed-memory.d.ts +39 -0
- package/dist/types/core/vector-math.d.ts +1 -0
- package/dist/types/core/veracity-consolidation.d.ts +60 -0
- package/dist/types/core/weibull.d.ts +96 -0
- package/dist/types/db.d.ts +16 -0
- package/dist/types/diagnose.d.ts +24 -0
- package/dist/types/dr/index.d.ts +1 -0
- package/dist/types/dr/recovery.d.ts +68 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/mcp-server.d.ts +40 -0
- package/dist/types/mcp-tools.d.ts +484 -0
- package/dist/types/migrations/e6-triplestore-split.d.ts +1 -0
- package/dist/types/migrations/index.d.ts +1 -0
- package/dist/types/types.d.ts +145 -0
- package/dist/types/util/datetime.d.ts +8 -0
- package/dist/types/util/env.d.ts +10 -0
- package/dist/types/util/ids.d.ts +3 -0
- package/dist/types/util/lru.d.ts +12 -0
- package/dist/types/util/regex.d.ts +10 -0
- package/package.json +85 -0
- package/src/cli.ts +398 -0
- package/src/config.ts +326 -0
- package/src/core/aaak.ts +142 -0
- package/src/core/annotations.ts +457 -0
- package/src/core/banks.ts +133 -0
- package/src/core/beam/consolidate.ts +965 -0
- package/src/core/beam/helpers.ts +977 -0
- package/src/core/beam/index.ts +353 -0
- package/src/core/beam/recall.ts +1100 -0
- package/src/core/beam/schema.ts +423 -0
- package/src/core/beam/store.ts +829 -0
- package/src/core/beam/types.ts +268 -0
- package/src/core/binary-vectors.ts +317 -0
- package/src/core/chat-normalize.ts +160 -0
- package/src/core/content-sanitizer.ts +136 -0
- package/src/core/cost-log.ts +103 -0
- package/src/core/embeddings.ts +423 -0
- package/src/core/entities.ts +259 -0
- package/src/core/episodic-graph.ts +708 -0
- package/src/core/extraction/client.ts +162 -0
- package/src/core/extraction/diagnostics.ts +193 -0
- package/src/core/extraction/prompts.ts +31 -0
- package/src/core/extraction.ts +335 -0
- package/src/core/index.ts +30 -0
- package/src/core/llm-backends.ts +51 -0
- package/src/core/local-llm.ts +436 -0
- package/src/core/memory.ts +630 -0
- package/src/core/migrations/e6-triplestore-split.ts +211 -0
- package/src/core/migrations/index.ts +1 -0
- package/src/core/mmr.ts +71 -0
- package/src/core/orchestrator.ts +62 -0
- package/src/core/patterns.ts +484 -0
- package/src/core/plugins.ts +375 -0
- package/src/core/polyphonic-recall.ts +563 -0
- package/src/core/query-cache.ts +354 -0
- package/src/core/query-intent.ts +139 -0
- package/src/core/recall-diagnostics.ts +157 -0
- package/src/core/runtime-options.ts +119 -0
- package/src/core/shmr.ts +460 -0
- package/src/core/streaming.ts +419 -0
- package/src/core/synonyms.ts +197 -0
- package/src/core/temporal-parser.ts +363 -0
- package/src/core/token-counter.ts +30 -0
- package/src/core/triples.ts +454 -0
- package/src/core/typed-memory.ts +407 -0
- package/src/core/vector-math.ts +23 -0
- package/src/core/veracity-consolidation.ts +477 -0
- package/src/core/weibull.ts +124 -0
- package/src/db.ts +128 -0
- package/src/diagnose.ts +174 -0
- package/src/dr/index.ts +1 -0
- package/src/dr/recovery.ts +405 -0
- package/src/index.ts +33 -0
- package/src/mcp-server.ts +155 -0
- package/src/mcp-tools.ts +970 -0
- package/src/migrations/e6-triplestore-split.ts +1 -0
- package/src/migrations/index.ts +1 -0
- package/src/types.ts +157 -0
- package/src/util/datetime.ts +69 -0
- package/src/util/env.ts +65 -0
- package/src/util/ids.ts +19 -0
- package/src/util/lru.ts +48 -0
- package/src/util/regex.ts +165 -0
|
@@ -0,0 +1,965 @@
|
|
|
1
|
+
import type { SQLQueryBindings } from "bun:sqlite";
|
|
2
|
+
import { generateId, stableMemoryId } from "../../util/ids";
|
|
3
|
+
import { aaakEncode } from "../aaak";
|
|
4
|
+
import { heuristicExtractFacts } from "../extraction";
|
|
5
|
+
import { clampVeracity } from "../veracity-consolidation";
|
|
6
|
+
import { scheduleEmbedding } from "./helpers";
|
|
7
|
+
import type { BeamMemoryState, BeamStats, JsonValue, MemoriaRetrieveResult, Metadata, SleepResult } from "./types";
|
|
8
|
+
|
|
9
|
+
type Row = Record<string, unknown>;
|
|
10
|
+
|
|
11
|
+
type FactCounts = {
|
|
12
|
+
metric: number;
|
|
13
|
+
date: number;
|
|
14
|
+
version: number;
|
|
15
|
+
entity: number;
|
|
16
|
+
sequence: number;
|
|
17
|
+
timeline: number;
|
|
18
|
+
negation: number;
|
|
19
|
+
decision: number;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type ConsolidateOptions = {
|
|
23
|
+
metadata?: Metadata | null;
|
|
24
|
+
validUntil?: string | null;
|
|
25
|
+
scope?: string;
|
|
26
|
+
veracity?: string | null;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const CONTAMINATED_VERACITY: Record<string, true> = {
|
|
30
|
+
inferred: true,
|
|
31
|
+
tool: true,
|
|
32
|
+
imported: true,
|
|
33
|
+
unknown: true,
|
|
34
|
+
false: true,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const EPISODIC_VERACITY_WEIGHT = {
|
|
38
|
+
true: 1.0,
|
|
39
|
+
stated: 1.0,
|
|
40
|
+
unknown: 0.8,
|
|
41
|
+
inferred: 0.7,
|
|
42
|
+
imported: 0.6,
|
|
43
|
+
tool: 0.5,
|
|
44
|
+
false: 0.0,
|
|
45
|
+
} as const;
|
|
46
|
+
|
|
47
|
+
type EpisodicVeracity = keyof typeof EPISODIC_VERACITY_WEIGHT;
|
|
48
|
+
|
|
49
|
+
function envInt(name: string, defaultValue: number): number {
|
|
50
|
+
const parsed = Number.parseInt(process.env[name] ?? "", 10);
|
|
51
|
+
return Number.isFinite(parsed) ? parsed : defaultValue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const SLEEP_BATCH_SIZE = envInt("PROMETHEUS_MEMORY_SLEEP_BATCH", 5000);
|
|
55
|
+
const TIER2_DAYS = envInt("PROMETHEUS_MEMORY_TIER2_DAYS", 30);
|
|
56
|
+
const TIER3_DAYS = envInt("PROMETHEUS_MEMORY_TIER3_DAYS", 180);
|
|
57
|
+
const DEGRADE_BATCH_SIZE = envInt("PROMETHEUS_MEMORY_DEGRADE_BATCH", 100);
|
|
58
|
+
const TIER3_MAX_CHARS = envInt("PROMETHEUS_MEMORY_TIER3_MAX_CHARS", 300);
|
|
59
|
+
|
|
60
|
+
function isoNow(): string {
|
|
61
|
+
return new Date().toISOString();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function cutoffIso(amount: number, unitMs: number): string {
|
|
65
|
+
return new Date(Date.now() - amount * unitMs).toISOString();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function json(metadata: Metadata | null | undefined): string {
|
|
69
|
+
return JSON.stringify(metadata ?? {});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function rowValue(row: Row, key: string): string | null {
|
|
73
|
+
const value = row[key];
|
|
74
|
+
return value == null ? null : String(value);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isEpisodicVeracity(value: string): value is EpisodicVeracity {
|
|
78
|
+
return Object.hasOwn(EPISODIC_VERACITY_WEIGHT, value);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function clampEpisodicVeracity(raw: unknown): EpisodicVeracity {
|
|
82
|
+
if (raw === null || raw === undefined) return "unknown";
|
|
83
|
+
const norm = String(raw).trim().toLowerCase();
|
|
84
|
+
if (norm === "") return "unknown";
|
|
85
|
+
if (isEpisodicVeracity(norm)) return norm;
|
|
86
|
+
const clamped = clampVeracity(raw, "consolidateToEpisodic.veracity");
|
|
87
|
+
return isEpisodicVeracity(clamped) ? clamped : "unknown";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function aggregateEpisodicVeracity(sourceVeracities: readonly string[]): EpisodicVeracity {
|
|
91
|
+
let winner: EpisodicVeracity | null = null;
|
|
92
|
+
let maxCount = 0;
|
|
93
|
+
const counts = new Map<EpisodicVeracity, number>();
|
|
94
|
+
for (const raw of sourceVeracities) {
|
|
95
|
+
const value = clampEpisodicVeracity(raw);
|
|
96
|
+
if (value === "unknown") continue;
|
|
97
|
+
const count = (counts.get(value) ?? 0) + 1;
|
|
98
|
+
counts.set(value, count);
|
|
99
|
+
if (
|
|
100
|
+
count > maxCount ||
|
|
101
|
+
(count === maxCount && (winner === null || EPISODIC_VERACITY_WEIGHT[value] < EPISODIC_VERACITY_WEIGHT[winner]))
|
|
102
|
+
) {
|
|
103
|
+
winner = value;
|
|
104
|
+
maxCount = count;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (winner !== null) return winner;
|
|
108
|
+
for (const raw of sourceVeracities) {
|
|
109
|
+
if (clampEpisodicVeracity(raw) === "unknown") return "unknown";
|
|
110
|
+
}
|
|
111
|
+
return "unknown";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function compactWhitespace(text: string): string {
|
|
115
|
+
return text.replace(/\s+/g, " ").trim();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function contextSnippet(content: string, index: number, width = 50): string {
|
|
119
|
+
const start = Math.max(0, index - width);
|
|
120
|
+
const end = Math.min(content.length, index + width);
|
|
121
|
+
return compactWhitespace(content.slice(start, end));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function sourceSession(beam: BeamMemoryState): string {
|
|
125
|
+
return beam.sessionId || "default";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function asRows(value: unknown): Row[] {
|
|
129
|
+
return Array.isArray(value) ? (value as Row[]) : [];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function escapeLike(value: string): string {
|
|
133
|
+
return value.replace(/[\\%_]/g, m => `\\${m}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function makeQuestionTokens(query: string): string[] {
|
|
137
|
+
const stop = new Set([
|
|
138
|
+
"a",
|
|
139
|
+
"an",
|
|
140
|
+
"and",
|
|
141
|
+
"are",
|
|
142
|
+
"as",
|
|
143
|
+
"at",
|
|
144
|
+
"did",
|
|
145
|
+
"do",
|
|
146
|
+
"does",
|
|
147
|
+
"for",
|
|
148
|
+
"from",
|
|
149
|
+
"how",
|
|
150
|
+
"i",
|
|
151
|
+
"in",
|
|
152
|
+
"is",
|
|
153
|
+
"it",
|
|
154
|
+
"me",
|
|
155
|
+
"my",
|
|
156
|
+
"of",
|
|
157
|
+
"on",
|
|
158
|
+
"or",
|
|
159
|
+
"the",
|
|
160
|
+
"to",
|
|
161
|
+
"was",
|
|
162
|
+
"were",
|
|
163
|
+
"what",
|
|
164
|
+
"when",
|
|
165
|
+
"where",
|
|
166
|
+
"which",
|
|
167
|
+
"who",
|
|
168
|
+
"with",
|
|
169
|
+
]);
|
|
170
|
+
return [...query.toLowerCase().matchAll(/[\p{L}\p{N}_.-]+/gu)]
|
|
171
|
+
.map(m => m[0] ?? "")
|
|
172
|
+
.filter(token => token.length > 1 && !stop.has(token))
|
|
173
|
+
.slice(0, 8);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function emitEvent(
|
|
177
|
+
beam: BeamMemoryState,
|
|
178
|
+
type: string,
|
|
179
|
+
memoryId: string,
|
|
180
|
+
content: string,
|
|
181
|
+
source: string,
|
|
182
|
+
importance: number,
|
|
183
|
+
metadata: Metadata,
|
|
184
|
+
): void {
|
|
185
|
+
const event = {
|
|
186
|
+
type,
|
|
187
|
+
sessionId: beam.sessionId,
|
|
188
|
+
timestamp: isoNow(),
|
|
189
|
+
memoryId,
|
|
190
|
+
content,
|
|
191
|
+
source,
|
|
192
|
+
importance,
|
|
193
|
+
metadata,
|
|
194
|
+
};
|
|
195
|
+
beam.eventEmitter?.(event);
|
|
196
|
+
void beam.pluginManager?.emit?.(event);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function insertFactRows(
|
|
200
|
+
beam: BeamMemoryState,
|
|
201
|
+
messageIdx: number,
|
|
202
|
+
factType: string,
|
|
203
|
+
key: string,
|
|
204
|
+
value: string,
|
|
205
|
+
context: string,
|
|
206
|
+
importance: number,
|
|
207
|
+
sourceMemoryId: string | null,
|
|
208
|
+
): void {
|
|
209
|
+
const timestamp = isoNow();
|
|
210
|
+
beam.db.run(
|
|
211
|
+
`INSERT INTO memoria_facts
|
|
212
|
+
(session_id, message_idx, fact_type, key, value, context_snippet, importance, timestamp, source_memory_id)
|
|
213
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
214
|
+
[sourceSession(beam), messageIdx, factType, key, value, context, importance, timestamp, sourceMemoryId],
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
const factId = stableMemoryId(`${sourceSession(beam)}\0${factType}\0${key}\0${value}`, sourceMemoryId ?? "");
|
|
218
|
+
beam.db.run(
|
|
219
|
+
`INSERT OR IGNORE INTO facts
|
|
220
|
+
(fact_id, session_id, subject, predicate, object, timestamp, source_msg_id, confidence)
|
|
221
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
222
|
+
[factId, sourceSession(beam), key, factType, value, timestamp, sourceMemoryId, importance],
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function insertTimeline(
|
|
227
|
+
beam: BeamMemoryState,
|
|
228
|
+
messageIdx: number,
|
|
229
|
+
date: string,
|
|
230
|
+
description: string,
|
|
231
|
+
sourceMemoryId: string | null,
|
|
232
|
+
): void {
|
|
233
|
+
beam.db.run(
|
|
234
|
+
`INSERT INTO memoria_timelines (session_id, date, message_idx, description, source, source_memory_id)
|
|
235
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
236
|
+
[sourceSession(beam), date, messageIdx, description, "extraction", sourceMemoryId],
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function insertKg(
|
|
241
|
+
beam: BeamMemoryState,
|
|
242
|
+
messageIdx: number,
|
|
243
|
+
subject: string,
|
|
244
|
+
predicate: string,
|
|
245
|
+
object: string,
|
|
246
|
+
sourceMemoryId: string | null,
|
|
247
|
+
): void {
|
|
248
|
+
beam.db.run(
|
|
249
|
+
`INSERT INTO memoria_kg (session_id, subject, predicate, object, message_idx, confidence, source_memory_id)
|
|
250
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
251
|
+
[sourceSession(beam), subject, predicate, object, messageIdx, 0.65, sourceMemoryId],
|
|
252
|
+
);
|
|
253
|
+
beam.db.run(
|
|
254
|
+
`INSERT INTO triples (subject, predicate, object, valid_from, source, confidence)
|
|
255
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
256
|
+
[subject, predicate, object, isoNow(), sourceMemoryId ?? "extraction", 0.65],
|
|
257
|
+
);
|
|
258
|
+
void beam.triples?.add?.(subject, predicate, object, {
|
|
259
|
+
source: sourceMemoryId ?? "extraction",
|
|
260
|
+
confidence: 0.65,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function consolidateToEpisodic(
|
|
265
|
+
beam: BeamMemoryState,
|
|
266
|
+
summary: string,
|
|
267
|
+
sourceWmIds: readonly string[],
|
|
268
|
+
source = "consolidation",
|
|
269
|
+
importance = 0.6,
|
|
270
|
+
options: ConsolidateOptions = {},
|
|
271
|
+
): string {
|
|
272
|
+
const memoryId = generateId(summary);
|
|
273
|
+
const timestamp = isoNow();
|
|
274
|
+
const scope = options.scope ?? "session";
|
|
275
|
+
const veracity = clampEpisodicVeracity(options.veracity ?? "unknown");
|
|
276
|
+
const metadata = options.metadata ?? {};
|
|
277
|
+
beam.db.run(
|
|
278
|
+
`INSERT INTO episodic_memory
|
|
279
|
+
(id, content, source, timestamp, session_id, importance, metadata_json, summary_of,
|
|
280
|
+
valid_until, scope, author_id, author_type, channel_id, memory_type, veracity, created_at)
|
|
281
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
282
|
+
[
|
|
283
|
+
memoryId,
|
|
284
|
+
summary,
|
|
285
|
+
source,
|
|
286
|
+
timestamp,
|
|
287
|
+
sourceSession(beam),
|
|
288
|
+
importance,
|
|
289
|
+
json(metadata),
|
|
290
|
+
sourceWmIds.join(","),
|
|
291
|
+
options.validUntil ?? null,
|
|
292
|
+
scope,
|
|
293
|
+
beam.authorId,
|
|
294
|
+
beam.authorType,
|
|
295
|
+
beam.channelId,
|
|
296
|
+
"unknown",
|
|
297
|
+
veracity,
|
|
298
|
+
timestamp,
|
|
299
|
+
],
|
|
300
|
+
);
|
|
301
|
+
extractAndStoreFacts(beam, summary, 0, memoryId);
|
|
302
|
+
scheduleEmbedding(beam, [{ memoryId, content: summary }]);
|
|
303
|
+
emitEvent(beam, "MEMORY_CONSOLIDATED", memoryId, summary, source, importance, {
|
|
304
|
+
summary_of: [...sourceWmIds],
|
|
305
|
+
...metadata,
|
|
306
|
+
});
|
|
307
|
+
return memoryId;
|
|
308
|
+
}
|
|
309
|
+
export function detectLanguage(_beam: BeamMemoryState, text: string): string {
|
|
310
|
+
if (typeof text !== "string" || text.length === 0) return "en";
|
|
311
|
+
const lower = text.toLowerCase();
|
|
312
|
+
const russianChars = [...lower].filter(c => "абвгдеёжзийклмнопрстуфхцчшщъыьэюя".includes(c)).length;
|
|
313
|
+
if (russianChars >= 5) return "ru";
|
|
314
|
+
if (russianChars >= 2) {
|
|
315
|
+
const markers = new Set(["я", "ты", "он", "она", "мы", "вы", "они", "не", "на", "что", "как", "это"]);
|
|
316
|
+
let hits = 0;
|
|
317
|
+
for (const word of lower.split(/\s+/)) if (markers.has(word)) hits++;
|
|
318
|
+
if (hits >= 2) return "ru";
|
|
319
|
+
}
|
|
320
|
+
if (/[äöüß]/.test(lower)) return "de";
|
|
321
|
+
const words = new Set(lower.match(/[\p{L}\p{N}_]+/gu) ?? []);
|
|
322
|
+
let german = 0;
|
|
323
|
+
for (const marker of [
|
|
324
|
+
"ich",
|
|
325
|
+
"du",
|
|
326
|
+
"wir",
|
|
327
|
+
"ist",
|
|
328
|
+
"nicht",
|
|
329
|
+
"für",
|
|
330
|
+
"und",
|
|
331
|
+
"der",
|
|
332
|
+
"die",
|
|
333
|
+
"das",
|
|
334
|
+
"ein",
|
|
335
|
+
"eine",
|
|
336
|
+
"habe",
|
|
337
|
+
"bin",
|
|
338
|
+
"sind",
|
|
339
|
+
]) {
|
|
340
|
+
if (words.has(marker)) german++;
|
|
341
|
+
}
|
|
342
|
+
if (german >= 2) return "de";
|
|
343
|
+
if (/[ñáéíóúü¿¡]/.test(lower)) return "es";
|
|
344
|
+
let spanish = 0;
|
|
345
|
+
for (const marker of [
|
|
346
|
+
"y",
|
|
347
|
+
"de",
|
|
348
|
+
"por",
|
|
349
|
+
"con",
|
|
350
|
+
"para",
|
|
351
|
+
"que",
|
|
352
|
+
"qué",
|
|
353
|
+
"como",
|
|
354
|
+
"el",
|
|
355
|
+
"la",
|
|
356
|
+
"un",
|
|
357
|
+
"una",
|
|
358
|
+
"mi",
|
|
359
|
+
"tu",
|
|
360
|
+
"soy",
|
|
361
|
+
"estoy",
|
|
362
|
+
]) {
|
|
363
|
+
if (words.has(marker)) spanish++;
|
|
364
|
+
}
|
|
365
|
+
return spanish >= 3 ? "es" : "en";
|
|
366
|
+
}
|
|
367
|
+
export function storeFactStrings(
|
|
368
|
+
beam: BeamMemoryState,
|
|
369
|
+
facts: readonly string[],
|
|
370
|
+
messageIdx = 0,
|
|
371
|
+
sourceMemoryId: string | null = null,
|
|
372
|
+
importance = 0.7,
|
|
373
|
+
): number {
|
|
374
|
+
let stored = 0;
|
|
375
|
+
for (const fact of facts) {
|
|
376
|
+
insertFactRows(beam, messageIdx, "entity", "fact", fact, fact, importance, sourceMemoryId);
|
|
377
|
+
stored++;
|
|
378
|
+
const pref = /^The user (prefers|dislikes) (.+)$/i.exec(fact);
|
|
379
|
+
if (pref?.[2]) {
|
|
380
|
+
beam.db.run(
|
|
381
|
+
`INSERT INTO memoria_preferences (session_id, message_idx, preference, topic, evolution, context_snippet, source_memory_id)
|
|
382
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
383
|
+
[sourceSession(beam), messageIdx, fact, pref[2], null, fact, sourceMemoryId],
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
const instruction = /^Instruction: (.+)$/i.exec(fact);
|
|
387
|
+
if (instruction?.[1]) {
|
|
388
|
+
beam.db.run(
|
|
389
|
+
`INSERT INTO memoria_instructions (session_id, message_idx, instruction, active, topic, context_snippet, source_memory_id)
|
|
390
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
391
|
+
[sourceSession(beam), messageIdx, instruction[1], 1, null, fact, sourceMemoryId],
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return stored;
|
|
396
|
+
}
|
|
397
|
+
export function extractAndStoreFacts(
|
|
398
|
+
beam: BeamMemoryState,
|
|
399
|
+
content: string,
|
|
400
|
+
messageIdx = 0,
|
|
401
|
+
sourceMemoryId: string | null = null,
|
|
402
|
+
): FactCounts {
|
|
403
|
+
const counts: FactCounts = {
|
|
404
|
+
metric: 0,
|
|
405
|
+
date: 0,
|
|
406
|
+
version: 0,
|
|
407
|
+
entity: 0,
|
|
408
|
+
sequence: 0,
|
|
409
|
+
timeline: 0,
|
|
410
|
+
negation: 0,
|
|
411
|
+
decision: 0,
|
|
412
|
+
};
|
|
413
|
+
const text = String(content ?? "");
|
|
414
|
+
for (const match of text.matchAll(
|
|
415
|
+
/(\d+(?:[.,]\d+)?)\s*(ms|sec|seconds?|minutes?|hours?|days?|weeks?|months?|%|KB|MB|GB|TB|rows?|columns?|roles?|features?|bugs?|commits?|cards?|users?|items?|tests?|APIs?|endpoints?|sprints?|tickets?)\b/gi,
|
|
416
|
+
)) {
|
|
417
|
+
const rawUnit = match[2] ?? "";
|
|
418
|
+
let unit = rawUnit.toLowerCase();
|
|
419
|
+
if (unit.endsWith("s") && !unit.endsWith("ms")) unit = unit.slice(0, -1);
|
|
420
|
+
const prefixWords = text
|
|
421
|
+
.slice(Math.max(0, (match.index ?? 0) - 50), match.index ?? 0)
|
|
422
|
+
.replace(/`[^`]*`/g, " ")
|
|
423
|
+
.split(/\s+/)
|
|
424
|
+
.map(w => w.replace(/[.,:;!?()[\]"'`*_]/g, ""))
|
|
425
|
+
.filter(w => w.length > 2 && !/^(the|and|for|was|of|to|an?|in|on|at|by|is|are|has|had|not|but|or)$/i.test(w))
|
|
426
|
+
.slice(-3)
|
|
427
|
+
.join("_")
|
|
428
|
+
.toLowerCase();
|
|
429
|
+
let key = prefixWords === "" ? unit : `${prefixWords}_${unit}`;
|
|
430
|
+
if (unit === "%") key = prefixWords === "" ? "pct" : `${prefixWords}_pct`;
|
|
431
|
+
insertFactRows(
|
|
432
|
+
beam,
|
|
433
|
+
messageIdx,
|
|
434
|
+
"metric",
|
|
435
|
+
key,
|
|
436
|
+
`${match[1]}${rawUnit}`,
|
|
437
|
+
contextSnippet(text, match.index ?? 0),
|
|
438
|
+
0.65,
|
|
439
|
+
sourceMemoryId,
|
|
440
|
+
);
|
|
441
|
+
counts.metric++;
|
|
442
|
+
if (counts.metric >= 10) break;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
for (const match of text.matchAll(/\b(\d{4}-\d{2}-\d{2})\b/g)) {
|
|
446
|
+
const date = match[1] ?? "";
|
|
447
|
+
const ctx = contextSnippet(text, match.index ?? 0, 100);
|
|
448
|
+
insertFactRows(beam, messageIdx, "date", "iso_date", date, ctx, 0.5, sourceMemoryId);
|
|
449
|
+
counts.date++;
|
|
450
|
+
if (/\b(release|deadline|meeting|launch|ship|shipped|due|start|started|finish|finished)\b/i.test(ctx)) {
|
|
451
|
+
insertTimeline(beam, messageIdx, date, ctx, sourceMemoryId);
|
|
452
|
+
counts.timeline++;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
for (const match of text.matchAll(/\b(v?\d+\.\d+(?:\.\d+)?(?:[-+][A-Za-z0-9.]+)?)\b/g)) {
|
|
457
|
+
const value = match[1] ?? "";
|
|
458
|
+
if (/^\d{4}-\d{2}$/.test(value)) continue;
|
|
459
|
+
insertFactRows(
|
|
460
|
+
beam,
|
|
461
|
+
messageIdx,
|
|
462
|
+
"version",
|
|
463
|
+
"version",
|
|
464
|
+
value,
|
|
465
|
+
contextSnippet(text, match.index ?? 0),
|
|
466
|
+
0.6,
|
|
467
|
+
sourceMemoryId,
|
|
468
|
+
);
|
|
469
|
+
counts.version++;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
counts.entity += storeFactStrings(beam, heuristicExtractFacts(text), messageIdx, sourceMemoryId);
|
|
473
|
+
|
|
474
|
+
for (const match of text.matchAll(
|
|
475
|
+
/\b([A-Z][A-Za-z0-9_-]{2,})\s+(?:is|uses|runs|owns|depends on)\s+([^.!?;]{2,80})/g,
|
|
476
|
+
)) {
|
|
477
|
+
insertKg(beam, messageIdx, match[1] ?? "", "related_to", compactWhitespace(match[2] ?? ""), sourceMemoryId);
|
|
478
|
+
}
|
|
479
|
+
if (/\b(no longer|not|never|don't|do not|isn't|wasn't)\b/i.test(text)) counts.negation++;
|
|
480
|
+
if (/\b(decided|decision|choose|chose|approved|rejected)\b/i.test(text)) counts.decision++;
|
|
481
|
+
return counts;
|
|
482
|
+
}
|
|
483
|
+
function classifyAbility(query: string): string {
|
|
484
|
+
const q = query.toLowerCase();
|
|
485
|
+
if (
|
|
486
|
+
[
|
|
487
|
+
"how many days",
|
|
488
|
+
"how many weeks",
|
|
489
|
+
"how many months",
|
|
490
|
+
"how long",
|
|
491
|
+
"what date",
|
|
492
|
+
"what day",
|
|
493
|
+
"when did",
|
|
494
|
+
"when does",
|
|
495
|
+
"deadline",
|
|
496
|
+
"timeline",
|
|
497
|
+
"how far apart",
|
|
498
|
+
].some(w => q.includes(w))
|
|
499
|
+
)
|
|
500
|
+
return "TR";
|
|
501
|
+
if (
|
|
502
|
+
["list the order", "walk me through", "chronological", "in what order", "sequence of events"].some(w =>
|
|
503
|
+
q.includes(w),
|
|
504
|
+
)
|
|
505
|
+
)
|
|
506
|
+
return "EO";
|
|
507
|
+
if (["have i", "did i", "am i", "has this", "contradict", "contradiction", "conflict"].some(w => q.includes(w)))
|
|
508
|
+
return "CR";
|
|
509
|
+
if (["across my", "across all", "in my project", "in my sessions", "across sessions"].some(w => q.includes(w)))
|
|
510
|
+
return "MR";
|
|
511
|
+
if (
|
|
512
|
+
/^(what|when|where|which|who|how)\s/.test(q) ||
|
|
513
|
+
["how many", "what is", "what was", "which version", "how much"].some(w => q.includes(w))
|
|
514
|
+
)
|
|
515
|
+
return "IE";
|
|
516
|
+
return "";
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function factRetrieve(beam: BeamMemoryState, query: string, topK: number): MemoriaRetrieveResult {
|
|
520
|
+
const tokens = makeQuestionTokens(query);
|
|
521
|
+
const clauses: string[] = [];
|
|
522
|
+
const params: SQLQueryBindings[] = [sourceSession(beam)];
|
|
523
|
+
for (const token of tokens) {
|
|
524
|
+
clauses.push(
|
|
525
|
+
"(lower(key) LIKE ? ESCAPE '\\' OR lower(value) LIKE ? ESCAPE '\\' OR lower(context_snippet) LIKE ? ESCAPE '\\')",
|
|
526
|
+
);
|
|
527
|
+
const like = `%${escapeLike(token)}%`;
|
|
528
|
+
params.push(like, like, like);
|
|
529
|
+
}
|
|
530
|
+
const where = clauses.length === 0 ? "1=1" : clauses.join(" OR ");
|
|
531
|
+
params.push(topK);
|
|
532
|
+
const results = asRows(
|
|
533
|
+
beam.db
|
|
534
|
+
.query(
|
|
535
|
+
`SELECT * FROM memoria_facts WHERE session_id = ? AND (${where}) ORDER BY importance DESC, id DESC LIMIT ?`,
|
|
536
|
+
)
|
|
537
|
+
.all(...params),
|
|
538
|
+
);
|
|
539
|
+
return { ability: "IE", query, results };
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function timelineRetrieve(beam: BeamMemoryState, query: string, topK: number): MemoriaRetrieveResult {
|
|
543
|
+
const tokens = makeQuestionTokens(query);
|
|
544
|
+
const clauses: string[] = [];
|
|
545
|
+
const params: SQLQueryBindings[] = [sourceSession(beam)];
|
|
546
|
+
for (const token of tokens) {
|
|
547
|
+
clauses.push("(lower(description) LIKE ? ESCAPE '\\' OR date LIKE ? ESCAPE '\\')");
|
|
548
|
+
const like = `%${escapeLike(token)}%`;
|
|
549
|
+
params.push(like, like);
|
|
550
|
+
}
|
|
551
|
+
const where = clauses.length === 0 ? "1=1" : clauses.join(" OR ");
|
|
552
|
+
params.push(topK);
|
|
553
|
+
const results = asRows(
|
|
554
|
+
beam.db
|
|
555
|
+
.query(
|
|
556
|
+
`SELECT * FROM memoria_timelines WHERE session_id = ? AND (${where}) ORDER BY date ASC, event_id ASC LIMIT ?`,
|
|
557
|
+
)
|
|
558
|
+
.all(...params),
|
|
559
|
+
);
|
|
560
|
+
return { ability: "TR", query, results };
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function kgRetrieve(beam: BeamMemoryState, query: string, topK: number): MemoriaRetrieveResult {
|
|
564
|
+
const tokens = makeQuestionTokens(query);
|
|
565
|
+
const clauses: string[] = [];
|
|
566
|
+
const params: SQLQueryBindings[] = [sourceSession(beam)];
|
|
567
|
+
for (const token of tokens) {
|
|
568
|
+
clauses.push(
|
|
569
|
+
"(lower(subject) LIKE ? ESCAPE '\\' OR lower(predicate) LIKE ? ESCAPE '\\' OR lower(object) LIKE ? ESCAPE '\\')",
|
|
570
|
+
);
|
|
571
|
+
const like = `%${escapeLike(token)}%`;
|
|
572
|
+
params.push(like, like, like);
|
|
573
|
+
}
|
|
574
|
+
const where = clauses.length === 0 ? "1=1" : clauses.join(" OR ");
|
|
575
|
+
params.push(topK);
|
|
576
|
+
const results = asRows(
|
|
577
|
+
beam.db
|
|
578
|
+
.query(
|
|
579
|
+
`SELECT * FROM memoria_kg WHERE session_id = ? AND (${where}) ORDER BY confidence DESC, id DESC LIMIT ?`,
|
|
580
|
+
)
|
|
581
|
+
.all(...params),
|
|
582
|
+
);
|
|
583
|
+
return { ability: "MR", query, results };
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
export function memoriaRetrieve(
|
|
587
|
+
beam: BeamMemoryState,
|
|
588
|
+
query: string,
|
|
589
|
+
ability: string | null = null,
|
|
590
|
+
topK = 10,
|
|
591
|
+
): MemoriaRetrieveResult {
|
|
592
|
+
const selected = ability ?? classifyAbility(query);
|
|
593
|
+
if (selected === "TR" || selected === "EO") return timelineRetrieve(beam, query, topK);
|
|
594
|
+
if (selected === "MR") return kgRetrieve(beam, query, topK);
|
|
595
|
+
if (selected === "IE" || selected === "KU" || selected === "PF" || selected === "IF" || selected === "CR")
|
|
596
|
+
return factRetrieve(beam, query, topK);
|
|
597
|
+
return { ability: selected, query, results: [] };
|
|
598
|
+
}
|
|
599
|
+
export function getEpisodicStats(
|
|
600
|
+
beam: BeamMemoryState,
|
|
601
|
+
authorId: string | null = null,
|
|
602
|
+
authorType: string | null = null,
|
|
603
|
+
channelId: string | null = null,
|
|
604
|
+
): BeamStats {
|
|
605
|
+
const clauses: string[] = [];
|
|
606
|
+
const params: SQLQueryBindings[] = [];
|
|
607
|
+
if (authorId) {
|
|
608
|
+
clauses.push("author_id = ?");
|
|
609
|
+
params.push(authorId);
|
|
610
|
+
}
|
|
611
|
+
if (authorType) {
|
|
612
|
+
clauses.push("author_type = ?");
|
|
613
|
+
params.push(authorType);
|
|
614
|
+
}
|
|
615
|
+
if (channelId) {
|
|
616
|
+
clauses.push("channel_id = ?");
|
|
617
|
+
params.push(channelId);
|
|
618
|
+
}
|
|
619
|
+
const where = clauses.length === 0 ? "" : ` WHERE ${clauses.join(" AND ")}`;
|
|
620
|
+
const total = (
|
|
621
|
+
beam.db.query(`SELECT COUNT(*) AS count FROM episodic_memory${where}`).get(...params) as {
|
|
622
|
+
count: number;
|
|
623
|
+
}
|
|
624
|
+
).count;
|
|
625
|
+
const last = beam.db
|
|
626
|
+
.query(`SELECT timestamp FROM episodic_memory${where} ORDER BY timestamp DESC LIMIT 1`)
|
|
627
|
+
.get(...params) as { timestamp: string | null } | null;
|
|
628
|
+
return { count: total, total, last: last?.timestamp ?? null, vectors: 0, vec_type: "none" };
|
|
629
|
+
}
|
|
630
|
+
export function getMemoriaStats(beam: BeamMemoryState): BeamStats {
|
|
631
|
+
const stats: Record<string, number> = Object.create(null);
|
|
632
|
+
let total = 0;
|
|
633
|
+
for (const table of [
|
|
634
|
+
"memoria_facts",
|
|
635
|
+
"memoria_timelines",
|
|
636
|
+
"memoria_kg",
|
|
637
|
+
"memoria_instructions",
|
|
638
|
+
"memoria_preferences",
|
|
639
|
+
] as const) {
|
|
640
|
+
const count = (beam.db.query(`SELECT COUNT(*) AS count FROM ${table}`).get() as { count: number }).count;
|
|
641
|
+
stats[table] = count;
|
|
642
|
+
total += count;
|
|
643
|
+
}
|
|
644
|
+
return { count: total, ...stats };
|
|
645
|
+
}
|
|
646
|
+
function extractKeySignal(content: string, maxChars: number): string {
|
|
647
|
+
const sentences = content.split(/(?<=[.!?])\s+/).filter(s => s.trim().length > 0);
|
|
648
|
+
if (sentences.length === 0) return content.slice(0, maxChars);
|
|
649
|
+
const scored = sentences.map((sentence, idx) => {
|
|
650
|
+
const score =
|
|
651
|
+
(sentence.match(/\b[A-Z][a-zA-Z0-9_-]+\b/g)?.length ?? 0) * 2 +
|
|
652
|
+
(sentence.match(/\b(prefer|always|never|deadline|release|version|decided|important|must|should)\b/gi)
|
|
653
|
+
?.length ?? 0);
|
|
654
|
+
return { sentence, idx, score };
|
|
655
|
+
});
|
|
656
|
+
scored.sort((a, b) => b.score - a.score || a.idx - b.idx);
|
|
657
|
+
const selected: typeof scored = [];
|
|
658
|
+
let used = 0;
|
|
659
|
+
for (const item of scored) {
|
|
660
|
+
const next = item.sentence.trim();
|
|
661
|
+
if (used + next.length + 1 > maxChars && selected.length > 0) continue;
|
|
662
|
+
selected.push(item);
|
|
663
|
+
used += next.length + 1;
|
|
664
|
+
if (used >= maxChars) break;
|
|
665
|
+
}
|
|
666
|
+
selected.sort((a, b) => a.idx - b.idx);
|
|
667
|
+
const text = selected.map(s => s.sentence.trim()).join(" ");
|
|
668
|
+
return text.length <= maxChars ? text : `${text.slice(0, Math.max(0, maxChars - 6)).trim()} [...]`;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function invalidateEpisodicVectors(beam: BeamMemoryState, memoryId: string): void {
|
|
672
|
+
beam.db.prepare("DELETE FROM memory_embeddings WHERE memory_id = ?").run(memoryId);
|
|
673
|
+
beam.db.prepare("UPDATE episodic_memory SET binary_vector = NULL WHERE id = ?").run(memoryId);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
export function degradeEpisodic(beam: BeamMemoryState, dryRun = false): Record<string, JsonValue> {
|
|
677
|
+
const now = isoNow();
|
|
678
|
+
const tier2Cutoff = cutoffIso(TIER2_DAYS, 24 * 60 * 60 * 1000);
|
|
679
|
+
const tier3Cutoff = cutoffIso(TIER3_DAYS, 24 * 60 * 60 * 1000);
|
|
680
|
+
const tier1Rows = asRows(
|
|
681
|
+
beam.db
|
|
682
|
+
.query(
|
|
683
|
+
`SELECT id, content FROM episodic_memory WHERE tier = 1 AND created_at < ? ORDER BY created_at ASC LIMIT ?`,
|
|
684
|
+
)
|
|
685
|
+
.all(tier2Cutoff, DEGRADE_BATCH_SIZE),
|
|
686
|
+
);
|
|
687
|
+
const tier2Rows = asRows(
|
|
688
|
+
beam.db
|
|
689
|
+
.query(
|
|
690
|
+
`SELECT id, content FROM episodic_memory WHERE tier = 2 AND created_at < ? ORDER BY created_at ASC LIMIT ?`,
|
|
691
|
+
)
|
|
692
|
+
.all(tier3Cutoff, Math.max(1, Math.floor(DEGRADE_BATCH_SIZE / 2))),
|
|
693
|
+
);
|
|
694
|
+
const result = {
|
|
695
|
+
status: dryRun ? "dry_run" : "degraded",
|
|
696
|
+
tier1_to_tier2: tier1Rows.length,
|
|
697
|
+
tier2_to_tier3: tier2Rows.length,
|
|
698
|
+
};
|
|
699
|
+
if (dryRun) return result;
|
|
700
|
+
for (const row of tier1Rows) {
|
|
701
|
+
const id = rowValue(row, "id");
|
|
702
|
+
const content = rowValue(row, "content") ?? "";
|
|
703
|
+
if (!id) continue;
|
|
704
|
+
const compressed = content.slice(0, 800);
|
|
705
|
+
beam.db.run("SAVEPOINT degrade_episodic");
|
|
706
|
+
try {
|
|
707
|
+
beam.db.run("UPDATE episodic_memory SET content = ?, tier = 2, degraded_at = ? WHERE id = ?", [
|
|
708
|
+
compressed,
|
|
709
|
+
now,
|
|
710
|
+
id,
|
|
711
|
+
]);
|
|
712
|
+
if (compressed !== content) invalidateEpisodicVectors(beam, id);
|
|
713
|
+
beam.db.run("RELEASE degrade_episodic");
|
|
714
|
+
} catch {
|
|
715
|
+
beam.db.run("ROLLBACK TO degrade_episodic");
|
|
716
|
+
beam.db.run("RELEASE degrade_episodic");
|
|
717
|
+
result.tier1_to_tier2--;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
for (const row of tier2Rows) {
|
|
721
|
+
const id = rowValue(row, "id");
|
|
722
|
+
const content = rowValue(row, "content") ?? "";
|
|
723
|
+
if (!id) continue;
|
|
724
|
+
const compressed = content.length > TIER3_MAX_CHARS ? extractKeySignal(content, TIER3_MAX_CHARS) : content;
|
|
725
|
+
beam.db.run("SAVEPOINT degrade_episodic");
|
|
726
|
+
try {
|
|
727
|
+
beam.db.run("UPDATE episodic_memory SET content = ?, tier = 3, degraded_at = ? WHERE id = ?", [
|
|
728
|
+
compressed,
|
|
729
|
+
now,
|
|
730
|
+
id,
|
|
731
|
+
]);
|
|
732
|
+
if (compressed !== content) invalidateEpisodicVectors(beam, id);
|
|
733
|
+
beam.db.run("RELEASE degrade_episodic");
|
|
734
|
+
} catch {
|
|
735
|
+
beam.db.run("ROLLBACK TO degrade_episodic");
|
|
736
|
+
beam.db.run("RELEASE degrade_episodic");
|
|
737
|
+
result.tier2_to_tier3--;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
return result;
|
|
741
|
+
}
|
|
742
|
+
export function getContaminated(beam: BeamMemoryState, limit = 50, minImportance = 0.0): Row[] {
|
|
743
|
+
const rows = asRows(
|
|
744
|
+
beam.db
|
|
745
|
+
.query(
|
|
746
|
+
`SELECT id, content, source, veracity, tier, importance, created_at, degraded_at, session_id
|
|
747
|
+
FROM episodic_memory
|
|
748
|
+
WHERE veracity IN ('inferred', 'tool', 'imported', 'unknown', 'false') AND importance >= ?
|
|
749
|
+
ORDER BY importance DESC, created_at DESC LIMIT ?`,
|
|
750
|
+
)
|
|
751
|
+
.all(minImportance, limit),
|
|
752
|
+
);
|
|
753
|
+
return rows.filter(row => CONTAMINATED_VERACITY[rowValue(row, "veracity") ?? "unknown"] === true);
|
|
754
|
+
}
|
|
755
|
+
export function health(
|
|
756
|
+
beam: BeamMemoryState,
|
|
757
|
+
staleThresholdHours = 24.0,
|
|
758
|
+
): Record<string, JsonValue | Record<string, JsonValue>> {
|
|
759
|
+
const last = beam.db
|
|
760
|
+
.query(`SELECT max(created_at) AS last_consolidation FROM consolidation_log WHERE items_consolidated > 0`)
|
|
761
|
+
.get() as { last_consolidation: string | null } | null;
|
|
762
|
+
const errors = beam.db
|
|
763
|
+
.query(
|
|
764
|
+
`SELECT count(*) AS err_count FROM consolidation_log
|
|
765
|
+
WHERE created_at > datetime('now', '-7 days')
|
|
766
|
+
AND ((items_consolidated = 0 AND summary_preview LIKE '%error%') OR summary_preview LIKE '%fail%')`,
|
|
767
|
+
)
|
|
768
|
+
.get() as { err_count: number };
|
|
769
|
+
const lastTs = last?.last_consolidation ?? null;
|
|
770
|
+
if (lastTs === null) {
|
|
771
|
+
return {
|
|
772
|
+
status: "no_data",
|
|
773
|
+
last_successful_consolidation: null,
|
|
774
|
+
error_count: errors.err_count,
|
|
775
|
+
stale_hours: null,
|
|
776
|
+
stale_threshold_hours: staleThresholdHours,
|
|
777
|
+
details: { stale: true, consolidation_log_entries_checked: "last 7 days" },
|
|
778
|
+
recommendation:
|
|
779
|
+
"No consolidation_log entries found with items_consolidated > 0. Run sleepAllSessions() or check logs.",
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
const staleHours = Math.round(((Date.now() - Date.parse(lastTs)) / 3_600_000) * 100) / 100;
|
|
783
|
+
const status = staleHours > staleThresholdHours ? "stale" : "healthy";
|
|
784
|
+
return {
|
|
785
|
+
status,
|
|
786
|
+
last_successful_consolidation: lastTs,
|
|
787
|
+
error_count: errors.err_count,
|
|
788
|
+
stale_hours: staleHours,
|
|
789
|
+
stale_threshold_hours: staleThresholdHours,
|
|
790
|
+
details: { stale: status === "stale", consolidation_log_entries_checked: "last 7 days" },
|
|
791
|
+
recommendation:
|
|
792
|
+
status === "stale"
|
|
793
|
+
? `Last successful consolidation was ${staleHours.toFixed(1)} hours ago (threshold: ${staleThresholdHours.toFixed(0)}h). Run sleepAllSessions().`
|
|
794
|
+
: "Consolidation is within the healthy window.",
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function eligibleWorkingRows(beam: BeamMemoryState, sessionId: string): Row[] {
|
|
799
|
+
const ttl = beam.config?.workingMemoryTtlHours ?? 24;
|
|
800
|
+
const cutoff = cutoffIso(Math.floor(ttl / 2), 60 * 60 * 1000);
|
|
801
|
+
return asRows(
|
|
802
|
+
beam.db
|
|
803
|
+
.query(
|
|
804
|
+
`SELECT id, content, source, timestamp, importance, metadata_json, scope, valid_until, veracity
|
|
805
|
+
FROM working_memory
|
|
806
|
+
WHERE COALESCE(session_id, 'default') = ? AND timestamp < ? AND consolidated_at IS NULL
|
|
807
|
+
ORDER BY timestamp ASC LIMIT ?`,
|
|
808
|
+
)
|
|
809
|
+
.all(sessionId, cutoff, SLEEP_BATCH_SIZE),
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
export function sleep(beam: BeamMemoryState, dryRun = false): SleepResult {
|
|
814
|
+
let rows = eligibleWorkingRows(beam, sourceSession(beam));
|
|
815
|
+
if (rows.length === 0)
|
|
816
|
+
return { dry_run: dryRun, status: "no_op", message: "No old working memories to consolidate" };
|
|
817
|
+
if (!dryRun) {
|
|
818
|
+
const claimTs = isoNow();
|
|
819
|
+
const ids = rows.map(row => rowValue(row, "id")).filter((id): id is string => id !== null);
|
|
820
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
821
|
+
beam.db.run(
|
|
822
|
+
`UPDATE working_memory SET consolidated_at = ? WHERE id IN (${placeholders}) AND consolidated_at IS NULL`,
|
|
823
|
+
[claimTs, ...ids],
|
|
824
|
+
);
|
|
825
|
+
const claimed = new Set(
|
|
826
|
+
asRows(
|
|
827
|
+
beam.db
|
|
828
|
+
.query(`SELECT id FROM working_memory WHERE id IN (${placeholders}) AND consolidated_at = ?`)
|
|
829
|
+
.all(...ids, claimTs),
|
|
830
|
+
).map(row => rowValue(row, "id")),
|
|
831
|
+
);
|
|
832
|
+
if (claimed.size === 0)
|
|
833
|
+
return {
|
|
834
|
+
dry_run: false,
|
|
835
|
+
status: "no_op",
|
|
836
|
+
message: "All eligible rows claimed by concurrent sleep",
|
|
837
|
+
};
|
|
838
|
+
rows = rows.filter(row => claimed.has(rowValue(row, "id")));
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const grouped = new Map<string, Row[]>();
|
|
842
|
+
for (const row of rows) {
|
|
843
|
+
const source = rowValue(row, "source") ?? "unknown";
|
|
844
|
+
const group = grouped.get(source);
|
|
845
|
+
if (group) group.push(row);
|
|
846
|
+
else grouped.set(source, [row]);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
const consolidatedIds: string[] = [];
|
|
850
|
+
let summariesCreated = 0;
|
|
851
|
+
for (const [source, items] of grouped) {
|
|
852
|
+
const lines = items.map(item => rowValue(item, "content") ?? "");
|
|
853
|
+
const ids = items.map(item => rowValue(item, "id")).filter((id): id is string => id !== null);
|
|
854
|
+
let scope = "session";
|
|
855
|
+
let validUntil: string | null = null;
|
|
856
|
+
for (const item of items) {
|
|
857
|
+
if (rowValue(item, "scope") === "global") scope = "global";
|
|
858
|
+
const itemValidUntil = rowValue(item, "valid_until");
|
|
859
|
+
if (itemValidUntil && (validUntil === null || itemValidUntil < validUntil)) validUntil = itemValidUntil;
|
|
860
|
+
}
|
|
861
|
+
const summary = `[${source}] ${aaakEncode(lines.join(" | "))}`;
|
|
862
|
+
if (!dryRun) {
|
|
863
|
+
consolidateToEpisodic(beam, summary, ids, "sleep_consolidation", 0.6, {
|
|
864
|
+
scope,
|
|
865
|
+
validUntil,
|
|
866
|
+
veracity: aggregateEpisodicVeracity(items.map(item => rowValue(item, "veracity") ?? "unknown")),
|
|
867
|
+
metadata: { original_count: items.length, source, llm_used: false },
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
consolidatedIds.push(...ids);
|
|
871
|
+
summariesCreated++;
|
|
872
|
+
}
|
|
873
|
+
if (!dryRun) {
|
|
874
|
+
beam.db.run(
|
|
875
|
+
`INSERT INTO consolidation_log (session_id, items_consolidated, summary_preview, created_at) VALUES (?, ?, ?, ?)`,
|
|
876
|
+
[
|
|
877
|
+
sourceSession(beam),
|
|
878
|
+
consolidatedIds.length,
|
|
879
|
+
`${summariesCreated} summaries (aaak) from ${consolidatedIds.length} items`,
|
|
880
|
+
isoNow(),
|
|
881
|
+
],
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
const degradation = degradeEpisodic(beam, dryRun);
|
|
885
|
+
return {
|
|
886
|
+
dry_run: dryRun,
|
|
887
|
+
status: dryRun ? "dry_run" : "consolidated",
|
|
888
|
+
items_consolidated: consolidatedIds.length,
|
|
889
|
+
summaries_created: summariesCreated,
|
|
890
|
+
conflicts_resolved: 0,
|
|
891
|
+
llm_used: 0,
|
|
892
|
+
method: "aaak",
|
|
893
|
+
consolidated_ids: consolidatedIds,
|
|
894
|
+
degradation,
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
export function sleepAllSessions(beam: BeamMemoryState, dryRun = false): SleepResult {
|
|
899
|
+
const ttl = beam.config?.workingMemoryTtlHours ?? 24;
|
|
900
|
+
const cutoff = cutoffIso(Math.floor(ttl / 2), 60 * 60 * 1000);
|
|
901
|
+
const sessions = asRows(
|
|
902
|
+
beam.db
|
|
903
|
+
.query(
|
|
904
|
+
`SELECT session_id, COUNT(*) AS eligible FROM working_memory
|
|
905
|
+
WHERE timestamp < ? AND consolidated_at IS NULL GROUP BY session_id ORDER BY MIN(timestamp) ASC`,
|
|
906
|
+
)
|
|
907
|
+
.all(cutoff),
|
|
908
|
+
);
|
|
909
|
+
if (sessions.length === 0) {
|
|
910
|
+
return {
|
|
911
|
+
dry_run: dryRun,
|
|
912
|
+
status: "no_op",
|
|
913
|
+
message: "No old working memories to consolidate",
|
|
914
|
+
sessions_scanned: 0,
|
|
915
|
+
sessions_consolidated: 0,
|
|
916
|
+
items_consolidated: 0,
|
|
917
|
+
summaries_created: 0,
|
|
918
|
+
llm_used: 0,
|
|
919
|
+
errors: 0,
|
|
920
|
+
session_results: [],
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
const originalSession = beam.sessionId;
|
|
924
|
+
const results: Row[] = [];
|
|
925
|
+
let items = 0;
|
|
926
|
+
let summaries = 0;
|
|
927
|
+
let consolidated = 0;
|
|
928
|
+
for (const row of sessions) {
|
|
929
|
+
const sessionId = rowValue(row, "session_id") ?? "default";
|
|
930
|
+
const scoped = Object.create(Object.getPrototypeOf(beam)) as BeamMemoryState;
|
|
931
|
+
Object.assign(scoped, beam, { sessionId, channelId: sessionId });
|
|
932
|
+
const result = sleep(scoped, dryRun) as Row;
|
|
933
|
+
result.session_id = sessionId;
|
|
934
|
+
result.eligible = row.eligible;
|
|
935
|
+
results.push(result);
|
|
936
|
+
if (result.status === "consolidated" || result.status === "dry_run") consolidated++;
|
|
937
|
+
items += Number(result.items_consolidated ?? 0);
|
|
938
|
+
summaries += Number(result.summaries_created ?? 0);
|
|
939
|
+
}
|
|
940
|
+
const degradation = degradeEpisodic(beam, dryRun);
|
|
941
|
+
return {
|
|
942
|
+
dry_run: dryRun,
|
|
943
|
+
status: dryRun ? "dry_run" : items > 0 ? "consolidated" : "no_op",
|
|
944
|
+
sessions_scanned: sessions.length,
|
|
945
|
+
sessions_consolidated: consolidated,
|
|
946
|
+
items_consolidated: items,
|
|
947
|
+
summaries_created: summaries,
|
|
948
|
+
llm_used: 0,
|
|
949
|
+
errors: 0,
|
|
950
|
+
error_details: [],
|
|
951
|
+
session_results: results,
|
|
952
|
+
degradation,
|
|
953
|
+
original_session: originalSession,
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
export function getConsolidationLog(beam: BeamMemoryState, limit = 10): Row[] {
|
|
957
|
+
return asRows(
|
|
958
|
+
beam.db
|
|
959
|
+
.query(
|
|
960
|
+
`SELECT id, session_id, items_consolidated, summary_preview, created_at
|
|
961
|
+
FROM consolidation_log WHERE session_id = ? ORDER BY created_at DESC LIMIT ?`,
|
|
962
|
+
)
|
|
963
|
+
.all(sourceSession(beam), limit),
|
|
964
|
+
);
|
|
965
|
+
}
|