@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,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
|
+
}
|