engram-sdk 0.1.10 → 0.3.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/dist/vault.js CHANGED
@@ -2,7 +2,7 @@ import path from 'path';
2
2
  import { MemoryStore } from './store.js';
3
3
  import { RememberInputSchema, RecallInputSchema } from './types.js';
4
4
  import { extract } from './extract.js';
5
- import { calculateRecencyBoost, DEFAULT_TEMPORAL_CONFIG, findContradictionCandidates, verifyContradiction } from './temporal.js';
5
+ import { calculateRecencyBoost, DEFAULT_TEMPORAL_CONFIG, findContradictionCandidates, verifyContradiction, temporalEdgeWeight } from './temporal.js';
6
6
  // ============================================================
7
7
  // Vault — The public API for Engram
8
8
  // ============================================================
@@ -66,6 +66,11 @@ export class Vault {
66
66
  // Contradiction detection: if this memory updates a previous fact,
67
67
  // mark the old one as superseded. Only runs when LLM is configured.
68
68
  return this.detectContradictions(memory);
69
+ })
70
+ .then(() => {
71
+ // Post-remember inference: extract implicit insights that a human
72
+ // would obviously infer (e.g., "builds hunting platform" → "likes hunting").
73
+ return this.inferInsights(memory);
69
74
  })
70
75
  .catch(err => {
71
76
  console.warn(`Failed to process embedding/contradictions for ${memory.id}:`, err);
@@ -197,6 +202,103 @@ export class Vault {
197
202
  // Reinforcement is best-effort
198
203
  }
199
204
  }
205
+ /**
206
+ * Post-remember inference: after storing a memory, use LLM to extract
207
+ * 0-2 implicit insights that a human would obviously infer.
208
+ *
209
+ * Example: "Ian is building a hunting land acquisition platform"
210
+ * → infers "Ian is interested in hunting"
211
+ *
212
+ * These are stored as low-confidence (0.3) semantic memories that
213
+ * accumulate via reinforcement over time. If someone is building
214
+ * a hunting platform AND talks about hunting trips AND buys hunting
215
+ * gear, the confidence on "Ian likes hunting" climbs naturally.
216
+ *
217
+ * Only runs when LLM is configured. Async, fire-and-forget.
218
+ * Skips memories that are already implicit or from consolidation.
219
+ */
220
+ async inferInsights(memory) {
221
+ if (!this.config.llm)
222
+ return;
223
+ // Don't infer from system/consolidation memories or already-implicit ones
224
+ if (memory.source?.type === 'consolidation')
225
+ return;
226
+ if (memory.topics?.includes('implicit'))
227
+ return;
228
+ if (memory.topics?.includes('meta'))
229
+ return;
230
+ // Skip low-salience memories (not worth the LLM call)
231
+ if (memory.salience < 0.4)
232
+ return;
233
+ // Skip very short memories (not enough signal)
234
+ if (memory.content.length < 40)
235
+ return;
236
+ const llmConfig = this.config.llm;
237
+ const model = llmConfig.provider === 'gemini' ? 'gemini-2.5-flash'
238
+ : llmConfig.provider === 'openai' ? 'gpt-4o-mini'
239
+ : 'claude-3-5-haiku-20241022';
240
+ const prompt = `Given this memory about a person, extract 0-2 basic personal insights that any human would obviously infer. Focus on interests, personality traits, preferences, and relationships.
241
+
242
+ Memory: "${memory.content}"
243
+ Entities: ${memory.entities?.join(', ') || 'none'}
244
+
245
+ Rules:
246
+ - Only include inferences that are clearly supported by the memory
247
+ - Keep each insight to one short sentence
248
+ - Do NOT restate the original memory — only new inferences
249
+ - If nothing interesting can be inferred, return empty array
250
+ - These should be things like "X is interested in Y", "X values Z", "X and Y are close"
251
+
252
+ JSON: {"insights": [{"content": "...", "entities": ["..."], "topics": ["..."]}]}
253
+ If nothing: {"insights": []}`;
254
+ try {
255
+ const response = await this.callLLM(model, prompt, llmConfig);
256
+ const parsed = JSON.parse(response);
257
+ for (const insight of parsed.insights ?? []) {
258
+ if (!insight.content || insight.content.length < 10)
259
+ continue;
260
+ // Check if this insight already exists (don't duplicate)
261
+ if (this.embedder && this.store.hasVectorSearch()) {
262
+ try {
263
+ const embedding = await this.embedder.embed(insight.content);
264
+ const similar = this.store.findSimilar(embedding, 0.15, 3);
265
+ if (similar.length > 0) {
266
+ // Similar insight exists — reinforce it instead of creating new
267
+ const existing = this.store.getMemoryDirect(similar[0].memoryId);
268
+ if (existing && existing.status === 'active') {
269
+ const newConf = Math.min(1.0, existing.confidence + 0.05);
270
+ this.store.updateMemory(existing.id, { confidence: newConf });
271
+ this.store.createEdge(memory.id, existing.id, 'supports', 0.6);
272
+ continue;
273
+ }
274
+ }
275
+ }
276
+ catch {
277
+ // Embedding check failed — create anyway
278
+ }
279
+ }
280
+ // Store as implicit memory with low confidence
281
+ const inferred = this.remember({
282
+ content: insight.content,
283
+ type: 'semantic',
284
+ entities: insight.entities ?? memory.entities ?? [],
285
+ topics: [...(insight.topics ?? []), 'implicit', 'inferred'],
286
+ salience: 0.4,
287
+ confidence: 0.3,
288
+ source: {
289
+ type: 'inference',
290
+ evidence: [memory.id],
291
+ },
292
+ });
293
+ // Link the insight to the source memory
294
+ this.store.createEdge(memory.id, inferred.id, 'derived_from', 0.7);
295
+ }
296
+ }
297
+ catch (err) {
298
+ // Inference is best-effort — never break the remember flow
299
+ console.error('Insight inference failed:', err);
300
+ }
301
+ }
200
302
  /**
201
303
  * Detect contradictions: when a new memory is stored, check if it
202
304
  * updates or replaces an existing fact about the same entity.
@@ -226,19 +328,35 @@ export class Vault {
226
328
  if (!memory.entities || memory.entities.length === 0)
227
329
  return;
228
330
  try {
229
- // Phase 1: Find candidates via vector similarity + entity overlap
331
+ // Phase 1: Find candidates via BOTH vector similarity AND entity overlap
332
+ // Vector similarity alone misses cases like "X is 79%" vs "X is 72%" where
333
+ // the surrounding text differs but the factual claim conflicts.
334
+ const candidateSet = new Map();
335
+ // 1a. Vector similarity search
230
336
  const embedding = this.store.getEmbedding(memory.id);
231
- if (!embedding)
232
- return;
233
- // Wider search than dedup — we want topically similar, not identical
234
- const similar = this.store.findSimilar(embedding, 0.5, 20);
235
- const candidateIds = similar
236
- .filter(s => s.memoryId !== memory.id)
237
- .map(s => s.memoryId);
238
- if (candidateIds.length === 0)
337
+ if (embedding) {
338
+ const similar = this.store.findSimilar(embedding, 0.5, 20);
339
+ const vectorIds = similar
340
+ .filter(s => s.memoryId !== memory.id)
341
+ .map(s => s.memoryId);
342
+ for (const mem of this.store.getMemoriesDirect(vectorIds)) {
343
+ if (mem.status === 'active')
344
+ candidateSet.set(mem.id, mem);
345
+ }
346
+ }
347
+ // 1b. Entity-based search — find ALL memories sharing entities with this one
348
+ // This catches contradictions that vector search misses
349
+ for (const entity of memory.entities) {
350
+ const entityMemories = this.store.getByEntity(entity, 30);
351
+ for (const mem of entityMemories) {
352
+ if (mem.id !== memory.id && mem.status === 'active') {
353
+ candidateSet.set(mem.id, mem);
354
+ }
355
+ }
356
+ }
357
+ if (candidateSet.size === 0)
239
358
  return;
240
- const candidateMemories = this.store.getMemoriesDirect(candidateIds)
241
- .filter(m => m.status === 'active');
359
+ const candidateMemories = [...candidateSet.values()];
242
360
  // Phase 1b: Heuristic filter — must share entities
243
361
  const threshold = this.config.temporal?.contradictionSimilarityThreshold ?? 0.75;
244
362
  const minOverlap = this.config.temporal?.minEntityOverlap ?? 1;
@@ -251,7 +369,7 @@ export class Vault {
251
369
  if (filtered.length === 0)
252
370
  return;
253
371
  // Phase 2: LLM verification — check top 3 candidates max
254
- const llmCall = (prompt) => this.callLLM(this.config.llm.model ?? 'gemini-2.0-flash', prompt, this.config.llm);
372
+ const llmCall = (prompt) => this.callLLM(this.config.llm.model ?? 'gemini-2.5-flash', prompt, this.config.llm);
255
373
  // Only check older memories — newer ones can't be superseded by this memory
256
374
  const olderCandidates = filtered
257
375
  .filter(c => new Date(c.createdAt) < new Date(memory.createdAt))
@@ -491,6 +609,34 @@ export class Vault {
491
609
  const oneWeekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
492
610
  results = results.filter(r => r.memory.createdAt >= oneWeekAgo);
493
611
  }
612
+ else if (parsed.temporalFocus === 'latest') {
613
+ // Deduplicate by entity+topic overlap: when multiple memories share
614
+ // the same primary entity AND topic, keep only the newest one.
615
+ // This prevents stale facts from polluting results.
616
+ const seen = new Map();
617
+ const deduped = [];
618
+ for (const r of results) {
619
+ if (r.memory.entities.length === 0) {
620
+ // No entities — can't dedup, keep it
621
+ deduped.push(r);
622
+ continue;
623
+ }
624
+ // Build a dedup key from primary entity + topics
625
+ const primaryEntity = r.memory.entities[0].toLowerCase();
626
+ const topicKey = (r.memory.topics ?? []).sort().join(',').toLowerCase();
627
+ const dedupKey = `${primaryEntity}:${topicKey}`;
628
+ const createdAt = new Date(r.memory.createdAt).getTime();
629
+ const existing = seen.get(dedupKey);
630
+ if (!existing || createdAt > existing.createdAt) {
631
+ seen.set(dedupKey, { memory: r.memory, score: r.score, createdAt });
632
+ }
633
+ }
634
+ // Collect deduped results: non-entity memories + latest per entity+topic
635
+ for (const entry of seen.values()) {
636
+ deduped.push({ memory: entry.memory, score: entry.score });
637
+ }
638
+ results = deduped;
639
+ }
494
640
  // 8. Score with salience, stability, and type weighting
495
641
  // Semantic memories with high stability are the "core knowledge" —
496
642
  // they should outrank noisy episodic results when the vector
@@ -557,9 +703,15 @@ export class Vault {
557
703
  const parentId = frontier.has(edge.sourceId) ? edge.sourceId : edge.targetId;
558
704
  const neighborId = edge.sourceId === parentId ? edge.targetId : edge.sourceId;
559
705
  const parentActivation = frontier.get(parentId) ?? 0;
560
- // Activation = parent × edge_strength × decay × edge_type_weight
706
+ // Activation = parent × temporalEdgeWeight(strength, recency) × decay × edge_type_weight
561
707
  const typeWeight = this.edgeTypeWeight(edge.type);
562
- const activation = parentActivation * edge.strength * opts.decay * typeWeight;
708
+ // Apply temporal weighting: edges to newer memories carry more activation energy
709
+ // Use getMemoryDirect to avoid bumping access count during traversal
710
+ const neighborMem = this.store.getMemoryDirect(neighborId);
711
+ const effectiveStrength = neighborMem
712
+ ? temporalEdgeWeight(edge.strength, neighborMem)
713
+ : edge.strength;
714
+ const activation = parentActivation * effectiveStrength * opts.decay * typeWeight;
563
715
  if (activation < opts.minActivation)
564
716
  continue;
565
717
  // Accumulate activation (multiple paths can reinforce)
@@ -684,13 +836,6 @@ export class Vault {
684
836
  entitiesDiscovered = result.entitiesDiscovered;
685
837
  connectionsFormed = result.connectionsFormed;
686
838
  }
687
- // Apply decay
688
- const decayed = this.store.applyDecay(this.config.decay?.halfLifeHours ?? 168);
689
- // Archive deeply decayed memories
690
- const archived = this.store.getDecayedMemories(this.config.decay?.archiveThreshold ?? 0.05);
691
- for (const mem of archived) {
692
- this.store.deleteMemory(mem.id); // TODO: move to cold storage instead of deleting
693
- }
694
839
  const report = {
695
840
  startedAt,
696
841
  completedAt: new Date().toISOString(),
@@ -700,12 +845,12 @@ export class Vault {
700
845
  entitiesDiscovered,
701
846
  connectionsFormed,
702
847
  contradictionsFound,
703
- memoriesDecayed: decayed,
704
- memoriesArchived: archived.length,
848
+ memoriesDecayed: 0,
849
+ memoriesArchived: 0,
705
850
  };
706
851
  // Store the consolidation report as a memory itself
707
852
  this.remember({
708
- content: `Consolidation completed: processed ${episodes.length} episodes, created ${semanticCreated} semantic memories, discovered ${entitiesDiscovered} entities, formed ${connectionsFormed} connections, decayed ${decayed} memories.`,
853
+ content: `Consolidation completed: processed ${episodes.length} episodes, created ${semanticCreated} semantic memories, discovered ${entitiesDiscovered} entities, formed ${connectionsFormed} connections.`,
709
854
  type: 'procedural',
710
855
  topics: ['meta', 'consolidation'],
711
856
  salience: 0.3,
@@ -867,6 +1012,315 @@ export class Vault {
867
1012
  return results.slice(0, limit);
868
1013
  }
869
1014
  // --------------------------------------------------------
1015
+ // ask() — Answer a question using memories as evidence.
1016
+ //
1017
+ // This is the feature that makes Engram useful for agents:
1018
+ // instead of returning 30 raw memories and making the agent
1019
+ // do the synthesis, ask() runs recall internally, then uses
1020
+ // the LLM to produce a coherent answer with confidence signal.
1021
+ //
1022
+ // The agent gets: one answer, the evidence behind it, and a
1023
+ // confidence level. No memory parsing, no synthesis burden.
1024
+ // --------------------------------------------------------
1025
+ async ask(question, opts) {
1026
+ if (!this.config.llm) {
1027
+ throw new Error('ask() requires LLM configuration (set llm in vault config)');
1028
+ }
1029
+ const limit = opts?.limit ?? 20;
1030
+ const spread = opts?.spread ?? true;
1031
+ // Step 1: Recall relevant memories
1032
+ const memories = await this.recall({
1033
+ context: question,
1034
+ limit,
1035
+ spread,
1036
+ temporalFocus: 'latest', // Deduplicate by entity+topic, keep newest
1037
+ });
1038
+ if (memories.length === 0) {
1039
+ return {
1040
+ answer: 'I have no memories related to this question.',
1041
+ confidence: 'low',
1042
+ memories: [],
1043
+ tokenEstimate: 0,
1044
+ evidenceQuality: {
1045
+ memoryCount: 0,
1046
+ avgConfidence: 0,
1047
+ totalAccesses: 0,
1048
+ newestMemoryAgeDays: -1,
1049
+ sourceBreakdown: { directInput: 0, autoIngested: 0 },
1050
+ },
1051
+ };
1052
+ }
1053
+ // Step 2: Build evidence block with metadata
1054
+ const evidenceLines = memories.map((m, i) => {
1055
+ const age = Math.floor((Date.now() - new Date(m.createdAt).getTime()) / (1000 * 60 * 60 * 24));
1056
+ const accessCount = m.accessCount ?? 0;
1057
+ const confidenceLabel = m.confidence >= 0.8 ? 'high' : m.confidence >= 0.5 ? 'medium' : 'low';
1058
+ const status = m.status !== 'active' ? ` [${m.status}]` : '';
1059
+ return `[${i + 1}] (${m.type}, confidence: ${confidenceLabel}, ${age}d ago, accessed ${accessCount}x${status}) ${m.content}`;
1060
+ });
1061
+ const prompt = `You are answering a question using memories from a knowledge vault.
1062
+
1063
+ RULES:
1064
+ - Answer the question directly and concisely based ONLY on the provided memories.
1065
+ - When multiple memories contain different values for the same fact, ALWAYS prefer the most recent one (lower "d ago" number).
1066
+ - If memories conflict, state the most recent fact and note it was updated.
1067
+ - If the memories don't contain enough information, say so honestly.
1068
+ - Do NOT make up information not supported by the memories.
1069
+
1070
+ MEMORIES:
1071
+ ${evidenceLines.join('\n')}
1072
+
1073
+ QUESTION: ${question}
1074
+
1075
+ Respond in JSON:
1076
+ {
1077
+ "answer": "Your concise, synthesized answer",
1078
+ "confidence": "high|medium|low",
1079
+ "reasoning": "Brief note on evidence quality"
1080
+ }
1081
+
1082
+ Confidence guide:
1083
+ - "high": Multiple memories support the answer, recent, frequently accessed
1084
+ - "medium": Answer is supported but by few memories or older data
1085
+ - "low": Sparse evidence, conflicting data, or mostly inference`;
1086
+ // Step 3: Call LLM for synthesis
1087
+ const llmConfig = this.config.llm;
1088
+ const model = llmConfig.model ?? 'gemini-2.5-flash';
1089
+ const response = await this.callLLM(model, prompt, llmConfig);
1090
+ // Step 4: Parse response
1091
+ let answer = 'Unable to synthesize an answer.';
1092
+ let confidence = 'low';
1093
+ try {
1094
+ const parsed = JSON.parse(response);
1095
+ answer = parsed.answer ?? answer;
1096
+ confidence = ['high', 'medium', 'low'].includes(parsed.confidence) ? parsed.confidence : 'low';
1097
+ }
1098
+ catch {
1099
+ // If JSON parsing fails, use raw text as answer
1100
+ answer = response.trim();
1101
+ }
1102
+ // Estimate tokens used (rough: 4 chars per token)
1103
+ const tokenEstimate = Math.ceil((prompt.length + answer.length) / 4);
1104
+ // Step 5: Build confidence metadata from evidence
1105
+ const avgConfidence = memories.reduce((sum, m) => sum + m.confidence, 0) / memories.length;
1106
+ const totalAccesses = memories.reduce((sum, m) => sum + m.accessCount, 0);
1107
+ const newestAge = Math.min(...memories.map(m => Math.floor((Date.now() - new Date(m.createdAt).getTime()) / (1000 * 60 * 60 * 24))));
1108
+ const autoIngestedCount = memories.filter(m => (m.topics ?? []).includes('auto-ingested')).length;
1109
+ return {
1110
+ answer,
1111
+ confidence,
1112
+ memories,
1113
+ tokenEstimate,
1114
+ evidenceQuality: {
1115
+ memoryCount: memories.length,
1116
+ avgConfidence: Math.round(avgConfidence * 100) / 100,
1117
+ totalAccesses,
1118
+ newestMemoryAgeDays: newestAge,
1119
+ sourceBreakdown: {
1120
+ directInput: memories.length - autoIngestedCount,
1121
+ autoIngested: autoIngestedCount,
1122
+ },
1123
+ },
1124
+ };
1125
+ }
1126
+ // --------------------------------------------------------
1127
+ // alerts() — What should the agent know RIGHT NOW?
1128
+ //
1129
+ // Unlike surface() (which needs context input) or briefing()
1130
+ // (which is a full session dump), alerts() returns only the
1131
+ // things that need attention. No context required.
1132
+ //
1133
+ // Three categories:
1134
+ // 1. Pending commitments — things promised but not fulfilled
1135
+ // 2. Stale follow-ups — things that haven't been touched in a while
1136
+ // 3. Contradictions — conflicting facts that need resolution
1137
+ //
1138
+ // Returns empty array when nothing needs attention.
1139
+ // Designed to be called on heartbeat or session start.
1140
+ // --------------------------------------------------------
1141
+ alerts(opts) {
1142
+ const staleDays = opts?.staleDays ?? 3;
1143
+ const limit = opts?.limit ?? 10;
1144
+ const includeContradictions = opts?.includeContradictions ?? true;
1145
+ const now = Date.now();
1146
+ const alerts = [];
1147
+ // 1. Pending commitments
1148
+ const pending = this.store.getByStatus('pending', 50);
1149
+ for (const mem of pending) {
1150
+ const ageDays = Math.floor((now - new Date(mem.createdAt).getTime()) / (1000 * 60 * 60 * 24));
1151
+ const priority = ageDays >= 7 ? 'high' :
1152
+ ageDays >= 3 ? 'medium' : 'low';
1153
+ // High-salience pending items are always worth surfacing
1154
+ // Low-salience ones only if they're getting stale
1155
+ if (mem.salience < 0.4 && ageDays < staleDays)
1156
+ continue;
1157
+ alerts.push({
1158
+ type: 'pending',
1159
+ priority,
1160
+ message: `Pending (${ageDays}d): ${mem.content}`,
1161
+ memoryId: mem.id,
1162
+ entities: mem.entities,
1163
+ ageDays,
1164
+ sortScore: (priority === 'high' ? 3 : priority === 'medium' ? 2 : 1) + mem.salience,
1165
+ });
1166
+ }
1167
+ // 2. Stale follow-ups — high-salience memories that haven't been accessed recently
1168
+ const allMemories = this.store.getByType('semantic', 100);
1169
+ const staleThreshold = now - staleDays * 24 * 60 * 60 * 1000;
1170
+ for (const mem of allMemories) {
1171
+ if (mem.status !== 'active')
1172
+ continue;
1173
+ if (mem.salience < 0.7)
1174
+ continue; // Only flag important stuff
1175
+ const lastAccessed = new Date(mem.lastAccessedAt).getTime();
1176
+ const ageDays = Math.floor((now - lastAccessed) / (1000 * 60 * 60 * 24));
1177
+ // Only flag if it hasn't been accessed in staleDays AND has topics suggesting follow-up
1178
+ if (lastAccessed > staleThreshold)
1179
+ continue;
1180
+ if (ageDays < staleDays)
1181
+ continue;
1182
+ // Look for action-oriented content
1183
+ const actionPatterns = /\b(should|need|must|todo|follow.?up|check|review|update|schedule|plan|deadline|due|remind)\b/i;
1184
+ if (!actionPatterns.test(mem.content))
1185
+ continue;
1186
+ alerts.push({
1187
+ type: 'stale',
1188
+ priority: ageDays >= 7 ? 'medium' : 'low',
1189
+ message: `Stale (${ageDays}d since accessed): ${mem.content}`,
1190
+ memoryId: mem.id,
1191
+ entities: mem.entities,
1192
+ ageDays,
1193
+ sortScore: (ageDays >= 7 ? 2 : 1) + mem.salience * 0.5,
1194
+ });
1195
+ }
1196
+ // 3. Contradictions
1197
+ if (includeContradictions) {
1198
+ const contradictions = this.contradictions(5);
1199
+ for (const c of contradictions) {
1200
+ const ageA = Math.floor((now - new Date(c.memoryA.createdAt).getTime()) / (1000 * 60 * 60 * 24));
1201
+ const ageB = Math.floor((now - new Date(c.memoryB.createdAt).getTime()) / (1000 * 60 * 60 * 24));
1202
+ alerts.push({
1203
+ type: 'contradiction',
1204
+ priority: 'medium',
1205
+ message: `Contradiction: "${c.memoryA.content.slice(0, 80)}" vs "${c.memoryB.content.slice(0, 80)}"`,
1206
+ entities: [...new Set([...c.memoryA.entities, ...c.memoryB.entities])],
1207
+ ageDays: Math.min(ageA, ageB),
1208
+ sortScore: 2.5, // Contradictions are always medium-high priority
1209
+ });
1210
+ }
1211
+ }
1212
+ // Sort by priority score descending, then by age descending
1213
+ alerts.sort((a, b) => b.sortScore - a.sortScore || b.ageDays - a.ageDays);
1214
+ // Return without the internal sortScore
1215
+ return alerts.slice(0, limit).map(({ sortScore, ...rest }) => rest);
1216
+ }
1217
+ // --------------------------------------------------------
1218
+ // audit() — Cross-reference external memory against vault.
1219
+ //
1220
+ // Takes content from an external source (e.g., MEMORY.md)
1221
+ // and checks for discrepancies with what's in the vault.
1222
+ // Returns claims that are outdated, missing, or contradicted.
1223
+ //
1224
+ // This is how Engram earns trust: instead of silently
1225
+ // disagreeing with the agent's other memory sources, it
1226
+ // speaks up.
1227
+ // --------------------------------------------------------
1228
+ async audit(externalContent, opts) {
1229
+ if (!this.config.llm) {
1230
+ throw new Error('audit() requires LLM configuration');
1231
+ }
1232
+ const maxClaims = opts?.maxClaims ?? 20;
1233
+ const relevanceThreshold = opts?.relevanceThreshold ?? 0.5;
1234
+ // Step 1: Extract factual claims from external content
1235
+ const extractPrompt = `Extract factual claims from this text. Each claim should be a single, verifiable statement.
1236
+
1237
+ TEXT:
1238
+ ${externalContent.slice(0, 8000)}
1239
+
1240
+ Respond as JSON:
1241
+ {"claims": ["claim 1", "claim 2", ...]}
1242
+
1243
+ Extract up to ${maxClaims} claims. Focus on specific facts (names, numbers, dates, statuses, relationships) not opinions or vague statements.`;
1244
+ const llmConfig = this.config.llm;
1245
+ const model = llmConfig.model ?? 'gemini-2.5-flash';
1246
+ const extractResponse = await this.callLLM(model, extractPrompt, llmConfig);
1247
+ let claims = [];
1248
+ try {
1249
+ const parsed = JSON.parse(extractResponse);
1250
+ claims = (parsed.claims ?? []).slice(0, maxClaims);
1251
+ }
1252
+ catch {
1253
+ return { discrepancies: [], verified: 0, total: 0 };
1254
+ }
1255
+ if (claims.length === 0) {
1256
+ return { discrepancies: [], verified: 0, total: 0 };
1257
+ }
1258
+ // Step 2: For each claim, check against vault
1259
+ const discrepancies = [];
1260
+ let verified = 0;
1261
+ for (const claim of claims) {
1262
+ // Recall memories relevant to this claim
1263
+ const memories = await this.recall({
1264
+ context: claim,
1265
+ limit: 5,
1266
+ spread: true,
1267
+ temporalFocus: 'latest',
1268
+ });
1269
+ if (memories.length === 0) {
1270
+ // No relevant memories — can't verify or contradict
1271
+ continue;
1272
+ }
1273
+ // Ask LLM to compare claim against vault memories
1274
+ const memoryContext = memories.map((m, i) => {
1275
+ const age = Math.floor((Date.now() - new Date(m.createdAt).getTime()) / (1000 * 60 * 60 * 24));
1276
+ return `[${i + 1}] (${age}d ago) ${m.content}`;
1277
+ }).join('\n');
1278
+ const comparePrompt = `Compare this claim against the vault memories below. Determine if the claim is:
1279
+ - "verified": Vault memories support this claim
1280
+ - "outdated": Vault has a MORE RECENT version of this fact
1281
+ - "contradicted": Vault directly contradicts this claim
1282
+ - "unrelated": Vault memories aren't relevant to this claim
1283
+
1284
+ CLAIM: ${claim}
1285
+
1286
+ VAULT MEMORIES:
1287
+ ${memoryContext}
1288
+
1289
+ Respond as JSON:
1290
+ {"status": "verified|outdated|contradicted|unrelated", "explanation": "brief reason", "relevantMemoryIndex": 1}`;
1291
+ try {
1292
+ const compareResponse = await this.callLLM(model, comparePrompt, llmConfig);
1293
+ const result = JSON.parse(compareResponse);
1294
+ if (result.status === 'verified') {
1295
+ verified++;
1296
+ }
1297
+ else if (result.status === 'outdated' || result.status === 'contradicted') {
1298
+ const memIdx = (result.relevantMemoryIndex ?? 1) - 1;
1299
+ const relevantMem = memories[memIdx] ?? memories[0];
1300
+ discrepancies.push({
1301
+ claim,
1302
+ source: 'external',
1303
+ vaultMemory: relevantMem.content,
1304
+ vaultCreatedAt: relevantMem.createdAt,
1305
+ type: result.status,
1306
+ explanation: result.explanation ?? '',
1307
+ });
1308
+ }
1309
+ // 'unrelated' and parse failures are silently skipped
1310
+ }
1311
+ catch {
1312
+ // LLM comparison failed for this claim, skip
1313
+ }
1314
+ // Rate limit between claims
1315
+ await new Promise(r => setTimeout(r, 500));
1316
+ }
1317
+ return {
1318
+ discrepancies,
1319
+ verified,
1320
+ total: claims.length,
1321
+ };
1322
+ }
1323
+ // --------------------------------------------------------
870
1324
  // surface() — Proactive memory surfacing.
871
1325
  //
872
1326
  // The key insight from the manifesto: memories should be
@@ -1097,7 +1551,7 @@ Respond in this exact JSON format:
1097
1551
 
1098
1552
  Keep entities specific and topics general. Limit to 10 entities and 8 topics max.`;
1099
1553
  try {
1100
- const response = await this.callLLM('gemini-2.0-flash', prompt, this.config.llm);
1554
+ const response = await this.callLLM('gemini-2.5-flash', prompt, this.config.llm);
1101
1555
  const result = JSON.parse(response);
1102
1556
  return {
1103
1557
  entities: (result.entities || []).slice(0, 10),
@@ -1160,7 +1614,7 @@ Keep entities specific and topics general. Limit to 10 entities and 8 topics max
1160
1614
  return { semanticCreated: 0, semanticUpdated: 0, entitiesDiscovered: 0, connectionsFormed: 0, contradictionsFound: 0 };
1161
1615
  }
1162
1616
  const llmConfig = this.config.llm;
1163
- const defaultModel = llmConfig.provider === 'gemini' ? 'gemini-2.0-flash'
1617
+ const defaultModel = llmConfig.provider === 'gemini' ? 'gemini-2.5-flash'
1164
1618
  : llmConfig.provider === 'openai' ? 'gpt-4o-mini'
1165
1619
  : 'claude-3-5-haiku-20241022';
1166
1620
  const model = llmConfig.model ?? defaultModel;
@@ -1313,7 +1767,7 @@ Be conservative with explicit memories. Be observant with implicit ones — look
1313
1767
  return jsonMatch ? (jsonMatch[1] ?? jsonMatch[0]) : text;
1314
1768
  }
1315
1769
  if (config.provider === 'gemini') {
1316
- const geminiModel = model.startsWith('gemini') ? model : 'gemini-2.0-flash';
1770
+ const geminiModel = model.startsWith('gemini') ? model : 'gemini-2.5-flash';
1317
1771
  const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${geminiModel}:generateContent?key=${config.apiKey}`, {
1318
1772
  method: 'POST',
1319
1773
  headers: { 'Content-Type': 'application/json' },