@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.
Files changed (128) hide show
  1. package/README.md +107 -0
  2. package/dist/types/cli.d.ts +35 -0
  3. package/dist/types/config.d.ts +77 -0
  4. package/dist/types/core/aaak.d.ts +55 -0
  5. package/dist/types/core/annotations.d.ts +75 -0
  6. package/dist/types/core/banks.d.ts +33 -0
  7. package/dist/types/core/beam/consolidate.d.ts +32 -0
  8. package/dist/types/core/beam/helpers.d.ts +76 -0
  9. package/dist/types/core/beam/index.d.ts +59 -0
  10. package/dist/types/core/beam/recall.d.ts +32 -0
  11. package/dist/types/core/beam/schema.d.ts +2 -0
  12. package/dist/types/core/beam/store.d.ts +35 -0
  13. package/dist/types/core/beam/types.d.ts +233 -0
  14. package/dist/types/core/binary-vectors.d.ts +54 -0
  15. package/dist/types/core/chat-normalize.d.ts +13 -0
  16. package/dist/types/core/content-sanitizer.d.ts +18 -0
  17. package/dist/types/core/cost-log.d.ts +13 -0
  18. package/dist/types/core/embeddings.d.ts +44 -0
  19. package/dist/types/core/entities.d.ts +7 -0
  20. package/dist/types/core/episodic-graph.d.ts +89 -0
  21. package/dist/types/core/extraction/client.d.ts +31 -0
  22. package/dist/types/core/extraction/diagnostics.d.ts +51 -0
  23. package/dist/types/core/extraction/prompts.d.ts +2 -0
  24. package/dist/types/core/extraction.d.ts +6 -0
  25. package/dist/types/core/index.d.ts +4 -0
  26. package/dist/types/core/llm-backends.d.ts +21 -0
  27. package/dist/types/core/local-llm.d.ts +15 -0
  28. package/dist/types/core/memory.d.ts +160 -0
  29. package/dist/types/core/migrations/e6-triplestore-split.d.ts +17 -0
  30. package/dist/types/core/migrations/index.d.ts +1 -0
  31. package/dist/types/core/mmr.d.ts +8 -0
  32. package/dist/types/core/orchestrator.d.ts +20 -0
  33. package/dist/types/core/patterns.d.ts +61 -0
  34. package/dist/types/core/plugins.d.ts +109 -0
  35. package/dist/types/core/polyphonic-recall.d.ts +66 -0
  36. package/dist/types/core/query-cache.d.ts +46 -0
  37. package/dist/types/core/query-intent.d.ts +20 -0
  38. package/dist/types/core/recall-diagnostics.d.ts +48 -0
  39. package/dist/types/core/runtime-options.d.ts +68 -0
  40. package/dist/types/core/shmr.d.ts +56 -0
  41. package/dist/types/core/streaming.d.ts +136 -0
  42. package/dist/types/core/synonyms.d.ts +46 -0
  43. package/dist/types/core/temporal-parser.d.ts +16 -0
  44. package/dist/types/core/token-counter.d.ts +8 -0
  45. package/dist/types/core/triples.d.ts +63 -0
  46. package/dist/types/core/typed-memory.d.ts +39 -0
  47. package/dist/types/core/vector-math.d.ts +1 -0
  48. package/dist/types/core/veracity-consolidation.d.ts +60 -0
  49. package/dist/types/core/weibull.d.ts +96 -0
  50. package/dist/types/db.d.ts +16 -0
  51. package/dist/types/diagnose.d.ts +24 -0
  52. package/dist/types/dr/index.d.ts +1 -0
  53. package/dist/types/dr/recovery.d.ts +68 -0
  54. package/dist/types/index.d.ts +5 -0
  55. package/dist/types/mcp-server.d.ts +40 -0
  56. package/dist/types/mcp-tools.d.ts +484 -0
  57. package/dist/types/migrations/e6-triplestore-split.d.ts +1 -0
  58. package/dist/types/migrations/index.d.ts +1 -0
  59. package/dist/types/types.d.ts +145 -0
  60. package/dist/types/util/datetime.d.ts +8 -0
  61. package/dist/types/util/env.d.ts +10 -0
  62. package/dist/types/util/ids.d.ts +3 -0
  63. package/dist/types/util/lru.d.ts +12 -0
  64. package/dist/types/util/regex.d.ts +10 -0
  65. package/package.json +85 -0
  66. package/src/cli.ts +398 -0
  67. package/src/config.ts +326 -0
  68. package/src/core/aaak.ts +142 -0
  69. package/src/core/annotations.ts +457 -0
  70. package/src/core/banks.ts +133 -0
  71. package/src/core/beam/consolidate.ts +965 -0
  72. package/src/core/beam/helpers.ts +977 -0
  73. package/src/core/beam/index.ts +353 -0
  74. package/src/core/beam/recall.ts +1100 -0
  75. package/src/core/beam/schema.ts +423 -0
  76. package/src/core/beam/store.ts +829 -0
  77. package/src/core/beam/types.ts +268 -0
  78. package/src/core/binary-vectors.ts +317 -0
  79. package/src/core/chat-normalize.ts +160 -0
  80. package/src/core/content-sanitizer.ts +136 -0
  81. package/src/core/cost-log.ts +103 -0
  82. package/src/core/embeddings.ts +423 -0
  83. package/src/core/entities.ts +259 -0
  84. package/src/core/episodic-graph.ts +708 -0
  85. package/src/core/extraction/client.ts +162 -0
  86. package/src/core/extraction/diagnostics.ts +193 -0
  87. package/src/core/extraction/prompts.ts +31 -0
  88. package/src/core/extraction.ts +335 -0
  89. package/src/core/index.ts +30 -0
  90. package/src/core/llm-backends.ts +51 -0
  91. package/src/core/local-llm.ts +436 -0
  92. package/src/core/memory.ts +630 -0
  93. package/src/core/migrations/e6-triplestore-split.ts +211 -0
  94. package/src/core/migrations/index.ts +1 -0
  95. package/src/core/mmr.ts +71 -0
  96. package/src/core/orchestrator.ts +62 -0
  97. package/src/core/patterns.ts +484 -0
  98. package/src/core/plugins.ts +375 -0
  99. package/src/core/polyphonic-recall.ts +563 -0
  100. package/src/core/query-cache.ts +354 -0
  101. package/src/core/query-intent.ts +139 -0
  102. package/src/core/recall-diagnostics.ts +157 -0
  103. package/src/core/runtime-options.ts +119 -0
  104. package/src/core/shmr.ts +460 -0
  105. package/src/core/streaming.ts +419 -0
  106. package/src/core/synonyms.ts +197 -0
  107. package/src/core/temporal-parser.ts +363 -0
  108. package/src/core/token-counter.ts +30 -0
  109. package/src/core/triples.ts +454 -0
  110. package/src/core/typed-memory.ts +407 -0
  111. package/src/core/vector-math.ts +23 -0
  112. package/src/core/veracity-consolidation.ts +477 -0
  113. package/src/core/weibull.ts +124 -0
  114. package/src/db.ts +128 -0
  115. package/src/diagnose.ts +174 -0
  116. package/src/dr/index.ts +1 -0
  117. package/src/dr/recovery.ts +405 -0
  118. package/src/index.ts +33 -0
  119. package/src/mcp-server.ts +155 -0
  120. package/src/mcp-tools.ts +970 -0
  121. package/src/migrations/e6-triplestore-split.ts +1 -0
  122. package/src/migrations/index.ts +1 -0
  123. package/src/types.ts +157 -0
  124. package/src/util/datetime.ts +69 -0
  125. package/src/util/env.ts +65 -0
  126. package/src/util/ids.ts +19 -0
  127. package/src/util/lru.ts +48 -0
  128. package/src/util/regex.ts +165 -0
@@ -0,0 +1,477 @@
1
+ import type { Database } from "bun:sqlite";
2
+ import { createHash } from "node:crypto";
3
+ import { type DatabasePath, openDatabase } from "../db";
4
+
5
+ export const VERACITY_WEIGHTS = Object.freeze({
6
+ stated: 1.0,
7
+ inferred: 0.7,
8
+ tool: 0.5,
9
+ imported: 0.6,
10
+ unknown: 0.8,
11
+ });
12
+
13
+ export type Veracity = keyof typeof VERACITY_WEIGHTS;
14
+
15
+ export const VERACITY_ALLOWED: Record<Veracity, true> = Object.freeze({
16
+ stated: true,
17
+ inferred: true,
18
+ tool: true,
19
+ imported: true,
20
+ unknown: true,
21
+ });
22
+
23
+ const VERACITY_WARN_VALUE_CAP = 80;
24
+ const TX_DEPTH = Symbol("mnemopi.veracity.txDepth");
25
+
26
+ type TxDatabase = Database & {
27
+ readonly inTransaction?: boolean;
28
+ readonly in_transaction?: boolean;
29
+ [TX_DEPTH]?: number;
30
+ };
31
+
32
+ export interface ConsolidatedFact {
33
+ readonly subject: string;
34
+ readonly predicate: string;
35
+ readonly object: string;
36
+ readonly confidence: number;
37
+ readonly mention_count: number;
38
+ readonly first_seen: string | null;
39
+ readonly last_seen: string | null;
40
+ readonly sources: string[];
41
+ readonly veracity: string;
42
+ readonly superseded: boolean;
43
+ readonly id: string | null;
44
+ }
45
+
46
+ interface ConsolidatedFactRow {
47
+ readonly id: string;
48
+ readonly subject: string;
49
+ readonly predicate: string;
50
+ readonly object: string;
51
+ readonly confidence: number;
52
+ readonly mention_count: number;
53
+ readonly first_seen: string | null;
54
+ readonly last_seen: string | null;
55
+ readonly sources_json: string | null;
56
+ readonly veracity: string;
57
+ readonly superseded_by: string | null;
58
+ }
59
+
60
+ interface ConflictRow {
61
+ readonly id: number;
62
+ readonly fact_a_id: string;
63
+ readonly fact_b_id: string;
64
+ readonly conflict_type: string | null;
65
+ readonly resolution: string | null;
66
+ readonly resolved_at: string | null;
67
+ readonly created_at: string | null;
68
+ }
69
+
70
+ export interface Conflict {
71
+ readonly id: number;
72
+ readonly fact_a_id: string;
73
+ readonly fact_b_id: string;
74
+ readonly type: string | null;
75
+ readonly created_at: string | null;
76
+ }
77
+
78
+ export interface ConsolidationStats {
79
+ readonly active_facts: number;
80
+ readonly superseded_facts: number;
81
+ readonly unresolved_conflicts: number;
82
+ readonly avg_confidence: number;
83
+ readonly avg_mentions: number;
84
+ }
85
+
86
+ function isVeracity(value: string): value is Veracity {
87
+ return Object.hasOwn(VERACITY_ALLOWED, value);
88
+ }
89
+
90
+ function sqliteInTransaction(db: Database): boolean {
91
+ const txDb = db as TxDatabase;
92
+ return txDb.inTransaction === true || txDb.in_transaction === true || (txDb[TX_DEPTH] ?? 0) > 0;
93
+ }
94
+
95
+ function parseSources(raw: string | null): string[] {
96
+ if (raw === null || raw === "") return [];
97
+ try {
98
+ const parsed: unknown = JSON.parse(raw);
99
+ if (!Array.isArray(parsed)) return [];
100
+ const out: string[] = [];
101
+ for (const item of parsed) {
102
+ if (typeof item === "string") out.push(item);
103
+ }
104
+ return out;
105
+ } catch {
106
+ return [];
107
+ }
108
+ }
109
+
110
+ function nowIso(): string {
111
+ return new Date().toISOString();
112
+ }
113
+
114
+ export function computeFactId(subject: string, predicate: string, object: string): string {
115
+ for (const [name, value] of [
116
+ ["subject", subject],
117
+ ["predicate", predicate],
118
+ ["object", object],
119
+ ] as const) {
120
+ if (typeof value !== "string") {
121
+ throw new TypeError(`compute_fact_id: ${name} must be a str, got ${typeof value}`);
122
+ }
123
+ if (value === "") throw new RangeError(`compute_fact_id: ${name} must be non-empty`);
124
+ }
125
+
126
+ const chunks: Buffer[] = [];
127
+ for (const value of [subject, predicate, object]) {
128
+ const bytes = Buffer.from(value.normalize("NFC"), "utf8");
129
+ chunks.push(Buffer.from(`${bytes.length}:`, "ascii"), bytes);
130
+ }
131
+ return `cf_${createHash("sha256").update(Buffer.concat(chunks)).digest("hex").slice(0, 24)}`;
132
+ }
133
+ export function clampVeracity(raw: unknown, context = "veracity"): Veracity {
134
+ if (raw === null || raw === undefined) return "unknown";
135
+ const norm = String(raw).trim().toLowerCase();
136
+ if (norm === "") return "unknown";
137
+ if (isVeracity(norm)) return norm;
138
+ const rawString = String(raw);
139
+ const rawForLog =
140
+ rawString.length > VERACITY_WARN_VALUE_CAP
141
+ ? `${rawString.slice(0, VERACITY_WARN_VALUE_CAP)}...[truncated]`
142
+ : rawString;
143
+ console.warn(`${context} received unknown veracity ${JSON.stringify(rawForLog)}; clamping to 'unknown'`);
144
+ return "unknown";
145
+ }
146
+ export function aggregateVeracity(sourceVeracities: readonly string[] | null | undefined): Veracity {
147
+ if (sourceVeracities === null || sourceVeracities === undefined || sourceVeracities.length === 0) return "unknown";
148
+ const valid = sourceVeracities.filter(isVeracity);
149
+ if (valid.length === 0) return "unknown";
150
+ const nonUnknown = valid.filter(v => v !== "unknown");
151
+ const candidates = nonUnknown.length === 0 ? valid : nonUnknown;
152
+ const counts = new Map<Veracity, number>();
153
+ for (const value of candidates) counts.set(value, (counts.get(value) ?? 0) + 1);
154
+ let max = 0;
155
+ for (const count of counts.values()) if (count > max) max = count;
156
+ let winner: Veracity | null = null;
157
+ for (const [value, count] of counts) {
158
+ if (count !== max) continue;
159
+ if (winner === null || VERACITY_WEIGHTS[value] < VERACITY_WEIGHTS[winner]) winner = value;
160
+ }
161
+ return winner ?? "unknown";
162
+ }
163
+ export class VeracityConsolidator {
164
+ readonly conn: Database;
165
+ readonly dbPath: DatabasePath;
166
+ readonly ownsConnection: boolean;
167
+
168
+ constructor(dbPath: DatabasePath = ":memory:", conn?: Database) {
169
+ this.dbPath = dbPath;
170
+ this.conn = conn ?? openDatabase(dbPath, { create: true, readwrite: true, strict: true, pragmas: true });
171
+ this.ownsConnection = conn === undefined;
172
+ this.initTables();
173
+ }
174
+
175
+ initTables(): void {
176
+ this.conn.run(`
177
+ CREATE TABLE IF NOT EXISTS consolidated_facts (
178
+ id TEXT PRIMARY KEY,
179
+ subject TEXT NOT NULL,
180
+ predicate TEXT NOT NULL,
181
+ object TEXT NOT NULL,
182
+ confidence REAL DEFAULT 0.5,
183
+ mention_count INTEGER DEFAULT 1,
184
+ first_seen TEXT,
185
+ last_seen TEXT,
186
+ sources_json TEXT,
187
+ veracity TEXT DEFAULT 'unknown',
188
+ superseded_by TEXT,
189
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
190
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
191
+ )
192
+ `);
193
+ this.conn.run("CREATE INDEX IF NOT EXISTS idx_cf_subject ON consolidated_facts(subject)");
194
+ this.conn.run("CREATE INDEX IF NOT EXISTS idx_cf_predicate ON consolidated_facts(predicate)");
195
+ this.conn.run("CREATE INDEX IF NOT EXISTS idx_cf_object ON consolidated_facts(object)");
196
+ this.conn.run(`
197
+ CREATE TABLE IF NOT EXISTS conflicts (
198
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
199
+ fact_a_id TEXT NOT NULL,
200
+ fact_b_id TEXT NOT NULL,
201
+ conflict_type TEXT,
202
+ resolution TEXT,
203
+ resolved_at TEXT,
204
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
205
+ )
206
+ `);
207
+ }
208
+
209
+ serializedWrite<T>(body: () => T): T {
210
+ const conn = this.conn;
211
+ if (sqliteInTransaction(conn)) return body();
212
+
213
+ let started = false;
214
+ try {
215
+ conn.exec("BEGIN IMMEDIATE");
216
+ started = true;
217
+ (conn as TxDatabase)[TX_DEPTH] = ((conn as TxDatabase)[TX_DEPTH] ?? 0) + 1;
218
+ const result = body();
219
+ conn.exec("COMMIT");
220
+ return result;
221
+ } catch (error) {
222
+ if (
223
+ !started &&
224
+ error instanceof Error &&
225
+ /within a transaction|transaction.*active|cannot start/i.test(error.message)
226
+ ) {
227
+ return body();
228
+ }
229
+ if (started) {
230
+ try {
231
+ conn.exec("ROLLBACK");
232
+ } catch {
233
+ // Preserve original error.
234
+ }
235
+ }
236
+ throw error;
237
+ } finally {
238
+ if (started) {
239
+ const txDb = conn as TxDatabase;
240
+ const depth = (txDb[TX_DEPTH] ?? 1) - 1;
241
+ if (depth > 0) txDb[TX_DEPTH] = depth;
242
+ else delete txDb[TX_DEPTH];
243
+ }
244
+ }
245
+ }
246
+
247
+ bayesianUpdate(currentConfidence: number, veracity: string): number {
248
+ const weight = isVeracity(veracity) ? VERACITY_WEIGHTS[veracity] : VERACITY_WEIGHTS.unknown;
249
+ const increment = (1.0 - currentConfidence) * weight * 0.3;
250
+ return Math.min(currentConfidence + increment, 1.0);
251
+ }
252
+
253
+ consolidateFact(
254
+ subject: string,
255
+ predicate: string,
256
+ object: string,
257
+ veracity = "unknown",
258
+ source?: string | null,
259
+ ): ConsolidatedFact {
260
+ return this.serializedWrite(() => {
261
+ const existing = this.conn
262
+ .query("SELECT * FROM consolidated_facts WHERE subject = ? AND predicate = ? AND object = ?")
263
+ .get(subject, predicate, object) as ConsolidatedFactRow | null;
264
+ const now = nowIso();
265
+
266
+ if (existing !== null) {
267
+ const newConfidence = this.bayesianUpdate(existing.confidence, veracity);
268
+ const newCount = existing.mention_count + 1;
269
+ const sources = parseSources(existing.sources_json);
270
+ if (source !== undefined && source !== null && source !== "" && !sources.includes(source))
271
+ sources.push(source);
272
+ this.conn
273
+ .query(`
274
+ UPDATE consolidated_facts
275
+ SET confidence = ?, mention_count = ?, last_seen = ?, sources_json = ?, veracity = ?, updated_at = ?
276
+ WHERE id = ?
277
+ `)
278
+ .run(newConfidence, newCount, now, JSON.stringify(sources), veracity, now, existing.id);
279
+ return {
280
+ subject,
281
+ predicate,
282
+ object,
283
+ confidence: newConfidence,
284
+ mention_count: newCount,
285
+ first_seen: existing.first_seen,
286
+ last_seen: now,
287
+ sources,
288
+ veracity,
289
+ superseded: existing.superseded_by !== null,
290
+ id: existing.id,
291
+ };
292
+ }
293
+
294
+ const conflicts = this.conn
295
+ .query("SELECT * FROM consolidated_facts WHERE subject = ? AND predicate = ? AND object != ?")
296
+ .all(subject, predicate, object) as ConsolidatedFactRow[];
297
+ const factId = computeFactId(subject, predicate, object);
298
+ const weight = isVeracity(veracity) ? VERACITY_WEIGHTS[veracity] : VERACITY_WEIGHTS.unknown;
299
+ const baseConfidence = weight * 0.5;
300
+ const sources = source !== undefined && source !== null && source !== "" ? [source] : [];
301
+ this.conn
302
+ .query(`
303
+ INSERT INTO consolidated_facts
304
+ (id, subject, predicate, object, confidence, mention_count, first_seen, last_seen, sources_json, veracity)
305
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
306
+ `)
307
+ .run(factId, subject, predicate, object, baseConfidence, 1, now, now, JSON.stringify(sources), veracity);
308
+ for (const conflict of conflicts) this.recordConflict(factId, conflict.id, "contradiction", false);
309
+ return {
310
+ subject,
311
+ predicate,
312
+ object,
313
+ confidence: baseConfidence,
314
+ mention_count: 1,
315
+ first_seen: now,
316
+ last_seen: now,
317
+ sources,
318
+ veracity,
319
+ superseded: false,
320
+ id: factId,
321
+ };
322
+ });
323
+ }
324
+
325
+ recordConflict(factAId: string, factBId: string, conflictType: string, commit = true): void {
326
+ this.conn
327
+ .query("INSERT INTO conflicts (fact_a_id, fact_b_id, conflict_type) VALUES (?, ?, ?)")
328
+ .run(factAId, factBId, conflictType);
329
+ void commit;
330
+ }
331
+
332
+ resolveConflict(conflictId: number, winningFactId: string): void {
333
+ this.serializedWrite(() => {
334
+ const conflict = this.conn.query("SELECT * FROM conflicts WHERE id = ?").get(conflictId) as ConflictRow | null;
335
+ if (conflict === null) return;
336
+ if (conflict.resolution !== null) {
337
+ console.warn(
338
+ `resolve_conflict: conflict ${conflictId} already resolved (resolution=${JSON.stringify(conflict.resolution)}); ignoring re-resolution attempt with winning_fact_id=${JSON.stringify(winningFactId)}`,
339
+ );
340
+ return;
341
+ }
342
+ let losingId: string;
343
+ if (winningFactId === conflict.fact_a_id) losingId = conflict.fact_b_id;
344
+ else if (winningFactId === conflict.fact_b_id) losingId = conflict.fact_a_id;
345
+ else {
346
+ console.warn(
347
+ `resolve_conflict: winning_fact_id ${JSON.stringify(winningFactId)} matches neither fact_a_id ${JSON.stringify(conflict.fact_a_id)} nor fact_b_id ${JSON.stringify(conflict.fact_b_id)}; declining to resolve`,
348
+ );
349
+ return;
350
+ }
351
+ const now = nowIso();
352
+ this.conn
353
+ .query("UPDATE consolidated_facts SET superseded_by = ?, updated_at = ? WHERE id = ?")
354
+ .run(winningFactId, now, losingId);
355
+ this.conn
356
+ .query("UPDATE conflicts SET resolution = ?, resolved_at = ? WHERE id = ?")
357
+ .run(`superseded_by_${winningFactId}`, now, conflictId);
358
+ });
359
+ }
360
+
361
+ getConflicts(): Conflict[] {
362
+ const rows = this.conn
363
+ .query("SELECT * FROM conflicts WHERE resolution IS NULL ORDER BY created_at DESC")
364
+ .all() as ConflictRow[];
365
+ return rows.map(row => ({
366
+ id: row.id,
367
+ fact_a_id: row.fact_a_id,
368
+ fact_b_id: row.fact_b_id,
369
+ type: row.conflict_type,
370
+ created_at: row.created_at,
371
+ }));
372
+ }
373
+
374
+ getConsolidatedFacts(subject?: string | null, minConfidence = 0.5): ConsolidatedFact[] {
375
+ const rows =
376
+ subject !== undefined && subject !== null
377
+ ? (this.conn
378
+ .query(`
379
+ SELECT * FROM consolidated_facts
380
+ WHERE subject = ? AND confidence >= ? AND superseded_by IS NULL
381
+ ORDER BY confidence DESC, mention_count DESC
382
+ `)
383
+ .all(subject, minConfidence) as ConsolidatedFactRow[])
384
+ : (this.conn
385
+ .query(`
386
+ SELECT * FROM consolidated_facts
387
+ WHERE confidence >= ? AND superseded_by IS NULL
388
+ ORDER BY confidence DESC, mention_count DESC
389
+ `)
390
+ .all(minConfidence) as ConsolidatedFactRow[]);
391
+ return rows.map(row => ({
392
+ subject: row.subject,
393
+ predicate: row.predicate,
394
+ object: row.object,
395
+ confidence: row.confidence,
396
+ mention_count: row.mention_count,
397
+ first_seen: row.first_seen,
398
+ last_seen: row.last_seen,
399
+ sources: parseSources(row.sources_json),
400
+ veracity: row.veracity,
401
+ superseded: row.superseded_by !== null,
402
+ id: row.id,
403
+ }));
404
+ }
405
+
406
+ getHighConfidenceSummary(subject: string, threshold = 0.8): string {
407
+ const facts = this.getConsolidatedFacts(subject, threshold);
408
+ if (facts.length === 0) return `No high-confidence facts about ${subject}.`;
409
+ const lines = [`High-confidence facts about ${subject}:`];
410
+ for (const fact of facts) {
411
+ lines.push(
412
+ ` - ${fact.subject} ${fact.predicate} ${fact.object} (conf: ${fact.confidence.toFixed(2)}, mentions: ${fact.mention_count})`,
413
+ );
414
+ }
415
+ return lines.join("\n");
416
+ }
417
+
418
+ runConsolidationPass(): void {
419
+ this.serializedWrite(() => {
420
+ const primaryRows = this.conn
421
+ .query(`
422
+ SELECT * FROM consolidated_facts
423
+ WHERE mention_count > 2 AND superseded_by IS NULL
424
+ ORDER BY mention_count DESC
425
+ `)
426
+ .all() as ConsolidatedFactRow[];
427
+ for (const row of primaryRows) {
428
+ const conflicts = this.conn
429
+ .query(`
430
+ SELECT * FROM consolidated_facts
431
+ WHERE subject = ? AND predicate = ? AND object != ? AND superseded_by IS NULL
432
+ `)
433
+ .all(row.subject, row.predicate, row.object) as ConsolidatedFactRow[];
434
+ for (const conflict of conflicts) {
435
+ if (row.confidence > conflict.confidence) this.resolveConflictByFacts(row.id, conflict.id);
436
+ }
437
+ }
438
+ });
439
+ }
440
+
441
+ resolveConflictByFacts(winningId: string, losingId: string): void {
442
+ this.serializedWrite(() => {
443
+ this.conn
444
+ .query("UPDATE consolidated_facts SET superseded_by = ?, updated_at = ? WHERE id = ?")
445
+ .run(winningId, nowIso(), losingId);
446
+ });
447
+ }
448
+
449
+ getStats(): ConsolidationStats {
450
+ const active = this.conn
451
+ .query("SELECT COUNT(*) AS count FROM consolidated_facts WHERE superseded_by IS NULL")
452
+ .get() as { count: number };
453
+ const superseded = this.conn
454
+ .query("SELECT COUNT(*) AS count FROM consolidated_facts WHERE superseded_by IS NOT NULL")
455
+ .get() as { count: number };
456
+ const unresolved = this.conn.query("SELECT COUNT(*) AS count FROM conflicts WHERE resolution IS NULL").get() as {
457
+ count: number;
458
+ };
459
+ const avgConfidence = this.conn
460
+ .query("SELECT AVG(confidence) AS avg FROM consolidated_facts WHERE superseded_by IS NULL")
461
+ .get() as { avg: number | null };
462
+ const avgMentions = this.conn
463
+ .query("SELECT AVG(mention_count) AS avg FROM consolidated_facts WHERE superseded_by IS NULL")
464
+ .get() as { avg: number | null };
465
+ return {
466
+ active_facts: active.count,
467
+ superseded_facts: superseded.count,
468
+ unresolved_conflicts: unresolved.count,
469
+ avg_confidence: Math.round((avgConfidence.avg ?? 0) * 1000) / 1000,
470
+ avg_mentions: Math.round((avgMentions.avg ?? 0) * 100) / 100,
471
+ };
472
+ }
473
+
474
+ close(): void {
475
+ if (this.ownsConnection) this.conn.close();
476
+ }
477
+ }
@@ -0,0 +1,124 @@
1
+ export type MemoryType = keyof typeof WEIBULL_PARAMS;
2
+
3
+ export interface WeibullParams {
4
+ readonly k: number;
5
+ readonly eta: number;
6
+ }
7
+
8
+ // Per-memory-type Weibull parameters (k=shape, eta=scale in hours).
9
+ // Higher eta = slower decay, lower k = more long-term retention.
10
+ export const WEIBULL_PARAMS = {
11
+ profile: { k: 0.3, eta: 8760.0 },
12
+ preference: { k: 0.4, eta: 4380.0 },
13
+ relationship: { k: 0.35, eta: 8760.0 },
14
+ learning: { k: 0.7, eta: 1440.0 },
15
+
16
+ fact: { k: 0.8, eta: 720.0 },
17
+ entity: { k: 0.5, eta: 4380.0 },
18
+ setup: { k: 0.6, eta: 2160.0 },
19
+ pattern: { k: 0.6, eta: 1680.0 },
20
+ context: { k: 0.85, eta: 360.0 },
21
+ observation: { k: 0.9, eta: 480.0 },
22
+ artifact: { k: 0.75, eta: 2160.0 },
23
+
24
+ project: { k: 0.85, eta: 1080.0 },
25
+ goal: { k: 0.9, eta: 720.0 },
26
+ decision: { k: 1.0, eta: 336.0 },
27
+ commitment: { k: 1.0, eta: 240.0 },
28
+
29
+ event: { k: 1.2, eta: 168.0 },
30
+ instruction: { k: 0.9, eta: 480.0 },
31
+ error: { k: 1.1, eta: 336.0 },
32
+ issue: { k: 1.1, eta: 336.0 },
33
+ request: { k: 1.5, eta: 72.0 },
34
+
35
+ general: { k: 1.0, eta: 168.0 },
36
+ } as const satisfies Record<string, WeibullParams>;
37
+
38
+ export const DEFAULT_HALFLIFE_HOURS = 168.0;
39
+
40
+ type TimestampInput = string | Date | null | undefined;
41
+ function capture(match: RegExpExecArray, index: number): string {
42
+ return match[index] ?? "";
43
+ }
44
+
45
+ function parseTimestamp(timestamp: TimestampInput): Date | null {
46
+ if (timestamp == null) return null;
47
+ if (timestamp instanceof Date) {
48
+ return Number.isFinite(timestamp.getTime()) ? timestamp : null;
49
+ }
50
+
51
+ if (typeof timestamp !== "string") return null;
52
+
53
+ const normalized = timestamp.replace("Z", "+00:00");
54
+ const parsed = new Date(normalized);
55
+ if (Number.isFinite(parsed.getTime())) return parsed;
56
+
57
+ const truncated = normalized.slice(0, 26);
58
+ const dateOnly = /^(\d{4})-(\d{2})-(\d{2})$/.exec(truncated);
59
+ if (dateOnly !== null) {
60
+ const year = Number(capture(dateOnly, 1));
61
+ const month = Number(capture(dateOnly, 2));
62
+ const day = Number(capture(dateOnly, 3));
63
+ const date = new Date(year, month - 1, day);
64
+ return Number.isFinite(date.getTime()) ? date : null;
65
+ }
66
+
67
+ const dateTime = /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,6}))?/.exec(truncated);
68
+ if (dateTime === null) return null;
69
+
70
+ const millis = Number(capture(dateTime, 7).padEnd(3, "0").slice(0, 3));
71
+ const date = new Date(
72
+ Number(capture(dateTime, 1)),
73
+ Number(capture(dateTime, 2)) - 1,
74
+ Number(capture(dateTime, 3)),
75
+ Number(capture(dateTime, 4)),
76
+ Number(capture(dateTime, 5)),
77
+ Number(capture(dateTime, 6)),
78
+ millis,
79
+ );
80
+ return Number.isFinite(date.getTime()) ? date : null;
81
+ }
82
+
83
+ function paramsFor(memoryType: string): WeibullParams | undefined {
84
+ return WEIBULL_PARAMS[memoryType as MemoryType];
85
+ }
86
+
87
+ export function weibullBoost(
88
+ timestamp: TimestampInput,
89
+ queryTime: Date | null = new Date(),
90
+ memoryType = "general",
91
+ halflifeHours?: number | null,
92
+ ): number {
93
+ const memoryTime = parseTimestamp(timestamp);
94
+ const resolvedQueryTime = queryTime ?? new Date();
95
+ if (memoryTime === null || !Number.isFinite(resolvedQueryTime.getTime())) return 0.0;
96
+
97
+ const ageHours = (resolvedQueryTime.getTime() - memoryTime.getTime()) / 3_600_000.0;
98
+ if (ageHours < 0) return 1.0;
99
+
100
+ if (halflifeHours != null) {
101
+ if (halflifeHours <= 0) return 0.0;
102
+ return Math.exp(-ageHours / halflifeHours);
103
+ }
104
+
105
+ const params = paramsFor(memoryType);
106
+ if (params === undefined) {
107
+ return Math.exp(-ageHours / DEFAULT_HALFLIFE_HOURS);
108
+ }
109
+
110
+ if (params.eta <= 0) return 0.0;
111
+ return Math.exp(-((ageHours / params.eta) ** params.k));
112
+ }
113
+
114
+ export function weibullDecayFactor(ageHours: number, memoryType = "general"): number {
115
+ if (ageHours <= 0) return 1.0;
116
+
117
+ const params = paramsFor(memoryType);
118
+ if (params === undefined) {
119
+ return Math.exp(-ageHours / DEFAULT_HALFLIFE_HOURS);
120
+ }
121
+
122
+ if (params.eta <= 0) return 0.0;
123
+ return Math.exp(-((ageHours / params.eta) ** params.k));
124
+ }