{"version":3,"sources":["../../../src/server/memory/index.ts","../../../src/server/memory/extract.ts"],"sourcesContent":["/**\n * Memory public surface — interfaces + light, dependency-free helpers.\n *\n * Imported via `@mordn/chat-widget/server/memory`. Pulls NO heavy deps: just the\n * contract and the extraction helpers. The Postgres default adapter lives behind\n * `@mordn/chat-widget/server/memory/drizzle`; mem0 behind `…/memory/mem0`; the\n * hosted HTTP client behind `…/memory/hosted`.\n */\nimport 'server-only';\n\nexport type {\n  Memory,\n  MemoryScope,\n  ListOptions,\n  RetrieveOptions,\n  RecordOptions,\n  MemoryAdapter,\n  MemoryAdapterFactory,\n} from './types';\n\nexport {\n  renderTurn,\n  llmExtract,\n  heuristicExtract,\n  redactDelta,\n  type MemoryDelta,\n} from './extract';\n","/**\n * Post-turn memory extraction.\n *\n * Two strategies, both producing a `{ upserts, deletes }` delta:\n *   1. LLM extraction (recommended) — `generateObject` against a cheap/fast\n *      model with a structured schema; can add new facts AND supersede stale\n *      ones. The system prompt forbids storing secrets/credentials/unvolunteered\n *      PII.\n *   2. Heuristic (fallback, no extra model call) — a small rule set that\n *      captures explicit imperative preferences (\"always…\", \"I prefer…\",\n *      \"call me…\", \"remember that…\") and stores them verbatim.\n *\n * Either way the output runs a redaction pass that drops obvious secret patterns\n * before persistence.\n */\n\nimport 'server-only';\nimport { generateObject, type LanguageModel } from 'ai';\nimport { z } from 'zod';\n\nexport interface MemoryDelta {\n  upserts: { text: string; kind: string }[];\n  deletes: string[];\n}\n\n/** Structured-output schema for LLM extraction (add facts + supersede stale). */\nconst MemoryDeltaSchema = z.object({\n  upserts: z.array(\n    z.object({\n      text: z.string().max(280),\n      kind: z.enum(['preference', 'fact', 'goal', 'context', 'instruction']),\n    }),\n  ),\n  deletes: z.array(z.string()).describe('ids of now-stale memories to remove'),\n});\n\nconst EXTRACTION_SYSTEM_PROMPT = [\n  'You extract durable, salient facts about the USER from a conversation turn.',\n  'Output self-contained, first-person-about-the-user statements (e.g. \"Prefers',\n  'TypeScript\", \"Is migrating off Firebase\"). Prefer UPDATING or SUPERSEDING',\n  'existing facts over duplicating them: if a new statement makes an existing',\n  'memory stale, add the new fact AND list the stale memory id in `deletes`.',\n  'NEVER store secrets, credentials, API keys, tokens, passwords, or PII the',\n  'user did not volunteer as a durable preference. Ignore ephemeral chit-chat',\n  'and one-off task details. If nothing is worth keeping, return empty arrays.',\n  'Each fact <= 280 chars. Allowed kinds: preference, fact, goal, context, instruction.',\n].join(' ');\n\n/** Obvious secret patterns to strip before persistence (defence in depth). */\nconst SECRET_PATTERNS: RegExp[] = [\n  /\\b(sk|pk|rk)-[A-Za-z0-9]{16,}\\b/g, // OpenAI-style keys\n  /\\b[A-Za-z0-9_-]*[Aa][Pp][Ii][_-]?[Kk][Ee][Yy][:=]\\s*\\S+/g, // api_key=...\n  /\\bBearer\\s+[A-Za-z0-9._-]{16,}\\b/gi, // bearer tokens\n  /\\b[A-Fa-f0-9]{40,}\\b/g, // long hex blobs (hashes/keys)\n  /\\beyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\b/g, // JWTs\n];\n\n/** True if a string smells like it contains a secret (drop it entirely). */\nfunction looksSecret(text: string): boolean {\n  return SECRET_PATTERNS.some((re) => {\n    re.lastIndex = 0;\n    return re.test(text);\n  });\n}\n\n/** Drop any upsert that looks like it carries a secret. */\nexport function redactDelta(delta: MemoryDelta): MemoryDelta {\n  return {\n    upserts: delta.upserts.filter((u) => u.text.trim().length > 0 && !looksSecret(u.text)),\n    deletes: delta.deletes,\n  };\n}\n\n/** Flatten a UI/Model message's text parts for the extraction prompt. */\nexport function renderTurn(messages: unknown[]): string {\n  const lines: string[] = [];\n  for (const m of messages) {\n    const msg = m as { role?: string; parts?: Array<{ type?: string; text?: string }>; content?: unknown };\n    const role = msg.role ?? 'user';\n    let text = '';\n    if (Array.isArray(msg.parts)) {\n      text = msg.parts\n        .filter((p) => p?.type === 'text' && typeof p.text === 'string')\n        .map((p) => p.text)\n        .join('');\n    } else if (typeof msg.content === 'string') {\n      text = msg.content;\n    }\n    if (text.trim()) lines.push(`${role.toUpperCase()}: ${text.trim()}`);\n  }\n  return lines.join('\\n');\n}\n\n/** LLM extraction via `generateObject`. Returns an empty delta on any failure. */\nexport async function llmExtract(\n  model: LanguageModel,\n  turnText: string,\n  existing: { id: string; text: string }[],\n): Promise<MemoryDelta> {\n  const existingBlock = existing.length\n    ? existing.map((e) => `- [${e.id}] ${e.text}`).join('\\n')\n    : '(none)';\n  const prompt = [\n    'Existing memories for this user:',\n    existingBlock,\n    '',\n    'Conversation turn:',\n    turnText,\n    '',\n    'Extract durable facts about the user. Return JSON only.',\n  ].join('\\n');\n\n  try {\n    const { object } = await generateObject({\n      model,\n      schema: MemoryDeltaSchema,\n      system: EXTRACTION_SYSTEM_PROMPT,\n      prompt,\n    });\n    return {\n      upserts: Array.isArray(object.upserts) ? object.upserts.slice(0, 20) : [],\n      deletes: Array.isArray(object.deletes) ? object.deletes.slice(0, 20) : [],\n    };\n  } catch {\n    return { upserts: [], deletes: [] };\n  }\n}\n\n/**\n * Heuristic extraction — zero extra model call. Captures explicit imperative\n * preferences the user states. Lower recall, no cost/latency.\n */\nexport function heuristicExtract(turnText: string): MemoryDelta {\n  const upserts: { text: string; kind: string }[] = [];\n  // Only mine USER lines.\n  const userLines = turnText\n    .split('\\n')\n    .filter((l) => l.startsWith('USER:'))\n    .map((l) => l.slice('USER:'.length).trim());\n\n  const triggers: Array<{ re: RegExp; kind: string }> = [\n    { re: /\\b(i prefer|i'd prefer|i like|i love|i hate|i dislike)\\b/i, kind: 'preference' },\n    { re: /\\b(always|never|in future|from now on|going forward)\\b/i, kind: 'instruction' },\n    { re: /\\b(call me|my name is|i am called)\\b/i, kind: 'fact' },\n    { re: /\\b(remember that|note that|keep in mind|fyi)\\b/i, kind: 'context' },\n    { re: /\\b(i'm building|i am building|i'm working on|my goal is|i want to)\\b/i, kind: 'goal' },\n  ];\n\n  for (const line of userLines) {\n    // Consider sentence-level fragments so one long line yields tight facts.\n    for (const sentence of line.split(/(?<=[.!?])\\s+/)) {\n      const s = sentence.trim();\n      if (s.length < 4 || s.length > 280) continue;\n      const hit = triggers.find((t) => t.re.test(s));\n      if (hit) upserts.push({ text: s.replace(/\\s+/g, ' '), kind: hit.kind });\n    }\n  }\n  // De-dupe by text.\n  const seen = new Set<string>();\n  return {\n    upserts: upserts.filter((u) => (seen.has(u.text) ? false : (seen.add(u.text), true))).slice(0, 10),\n    deletes: [],\n  };\n}\n"],"mappings":"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","names":[]}