@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,563 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
import { type Env, polyphonicRecallEnabled } from "../config";
|
|
3
|
+
import { closeQuietly, type DatabasePath, openDatabase } from "../db";
|
|
4
|
+
import type { BeamMemoryState, JsonValue, Metadata, RecallResult } from "./beam/types";
|
|
5
|
+
import { EpisodicGraph } from "./episodic-graph";
|
|
6
|
+
import { VeracityConsolidator } from "./veracity-consolidation";
|
|
7
|
+
|
|
8
|
+
export type PolyphonicVoice = "vector" | "graph" | "fact" | "temporal";
|
|
9
|
+
|
|
10
|
+
export interface VoiceRecallResult {
|
|
11
|
+
readonly memoryId: string;
|
|
12
|
+
readonly score: number;
|
|
13
|
+
readonly voice: PolyphonicVoice;
|
|
14
|
+
readonly metadata: Metadata;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface PolyphonicResult {
|
|
18
|
+
readonly memoryId: string;
|
|
19
|
+
combinedScore: number;
|
|
20
|
+
readonly voiceScores: Partial<Record<PolyphonicVoice, number>>;
|
|
21
|
+
readonly metadata: Metadata;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface PolyphonicMemoryResult extends Omit<RecallResult, "metadata" | "score" | "tier"> {
|
|
25
|
+
score: number;
|
|
26
|
+
combined_score: number;
|
|
27
|
+
voice_scores: Partial<Record<PolyphonicVoice, number>>;
|
|
28
|
+
metadata: Metadata;
|
|
29
|
+
tier: "working" | "episodic";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface PolyphonicRecallOptions {
|
|
33
|
+
readonly queryEmbedding?: readonly number[] | Float32Array | null;
|
|
34
|
+
readonly contextBudget?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface PolyphonicEngineOptions {
|
|
38
|
+
readonly dbPath?: DatabasePath;
|
|
39
|
+
readonly db?: Database;
|
|
40
|
+
readonly graph?: EpisodicGraph;
|
|
41
|
+
readonly consolidator?: VeracityConsolidator;
|
|
42
|
+
readonly sessionId?: string | null;
|
|
43
|
+
readonly channelId?: string | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface MemoryHydrationRow {
|
|
47
|
+
readonly id: string;
|
|
48
|
+
readonly content: string;
|
|
49
|
+
readonly source: string | null;
|
|
50
|
+
readonly timestamp: string | null;
|
|
51
|
+
readonly session_id: string;
|
|
52
|
+
readonly importance: number;
|
|
53
|
+
readonly metadata_json: string | null;
|
|
54
|
+
readonly veracity: string;
|
|
55
|
+
readonly memory_type: string | null;
|
|
56
|
+
readonly recall_count: number | null;
|
|
57
|
+
readonly last_recalled: string | null;
|
|
58
|
+
readonly valid_until: string | null;
|
|
59
|
+
readonly superseded_by: string | null;
|
|
60
|
+
readonly scope: string | null;
|
|
61
|
+
readonly author_id: string | null;
|
|
62
|
+
readonly author_type: string | null;
|
|
63
|
+
readonly channel_id: string | null;
|
|
64
|
+
readonly trust_tier: string | null;
|
|
65
|
+
readonly created_at: string;
|
|
66
|
+
readonly rowid?: number;
|
|
67
|
+
readonly summary_of?: string;
|
|
68
|
+
readonly tier?: number;
|
|
69
|
+
readonly tier_name: "working" | "episodic";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface EmbeddingRow {
|
|
73
|
+
readonly memory_id: string;
|
|
74
|
+
readonly embedding_json: string;
|
|
75
|
+
readonly embedding_tier: "working" | "episodic";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface TemporalRow {
|
|
79
|
+
readonly id: string;
|
|
80
|
+
readonly timestamp: string | null;
|
|
81
|
+
readonly importance: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const RRF_K = 60;
|
|
85
|
+
const POLYPHONIC_VOICES: readonly PolyphonicVoice[] = ["vector", "graph", "fact", "temporal"];
|
|
86
|
+
|
|
87
|
+
export function polyphonicRecallIsEnabled(env: Env = process.env): boolean {
|
|
88
|
+
return polyphonicRecallEnabled(env);
|
|
89
|
+
}
|
|
90
|
+
function envDisabled(name: string, env: Env = process.env): boolean {
|
|
91
|
+
const value = env[name];
|
|
92
|
+
if (value === undefined) return false;
|
|
93
|
+
return ["0", "false", "no", "off"].includes(value.trim().toLowerCase());
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function metadataValue(value: unknown): JsonValue {
|
|
97
|
+
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
if (Array.isArray(value)) return value.map(metadataValue);
|
|
101
|
+
if (typeof value === "object") {
|
|
102
|
+
const out: Record<string, JsonValue> = {};
|
|
103
|
+
const record = value as Record<string, unknown>;
|
|
104
|
+
for (const key in record) {
|
|
105
|
+
out[key] = metadataValue(record[key]);
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
return String(value);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function parseMetadata(raw: string | null): Metadata {
|
|
113
|
+
if (raw === null || raw.length === 0) return {};
|
|
114
|
+
try {
|
|
115
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
116
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
117
|
+
return metadataValue(parsed) as Metadata;
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
120
|
+
// Malformed metadata must not make recall fail.
|
|
121
|
+
}
|
|
122
|
+
return {};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function normalizeVector(vector: readonly number[] | Float32Array): Float32Array | null {
|
|
126
|
+
if (vector.length === 0) return null;
|
|
127
|
+
let normSq = 0;
|
|
128
|
+
for (let i = 0; i < vector.length; i++) {
|
|
129
|
+
const value = vector[i];
|
|
130
|
+
if (value === undefined || !Number.isFinite(value)) return null;
|
|
131
|
+
normSq += value * value;
|
|
132
|
+
}
|
|
133
|
+
if (normSq === 0) return null;
|
|
134
|
+
const norm = Math.sqrt(normSq);
|
|
135
|
+
const out = new Float32Array(vector.length);
|
|
136
|
+
for (let i = 0; i < vector.length; i++) out[i] = (vector[i] as number) / norm;
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function cosineAgainstUnit(unit: Float32Array, raw: unknown): number | null {
|
|
141
|
+
if (!Array.isArray(raw) || raw.length !== unit.length) return null;
|
|
142
|
+
let normSq = 0;
|
|
143
|
+
let dot = 0;
|
|
144
|
+
for (let i = 0; i < raw.length; i++) {
|
|
145
|
+
const value = raw[i];
|
|
146
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return null;
|
|
147
|
+
normSq += value * value;
|
|
148
|
+
const unitValue = unit[i];
|
|
149
|
+
if (unitValue === undefined) return null;
|
|
150
|
+
dot += unitValue * value;
|
|
151
|
+
}
|
|
152
|
+
if (normSq === 0) return null;
|
|
153
|
+
return dot / Math.sqrt(normSq);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function extractEntities(text: string): string[] {
|
|
157
|
+
const seen = new Set<string>();
|
|
158
|
+
const matches = text.matchAll(/\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+)*\b/g);
|
|
159
|
+
for (const match of matches) {
|
|
160
|
+
const entity = match[0];
|
|
161
|
+
if (entity.length > 0) seen.add(entity);
|
|
162
|
+
}
|
|
163
|
+
return [...seen];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function queryWords(query: string): string[] {
|
|
167
|
+
const seen = new Set<string>();
|
|
168
|
+
for (const match of query.toLowerCase().matchAll(/[\p{L}\p{N}_-]+/gu)) {
|
|
169
|
+
const word = match[0];
|
|
170
|
+
if (word.length >= 3) seen.add(word);
|
|
171
|
+
}
|
|
172
|
+
return [...seen];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function looksTemporal(query: string): boolean {
|
|
176
|
+
const lower = query.toLowerCase();
|
|
177
|
+
return ["yesterday", "today", "recent", "last", "latest", "this week", "this month", "ago", "before"].some(keyword =>
|
|
178
|
+
lower.includes(keyword),
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export class PolyphonicRecallEngine {
|
|
183
|
+
readonly dbPath: DatabasePath;
|
|
184
|
+
readonly db: Database;
|
|
185
|
+
readonly ownsConnection: boolean;
|
|
186
|
+
readonly graph: EpisodicGraph;
|
|
187
|
+
readonly consolidator: VeracityConsolidator;
|
|
188
|
+
readonly sessionId: string;
|
|
189
|
+
readonly channelId: string | null;
|
|
190
|
+
readonly voiceWeights: Readonly<Record<PolyphonicVoice, number>> = Object.freeze({
|
|
191
|
+
vector: 0.35,
|
|
192
|
+
graph: 0.25,
|
|
193
|
+
fact: 0.25,
|
|
194
|
+
temporal: 0.15,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
constructor(options: PolyphonicEngineOptions = {}) {
|
|
198
|
+
this.dbPath = options.dbPath ?? ":memory:";
|
|
199
|
+
this.db = options.db ?? openDatabase(this.dbPath);
|
|
200
|
+
this.ownsConnection = options.db === undefined;
|
|
201
|
+
this.graph = options.graph ?? new EpisodicGraph({ db: this.db, dbPath: this.dbPath });
|
|
202
|
+
this.consolidator = options.consolidator ?? new VeracityConsolidator(this.dbPath, this.db);
|
|
203
|
+
this.sessionId = options.sessionId ?? "default";
|
|
204
|
+
this.channelId = options.channelId ?? null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
recall(
|
|
208
|
+
query: string,
|
|
209
|
+
queryEmbedding: readonly number[] | Float32Array | null = null,
|
|
210
|
+
topK = 10,
|
|
211
|
+
contextBudget = 4000,
|
|
212
|
+
): PolyphonicMemoryResult[] {
|
|
213
|
+
const vectorResults = this.vectorVoice(queryEmbedding);
|
|
214
|
+
const graphResults = this.graphVoice(query);
|
|
215
|
+
const factResults = this.factVoice(query);
|
|
216
|
+
const temporalResults = this.temporalVoice(query);
|
|
217
|
+
const combined = this.combineVoices(vectorResults, graphResults, factResults, temporalResults);
|
|
218
|
+
const reranked = this.diversityRerank(combined, topK);
|
|
219
|
+
return this.hydrateResults(this.assembleContext(reranked, contextBudget));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
vectorVoice(queryEmbedding: readonly number[] | Float32Array | null): VoiceRecallResult[] {
|
|
223
|
+
if (envDisabled("PROMETHEUS_MEMORY_VOICE_VECTOR") || queryEmbedding === null) return [];
|
|
224
|
+
const queryUnit = normalizeVector(queryEmbedding);
|
|
225
|
+
if (queryUnit === null) return [];
|
|
226
|
+
const now = new Date().toISOString();
|
|
227
|
+
let rows: EmbeddingRow[] = [];
|
|
228
|
+
try {
|
|
229
|
+
rows = this.db
|
|
230
|
+
.query(`
|
|
231
|
+
SELECT me.memory_id, me.embedding_json, 'working' AS embedding_tier
|
|
232
|
+
FROM memory_embeddings me
|
|
233
|
+
JOIN working_memory wm ON wm.id = me.memory_id
|
|
234
|
+
WHERE wm.superseded_by IS NULL
|
|
235
|
+
AND (wm.valid_until IS NULL OR wm.valid_until > ?)
|
|
236
|
+
AND (wm.session_id = ? OR wm.scope = 'global')
|
|
237
|
+
UNION ALL
|
|
238
|
+
SELECT me.memory_id, me.embedding_json, 'episodic' AS embedding_tier
|
|
239
|
+
FROM memory_embeddings me
|
|
240
|
+
JOIN episodic_memory em ON em.id = me.memory_id
|
|
241
|
+
WHERE em.superseded_by IS NULL
|
|
242
|
+
AND (em.valid_until IS NULL OR em.valid_until > ?)
|
|
243
|
+
AND (em.session_id = ? OR em.scope = 'global')
|
|
244
|
+
LIMIT 50000
|
|
245
|
+
`)
|
|
246
|
+
.all(now, this.sessionId, now, this.sessionId) as EmbeddingRow[];
|
|
247
|
+
} catch {
|
|
248
|
+
return [];
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const byId = new Map<string, VoiceRecallResult>();
|
|
252
|
+
for (const row of rows) {
|
|
253
|
+
let parsed: unknown;
|
|
254
|
+
try {
|
|
255
|
+
parsed = JSON.parse(row.embedding_json) as unknown;
|
|
256
|
+
} catch {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const cosine = cosineAgainstUnit(queryUnit, parsed);
|
|
260
|
+
if (cosine === null) continue;
|
|
261
|
+
const similarity = (cosine + 1) / 2;
|
|
262
|
+
const existing = byId.get(row.memory_id);
|
|
263
|
+
if (existing === undefined || similarity > existing.score) {
|
|
264
|
+
byId.set(row.memory_id, {
|
|
265
|
+
memoryId: row.memory_id,
|
|
266
|
+
score: similarity,
|
|
267
|
+
voice: "vector",
|
|
268
|
+
metadata: {
|
|
269
|
+
similarity,
|
|
270
|
+
cosine_similarity: cosine,
|
|
271
|
+
embedding_tier: row.embedding_tier,
|
|
272
|
+
backend: "memory_embeddings",
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return [...byId.values()].sort((a, b) => b.score - a.score || a.memoryId.localeCompare(b.memoryId)).slice(0, 20);
|
|
278
|
+
}
|
|
279
|
+
graphVoice(query: string): VoiceRecallResult[] {
|
|
280
|
+
if (envDisabled("PROMETHEUS_MEMORY_VOICE_GRAPH")) return [];
|
|
281
|
+
const results: VoiceRecallResult[] = [];
|
|
282
|
+
const seedIds = new Set<string>();
|
|
283
|
+
for (const entity of extractEntities(query)) {
|
|
284
|
+
for (const gist of this.graph.findGistsByParticipant(entity)) {
|
|
285
|
+
const memoryId = gist.id.startsWith("gist_") ? gist.id.slice(5) : gist.id;
|
|
286
|
+
seedIds.add(memoryId);
|
|
287
|
+
results.push({
|
|
288
|
+
memoryId,
|
|
289
|
+
score: 0.6,
|
|
290
|
+
voice: "graph",
|
|
291
|
+
metadata: { entity, gist: gist.text },
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
for (const fact of this.graph.findFactsBySubject(entity)) {
|
|
295
|
+
const memoryId = fact.id.includes("_") ? (fact.id.split("_").at(-1) ?? fact.id) : fact.id;
|
|
296
|
+
seedIds.add(memoryId);
|
|
297
|
+
results.push({
|
|
298
|
+
memoryId,
|
|
299
|
+
score: fact.confidence * 0.5,
|
|
300
|
+
voice: "graph",
|
|
301
|
+
metadata: { entity, fact: `${fact.subject} ${fact.predicate} ${fact.object}` },
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const traversed = new Set<string>();
|
|
306
|
+
for (const seedId of seedIds) {
|
|
307
|
+
for (const related of this.graph.findRelatedMemories(seedId, 2, "ctx", 0.3)) {
|
|
308
|
+
if (seedIds.has(related.memoryId) || traversed.has(related.memoryId)) continue;
|
|
309
|
+
traversed.add(related.memoryId);
|
|
310
|
+
results.push({
|
|
311
|
+
memoryId: related.memoryId,
|
|
312
|
+
score: 0.4 / Math.max(1, related.depth),
|
|
313
|
+
voice: "graph",
|
|
314
|
+
metadata: {
|
|
315
|
+
seed: seedId,
|
|
316
|
+
edge_type: related.edgeType,
|
|
317
|
+
depth: related.depth,
|
|
318
|
+
weight: related.weight,
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return results;
|
|
324
|
+
}
|
|
325
|
+
factVoice(query: string): VoiceRecallResult[] {
|
|
326
|
+
if (envDisabled("PROMETHEUS_MEMORY_VOICE_FACT")) return [];
|
|
327
|
+
const byId = new Map<string, VoiceRecallResult>();
|
|
328
|
+
for (const word of queryWords(query)) {
|
|
329
|
+
const subject = word[0] === undefined ? word : word[0].toUpperCase() + word.slice(1);
|
|
330
|
+
for (const fact of this.consolidator.getConsolidatedFacts(subject, 0.5)) {
|
|
331
|
+
for (const source of fact.sources) {
|
|
332
|
+
const memoryId = source.trim();
|
|
333
|
+
if (memoryId.length === 0) continue;
|
|
334
|
+
const existing = byId.get(memoryId);
|
|
335
|
+
if (existing !== undefined && existing.score >= fact.confidence) continue;
|
|
336
|
+
byId.set(memoryId, {
|
|
337
|
+
memoryId,
|
|
338
|
+
score: fact.confidence,
|
|
339
|
+
voice: "fact",
|
|
340
|
+
metadata: {
|
|
341
|
+
fact_id: fact.id ?? "",
|
|
342
|
+
subject: fact.subject,
|
|
343
|
+
predicate: fact.predicate,
|
|
344
|
+
object: fact.object,
|
|
345
|
+
mentions: fact.mention_count,
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return [...byId.values()].sort((a, b) => b.score - a.score || a.memoryId.localeCompare(b.memoryId));
|
|
352
|
+
}
|
|
353
|
+
temporalVoice(query: string): VoiceRecallResult[] {
|
|
354
|
+
if (envDisabled("PROMETHEUS_MEMORY_VOICE_TEMPORAL") || !looksTemporal(query)) return [];
|
|
355
|
+
const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
|
|
356
|
+
let rows: TemporalRow[] = [];
|
|
357
|
+
try {
|
|
358
|
+
rows = this.db
|
|
359
|
+
.query(`
|
|
360
|
+
SELECT id, timestamp, importance
|
|
361
|
+
FROM working_memory
|
|
362
|
+
WHERE timestamp > ?
|
|
363
|
+
AND superseded_by IS NULL
|
|
364
|
+
AND (valid_until IS NULL OR valid_until > ?)
|
|
365
|
+
AND (session_id = ? OR scope = 'global')
|
|
366
|
+
ORDER BY timestamp DESC
|
|
367
|
+
LIMIT 20
|
|
368
|
+
`)
|
|
369
|
+
.all(weekAgo, new Date().toISOString(), this.sessionId) as TemporalRow[];
|
|
370
|
+
} catch {
|
|
371
|
+
return [];
|
|
372
|
+
}
|
|
373
|
+
const now = Date.now();
|
|
374
|
+
const results: VoiceRecallResult[] = [];
|
|
375
|
+
for (const row of rows) {
|
|
376
|
+
if (row.timestamp === null) continue;
|
|
377
|
+
const then = Date.parse(row.timestamp);
|
|
378
|
+
if (!Number.isFinite(then)) continue;
|
|
379
|
+
const ageDays = Math.max(0, (now - then) / 86_400_000);
|
|
380
|
+
const temporalScore = Math.exp(-ageDays / 7) * row.importance;
|
|
381
|
+
results.push({
|
|
382
|
+
memoryId: row.id,
|
|
383
|
+
score: temporalScore,
|
|
384
|
+
voice: "temporal",
|
|
385
|
+
metadata: { age_days: ageDays, importance: row.importance },
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
return results;
|
|
389
|
+
}
|
|
390
|
+
combineVoices(...voiceResults: readonly VoiceRecallResult[][]): Map<string, PolyphonicResult> {
|
|
391
|
+
const combined = new Map<string, PolyphonicResult>();
|
|
392
|
+
for (const results of voiceResults) {
|
|
393
|
+
if (results.length === 0) continue;
|
|
394
|
+
const sorted = [...results].sort((a, b) => b.score - a.score || a.memoryId.localeCompare(b.memoryId));
|
|
395
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
396
|
+
const result = sorted[i];
|
|
397
|
+
if (result === undefined) continue;
|
|
398
|
+
const rank = i + 1;
|
|
399
|
+
let existing = combined.get(result.memoryId);
|
|
400
|
+
if (existing === undefined) {
|
|
401
|
+
existing = { memoryId: result.memoryId, combinedScore: 0, voiceScores: {}, metadata: {} };
|
|
402
|
+
combined.set(result.memoryId, existing);
|
|
403
|
+
}
|
|
404
|
+
const contribution = 1 / (RRF_K + rank);
|
|
405
|
+
existing.voiceScores[result.voice] = (existing.voiceScores[result.voice] ?? 0) + contribution;
|
|
406
|
+
existing.combinedScore += contribution;
|
|
407
|
+
Object.assign(existing.metadata, result.metadata);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return combined;
|
|
411
|
+
}
|
|
412
|
+
diversityRerank(results: ReadonlyMap<string, PolyphonicResult>, topK: number): PolyphonicResult[] {
|
|
413
|
+
const sorted = [...results.values()].sort(
|
|
414
|
+
(a, b) => b.combinedScore - a.combinedScore || a.memoryId.localeCompare(b.memoryId),
|
|
415
|
+
);
|
|
416
|
+
const selected: PolyphonicResult[] = [];
|
|
417
|
+
const limit = Math.max(0, Math.trunc(topK));
|
|
418
|
+
for (const result of sorted) {
|
|
419
|
+
if (selected.length >= limit) break;
|
|
420
|
+
let diverse = true;
|
|
421
|
+
for (const prior of selected) {
|
|
422
|
+
if (this.estimateSimilarity(result, prior) > 0.8) {
|
|
423
|
+
diverse = false;
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (diverse) selected.push(result);
|
|
428
|
+
}
|
|
429
|
+
return selected;
|
|
430
|
+
}
|
|
431
|
+
estimateSimilarity(a: PolyphonicResult, b: PolyphonicResult): number {
|
|
432
|
+
let aCount = 0;
|
|
433
|
+
let bCount = 0;
|
|
434
|
+
let intersection = 0;
|
|
435
|
+
for (const voice of POLYPHONIC_VOICES) {
|
|
436
|
+
const inA = a.voiceScores[voice] !== undefined;
|
|
437
|
+
const inB = b.voiceScores[voice] !== undefined;
|
|
438
|
+
if (inA) aCount++;
|
|
439
|
+
if (inB) bCount++;
|
|
440
|
+
if (inA && inB) intersection++;
|
|
441
|
+
}
|
|
442
|
+
if (aCount === 0 || bCount === 0) return 0;
|
|
443
|
+
return intersection / (aCount + bCount - intersection);
|
|
444
|
+
}
|
|
445
|
+
assembleContext(results: readonly PolyphonicResult[], budget: number): PolyphonicResult[] {
|
|
446
|
+
const maxChars = Math.max(0, Math.trunc(budget)) * 4;
|
|
447
|
+
let chars = 0;
|
|
448
|
+
const selected: PolyphonicResult[] = [];
|
|
449
|
+
for (const result of results) {
|
|
450
|
+
const size = JSON.stringify(result.metadata).length + 100;
|
|
451
|
+
if (chars + size > maxChars) break;
|
|
452
|
+
selected.push(result);
|
|
453
|
+
chars += size;
|
|
454
|
+
}
|
|
455
|
+
return selected;
|
|
456
|
+
}
|
|
457
|
+
getStats(): Record<string, JsonValue> {
|
|
458
|
+
let embeddedRows = 0;
|
|
459
|
+
try {
|
|
460
|
+
const row = this.db.query("SELECT COUNT(*) AS count FROM memory_embeddings").get() as {
|
|
461
|
+
count: number;
|
|
462
|
+
};
|
|
463
|
+
embeddedRows = row.count;
|
|
464
|
+
} catch {
|
|
465
|
+
embeddedRows = 0;
|
|
466
|
+
}
|
|
467
|
+
return {
|
|
468
|
+
voice_weights: {
|
|
469
|
+
vector: this.voiceWeights.vector,
|
|
470
|
+
graph: this.voiceWeights.graph,
|
|
471
|
+
fact: this.voiceWeights.fact,
|
|
472
|
+
temporal: this.voiceWeights.temporal,
|
|
473
|
+
},
|
|
474
|
+
vector_stats: { embedded_rows: embeddedRows },
|
|
475
|
+
graph_stats: this.graph.getStats() as unknown as Record<string, JsonValue>,
|
|
476
|
+
consolidation_stats: this.consolidator.getStats() as unknown as Record<string, JsonValue>,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
close(): void {
|
|
480
|
+
if (this.ownsConnection) closeQuietly(this.db);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
private hydrateResults(results: readonly PolyphonicResult[]): PolyphonicMemoryResult[] {
|
|
484
|
+
const hydrated: PolyphonicMemoryResult[] = [];
|
|
485
|
+
for (const result of results) {
|
|
486
|
+
const row = this.lookupMemory(result.memoryId);
|
|
487
|
+
if (row === null) continue;
|
|
488
|
+
const rowMetadata = parseMetadata(row.metadata_json);
|
|
489
|
+
const voiceScores = sortedVoiceScores(result.voiceScores);
|
|
490
|
+
hydrated.push({
|
|
491
|
+
...row,
|
|
492
|
+
metadata: { ...rowMetadata, polyphonic: result.metadata },
|
|
493
|
+
recall_count: row.recall_count ?? undefined,
|
|
494
|
+
score: result.combinedScore,
|
|
495
|
+
combined_score: result.combinedScore,
|
|
496
|
+
voice_scores: voiceScores,
|
|
497
|
+
tier: row.tier_name,
|
|
498
|
+
tier_label: row.tier_name,
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
return hydrated;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
private lookupMemory(memoryId: string): MemoryHydrationRow | null {
|
|
505
|
+
const now = new Date().toISOString();
|
|
506
|
+
const working = this.db
|
|
507
|
+
.query(`
|
|
508
|
+
SELECT id, content, source, timestamp, session_id, importance, metadata_json, veracity,
|
|
509
|
+
memory_type, recall_count, last_recalled, valid_until, superseded_by, scope,
|
|
510
|
+
author_id, author_type, channel_id, trust_tier, created_at, 'working' AS tier_name
|
|
511
|
+
FROM working_memory
|
|
512
|
+
WHERE id = ?
|
|
513
|
+
AND superseded_by IS NULL
|
|
514
|
+
AND (valid_until IS NULL OR valid_until > ?)
|
|
515
|
+
AND (session_id = ? OR scope = 'global')
|
|
516
|
+
`)
|
|
517
|
+
.get(memoryId, now, this.sessionId) as MemoryHydrationRow | null;
|
|
518
|
+
if (working !== null) return working;
|
|
519
|
+
return this.db
|
|
520
|
+
.query(`
|
|
521
|
+
SELECT id, content, source, timestamp, session_id, importance, metadata_json, veracity,
|
|
522
|
+
memory_type, recall_count, last_recalled, valid_until, superseded_by, scope,
|
|
523
|
+
author_id, author_type, channel_id, trust_tier, created_at, rowid, summary_of,
|
|
524
|
+
tier, 'episodic' AS tier_name
|
|
525
|
+
FROM episodic_memory
|
|
526
|
+
WHERE id = ?
|
|
527
|
+
AND superseded_by IS NULL
|
|
528
|
+
AND (valid_until IS NULL OR valid_until > ?)
|
|
529
|
+
AND (session_id = ? OR scope = 'global')
|
|
530
|
+
`)
|
|
531
|
+
.get(memoryId, now, this.sessionId) as MemoryHydrationRow | null;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function sortedVoiceScores(scores: Partial<Record<PolyphonicVoice, number>>): Partial<Record<PolyphonicVoice, number>> {
|
|
536
|
+
const out: Partial<Record<PolyphonicVoice, number>> = {};
|
|
537
|
+
for (const voice of POLYPHONIC_VOICES) {
|
|
538
|
+
const score = scores[voice];
|
|
539
|
+
if (score !== undefined && Number.isFinite(score)) out[voice] = score;
|
|
540
|
+
}
|
|
541
|
+
return out;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
export function getPolyphonicEngine(beam: BeamMemoryState): PolyphonicRecallEngine {
|
|
545
|
+
const cached = beam.caches.polyphonicEngine;
|
|
546
|
+
if (cached instanceof PolyphonicRecallEngine) return cached;
|
|
547
|
+
const engine = new PolyphonicRecallEngine({
|
|
548
|
+
db: beam.db,
|
|
549
|
+
dbPath: beam.dbPath,
|
|
550
|
+
sessionId: beam.sessionId,
|
|
551
|
+
channelId: beam.channelId,
|
|
552
|
+
});
|
|
553
|
+
beam.caches.polyphonicEngine = engine;
|
|
554
|
+
return engine;
|
|
555
|
+
}
|
|
556
|
+
export function polyphonicRecall(
|
|
557
|
+
beam: BeamMemoryState,
|
|
558
|
+
query: string,
|
|
559
|
+
topK = 10,
|
|
560
|
+
options: PolyphonicRecallOptions = {},
|
|
561
|
+
): PolyphonicMemoryResult[] {
|
|
562
|
+
return getPolyphonicEngine(beam).recall(query, options.queryEmbedding ?? null, topK, options.contextBudget ?? 4000);
|
|
563
|
+
}
|