titan-agent 5.3.2 → 5.4.1

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 (48) hide show
  1. package/dist/agent/agent.js +11 -1
  2. package/dist/agent/agent.js.map +1 -1
  3. package/dist/agent/agentLoop.js +36 -1
  4. package/dist/agent/agentLoop.js.map +1 -1
  5. package/dist/agent/session.js +106 -5
  6. package/dist/agent/session.js.map +1 -1
  7. package/dist/agent/subAgent.js +62 -1
  8. package/dist/agent/subAgent.js.map +1 -1
  9. package/dist/config/config.js +30 -8
  10. package/dist/config/config.js.map +1 -1
  11. package/dist/config/schema.js +25 -2
  12. package/dist/config/schema.js.map +1 -1
  13. package/dist/gateway/server.js +32 -1
  14. package/dist/gateway/server.js.map +1 -1
  15. package/dist/memory/graph.js +49 -15
  16. package/dist/memory/graph.js.map +1 -1
  17. package/dist/memory/index.js +192 -0
  18. package/dist/memory/index.js.map +1 -0
  19. package/dist/memory/memory.js +1 -0
  20. package/dist/memory/memory.js.map +1 -1
  21. package/dist/mesh/transport.js +60 -8
  22. package/dist/mesh/transport.js.map +1 -1
  23. package/dist/providers/anthropic.js +3 -2
  24. package/dist/providers/anthropic.js.map +1 -1
  25. package/dist/providers/base.js.map +1 -1
  26. package/dist/providers/google.js +94 -20
  27. package/dist/providers/google.js.map +1 -1
  28. package/dist/providers/modelCapabilities.js +59 -0
  29. package/dist/providers/modelCapabilities.js.map +1 -0
  30. package/dist/providers/ollama.js +3 -2
  31. package/dist/providers/ollama.js.map +1 -1
  32. package/dist/providers/openai.js +4 -3
  33. package/dist/providers/openai.js.map +1 -1
  34. package/dist/providers/openai_compat.js +3 -2
  35. package/dist/providers/openai_compat.js.map +1 -1
  36. package/dist/providers/router.js +63 -21
  37. package/dist/providers/router.js.map +1 -1
  38. package/dist/safety/fabricationGuard.js +140 -0
  39. package/dist/safety/fabricationGuard.js.map +1 -0
  40. package/dist/skills/builtin/gepa.js +23 -1
  41. package/dist/skills/builtin/gepa.js.map +1 -1
  42. package/dist/skills/builtin/model_trainer.js +31 -4
  43. package/dist/skills/builtin/model_trainer.js.map +1 -1
  44. package/dist/skills/builtin/self_improve.js +50 -2
  45. package/dist/skills/builtin/self_improve.js.map +1 -1
  46. package/dist/utils/constants.js +2 -2
  47. package/dist/utils/constants.js.map +1 -1
  48. package/package.json +1 -1
@@ -8,6 +8,7 @@ import { loadConfig } from "../config/config.js";
8
8
  import { recordTokenUsage } from "../agent/costOptimizer.js";
9
9
  import logger from "../utils/logger.js";
10
10
  import { isVectorSearchAvailable, addVector, searchVectors } from "./vectors.js";
11
+ import { getMemoryIndex } from "./index.js";
11
12
  const COMPONENT = "Graph";
12
13
  const TITAN_HOME = join(homedir(), ".titan");
13
14
  const GRAPH_PATH = join(TITAN_HOME, "graph.json");
@@ -369,16 +370,31 @@ function findOrCreateEntity(name, type, facts) {
369
370
  function enforceMemoryBounds() {
370
371
  if (graph.episodes.length > MAX_EPISODES) {
371
372
  const excess = graph.episodes.length - MAX_EPISODES;
372
- graph.episodes.splice(0, excess);
373
+ const removed = graph.episodes.splice(0, excess);
374
+ try {
375
+ const idx = getMemoryIndex();
376
+ for (const ep of removed) idx.removeEpisode(ep.id);
377
+ } catch {
378
+ }
373
379
  logger.info(COMPONENT, `Pruned ${excess} oldest episodes (limit: ${MAX_EPISODES})`);
374
380
  }
375
381
  if (graph.entities.length > MAX_ENTITIES) {
376
- graph.entities.sort((a, b) => b.lastSeen.localeCompare(a.lastSeen));
382
+ const SCORE_FALLBACK = 0.5;
383
+ graph.entities.sort((a, b) => {
384
+ const wA = ENTITY_TYPE_WEIGHT[(a.type || "").toLowerCase()] ?? SCORE_FALLBACK;
385
+ const wB = ENTITY_TYPE_WEIGHT[(b.type || "").toLowerCase()] ?? SCORE_FALLBACK;
386
+ const fA = (a.episodeIds?.length ?? 0) + (a.facts?.length ?? 0);
387
+ const fB = (b.episodeIds?.length ?? 0) + (b.facts?.length ?? 0);
388
+ const salienceA = wA * (1 + fA);
389
+ const salienceB = wB * (1 + fB);
390
+ if (salienceB !== salienceA) return salienceB - salienceA;
391
+ return b.lastSeen.localeCompare(a.lastSeen);
392
+ });
377
393
  const excess = graph.entities.length - MAX_ENTITIES;
378
394
  const removed = graph.entities.splice(MAX_ENTITIES, excess);
379
395
  const removedIds = new Set(removed.map((e) => e.id));
380
396
  graph.edges = graph.edges.filter((e) => !removedIds.has(e.from) && !removedIds.has(e.to));
381
- logger.info(COMPONENT, `Pruned ${excess} oldest entities (limit: ${MAX_ENTITIES})`);
397
+ logger.info(COMPONENT, `Pruned ${excess} lowest-salience entities (limit: ${MAX_ENTITIES})`);
382
398
  buildGraphIndexes();
383
399
  }
384
400
  for (const entity of graph.entities) {
@@ -404,7 +420,7 @@ const POISON_PHRASES = [
404
420
  "could not find it through search",
405
421
  "does not appear in my knowledge"
406
422
  ];
407
- async function addEpisode(content, source, provenance) {
423
+ async function addEpisode(content, source, provenance, options) {
408
424
  if (!initialized) initGraph();
409
425
  if (containsInjection(content)) {
410
426
  logger.warn(COMPONENT, `Blocked injection attempt in episode from ${source}`);
@@ -423,6 +439,10 @@ async function addEpisode(content, source, provenance) {
423
439
  };
424
440
  graph.episodes.push(episode);
425
441
  episodeById.set(episode.id, episode);
442
+ try {
443
+ getMemoryIndex().addEpisode(episode.id, content);
444
+ } catch {
445
+ }
426
446
  enforceMemoryBounds();
427
447
  saveGraph();
428
448
  if (provenance) {
@@ -444,7 +464,7 @@ async function addEpisode(content, source, provenance) {
444
464
  if (isVectorSearchAvailable()) {
445
465
  addVector(`graph:${episode.id}`, content, "graph", { source, episodeId: episode.id }).catch((e) => logger.debug("Graph", `Vector op failed: ${e.message}`));
446
466
  }
447
- extractEntities(content).then((result) => {
467
+ const extractionPromise = extractEntities(content).then((result) => {
448
468
  const { entities: extracted, relations } = result;
449
469
  if (!extracted || extracted.length === 0) return;
450
470
  if (!Array.isArray(graph.edges)) graph.edges = [];
@@ -527,6 +547,9 @@ async function addEpisode(content, source, provenance) {
527
547
  saveGraph();
528
548
  logger.info(COMPONENT, `Episode ${episode.id.slice(0, 8)}: extracted ${extracted.length} entities, total ${graph.entities.length} entities, ${graph.edges.length} edges`);
529
549
  }).catch((err) => logger.warn(COMPONENT, `Background entity extraction failed: ${err.message}`));
550
+ if (options?.awaitEntities) {
551
+ await extractionPromise;
552
+ }
530
553
  return episode;
531
554
  }
532
555
  async function searchMemory(query, limit = 20) {
@@ -535,17 +558,28 @@ async function searchMemory(query, limit = 20) {
535
558
  const STOP_WORDS = /* @__PURE__ */ new Set(["a", "an", "the", "is", "it", "in", "on", "at", "to", "of", "do", "you", "we", "i", "me", "my", "that", "this", "was", "are", "be", "been", "have", "has", "had", "and", "or", "but", "if", "so", "not", "no", "yes", "can", "how", "what", "about", "from", "with", "for", "up", "out", "its", "our", "your", "they", "them", "he", "she", "his", "her", "will", "would", "could", "should", "did", "does", "just", "now", "some", "any", "all", "very", "too", "also", "than", "then", "when", "where", "who", "which", "there", "here", "again", "today", "earlier", "remember"]);
536
559
  const terms = query.toLowerCase().split(/\s+/).filter((t) => t.length > 1 && !STOP_WORDS.has(t));
537
560
  const scored = /* @__PURE__ */ new Map();
538
- for (const ep of graph.episodes) {
539
- const text = ep.content.toLowerCase();
540
- let score = 0;
541
- for (const term of terms) {
542
- if (text.includes(term)) {
543
- score += 1;
544
- if (text.slice(0, 100).includes(term)) score += 0.5;
545
- }
561
+ try {
562
+ const idx = getMemoryIndex();
563
+ if (idx.size() === 0 && graph.episodes.length > 0) {
564
+ for (const ep of graph.episodes) idx.addEpisode(ep.id, ep.content);
546
565
  }
547
- if (score > 0) {
548
- scored.set(ep.id, { ep, score });
566
+ const matches = idx.search(query, limit * 3);
567
+ for (const m of matches) {
568
+ const ep = episodeById.get(m.episodeId);
569
+ if (ep) scored.set(m.episodeId, { ep, score: m.score });
570
+ }
571
+ } catch (e) {
572
+ logger.debug(COMPONENT, `Inverted index unavailable, falling back to linear scan: ${e.message}`);
573
+ for (const ep of graph.episodes) {
574
+ const text = ep.content.toLowerCase();
575
+ let score = 0;
576
+ for (const term of terms) {
577
+ if (text.includes(term)) {
578
+ score += 1;
579
+ if (text.slice(0, 100).includes(term)) score += 0.5;
580
+ }
581
+ }
582
+ if (score > 0) scored.set(ep.id, { ep, score });
549
583
  }
550
584
  }
551
585
  for (const entity of graph.entities) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/memory/graph.ts"],"sourcesContent":["/**\n * TITAN — Native Temporal Knowledge Graph\n * Pure TypeScript graph memory: no Docker, no Python, no extra API keys.\n * Uses TITAN's own LLM for entity extraction.\n */\nimport { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from 'fs';\nimport { writeFile, rename } from 'fs/promises';\nimport { join } from 'path';\nimport { homedir } from 'os';\nimport { v4 as uuid } from 'uuid';\nimport { loadConfig } from '../config/config.js';\nimport { recordTokenUsage } from '../agent/costOptimizer.js';\nimport logger from '../utils/logger.js';\nimport { isVectorSearchAvailable, addVector, searchVectors } from './vectors.js';\n\nconst COMPONENT = 'Graph';\nconst TITAN_HOME = join(homedir(), '.titan');\nconst GRAPH_PATH = join(TITAN_HOME, 'graph.json');\n\n// Short-term cache for getGraphContext to avoid recomputing identical queries\nconst graphContextCache = new Map<string, { result: string; timestamp: number }>();\nconst GRAPH_CONTEXT_CACHE_TTL = 5000; // 5 seconds\nconst MAX_GRAPH_CONTEXT_CACHE = 100; // prevent unbounded growth\n\nconst ENTITY_STOP_WORDS = new Set(['a', 'an', 'the', 'is', 'it', 'in', 'on', 'at', 'to', 'of', 'do', 'you', 'we', 'i', 'me', 'my', 'that', 'this', 'was', 'are', 'be', 'been', 'have', 'has', 'had', 'and', 'or', 'but', 'if', 'so', 'not', 'no', 'yes', 'can', 'how', 'what', 'about', 'from', 'with', 'for', 'up', 'out', 'its', 'our', 'your', 'they', 'them', 'he', 'she', 'his', 'her', 'will', 'would', 'could', 'should', 'did', 'does', 'just', 'now', 'some', 'any', 'all', 'very', 'too', 'also', 'than', 'then', 'when', 'where', 'who', 'which', 'there', 'here', 'again', 'today', 'earlier', 'remember']);\nconst ENTITY_TYPE_WEIGHT: Record<string, number> = { person: 1.5, project: 1.3, company: 1.2, technology: 1.1, event: 1.0, place: 0.9, topic: 0.7 };\n\n// ── Memory Bounds (Hermes-inspired) ──────────────────────────────\nconst MAX_ENTITIES = 500; // Prune oldest when exceeded\nconst MAX_FACTS_PER_ENTITY = 50; // Cap facts per entity\nconst MAX_EPISODES = 5000; // Prune oldest episodes\nconst MAX_FACT_CHARS = 500; // Max chars per fact entry\n\n// ── Injection Protection ─────────────────────────────────────────\nconst INJECTION_PATTERNS = [\n /ignore\\s+(all\\s+)?previous\\s+instructions/i,\n /you\\s+are\\s+now\\s+a/i,\n /forget\\s+(all\\s+)?your\\s+(instructions|rules)/i,\n /system\\s*:\\s*you\\s+are/i,\n /\\[INST\\]/i,\n /<<SYS>>/i,\n /\\bACT\\s+AS\\b/i,\n /new\\s+instructions?\\s*:/i,\n /override\\s+(the\\s+)?system/i,\n];\n\n// ── Data Model ────────────────────────────────────────────────────\n\nexport interface Episode {\n id: string;\n content: string;\n source: string;\n createdAt: string;\n entities: string[]; // Entity IDs referenced\n}\n\nexport interface Entity {\n id: string;\n name: string;\n aliases: string[];\n type: string; // person | project | topic | place | company | technology | event\n summary: string;\n firstSeen: string;\n lastSeen: string;\n facts: string[];\n episodeIds: string[];\n}\n\n// ── Allowed entity types ─────────────────────────────────────────\nconst ALLOWED_TYPES = new Set(['person', 'project', 'topic', 'place', 'company', 'technology', 'event', 'social_post']);\n\nconst TYPE_COERCION: Record<string, string> = {\n fact: 'topic',\n preference: 'topic',\n feature: 'technology',\n software: 'technology',\n system: 'technology',\n hardware: 'technology',\n component: 'technology',\n tool: 'technology',\n product: 'technology',\n file: '__skip__',\n directory: '__skip__',\n scenario: '__skip__',\n 'person|organization': 'company',\n social_post: 'social_post',\n post: 'social_post',\n};\n\n/** Validate and coerce entity type to allowed set */\nfunction coerceType(raw: string): string | null {\n const t = raw?.toLowerCase().trim();\n if (ALLOWED_TYPES.has(t)) return t;\n const coerced = TYPE_COERCION[t];\n if (coerced === '__skip__') return null; // filter out\n return coerced ?? 'topic'; // default fallback\n}\n\n// Well-known tech names that look like filenames but aren't\nconst NOT_NOISE = new Set(['node.js', 'vue.js', 'next.js', 'nuxt.js', 'three.js', 'p5.js', 'express.js', 'd3.js', 'socket.io']);\n\n/** Check if a name is noise (file paths, URLs, numbers, tokens) */\nfunction isNoiseEntity(name: string): boolean {\n const n = name.trim();\n if (n.length < 2) return true;\n if (/^\\d+$/.test(n)) return true; // pure numbers\n if (/^[/~]/.test(n)) return true; // absolute/home paths\n if (n.includes('/') && n.includes('.') && !n.includes(' ')) return true; // file paths like src/foo.ts\n if (/^https?:\\/\\//i.test(n)) return true; // URLs\n if (!NOT_NOISE.has(n.toLowerCase()) && /^\\w+\\.\\w{1,4}$/.test(n) && /\\.(ts|js|json|md|py|sh|txt|log|html|css|yml|yaml|toml|cfg)$/i.test(n)) return true; // filenames\n if (/^(api\\/|http:|localhost|127\\.0\\.|192\\.168\\.)/.test(n.toLowerCase())) return true; // API paths/IPs\n return false;\n}\n\nexport interface Edge {\n id: string;\n from: string;\n to: string;\n relation: string;\n createdAt: string;\n}\n\nexport interface TitanGraph {\n episodes: Episode[];\n entities: Entity[];\n edges: Edge[];\n}\n\nexport interface GraphNode {\n id: string;\n label: string;\n type: string;\n size: number;\n facts: string[];\n}\n\nexport interface GraphEdge {\n from: string;\n to: string;\n label: string;\n}\n\n// ── In-memory graph ───────────────────────────────────────────────\nlet graph: TitanGraph = { episodes: [], entities: [], edges: [] };\nlet initialized = false;\nlet dirty = false;\n\n// ── Graph indexes (rebuilt on load / mutation) ────────────────────\nlet episodeById = new Map<string, Episode>();\nlet entitiesByRecency: Entity[] = []; // pre-sorted by lastSeen desc\n\nfunction buildGraphIndexes(): void {\n episodeById = new Map(graph.episodes.map(e => [e.id, e]));\n entitiesByRecency = graph.entities\n .slice()\n .sort((a, b) => b.lastSeen.localeCompare(a.lastSeen));\n}\n\n// ── Persistence ───────────────────────────────────────────────────\n// NOTE: Sync I/O is intentional — runs only once at cold start, then cached in-memory.\nfunction loadGraph(): void {\n mkdirSync(TITAN_HOME, { recursive: true });\n if (existsSync(GRAPH_PATH)) {\n try {\n const raw = readFileSync(GRAPH_PATH, 'utf-8');\n const parsed = JSON.parse(raw);\n const rawEdges = Array.isArray(parsed.edges) ? parsed.edges : [];\n // Normalize edges that use source/target/label (legacy) to from/to/relation\n const edges: Edge[] = rawEdges.map((e: Record<string, unknown>) => ({\n id: (e.id as string) || uuid(),\n from: (e.from as string) || (e.source as string) || '',\n to: (e.to as string) || (e.target as string) || '',\n relation: (e.relation as string) || (e.label as string) || 'related',\n createdAt: (e.createdAt as string) || new Date().toISOString(),\n }));\n // Normalize entities to ensure all required fields exist\n const entities = (Array.isArray(parsed.entities) ? parsed.entities : []).map(\n (e: Record<string, unknown>) => ({\n ...e,\n aliases: Array.isArray(e.aliases) ? e.aliases : [],\n summary: (e.summary as string) || '',\n })\n );\n graph = {\n episodes: Array.isArray(parsed.episodes) ? parsed.episodes : [],\n entities,\n edges,\n };\n buildGraphIndexes();\n } catch (e) {\n logger.warn(COMPONENT, `Failed to parse graph.json, starting fresh: ${(e as Error).message}`);\n graph = { episodes: [], entities: [], edges: [] };\n buildGraphIndexes();\n }\n }\n}\n\nlet graphSaveTimeout: ReturnType<typeof setTimeout> | null = null;\n\nfunction saveGraph(): void {\n if (dirty) {\n // Bypass debounce if a previous write failed\n if (graphSaveTimeout) { clearTimeout(graphSaveTimeout); graphSaveTimeout = null; }\n doAsyncSave();\n return;\n }\n if (graphSaveTimeout) clearTimeout(graphSaveTimeout);\n graphSaveTimeout = setTimeout(doAsyncSave, 1000);\n graphSaveTimeout.unref();\n}\n\nlet saveInProgress = false;\n\nasync function doAsyncSave(): Promise<void> {\n if (saveInProgress) {\n dirty = true;\n return;\n }\n saveInProgress = true;\n try {\n const tmpFile = GRAPH_PATH + '.tmp';\n await writeFile(tmpFile, JSON.stringify(graph, null, 2), 'utf-8');\n await rename(tmpFile, GRAPH_PATH);\n dirty = false;\n } catch (e) {\n dirty = true;\n logger.error(COMPONENT, `Failed to save graph.json: ${(e as Error).message}`);\n } finally {\n saveInProgress = false;\n }\n}\n\n/** Flush graph to disk immediately (for shutdown) */\nexport function flushGraph(): void {\n if (graphSaveTimeout) { clearTimeout(graphSaveTimeout); graphSaveTimeout = null; }\n try {\n const tmpFile = GRAPH_PATH + '.tmp';\n writeFileSync(tmpFile, JSON.stringify(graph, null, 2), 'utf-8');\n renameSync(tmpFile, GRAPH_PATH);\n dirty = false;\n } catch (e) {\n dirty = true;\n logger.error(COMPONENT, `Failed to flush graph.json: ${(e as Error).message}`);\n }\n if (dirty) {\n logger.error(COMPONENT, 'DATA MAY BE LOST — failed to flush graph on shutdown');\n }\n}\n\n// ── Init ─────────────────────────────────────────────────────────\nexport function initGraph(): void {\n if (initialized) return;\n loadGraph();\n initialized = true;\n\n // Self-healing: purge poisoned episodes on startup\n const beforeCount = graph.episodes.length;\n graph.episodes = graph.episodes.filter(ep => {\n const c = ep.content.toLowerCase();\n if (c.includes('[titan') && POISON_PHRASES.some(p => c.includes(p))) return false;\n return true;\n });\n buildGraphIndexes();\n const purged = beforeCount - graph.episodes.length;\n if (purged > 0) {\n logger.info(COMPONENT, `Self-heal: purged ${purged} poisoned episodes (negative recall responses)`);\n saveGraph();\n }\n\n logger.info(COMPONENT, `Graph loaded: ${graph.episodes.length} episodes, ${graph.entities.length} entities`);\n\n // Schedule periodic self-healing every 24 hours\n setInterval(() => {\n const before = graph.episodes.length;\n graph.episodes = graph.episodes.filter(ep => {\n const c = ep.content.toLowerCase();\n if (c.includes('[titan') && POISON_PHRASES.some(p => c.includes(p))) return false;\n return true;\n });\n const cleaned = before - graph.episodes.length;\n if (cleaned > 0) {\n logger.info(COMPONENT, `Self-heal (periodic): purged ${cleaned} poisoned episodes`);\n saveGraph();\n }\n }, 24 * 60 * 60 * 1000).unref();\n}\n\n// ── Entity extraction via any configured LLM ────────────────────\nasync function extractEntities(content: string): Promise<{ entities: Array<{ name: string; type: string; facts: string[] }>; relations: Array<{ from: string; to: string; relation: string }> }> {\n try {\n // Dynamic import to avoid circular dependency (router → config → graph)\n // Dynamic import breaks circular dependency: graph → router → agent → graph. This is intentional.\n const { chat: routerChat } = await import('../providers/router.js');\n const config = loadConfig();\n const model = config.agent.model.toLowerCase();\n\n // Skip entity extraction for very small models that can't produce valid JSON\n const SKIP_MODELS = ['llama3.2:1b', 'tinyllama', 'phi-2'];\n if (SKIP_MODELS.some(m => model.includes(m))) return { entities: [], relations: [] };\n\n logger.info(COMPONENT, `Extracting entities from ${content.length} char episode via ${config.agent.model}`);\n\n const prompt = `Extract named entities and their relationships from this text as JSON.\n\nRULES:\n- Extract MAX 3 entities (most important only)\n- Types MUST be one of: person, project, topic, place, company, technology, social_post\n- SKIP: file paths, URLs, API endpoints, code tokens, session IDs, config keys\n- Keep facts short (under 10 words each, max 2 facts per entity)\n\nReturn format: {\"entities\":[{\"name\":\"...\",\"type\":\"...\",\"facts\":[\"...\"]}],\"relations\":[]}\n\nReturn ONLY valid JSON, no markdown, no explanation.\n\nText: ${content.slice(0, 500)}`;\n\n const response = await routerChat({\n model: config.agent.model,\n messages: [{ role: 'user', content: prompt }],\n maxTokens: 512,\n temperature: 0.1,\n thinking: false,\n });\n\n // Track LLM costs for entity extraction so cost optimizer sees them\n if (response.usage) {\n recordTokenUsage(\n 'graph:extractEntities',\n config.agent.model,\n response.usage.promptTokens ?? 0,\n response.usage.completionTokens ?? 0,\n );\n }\n\n const rawText = response.content || '';\n logger.info(COMPONENT, `Extraction response: ${rawText.length} chars`);\n\n // Strip markdown code fences, leading/trailing noise before regex matching\n const text = rawText\n .replace(/```json\\s*/gi, '')\n .replace(/```\\s*/g, '')\n .trim();\n\n // Match JSON — try structured formats first, then fallback to any JSON\n // 1. {entities:[...], relations:[...]} format\n // 2. [{name:...},...] array format (legacy)\n // 3. Any JSON object with \"name\" key\n // 4. Any JSON array\n const match = text.match(/\\{[\\s\\S]*\"entities\"\\s*:\\s*\\[[\\s\\S]*\\}/)\n ?? text.match(/\\[[\\s\\S]*\"name\"\\s*:[\\s\\S]*\\]/)\n ?? text.match(/\\{[\\s\\S]*\"name\"\\s*:[\\s\\S]*\\}/);\n if (!match) {\n logger.warn(COMPONENT, `No JSON found in extraction response: ${text.slice(0, 200)}`);\n return { entities: [], relations: [] };\n }\n\n // Try to repair common JSON issues from small models\n const jsonStr = match[0]\n .replace(/,\\s*]/g, ']') // trailing commas in arrays\n .replace(/,\\s*}/g, '}') // trailing commas in objects\n .replace(/'/g, '\"'); // single quotes to double\n\n // Attempt parse, with truncated JSON recovery on failure\n const tryParse = (str: string): unknown => {\n try {\n return JSON.parse(str);\n } catch { return null; }\n };\n\n let parsed = tryParse(jsonStr);\n\n // If parse failed, try closing truncated brackets (LLM output cut off mid-JSON)\n if (!parsed) {\n let recovered = jsonStr.replace(/,\\s*\\{[^}]*$/, '');\n const opens = (recovered.match(/\\[/g) || []).length;\n const openBraces = (recovered.match(/\\{/g) || []).length;\n const closeBraces = (recovered.match(/\\}/g) || []).length;\n recovered += '}'.repeat(Math.max(0, openBraces - closeBraces));\n recovered = recovered.replace(/,\\s*}/g, '}').replace(/,\\s*]/g, ']');\n recovered += ']'.repeat(Math.max(0, opens - (recovered.match(/\\]/g) || []).length));\n parsed = tryParse(recovered);\n if (parsed) {\n logger.info(COMPONENT, `Recovered truncated JSON (${jsonStr.length} → ${recovered.length} chars)`);\n }\n }\n\n if (!parsed) {\n logger.warn(COMPONENT, `Entity extraction JSON parse failed, raw: ${jsonStr.slice(0, 200)}`);\n return { entities: [], relations: [] };\n }\n\n // Handle new format: {entities: [...], relations: [...]} or legacy array format\n let rawEntities: unknown[] = [];\n let rawRelations: Array<{ from: string; to: string; relation: string }> = [];\n\n if (Array.isArray(parsed)) {\n // Legacy array format — check if first element has 'entities' key (wrapped in array)\n if (parsed.length === 1 && parsed[0] && typeof parsed[0] === 'object' && 'entities' in (parsed[0] as object)) {\n const obj = parsed[0] as { entities?: unknown[]; relations?: unknown[] };\n rawEntities = Array.isArray(obj.entities) ? obj.entities : [];\n rawRelations = Array.isArray(obj.relations) ? obj.relations as typeof rawRelations : [];\n } else {\n rawEntities = parsed;\n }\n } else if (typeof parsed === 'object' && parsed !== null) {\n // Direct object with entities/relations\n const obj = parsed as unknown as { entities?: unknown[]; relations?: unknown[] };\n rawEntities = Array.isArray(obj.entities) ? obj.entities : [];\n rawRelations = Array.isArray(obj.relations) ? obj.relations as typeof rawRelations : [];\n }\n\n // Validate and filter entities\n const entities = rawEntities\n .filter((e: unknown): e is { name: string; type: string; facts: string[] } =>\n e != null && typeof e === 'object' && 'name' in (e as object) && 'type' in (e as object))\n .filter(e => !isNoiseEntity(e.name))\n .map(e => {\n const type = coerceType(e.type);\n if (!type) return null; // skipped type\n return { name: e.name, type, facts: Array.isArray(e.facts) ? e.facts : [] };\n })\n .filter((e): e is { name: string; type: string; facts: string[] } => e !== null);\n\n // Build validated relations to return alongside entities\n const validatedRelations = rawRelations\n .filter(r => r.from && r.to && r.relation && typeof r.relation === 'string')\n .map(r => ({ from: r.from, to: r.to, relation: r.relation.toLowerCase().replace(/\\s+/g, '_') }));\n\n logger.info(COMPONENT, `Extraction: ${rawEntities.length} raw → ${entities.length} valid entities, ${rawRelations.length} relations`);\n return { entities, relations: validatedRelations };\n } catch (err) {\n logger.warn(COMPONENT, `Entity extraction failed: ${(err as Error).message}`);\n return { entities: [], relations: [] };\n }\n}\n\n/** Word-overlap fuzzy matching — requires >60% shared words or one name is a prefix of the other */\nfunction fuzzyNameMatch(a: string, b: string): boolean {\n // Exact prefix/suffix match for compound names: \"Tony\" vs \"Tony Elliott\"\n const wordsA = a.split(/[\\s\\-_]+/).filter(w => w.length > 0);\n const wordsB = b.split(/[\\s\\-_]+/).filter(w => w.length > 0);\n\n // If one is a single word and the other is multi-word, the single word must be a full word in the other\n if (wordsA.length === 1 && wordsB.length > 1) {\n return wordsB.some(w => w === wordsA[0]);\n }\n if (wordsB.length === 1 && wordsA.length > 1) {\n return wordsA.some(w => w === wordsB[0]);\n }\n\n // Multi-word: require >60% word overlap\n const setA = new Set(wordsA);\n const setB = new Set(wordsB);\n let overlap = 0;\n for (const w of setA) { if (setB.has(w)) overlap++; }\n const minLen = Math.min(setA.size, setB.size);\n return minLen > 0 && overlap / minLen >= 0.6;\n}\n\n// ── Find or create an entity by name ────────────────────────────\nfunction findOrCreateEntity(name: string, type: string, facts: string[]): Entity {\n const nameLower = name.toLowerCase().trim();\n\n // Search by exact name and aliases\n let existing = graph.entities.find((e) => {\n if (e.name.toLowerCase() === nameLower) return true;\n if (Array.isArray(e.aliases) && e.aliases.some((a) => a.toLowerCase() === nameLower)) return true;\n return false;\n });\n\n // Fuzzy match: word-overlap based (not substring) to avoid false merges\n if (!existing) {\n existing = graph.entities.find((e) => {\n if (e.type !== type && type !== 'topic') return false;\n if (nameLower.length < 4 || e.name.length < 4) return false; // skip short names\n if (fuzzyNameMatch(nameLower, e.name.toLowerCase())) return true;\n if (Array.isArray(e.aliases) && e.aliases.some((a) => a.length >= 4 && fuzzyNameMatch(nameLower, a.toLowerCase()))) return true;\n return false;\n });\n if (existing) {\n // Add the shorter name as an alias if not already present\n const shorter = nameLower.length < existing.name.toLowerCase().length ? name : existing.name;\n const longer = nameLower.length >= existing.name.toLowerCase().length ? name : existing.name;\n existing.name = longer; // prefer the longer, more specific name\n if (!existing.aliases.some((a) => a.toLowerCase() === shorter.toLowerCase())) {\n existing.aliases.push(shorter);\n }\n logger.debug(COMPONENT, `Fuzzy-merged \"${name}\" into entity \"${existing.name}\"`);\n }\n }\n\n if (existing) {\n // Merge new facts\n const newFacts = facts.filter((f) => !existing!.facts.includes(f));\n if (newFacts.length > 0) existing.facts.push(...newFacts);\n existing.lastSeen = new Date().toISOString();\n return existing;\n }\n\n // Create new entity\n const entity: Entity = {\n id: uuid(),\n name,\n aliases: [],\n type,\n summary: facts[0] || '',\n firstSeen: new Date().toISOString(),\n lastSeen: new Date().toISOString(),\n facts,\n episodeIds: [],\n };\n // Pre-push bounds check: if at 2x the limit, enforce immediately\n // to prevent unbounded growth if enforceMemoryBounds fails\n if (graph.entities.length >= MAX_ENTITIES * 2) {\n enforceMemoryBounds();\n }\n graph.entities.push(entity);\n entitiesByRecency.push(entity);\n // Keep recency list sorted — single insertion into nearly-sorted list\n // (entity.lastSeen >= most recent existing, so usually already in place)\n if (entitiesByRecency.length > 1) {\n const lastIdx = entitiesByRecency.length - 1;\n const prev = entitiesByRecency[lastIdx - 1];\n if (entity.lastSeen.localeCompare(prev.lastSeen) > 0) {\n // Bubble up one position if needed\n entitiesByRecency[lastIdx] = prev;\n entitiesByRecency[lastIdx - 1] = entity;\n }\n }\n return entity;\n}\n\n// ── Memory Bounds Enforcement ────────────────────────────────────\n\nfunction enforceMemoryBounds(): void {\n // Prune episodes if over limit (keep newest)\n if (graph.episodes.length > MAX_EPISODES) {\n const excess = graph.episodes.length - MAX_EPISODES;\n graph.episodes.splice(0, excess);\n logger.info(COMPONENT, `Pruned ${excess} oldest episodes (limit: ${MAX_EPISODES})`);\n }\n // Prune entities if over limit (keep most recently seen)\n if (graph.entities.length > MAX_ENTITIES) {\n graph.entities.sort((a, b) => b.lastSeen.localeCompare(a.lastSeen));\n const excess = graph.entities.length - MAX_ENTITIES;\n const removed = graph.entities.splice(MAX_ENTITIES, excess);\n const removedIds = new Set(removed.map(e => e.id));\n graph.edges = graph.edges.filter(e => !removedIds.has(e.from) && !removedIds.has(e.to));\n logger.info(COMPONENT, `Pruned ${excess} oldest entities (limit: ${MAX_ENTITIES})`);\n buildGraphIndexes();\n }\n // Cap facts per entity\n for (const entity of graph.entities) {\n if (entity.facts.length > MAX_FACTS_PER_ENTITY) {\n entity.facts = entity.facts.slice(-MAX_FACTS_PER_ENTITY);\n }\n // Truncate individual facts\n entity.facts = entity.facts.map(f => f.length > MAX_FACT_CHARS ? f.slice(0, MAX_FACT_CHARS) + '…' : f);\n }\n}\n\nfunction containsInjection(text: string): boolean {\n return INJECTION_PATTERNS.some(p => p.test(text));\n}\n\n// ── Add Episode ──────────────────────────────────────────────────\n// Phrases that indicate a failed/negative response — don't store these as memories\nconst POISON_PHRASES = [\n 'i do not recall', 'i do not remember', 'i am not able to find',\n 'i am not certain', 'was not retained', 'not stored', 'i did not retain',\n 'my memory does not contain', 'i do not know that specific',\n 'could not find it through search', 'does not appear in my knowledge',\n];\n\nexport async function addEpisode(\n content: string,\n source: string,\n provenance?: {\n source: import('./provenance.js').ProvenanceSource;\n confidence?: number;\n writtenBy?: string;\n },\n): Promise<Episode> {\n if (!initialized) initGraph();\n\n // Guard: don't store injection attempts\n if (containsInjection(content)) {\n logger.warn(COMPONENT, `Blocked injection attempt in episode from ${source}`);\n return { id: '', content, source, createdAt: new Date().toISOString(), entities: [] };\n }\n\n // Guard: don't store TITAN's \"I don't know\" responses — they poison future recall\n const contentLower = content.toLowerCase();\n if (contentLower.includes('[titan') && POISON_PHRASES.some(p => contentLower.includes(p))) {\n return { id: '', content, source, createdAt: new Date().toISOString(), entities: [] };\n }\n\n const episode: Episode = {\n id: uuid(),\n content,\n source,\n createdAt: new Date().toISOString(),\n entities: [],\n };\n graph.episodes.push(episode);\n episodeById.set(episode.id, episode);\n enforceMemoryBounds();\n saveGraph();\n\n // Record provenance if source info provided.\n if (provenance) {\n void (async () => {\n try {\n const { recordProvenance } = await import('./provenance.js');\n recordProvenance({\n memoryId: episode.id,\n memoryType: 'episode',\n source: provenance.source,\n confidence: provenance.confidence,\n writtenBy: provenance.writtenBy,\n content: episode.content,\n });\n } catch { /* ok */ }\n })();\n }\n\n // Index to vector store for semantic search (fire-and-forget)\n if (isVectorSearchAvailable()) {\n addVector(`graph:${episode.id}`, content, 'graph', { source, episodeId: episode.id }).catch(e => logger.debug('Graph', `Vector op failed: ${(e as Error).message}`));\n }\n\n // Background entity extraction (non-blocking)\n extractEntities(content).then((result) => {\n const { entities: extracted, relations } = result;\n if (!extracted || extracted.length === 0) return;\n\n // Ensure graph arrays exist (defensive against race conditions)\n if (!Array.isArray(graph.edges)) graph.edges = [];\n if (!Array.isArray(graph.entities)) graph.entities = [];\n if (!Array.isArray(graph.episodes)) graph.episodes = [];\n\n for (const e of extracted) {\n if (!e.name) continue;\n const entity = findOrCreateEntity(e.name, e.type || 'topic', e.facts || []);\n if (!episode.entities.includes(entity.id)) {\n episode.entities.push(entity.id);\n }\n // Reverse link: entity -> episode\n if (!entity.episodeIds) entity.episodeIds = [];\n if (!entity.episodeIds.includes(episode.id)) {\n entity.episodeIds.push(episode.id);\n }\n }\n // Create edges — use extracted relations when available, fall back to co_mentioned\n const entityNameToId = new Map<string, string>();\n for (const eid of episode.entities) {\n const ent = graph.entities.find(en => en.id === eid);\n if (ent) {\n entityNameToId.set(ent.name.toLowerCase(), eid);\n for (const alias of ent.aliases) entityNameToId.set(alias.toLowerCase(), eid);\n }\n }\n\n // Apply LLM-extracted semantic relations\n let newEdgeCount = 0;\n const MAX_NEW_EDGES = 5;\n const usedPairs = new Set<string>();\n if (relations.length > 0) {\n for (const rel of relations) {\n if (newEdgeCount >= MAX_NEW_EDGES) break;\n const fromId = entityNameToId.get(rel.from.toLowerCase());\n const toId = entityNameToId.get(rel.to.toLowerCase());\n if (!fromId || !toId || fromId === toId) continue;\n const pairKey = [fromId, toId].sort().join(':');\n if (usedPairs.has(pairKey)) continue;\n usedPairs.add(pairKey);\n const exists = graph.edges.some(\n (edge) => (edge.from === fromId && edge.to === toId) || (edge.from === toId && edge.to === fromId)\n );\n if (!exists) {\n graph.edges.push({\n id: uuid(),\n from: fromId,\n to: toId,\n relation: rel.relation || 'related_to',\n createdAt: new Date().toISOString(),\n });\n newEdgeCount++;\n }\n }\n }\n\n // Fall back to co_mentioned for remaining entity pairs (limit to avoid edge explosion)\n const MAX_CO_EDGES = 5;\n let newCoEdgeCount = 0;\n if (episode.entities.length > 1 && episode.entities.length <= 8) {\n for (let i = 0; i < episode.entities.length; i++) {\n if (newCoEdgeCount >= MAX_CO_EDGES) break;\n for (let j = i + 1; j < episode.entities.length; j++) {\n if (newCoEdgeCount >= MAX_CO_EDGES) break;\n const fromId = episode.entities[i];\n const toId = episode.entities[j];\n const pairKey = [fromId, toId].sort().join(':');\n if (usedPairs.has(pairKey)) continue;\n const exists = graph.edges.some(\n (edge) => (edge.from === fromId && edge.to === toId) || (edge.from === toId && edge.to === fromId)\n );\n if (!exists) {\n graph.edges.push({\n id: uuid(),\n from: fromId,\n to: toId,\n relation: 'co_mentioned',\n createdAt: new Date().toISOString(),\n });\n newCoEdgeCount++;\n }\n }\n }\n }\n enforceMemoryBounds();\n saveGraph();\n logger.info(COMPONENT, `Episode ${episode.id.slice(0, 8)}: extracted ${extracted.length} entities, total ${graph.entities.length} entities, ${graph.edges.length} edges`);\n }).catch((err) => logger.warn(COMPONENT, `Background entity extraction failed: ${(err as Error).message}`));\n\n return episode;\n}\n\n// ── Search (hybrid keyword + vector) ─────────────────────────────\nexport async function searchMemory(query: string, limit = 20): Promise<Episode[]> {\n if (!initialized) initGraph();\n if (!query) return getRecentEpisodes(limit);\n\n const STOP_WORDS = new Set(['a', 'an', 'the', 'is', 'it', 'in', 'on', 'at', 'to', 'of', 'do', 'you', 'we', 'i', 'me', 'my', 'that', 'this', 'was', 'are', 'be', 'been', 'have', 'has', 'had', 'and', 'or', 'but', 'if', 'so', 'not', 'no', 'yes', 'can', 'how', 'what', 'about', 'from', 'with', 'for', 'up', 'out', 'its', 'our', 'your', 'they', 'them', 'he', 'she', 'his', 'her', 'will', 'would', 'could', 'should', 'did', 'does', 'just', 'now', 'some', 'any', 'all', 'very', 'too', 'also', 'than', 'then', 'when', 'where', 'who', 'which', 'there', 'here', 'again', 'today', 'earlier', 'remember']);\n const terms = query.toLowerCase().split(/\\s+/).filter(t => t.length > 1 && !STOP_WORDS.has(t));\n const scored = new Map<string, { ep: Episode; score: number }>();\n\n // Keyword search (BM25-style scoring)\n for (const ep of graph.episodes) {\n const text = ep.content.toLowerCase();\n let score = 0;\n for (const term of terms) {\n if (text.includes(term)) {\n score += 1;\n // Boost for term appearing in first 100 chars (title/summary area)\n if (text.slice(0, 100).includes(term)) score += 0.5;\n }\n }\n if (score > 0) {\n scored.set(ep.id, { ep, score });\n }\n }\n\n // Bridge vague queries to specific memories via entity matching\n // When user says \"the joke\" → find entity \"two cannibals eating the Clown\" → find episodes mentioning it\n for (const entity of graph.entities) {\n const entityText = `${entity.name} ${entity.summary || ''} ${entity.facts.join(' ')}`.toLowerCase();\n let entityMatch = false;\n for (const term of terms) {\n if (entityText.includes(term)) { entityMatch = true; break; }\n }\n if (entityMatch) {\n // Use entity.episodeIds (O(1) lookup per episode) instead of scanning all episodes\n for (const epId of (entity.episodeIds || [])) {\n if (!scored.has(epId)) {\n const ep = episodeById.get(epId);\n if (ep) {\n scored.set(epId, { ep, score: 0.8 });\n }\n }\n }\n // Also search for key terms from entity facts in linked episodes\n const factTerms = entity.facts.slice(0, 3).join(' ').toLowerCase().split(/\\s+/).filter(t => t.length > 4);\n for (const epId of (entity.episodeIds || [])) {\n if (!scored.has(epId)) {\n const ep = episodeById.get(epId);\n if (!ep) continue;\n let factScore = 0;\n const contentLower = ep.content.toLowerCase();\n for (const ft of factTerms) {\n if (contentLower.includes(ft)) factScore += 0.3;\n }\n if (factScore > 0.5) scored.set(epId, { ep, score: factScore });\n }\n }\n }\n }\n\n // Vector search augmentation (awaited — previously fire-and-forget caused stale results)\n if (isVectorSearchAvailable()) {\n try {\n const vectorResults = await searchVectors(query, limit * 2, 'graph', 0.35);\n for (const vr of vectorResults) {\n const epId = vr.id.replace('graph:', '');\n const ep = episodeById.get(epId);\n if (!ep) continue;\n const existing = scored.get(ep.id);\n if (existing) {\n existing.score += vr.score * 2; // Boost keyword matches with semantic similarity\n } else {\n scored.set(ep.id, { ep, score: vr.score * 1.5 });\n }\n }\n } catch (e) {\n logger.debug('Graph', `Vector op failed: ${(e as Error).message}`);\n }\n }\n\n return Array.from(scored.values())\n .sort((a, b) => {\n if (Math.abs(b.score - a.score) > 0.1) return b.score - a.score;\n return b.ep.createdAt.localeCompare(a.ep.createdAt);\n })\n .slice(0, limit)\n .map((s) => s.ep);\n}\n\n\n\n// ── Entity lookups ────────────────────────────────────────────────\nexport function getEntity(name: string): Entity | null {\n if (!initialized) initGraph();\n const nameLower = name.toLowerCase().trim();\n return graph.entities.find((e) =>\n e.name.toLowerCase() === nameLower ||\n (Array.isArray(e.aliases) && e.aliases.some((a) => a.toLowerCase() === nameLower))\n ) || null;\n}\n\nexport function listEntities(type?: string): Entity[] {\n if (!initialized) initGraph();\n const all = type ? graph.entities.filter((e) => e.type === type) : graph.entities;\n return all.slice().sort((a, b) => b.lastSeen.localeCompare(a.lastSeen));\n}\n\nexport function getEntityEpisodes(entityId: string, limit = 10): Episode[] {\n if (!initialized) initGraph();\n return graph.episodes\n .filter((ep) => ep.entities.includes(entityId))\n .sort((a, b) => b.createdAt.localeCompare(a.createdAt))\n .slice(0, limit);\n}\n\nexport function getRecentEpisodes(limit = 20): Episode[] {\n if (!initialized) initGraph();\n return graph.episodes\n .slice()\n .sort((a, b) => b.createdAt.localeCompare(a.createdAt))\n .slice(0, limit);\n}\n\nexport function getEpisodesBySource(source: string | string[], limit = 20): Episode[] {\n if (!initialized) initGraph();\n const sources = Array.isArray(source) ? source : [source];\n return graph.episodes\n .filter(ep => sources.includes(ep.source))\n .sort((a, b) => b.createdAt.localeCompare(a.createdAt))\n .slice(0, limit);\n}\n\n// ── Graph data for Mission Control ────────────────────────────────\nexport function getGraphData(): { nodes: GraphNode[]; edges: GraphEdge[] } {\n if (!initialized) initGraph();\n const nodes: GraphNode[] = graph.entities.map((e) => ({\n id: e.id,\n label: e.name,\n type: e.type,\n size: Math.max(12, Math.min(30, 12 + e.facts.length * 3)),\n facts: e.facts,\n }));\n const edges: GraphEdge[] = graph.edges.map((e) => ({\n from: e.from,\n to: e.to,\n label: e.relation,\n }));\n return { nodes, edges };\n}\n\nexport function getGraphStats(): { episodeCount: number; entityCount: number; edgeCount: number } {\n if (!initialized) initGraph();\n return {\n episodeCount: graph.episodes.length,\n entityCount: graph.entities.length,\n edgeCount: graph.edges.length,\n };\n}\n\n/** Get relevant graph context for a user message (for system prompt injection) */\nexport async function getGraphContext(query: string): Promise<string> {\n if (!initialized) initGraph();\n if (graph.episodes.length === 0 && graph.entities.length === 0) return '';\n\n // Check short-term cache for identical queries\n const cached = graphContextCache.get(query);\n if (cached && Date.now() - cached.timestamp < GRAPH_CONTEXT_CACHE_TTL) {\n return cached.result;\n }\n\n const parts: string[] = [];\n\n // Search for relevant episodes — prioritize TITAN's informative responses over user questions and \"I don't know\" responses\n if (query) {\n const relevant = await searchMemory(query, 15);\n // Filter: remove TITAN's \"I don't recall/remember/know\" responses — they poison the context\n // Also remove bare user questions (they don't contain useful info)\n // Keep: TITAN responses with actual content (answers, facts, jokes, etc.)\n const filtered = relevant.filter(ep => {\n const c = ep.content.toLowerCase();\n // Skip \"I don't know\" responses\n if (c.includes('i do not recall') || c.includes('i do not remember') || c.includes('i am not able to find') || c.includes('i am not certain') || c.includes('was not retained') || c.includes('not stored')) return false;\n // Skip bare user questions that are just the same query repeated\n if (c.startsWith('[voice/voice-user]') && c.includes('remember') && c.length < 100) return false;\n return true;\n }).slice(0, 5);\n\n if (filtered.length > 0) {\n parts.push('Relevant memories from past conversations:');\n for (const ep of filtered) {\n parts.push(`- [${ep.source}, ${ep.createdAt.slice(0, 10)}]: ${ep.content.slice(0, 300)}`);\n }\n }\n }\n\n // Search entities with quality scoring\n const queryTerms = query ? query.toLowerCase().split(/\\s+/).filter(t => t.length > 1 && !ENTITY_STOP_WORDS.has(t)) : [];\n const matchedEntities: Array<{ entity: typeof graph.entities[0]; score: number }> = [];\n\n // Type quality weights — persons and projects are more valuable context than generic topics\n\n for (const e of graph.entities) {\n // Skip noise entities that slipped through\n if (isNoiseEntity(e.name)) continue;\n // Skip entities with no facts at all\n if (e.facts.length === 0) continue;\n\n let score = 0;\n const searchText = `${e.name} ${e.summary || ''} ${e.facts.join(' ')}`.toLowerCase();\n for (const term of queryTerms) {\n if (searchText.includes(term)) score += 1;\n if (e.name.toLowerCase().includes(term)) score += 2;\n }\n if (score > 0) {\n // Apply type weight\n score *= (ENTITY_TYPE_WEIGHT[e.type?.toLowerCase()] ?? 0.8);\n // Boost entities with more facts (richer context)\n score *= 1 + Math.min(e.facts.length, 10) * 0.05;\n // Mild recency boost (within last 7 days)\n const ageMs = Date.now() - new Date(e.lastSeen).getTime();\n if (ageMs < 7 * 24 * 3600 * 1000) score *= 1.1;\n matchedEntities.push({ entity: e, score });\n }\n }\n\n matchedEntities.sort((a, b) => b.score - a.score);\n const topMatched = matchedEntities.slice(0, 5).map(m => m.entity);\n\n const matchedIds = new Set(topMatched.map(e => e.name));\n const recentEntities = entitiesByRecency\n .filter(e => !matchedIds.has(e.name) && !isNoiseEntity(e.name) && e.facts.length > 0)\n .slice(0, 3);\n\n const allEntities = [...topMatched, ...recentEntities];\n\n // Build output with token budget (~600 tokens ≈ ~2400 chars)\n const TOKEN_BUDGET = 2400;\n let charCount = 0;\n\n if (allEntities.length > 0) {\n parts.push('Known entities and facts:');\n charCount += 30;\n for (const e of allEntities) {\n if (charCount > TOKEN_BUDGET) break;\n const isMatched = matchedIds.has(e.name);\n const maxFacts = isMatched ? 4 : 2;\n const factsStr = e.facts.length > 0 ? `\\n Facts: ${e.facts.slice(0, maxFacts).join('; ')}` : '';\n const summaryStr = e.summary ? ` — ${e.summary}` : '';\n const line = `- ${e.name} [${e.type}]${summaryStr}${factsStr}`;\n charCount += line.length;\n parts.push(line);\n }\n }\n\n const result = parts.length > 0 ? parts.join('\\n') : '';\n if (graphContextCache.size >= MAX_GRAPH_CONTEXT_CACHE) {\n const oldest = graphContextCache.keys().next().value;\n if (oldest) graphContextCache.delete(oldest);\n }\n graphContextCache.set(query, { result, timestamp: Date.now() });\n return result;\n}\n\n/**\n * Flush important facts from conversation context into graph memory before compaction.\n * Prevents memory loss when context is trimmed during long sessions.\n */\nexport async function flushMemoryBeforeCompaction(messages: Array<{ role: string; content?: string }>): Promise<number> {\n if (!initialized) initGraph();\n\n // Collect user and assistant messages that are about to be compacted\n const contentParts: string[] = [];\n for (const msg of messages) {\n if ((msg.role === 'user' || msg.role === 'assistant') && msg.content) {\n contentParts.push(msg.content.slice(0, 500));\n }\n }\n\n if (contentParts.length === 0) return 0;\n\n const combined = contentParts.join('\\n---\\n').slice(0, 3000);\n\n try {\n const episode = await addEpisode(combined, 'context-flush');\n logger.info(COMPONENT, `Memory flush: saved ${contentParts.length} messages as episode ${episode.id.slice(0, 8)}`);\n return contentParts.length;\n } catch (err) {\n logger.warn(COMPONENT, `Memory flush failed: ${(err as Error).message}`);\n return 0;\n }\n}\n\n/** Clear all graph data */\nexport function clearGraph(): void {\n graph = { episodes: [], entities: [], edges: [] };\n buildGraphIndexes();\n saveGraph();\n logger.info(COMPONENT, 'Graph cleared');\n}\n\n/** Clean up noisy entities and orphaned edges from the graph */\nexport function cleanupGraph(): { removedEntities: number; removedEdges: number; coercedTypes: number } {\n if (!initialized) initGraph();\n\n const beforeEntities = graph.entities.length;\n const beforeEdges = graph.edges.length;\n let coercedTypes = 0;\n\n // 1. Remove noise entities (file paths, URLs, numbers, short tokens)\n const removedIds = new Set<string>();\n graph.entities = graph.entities.filter(e => {\n if (isNoiseEntity(e.name)) {\n removedIds.add(e.id);\n return false;\n }\n return true;\n });\n\n // 2. Coerce entity types to allowed set\n for (const e of graph.entities) {\n const coerced = coerceType(e.type);\n if (coerced === null) {\n removedIds.add(e.id);\n continue;\n }\n if (coerced !== e.type) {\n e.type = coerced;\n coercedTypes++;\n }\n }\n graph.entities = graph.entities.filter(e => !removedIds.has(e.id));\n\n // 3. Remove orphaned edges\n const validIds = new Set(graph.entities.map(e => e.id));\n graph.edges = graph.edges.filter(e => validIds.has(e.from) && validIds.has(e.to));\n\n // 4. Remove entity references from episodes\n for (const ep of graph.episodes) {\n ep.entities = ep.entities.filter(id => validIds.has(id));\n }\n\n const removedEntities = beforeEntities - graph.entities.length;\n const removedEdges = beforeEdges - graph.edges.length;\n\n saveGraph();\n logger.info(COMPONENT, `Cleanup: removed ${removedEntities} entities, ${removedEdges} edges, coerced ${coercedTypes} types. Now ${graph.entities.length} entities, ${graph.edges.length} edges.`);\n return { removedEntities, removedEdges, coercedTypes };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/memory/graph.ts"],"sourcesContent":["/**\n * TITAN — Native Temporal Knowledge Graph\n * Pure TypeScript graph memory: no Docker, no Python, no extra API keys.\n * Uses TITAN's own LLM for entity extraction.\n */\nimport { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from 'fs';\nimport { writeFile, rename } from 'fs/promises';\nimport { join } from 'path';\nimport { homedir } from 'os';\nimport { v4 as uuid } from 'uuid';\nimport { loadConfig } from '../config/config.js';\nimport { recordTokenUsage } from '../agent/costOptimizer.js';\nimport logger from '../utils/logger.js';\nimport { isVectorSearchAvailable, addVector, searchVectors } from './vectors.js';\nimport { getMemoryIndex } from './index.js';\n\nconst COMPONENT = 'Graph';\nconst TITAN_HOME = join(homedir(), '.titan');\nconst GRAPH_PATH = join(TITAN_HOME, 'graph.json');\n\n// Short-term cache for getGraphContext to avoid recomputing identical queries\nconst graphContextCache = new Map<string, { result: string; timestamp: number }>();\nconst GRAPH_CONTEXT_CACHE_TTL = 5000; // 5 seconds\nconst MAX_GRAPH_CONTEXT_CACHE = 100; // prevent unbounded growth\n\nconst ENTITY_STOP_WORDS = new Set(['a', 'an', 'the', 'is', 'it', 'in', 'on', 'at', 'to', 'of', 'do', 'you', 'we', 'i', 'me', 'my', 'that', 'this', 'was', 'are', 'be', 'been', 'have', 'has', 'had', 'and', 'or', 'but', 'if', 'so', 'not', 'no', 'yes', 'can', 'how', 'what', 'about', 'from', 'with', 'for', 'up', 'out', 'its', 'our', 'your', 'they', 'them', 'he', 'she', 'his', 'her', 'will', 'would', 'could', 'should', 'did', 'does', 'just', 'now', 'some', 'any', 'all', 'very', 'too', 'also', 'than', 'then', 'when', 'where', 'who', 'which', 'there', 'here', 'again', 'today', 'earlier', 'remember']);\nconst ENTITY_TYPE_WEIGHT: Record<string, number> = { person: 1.5, project: 1.3, company: 1.2, technology: 1.1, event: 1.0, place: 0.9, topic: 0.7 };\n\n// ── Memory Bounds (Hermes-inspired) ──────────────────────────────\nconst MAX_ENTITIES = 500; // Prune oldest when exceeded\nconst MAX_FACTS_PER_ENTITY = 50; // Cap facts per entity\nconst MAX_EPISODES = 5000; // Prune oldest episodes\nconst MAX_FACT_CHARS = 500; // Max chars per fact entry\n\n// ── Injection Protection ─────────────────────────────────────────\nconst INJECTION_PATTERNS = [\n /ignore\\s+(all\\s+)?previous\\s+instructions/i,\n /you\\s+are\\s+now\\s+a/i,\n /forget\\s+(all\\s+)?your\\s+(instructions|rules)/i,\n /system\\s*:\\s*you\\s+are/i,\n /\\[INST\\]/i,\n /<<SYS>>/i,\n /\\bACT\\s+AS\\b/i,\n /new\\s+instructions?\\s*:/i,\n /override\\s+(the\\s+)?system/i,\n];\n\n// ── Data Model ────────────────────────────────────────────────────\n\nexport interface Episode {\n id: string;\n content: string;\n source: string;\n createdAt: string;\n entities: string[]; // Entity IDs referenced\n}\n\nexport interface Entity {\n id: string;\n name: string;\n aliases: string[];\n type: string; // person | project | topic | place | company | technology | event\n summary: string;\n firstSeen: string;\n lastSeen: string;\n facts: string[];\n episodeIds: string[];\n}\n\n// ── Allowed entity types ─────────────────────────────────────────\nconst ALLOWED_TYPES = new Set(['person', 'project', 'topic', 'place', 'company', 'technology', 'event', 'social_post']);\n\nconst TYPE_COERCION: Record<string, string> = {\n fact: 'topic',\n preference: 'topic',\n feature: 'technology',\n software: 'technology',\n system: 'technology',\n hardware: 'technology',\n component: 'technology',\n tool: 'technology',\n product: 'technology',\n file: '__skip__',\n directory: '__skip__',\n scenario: '__skip__',\n 'person|organization': 'company',\n social_post: 'social_post',\n post: 'social_post',\n};\n\n/** Validate and coerce entity type to allowed set */\nfunction coerceType(raw: string): string | null {\n const t = raw?.toLowerCase().trim();\n if (ALLOWED_TYPES.has(t)) return t;\n const coerced = TYPE_COERCION[t];\n if (coerced === '__skip__') return null; // filter out\n return coerced ?? 'topic'; // default fallback\n}\n\n// Well-known tech names that look like filenames but aren't\nconst NOT_NOISE = new Set(['node.js', 'vue.js', 'next.js', 'nuxt.js', 'three.js', 'p5.js', 'express.js', 'd3.js', 'socket.io']);\n\n/** Check if a name is noise (file paths, URLs, numbers, tokens) */\nfunction isNoiseEntity(name: string): boolean {\n const n = name.trim();\n if (n.length < 2) return true;\n if (/^\\d+$/.test(n)) return true; // pure numbers\n if (/^[/~]/.test(n)) return true; // absolute/home paths\n if (n.includes('/') && n.includes('.') && !n.includes(' ')) return true; // file paths like src/foo.ts\n if (/^https?:\\/\\//i.test(n)) return true; // URLs\n if (!NOT_NOISE.has(n.toLowerCase()) && /^\\w+\\.\\w{1,4}$/.test(n) && /\\.(ts|js|json|md|py|sh|txt|log|html|css|yml|yaml|toml|cfg)$/i.test(n)) return true; // filenames\n if (/^(api\\/|http:|localhost|127\\.0\\.|192\\.168\\.)/.test(n.toLowerCase())) return true; // API paths/IPs\n return false;\n}\n\nexport interface Edge {\n id: string;\n from: string;\n to: string;\n relation: string;\n createdAt: string;\n}\n\nexport interface TitanGraph {\n episodes: Episode[];\n entities: Entity[];\n edges: Edge[];\n}\n\nexport interface GraphNode {\n id: string;\n label: string;\n type: string;\n size: number;\n facts: string[];\n}\n\nexport interface GraphEdge {\n from: string;\n to: string;\n label: string;\n}\n\n// ── In-memory graph ───────────────────────────────────────────────\nlet graph: TitanGraph = { episodes: [], entities: [], edges: [] };\nlet initialized = false;\nlet dirty = false;\n\n// ── Graph indexes (rebuilt on load / mutation) ────────────────────\nlet episodeById = new Map<string, Episode>();\nlet entitiesByRecency: Entity[] = []; // pre-sorted by lastSeen desc\n\nfunction buildGraphIndexes(): void {\n episodeById = new Map(graph.episodes.map(e => [e.id, e]));\n entitiesByRecency = graph.entities\n .slice()\n .sort((a, b) => b.lastSeen.localeCompare(a.lastSeen));\n}\n\n// ── Persistence ───────────────────────────────────────────────────\n// NOTE: Sync I/O is intentional — runs only once at cold start, then cached in-memory.\nfunction loadGraph(): void {\n mkdirSync(TITAN_HOME, { recursive: true });\n if (existsSync(GRAPH_PATH)) {\n try {\n const raw = readFileSync(GRAPH_PATH, 'utf-8');\n const parsed = JSON.parse(raw);\n const rawEdges = Array.isArray(parsed.edges) ? parsed.edges : [];\n // Normalize edges that use source/target/label (legacy) to from/to/relation\n const edges: Edge[] = rawEdges.map((e: Record<string, unknown>) => ({\n id: (e.id as string) || uuid(),\n from: (e.from as string) || (e.source as string) || '',\n to: (e.to as string) || (e.target as string) || '',\n relation: (e.relation as string) || (e.label as string) || 'related',\n createdAt: (e.createdAt as string) || new Date().toISOString(),\n }));\n // Normalize entities to ensure all required fields exist\n const entities = (Array.isArray(parsed.entities) ? parsed.entities : []).map(\n (e: Record<string, unknown>) => ({\n ...e,\n aliases: Array.isArray(e.aliases) ? e.aliases : [],\n summary: (e.summary as string) || '',\n })\n );\n graph = {\n episodes: Array.isArray(parsed.episodes) ? parsed.episodes : [],\n entities,\n edges,\n };\n buildGraphIndexes();\n } catch (e) {\n logger.warn(COMPONENT, `Failed to parse graph.json, starting fresh: ${(e as Error).message}`);\n graph = { episodes: [], entities: [], edges: [] };\n buildGraphIndexes();\n }\n }\n}\n\nlet graphSaveTimeout: ReturnType<typeof setTimeout> | null = null;\n\nfunction saveGraph(): void {\n if (dirty) {\n // Bypass debounce if a previous write failed\n if (graphSaveTimeout) { clearTimeout(graphSaveTimeout); graphSaveTimeout = null; }\n doAsyncSave();\n return;\n }\n if (graphSaveTimeout) clearTimeout(graphSaveTimeout);\n graphSaveTimeout = setTimeout(doAsyncSave, 1000);\n graphSaveTimeout.unref();\n}\n\nlet saveInProgress = false;\n\nasync function doAsyncSave(): Promise<void> {\n if (saveInProgress) {\n dirty = true;\n return;\n }\n saveInProgress = true;\n try {\n const tmpFile = GRAPH_PATH + '.tmp';\n await writeFile(tmpFile, JSON.stringify(graph, null, 2), 'utf-8');\n await rename(tmpFile, GRAPH_PATH);\n dirty = false;\n } catch (e) {\n dirty = true;\n logger.error(COMPONENT, `Failed to save graph.json: ${(e as Error).message}`);\n } finally {\n saveInProgress = false;\n }\n}\n\n/** Flush graph to disk immediately (for shutdown) */\nexport function flushGraph(): void {\n if (graphSaveTimeout) { clearTimeout(graphSaveTimeout); graphSaveTimeout = null; }\n try {\n const tmpFile = GRAPH_PATH + '.tmp';\n writeFileSync(tmpFile, JSON.stringify(graph, null, 2), 'utf-8');\n renameSync(tmpFile, GRAPH_PATH);\n dirty = false;\n } catch (e) {\n dirty = true;\n logger.error(COMPONENT, `Failed to flush graph.json: ${(e as Error).message}`);\n }\n if (dirty) {\n logger.error(COMPONENT, 'DATA MAY BE LOST — failed to flush graph on shutdown');\n }\n}\n\n// ── Init ─────────────────────────────────────────────────────────\nexport function initGraph(): void {\n if (initialized) return;\n loadGraph();\n initialized = true;\n\n // Self-healing: purge poisoned episodes on startup\n const beforeCount = graph.episodes.length;\n graph.episodes = graph.episodes.filter(ep => {\n const c = ep.content.toLowerCase();\n if (c.includes('[titan') && POISON_PHRASES.some(p => c.includes(p))) return false;\n return true;\n });\n buildGraphIndexes();\n const purged = beforeCount - graph.episodes.length;\n if (purged > 0) {\n logger.info(COMPONENT, `Self-heal: purged ${purged} poisoned episodes (negative recall responses)`);\n saveGraph();\n }\n\n logger.info(COMPONENT, `Graph loaded: ${graph.episodes.length} episodes, ${graph.entities.length} entities`);\n\n // Schedule periodic self-healing every 24 hours\n setInterval(() => {\n const before = graph.episodes.length;\n graph.episodes = graph.episodes.filter(ep => {\n const c = ep.content.toLowerCase();\n if (c.includes('[titan') && POISON_PHRASES.some(p => c.includes(p))) return false;\n return true;\n });\n const cleaned = before - graph.episodes.length;\n if (cleaned > 0) {\n logger.info(COMPONENT, `Self-heal (periodic): purged ${cleaned} poisoned episodes`);\n saveGraph();\n }\n }, 24 * 60 * 60 * 1000).unref();\n}\n\n// ── Entity extraction via any configured LLM ────────────────────\nasync function extractEntities(content: string): Promise<{ entities: Array<{ name: string; type: string; facts: string[] }>; relations: Array<{ from: string; to: string; relation: string }> }> {\n try {\n // Dynamic import to avoid circular dependency (router → config → graph)\n // Dynamic import breaks circular dependency: graph → router → agent → graph. This is intentional.\n const { chat: routerChat } = await import('../providers/router.js');\n const config = loadConfig();\n const model = config.agent.model.toLowerCase();\n\n // Skip entity extraction for very small models that can't produce valid JSON\n const SKIP_MODELS = ['llama3.2:1b', 'tinyllama', 'phi-2'];\n if (SKIP_MODELS.some(m => model.includes(m))) return { entities: [], relations: [] };\n\n logger.info(COMPONENT, `Extracting entities from ${content.length} char episode via ${config.agent.model}`);\n\n const prompt = `Extract named entities and their relationships from this text as JSON.\n\nRULES:\n- Extract MAX 3 entities (most important only)\n- Types MUST be one of: person, project, topic, place, company, technology, social_post\n- SKIP: file paths, URLs, API endpoints, code tokens, session IDs, config keys\n- Keep facts short (under 10 words each, max 2 facts per entity)\n\nReturn format: {\"entities\":[{\"name\":\"...\",\"type\":\"...\",\"facts\":[\"...\"]}],\"relations\":[]}\n\nReturn ONLY valid JSON, no markdown, no explanation.\n\nText: ${content.slice(0, 500)}`;\n\n const response = await routerChat({\n model: config.agent.model,\n messages: [{ role: 'user', content: prompt }],\n maxTokens: 512,\n temperature: 0.1,\n thinking: false,\n });\n\n // Track LLM costs for entity extraction so cost optimizer sees them\n if (response.usage) {\n recordTokenUsage(\n 'graph:extractEntities',\n config.agent.model,\n response.usage.promptTokens ?? 0,\n response.usage.completionTokens ?? 0,\n );\n }\n\n const rawText = response.content || '';\n logger.info(COMPONENT, `Extraction response: ${rawText.length} chars`);\n\n // Strip markdown code fences, leading/trailing noise before regex matching\n const text = rawText\n .replace(/```json\\s*/gi, '')\n .replace(/```\\s*/g, '')\n .trim();\n\n // Match JSON — try structured formats first, then fallback to any JSON\n // 1. {entities:[...], relations:[...]} format\n // 2. [{name:...},...] array format (legacy)\n // 3. Any JSON object with \"name\" key\n // 4. Any JSON array\n const match = text.match(/\\{[\\s\\S]*\"entities\"\\s*:\\s*\\[[\\s\\S]*\\}/)\n ?? text.match(/\\[[\\s\\S]*\"name\"\\s*:[\\s\\S]*\\]/)\n ?? text.match(/\\{[\\s\\S]*\"name\"\\s*:[\\s\\S]*\\}/);\n if (!match) {\n logger.warn(COMPONENT, `No JSON found in extraction response: ${text.slice(0, 200)}`);\n return { entities: [], relations: [] };\n }\n\n // Try to repair common JSON issues from small models\n const jsonStr = match[0]\n .replace(/,\\s*]/g, ']') // trailing commas in arrays\n .replace(/,\\s*}/g, '}') // trailing commas in objects\n .replace(/'/g, '\"'); // single quotes to double\n\n // Attempt parse, with truncated JSON recovery on failure\n const tryParse = (str: string): unknown => {\n try {\n return JSON.parse(str);\n } catch { return null; }\n };\n\n let parsed = tryParse(jsonStr);\n\n // If parse failed, try closing truncated brackets (LLM output cut off mid-JSON)\n if (!parsed) {\n let recovered = jsonStr.replace(/,\\s*\\{[^}]*$/, '');\n const opens = (recovered.match(/\\[/g) || []).length;\n const openBraces = (recovered.match(/\\{/g) || []).length;\n const closeBraces = (recovered.match(/\\}/g) || []).length;\n recovered += '}'.repeat(Math.max(0, openBraces - closeBraces));\n recovered = recovered.replace(/,\\s*}/g, '}').replace(/,\\s*]/g, ']');\n recovered += ']'.repeat(Math.max(0, opens - (recovered.match(/\\]/g) || []).length));\n parsed = tryParse(recovered);\n if (parsed) {\n logger.info(COMPONENT, `Recovered truncated JSON (${jsonStr.length} → ${recovered.length} chars)`);\n }\n }\n\n if (!parsed) {\n logger.warn(COMPONENT, `Entity extraction JSON parse failed, raw: ${jsonStr.slice(0, 200)}`);\n return { entities: [], relations: [] };\n }\n\n // Handle new format: {entities: [...], relations: [...]} or legacy array format\n let rawEntities: unknown[] = [];\n let rawRelations: Array<{ from: string; to: string; relation: string }> = [];\n\n if (Array.isArray(parsed)) {\n // Legacy array format — check if first element has 'entities' key (wrapped in array)\n if (parsed.length === 1 && parsed[0] && typeof parsed[0] === 'object' && 'entities' in (parsed[0] as object)) {\n const obj = parsed[0] as { entities?: unknown[]; relations?: unknown[] };\n rawEntities = Array.isArray(obj.entities) ? obj.entities : [];\n rawRelations = Array.isArray(obj.relations) ? obj.relations as typeof rawRelations : [];\n } else {\n rawEntities = parsed;\n }\n } else if (typeof parsed === 'object' && parsed !== null) {\n // Direct object with entities/relations\n const obj = parsed as unknown as { entities?: unknown[]; relations?: unknown[] };\n rawEntities = Array.isArray(obj.entities) ? obj.entities : [];\n rawRelations = Array.isArray(obj.relations) ? obj.relations as typeof rawRelations : [];\n }\n\n // Validate and filter entities\n const entities = rawEntities\n .filter((e: unknown): e is { name: string; type: string; facts: string[] } =>\n e != null && typeof e === 'object' && 'name' in (e as object) && 'type' in (e as object))\n .filter(e => !isNoiseEntity(e.name))\n .map(e => {\n const type = coerceType(e.type);\n if (!type) return null; // skipped type\n return { name: e.name, type, facts: Array.isArray(e.facts) ? e.facts : [] };\n })\n .filter((e): e is { name: string; type: string; facts: string[] } => e !== null);\n\n // Build validated relations to return alongside entities\n const validatedRelations = rawRelations\n .filter(r => r.from && r.to && r.relation && typeof r.relation === 'string')\n .map(r => ({ from: r.from, to: r.to, relation: r.relation.toLowerCase().replace(/\\s+/g, '_') }));\n\n logger.info(COMPONENT, `Extraction: ${rawEntities.length} raw → ${entities.length} valid entities, ${rawRelations.length} relations`);\n return { entities, relations: validatedRelations };\n } catch (err) {\n logger.warn(COMPONENT, `Entity extraction failed: ${(err as Error).message}`);\n return { entities: [], relations: [] };\n }\n}\n\n/** Word-overlap fuzzy matching — requires >60% shared words or one name is a prefix of the other */\nfunction fuzzyNameMatch(a: string, b: string): boolean {\n // Exact prefix/suffix match for compound names: \"Tony\" vs \"Tony Elliott\"\n const wordsA = a.split(/[\\s\\-_]+/).filter(w => w.length > 0);\n const wordsB = b.split(/[\\s\\-_]+/).filter(w => w.length > 0);\n\n // If one is a single word and the other is multi-word, the single word must be a full word in the other\n if (wordsA.length === 1 && wordsB.length > 1) {\n return wordsB.some(w => w === wordsA[0]);\n }\n if (wordsB.length === 1 && wordsA.length > 1) {\n return wordsA.some(w => w === wordsB[0]);\n }\n\n // Multi-word: require >60% word overlap\n const setA = new Set(wordsA);\n const setB = new Set(wordsB);\n let overlap = 0;\n for (const w of setA) { if (setB.has(w)) overlap++; }\n const minLen = Math.min(setA.size, setB.size);\n return minLen > 0 && overlap / minLen >= 0.6;\n}\n\n// ── Find or create an entity by name ────────────────────────────\nfunction findOrCreateEntity(name: string, type: string, facts: string[]): Entity {\n const nameLower = name.toLowerCase().trim();\n\n // Search by exact name and aliases\n let existing = graph.entities.find((e) => {\n if (e.name.toLowerCase() === nameLower) return true;\n if (Array.isArray(e.aliases) && e.aliases.some((a) => a.toLowerCase() === nameLower)) return true;\n return false;\n });\n\n // Fuzzy match: word-overlap based (not substring) to avoid false merges\n if (!existing) {\n existing = graph.entities.find((e) => {\n if (e.type !== type && type !== 'topic') return false;\n if (nameLower.length < 4 || e.name.length < 4) return false; // skip short names\n if (fuzzyNameMatch(nameLower, e.name.toLowerCase())) return true;\n if (Array.isArray(e.aliases) && e.aliases.some((a) => a.length >= 4 && fuzzyNameMatch(nameLower, a.toLowerCase()))) return true;\n return false;\n });\n if (existing) {\n // Add the shorter name as an alias if not already present\n const shorter = nameLower.length < existing.name.toLowerCase().length ? name : existing.name;\n const longer = nameLower.length >= existing.name.toLowerCase().length ? name : existing.name;\n existing.name = longer; // prefer the longer, more specific name\n if (!existing.aliases.some((a) => a.toLowerCase() === shorter.toLowerCase())) {\n existing.aliases.push(shorter);\n }\n logger.debug(COMPONENT, `Fuzzy-merged \"${name}\" into entity \"${existing.name}\"`);\n }\n }\n\n if (existing) {\n // Merge new facts\n const newFacts = facts.filter((f) => !existing!.facts.includes(f));\n if (newFacts.length > 0) existing.facts.push(...newFacts);\n existing.lastSeen = new Date().toISOString();\n return existing;\n }\n\n // Create new entity\n const entity: Entity = {\n id: uuid(),\n name,\n aliases: [],\n type,\n summary: facts[0] || '',\n firstSeen: new Date().toISOString(),\n lastSeen: new Date().toISOString(),\n facts,\n episodeIds: [],\n };\n // Pre-push bounds check: if at 2x the limit, enforce immediately\n // to prevent unbounded growth if enforceMemoryBounds fails\n if (graph.entities.length >= MAX_ENTITIES * 2) {\n enforceMemoryBounds();\n }\n graph.entities.push(entity);\n entitiesByRecency.push(entity);\n // Keep recency list sorted — single insertion into nearly-sorted list\n // (entity.lastSeen >= most recent existing, so usually already in place)\n if (entitiesByRecency.length > 1) {\n const lastIdx = entitiesByRecency.length - 1;\n const prev = entitiesByRecency[lastIdx - 1];\n if (entity.lastSeen.localeCompare(prev.lastSeen) > 0) {\n // Bubble up one position if needed\n entitiesByRecency[lastIdx] = prev;\n entitiesByRecency[lastIdx - 1] = entity;\n }\n }\n return entity;\n}\n\n// ── Memory Bounds Enforcement ────────────────────────────────────\n\nfunction enforceMemoryBounds(): void {\n // Prune episodes if over limit (keep newest)\n if (graph.episodes.length > MAX_EPISODES) {\n const excess = graph.episodes.length - MAX_EPISODES;\n const removed = graph.episodes.splice(0, excess);\n // v5.4.0 / Track B2: drop pruned episodes from the inverted index\n // so search results don't return ids that no longer exist.\n try {\n const idx = getMemoryIndex();\n for (const ep of removed) idx.removeEpisode(ep.id);\n } catch { /* index optional */ }\n logger.info(COMPONENT, `Pruned ${excess} oldest episodes (limit: ${MAX_EPISODES})`);\n }\n // Prune entities if over limit — v5.4.0 / Track B4: salience-aware.\n // Score entities by (entity-type weight × episode reference count) and\n // keep the highest-scoring MAX_ENTITIES; FIFO drops \"Tony\" (a high-\n // value identity entity) when 5000 low-salience log entries land,\n // even though Tony is referenced everywhere. Salience scoring\n // protects against that. lastSeen still breaks ties so genuinely\n // stale entities still age out first.\n if (graph.entities.length > MAX_ENTITIES) {\n const SCORE_FALLBACK = 0.5;\n graph.entities.sort((a, b) => {\n const wA = ENTITY_TYPE_WEIGHT[(a.type || '').toLowerCase()] ?? SCORE_FALLBACK;\n const wB = ENTITY_TYPE_WEIGHT[(b.type || '').toLowerCase()] ?? SCORE_FALLBACK;\n const fA = (a.episodeIds?.length ?? 0) + (a.facts?.length ?? 0);\n const fB = (b.episodeIds?.length ?? 0) + (b.facts?.length ?? 0);\n // Salience = type weight × (1 + frequency). Higher first.\n const salienceA = wA * (1 + fA);\n const salienceB = wB * (1 + fB);\n if (salienceB !== salienceA) return salienceB - salienceA;\n // Tie-breaker: more recently seen survives.\n return b.lastSeen.localeCompare(a.lastSeen);\n });\n const excess = graph.entities.length - MAX_ENTITIES;\n const removed = graph.entities.splice(MAX_ENTITIES, excess);\n const removedIds = new Set(removed.map(e => e.id));\n graph.edges = graph.edges.filter(e => !removedIds.has(e.from) && !removedIds.has(e.to));\n logger.info(COMPONENT, `Pruned ${excess} lowest-salience entities (limit: ${MAX_ENTITIES})`);\n buildGraphIndexes();\n }\n // Cap facts per entity\n for (const entity of graph.entities) {\n if (entity.facts.length > MAX_FACTS_PER_ENTITY) {\n entity.facts = entity.facts.slice(-MAX_FACTS_PER_ENTITY);\n }\n // Truncate individual facts\n entity.facts = entity.facts.map(f => f.length > MAX_FACT_CHARS ? f.slice(0, MAX_FACT_CHARS) + '…' : f);\n }\n}\n\nfunction containsInjection(text: string): boolean {\n return INJECTION_PATTERNS.some(p => p.test(text));\n}\n\n// ── Add Episode ──────────────────────────────────────────────────\n// Phrases that indicate a failed/negative response — don't store these as memories\nconst POISON_PHRASES = [\n 'i do not recall', 'i do not remember', 'i am not able to find',\n 'i am not certain', 'was not retained', 'not stored', 'i did not retain',\n 'my memory does not contain', 'i do not know that specific',\n 'could not find it through search', 'does not appear in my knowledge',\n];\n\nexport async function addEpisode(\n content: string,\n source: string,\n provenance?: {\n source: import('./provenance.js').ProvenanceSource;\n confidence?: number;\n writtenBy?: string;\n },\n options?: {\n /**\n * v5.4.0 / Track B3: when true, addEpisode awaits the entity-extraction\n * pass before returning. Default false preserves the historical\n * non-blocking behaviour for chat hot paths (where the caller doesn't\n * need entities right away). Tests + multi-turn recall paths that DO\n * depend on entities being present immediately should pass true to\n * close the race window.\n */\n awaitEntities?: boolean;\n },\n): Promise<Episode> {\n if (!initialized) initGraph();\n\n // Guard: don't store injection attempts\n if (containsInjection(content)) {\n logger.warn(COMPONENT, `Blocked injection attempt in episode from ${source}`);\n return { id: '', content, source, createdAt: new Date().toISOString(), entities: [] };\n }\n\n // Guard: don't store TITAN's \"I don't know\" responses — they poison future recall\n const contentLower = content.toLowerCase();\n if (contentLower.includes('[titan') && POISON_PHRASES.some(p => contentLower.includes(p))) {\n return { id: '', content, source, createdAt: new Date().toISOString(), entities: [] };\n }\n\n const episode: Episode = {\n id: uuid(),\n content,\n source,\n createdAt: new Date().toISOString(),\n entities: [],\n };\n graph.episodes.push(episode);\n episodeById.set(episode.id, episode);\n // v5.4.0 / Track B2: index for fast keyword search.\n try { getMemoryIndex().addEpisode(episode.id, content); } catch { /* index optional */ }\n enforceMemoryBounds();\n saveGraph();\n\n // Record provenance if source info provided.\n if (provenance) {\n void (async () => {\n try {\n const { recordProvenance } = await import('./provenance.js');\n recordProvenance({\n memoryId: episode.id,\n memoryType: 'episode',\n source: provenance.source,\n confidence: provenance.confidence,\n writtenBy: provenance.writtenBy,\n content: episode.content,\n });\n } catch { /* ok */ }\n })();\n }\n\n // Index to vector store for semantic search (fire-and-forget)\n if (isVectorSearchAvailable()) {\n addVector(`graph:${episode.id}`, content, 'graph', { source, episodeId: episode.id }).catch(e => logger.debug('Graph', `Vector op failed: ${(e as Error).message}`));\n }\n\n // v5.4.0 / Track B3: optionally await entity extraction so callers\n // who run a recall query immediately after addEpisode see the new\n // entities. Otherwise keep the legacy fire-and-forget pattern.\n const extractionPromise = extractEntities(content).then((result) => {\n const { entities: extracted, relations } = result;\n if (!extracted || extracted.length === 0) return;\n\n // Ensure graph arrays exist (defensive against race conditions)\n if (!Array.isArray(graph.edges)) graph.edges = [];\n if (!Array.isArray(graph.entities)) graph.entities = [];\n if (!Array.isArray(graph.episodes)) graph.episodes = [];\n\n for (const e of extracted) {\n if (!e.name) continue;\n const entity = findOrCreateEntity(e.name, e.type || 'topic', e.facts || []);\n if (!episode.entities.includes(entity.id)) {\n episode.entities.push(entity.id);\n }\n // Reverse link: entity -> episode\n if (!entity.episodeIds) entity.episodeIds = [];\n if (!entity.episodeIds.includes(episode.id)) {\n entity.episodeIds.push(episode.id);\n }\n }\n // Create edges — use extracted relations when available, fall back to co_mentioned\n const entityNameToId = new Map<string, string>();\n for (const eid of episode.entities) {\n const ent = graph.entities.find(en => en.id === eid);\n if (ent) {\n entityNameToId.set(ent.name.toLowerCase(), eid);\n for (const alias of ent.aliases) entityNameToId.set(alias.toLowerCase(), eid);\n }\n }\n\n // Apply LLM-extracted semantic relations\n let newEdgeCount = 0;\n const MAX_NEW_EDGES = 5;\n const usedPairs = new Set<string>();\n if (relations.length > 0) {\n for (const rel of relations) {\n if (newEdgeCount >= MAX_NEW_EDGES) break;\n const fromId = entityNameToId.get(rel.from.toLowerCase());\n const toId = entityNameToId.get(rel.to.toLowerCase());\n if (!fromId || !toId || fromId === toId) continue;\n const pairKey = [fromId, toId].sort().join(':');\n if (usedPairs.has(pairKey)) continue;\n usedPairs.add(pairKey);\n const exists = graph.edges.some(\n (edge) => (edge.from === fromId && edge.to === toId) || (edge.from === toId && edge.to === fromId)\n );\n if (!exists) {\n graph.edges.push({\n id: uuid(),\n from: fromId,\n to: toId,\n relation: rel.relation || 'related_to',\n createdAt: new Date().toISOString(),\n });\n newEdgeCount++;\n }\n }\n }\n\n // Fall back to co_mentioned for remaining entity pairs (limit to avoid edge explosion)\n const MAX_CO_EDGES = 5;\n let newCoEdgeCount = 0;\n if (episode.entities.length > 1 && episode.entities.length <= 8) {\n for (let i = 0; i < episode.entities.length; i++) {\n if (newCoEdgeCount >= MAX_CO_EDGES) break;\n for (let j = i + 1; j < episode.entities.length; j++) {\n if (newCoEdgeCount >= MAX_CO_EDGES) break;\n const fromId = episode.entities[i];\n const toId = episode.entities[j];\n const pairKey = [fromId, toId].sort().join(':');\n if (usedPairs.has(pairKey)) continue;\n const exists = graph.edges.some(\n (edge) => (edge.from === fromId && edge.to === toId) || (edge.from === toId && edge.to === fromId)\n );\n if (!exists) {\n graph.edges.push({\n id: uuid(),\n from: fromId,\n to: toId,\n relation: 'co_mentioned',\n createdAt: new Date().toISOString(),\n });\n newCoEdgeCount++;\n }\n }\n }\n }\n enforceMemoryBounds();\n saveGraph();\n logger.info(COMPONENT, `Episode ${episode.id.slice(0, 8)}: extracted ${extracted.length} entities, total ${graph.entities.length} entities, ${graph.edges.length} edges`);\n }).catch((err) => logger.warn(COMPONENT, `Background entity extraction failed: ${(err as Error).message}`));\n\n if (options?.awaitEntities) {\n // Block until entities + edges land. Used by tests + recall-immediately\n // call sites where the race window mattered.\n await extractionPromise;\n }\n\n return episode;\n}\n\n// ── Search (hybrid keyword + vector) ─────────────────────────────\nexport async function searchMemory(query: string, limit = 20): Promise<Episode[]> {\n if (!initialized) initGraph();\n if (!query) return getRecentEpisodes(limit);\n\n const STOP_WORDS = new Set(['a', 'an', 'the', 'is', 'it', 'in', 'on', 'at', 'to', 'of', 'do', 'you', 'we', 'i', 'me', 'my', 'that', 'this', 'was', 'are', 'be', 'been', 'have', 'has', 'had', 'and', 'or', 'but', 'if', 'so', 'not', 'no', 'yes', 'can', 'how', 'what', 'about', 'from', 'with', 'for', 'up', 'out', 'its', 'our', 'your', 'they', 'them', 'he', 'she', 'his', 'her', 'will', 'would', 'could', 'should', 'did', 'does', 'just', 'now', 'some', 'any', 'all', 'very', 'too', 'also', 'than', 'then', 'when', 'where', 'who', 'which', 'there', 'here', 'again', 'today', 'earlier', 'remember']);\n const terms = query.toLowerCase().split(/\\s+/).filter(t => t.length > 1 && !STOP_WORDS.has(t));\n const scored = new Map<string, { ep: Episode; score: number }>();\n\n // v5.4.0 / Track B2: keyword search via inverted index. Was a linear\n // O(n_episodes × n_terms × n_chars) substring scan; now a posting-list\n // walk with TF-IDF scoring. At 5000 episodes this drops query time\n // from ~50ms to ~3ms.\n //\n // Self-heal: if the index is empty (e.g. fresh boot before the\n // backfill has run, or someone called searchMemory before any\n // addEpisode), rebuild it from the graph. Cheap when episodes is\n // small, and idempotent.\n try {\n const idx = getMemoryIndex();\n if (idx.size() === 0 && graph.episodes.length > 0) {\n for (const ep of graph.episodes) idx.addEpisode(ep.id, ep.content);\n }\n const matches = idx.search(query, limit * 3);\n for (const m of matches) {\n const ep = episodeById.get(m.episodeId);\n if (ep) scored.set(m.episodeId, { ep, score: m.score });\n }\n } catch (e) {\n // Index unavailable for any reason — fall back to the legacy linear\n // scan so search never goes silent.\n logger.debug(COMPONENT, `Inverted index unavailable, falling back to linear scan: ${(e as Error).message}`);\n for (const ep of graph.episodes) {\n const text = ep.content.toLowerCase();\n let score = 0;\n for (const term of terms) {\n if (text.includes(term)) {\n score += 1;\n if (text.slice(0, 100).includes(term)) score += 0.5;\n }\n }\n if (score > 0) scored.set(ep.id, { ep, score });\n }\n }\n\n // Bridge vague queries to specific memories via entity matching\n // When user says \"the joke\" → find entity \"two cannibals eating the Clown\" → find episodes mentioning it\n for (const entity of graph.entities) {\n const entityText = `${entity.name} ${entity.summary || ''} ${entity.facts.join(' ')}`.toLowerCase();\n let entityMatch = false;\n for (const term of terms) {\n if (entityText.includes(term)) { entityMatch = true; break; }\n }\n if (entityMatch) {\n // Use entity.episodeIds (O(1) lookup per episode) instead of scanning all episodes\n for (const epId of (entity.episodeIds || [])) {\n if (!scored.has(epId)) {\n const ep = episodeById.get(epId);\n if (ep) {\n scored.set(epId, { ep, score: 0.8 });\n }\n }\n }\n // Also search for key terms from entity facts in linked episodes\n const factTerms = entity.facts.slice(0, 3).join(' ').toLowerCase().split(/\\s+/).filter(t => t.length > 4);\n for (const epId of (entity.episodeIds || [])) {\n if (!scored.has(epId)) {\n const ep = episodeById.get(epId);\n if (!ep) continue;\n let factScore = 0;\n const contentLower = ep.content.toLowerCase();\n for (const ft of factTerms) {\n if (contentLower.includes(ft)) factScore += 0.3;\n }\n if (factScore > 0.5) scored.set(epId, { ep, score: factScore });\n }\n }\n }\n }\n\n // Vector search augmentation (awaited — previously fire-and-forget caused stale results)\n if (isVectorSearchAvailable()) {\n try {\n const vectorResults = await searchVectors(query, limit * 2, 'graph', 0.35);\n for (const vr of vectorResults) {\n const epId = vr.id.replace('graph:', '');\n const ep = episodeById.get(epId);\n if (!ep) continue;\n const existing = scored.get(ep.id);\n if (existing) {\n existing.score += vr.score * 2; // Boost keyword matches with semantic similarity\n } else {\n scored.set(ep.id, { ep, score: vr.score * 1.5 });\n }\n }\n } catch (e) {\n logger.debug('Graph', `Vector op failed: ${(e as Error).message}`);\n }\n }\n\n return Array.from(scored.values())\n .sort((a, b) => {\n if (Math.abs(b.score - a.score) > 0.1) return b.score - a.score;\n return b.ep.createdAt.localeCompare(a.ep.createdAt);\n })\n .slice(0, limit)\n .map((s) => s.ep);\n}\n\n\n\n// ── Entity lookups ────────────────────────────────────────────────\nexport function getEntity(name: string): Entity | null {\n if (!initialized) initGraph();\n const nameLower = name.toLowerCase().trim();\n return graph.entities.find((e) =>\n e.name.toLowerCase() === nameLower ||\n (Array.isArray(e.aliases) && e.aliases.some((a) => a.toLowerCase() === nameLower))\n ) || null;\n}\n\nexport function listEntities(type?: string): Entity[] {\n if (!initialized) initGraph();\n const all = type ? graph.entities.filter((e) => e.type === type) : graph.entities;\n return all.slice().sort((a, b) => b.lastSeen.localeCompare(a.lastSeen));\n}\n\nexport function getEntityEpisodes(entityId: string, limit = 10): Episode[] {\n if (!initialized) initGraph();\n return graph.episodes\n .filter((ep) => ep.entities.includes(entityId))\n .sort((a, b) => b.createdAt.localeCompare(a.createdAt))\n .slice(0, limit);\n}\n\nexport function getRecentEpisodes(limit = 20): Episode[] {\n if (!initialized) initGraph();\n return graph.episodes\n .slice()\n .sort((a, b) => b.createdAt.localeCompare(a.createdAt))\n .slice(0, limit);\n}\n\nexport function getEpisodesBySource(source: string | string[], limit = 20): Episode[] {\n if (!initialized) initGraph();\n const sources = Array.isArray(source) ? source : [source];\n return graph.episodes\n .filter(ep => sources.includes(ep.source))\n .sort((a, b) => b.createdAt.localeCompare(a.createdAt))\n .slice(0, limit);\n}\n\n// ── Graph data for Mission Control ────────────────────────────────\nexport function getGraphData(): { nodes: GraphNode[]; edges: GraphEdge[] } {\n if (!initialized) initGraph();\n const nodes: GraphNode[] = graph.entities.map((e) => ({\n id: e.id,\n label: e.name,\n type: e.type,\n size: Math.max(12, Math.min(30, 12 + e.facts.length * 3)),\n facts: e.facts,\n }));\n const edges: GraphEdge[] = graph.edges.map((e) => ({\n from: e.from,\n to: e.to,\n label: e.relation,\n }));\n return { nodes, edges };\n}\n\nexport function getGraphStats(): { episodeCount: number; entityCount: number; edgeCount: number } {\n if (!initialized) initGraph();\n return {\n episodeCount: graph.episodes.length,\n entityCount: graph.entities.length,\n edgeCount: graph.edges.length,\n };\n}\n\n/** Get relevant graph context for a user message (for system prompt injection) */\nexport async function getGraphContext(query: string): Promise<string> {\n if (!initialized) initGraph();\n if (graph.episodes.length === 0 && graph.entities.length === 0) return '';\n\n // Check short-term cache for identical queries\n const cached = graphContextCache.get(query);\n if (cached && Date.now() - cached.timestamp < GRAPH_CONTEXT_CACHE_TTL) {\n return cached.result;\n }\n\n const parts: string[] = [];\n\n // Search for relevant episodes — prioritize TITAN's informative responses over user questions and \"I don't know\" responses\n if (query) {\n const relevant = await searchMemory(query, 15);\n // Filter: remove TITAN's \"I don't recall/remember/know\" responses — they poison the context\n // Also remove bare user questions (they don't contain useful info)\n // Keep: TITAN responses with actual content (answers, facts, jokes, etc.)\n const filtered = relevant.filter(ep => {\n const c = ep.content.toLowerCase();\n // Skip \"I don't know\" responses\n if (c.includes('i do not recall') || c.includes('i do not remember') || c.includes('i am not able to find') || c.includes('i am not certain') || c.includes('was not retained') || c.includes('not stored')) return false;\n // Skip bare user questions that are just the same query repeated\n if (c.startsWith('[voice/voice-user]') && c.includes('remember') && c.length < 100) return false;\n return true;\n }).slice(0, 5);\n\n if (filtered.length > 0) {\n parts.push('Relevant memories from past conversations:');\n for (const ep of filtered) {\n parts.push(`- [${ep.source}, ${ep.createdAt.slice(0, 10)}]: ${ep.content.slice(0, 300)}`);\n }\n }\n }\n\n // Search entities with quality scoring\n const queryTerms = query ? query.toLowerCase().split(/\\s+/).filter(t => t.length > 1 && !ENTITY_STOP_WORDS.has(t)) : [];\n const matchedEntities: Array<{ entity: typeof graph.entities[0]; score: number }> = [];\n\n // Type quality weights — persons and projects are more valuable context than generic topics\n\n for (const e of graph.entities) {\n // Skip noise entities that slipped through\n if (isNoiseEntity(e.name)) continue;\n // Skip entities with no facts at all\n if (e.facts.length === 0) continue;\n\n let score = 0;\n const searchText = `${e.name} ${e.summary || ''} ${e.facts.join(' ')}`.toLowerCase();\n for (const term of queryTerms) {\n if (searchText.includes(term)) score += 1;\n if (e.name.toLowerCase().includes(term)) score += 2;\n }\n if (score > 0) {\n // Apply type weight\n score *= (ENTITY_TYPE_WEIGHT[e.type?.toLowerCase()] ?? 0.8);\n // Boost entities with more facts (richer context)\n score *= 1 + Math.min(e.facts.length, 10) * 0.05;\n // Mild recency boost (within last 7 days)\n const ageMs = Date.now() - new Date(e.lastSeen).getTime();\n if (ageMs < 7 * 24 * 3600 * 1000) score *= 1.1;\n matchedEntities.push({ entity: e, score });\n }\n }\n\n matchedEntities.sort((a, b) => b.score - a.score);\n const topMatched = matchedEntities.slice(0, 5).map(m => m.entity);\n\n const matchedIds = new Set(topMatched.map(e => e.name));\n const recentEntities = entitiesByRecency\n .filter(e => !matchedIds.has(e.name) && !isNoiseEntity(e.name) && e.facts.length > 0)\n .slice(0, 3);\n\n const allEntities = [...topMatched, ...recentEntities];\n\n // Build output with token budget (~600 tokens ≈ ~2400 chars)\n const TOKEN_BUDGET = 2400;\n let charCount = 0;\n\n if (allEntities.length > 0) {\n parts.push('Known entities and facts:');\n charCount += 30;\n for (const e of allEntities) {\n if (charCount > TOKEN_BUDGET) break;\n const isMatched = matchedIds.has(e.name);\n const maxFacts = isMatched ? 4 : 2;\n const factsStr = e.facts.length > 0 ? `\\n Facts: ${e.facts.slice(0, maxFacts).join('; ')}` : '';\n const summaryStr = e.summary ? ` — ${e.summary}` : '';\n const line = `- ${e.name} [${e.type}]${summaryStr}${factsStr}`;\n charCount += line.length;\n parts.push(line);\n }\n }\n\n const result = parts.length > 0 ? parts.join('\\n') : '';\n if (graphContextCache.size >= MAX_GRAPH_CONTEXT_CACHE) {\n const oldest = graphContextCache.keys().next().value;\n if (oldest) graphContextCache.delete(oldest);\n }\n graphContextCache.set(query, { result, timestamp: Date.now() });\n return result;\n}\n\n/**\n * Flush important facts from conversation context into graph memory before compaction.\n * Prevents memory loss when context is trimmed during long sessions.\n */\nexport async function flushMemoryBeforeCompaction(messages: Array<{ role: string; content?: string }>): Promise<number> {\n if (!initialized) initGraph();\n\n // Collect user and assistant messages that are about to be compacted\n const contentParts: string[] = [];\n for (const msg of messages) {\n if ((msg.role === 'user' || msg.role === 'assistant') && msg.content) {\n contentParts.push(msg.content.slice(0, 500));\n }\n }\n\n if (contentParts.length === 0) return 0;\n\n const combined = contentParts.join('\\n---\\n').slice(0, 3000);\n\n try {\n const episode = await addEpisode(combined, 'context-flush');\n logger.info(COMPONENT, `Memory flush: saved ${contentParts.length} messages as episode ${episode.id.slice(0, 8)}`);\n return contentParts.length;\n } catch (err) {\n logger.warn(COMPONENT, `Memory flush failed: ${(err as Error).message}`);\n return 0;\n }\n}\n\n/** Clear all graph data */\nexport function clearGraph(): void {\n graph = { episodes: [], entities: [], edges: [] };\n buildGraphIndexes();\n saveGraph();\n logger.info(COMPONENT, 'Graph cleared');\n}\n\n/** Clean up noisy entities and orphaned edges from the graph */\nexport function cleanupGraph(): { removedEntities: number; removedEdges: number; coercedTypes: number } {\n if (!initialized) initGraph();\n\n const beforeEntities = graph.entities.length;\n const beforeEdges = graph.edges.length;\n let coercedTypes = 0;\n\n // 1. Remove noise entities (file paths, URLs, numbers, short tokens)\n const removedIds = new Set<string>();\n graph.entities = graph.entities.filter(e => {\n if (isNoiseEntity(e.name)) {\n removedIds.add(e.id);\n return false;\n }\n return true;\n });\n\n // 2. Coerce entity types to allowed set\n for (const e of graph.entities) {\n const coerced = coerceType(e.type);\n if (coerced === null) {\n removedIds.add(e.id);\n continue;\n }\n if (coerced !== e.type) {\n e.type = coerced;\n coercedTypes++;\n }\n }\n graph.entities = graph.entities.filter(e => !removedIds.has(e.id));\n\n // 3. Remove orphaned edges\n const validIds = new Set(graph.entities.map(e => e.id));\n graph.edges = graph.edges.filter(e => validIds.has(e.from) && validIds.has(e.to));\n\n // 4. Remove entity references from episodes\n for (const ep of graph.episodes) {\n ep.entities = ep.entities.filter(id => validIds.has(id));\n }\n\n const removedEntities = beforeEntities - graph.entities.length;\n const removedEdges = beforeEdges - graph.edges.length;\n\n saveGraph();\n logger.info(COMPONENT, `Cleanup: removed ${removedEntities} entities, ${removedEdges} edges, coerced ${coercedTypes} types. Now ${graph.entities.length} entities, ${graph.edges.length} edges.`);\n return { removedEntities, removedEdges, coercedTypes };\n}\n"],"mappings":"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","names":[]}