@amemhq/core 1.0.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/chunk-K6WZTDM7.js +2353 -0
- package/dist/chunk-K6WZTDM7.js.map +1 -0
- package/dist/cli-migrate.cjs +1086 -0
- package/dist/cli-migrate.cjs.map +1 -0
- package/dist/cli-migrate.d.cts +30 -0
- package/dist/cli-migrate.d.ts +30 -0
- package/dist/cli-migrate.js +147 -0
- package/dist/cli-migrate.js.map +1 -0
- package/dist/index.cjs +289 -57
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +181 -28
- package/dist/index.d.ts +181 -28
- package/dist/index.js +82 -2120
- package/dist/index.js.map +1 -1
- package/package.json +4 -1
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/config.ts","../src/embedding.ts","../src/memory.ts","../src/auth.ts","../src/storage.ts","../src/llm.ts","../src/prompts.ts","../src/evo-counter.ts","../src/quality.ts","../src/migrate.ts","../src/crud-guard.ts"],"sourcesContent":["/**\n * config.ts — runtime configuration for the amem engine.\n *\n * `dataDir` holds the evolution-throttle counter and consolidation logs.\n * Defaults to ~/.amem so the engine stays framework-agnostic; the OpenClaw\n * plugin calls configure({ dataDir: '<home>/.openclaw' }) to preserve its\n * existing on-disk location. Override via AMEM_DATA_DIR env var or configure().\n */\nimport * as os from 'os'\nimport * as path from 'path'\n\nlet _dataDir = process.env.AMEM_DATA_DIR || path.join(os.homedir(), '.amem')\n\nexport function configure(opts: { dataDir?: string }): void {\n if (opts.dataDir) _dataDir = opts.dataDir\n}\n\nexport function getDataDir(): string {\n return _dataDir\n}\n","/**\n * embedding.ts — Local ONNX embedding via @huggingface/transformers\n * Matches Python: SentenceTransformer.encode(text, normalize_embeddings=True)\n *\n * The model is selectable because the default is not a good retrieval model: it\n * caps at 128 tokens, so anything longer is truncated before it reaches the\n * vector. Changing it is a breaking change whenever the dimension differs —\n * Qdrant fixes a collection's vector size at creation — so the default stays put\n * and the switch is opt-in. See docs/reference/embedding-models.md.\n */\n\n// Dynamic import to avoid issues with CJS bundling\nlet pipeline: any = null\nlet extractor: any = null\nlet loadedModelName: string | null = null\nlet cachedDim: number | null = null\n\n/** The model shipped since the beginning. Not changed here on purpose. */\nexport const DEFAULT_EMBEDDING_MODEL = 'Xenova/paraphrase-multilingual-MiniLM-L12-v2'\n\n/** Which model this process embeds with. */\nexport function getEmbeddingModel(): string {\n return process.env.AMEM_EMBED_MODEL?.trim() || DEFAULT_EMBEDDING_MODEL\n}\n\n/** How token embeddings are collapsed into one sentence vector. */\nexport type PoolingMode = 'mean' | 'cls'\n\n/**\n * Models trained with CLS pooling, by repo basename.\n *\n * Unlike the vector dimension — which is measured, because a table is silently\n * wrong for anything not in it — pooling cannot be probed: both modes return a\n * correctly-shaped, plausible vector, and only one of them is the model the\n * benchmark measured. So this is a table, and it is wrong-by-omission on purpose:\n * an unlisted model falls back to `mean`, which is what every version before this\n * one did unconditionally.\n *\n * Keyed on the basename so the ONNX mirror and the original resolve alike —\n * `Xenova/bge-m3` and `BAAI/bge-m3` are the same model. Each entry was read from\n * that model's own `1_Pooling/config.json`.\n */\nconst CLS_POOLED_MODELS = new Set([\n 'bge-m3',\n 'bge-base-zh-v1.5',\n 'bge-small-zh-v1.5',\n 'bge-base-en-v1.5',\n 'bge-small-en-v1.5',\n 'bge-large-en-v1.5',\n 'gte-multilingual-base',\n 'gte-modernbert-base',\n 'gte-large-en-v1.5',\n 'snowflake-arctic-embed-m',\n 'snowflake-arctic-embed-l',\n])\n\n/**\n * Which pooling this process uses: `AMEM_EMBED_POOLING`, else the model's known\n * mode, else `mean`.\n *\n * Getting this wrong does not fail. `encode()` returns a normalized vector of the\n * right width either way, and search keeps working because notes and queries are\n * embedded by the same function — it just retrieves worse than the model can,\n * with nothing to indicate it. That is why the mode is resolved rather than\n * assumed.\n */\nexport function getEmbeddingPooling(): PoolingMode {\n const explicit = process.env.AMEM_EMBED_POOLING?.trim().toLowerCase()\n if (explicit === 'mean' || explicit === 'cls') return explicit\n const basename = getEmbeddingModel().split('/').pop()?.toLowerCase() ?? ''\n return CLS_POOLED_MODELS.has(basename) ? 'cls' : 'mean'\n}\n\nasync function getExtractor() {\n const wanted = getEmbeddingModel()\n // Re-resolving on every call keeps the env var honest in tests and lets a\n // long-lived process pick up a change without a restart; the cached vector\n // dimension belongs to the OLD model, so drop it with the extractor.\n if (extractor && loadedModelName === wanted) return extractor\n if (!pipeline) {\n const mod = await import('@huggingface/transformers')\n pipeline = mod.pipeline\n }\n extractor = await pipeline('feature-extraction', wanted, {\n revision: 'main',\n })\n loadedModelName = wanted\n cachedDim = null\n return extractor\n}\n\n/**\n * The vector width this model produces, measured rather than looked up.\n *\n * A hardcoded table would be wrong the moment someone points AMEM_EMBED_MODEL at\n * something not in it, and wrong silently — the collection would be created with\n * the wrong size and every insert would fail. Encoding one short string costs one\n * forward pass on a model that has to load anyway, and is right for any model.\n */\nexport async function getEmbeddingDim(): Promise<number> {\n if (cachedDim !== null && loadedModelName === getEmbeddingModel()) return cachedDim\n const probe = await encode('dimension probe')\n cachedDim = probe.length\n return cachedDim\n}\n\n/**\n * Pool token embeddings into one vector, then L2 normalize.\n * Matches sentence-transformers encode(normalize_embeddings=True).\n *\n * `cls` takes the first token, which is what BGE- and GTE-family models were\n * trained to read; `mean` averages over the attention mask.\n */\nfunction poolNormalize(output: number[][], attentionMask: number[], mode: PoolingMode): number[] {\n const seqLen = output.length\n const dim = output[0].length\n const pooled = new Array(dim).fill(0)\n\n if (mode === 'cls') {\n for (let j = 0; j < dim; j++) pooled[j] = output[0][j]\n } else {\n let maskSum = 0\n for (let i = 0; i < seqLen; i++) {\n const m = attentionMask[i]\n maskSum += m\n for (let j = 0; j < dim; j++) {\n pooled[j] += output[i][j] * m\n }\n }\n for (let j = 0; j < dim; j++) {\n pooled[j] /= Math.max(maskSum, 1e-9)\n }\n }\n\n // L2 normalize\n let norm = 0\n for (const v of pooled) norm += v * v\n norm = Math.sqrt(norm)\n return pooled.map((v) => v / Math.max(norm, 1e-9))\n}\n\n/**\n * Encode text to 384-dim normalized embedding vector.\n * Singleton model, loaded once and reused.\n */\nexport async function encode(text: string): Promise<number[]> {\n const ext = await getExtractor()\n const pooling = getEmbeddingPooling()\n const result = await ext(text, { pooling, normalize: true })\n\n // result.data is a Float32Array of shape [dim]\n // @huggingface/transformers v3 returns already pooled+normalized when pooling+normalize options given\n if (result && result.data) {\n return Array.from(result.data as Float32Array)\n }\n\n // Fallback: manual mean pool if result is nested\n const tensor = result as any\n if (tensor.dims && tensor.dims.length === 3) {\n // shape: [1, seq_len, dim]\n const seqLen = tensor.dims[1]\n const dim = tensor.dims[2]\n const raw: number[][] = []\n for (let i = 0; i < seqLen; i++) {\n const row: number[] = []\n for (let j = 0; j < dim; j++) {\n row.push(tensor.data[i * dim + j])\n }\n raw.push(row)\n }\n return poolNormalize(raw, new Array(seqLen).fill(1), pooling)\n }\n\n throw new Error('Unexpected embedding output shape')\n}\n\n/**\n * Load the model now rather than on the first encode(). A long-lived service\n * pays the download at startup, not on a user's first write.\n */\nexport async function loadModel(): Promise<void> {\n await getExtractor()\n}\n\n/**\n * Whether the model is resident. Synchronous and I/O-free — unlike encode() it\n * can never trigger the several-hundred-megabyte download, so a health check is\n * free to poll it.\n */\nexport function isModelLoaded(): boolean {\n return extractor !== null\n}\n\n/**\n * Cosine similarity between two normalized vectors (already L2-normalized → just dot product)\n */\nexport function cosineSimilarity(a: number[], b: number[]): number {\n let dot = 0\n for (let i = 0; i < a.length; i++) dot += a[i] * b[i]\n return dot\n}\n","/**\n * memory.ts — A-MEM core logic: addMemory, searchMemory, listMemories\n * Full TypeScript port of amem_client.py\n */\n\nimport { v4 as uuidv4 } from 'uuid'\nimport { createHash } from 'crypto'\nimport * as fs from 'fs'\nimport * as path from 'path'\nimport { encode, cosineSimilarity } from './embedding.js'\nimport { createStorageContext, type MemoryNote, type StorageContext } from './storage.js'\nimport { canWrite } from './auth.js'\nimport {\n llmConstructNote,\n llmShouldLink,\n llmEvolveNote,\n llmShouldMerge,\n llmEvolutionJudge,\n llmConflictScan,\n} from './llm.js'\nimport { shouldRunEvolution } from './evo-counter.js'\nimport { getDataDir } from './config.js'\nimport { Jieba } from '@node-rs/jieba'\n\n/**\n * Strip line breaks from an identifier before it reaches a log line. Agent ids\n * come from session/config input and these logs go to a plain console, so a\n * crafted id could otherwise forge extra log entries (CodeQL: js/log-injection).\n */\nconst logSafe = (id: string): string => id.replace(/[\\r\\n]/g, '')\n\n// ── BM25 helpers ──────────────────────────────────────────────────────────────\n\n// Lazy-initialized Jieba instance (Story 21: Chinese word segmentation)\nlet _jieba: Jieba | null = null\nfunction getJieba(): Jieba {\n if (!_jieba) _jieba = new Jieba()\n return _jieba\n}\n\n/**\n * Tokenize text for BM25 indexing.\n * Story 21: Chinese text is segmented with Jieba (HMM mode) before indexing.\n * Non-Chinese text falls back to whitespace/word-boundary splitting.\n * Mixed text (e.g. \"检索Qdrant结果\") is handled correctly — Jieba preserves\n * ASCII tokens as-is while segmenting CJK spans.\n */\nexport function simpleTokenize(text: string): string[] {\n const hasChinese = /[\\u4e00-\\u9fff]/.test(text)\n if (hasChinese) {\n // Jieba cut with HMM=true for unknown word recognition\n return getJieba()\n .cut(text, true)\n .map((t) => t.toLowerCase().trim())\n .filter((t) => t.length > 0 && /[\\w\\u4e00-\\u9fff]/.test(t))\n }\n return Array.from(text.toLowerCase().matchAll(/[\\w]+/g)).map((m) => m[0])\n}\n\nexport interface BM25State {\n ids: string[]\n corpus: string[][]\n idf: Map<string, number>\n avgdl: number\n}\n\nexport function buildBM25(notes: MemoryNote[]): BM25State {\n const ids = notes.map((n) => n.id)\n const corpus = notes.map((n) => {\n const text = [n.content, ...n.keywords, ...n.tags].join(' ')\n return simpleTokenize(text)\n })\n\n // IDF\n const df = new Map<string, number>()\n for (const tokens of corpus) {\n for (const t of new Set(tokens)) df.set(t, (df.get(t) ?? 0) + 1)\n }\n const N = corpus.length\n const idf = new Map<string, number>()\n df.forEach((freq, term) => {\n idf.set(term, Math.log((N - freq + 0.5) / (freq + 0.5) + 1))\n })\n\n const avgdl = corpus.reduce((s, t) => s + t.length, 0) / Math.max(N, 1)\n return { ids, corpus, idf, avgdl }\n}\n\nexport function bm25Score(state: BM25State, queryTokens: string[], k1 = 1.5, b = 0.75): [string, number][] {\n const scores: [string, number][] = state.ids.map((id, i) => {\n const doc = state.corpus[i]\n const dl = doc.length\n const tf = new Map<string, number>()\n for (const t of doc) tf.set(t, (tf.get(t) ?? 0) + 1)\n\n let score = 0\n for (const t of queryTokens) {\n const f = tf.get(t) ?? 0\n if (f === 0) continue\n const idfVal = state.idf.get(t) ?? 0\n score += idfVal * ((f * (k1 + 1)) / (f + k1 * (1 - b + b * (dl / state.avgdl))))\n }\n return [id, score]\n })\n return scores.sort((a, b) => b[1] - a[1])\n}\n\n// ── RRF merge ─────────────────────────────────────────────────────────────────\nexport function rrfMerge(embIds: string[], bm25Ids: string[], k = 60): [string, number][] {\n const scores = new Map<string, number>()\n embIds.forEach((id, rank) => scores.set(id, (scores.get(id) ?? 0) + 1 / (k + rank + 1)))\n bm25Ids.forEach((id, rank) => scores.set(id, (scores.get(id) ?? 0) + 1 / (k + rank + 1)))\n return Array.from(scores.entries()).sort((a, b) => b[1] - a[1])\n}\n\n// ── Build embedding text (same as Python) ─────────────────────────────────────\nexport function buildEmbedText(note: Pick<MemoryNote, 'content' | 'keywords' | 'tags' | 'context'>): string {\n let text = note.content\n if (note.keywords.length) text += ' ' + note.keywords.join(' ')\n if (note.tags.length) text += ' ' + note.tags.join(' ')\n if (note.context) text += ' ' + note.context\n return text\n}\n\n// ── Story 31: Quality gate ────────────────────────────────────────────────────\nconst EPHEMERAL_SIGNALS = ['待跑', '等确认', '昨日', '明天完成']\n\nexport interface QualityCheckResult {\n ok: boolean\n ephemeral: boolean\n reason?: string\n}\n\nexport function checkQuality(content: string): QualityCheckResult {\n const trimmed = content.trim()\n if (trimmed.length < 10) {\n return { ok: false, ephemeral: false, reason: `内容过短(${trimmed.length} 字,最少 10 字)` }\n }\n const ephemeral = EPHEMERAL_SIGNALS.some((w) => trimmed.includes(w))\n return { ok: true, ephemeral }\n}\n\n// ── Story 32: default storage context helper ──────────────────────────────────\n/** Returns the default storage context (mode A: shared collection, agent filter). */\nfunction defaultCtx(): StorageContext {\n return createStorageContext()\n}\n\n// ── addMemory ─────────────────────────────────────────────────────────────────\nexport async function addMemory(\n content: string,\n agentId = 'main',\n opts?: {\n scope?: 'private' | 'shared'\n storageCtx?: StorageContext\n /**\n * Story 44: who this memory is about. Empty (the default) means it is about\n * the world or the agent itself, and stays visible whoever is present.\n */\n subjects?: string[]\n }\n): Promise<string> {\n const scope = opts?.scope ?? 'private'\n const subjects = opts?.subjects ?? []\n const ctx = opts?.storageCtx ?? defaultCtx()\n\n // ── Story 31: Quality gate ──────────────────────────────────────────────────\n const quality = checkQuality(content)\n if (!quality.ok) {\n throw new Error(`[quality] 写入拒绝: ${quality.reason}`)\n }\n\n // ── Story 32: effective agent_id for the stored note ─────────────────────────\n // shared scope writes agent_id='shared'; private scope writes the real agentId\n const effectiveNoteAgentId = scope === 'shared' ? 'shared' : agentId\n\n // ── Layer 1: Exact hash dedup (before LLM & embedding, cheapest check) ──────\n const hash = createHash('md5').update(content).digest('hex')\n const existingByHash = await ctx.findByHash(hash, agentId)\n if (existingByHash) {\n console.log(`[add] dedup: exact hash match, skipping (id=${existingByHash.id.slice(0, 8)})`)\n return existingByHash.id\n }\n\n console.log('[add] Constructing note...')\n\n // Step 1: Note Construction\n const { keywords, tags, context, category, note_type, topics } = await llmConstructNote(content)\n console.log(` keywords: ${keywords.join(', ')}`)\n console.log(` tags: ${tags.join(', ')}`)\n console.log(` context: ${context}`)\n console.log(` category: ${category}`)\n console.log(` note_type: ${note_type}`)\n console.log(` topics: ${topics.join(', ')}`)\n\n const fieldsText = buildEmbedText({ content, keywords, tags, context })\n const embedding = await encode(fieldsText)\n\n // ── Layer 2: High-similarity vector dedup (UPDATE instead of INSERT) ─────────\n const topMatch = await ctx.queryByEmbedding(embedding, 1, agentId, 0.0)\n if (topMatch.length > 0 && topMatch[0].score >= 0.85) {\n // Story 33: the query also returns SHARED notes owned by other agents. Only\n // fold into the match when we may write it; otherwise fall through and insert\n // our own note rather than overwriting someone else's memory.\n if (canWrite(topMatch[0].note, agentId)) {\n console.log(`[add] dedup: high-sim match (sim=${topMatch[0].score.toFixed(3)}), updating existing`)\n await ctx.updateNoteContent(topMatch[0].note.id, content, embedding, hash)\n return topMatch[0].note.id\n }\n console.log(\n `[add] dedup: high-sim match ${topMatch[0].note.id.slice(0, 8)} is not writable by ${logSafe(agentId)} — inserting a new note instead`\n )\n }\n\n // ── Layer 2b: Pending merge flag for borderline similarity (0.72-0.85) ──────\n const pendingMerge = topMatch.length > 0 && topMatch[0].score >= 0.72 && topMatch[0].score < 0.85\n if (pendingMerge) {\n console.log(`[add] dedup: borderline sim (sim=${topMatch[0].score.toFixed(3)}), marking pending_merge=true`)\n }\n\n // ── Story 32: ownership and access control fields ─────────────────────────\n const readers: string[] = scope === 'shared' ? ['*'] : [agentId]\n const writers: string[] = [agentId]\n\n const note: MemoryNote = {\n id: uuidv4(),\n subjects,\n content,\n timestamp: new Date().toISOString(),\n keywords,\n tags,\n context,\n embedding,\n links: [],\n agent_id: effectiveNoteAgentId,\n hash,\n // 13-A\n retrieval_count: 0,\n last_accessed: new Date().toISOString(),\n // 13-B\n evolution_history: [],\n // 13-E\n category,\n is_active: true,\n // 26A\n note_type,\n // 26B\n topics,\n // 29\n pending_merge: pendingMerge,\n // 30\n conflict: false,\n // 31\n ephemeral: quality.ephemeral,\n low_quality: false,\n // 32\n owner: agentId,\n readers,\n writers,\n }\n\n // Save first\n await ctx.addNote(note)\n console.log(` saved note ${note.id}`)\n\n // Step 2: Link Generation\n try {\n const total = await ctx.countNotes(agentId)\n if (total > 1) {\n const candidates = await ctx.queryByEmbedding(embedding, 6, agentId, 0.0)\n\n const linkedIds: string[] = []\n const linkedContents: string[] = []\n\n for (const { note: cand, score } of candidates) {\n if (cand.id === note.id) continue\n if (score < 0.3) continue\n\n console.log(` candidate ${cand.id.slice(0, 8)}... sim=${score.toFixed(3)}, asking LLM...`)\n const shouldLink = await llmShouldLink(content, cand.content)\n if (shouldLink) {\n linkedIds.push(cand.id)\n linkedContents.push(cand.content)\n console.log(` → linked!`)\n }\n }\n\n if (linkedIds.length > 0) {\n note.links = linkedIds\n await ctx.updateNote(note)\n\n // Bidirectional links — Story 33: only write the back-link into notes we\n // may write. A linked note can be another agent's shared note; the forward\n // link on our own note still stands.\n for (const lid of linkedIds) {\n const linked = await ctx.getNote(lid)\n if (linked && !linked.links.includes(note.id)) {\n if (!canWrite(linked, agentId)) {\n console.log(`[link] back-link into ${lid.slice(0, 8)} skipped — not writable by ${logSafe(agentId)}`)\n continue\n }\n linked.links.push(note.id)\n await ctx.updateNote(linked)\n }\n }\n\n // Step 3: Memory Evolution (up to 3) — gated by evo_threshold (Story 13-C)\n if (shouldRunEvolution()) {\n console.log(` [evo] threshold reached, running evolution for ${Math.min(linkedIds.length, 3)} linked notes`)\n for (const lid of linkedIds.slice(0, 3)) {\n const linked = await ctx.getNote(lid)\n if (!linked) continue\n // Story 33: evolution rewrites the linked note's tags/context/embedding.\n // Skip notes we may not write (e.g. another agent's shared note) — this\n // one check covers every mutation the evolution of `linked` would make.\n if (!canWrite(linked, agentId)) {\n console.log(` [evo] skipping ${lid.slice(0, 8)} — not writable by ${logSafe(agentId)}`)\n continue\n }\n\n // Gather link contents and IDs — Story 36: `linked` may be a shared\n // note whose links name its owner's private notes. Reading by id\n // bypasses the agent filter, so pass the caller; unreadable\n // neighbours come back null and never reach the LLM prompt.\n const linkedNotes: Array<{ id: string; content: string }> = []\n for (const llid of linked.links.slice(0, 5)) {\n if (llid === note.id) continue\n const ln = await ctx.getNote(llid, agentId)\n if (ln) linkedNotes.push({ id: ln.id, content: ln.content })\n }\n linkedNotes.push({ id: note.id, content }) // new note exactly once\n\n const oldTags = [...linked.tags]\n const oldContext = linked.context\n\n const {\n tags: newTags,\n context: newContext,\n shouldStrengthen,\n suggestedConnections,\n tagsToUpdate,\n } = await llmEvolveNote(linked.content, linkedNotes)\n\n let evolved = false\n\n // Standard update_neighbor action\n if (newTags !== null || newContext !== null) {\n if (newTags !== null) linked.tags = newTags\n if (newContext !== null) linked.context = newContext\n\n linked.evolution_history = linked.evolution_history || []\n linked.evolution_history.push({\n triggeredBy: note.id,\n triggeredAt: new Date().toISOString(),\n oldContext,\n newContext: newContext ?? oldContext,\n oldTags,\n newTags: newTags ?? oldTags,\n action: 'update_neighbor',\n })\n evolved = true\n }\n\n let noteChanged = false\n let noteTagsChanged = false\n\n // Strengthen action\n if (shouldStrengthen && suggestedConnections.length > 0) {\n // 1. 双向链接绑定\n for (const targetId of suggestedConnections) {\n if (!note.links.includes(targetId)) {\n note.links.push(targetId)\n noteChanged = true\n }\n // Story 36: targetId comes from the LLM's suggestedConnections —\n // an arbitrary id, not something the agent filter vetted.\n const target = await ctx.getNote(targetId, agentId)\n if (target && !target.links.includes(note.id)) {\n // Story 33: strengthen reaches notes via the link neighbourhood,\n // which can include notes this agent may not write.\n if (canWrite(target, agentId)) {\n target.links.push(note.id)\n await ctx.updateNote(target)\n } else {\n console.log(` [evo] strengthen back-link into ${targetId.slice(0, 8)} skipped — not writable`)\n }\n }\n }\n // 2. 更新新写入 memory 的 note.tags\n if (tagsToUpdate.length > 0) {\n note.tags = tagsToUpdate\n noteChanged = true\n noteTagsChanged = true\n }\n\n // 3. 记录 strengthen 操作到被加强的邻居 (linked) 的 evolution_history 中\n linked.evolution_history = linked.evolution_history || []\n linked.evolution_history.push({\n triggeredBy: note.id,\n triggeredAt: new Date().toISOString(),\n oldContext: linked.context,\n newContext: linked.context,\n oldTags: [...linked.tags],\n newTags: [...linked.tags],\n action: 'strengthen',\n suggestedConnections,\n tagsUpdated: tagsToUpdate,\n })\n evolved = true\n }\n\n if (noteChanged) {\n if (noteTagsChanged) {\n note.embedding = await encode(buildEmbedText(note))\n }\n await ctx.updateNote(note)\n }\n\n if (evolved) {\n // Re-compute embedding after evolution\n if (newTags !== null || newContext !== null) {\n linked.embedding = await encode(buildEmbedText(linked))\n }\n await ctx.updateNote(linked)\n console.log(` evolved/strengthened note ${lid.slice(0, 8)}...`)\n }\n }\n } else {\n console.log(` [evo] threshold not reached, skipping evolution this round`)\n }\n }\n }\n } catch (e) {\n console.error(`[warn] Link/Evolution phase failed: ${(e as Error).message}`)\n }\n\n console.log(`[done] Note added: ${note.id}`)\n return note.id\n}\n\n// ── addEpisodic ───────────────────────────────────────────────────────────────\n/**\n * The cheap write path: quality gate → embed the raw content → store.\n *\n * Deliberately skips LLM note construction, similarity dedup, link generation\n * and evolution, so a real-time caller (a game brain logging events tick by\n * tick) never pays for an LLM round-trip. Cost is one embed + one upsert.\n *\n * Episodic notes are an **append-only, faithful event log**: the same content\n * written twice is two events, so there is no hash or vector dedup here.\n * Evolution rewrites a note's context over time — precisely what you do not\n * want for \"remember the time the ender dragon killed us\". The offline\n * consolidation pass distils these raw events into long-term, linked notes.\n */\nexport async function addEpisodic(\n content: string,\n agentId = 'main',\n opts?: {\n scope?: 'private' | 'shared'\n storageCtx?: StorageContext\n /** Story 44: who this episode is about. Empty = world/self. */\n subjects?: string[]\n }\n): Promise<string> {\n const scope = opts?.scope ?? 'private'\n const subjects = opts?.subjects ?? []\n const ctx = opts?.storageCtx ?? defaultCtx()\n\n const quality = checkQuality(content)\n if (!quality.ok) {\n throw new Error(`[quality] 写入拒绝: ${quality.reason}`)\n }\n\n // No note construction, so keywords/tags/context/topics stay empty and the\n // embedding covers the raw content alone.\n const embedding = await encode(content)\n const now = new Date().toISOString()\n\n const note: MemoryNote = {\n id: uuidv4(),\n subjects,\n content,\n timestamp: now,\n keywords: [],\n tags: [],\n context: '',\n embedding,\n links: [],\n agent_id: scope === 'shared' ? 'shared' : agentId,\n hash: createHash('md5').update(content).digest('hex'),\n retrieval_count: 0,\n last_accessed: now,\n evolution_history: [],\n category: 'General',\n is_active: true,\n note_type: 'memory',\n topics: [],\n pending_merge: false,\n conflict: false,\n ephemeral: quality.ephemeral,\n low_quality: false,\n owner: agentId,\n readers: scope === 'shared' ? ['*'] : [agentId],\n writers: [agentId],\n }\n\n await ctx.addNote(note)\n return note.id\n}\n\n// ── searchMemory ──────────────────────────────────────────────────────────────\nexport interface SearchResult {\n id: string\n content: string\n context: string\n tags: string[]\n keywords: string[]\n links: string[]\n timestamp: string\n similarity: number\n rrf: number\n // Story 26B\n topics: string[]\n note_type: 'memory' | 'knowledge'\n}\n\nexport async function searchMemory(\n query: string,\n topK = 5,\n agentId = 'main',\n opts?: {\n useBfs?: boolean\n // Story 22: BFS relevance gate — linked notes with cos-sim below this threshold\n // are skipped to reduce noise. Set to 0 to disable (admit all linked notes).\n bfsSimThreshold?: number\n // Story 26B: if set, only return knowledge notes that contain ALL of these topics\n topicsFilter?: string[]\n // Story 32: optional storage context for mode B isolation\n storageCtx?: StorageContext\n /**\n * Story 44: scope retrieval to one person. Returns memories that name them\n * plus memories that name nobody (world facts, facts about the agent).\n * Omitted = no person scoping, i.e. today's behaviour.\n */\n subject?: string\n }\n): Promise<SearchResult[]> {\n const useBfs = opts?.useBfs !== false // default true\n const subject = opts?.subject\n const bfsSimThreshold = opts?.bfsSimThreshold ?? 0.25 // Story 22 default\n const ctx = opts?.storageCtx ?? defaultCtx()\n const total = await ctx.countNotes(agentId)\n if (total === 0) return []\n\n // Embedding retrieval\n const queryEmbedding = await encode(query)\n const n = Math.min(Math.max(topK * 4, 20), total)\n const embResults = await ctx.queryByEmbedding(queryEmbedding, n, agentId, 0.0, subject)\n\n // BM25 retrieval\n // Scope this too: it feeds BM25 AND the BFS neighbourhood map, so leaving it\n // unscoped would leak another person's memories through keyword search or a\n // link expansion even though the vector path was filtered.\n const allNotes = await ctx.listNotes(agentId, subject)\n const bm25State = buildBM25(allNotes)\n const queryTokens = simpleTokenize(query)\n const bm25Ranked = bm25Score(bm25State, queryTokens).slice(0, n)\n\n // RRF fusion with retrieval_count heat boost (Story 13-A)\n const merged = rrfMerge(\n embResults.map((r) => r.note.id),\n bm25Ranked.map((r) => r[0])\n )\n\n // Story 23: heat boost with time decay\n // Older frequently-retrieved notes should not permanently outrank fresher ones.\n // Score = RRF × (1 + 0.05 × ln(1 + retrieval_count) / (age_days + 1))\n // age_days is measured from last_accessed so re-retrieval resets the clock.\n const now = Date.now()\n const noteMap = new Map(allNotes.map((n) => [n.id, n]))\n const boostedMerged: [string, number][] = merged.map(([id, rrfScore]) => {\n const note = noteMap.get(id)\n if (!note) return [id, rrfScore]\n // Story 26A: knowledge notes are timeless — skip time decay boost\n if (note.note_type === 'knowledge') return [id, rrfScore]\n const lastAccessed = new Date(note.last_accessed || note.timestamp).getTime()\n const ageDays = (now - lastAccessed) / 86_400_000\n const recencyBoost = 1 + (0.05 * Math.log(1 + (note.retrieval_count || 0))) / (ageDays + 1)\n return [id, rrfScore * recencyBoost]\n })\n boostedMerged.sort((a, b) => b[1] - a[1])\n\n const topIds = boostedMerged.slice(0, topK).map(([id]) => id)\n\n // Story 18: 2-hop BFS link expansion (can be disabled via opts.useBfs=false for ablation)\n // Walk the link graph up to 2 hops from each top result to surface\n // contextually related notes that scored too low for direct retrieval.\n const BFS_MAX_HOPS = 2\n const BFS_MAX_EXPAND = 8 // max extra notes to add via BFS (cap to avoid bloat)\n const visitedIds = new Set<string>(topIds)\n const bfsQueue: Array<{ id: string; hop: number }> = useBfs ? topIds.map((id) => ({ id, hop: 0 })) : []\n const bfsExtra: string[] = [] // IDs discovered via BFS, in discovery order\n\n while (bfsQueue.length > 0 && bfsExtra.length < BFS_MAX_EXPAND) {\n const item = bfsQueue.shift()!\n if (item.hop >= BFS_MAX_HOPS) continue\n const note = noteMap.get(item.id)\n if (!note) continue\n for (const linkedId of note.links) {\n if (visitedIds.has(linkedId)) continue\n visitedIds.add(linkedId)\n // Only include active notes (is_active !== false)\n const linked = noteMap.get(linkedId)\n if (!linked || linked.is_active === false) continue\n // Story 22: relevance gate — skip BFS nodes too far from the query\n if (bfsSimThreshold > 0 && linked.embedding) {\n const sim = cosineSimilarity(queryEmbedding, linked.embedding)\n if (sim < bfsSimThreshold) continue\n }\n bfsExtra.push(linkedId)\n bfsQueue.push({ id: linkedId, hop: item.hop + 1 })\n if (bfsExtra.length >= BFS_MAX_EXPAND) break\n }\n }\n\n // Story 26B: apply topicsFilter — keep only knowledge notes that contain ALL requested topics\n const topicsFilter = opts?.topicsFilter\n const filteredTopIds =\n topicsFilter && topicsFilter.length > 0\n ? topIds.filter((id) => {\n const note = noteMap.get(id)\n if (!note) return false\n if (note.note_type !== 'knowledge') return true // pass-through non-knowledge notes\n return topicsFilter.every((t) => note.topics.map((s) => s.toLowerCase()).includes(t.toLowerCase()))\n })\n : topIds\n\n // Build result map\n const embSimMap = new Map(embResults.map((r) => [r.note.id, r.score]))\n const rrfMap = new Map(boostedMerged.map(([id, score]) => [id, score]))\n\n const results: SearchResult[] = []\n for (const id of [...filteredTopIds, ...bfsExtra]) {\n const note = noteMap.get(id)\n if (!note) continue\n results.push({\n id: note.id,\n content: note.content,\n context: note.context,\n tags: note.tags,\n keywords: note.keywords,\n links: note.links,\n timestamp: note.timestamp,\n similarity: embSimMap.get(id) ?? 0,\n rrf: rrfMap.get(id) ?? 0,\n topics: note.topics ?? [],\n note_type: note.note_type ?? 'memory',\n })\n }\n\n return results\n}\n\n// ── listMemories ──────────────────────────────────────────────────────────────\nexport async function listMemories(agentId = 'main', storageCtx?: StorageContext): Promise<{ count: number }> {\n const ctx = storageCtx ?? defaultCtx()\n const count = await ctx.countNotes(agentId)\n return { count }\n}\n\n// ── mergeSimilarNotes ──────────────────────────────────────────────────────────\n\n/** Sleep helper */\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n\n/**\n * Merge semantically similar notes written today.\n * Called asynchronously from agent_end hook; failures are silent.\n * Returns the number of notes merged (deleted).\n *\n * Story 30: pending_merge=true notes are routed through LLM evolution judgment\n * (EVOLVE/CONFLICT/EXPAND/NEW) instead of simple merge.\n * Story 32: shared notes (agent_id='shared') are never merged/consolidated.\n */\nexport async function mergeSimilarNotes(agentId: string, storageCtx?: StorageContext): Promise<number> {\n const ctx = storageCtx ?? defaultCtx()\n const today = new Date().toISOString().slice(0, 10) // \"YYYY-MM-DD\"\n const allNotes = await ctx.getNotesByDatePrefix(today, agentId)\n\n // Story 32: only process private notes — skip shared entries entirely\n const notes = allNotes.filter((n) => n.agent_id !== 'shared')\n\n // ── Story 30: Evolution processing for pending_merge notes ────────────────\n const pendingNotes = notes.filter((n) => n.pending_merge === true)\n let evolvedCount = 0\n\n for (const pendingNote of pendingNotes) {\n // Find the closest non-pending neighbor\n let bestSim = -1\n let bestNeighbor: MemoryNote | null = null\n for (const other of notes) {\n if (other.id === pendingNote.id) continue\n if (other.pending_merge) continue\n if (!other.embedding.length || !pendingNote.embedding.length) continue\n const sim = cosineSimilarity(pendingNote.embedding, other.embedding)\n if (sim > bestSim) {\n bestSim = sim\n bestNeighbor = other\n }\n }\n\n if (!bestNeighbor) {\n await ctx.patchNotePayload(pendingNote.id, { pending_merge: false })\n continue\n }\n\n const judgment = await llmEvolutionJudge(bestNeighbor.content, pendingNote.content)\n console.log(\n `[merge] evolution judgment: ${pendingNote.id.slice(0, 8)} → ${bestNeighbor.id.slice(0, 8)}: ${judgment.type}`\n )\n\n if (judgment.type === 'EVOLVE') {\n const oldHistory = bestNeighbor.evolution_history || []\n oldHistory.push({\n triggeredBy: pendingNote.id,\n triggeredAt: new Date().toISOString(),\n oldContext: bestNeighbor.context,\n newContext: bestNeighbor.context,\n oldTags: [...bestNeighbor.tags],\n newTags: [...bestNeighbor.tags],\n action: 'consolidate',\n })\n const mergedContent = judgment.mergedContent || pendingNote.content\n const newEmbedding = await encode(buildEmbedText({ ...bestNeighbor, content: mergedContent }))\n const newHash = createHash('md5').update(mergedContent).digest('hex')\n await ctx.updateNoteContent(bestNeighbor.id, mergedContent, newEmbedding, newHash)\n await ctx.patchNotePayload(bestNeighbor.id, {\n evolution_history: JSON.stringify(oldHistory),\n evolution_type: 'EVOLVE',\n })\n await ctx.deleteNote(pendingNote.id)\n evolvedCount++\n } else if (judgment.type === 'CONFLICT') {\n await ctx.patchNotePayload(pendingNote.id, { pending_merge: false, conflict: true, evolution_type: 'CONFLICT' })\n await ctx.patchNotePayload(bestNeighbor.id, { conflict: true, evolution_type: 'CONFLICT' })\n } else if (judgment.type === 'EXPAND') {\n const oldHistory = bestNeighbor.evolution_history || []\n oldHistory.push({\n triggeredBy: pendingNote.id,\n triggeredAt: new Date().toISOString(),\n oldContext: bestNeighbor.context,\n newContext: bestNeighbor.context,\n oldTags: [...bestNeighbor.tags],\n newTags: [...bestNeighbor.tags],\n action: 'consolidate',\n })\n const mergedContent = judgment.mergedContent || `${bestNeighbor.content};${pendingNote.content}`\n const newEmbedding = await encode(buildEmbedText({ ...bestNeighbor, content: mergedContent }))\n const newHash = createHash('md5').update(mergedContent).digest('hex')\n await ctx.updateNoteContent(bestNeighbor.id, mergedContent, newEmbedding, newHash)\n await ctx.patchNotePayload(bestNeighbor.id, {\n evolution_history: JSON.stringify(oldHistory),\n evolution_type: 'EXPAND',\n })\n await ctx.deleteNote(pendingNote.id)\n evolvedCount++\n } else {\n // NEW — just clear pending_merge\n await ctx.patchNotePayload(pendingNote.id, { pending_merge: false, evolution_type: 'NEW' })\n }\n\n await sleep(200)\n }\n\n // ── Original merge logic for non-pending notes ────────────────────────────\n // Not enough notes to bother\n if (notes.length < 5) return evolvedCount\n\n // Build list of (i, j) candidate pairs with sim >= 0.80\n // Exclude pending_merge notes (already handled above)\n const pendingIds = new Set(pendingNotes.map((n) => n.id))\n\n interface SimPair {\n i: number\n j: number\n sim: number\n }\n\n const pairs: SimPair[] = []\n for (let i = 0; i < notes.length; i++) {\n if (pendingIds.has(notes[i].id)) continue\n for (let j = i + 1; j < notes.length; j++) {\n if (pendingIds.has(notes[j].id)) continue\n if (!notes[i].embedding.length || !notes[j].embedding.length) continue\n const sim = cosineSimilarity(notes[i].embedding, notes[j].embedding)\n if (sim >= 0.8) {\n pairs.push({ i, j, sim })\n }\n }\n }\n\n if (pairs.length === 0) return evolvedCount\n\n // Sort by similarity descending, take top 10\n pairs.sort((a, b) => b.sim - a.sim)\n const topPairs = pairs.slice(0, 10)\n\n // Track deleted IDs to skip stale pairs\n const deletedIds = new Set<string>()\n let mergedCount = 0\n\n for (const { i, j } of topPairs) {\n const noteA = notes[i]\n const noteB = notes[j]\n\n // Skip if either has already been deleted\n if (deletedIds.has(noteA.id) || deletedIds.has(noteB.id)) continue\n\n const result = await llmShouldMerge(noteA.content, noteB.content)\n\n if (result.shouldMerge && result.merged) {\n // Keep the longer note (more complete), update its content, delete the other\n const [keepNote, dropNote] = noteA.content.length >= noteB.content.length ? [noteA, noteB] : [noteB, noteA]\n\n const newEmbedding = await encode(result.merged)\n const newHash = createHash('md5').update(result.merged).digest('hex')\n await ctx.updateNoteContent(keepNote.id, result.merged, newEmbedding, newHash)\n await ctx.deleteNote(dropNote.id)\n deletedIds.add(dropNote.id)\n mergedCount++\n }\n\n // Rate-limit: 200ms between LLM calls\n await sleep(200)\n }\n\n return evolvedCount + mergedCount\n}\n\n/**\n * Consolidate semantically similar memories.\n * Performs deep deduplication by category and similarity score.\n * Story 32: shared notes (agent_id='shared') are skipped entirely.\n */\nexport async function consolidateMemories(agentId: string, logger?: any, storageCtx?: StorageContext): Promise<number> {\n const ctx = storageCtx ?? defaultCtx()\n const log = {\n info: (msg: string) => (logger ? logger.info(msg) : console.log(msg)),\n warn: (msg: string) => (logger ? logger.warn(msg) : console.warn(msg)),\n error: (msg: string) => (logger ? logger.error(msg) : console.error(msg)),\n }\n\n log.info(`[Consolidation] Starting consolidation for agentId: ${agentId}`)\n\n // 1. 加载记忆:获取所有活动(is_active: true)记忆条目\n const rawNotes = await ctx.listNotes(agentId)\n // Story 32: skip shared notes — they are read-only for non-owners\n const allNotes = rawNotes.filter((n) => n.agent_id !== 'shared')\n log.info(\n `[Consolidation] Loaded ${allNotes.length} active private notes (${rawNotes.length - allNotes.length} shared skipped).`\n )\n\n // 2. 分类分组:根据 category 字段将记忆分组\n // Story 26A: skip knowledge notes — they are durable and should not be merged\n const groups = new Map<string, MemoryNote[]>()\n for (const note of allNotes) {\n if (note.note_type === 'knowledge') continue\n const category = note.category || 'General'\n if (!groups.has(category)) {\n groups.set(category, [])\n }\n groups.get(category)!.push(note)\n }\n\n // 3. 两两比对:在每个分类分组内计算余弦相似度\n interface CandidatePair {\n noteA: MemoryNote\n noteB: MemoryNote\n similarity: number\n }\n const candidates: CandidatePair[] = []\n\n for (const [category, groupNotes] of groups.entries()) {\n log.info(`[Consolidation] Category \"${category}\" has ${groupNotes.length} notes.`)\n for (let i = 0; i < groupNotes.length; i++) {\n for (let j = i + 1; j < groupNotes.length; j++) {\n const noteA = groupNotes[i]\n const noteB = groupNotes[j]\n if (!noteA.embedding.length || !noteB.embedding.length) continue\n const sim = cosineSimilarity(noteA.embedding, noteB.embedding)\n if (sim >= 0.75) {\n candidates.push({ noteA, noteB, similarity: sim })\n }\n }\n }\n }\n\n // 4. 筛选候选对:按相似度从高到低排序,限制最多 15 对\n candidates.sort((a, b) => b.similarity - a.similarity)\n const topPairs = candidates.slice(0, 15)\n log.info(\n `[Consolidation] Found ${candidates.length} candidate pairs with similarity >= 0.75. Processing top ${topPairs.length}.`\n )\n\n const processedIds = new Set<string>()\n let mergedCount = 0\n\n // Helper: append log to log file\n function logMergeToFile(keepId: string, dropId: string, mergedContent: string) {\n const logDir = path.join(getDataDir(), 'logs')\n const logFile = path.join(logDir, 'amem-consolidate.log')\n const timestamp = new Date().toISOString()\n const logMsg = `[${timestamp}] Consolidated: KeepNote ${keepId} and DropNote ${dropId}. Merged length: ${mergedContent.length} chars.\\n`\n\n try {\n fs.mkdirSync(logDir, { recursive: true })\n fs.appendFileSync(logFile, logMsg, 'utf8')\n } catch (err) {\n log.error(`[Consolidation] Failed to write log: ${(err as Error).message}`)\n }\n }\n\n // 5. LLM 融合决策与信息继承\n for (const { noteA, noteB, similarity } of topPairs) {\n if (processedIds.has(noteA.id) || processedIds.has(noteB.id)) {\n log.info(\n `[Consolidation] Skipping pair (${noteA.id.slice(0, 8)}, ${noteB.id.slice(0, 8)}) as one or both already merged.`\n )\n continue\n }\n\n log.info(\n `[Consolidation] Evaluating pair (${noteA.id.slice(0, 8)}, ${noteB.id.slice(0, 8)}) with sim ${similarity.toFixed(4)}...`\n )\n const mergeDecision = await llmShouldMerge(noteA.content, noteB.content)\n\n if (mergeDecision.shouldMerge && mergeDecision.merged) {\n log.info(` -> LLM decision: MERGE!`)\n\n // 比较两条记忆的长度,将较长的保留作为主节点 (KeepNote)\n const [keepNote, dropNote] = noteA.content.length >= noteB.content.length ? [noteA, noteB] : [noteB, noteA]\n\n log.info(\n ` -> KeepNote: ${keepNote.id.slice(0, 8)} (len: ${keepNote.content.length}), DropNote: ${dropNote.id.slice(0, 8)} (len: ${dropNote.content.length})`\n )\n\n const oldContext = keepNote.context\n const oldTags = [...keepNote.tags]\n\n // 内容更新\n keepNote.content = mergeDecision.merged\n\n // 元数据继承:\n // - 合并 tags 与 keywords(合并后去重)\n keepNote.tags = Array.from(new Set([...keepNote.tags, ...dropNote.tags]))\n keepNote.keywords = Array.from(new Set([...keepNote.keywords, ...dropNote.keywords]))\n\n // - 合并 links 链接数组(去重且排除自身 and DropNote)\n keepNote.links = Array.from(new Set([...keepNote.links, ...dropNote.links])).filter(\n (id) => id !== keepNote.id && id !== dropNote.id\n )\n\n // - retrieval_count 累加\n keepNote.retrieval_count = (keepNote.retrieval_count || 0) + (dropNote.retrieval_count || 0)\n\n // - last_accessed 取最新的时间戳\n const keepAccessTime = new Date(keepNote.last_accessed || keepNote.timestamp).getTime()\n const dropAccessTime = new Date(dropNote.last_accessed || dropNote.timestamp).getTime()\n keepNote.last_accessed =\n keepAccessTime >= dropAccessTime\n ? keepNote.last_accessed || keepNote.timestamp\n : dropNote.last_accessed || dropNote.timestamp\n\n // 重算 embedding 与 MD5 hash\n const embedText = buildEmbedText(keepNote)\n keepNote.embedding = await encode(embedText)\n keepNote.hash = createHash('md5').update(keepNote.content).digest('hex')\n\n // 将 DropNote 的合并事件记录在 KeepNote 的 evolution_history 中\n keepNote.evolution_history = keepNote.evolution_history || []\n keepNote.evolution_history.push({\n triggeredBy: dropNote.id,\n triggeredAt: new Date().toISOString(),\n oldContext,\n newContext: keepNote.context,\n oldTags,\n newTags: keepNote.tags,\n action: 'consolidate',\n })\n\n // 更新 KeepNote\n await ctx.updateNote(keepNote)\n\n // 软删除 DropNote\n await ctx.invalidateNote(dropNote.id)\n\n // 级联更新 links\n await ctx.replaceLinkReferences(dropNote.id, keepNote.id, agentId)\n\n // 写入日志\n logMergeToFile(keepNote.id, dropNote.id, keepNote.content)\n\n processedIds.add(keepNote.id)\n processedIds.add(dropNote.id)\n mergedCount++\n } else {\n log.info(` -> LLM decision: DO NOT MERGE.`)\n }\n\n // Rate limit sleep\n await sleep(200)\n }\n\n log.info(`[Consolidation] Completed consolidation run. Merged ${mergedCount} pairs.`)\n return mergedCount\n}\n\n// ── Story 43: cold-layer contradiction sweep ──────────────────────────────────\n\n/**\n * How a detected contradiction is handled.\n *\n * `review` (default) marks both notes and leaves the decision to a human — the\n * safe option, because even a strong model is only around 55% accurate at\n * spotting implicit contradictions.\n *\n * `auto` additionally retires the older note of each pair. It needs no human,\n * but at that accuracy roughly two in five retirements will silence a memory\n * that was still true. The retirement is a soft delete, so it is recoverable —\n * but for a system answering in real time, \"recoverable\" only helps once someone\n * notices. Documented as such; opt in deliberately.\n */\nexport type ConflictMode = 'review' | 'auto'\n\nfunction resolveConflictMode(override?: ConflictMode): ConflictMode {\n const raw = (process.env.AMEM_CONFLICT_MODE || override || 'review').trim().toLowerCase()\n return raw === 'auto' ? 'auto' : 'review'\n}\n\n/** How many notes go to the model in one scan. Bounds cost and prompt size. */\nconst CONFLICT_BATCH_SIZE = 25\n\nexport interface ConflictSweepResult {\n scanned: number\n pairsFound: number\n retired: number\n batchesScanned: number\n batchesSkipped: number\n}\n\n/**\n * Find memories that contradict each other and mark them.\n *\n * This is the cold half of the tiering split. The per-turn CRUD decision runs on\n * a cheap model, which is safe (the update guard stops it writing to the wrong\n * note) but dull — it misses contradictions it should have caught. This sweep is\n * what catches them: it runs offline, in batches, on the strong tier, and it\n * sees far more context than any single turn does.\n *\n * Batches by category and hands each batch to the model whole, rather than\n * pairing by similarity — see llmConflictScan for why that distinction is the\n * entire point.\n */\nexport async function conflictSweep(\n agentId: string,\n opts?: {\n mode?: ConflictMode\n storageCtx?: StorageContext\n logger?: { info: (m: string) => void }\n /** Re-read every batch, including ones already judged. For a full re-sweep\n * after changing the prompt or the model. */\n force?: boolean\n }\n): Promise<ConflictSweepResult> {\n const ctx = opts?.storageCtx ?? defaultCtx()\n const force = opts?.force === true\n const mode = resolveConflictMode(opts?.mode)\n const log = opts?.logger?.info ?? ((m: string) => console.log(m))\n\n const raw = await ctx.listNotes(agentId)\n // Only this agent's own episodic notes: shared notes belong to someone else,\n // and knowledge notes are durable reference rather than claims about a person.\n const notes = raw.filter((n) => n.agent_id !== 'shared' && n.note_type !== 'knowledge' && n.is_active !== false)\n\n const groups = new Map<string, MemoryNote[]>()\n for (const n of notes) {\n const c = n.category || 'General'\n if (!groups.has(c)) groups.set(c, [])\n groups.get(c)!.push(n)\n }\n\n let pairsFound = 0\n let retired = 0\n\n let batchesScanned = 0\n let batchesSkipped = 0\n\n for (const [category, groupNotes] of groups.entries()) {\n // Newest first, so a note written today batches with the most recent notes\n // in its category — the ones it is most likely to contradict.\n groupNotes.sort((a, b) => Date.parse(b.timestamp) - Date.parse(a.timestamp))\n\n for (let start = 0; start < groupNotes.length; start += CONFLICT_BATCH_SIZE) {\n const batch = groupNotes.slice(start, start + CONFLICT_BATCH_SIZE)\n if (batch.length < 2) continue\n\n // Skip a batch the model has already read in full. Without this the sweep\n // re-judges every old pair every night: ~40 calls a run here, growing with\n // the store, to re-derive answers it already had. With it, a run costs one\n // call per batch that actually gained a note.\n //\n // The tradeoff, stated plainly: a new note is compared against the batch\n // it lands in, not against the category's entire history. Contradictions\n // between two OLD notes that were never batched together are not found.\n // That is the price of not paying for a full re-read daily; a full sweep\n // is still available by clearing conflict_scanned_at.\n if (!force && batch.every((n) => n.conflict_scanned_at)) {\n batchesSkipped++\n continue\n }\n batchesScanned++\n\n const pairs = await llmConflictScan(batch.map((n) => n.content))\n for (const { a, b, reason, supersededIndex } of pairs) {\n const noteA = batch[a]\n const noteB = batch[b]\n if (!noteA || !noteB) continue\n pairsFound++\n\n // Mark BOTH sides, each pointing at the other, so the pair can be shown\n // as one decision instead of two disconnected review entries.\n await ctx.patchNotePayload(noteA.id, {\n conflict: true,\n evolution_type: 'CONFLICT',\n conflicts_with: Array.from(new Set([...(noteA.conflicts_with ?? []), noteB.id])),\n conflict_reason: reason,\n })\n await ctx.patchNotePayload(noteB.id, {\n conflict: true,\n evolution_type: 'CONFLICT',\n conflicts_with: Array.from(new Set([...(noteB.conflicts_with ?? []), noteA.id])),\n conflict_reason: reason,\n })\n log(`[conflict] ${category}: ${noteA.id.slice(0, 8)} ↔ ${noteB.id.slice(0, 8)} — ${reason}`)\n\n if (mode === 'auto') {\n // Retire the side the MODEL judged superseded — never the older-by-\n // wall-clock one. A note records when it was WRITTEN, not when the fact\n // became true, and the two come apart constantly: \"back in 2019 I was\n // vegetarian\", written today, is the newer row and the older fact.\n // Retiring by ingestion time would then silence the CURRENT memory.\n const superseded = supersededIndex === a ? noteA : supersededIndex === b ? noteB : null\n if (!superseded) {\n // The model could not tell. Marking already happened above, so the\n // conflict is visible for review — but nothing gets retired on a\n // guess. This is the safe half of auto mode, not a failure.\n log(`[conflict] auto: no superseded side identified — marked only, nothing retired`)\n } else {\n const ok = await ctx.invalidateNote(superseded.id, agentId)\n if (ok) {\n retired++\n log(`[conflict] auto-retired the superseded note ${superseded.id.slice(0, 8)}`)\n }\n }\n }\n }\n\n // Mark the whole batch, not just the notes in a pair: what was judged is\n // the batch as a set, so re-reading it would ask the same question again.\n const scannedAt = new Date().toISOString()\n for (const n of batch) {\n await ctx.patchNotePayload(n.id, { conflict_scanned_at: scannedAt })\n }\n }\n }\n\n log(\n `[conflict] ${batchesScanned} batch(es) scanned, ${batchesSkipped} already up to date; ` +\n `${pairsFound} pair(s) found, ${retired} retired`\n )\n return { scanned: notes.length, pairsFound, retired, batchesScanned, batchesSkipped }\n}\n","/**\n * auth.ts — write authorization for the Access Protocol (Story 33).\n *\n * Story 32 gave every note `owner` / `readers` / `writers` and enforced `readers`\n * at query time. It deliberately left `writers` unenforced. The consequence: the\n * agent filter matches `agent_id == caller OR agent_id == 'shared'`, so ANY query\n * can return another agent's shared note — and every mutation then wrote to it\n * unchecked. An audit found eight such write sites (dedup, link generation,\n * evolution ×2, CRUD update/delete, quality scan, link rewriting).\n *\n * This is the one rule they all gate on. Kept pure and dependency-free so the\n * policy is unit-testable on its own and identical everywhere it is applied.\n */\nimport type { MemoryNote } from './storage.js'\n\n/**\n * May `callerAgentId` mutate `note`?\n *\n * True when the caller owns it, is listed in `writers`, or `writers` is open\n * (`'*'`). Everything else — notably another agent's shared note, which is\n * readable but not writable — is denied.\n */\nexport function canWrite(note: Pick<MemoryNote, 'owner' | 'writers'>, callerAgentId: string): boolean {\n return note.owner === callerAgentId || note.writers.includes(callerAgentId) || note.writers.includes('*')\n}\n\n/**\n * May `callerAgentId` read `note`? (Story 36 — the read half of the protocol.)\n *\n * True when the caller owns it, is listed in `readers`, or the note is public\n * (`readers` contains `'*'`, which is how a shared-scope write is stored).\n *\n * Queries already filter by `agent_id`, so list/search paths never surface an\n * unreadable note. This guards the one primitive that bypasses that filter —\n * `getNote(id)` fetches straight by UUID — and the link-neighbourhood walks that\n * use it: a shared note's `links[]` can name its owner's PRIVATE notes, so\n * following those links would otherwise read memory the caller may not see.\n */\nexport function canRead(note: Pick<MemoryNote, 'owner' | 'readers'>, callerAgentId: string): boolean {\n return note.owner === callerAgentId || note.readers.includes(callerAgentId) || note.readers.includes('*')\n}\n","/**\n * storage.ts — Qdrant vector storage for A-MEM\n * Uses native fetch (Node 18+) to avoid undici compatibility issues with Node v26\n * Collection: amem_notes, 384-dim cosine, with agent_id isolation\n */\n\nimport { canWrite, canRead } from './auth.js'\nimport { getEmbeddingDim, getEmbeddingModel } from './embedding.js'\n\n// ── Types ─────────────────────────────────────────────────────────────────────\n\n// ── Story 32: Per-agent config types ──────────────────────────────────────────\n\n/** Per-agent override config. If collection is set, mode B (isolated collection) is used. */\nexport interface AgentAmemConfig {\n agentId?: string\n collection?: string\n}\n\n/** Top-level plugin config shape (superset — existing fields preserved). */\nexport interface AmemPluginConfig {\n agentId?: string\n collection?: string\n topK?: number\n /** Per-agent overrides keyed by agentId */\n agents?: Record<string, AgentAmemConfig>\n // ── Story 35: LLM settings, so a host can pick the model without env vars ────\n // Env vars still win over all three. There is deliberately no key field — see\n // the precedence note in llm.ts.\n llmProvider?: string\n llmModel?: string\n llmBaseURL?: string\n // ── Story 42: the optional `strong` tier ────────────────────────────────────\n // Each falls back to its `fast` counterpart individually, so setting only\n // `llmStrongModel` keeps the same provider/endpoint. Unset entirely = strong\n // is fast, i.e. today's single-model behaviour.\n llmStrongProvider?: string\n llmStrongModel?: string\n llmStrongBaseURL?: string\n /** Which tier the agent_end CRUD decision runs on: `fast` (default) or `strong`. */\n llmCrudRole?: 'fast' | 'strong'\n /** Story 43: run the nightly contradiction sweep. Default true. */\n conflictSweep?: boolean\n // ── Story 41: CRUD write safety ─────────────────────────────────────────────\n /** Similarity floor for accepting an LLM-chosen UPDATE target. Raise it for\n * cheaper models — a rejected update is stored as a new memory, never lost. */\n crudUpdateMinSim?: number\n}\n\n/** One entry in a note's evolution history (Story 13-B) */\nexport interface EvolutionEntry {\n triggeredBy: string // ID of the new note that caused this evolution\n triggeredAt: string // ISO timestamp\n oldContext: string\n newContext: string\n oldTags: string[]\n newTags: string[]\n action?: 'update_neighbor' | 'strengthen' | 'consolidate' | 'crud_update'\n /** Story 41: the content this entry replaced, so an overwrite stays recoverable. */\n oldContent?: string\n suggestedConnections?: string[]\n tagsUpdated?: string[]\n}\n\nexport interface MemoryNote {\n id: string\n content: string\n keywords: string[]\n tags: string[]\n context: string\n links: string[] // linked note IDs\n embedding: number[]\n timestamp: string\n agent_id: string // \"main\" | \"subagent-xxx\" | \"shared\"\n hash: string // md5(content), for exact-match dedup\n // ── Story 13-A: retrieval heat tracking ──────────────────────────────────\n retrieval_count: number // times this note has been returned by queryByEmbedding\n last_accessed: string // ISO timestamp of most recent retrieval\n // ── Story 13-B: evolution history ────────────────────────────────────────\n evolution_history: EvolutionEntry[] // log of tag/context changes\n // ── Story 13-E: coarse category ──────────────────────────────────────────\n category: string // e.g. \"Technical\" | \"Business\" | … | \"General\"\n is_active: boolean\n // ── Story 26A: knowledge type classification ──────────────────────────────\n note_type: 'memory' | 'knowledge' // memory: episodic; knowledge: durable reference\n // ── Story 26B: topic tags for knowledge notes ─────────────────────────────────────────\n topics: string[] // subject tags, e.g. [\"TypeScript\", \"Qdrant\"]; empty for memory notes\n // ── Story 29: dedup pending merge flag ──────────────────────────────────────\n pending_merge: boolean // true when similarity 0.72-0.85 — candidate for future merge\n // ── Story 30: evolution mechanism ──────────────────────────────────────────\n evolution_type?: 'EVOLVE' | 'CONFLICT' | 'EXPAND' | 'NEW'\n conflict: boolean\n // ── Story 43: which note it conflicts with, and why ─────────────────────────\n // `conflict` alone is a bare boolean — it cannot say WHO the note contradicts,\n // so a reviewer has to reconstruct the pair by hand. These make a conflict\n // renderable as ONE decision instead of two disconnected entries.\n conflicts_with?: string[]\n conflict_reason?: string\n /**\n * Story 43: when this note was last included in a contradiction scan.\n * Absent = never scanned. Lets the sweep skip batches it has already judged,\n * which is what makes a daily run cost one or two calls instead of re-reading\n * the whole store every night.\n */\n conflict_scanned_at?: string\n // ── Story 44: who this memory is ABOUT ──────────────────────────────────────\n // Distinct from `agent_id`/`owner`, which say whose STORE it lives in. A\n // companion meeting several players needs both: one memory store, many people\n // it holds memories about.\n //\n // [] a fact about the world, or about the agent itself — always visible\n // [a] about one person — surfaced only when that person is present\n // [a, b] a shared experience — surfaced for either of them\n //\n // An array rather than a single value because shared experience is the normal\n // case for a companion (\"we fought the dragon together\"), not an edge case.\n // Defaulting to [] keeps every pre-existing memory a world fact, so behaviour\n // is unchanged until subjects are actually used.\n subjects: string[]\n // ── Story 31: quality scoring ──────────────────────────────────────────────\n ephemeral: boolean // true when content contains temporal signal words\n low_quality: boolean // true when content is too short or otherwise low-quality\n // ── Story 32: per-agent ownership and access control ─────────────────────\n owner: string // agent_id of the writer\n readers: string[] // [\"*\"] = all agents; [agentId] = owner-only\n writers: string[] // default [owner]; enforcement TODO in Story 33\n}\n\nexport interface QueryResult {\n note: MemoryNote\n score: number\n}\n\n// ── Config ────────────────────────────────────────────────────────────────────\nconst QDRANT_URL = 'http://localhost:6333'\nconst getCollection = () => process.env.AMEM_COLLECTION || 'amem_notes'\n/**\n * Raised when the configured model's vector width does not match the collection\n * that already exists. Its own class so the plugin can log it loudly instead of\n * as one more startup warning — this one needs the operator to act.\n */\nexport class EmbeddingDimensionMismatchError extends Error {\n constructor(\n readonly collection: string,\n readonly collectionDim: number,\n readonly modelDim: number,\n readonly model: string\n ) {\n super(\n `Collection \"${collection}\" stores ${collectionDim}-dimension vectors, but ` +\n `the embedding model \"${model}\" produces ${modelDim}. Qdrant fixes a ` +\n `collection's vector size at creation and cannot change it, so writes and ` +\n `searches would both fail.\\n` +\n `Either set AMEM_EMBED_MODEL back to the model this collection was built ` +\n `with, or migrate: build a new collection with the new model, backfill it, ` +\n `then point AMEM_COLLECTION at it. See docs/reference/embedding-models.md.`\n )\n this.name = 'EmbeddingDimensionMismatchError'\n }\n}\n\n// ── HTTP helpers ──────────────────────────────────────────────────────────────\nasync function qdrant(method: string, path: string, body?: unknown): Promise<unknown> {\n const res = await fetch(`${QDRANT_URL}${path}`, {\n method,\n headers: { 'Content-Type': 'application/json' },\n body: body ? JSON.stringify(body) : undefined,\n })\n const data = (await res.json()) as { status: string; result?: unknown; error?: string }\n if (!res.ok || (data.status && data.status !== 'ok' && data.status !== 'acknowledged')) {\n throw new Error(`Qdrant ${method} ${path} failed: ${data.error || JSON.stringify(data)}`)\n }\n return data.result\n}\n\n/**\n * Ask Qdrant whether it can serve, right now.\n *\n * `ensureCollection()` cannot answer this: it latches `_collectionReady` and\n * short-circuits on every later call, so once it has succeeded it keeps\n * reporting success long after Qdrant has gone away. `/readyz` answers in plain\n * text, so it deliberately bypasses the JSON-parsing `qdrant()` helper above.\n */\nexport async function pingQdrant(): Promise<void> {\n const res = await fetch(`${QDRANT_URL}/readyz`)\n if (!res.ok) throw new Error(`Qdrant GET /readyz failed: ${res.status}`)\n}\n\n// ── Collection init ───────────────────────────────────────────────────────────\nlet _collectionReady = false\n/** Track ready state per named collection (for mode B isolated collections). */\nconst _collectionReadyMap = new Map<string, boolean>()\n\n/** Reset the collection-ready flag. Used in tests after dropping the collection. */\nexport function resetCollectionReady(): void {\n _collectionReady = false\n _collectionReadyMap.clear()\n}\n\n/**\n * Ensure the given Qdrant collection exists with the correct schema.\n * If collectionName is omitted, uses process.env.AMEM_COLLECTION (default: amem_notes).\n * Mode B agents pass their dedicated collection name here.\n */\nexport async function ensureCollection(collectionName?: string): Promise<void> {\n const col = collectionName || getCollection()\n if (collectionName) {\n if (_collectionReadyMap.get(col)) return\n } else {\n if (_collectionReady) return\n }\n const markReady = () => {\n if (collectionName) _collectionReadyMap.set(col, true)\n else _collectionReady = true\n }\n type CollectionInfo = { config?: { params?: { vectors?: { size?: number } } } }\n let existing: CollectionInfo | null = null\n try {\n existing = (await qdrant('GET', `/collections/${col}`)) as CollectionInfo\n } catch {\n // Collection does not exist — create it below\n }\n\n if (existing) {\n // Check the dimension NOW rather than letting the first upsert fail. Qdrant\n // rejects a wrong-width vector at insert time, which surfaces as a raw\n // storage error in the middle of a working session — long after the change\n // that caused it, and nowhere near the setting to blame.\n const collectionDim = existing.config?.params?.vectors?.size\n if (typeof collectionDim === 'number') {\n const modelDim = await getEmbeddingDim()\n if (collectionDim !== modelDim) {\n throw new EmbeddingDimensionMismatchError(col, collectionDim, modelDim, getEmbeddingModel())\n }\n }\n markReady()\n return\n }\n\n try {\n // Measured, not looked up: a hardcoded table would be silently wrong for any\n // model not in it, and the collection would be created at the wrong width.\n const size = await getEmbeddingDim()\n await qdrant('PUT', `/collections/${col}`, {\n vectors: { size, distance: 'Cosine' },\n })\n } catch (err) {\n // If another concurrent call already created it, that's fine\n if (!(err instanceof Error) || !err.message.includes('already exists')) throw err\n }\n // Index agent_id for fast filtering\n await qdrant('PUT', `/collections/${col}/index`, {\n field_name: 'agent_id',\n field_schema: 'keyword',\n })\n // Index hash for exact-match dedup\n await qdrant('PUT', `/collections/${col}/index`, {\n field_name: 'hash',\n field_schema: 'keyword',\n })\n // Story 26B: Index topics for knowledge note filtering\n await qdrant('PUT', `/collections/${col}/index`, {\n field_name: 'topics',\n field_schema: 'keyword',\n })\n // Story 44: index subjects so \"memories about this person\" is an index lookup.\n await qdrant('PUT', `/collections/${col}/index`, {\n field_name: 'subjects',\n field_schema: 'keyword',\n })\n markReady()\n}\n\n// ── Raw access for migration ──────────────────────────────────────────────────\n// These deliberately bypass ensureCollection. A migration reads a collection\n// whose vectors were written by the OLD model, so the dimension check that\n// protects normal operation would reject exactly the read the migration needs.\n\n/** Scroll every point in a collection, no filter, no readiness check. */\nexport async function scrollAllRaw(\n collection: string,\n limit = 10000\n): Promise<Array<{ id: string; payload: Record<string, unknown>; vector: number[] }>> {\n const out: Array<{ id: string; payload: Record<string, unknown>; vector: number[] }> = []\n let offset: unknown = undefined\n for (;;) {\n const body: Record<string, unknown> = { with_payload: true, with_vector: true, limit }\n if (offset !== undefined && offset !== null) body.offset = offset\n const res = (await qdrant('POST', `/collections/${collection}/points/scroll`, body)) as {\n points: Array<{ id: string; payload: Record<string, unknown>; vector: number[] }>\n next_page_offset?: unknown\n }\n out.push(...res.points)\n offset = res.next_page_offset\n if (offset === undefined || offset === null || res.points.length === 0) break\n }\n return out\n}\n\n/** How many points a collection holds. Used to verify a backfill. */\nexport async function countPointsRaw(collection: string): Promise<number> {\n const res = (await qdrant('POST', `/collections/${collection}/points/count`, { exact: true })) as {\n count: number\n }\n return res.count\n}\n\n/** The vector width a collection was created with, or null if it does not exist. */\nexport async function collectionDimRaw(collection: string): Promise<number | null> {\n try {\n const info = (await qdrant('GET', `/collections/${collection}`)) as {\n config?: { params?: { vectors?: { size?: number } } }\n }\n return info.config?.params?.vectors?.size ?? null\n } catch {\n return null\n }\n}\n\n/** Create a collection at an explicit width, with the same payload indexes as ensureCollection. */\nexport async function createCollectionRaw(collection: string, size: number): Promise<void> {\n await qdrant('PUT', `/collections/${collection}`, { vectors: { size, distance: 'Cosine' } })\n for (const field_name of ['agent_id', 'hash', 'topics', 'subjects']) {\n await qdrant('PUT', `/collections/${collection}/index`, { field_name, field_schema: 'keyword' })\n }\n}\n\n/** Upsert prepared points into a collection. */\nexport async function upsertPointsRaw(\n collection: string,\n points: Array<{ id: string; vector: number[]; payload: Record<string, unknown> }>\n): Promise<void> {\n await qdrant('PUT', `/collections/${collection}/points?wait=true`, { points })\n}\n\n// ── Payload mapping ───────────────────────────────────────────────────────────\nexport function noteToPoint(note: MemoryNote) {\n return {\n id: note.id,\n vector: note.embedding,\n payload: {\n content: note.content,\n keywords: note.keywords,\n tags: note.tags,\n context: note.context,\n links: note.links,\n timestamp: note.timestamp,\n agent_id: note.agent_id,\n hash: note.hash,\n // 13-A\n retrieval_count: note.retrieval_count ?? 0,\n last_accessed: note.last_accessed || note.timestamp,\n // 13-B: stored as JSON string (Qdrant payload can't handle nested array-of-objects)\n evolution_history: JSON.stringify(note.evolution_history ?? []),\n // 13-E\n category: note.category || 'General',\n is_active: note.is_active !== false,\n // 26B\n topics: note.topics ?? [],\n // 26A\n note_type: note.note_type || 'memory',\n // 29\n pending_merge: note.pending_merge ?? false,\n // 30\n evolution_type: note.evolution_type || '',\n conflict: note.conflict ?? false,\n conflicts_with: note.conflicts_with ?? [],\n conflict_reason: note.conflict_reason ?? '',\n conflict_scanned_at: note.conflict_scanned_at ?? '',\n subjects: note.subjects ?? [],\n // 31\n ephemeral: note.ephemeral ?? false,\n low_quality: note.low_quality ?? false,\n // 32\n owner: note.owner || note.agent_id,\n readers: note.readers ?? [note.agent_id],\n writers: note.writers ?? [note.agent_id],\n },\n }\n}\n\nexport function pointToNote(point: { id: string; payload: Record<string, unknown>; vector?: number[] }): MemoryNote {\n const p = point.payload\n const timestamp = (p.timestamp as string) || ''\n\n // 13-B: deserialize evolution_history from JSON string\n let evolutionHistory: EvolutionEntry[] = []\n try {\n const raw = p.evolution_history\n if (typeof raw === 'string' && raw.length > 0) {\n evolutionHistory = JSON.parse(raw) as EvolutionEntry[]\n } else if (Array.isArray(raw)) {\n // handle legacy case where it was stored as array\n evolutionHistory = raw as EvolutionEntry[]\n }\n } catch {\n evolutionHistory = []\n }\n\n return {\n id: String(point.id),\n content: (p.content as string) || '',\n keywords: (p.keywords as string[]) || [],\n tags: (p.tags as string[]) || [],\n context: (p.context as string) || '',\n links: (p.links as string[]) || [],\n timestamp,\n agent_id: (p.agent_id as string) || 'main',\n embedding: point.vector || [],\n hash: (p.hash as string) || '',\n // 13-A\n retrieval_count: typeof p.retrieval_count === 'number' ? p.retrieval_count : 0,\n last_accessed: (p.last_accessed as string) || timestamp,\n // 13-B\n evolution_history: evolutionHistory,\n // 13-E\n category: (p.category as string) || 'General',\n is_active: p.is_active !== false,\n // 26A\n note_type: ((p.note_type as string) === 'knowledge' ? 'knowledge' : 'memory') as 'memory' | 'knowledge',\n // 26B\n topics: Array.isArray(p.topics) ? (p.topics as string[]) : [],\n // 29\n pending_merge: p.pending_merge === true,\n // 30\n evolution_type:\n typeof p.evolution_type === 'string' && ['EVOLVE', 'CONFLICT', 'EXPAND', 'NEW'].includes(p.evolution_type)\n ? (p.evolution_type as 'EVOLVE' | 'CONFLICT' | 'EXPAND' | 'NEW')\n : undefined,\n conflict: p.conflict === true,\n conflicts_with: Array.isArray(p.conflicts_with)\n ? (p.conflicts_with as unknown[]).filter((v): v is string => typeof v === 'string')\n : [],\n conflict_reason: typeof p.conflict_reason === 'string' ? p.conflict_reason : '',\n conflict_scanned_at: typeof p.conflict_scanned_at === 'string' ? p.conflict_scanned_at : '',\n subjects: Array.isArray(p.subjects)\n ? (p.subjects as unknown[]).filter((v): v is string => typeof v === 'string')\n : [],\n // 31\n ephemeral: p.ephemeral === true,\n low_quality: p.low_quality === true,\n // 32\n owner: (p.owner as string) || (p.agent_id as string) || 'main',\n readers: Array.isArray(p.readers) ? (p.readers as string[]) : [(p.agent_id as string) || 'main'],\n writers: Array.isArray(p.writers) ? (p.writers as string[]) : [(p.agent_id as string) || 'main'],\n }\n}\n\n// ── Agent filter ──────────────────────────────────────────────────────────────\nfunction agentFilter(agentId: string, subject?: string) {\n const must: unknown[] = [\n {\n should: [\n { key: 'agent_id', match: { value: agentId } },\n { key: 'agent_id', match: { value: 'shared' } },\n ],\n },\n ]\n\n // Story 44: scope to one person when asked. A memory is in scope if it names\n // them, OR if it names nobody — an empty `subjects` is a fact about the world\n // or about the agent itself, which stays relevant whoever is present. A shared\n // experience names several people and so surfaces for each of them.\n //\n // Omitting `subject` means \"no person scoping\", which is what every existing\n // caller does and what keeps behaviour unchanged.\n if (subject !== undefined) {\n must.push({\n should: [{ key: 'subjects', match: { value: subject } }, { is_empty: { key: 'subjects' } }],\n })\n }\n\n return {\n must,\n must_not: [{ key: 'is_active', match: { value: false } }],\n }\n}\n\n// ── CRUD ──────────────────────────────────────────────────────────────────────\n\n/**\n * Core CRUD implementation scoped to a specific collection and agent filter mode.\n * collectionName: which Qdrant collection to operate on.\n * modeBIsolated: if true, skip the \"also include shared\" filter in agentFilter\n * (mode B collections are already per-agent, so no cross-agent filter needed).\n */\nfunction makeCrud(collectionName: string, modeBIsolated = false) {\n const col = collectionName\n\n function scopedAgentFilter(agentId: string, subject?: string) {\n if (modeBIsolated) {\n // Mode B: the collection is already agent-isolated, so no agent clause is\n // needed — but subject scoping still applies. Mode B separates AGENTS;\n // `subjects` separates the PEOPLE one agent holds memories about, and a\n // single agent in its own collection still meets several of them.\n const must: unknown[] = []\n if (subject !== undefined) {\n must.push({\n should: [{ key: 'subjects', match: { value: subject } }, { is_empty: { key: 'subjects' } }],\n })\n }\n return {\n ...(must.length > 0 && { must }),\n must_not: [{ key: 'is_active', match: { value: false } }],\n }\n }\n return agentFilter(agentId, subject)\n }\n\n return {\n async addNote(note: MemoryNote): Promise<void> {\n await ensureCollection(col)\n await qdrant('PUT', `/collections/${col}/points?wait=true`, {\n points: [noteToPoint(note)],\n })\n },\n\n /**\n * Story 36: this is the one read that bypasses the agent filter — it fetches\n * straight by UUID. Pass `readerAgentId` to enforce `readers`; an unreadable\n * note comes back as `null` (indistinguishable from missing, so nothing leaks,\n * and callers already handle null). Omitting it skips the check, preserving\n * behaviour for internal callers that only ever hold their own ids.\n */\n async getNote(id: string, readerAgentId?: string): Promise<MemoryNote | null> {\n await ensureCollection(col)\n try {\n const result = (await qdrant('POST', `/collections/${col}/points`, {\n ids: [id],\n with_payload: true,\n with_vector: true,\n })) as Array<{ id: string; payload: Record<string, unknown>; vector: number[] }>\n if (!result.length) return null\n const note = pointToNote(result[0])\n if (readerAgentId !== undefined && !canRead(note, readerAgentId)) return null\n return note\n } catch {\n return null\n }\n },\n\n async updateNote(note: MemoryNote): Promise<void> {\n await ensureCollection(col)\n await qdrant('PUT', `/collections/${col}/points?wait=true`, {\n points: [noteToPoint(note)],\n })\n },\n\n async findByHash(hash: string, agentId: string): Promise<MemoryNote | null> {\n await ensureCollection(col)\n const body = {\n filter: {\n must: [\n { key: 'hash', match: { value: hash } },\n { key: 'is_active', match: { value: true } },\n ...(modeBIsolated\n ? []\n : [\n {\n should: [\n { key: 'agent_id', match: { value: agentId } },\n { key: 'agent_id', match: { value: 'shared' } },\n ],\n },\n ]),\n ],\n },\n with_payload: true,\n with_vector: true,\n limit: 1,\n }\n const result = (await qdrant('POST', `/collections/${col}/points/scroll`, body)) as {\n points: Array<{ id: string; payload: Record<string, unknown>; vector: number[] }>\n }\n if (!result.points.length) return null\n return pointToNote(result.points[0])\n },\n\n /**\n * Story 33: pass `callerAgentId` to enforce the writers policy. Callers that\n * hold the note already should prefer checking `canWrite` themselves; this\n * fetch-then-check path exists for callers that only have an id (the plugin's\n * CRUD hook). Returns false — without writing — when the caller may not write.\n * Omitting `callerAgentId` skips the check, preserving existing behaviour for\n * internal callers that are already scoped to their own notes.\n */\n async updateNoteContent(\n id: string,\n content: string,\n embedding: number[],\n hash: string,\n callerAgentId?: string\n ): Promise<boolean> {\n await ensureCollection(col)\n let existing: MemoryNote | null = null\n if (callerAgentId !== undefined) {\n existing = await this.getNote(id)\n if (existing && !canWrite(existing, callerAgentId)) return false\n }\n await qdrant('PUT', `/collections/${col}/points/vectors?wait=true`, {\n points: [{ id, vector: embedding }],\n })\n const payload: Record<string, unknown> = { content, hash }\n // Story 41: this overwrite is destructive. Keep the replaced text so a\n // mis-targeted UPDATE stays recoverable — the guard has false negatives,\n // and this is the last line before content is gone for good. Only done\n // when we already fetched the note (the caller-scoped CRUD path); the\n // dedup and merge paths pass no callerAgentId and are unchanged, so they\n // pay no extra read.\n if (existing) {\n const history: EvolutionEntry[] = [\n ...(existing.evolution_history ?? []),\n {\n triggeredBy: '',\n triggeredAt: new Date().toISOString(),\n oldContext: existing.context,\n newContext: existing.context,\n oldTags: existing.tags,\n newTags: existing.tags,\n action: 'crud_update',\n oldContent: existing.content,\n },\n ]\n payload.evolution_history = JSON.stringify(history)\n }\n await qdrant('POST', `/collections/${col}/points/payload?wait=true`, {\n payload,\n points: [id],\n })\n return true\n },\n\n async queryByEmbedding(\n embedding: number[],\n topK: number,\n agentId: string,\n scoreThreshold = 0.0,\n subject?: string\n ): Promise<QueryResult[]> {\n await ensureCollection(col)\n const result = (await qdrant('POST', `/collections/${col}/points/search`, {\n vector: embedding,\n limit: topK,\n with_payload: true,\n with_vector: true,\n score_threshold: scoreThreshold,\n filter: scopedAgentFilter(agentId, subject),\n })) as Array<{ id: string; score: number; payload: Record<string, unknown>; vector: number[] }>\n\n const queryResults = result.map((r) => ({\n note: pointToNote(r),\n score: r.score,\n }))\n\n if (queryResults.length > 0) {\n const now = new Date().toISOString()\n const ids = queryResults.map((r) => r.note.id)\n const patches = queryResults.map((r) => ({\n id: r.note.id,\n retrieval_count: (r.note.retrieval_count || 0) + 1,\n }))\n Promise.all([\n qdrant('POST', `/collections/${col}/points/payload?wait=false`, {\n payload: { last_accessed: now },\n points: ids,\n }),\n ...patches.map((p) =>\n qdrant('POST', `/collections/${col}/points/payload?wait=false`, {\n payload: { retrieval_count: p.retrieval_count },\n points: [p.id],\n })\n ),\n ]).catch((err: unknown) => {\n console.error(`[amem] retrieval tracking patch failed: ${(err as Error).message}`)\n })\n for (const r of queryResults) {\n r.note.retrieval_count = (r.note.retrieval_count || 0) + 1\n r.note.last_accessed = now\n }\n }\n\n return queryResults\n },\n\n async listNotes(agentId?: string, subject?: string): Promise<MemoryNote[]> {\n await ensureCollection(col)\n const body: Record<string, unknown> = {\n with_payload: true,\n with_vector: true,\n limit: 10000,\n }\n if (agentId) body.filter = scopedAgentFilter(agentId, subject)\n\n const result = (await qdrant('POST', `/collections/${col}/points/scroll`, body)) as {\n points: Array<{ id: string; payload: Record<string, unknown>; vector: number[] }>\n }\n return result.points.map(pointToNote)\n },\n\n async deleteNote(id: string): Promise<void> {\n await ensureCollection(col)\n await qdrant('POST', `/collections/${col}/points/delete`, {\n points: [id],\n })\n },\n\n /** Story 33: see `updateNoteContent` — returns false, unwritten, when denied. */\n async invalidateNote(id: string, callerAgentId?: string): Promise<boolean> {\n await ensureCollection(col)\n if (callerAgentId !== undefined) {\n const existing = await this.getNote(id)\n if (existing && !canWrite(existing, callerAgentId)) return false\n }\n await qdrant('POST', `/collections/${col}/points/payload?wait=true`, {\n payload: { is_active: false },\n points: [id],\n })\n return true\n },\n\n async getNotesByDatePrefix(datePrefix: string, agentId: string): Promise<MemoryNote[]> {\n await ensureCollection(col)\n const filterClauses: unknown[] = [{ key: 'is_active', match: { value: true } }]\n if (!modeBIsolated) {\n filterClauses.push({\n should: [\n { key: 'agent_id', match: { value: agentId } },\n { key: 'agent_id', match: { value: 'shared' } },\n ],\n })\n }\n const body: Record<string, unknown> = {\n filter: { must: filterClauses },\n with_payload: true,\n with_vector: true,\n limit: 10000,\n }\n const result = (await qdrant('POST', `/collections/${col}/points/scroll`, body)) as {\n points: Array<{ id: string; payload: Record<string, unknown>; vector: number[] }>\n }\n return result.points.map(pointToNote).filter((n) => n.timestamp.startsWith(datePrefix))\n },\n\n async countNotes(agentId?: string): Promise<number> {\n await ensureCollection(col)\n const body: Record<string, unknown> = { exact: true }\n if (agentId) body.filter = scopedAgentFilter(agentId)\n const result = (await qdrant('POST', `/collections/${col}/points/count`, body)) as { count: number }\n return result.count\n },\n\n async updateNoteLinks(id: string, links: string[]): Promise<void> {\n await ensureCollection(col)\n await qdrant('POST', `/collections/${col}/points/payload?wait=true`, {\n payload: { links },\n points: [id],\n })\n },\n\n async patchNotePayload(id: string, fields: Record<string, unknown>): Promise<void> {\n await ensureCollection(col)\n await qdrant('POST', `/collections/${col}/points/payload?wait=true`, {\n payload: fields,\n points: [id],\n })\n },\n\n async replaceLinkReferences(oldId: string, newId: string, agentId: string): Promise<void> {\n const notes = await this.listNotes(agentId)\n for (const note of notes) {\n // Story 33: listNotes also returns other agents' shared notes. Rewriting\n // their links is a mutation we may not be authorized to make; leaving the\n // stale link is harmless (it points at an invalidated note, which queries\n // already filter out).\n if (!canWrite(note, agentId)) continue\n if (note.links.includes(oldId)) {\n const newLinks = note.links.map((linkId) => (linkId === oldId ? newId : linkId))\n const filteredLinks = newLinks.filter((linkId) => linkId !== note.id)\n const uniqueLinks = Array.from(new Set(filteredLinks))\n await this.updateNoteLinks(note.id, uniqueLinks)\n }\n }\n },\n }\n}\n\nexport type StorageContext = ReturnType<typeof makeCrud>\n\n/**\n * Create a StorageContext scoped to a specific collection (mode B) or the default collection (mode A).\n * Mode A (same collection): pass collectionName = undefined → uses AMEM_COLLECTION env var.\n * Mode B (isolated collection): pass collectionName = 'amem_notes_<agentId>' and modeBIsolated = true.\n */\nexport function createStorageContext(collectionName?: string, modeBIsolated = false): StorageContext {\n return makeCrud(collectionName || getCollection(), modeBIsolated)\n}\n\n// ── Legacy top-level exports (backwards compat, use default collection) ────────\n\nexport async function addNote(note: MemoryNote): Promise<void> {\n return makeCrud(getCollection()).addNote(note)\n}\n\nexport async function getNote(id: string, readerAgentId?: string): Promise<MemoryNote | null> {\n return makeCrud(getCollection()).getNote(id, readerAgentId)\n}\n\nexport async function updateNote(note: MemoryNote): Promise<void> {\n return makeCrud(getCollection()).updateNote(note)\n}\n\nexport async function findByHash(hash: string, agentId: string): Promise<MemoryNote | null> {\n return makeCrud(getCollection()).findByHash(hash, agentId)\n}\n\nexport async function updateNoteContent(\n id: string,\n content: string,\n embedding: number[],\n hash: string,\n callerAgentId?: string\n): Promise<boolean> {\n return makeCrud(getCollection()).updateNoteContent(id, content, embedding, hash, callerAgentId)\n}\n\nexport async function queryByEmbedding(\n embedding: number[],\n topK: number,\n agentId: string,\n scoreThreshold = 0.0,\n subject?: string\n): Promise<QueryResult[]> {\n return makeCrud(getCollection()).queryByEmbedding(embedding, topK, agentId, scoreThreshold, subject)\n}\n\nexport async function listNotes(agentId?: string, subject?: string): Promise<MemoryNote[]> {\n return makeCrud(getCollection()).listNotes(agentId, subject)\n}\n\nexport async function deleteNote(id: string): Promise<void> {\n return makeCrud(getCollection()).deleteNote(id)\n}\n\nexport async function invalidateNote(id: string, callerAgentId?: string): Promise<boolean> {\n return makeCrud(getCollection()).invalidateNote(id, callerAgentId)\n}\n\nexport async function getNotesByDatePrefix(datePrefix: string, agentId: string): Promise<MemoryNote[]> {\n return makeCrud(getCollection()).getNotesByDatePrefix(datePrefix, agentId)\n}\n\nexport async function countNotes(agentId?: string): Promise<number> {\n return makeCrud(getCollection()).countNotes(agentId)\n}\n\nexport async function updateNoteLinks(id: string, links: string[]): Promise<void> {\n return makeCrud(getCollection()).updateNoteLinks(id, links)\n}\n\nexport async function patchNotePayload(id: string, fields: Record<string, unknown>): Promise<void> {\n return makeCrud(getCollection()).patchNotePayload(id, fields)\n}\n\nexport async function replaceLinkReferences(oldId: string, newId: string, agentId: string): Promise<void> {\n return makeCrud(getCollection()).replaceLinkReferences(oldId, newId, agentId)\n}\n","/**\n * llm.ts — LLM helpers for A-MEM note construction, linking, evolution\n */\n\nimport Anthropic from '@anthropic-ai/sdk'\nimport OpenAI from 'openai'\nimport { t } from './prompts.js'\n\n// ── Provider selection ────────────────────────────────────────────────────────\n// The engine's LLM calls are all \"prompt in, text out\" — no streaming, tools, or\n// vision — so one switch covers every backend. `anthropic` (default) keeps the\n// native Messages API; `openai` speaks the Chat Completions API, which every\n// OpenAI-compatible gateway implements (OpenAI, DeepSeek, OpenRouter, Groq,\n// Together, Ollama, vLLM, LM Studio…). Point the base URL at whichever one.\n//\n// Story 35: these used to be top-level consts, which froze the choice at import\n// and left a host with no way in short of relaunching the process. They are now\n// resolved per call, so a host can hand the engine a model through\n// `configureLlm()` after import. Precedence, highest first:\n//\n// 1. environment variable — the operator's override, always wins\n// 2. configureLlm() — what the host (e.g. openclaw.json) asked for\n// 3. built-in default — per provider\n//\n// Note what is deliberately absent: there is no way to inject an API key. Keys\n// come from the environment only. Configuration arrives from a host config file,\n// and an apiKey field here would be an invitation to pipe a user's credentials\n// out of that file and into the memory engine. Endpoint and model, yes; secrets,\n// no.\n\n/**\n * Which tier a call runs on (Story 42).\n *\n * The engine's calls split cleanly by how much model capability they actually\n * need. Published results are consistent that memory quality is mostly\n * architecture-bound — extraction differs ~2 points between a cheap and a strong\n * model — with ONE exception: judging whether a new fact CONTRADICTS a stored\n * one, where the gap is large. So the frequent, easy calls run `fast`, and the\n * rare, genuinely hard judgements can run `strong` if the operator configures\n * one. See docs/guide/design-rationale.md for the evidence.\n */\nexport type LlmRole = 'fast' | 'strong'\n\n/** Provider/model/endpoint for one role. */\nexport interface LlmRoleConfig {\n provider?: string\n model?: string\n baseURL?: string\n}\n\n/** Runtime LLM settings a host may inject. See the precedence note above. */\nexport interface LlmConfig extends LlmRoleConfig {\n /** Per-request timeout in ms for the SDK client. Guards against a slow or\n * stuck endpoint hanging the whole addMemory pipeline. Default 30000.\n * Shared by both roles — it is a transport concern, not a tier one. */\n timeoutMs?: number\n /**\n * Optional `strong` tier. Each field falls back to the `fast` value\n * INDIVIDUALLY, so all three useful shapes work:\n * - only `model` → same endpoint, better model (gpt-4o-mini → gpt-4o)\n * - all three → a wholly separate backend (local Ollama + cloud Claude)\n * - nothing → strong IS fast, i.e. today's single-model behaviour\n * There is deliberately no built-in strong default: inventing one would start\n * spending an existing user's money on a pricier model without them asking.\n */\n strong?: LlmRoleConfig\n /** Which role the agent_end CRUD decision uses. Default `fast`. */\n crudRole?: LlmRole\n}\n\nlet _override: LlmConfig = {}\n\n/**\n * Point the engine's LLM calls at a provider/model/endpoint chosen by the host.\n *\n * Environment variables still win over anything passed here, and any field left\n * undefined falls through to the default — so `configureLlm({ model })` changes\n * only the model. Safe to call before or after the first LLM call.\n */\nexport function configureLlm(cfg: LlmConfig): void {\n _override = { ...cfg }\n // The clients capture the base URL and key chain at construction, so a cached\n // one would keep talking to the old endpoint. Drop them and let the next call\n // rebuild against the new settings.\n _anthropicClients.clear()\n _openaiClients.clear()\n}\n\n// Resolution runs on every call, so a bad provider value would log on every call.\nconst _warned = new Set<string>()\nfunction warnOnce(key: string, message: string): void {\n if (_warned.has(key)) return\n _warned.add(key)\n console.error(message)\n}\n\n// `||`, not `??`, on purpose: an env var set to the empty string means \"unset\"\n// here. With `??` an exported-but-empty AMEM_LLM_MODEL would outrank a perfectly\n// valid model from openclaw.json and silently win, which is a miserable thing to\n// debug.\nfunction resolveProvider(role: LlmRole = 'fast'): string {\n // Strong falls back to fast per field, so an operator who only names a strong\n // MODEL keeps the same provider and endpoint — the common \"same API, better\n // model\" case.\n const raw =\n role === 'strong' ? process.env.AMEM_LLM_STRONG_PROVIDER || _override.strong?.provider || undefined : undefined\n const p = (raw || process.env.AMEM_LLM_PROVIDER || _override.provider || 'anthropic').trim().toLowerCase()\n if (p !== 'anthropic' && p !== 'openai') {\n // An unrecognised value silently routes to the anthropic path with the wrong\n // model/endpoint — surface it instead of failing invisibly on every call.\n warnOnce(`provider:${p}`, `[amem] unknown LLM provider \"${p}\"; falling back to anthropic`)\n }\n return p\n}\n\nfunction resolveModel(role: LlmRole = 'fast'): string {\n const strong =\n role === 'strong' ? process.env.AMEM_LLM_STRONG_MODEL || _override.strong?.model || undefined : undefined\n return (\n strong ||\n process.env.AMEM_LLM_MODEL ||\n _override.model ||\n (resolveProvider(role) === 'openai' ? 'gpt-4o-mini' : 'claude-sonnet-4-6')\n )\n}\n\nfunction resolveBaseURL(role: LlmRole = 'fast'): string | undefined {\n const strong =\n role === 'strong' ? process.env.AMEM_LLM_STRONG_BASE_URL || _override.strong?.baseURL || undefined : undefined\n return strong || process.env.AMEM_LLM_BASE_URL || _override.baseURL || undefined\n}\n\n/**\n * Which role the agent_end CRUD decision runs on. Defaults to `fast` — see the\n * note at its call site. An unrecognised value falls back to `fast` rather than\n * failing, and warns once.\n */\nfunction resolveCrudRole(): LlmRole {\n const raw = (process.env.AMEM_LLM_CRUD_ROLE || _override.crudRole || 'fast').trim().toLowerCase()\n if (raw === 'strong') return 'strong'\n if (raw !== 'fast') {\n warnOnce(`crudRole:${raw}`, `[amem] unknown AMEM_LLM_CRUD_ROLE \"${raw}\"; using fast`)\n }\n return 'fast'\n}\n\nconst DEFAULT_TIMEOUT_MS = 30_000\nfunction resolveTimeoutMs(): number {\n const envVal = Number(process.env.AMEM_LLM_TIMEOUT)\n if (Number.isFinite(envVal) && envVal > 0) return envVal\n if (_override.timeoutMs && _override.timeoutMs > 0) return _override.timeoutMs\n return DEFAULT_TIMEOUT_MS\n}\n\n// ── Clients (lazy, keyed by endpoint) ─────────────────────────────────────────\n// Constructed on first use, not at import, so loading the engine never builds a\n// client for the provider you are not using — nor demands its API key. Local\n// OpenAI-compatible servers (Ollama, vLLM) accept any key, so a placeholder lets\n// them run keyless.\n//\n// Keyed by base URL rather than held as a singleton: the two roles may point at\n// different backends (a local Ollama for `fast`, a hosted API for `strong`), and\n// a single cached client would silently send one role's calls to the other's\n// endpoint.\nconst _anthropicClients = new Map<string, Anthropic>()\nfunction anthropic(baseURL: string | undefined): Anthropic {\n const key = baseURL ?? ''\n let client = _anthropicClients.get(key)\n if (!client) {\n client = new Anthropic({\n ...(process.env.AMEM_LLM_API_KEY && { apiKey: process.env.AMEM_LLM_API_KEY }),\n ...(baseURL && { baseURL }),\n timeout: resolveTimeoutMs(),\n })\n _anthropicClients.set(key, client)\n }\n return client\n}\n\nconst _openaiClients = new Map<string, OpenAI>()\nfunction openai(baseURL: string | undefined): OpenAI {\n const key = baseURL ?? ''\n let client = _openaiClients.get(key)\n if (!client) {\n client = new OpenAI({\n // AMEM_LLM_API_KEY first (engine convention), then the SDK's own\n // OPENAI_API_KEY (the standard) — passing an explicit key blocks the SDK's\n // env fallback, so read it here. Placeholder last, so keyless local servers\n // (Ollama, vLLM) still work.\n apiKey: process.env.AMEM_LLM_API_KEY || process.env.OPENAI_API_KEY || 'sk-no-key-required',\n ...(baseURL && { baseURL }),\n timeout: resolveTimeoutMs(),\n })\n _openaiClients.set(key, client)\n }\n return client\n}\n\n// ── Base LLM call ─────────────────────────────────────────────────────────────\nexport async function llmCall(prompt: string, maxTokens = 500, role: LlmRole = 'fast'): Promise<string | null> {\n const provider = resolveProvider(role)\n const model = resolveModel(role)\n const baseURL = resolveBaseURL(role)\n // Gemini thinking models consume extra tokens for reasoning; scale up automatically\n const isThinking = model.includes('gemini') || model.includes('pro-agent')\n const effectiveMaxTokens = isThinking ? Math.max(maxTokens * 8, 4000) : maxTokens\n try {\n return provider === 'openai'\n ? await openaiCall(prompt, model, effectiveMaxTokens, baseURL)\n : await anthropicCall(prompt, model, effectiveMaxTokens, baseURL)\n } catch (e) {\n console.error(`[amem] LLM call failed: ${(e as Error).message}`)\n return null\n }\n}\n\nasync function anthropicCall(\n prompt: string,\n model: string,\n maxTokens: number,\n baseURL: string | undefined\n): Promise<string | null> {\n const resp = await anthropic(baseURL).messages.create({\n model,\n max_tokens: maxTokens,\n messages: [{ role: 'user', content: prompt }],\n })\n for (const block of resp.content) {\n if (block.type === 'text') return block.text.trim()\n }\n return null\n}\n\nasync function openaiCall(\n prompt: string,\n model: string,\n maxTokens: number,\n baseURL: string | undefined\n): Promise<string | null> {\n // OpenAI's own reasoning models (o1/o3, gpt-5) reject `max_tokens` and require\n // `max_completion_tokens`; everything else takes `max_tokens`. Same budget for\n // our single-shot completions — only the parameter name differs. Match OpenAI\n // names narrowly: a broad `includes('reason')` would wrongly catch other\n // gateways' models (e.g. DeepSeek's `deepseek-reasoner`, which uses max_tokens).\n const isReasoning = /^o\\d/.test(model) || model.startsWith('gpt-5')\n const resp = await openai(baseURL).chat.completions.create({\n model,\n ...(isReasoning ? { max_completion_tokens: maxTokens } : { max_tokens: maxTokens }),\n messages: [{ role: 'user', content: prompt }],\n })\n return resp.choices[0]?.message?.content?.trim() ?? null\n}\n\n// ── Response cleaning + tolerant JSON parsing ─────────────────────────────────\n\n/**\n * Remove reasoning-model scaffolding that would otherwise break JSON.parse:\n * `<think>…</think>` blocks and chat special tokens (`<|eot_id|>`, `<|im_end|>`,\n * …). Open-weight models reachable via any OpenAI-compatible `baseURL`\n * (DeepSeek-R1, Qwen, LLaMA-3 through Ollama/vLLM) emit these around otherwise\n * valid JSON. Without stripping them every JSON task silently falls back to its\n * default on a good response — and nothing in the logs says why.\n */\nfunction stripReasoning(raw: string): string {\n return raw\n .replace(/<think>[\\s\\S]*?<\\/think>/gi, '')\n .replace(/<\\|(?:eot_id|im_start|im_end|begin_of_text|end_of_text|endoftext)\\|>/g, '')\n .trim()\n}\n\nfunction stripFences(raw: string): string {\n raw = stripReasoning(raw)\n if (raw.startsWith('```')) {\n const lines = raw.split('\\n')\n lines.shift()\n if (lines[lines.length - 1] === '```') lines.pop()\n raw = lines.join('\\n').trim()\n }\n // Handle models that wrap JSON in outer quotes: \"{ ... }\" or \"[...]\"\n if ((raw.startsWith('\"') && raw.endsWith('\"')) || (raw.startsWith(\"'\") && raw.endsWith(\"'\"))) {\n try {\n raw = JSON.parse(raw)\n } catch {\n /* keep as-is */\n }\n }\n return raw\n}\n\n/**\n * Parse a JSON object from an LLM response, tolerant of a leading preamble\n * sentence before the object (common with smaller instruction-tuned models).\n * Drop-in for `JSON.parse(stripFences(raw))`: it still THROWS when nothing\n * parses, so every caller's existing try/catch → default still fires, and it\n * returns `any` for the same reason JSON.parse does — callers guard each field.\n */\nfunction parseJsonLoose(raw: string): any {\n const cleaned = stripFences(raw)\n try {\n return JSON.parse(cleaned)\n } catch (e) {\n const m = cleaned.match(/\\{[\\s\\S]*\\}/)\n if (m) return JSON.parse(m[0]) // may throw again → caller's catch handles it\n throw e\n }\n}\n\n// ── Note construction ─────────────────────────────────────────────────────────\n\n/**\n * Valid category values (Story 13-E).\n * \"General\" is the fallback for anything that doesn't fit a specific bucket.\n */\nexport type NoteCategory = 'Technical' | 'Business' | 'Personal' | 'Project' | 'Research' | 'System' | 'General'\n\nexport interface NoteStructure {\n keywords: string[]\n tags: string[]\n context: string\n /** Story 13-E: coarse-grained category */\n category: NoteCategory\n /** Story 26A: episodic memory vs durable knowledge */\n note_type: 'memory' | 'knowledge'\n /** Story 26B: topic tags, non-empty only for knowledge notes */\n topics: string[]\n /** Story 27: LLM self-reported confidence in note_type classification */\n confidence: 'high' | 'medium' | 'low'\n}\n\nconst VALID_CONFIDENCE = new Set<string>(['high', 'medium', 'low'])\n\nconst VALID_CATEGORIES = new Set<string>([\n 'Technical',\n 'Business',\n 'Personal',\n 'Project',\n 'Research',\n 'System',\n 'General',\n])\n\nexport async function llmConstructNote(content: string): Promise<NoteStructure> {\n const prompt = `Analyze the following text and respond with valid JSON only (no markdown fences, no explanation, no comments). All string values must use standard double quotes and be properly escaped:\n{\n \"keywords\": [\"keyword1\", \"keyword2\"],\n \"tags\": [\"tag1\", \"tag2\"],\n \"context\": \"one sentence summary in the same language as the input\",\n \"category\": \"Technical|Business|Personal|Project|Research|System|General\",\n \"note_type\": \"memory|knowledge\",\n \"topics\": [\"Topic1\", \"Topic2\"],\n \"confidence\": \"high|medium|low\"\n}\n\nCategory guide:\n- Technical: code, tools, configuration, APIs, debugging\n- Business: company, finance, compliance, contracts, invoices\n- Personal: personal state, habits, preferences, emotions\n- Project: project progress, decisions, milestones\n- Research: research, literature, evaluation, comparison\n- System: system services, monitoring, operations\n- General: anything that does not fit the above\n\nnote_type guide:\n- knowledge: books, methodologies, tools, domain knowledge, reference material — durable, no strong time component\n- memory: events, decisions, preferences, states, observations — episodic, time-sensitive\n\ntopics guide (Story 26B):\n- Only populate for knowledge notes (note_type=knowledge). For memory notes, return [].\n- List 1-5 concise subject tags representing the main topics of this knowledge, e.g. [\"TypeScript\", \"Qdrant\", \"Vector DB\"].\n\nconfidence guide (Story 27):\n- high: note_type is unambiguous — clearly episodic (event/decision/state) or clearly durable knowledge (tool doc/methodology)\n- medium: some ambiguity — e.g. \"learned X method\" could be either memory or knowledge\n- low: LLM is uncertain — vague, fragmentary, or mixed content\n\nText: ${content}`\n\n const raw = await llmCall(prompt, 400)\n if (!raw)\n return {\n keywords: [],\n tags: [],\n context: '',\n category: 'General',\n note_type: 'memory',\n topics: [],\n confidence: 'medium',\n }\n\n try {\n const data = parseJsonLoose(raw)\n const rawCategory = typeof data.category === 'string' ? data.category : 'General'\n const category: NoteCategory = VALID_CATEGORIES.has(rawCategory) ? (rawCategory as NoteCategory) : 'General'\n const note_type: 'memory' | 'knowledge' = data.note_type === 'knowledge' ? 'knowledge' : 'memory'\n const topics: string[] =\n note_type === 'knowledge' && Array.isArray(data.topics)\n ? (data.topics as unknown[]).filter((v): v is string => typeof v === 'string')\n : []\n const rawConfidence = typeof data.confidence === 'string' ? data.confidence : 'medium'\n const confidence: 'high' | 'medium' | 'low' = VALID_CONFIDENCE.has(rawConfidence)\n ? (rawConfidence as 'high' | 'medium' | 'low')\n : 'medium'\n return {\n keywords: Array.isArray(data.keywords) ? data.keywords : [],\n tags: Array.isArray(data.tags) ? data.tags : [],\n context: typeof data.context === 'string' ? data.context : '',\n category,\n note_type,\n topics,\n confidence,\n }\n } catch (e) {\n console.error(`[amem] Note construction parse failed: ${(e as Error).message}`)\n return {\n keywords: [],\n tags: [],\n context: '',\n category: 'General',\n note_type: 'memory',\n topics: [],\n confidence: 'medium',\n }\n }\n}\n\n// ── Link judgment ─────────────────────────────────────────────────────────────\nexport async function llmShouldLink(noteContent: string, candidateContent: string): Promise<boolean> {\n const prompt = `Do these two memory notes have a meaningful relationship that would be useful to link?\nReply with only \"yes\" or \"no\".\n\nNote A: ${noteContent}\nNote B: ${candidateContent}`\n\n const raw = await llmCall(prompt, 10)\n if (!raw) return false\n return raw.toLowerCase().startsWith('yes')\n}\n\n// ── CRUD Decision ────────────────────────────────────────────────────────────\nexport interface MemoryOperation {\n action: 'NEW' | 'UPDATE' | 'DELETE' | 'NONE'\n fact: string\n existingIdx?: number // For UPDATE/DELETE: integer index into existingMemories (guards against hallucination)\n reason?: string\n}\n\nexport async function llmCrudDecision(\n userText: string,\n assistantText: string,\n existingMemories: Array<{ idx: number; content: string }>\n): Promise<MemoryOperation[]> {\n const memoryList =\n existingMemories.length > 0 ? existingMemories.map((m) => `[${m.idx}] ${m.content}`).join('\\n') : '(none)'\n\n const prompt = t.crudDecision(userText.slice(0, 500), assistantText.slice(0, 500), memoryList)\n\n try {\n // Story 42: defaults to `fast`. It IS a contradiction judgement, but it runs\n // on every turn, and its one destructive failure mode (overwriting the wrong\n // memory) is already handled architecturally by the Story 41 guard rather\n // than by buying a bigger model. Operators who want it on `strong` can say so.\n const raw = await llmCall(prompt, 400, resolveCrudRole())\n if (!raw) return []\n // Strip reasoning scaffolding first — this path extracts the array straight\n // from the response and would otherwise trip over a <think> block.\n const match = stripReasoning(raw).match(/\\[.*\\]/s)\n if (!match) return []\n const parsed = JSON.parse(match[0])\n if (!Array.isArray(parsed)) return []\n const ops: MemoryOperation[] = []\n for (const item of parsed) {\n if (!item || typeof item !== 'object') continue\n const action = item.action\n if (!['NEW', 'UPDATE', 'DELETE', 'NONE'].includes(action)) continue\n if (action === 'NONE') continue\n const op: MemoryOperation = {\n action,\n fact: typeof item.fact === 'string' ? item.fact : '',\n reason: typeof item.reason === 'string' ? item.reason : undefined,\n }\n if (typeof item.existingIdx === 'number') {\n op.existingIdx = item.existingIdx\n }\n ops.push(op)\n }\n return ops.slice(0, 3)\n } catch (e) {\n console.error(`[amem] llmCrudDecision failed: ${(e as Error).message}`)\n return []\n }\n}\n\n// ── Merge judgment ───────────────────────────────────────────────────────────\nexport async function llmShouldMerge(\n contentA: string,\n contentB: string\n): Promise<{ shouldMerge: boolean; merged?: string }> {\n const prompt = t.shouldMerge(contentA, contentB)\n\n // Story 42: merge adjudication is the contradiction class — the one place a\n // stronger model measurably helps. Runs on `strong` when one is configured.\n const raw = await llmCall(prompt, 300, 'strong')\n if (!raw) return { shouldMerge: false }\n\n try {\n const data = parseJsonLoose(raw)\n if (typeof data.shouldMerge !== 'boolean') return { shouldMerge: false }\n if (data.shouldMerge && typeof data.merged === 'string') {\n return { shouldMerge: true, merged: data.merged }\n }\n return { shouldMerge: false }\n } catch (e) {\n console.error(`[amem] llmShouldMerge parse failed: ${(e as Error).message}`)\n return { shouldMerge: false }\n }\n}\n\n// ── Note evolution ────────────────────────────────────────────────────────────\n\n// ── Story 30: Evolution type judgment ────────────────────────────────────────\nexport type EvolutionType = 'EVOLVE' | 'CONFLICT' | 'EXPAND' | 'NEW'\n\nconst VALID_EVOLUTION_TYPES = new Set<string>(['EVOLVE', 'CONFLICT', 'EXPAND', 'NEW'])\n\nexport async function llmEvolutionJudge(\n oldContent: string,\n newContent: string\n): Promise<{ type: EvolutionType; mergedContent?: string }> {\n const prompt = t.evolutionJudge(oldContent, newContent)\n\n // Story 42: EVOLVE/CONFLICT/EXPAND/NEW is literally contradiction\n // classification — the tier-sensitive call. Runs on `strong` when configured.\n const raw = await llmCall(prompt, 300, 'strong')\n if (!raw) return { type: 'NEW' }\n\n try {\n const data = parseJsonLoose(raw)\n const type: EvolutionType = VALID_EVOLUTION_TYPES.has(data.type) ? (data.type as EvolutionType) : 'NEW'\n return {\n type,\n mergedContent: typeof data.mergedContent === 'string' ? data.mergedContent : undefined,\n }\n } catch (e) {\n console.error(`[amem] llmEvolutionJudge parse failed: ${(e as Error).message}`)\n return { type: 'NEW' }\n }\n}\n\n// ── Note evolution (legacy) ──────────────────────────────────────────────────\nexport interface EvolvedNote {\n tags: string[] | null\n context: string | null\n shouldStrengthen: boolean\n suggestedConnections: string[]\n tagsToUpdate: string[]\n}\n\nexport async function llmEvolveNote(\n content: string,\n linkedNotes: Array<{ id: string; content: string }>\n): Promise<EvolvedNote> {\n const linkedStr = linkedNotes.map((n) => `- ID: ${n.id}\\n Content: ${n.content}`).join('\\n')\n const prompt = `A memory note has gained new connections. Update its context, tags, and decide whether to strengthen connections with specific neighbors.\nReply with JSON only (no markdown):\n{\n \"tags\": [\"tag1\", \"tag2\", ...],\n \"context\": \"updated one sentence summary\",\n \"should_strengthen\": true|false,\n \"suggested_connections\": [\"neighbor_id_1\", \"neighbor_id_2\", ...],\n \"tags_to_update\": [\"tag_1\", ..., \"tag_n\"]\n}\n\nGuidelines:\n- \"tags\" and \"context\" are for updating the original note based on new connections.\n- \"should_strengthen\" is a decision whether this note should strengthen its connections to any of the newly linked notes (neighbors).\n- \"suggested_connections\" must contain only IDs from the newly linked notes (neighbors) listed below.\n- \"tags_to_update\" are updated tags for the original note itself if we strengthen connections.\n\nOriginal note content: ${content}\n\nNewly linked notes (neighbors):\n${linkedStr}`\n\n const raw = await llmCall(prompt, 500)\n if (!raw) return { tags: null, context: null, shouldStrengthen: false, suggestedConnections: [], tagsToUpdate: [] }\n\n try {\n const data = parseJsonLoose(raw)\n return {\n tags: Array.isArray(data.tags) ? data.tags : null,\n context: typeof data.context === 'string' ? data.context : null,\n shouldStrengthen: typeof data.should_strengthen === 'boolean' ? data.should_strengthen : false,\n suggestedConnections: Array.isArray(data.suggested_connections) ? data.suggested_connections.map(String) : [],\n tagsToUpdate: Array.isArray(data.tags_to_update) ? data.tags_to_update.map(String) : [],\n }\n } catch (e) {\n console.error(`[amem] Evolution parse failed: ${(e as Error).message}`)\n return { tags: null, context: null, shouldStrengthen: false, suggestedConnections: [], tagsToUpdate: [] }\n }\n}\n\n// ── Story 43: batched contradiction scan ──────────────────────────────────────\n\n/** One contradicting pair, as indices into the batch that was scanned. */\nexport interface ConflictPair {\n a: number\n b: number\n reason: string\n /**\n * Which side the model judged to be SUPERSEDED — the one no longer true.\n * `null` when it could not tell, which is common and must be respected.\n *\n * This is deliberately a semantic judgement rather than a timestamp\n * comparison. A note carries only its INGESTION time, and the two clocks come\n * apart constantly: \"back in 2019 I was vegetarian\", written today, is newer\n * on the wall clock and older in fact. The evidence for which fact is current\n * lives in the TEXT (\"moved last month\", \"used to\", \"switched to\"), which is\n * exactly what the model is already reading.\n */\n supersededIndex: number | null\n}\n\n/**\n * Ask the model which memories in a batch contradict each other.\n *\n * Deliberately NOT pairwise. The engine's existing consolidation pairs notes by\n * cosine similarity, which structurally cannot surface the contradictions that\n * matter here — \"is vegetarian\" and \"loved the steak\" sit far apart in embedding\n * space. Handing the model the whole batch at once is what lets it notice a pair\n * that no similarity gate would have put together.\n *\n * Runs on the `strong` tier: this is the contradiction-judgement class, the one\n * place where model capability measurably pays.\n *\n * Indices are validated against the batch size, so a hallucinated number is\n * dropped rather than mis-targeting a note (the Story 41 lesson).\n */\nexport async function llmConflictScan(contents: string[]): Promise<ConflictPair[]> {\n if (contents.length < 2) return []\n const numbered = contents.map((c, i) => `[${i}] ${c}`).join('\\n')\n\n try {\n const raw = await llmCall(t.conflictScan(numbered), 600, 'strong')\n if (!raw) return []\n const cleaned = stripReasoning(raw)\n const match = cleaned.match(/\\[[\\s\\S]*\\]/)\n if (!match) return []\n const parsed = JSON.parse(match[0])\n if (!Array.isArray(parsed)) return []\n\n const pairs: ConflictPair[] = []\n const seen = new Set<string>()\n for (const item of parsed) {\n if (!item || typeof item !== 'object') continue\n const { a, b } = item as { a: unknown; b: unknown }\n if (typeof a !== 'number' || typeof b !== 'number') continue\n if (!Number.isInteger(a) || !Number.isInteger(b)) continue\n // A hallucinated index must never reach a note. Bounds-check both.\n if (a < 0 || b < 0 || a >= contents.length || b >= contents.length) continue\n if (a === b) continue\n const key = a < b ? `${a}:${b}` : `${b}:${a}`\n if (seen.has(key)) continue\n seen.add(key)\n // Only accept a superseded marker that names one of THIS pair. Anything\n // else (a hallucinated index, a third note, a non-number) means \"unknown\",\n // which callers must treat as \"do not retire\".\n const rawSup = (item as { superseded?: unknown }).superseded\n const supersededIndex = rawSup === a || rawSup === b ? (rawSup as number) : null\n\n pairs.push({\n a,\n b,\n reason: typeof (item as { reason?: unknown }).reason === 'string' ? (item as { reason: string }).reason : '',\n supersededIndex,\n })\n }\n return pairs\n } catch (e) {\n console.error(`[amem] llmConflictScan failed: ${(e as Error).message}`)\n return []\n }\n}\n","/**\n * prompts.ts — Locale-aware prompt templates for A-MEM LLM functions\n *\n * Only the 3 content-sensitive functions need locale variants:\n * - crudDecision: extracts facts from conversation (needs natural language output)\n * - shouldMerge: merges duplicate memories (needs natural language output)\n * - evolutionJudge: judges memory evolution (needs natural language output)\n *\n * The other 3 functions (constructNote, shouldLink, evolveNote) produce\n * structured/binary output and work equally well in English for any input language.\n *\n * PROMPT_VERSION: 1 — created: 2026-06-24\n * When updating any locale, review the other locale for behavioral parity.\n */\n\n// ── Types ────────────────────────────────────────────────────────────────────\n\nexport type PromptLocale = 'en' | 'zh'\n\nexport interface LocalePrompts {\n crudDecision: (userText: string, assistantText: string, memoryList: string) => string\n shouldMerge: (contentA: string, contentB: string) => string\n evolutionJudge: (oldContent: string, newContent: string) => string\n /** Story 43: scan a BATCH of notes for mutually contradictory pairs. */\n conflictScan: (numberedNotes: string) => string\n}\n\n// ── Locale resolution ────────────────────────────────────────────────────────\n\nconst LOCALE: PromptLocale = (process.env.AMEM_PROMPT_LOCALE as PromptLocale) === 'zh' ? 'zh' : 'en'\n\n// ── English templates ────────────────────────────────────────────────────────\n\nconst en: LocalePrompts = {\n crudDecision: (\n userText,\n assistantText,\n memoryList\n ) => `You are a memory management agent. Analyze the conversation and decide what memory operations are needed.\n\n## Conversation\n\nUser: ${userText}\nAssistant: ${assistantText}\n\n## Existing relevant memories (identified by integer idx)\n\n${memoryList}\n\n## Task\n\nExtract only genuinely important long-term facts (decisions, preferences, account info, project status, key insights). Skip small talk, confirmations, and information already captured in existing memories.\n\n## Operation types\n- NEW: Extract a brand new fact not present in existing memories\n- UPDATE: New information refines or supersedes an existing memory; specify existingIdx\n- DELETE: An existing memory is outdated, contradicted, or wrong; specify existingIdx, fact = original content\n- NONE: Nothing worth recording, or information already fully captured\n\n## Output format\n\nReturn a JSON array. Each item:\n{\"action\": \"NEW\"|\"UPDATE\"|\"DELETE\"|\"NONE\", \"fact\": \"fact content\", \"existingIdx\": integer or omit, \"reason\": \"optional\"}\n\nReturn at most 3 operations. If nothing is worth recording, return [].\nReturn only the JSON array, no other text.\n\nExamples:\n\n1. New preference:\n[{\"action\": \"NEW\", \"fact\": \"User prefers TypeScript over JavaScript\", \"reason\": \"Explicitly stated tech preference\"}]\n\n2. Updating an existing memory (idx 0 was \"User is evaluating React and Vue\"):\n[{\"action\": \"UPDATE\", \"fact\": \"User decided to use React (dropped Vue)\", \"existingIdx\": 0, \"reason\": \"Decision finalized, update evaluation status\"}]\n\n3. Conversation is just \"Sure, thanks\" / \"Got it\" with no new info:\n[]`,\n\n shouldMerge: (\n contentA,\n contentB\n ) => `You are a memory deduplication assistant. Determine whether two memories express essentially the same information.\n\nMemory A: ${contentA}\nMemory B: ${contentB}\n\nRules:\n- If both memories express the same core fact (possibly different wording or granularity), return:\n {\"shouldMerge\": true, \"merged\": \"Concise merged statement preserving key details from both, more complete than either alone\"}\n- If the memories are complementary, on different topics, or contain different specific facts, return:\n {\"shouldMerge\": false}\n\nReturn only JSON, no other text.\n\nExamples:\n\n1. Should merge (different granularity):\nA: \"Project uses PostgreSQL\"\nB: \"Project's primary database is PostgreSQL 16, deployed on AWS RDS\"\n-> {\"shouldMerge\": true, \"merged\": \"Project uses PostgreSQL 16 as primary database, deployed on AWS RDS\"}\n\n2. Should NOT merge (complementary but distinct):\nA: \"User prefers VS Code\"\nB: \"User's VS Code uses One Dark Pro theme\"\n-> {\"shouldMerge\": false}`,\n\n evolutionJudge: (\n oldContent,\n newContent\n ) => `You are a memory evolution judge. Analyze the relationship between an old and new memory and return JSON.\n\nOld memory: ${oldContent}\nNew memory: ${newContent}\n\nClassification rules:\n\n- EVOLVE: New content deepens or updates the old memory (e.g. \"Considering Next.js\" -> \"Decided on Next.js 14 App Router\")\n Return: {\"type\": \"EVOLVE\", \"mergedContent\": \"Merged content preserving the evolution trajectory\"}\n\n- CONFLICT: Old and new information directly contradict each other on the same attribute (e.g. \"Uses MySQL as primary DB\" vs \"Migrated to PostgreSQL\")\n Return: {\"type\": \"CONFLICT\"}\n\n- EXPAND: New information supplements the old memory on the same topic (e.g. \"Handles backend dev\" + \"Backend uses Go and gRPC\")\n Return: {\"type\": \"EXPAND\", \"mergedContent\": \"Merged content integrating both pieces of information\"}\n\n- NEW: Completely unrelated information, no substantive connection to the old memory\n Return: {\"type\": \"NEW\"}\n\nReturn only JSON, no other text.`,\n conflictScan: (numberedNotes) => `You are auditing a person's memory store for CONTRADICTIONS.\n\nBelow are numbered memories. Find pairs that CANNOT both be true of the same person at the same time.\n\n${numberedNotes}\n\nWhat counts as a contradiction:\n- The same attribute holding two incompatible values (\"lives in Paris\" vs \"moved to Berlin\")\n- A stated preference or constraint that a later memory violates (\"is vegetarian\" vs \"loved the steak\")\n- A fact that a later memory supersedes (\"uses MySQL\" vs \"migrated to PostgreSQL\")\n\nWhat does NOT count — be strict, these are the common false positives:\n- Additive facts. Two things can both be true (\"has a dog named Buddy\" + \"adopted a second dog, Scout\" is NOT a contradiction)\n- Change over time that both memories already acknowledge\n- Merely similar or related topics\n- Different contexts (likes coffee at work, tea at home)\n\nFor each contradicting pair, also say which one is SUPERSEDED — the one that is\nno longer true. Judge this from the WORDING, not from any assumed order: phrases\nlike \"used to\", \"back in 2019\", \"moved last month\", \"switched to\" tell you which\nstatement describes the past. The memories are NOT listed in chronological order,\nand the number does not imply age.\n\nIf you cannot tell which one is superseded, set it to null. That is a normal and\nuseful answer — say null rather than guessing, because a wrong guess retires a\nmemory that is still true.\n\nReturn ONLY a JSON array. Empty array if nothing genuinely contradicts:\n[{\"a\": 0, \"b\": 3, \"superseded\": 0, \"reason\": \"one short sentence naming the incompatible attribute\"}]\n\n\"superseded\" must be either the value of \"a\", the value of \"b\", or null.\nUse the numbers shown. Report a pair once. Prefer returning nothing over guessing.`,\n}\n\n// ── Chinese templates ────────────────────────────────────────────────────────\n\nconst zh: LocalePrompts = {\n crudDecision: (userText, assistantText, memoryList) => `你是一个记忆管理 agent,负责分析对话内容并决定如何操作记忆库。\n\n## 对话内容\n\n用户:${userText}\n助手:${assistantText}\n\n## 已有相关记忆(用整数 idx 标识)\n\n${memoryList}\n\n## 任务\n\n分析上述对话,决定需要哪些记忆操作。只提取真正重要的长期事实(决策、偏好、账号信息、项目状态、关键洞察)。跳过闲聊、确认语、重复信息。\n\n## 操作类型\n- NEW:提取全新事实(已有记忆中没有的信息)\n- UPDATE:新信息更新了某条已有记忆,用 existingIdx 指定要更新的条目\n- DELETE:某条已有记忆已经过时、发生冲突或错误,用 existingIdx 指定,fact 填原内容\n- NONE:不值得记录或已有完全相同的信息\n\n## 输出格式\n\n返回 JSON 数组,每条格式:\n{\"action\": \"NEW\"|\"UPDATE\"|\"DELETE\"|\"NONE\", \"fact\": \"事实内容\", \"existingIdx\": 整数或省略, \"reason\": \"原因(可选)\"}\n\n每次最多返回 3 条操作。如果没有值得操作的内容,返回 []。\n只返回 JSON 数组,不要任何其他文字。\n\n示例:\n\n1. 提取新偏好:\n[{\"action\": \"NEW\", \"fact\": \"用户偏好 TypeScript 而非 JavaScript\", \"reason\": \"明确表达的技术偏好\"}]\n\n2. 更新已有记忆(idx 0 原为\"用户正在评估 React 和 Vue\"):\n[{\"action\": \"UPDATE\", \"fact\": \"用户决定使用 React(放弃了 Vue)\", \"existingIdx\": 0, \"reason\": \"决策已明确,更新评估状态\"}]\n\n3. 对话仅为\"好的,谢谢\"/\"没问题\"等确认语,无新信息:\n[]`,\n\n shouldMerge: (contentA, contentB) => `你是一个记忆去重助手,负责判断两条记忆是否表达了本质相同的信息。\n\n记忆A:${contentA}\n记忆B:${contentB}\n\n判断规则:\n- 如果两条记忆表达的是本质相同的信息(可能措辞不同、粒度不同,但核心事实一致),返回 JSON:\n {\"shouldMerge\": true, \"merged\": \"合并后的简洁表述,保留两条记忆的关键信息,比任何一条都更完整\"}\n- 如果两条记忆是互补信息、不同主题、或包含不同的具体事实,返回 JSON:\n {\"shouldMerge\": false}\n\n只返回 JSON,不要任何其他文字。\n\n示例:\n\n1. 应合并(粒度不同):\nA: \"项目使用 PostgreSQL 数据库\"\nB: \"项目的主数据库是 PostgreSQL 16,部署在 AWS RDS 上\"\n→ {\"shouldMerge\": true, \"merged\": \"项目使用 PostgreSQL 16 作为主数据库,部署在 AWS RDS 上\"}\n\n2. 不应合并(互补但不同):\nA: \"用户喜欢用 VS Code\"\nB: \"用户的 VS Code 使用 One Dark Pro 主题\"\n→ {\"shouldMerge\": false}`,\n\n evolutionJudge: (oldContent, newContent) => `你是一个记忆演化判断助手。分析以下两条记忆的关系并返回 JSON。\n\n旧记忆:${oldContent}\n新记忆:${newContent}\n\n判断规则:\n\n- EVOLVE:新内容是对旧记忆的深化/更新(如「正在考虑用 Next.js」→「决定用 Next.js 14 App Router」)\n 返回:{\"type\": \"EVOLVE\", \"mergedContent\": \"融合后的完整内容,保留演化轨迹\"}\n\n- CONFLICT:新旧信息在同一属性上直接矛盾(如「使用 MySQL 作为主数据库」vs「已迁移到 PostgreSQL」)\n 返回:{\"type\": \"CONFLICT\"}\n\n- EXPAND:新信息是对旧记忆同一主题的补充扩展(如「负责后端开发」+「后端使用 Go 和 gRPC」)\n 返回:{\"type\": \"EXPAND\", \"mergedContent\": \"合并后的完整内容,整合双方信息\"}\n\n- NEW:全新信息,与旧记忆无实质关联(如「喜欢 dark mode」vs「下周要去出差」)\n 返回:{\"type\": \"NEW\"}\n\n只返回 JSON,不要任何其他文字。`,\n conflictScan: (numberedNotes) => `你在审计一个人的记忆库,找出其中**互相矛盾**的条目。\n\n下面是编号的记忆。找出那些**不可能同时为真**的配对。\n\n${numberedNotes}\n\n算矛盾的情况:\n- 同一属性上出现互斥的值(「住在巴黎」vs「搬到了柏林」)\n- 后来的记忆违反了先前陈述的偏好或约束(「吃素」vs「那块牛排很好吃」)\n- 后来的事实取代了先前的(「用 MySQL」vs「已迁移到 PostgreSQL」)\n\n**不算**矛盾 —— 请严格,以下是最常见的误判:\n- 累加的事实。两者可以同时成立(「养了一只狗叫 Buddy」+「又领养了第二只叫 Scout」**不是**矛盾)\n- 两条记忆本身已经体现了随时间的变化\n- 只是主题相似或相关\n- 场景不同(在公司喝咖啡,在家喝茶)\n\n对每一对矛盾,还要指出哪一条是**已失效的**(不再为真的那条)。请从**措辞**判断,不要假设顺序:\n「以前」「2019 年那会儿」「上个月搬了」「改用了」这类说法能告诉你哪条描述的是过去。\n这些记忆**不是按时间顺序排列的**,编号也不代表新旧。\n\n如果无法判断哪条已失效,就填 null。这是一个**正常且有用**的回答 —— 宁可填 null 也不要猜,\n因为猜错会让一条**仍然为真**的记忆被停用。\n\n只返回 JSON 数组。没有真正矛盾就返回空数组:\n[{\"a\": 0, \"b\": 3, \"superseded\": 0, \"reason\": \"一句话说明是哪个属性互斥\"}]\n\n\"superseded\" 只能是 \"a\" 的值、\"b\" 的值,或 null。\n使用上面显示的编号。同一对只报一次。**宁可不报,也不要猜。**`,\n}\n\n// ── Export ────────────────────────────────────────────────────────────────────\n\nconst templates: Record<PromptLocale, LocalePrompts> = { en, zh }\n\nexport const t = templates[LOCALE]\n","/**\n * evo-counter.ts — Evolution throttle counter (Story 13-C)\n *\n * Tracks how many times addMemory has been called and gates evolution\n * behind an EVO_THRESHOLD counter. This reduces LLM calls dramatically\n * in high-frequency write scenarios.\n *\n * Counter persisted to <dataDir>/amem_evo_cnt.json (dataDir via config, default ~/.amem).\n */\n\nimport * as fs from 'fs'\nimport * as path from 'path'\nimport { getDataDir } from './config.js'\n\nfunction counterFile(): string {\n return process.env.AMEM_EVO_COUNTER_PATH || path.join(getDataDir(), 'amem_evo_cnt.json')\n}\nconst EVO_THRESHOLD = 20\n\ninterface CounterData {\n count: number\n updatedAt: string\n}\n\nexport function getEvoCount(): number {\n try {\n const data = JSON.parse(fs.readFileSync(counterFile(), 'utf-8')) as CounterData\n return data.count || 0\n } catch {\n return 0\n }\n}\n\nexport function incrementEvoCount(): number {\n const count = getEvoCount() + 1\n fs.writeFileSync(counterFile(), JSON.stringify({ count, updatedAt: new Date().toISOString() }))\n return count\n}\n\nexport function shouldRunEvolution(): boolean {\n const count = incrementEvoCount()\n return count % EVO_THRESHOLD === 0\n}\n","/**\n * quality.ts — Memory quality scanning and review batch generation (Story 31)\n */\n\nimport * as fs from 'fs'\nimport * as path from 'path'\nimport { listNotes, patchNotePayload, type MemoryNote } from './storage.js'\nimport { canWrite } from './auth.js'\nimport type { PromptLocale } from './prompts.js'\n\nconst LOCALE: PromptLocale = (process.env.AMEM_PROMPT_LOCALE as PromptLocale) === 'zh' ? 'zh' : 'en'\n\n// ── Types ─────────────────────────────────────────────────────────────────────\n\nexport interface LowQualityItem {\n note: MemoryNote\n reasons: LowQualityReason[]\n}\n\nexport type LowQualityReason = 'too_short' | 'expired_ephemeral' | 'pending_conflict'\n\n// ── scanLowQuality ────────────────────────────────────────────────────────────\n\nexport async function scanLowQuality(agentId: string): Promise<LowQualityItem[]> {\n const notes = await listNotes(agentId)\n const now = Date.now()\n const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000\n const results: LowQualityItem[] = []\n\n for (const note of notes) {\n // Story 33: listNotes also returns SHARED notes owned by other agents. Quality\n // enforcement marks notes low_quality, so scan only what this agent may write —\n // flagging a note we cannot act on would be noise, and patching it a violation.\n if (!canWrite(note, agentId)) continue\n\n const reasons: LowQualityReason[] = []\n\n if (note.content.trim().length < 10) {\n reasons.push('too_short')\n }\n\n if (note.ephemeral === true) {\n const createdAt = new Date(note.timestamp).getTime()\n if (now - createdAt > SEVEN_DAYS_MS) {\n reasons.push('expired_ephemeral')\n }\n }\n\n if (note.conflict === true) {\n reasons.push('pending_conflict')\n }\n\n if (reasons.length > 0) {\n if (!note.low_quality) {\n await patchNotePayload(note.id, { low_quality: true })\n }\n results.push({ note, reasons })\n }\n }\n\n return results\n}\n\n// ── generateReviewBatch ───────────────────────────────────────────────────────\n\nconst DEFAULT_OUTPUT_DIR = process.env.AMEM_REVIEW_DIR || process.cwd()\n\nfunction nextBatchNumber(dir: string): number {\n let max = 0\n try {\n const files = fs.readdirSync(dir)\n for (const f of files) {\n const match = f.match(/^amem-review-batch(\\d+)\\.md$/)\n if (match) {\n const n = parseInt(match[1], 10)\n if (n > max) max = n\n }\n }\n } catch {\n // dir doesn't exist yet\n }\n return max + 1\n}\n\nfunction reasonLabel(r: LowQualityReason): string {\n if (LOCALE === 'zh') {\n switch (r) {\n case 'too_short':\n return '内容过短(<10字)'\n case 'expired_ephemeral':\n return '临时记忆已过期(>7天)'\n case 'pending_conflict':\n return '存在冲突标记'\n }\n }\n switch (r) {\n case 'too_short':\n return 'Content too short (<10 chars)'\n case 'expired_ephemeral':\n return 'Ephemeral memory expired (>7 days)'\n case 'pending_conflict':\n return 'Pending conflict flag'\n }\n}\n\nfunction severityBadge(reasons: LowQualityReason[]): string {\n if (reasons.includes('too_short')) return '🔴 LOW'\n if (reasons.includes('expired_ephemeral')) return '🟡 EXPIRED'\n return '🟠 CONFLICT'\n}\n\nexport async function generateReviewBatch(agentId: string, outputPath?: string): Promise<string> {\n // outputPath is a bare filename, not a path. It arrives from the\n // memory_quality_scan tool, so a prompt-injected agent could otherwise hand\n // us an absolute path or a ../ traversal and overwrite any file the process\n // can write (CodeQL js/path-injection). path.basename() strips every\n // directory component, so the write can only ever land in the review root;\n // we reject anything that carried a directory part loudly rather than\n // silently rewriting it. Operators choose the root with AMEM_REVIEW_DIR.\n const root = path.resolve(DEFAULT_OUTPUT_DIR)\n let filePath: string\n let batchN: number\n if (outputPath) {\n const name = path.basename(outputPath)\n if (name !== outputPath || name === '' || name === '.' || name === '..') {\n throw new Error(`[quality] outputPath 必须是纯文件名(不含目录): ${outputPath}`)\n }\n filePath = path.join(root, name)\n batchN = 0\n } else {\n batchN = nextBatchNumber(root)\n filePath = path.join(root, `amem-review-batch${batchN}.md`)\n }\n\n const items = await scanLowQuality(agentId)\n\n const now = new Date().toISOString().slice(0, 10)\n const lines: string[] = []\n\n const title = LOCALE === 'zh' ? 'A-MEM 质量审核' : 'A-MEM Quality Review'\n const genLabel = LOCALE === 'zh' ? '生成时间' : 'Generated'\n const countLabel = LOCALE === 'zh' ? `共 ${items.length} 条低质量条目` : `${items.length} low-quality item(s)`\n const applyHint =\n LOCALE === 'zh' ? '勾选后交给助手处理这些条目' : 'Tick your choices, then ask the assistant to act on them'\n\n lines.push(`# ${title} — Batch ${batchN || 'custom'}`)\n lines.push('')\n lines.push(`> ${genLabel}:${now} | ${countLabel}`)\n lines.push(`> ${applyHint}`)\n lines.push('')\n\n if (items.length === 0) {\n lines.push(LOCALE === 'zh' ? '✅ 没有发现低质量条目。' : '✅ No low-quality items found.')\n }\n\n // ── Story 43: conflicts render as ONE decision, not two entries ────────────\n // A contradiction involves a PAIR. Listing each note separately forces the\n // reviewer to find both, reconstruct that they belong together, then tick two\n // boxes — which is the single biggest source of review friction. Shown side by\n // side with timestamps, the reason, and a recommendation, it is one glance and\n // one tick. Each note still gets its own entry below for the apply tool.\n const byId = new Map(items.map((it) => [it.note.id, it.note]))\n const renderedPairs = new Set<string>()\n const pairLines: string[] = []\n for (const { note } of items) {\n for (const otherId of note.conflicts_with ?? []) {\n const other = byId.get(otherId)\n if (!other) continue\n const key = note.id < otherId ? `${note.id}:${otherId}` : `${otherId}:${note.id}`\n if (renderedPairs.has(key)) continue\n renderedPairs.add(key)\n\n // Newer first — the later statement is usually the current one.\n const [newer, older] = Date.parse(note.timestamp) >= Date.parse(other.timestamp) ? [note, other] : [other, note]\n const zh = LOCALE === 'zh'\n pairLines.push(`### 🟠 ${zh ? '冲突' : 'CONFLICT'} | ${newer.category || 'General'}`)\n if (newer.conflict_reason) {\n pairLines.push(`**${zh ? '判定理由' : 'Why'}:** ${newer.conflict_reason}`)\n pairLines.push('')\n }\n pairLines.push(`| | ${zh ? '时间' : 'When'} | ${zh ? '内容' : 'Content'} |`)\n pairLines.push('| :-- | :-- | :-- |')\n pairLines.push(`| **A** | ${newer.timestamp.slice(0, 10)} | ${newer.content.replace(/\\n/g, ' ')} |`)\n pairLines.push(`| **B** | ${older.timestamp.slice(0, 10)} | ${older.content.replace(/\\n/g, ' ')} |`)\n pairLines.push('')\n pairLines.push(`\\`A: ${newer.id}\\``)\n pairLines.push(`\\`B: ${older.id}\\``)\n pairLines.push('')\n pairLines.push(\n zh\n ? `- [ ] ✅ **A 是当前状态,停用 B**(推荐:A 更新)`\n : `- [ ] ✅ **A is current — retire B** (recommended: A is newer)`\n )\n pairLines.push(zh ? `- [ ] ↩️ B 是当前状态,停用 A` : `- [ ] ↩️ B is current — retire A`)\n pairLines.push(zh ? `- [ ] 🤝 两者都成立(误判)` : `- [ ] 🤝 Both hold — not a contradiction`)\n pairLines.push('')\n pairLines.push('---')\n pairLines.push('')\n }\n }\n if (pairLines.length > 0) {\n lines.push(LOCALE === 'zh' ? '## 冲突(成对,一个冲突一个决定)' : '## Conflicts (paired — one decision each)')\n lines.push('')\n lines.push(...pairLines)\n lines.push(LOCALE === 'zh' ? '## 其余条目' : '## Other items')\n lines.push('')\n }\n\n for (let i = 0; i < items.length; i++) {\n const { note, reasons } = items[i]\n const badge = severityBadge(reasons)\n const reasonStr = reasons.map(reasonLabel).join('、')\n\n const issueLabel = LOCALE === 'zh' ? '问题' : 'Issue'\n const contentLabel = LOCALE === 'zh' ? '内容' : 'Content'\n const kwLabel = LOCALE === 'zh' ? '关键词' : 'Keywords'\n const tagLabel = LOCALE === 'zh' ? '标签' : 'Tags'\n const keepLabel = LOCALE === 'zh' ? '保留' : 'Keep'\n const rewriteLabel = LOCALE === 'zh' ? '改写' : 'Rewrite'\n const deleteLabel = LOCALE === 'zh' ? '删除' : 'Delete'\n\n lines.push(`### [${i + 1}] ${badge} | ${note.category || 'General'}`)\n lines.push(`\\`${note.id}\\``)\n lines.push('')\n lines.push(`**${issueLabel}:** ${reasonStr}`)\n lines.push('')\n lines.push(`**${contentLabel}:**`)\n lines.push('```')\n lines.push(note.content)\n lines.push('```')\n lines.push('')\n lines.push(`**${kwLabel}:** ${note.keywords.join(', ')}`)\n lines.push(`**${tagLabel}:** ${note.tags.join(', ')}`)\n lines.push('')\n lines.push(`- [ ] ✅ ${keepLabel}`)\n lines.push(`- [ ] 🔧 ${rewriteLabel}`)\n lines.push(`- [ ] 🗑️ ${deleteLabel}`)\n lines.push('')\n lines.push('---')\n lines.push('')\n }\n\n fs.mkdirSync(path.dirname(filePath), { recursive: true })\n fs.writeFileSync(filePath, lines.join('\\n'), 'utf8')\n\n return filePath\n}\n","/**\n * migrate.ts — rebuild a collection under a different embedding model.\n *\n * Changing the embedding model is a breaking change whenever the vector width\n * differs: Qdrant fixes a collection's size at creation and cannot alter it. So\n * the move is always build-alongside → backfill → verify → switch, never in\n * place. The source collection is read and never written, which is what makes\n * the whole thing reversible: if anything looks wrong, point AMEM_COLLECTION\n * back at it.\n *\n * Re-embedding costs no LLM calls. Every field that feeds the vector — content,\n * keywords, tags, context — is already in the payload, so a backfill is local\n * compute. The one exception is notes written before the extraction pipeline\n * filled those fields in: `refreshFields` re-runs construction for those, and\n * only those, because a vector built from a note with no keywords or tags is\n * built from less text than the same note would produce today.\n */\nimport {\n scrollAllRaw,\n countPointsRaw,\n collectionDimRaw,\n createCollectionRaw,\n upsertPointsRaw,\n pointToNote,\n noteToPoint,\n type MemoryNote,\n} from './storage.js'\nimport { encode, getEmbeddingDim, getEmbeddingModel } from './embedding.js'\nimport { llmConstructNote } from './llm.js'\nimport { buildEmbedText } from './memory.js'\n\nexport interface MigrateResult {\n /** Points found in the source. */\n total: number\n /** Notes whose derived fields were empty — the pre-pipeline cohort. */\n missingDerived: number\n /** Notes whose fields were re-extracted. 0 unless refreshFields. */\n refreshed: number\n /** Notes written into the target. 0 on a dry run. */\n migrated: number\n sourceDim: number | null\n targetDim: number\n model: string\n dryRun: boolean\n}\n\n/** A note that predates the extraction pipeline filling these in. */\nfunction missingDerivedFields(n: MemoryNote): boolean {\n return n.keywords.length === 0 || n.tags.length === 0\n}\n\nexport async function migrateCollection(opts: {\n /** Source collection. Read-only; never modified. */\n from: string\n /** Target collection. Created if absent; must not already hold points. */\n to: string\n /** Re-extract keywords/tags/context for notes that never had them. Default true. */\n refreshFields?: boolean\n /** Report what would happen and write nothing. Default TRUE — opt in to writing. */\n dryRun?: boolean\n logger?: { info: (m: string) => void; warn: (m: string) => void }\n}): Promise<MigrateResult> {\n const { from, to } = opts\n const refreshFields = opts.refreshFields !== false\n const dryRun = opts.dryRun !== false\n const log = opts.logger?.info ?? ((m: string) => console.log(m))\n const warn = opts.logger?.warn ?? ((m: string) => console.warn(m))\n\n if (from === to) throw new Error(`migrate: source and target are the same collection (\"${from}\")`)\n\n const model = getEmbeddingModel()\n const targetDim = await getEmbeddingDim()\n const sourceDim = await collectionDimRaw(from)\n if (sourceDim === null) throw new Error(`migrate: source collection \"${from}\" does not exist`)\n\n const points = await scrollAllRaw(from)\n const notes = points.map(pointToNote)\n const missingDerived = notes.filter(missingDerivedFields).length\n\n log(\n `[migrate] ${from} (${sourceDim}d, ${notes.length} notes) → ${to} (${targetDim}d, ${model}); ` +\n `${missingDerived} note(s) missing keywords/tags`\n )\n\n if (dryRun) {\n log('[migrate] dry run — nothing written. Pass dryRun: false to apply.')\n return { total: notes.length, missingDerived, refreshed: 0, migrated: 0, sourceDim, targetDim, model, dryRun: true }\n }\n\n // Refuse to write into a collection that already holds data. Backfilling twice\n // would be idempotent by id, but a target with UNRELATED points is a sign the\n // name is wrong, and silently mixing two stores is not recoverable by pointing\n // a config back.\n const existingTargetDim = await collectionDimRaw(to)\n if (existingTargetDim === null) {\n await createCollectionRaw(to, targetDim)\n log(`[migrate] created ${to} at ${targetDim}d`)\n } else {\n if (existingTargetDim !== targetDim) {\n throw new Error(`migrate: target \"${to}\" exists at ${existingTargetDim}d but the model produces ${targetDim}d`)\n }\n const existingCount = await countPointsRaw(to)\n if (existingCount > 0) {\n throw new Error(`migrate: target \"${to}\" already holds ${existingCount} point(s); use an empty collection`)\n }\n }\n\n let refreshed = 0\n let migrated = 0\n const BATCH = 64\n let buffer: Array<{ id: string; vector: number[]; payload: Record<string, unknown> }> = []\n\n const flush = async () => {\n if (!buffer.length) return\n await upsertPointsRaw(to, buffer)\n migrated += buffer.length\n buffer = []\n }\n\n for (const note of notes) {\n if (refreshFields && missingDerivedFields(note)) {\n try {\n const built = await llmConstructNote(note.content)\n // Only fill gaps. A note that already has tags keeps the ones it has —\n // this is a backfill for what was never extracted, not a re-labelling of\n // everything, which would churn memories the user may have curated.\n if (note.keywords.length === 0) note.keywords = built.keywords\n if (note.tags.length === 0) note.tags = built.tags\n if (!note.context) note.context = built.context\n refreshed++\n } catch (e) {\n warn(`[migrate] re-extract failed for ${note.id.slice(0, 8)} — keeping as-is: ${(e as Error).message}`)\n }\n }\n\n const point = noteToPoint({ ...note, embedding: await encode(buildEmbedText(note)) })\n buffer.push(point as { id: string; vector: number[]; payload: Record<string, unknown> })\n if (buffer.length >= BATCH) {\n await flush()\n log(`[migrate] ${migrated}/${notes.length}`)\n }\n }\n await flush()\n\n const finalCount = await countPointsRaw(to)\n if (finalCount !== notes.length) {\n warn(`[migrate] target holds ${finalCount} point(s) but the source had ${notes.length} — check before switching`)\n }\n\n log(\n `[migrate] done: ${migrated} migrated, ${refreshed} re-extracted. ` +\n `\"${from}\" is untouched — switch with AMEM_COLLECTION=${to}, and keep the old one until you are satisfied.`\n )\n\n return { total: notes.length, missingDerived, refreshed, migrated, sourceDim, targetDim, model, dryRun: false }\n}\n","/**\n * crud-guard.ts — write-safety policy for the agent_end CRUD decision (Story 41).\n *\n * The CRUD step hands the LLM a numbered list of candidate memories and asks it\n * to pick one to UPDATE or DELETE. Picking the WRONG number is the engine's only\n * silent, unrecoverable failure:\n *\n * - DELETE is already safe — `invalidateNote` is a soft delete (is_active=false).\n * - UPDATE is not — `updateNoteContent` overwrites content + embedding in place.\n *\n * An out-of-range index is harmless (`memories[bad]` is undefined and the caller\n * skips it). The dangerous case is an in-range but WRONG index: both are valid\n * array positions, so nothing structural catches it, and the access protocol\n * (Story 33/36) does not either — the caller usually does own the note it is\n * about to clobber.\n *\n * This is a documented failure class, not a hypothetical: mem0 removed its own\n * CRUD step in part because \"overwrites sometimes erased key information from the\n * original fact\", and Memory-R1 exists because vanilla LLMs mis-classify additive\n * facts as contradictions. The risk scales inversely with model capability, and\n * the memories that reach this step have already survived hash and vector dedup —\n * i.e. they are the HARDEST subset, exactly where a cheap model is least reliable.\n *\n * The rule below is the architectural answer to that, rather than paying for a\n * bigger model: before overwriting a memory, check that the replacement text is\n * at least plausibly ABOUT that memory. A mis-targeted UPDATE rewrites a note\n * with content that has nothing to do with it, which is cheap to detect — both\n * embeddings are already in hand, so this costs one dot product and no LLM call.\n */\nimport { cosineSimilarity } from './embedding.js'\n\n/**\n * Similarity floor for accepting an UPDATE target.\n *\n * Heuristic, not empirically tuned: it sits just above the 0.3 bar the engine\n * already uses for \"these two notes are related at all\", because a legitimate\n * CRUD UPDATE is often a correction or contradiction (\"drinks tea\" → \"switched to\n * coffee\") that is related but not near-identical. Set it too high and real\n * corrections get downgraded; too low and the guard does nothing.\n *\n * Failing this check is SAFE by construction — the caller inserts the fact as a\n * new memory instead of overwriting, and scheduled consolidation can merge later.\n * So the cost of a false positive is a duplicate, and the cost of a false\n * negative is a destroyed memory. Bias accordingly: raise it for cheaper models.\n */\nexport const DEFAULT_CRUD_UPDATE_MIN_SIM = 0.35\n\n/** Resolve the threshold: env var wins, then an explicit override, then default. */\nexport function resolveCrudUpdateMinSim(override?: number): number {\n const envVal = Number(process.env.AMEM_CRUD_UPDATE_MIN_SIM)\n if (Number.isFinite(envVal) && envVal >= 0) return envVal\n if (override !== undefined && Number.isFinite(override) && override >= 0) return override\n return DEFAULT_CRUD_UPDATE_MIN_SIM\n}\n\n/**\n * May `newEmbedding`'s fact overwrite the memory `targetEmbedding` belongs to?\n *\n * True when the replacement is plausibly about the same thing. False means the\n * LLM most likely named the wrong index — the caller should insert instead of\n * overwrite, never throw.\n *\n * Both vectors are L2-normalized by `encode`, so this is a dot product.\n */\nexport function isPlausibleUpdateTarget(\n newEmbedding: number[],\n targetEmbedding: number[],\n minSimilarity?: number\n): boolean {\n // A missing or malformed vector is not evidence of a good target. Refuse\n // rather than let a degenerate similarity wave the overwrite through.\n if (!newEmbedding?.length || !targetEmbedding?.length) return false\n if (newEmbedding.length !== targetEmbedding.length) return false\n return cosineSimilarity(newEmbedding, targetEmbedding) >= resolveCrudUpdateMinSim(minSimilarity)\n}\n"],"mappings":";AAQA,YAAY,QAAQ;AACpB,YAAY,UAAU;AAEtB,IAAI,WAAW,QAAQ,IAAI,iBAAsB,UAAQ,WAAQ,GAAG,OAAO;AAEpE,SAAS,UAAU,MAAkC;AAC1D,MAAI,KAAK,QAAS,YAAW,KAAK;AACpC;AAEO,SAAS,aAAqB;AACnC,SAAO;AACT;;;ACPA,IAAI,WAAgB;AACpB,IAAI,YAAiB;AACrB,IAAI,kBAAiC;AACrC,IAAI,YAA2B;AAGxB,IAAM,0BAA0B;AAGhC,SAAS,oBAA4B;AAC1C,SAAO,QAAQ,IAAI,kBAAkB,KAAK,KAAK;AACjD;AAmBA,IAAM,oBAAoB,oBAAI,IAAI;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAYM,SAAS,sBAAmC;AACjD,QAAM,WAAW,QAAQ,IAAI,oBAAoB,KAAK,EAAE,YAAY;AACpE,MAAI,aAAa,UAAU,aAAa,MAAO,QAAO;AACtD,QAAMA,YAAW,kBAAkB,EAAE,MAAM,GAAG,EAAE,IAAI,GAAG,YAAY,KAAK;AACxE,SAAO,kBAAkB,IAAIA,SAAQ,IAAI,QAAQ;AACnD;AAEA,eAAe,eAAe;AAC5B,QAAM,SAAS,kBAAkB;AAIjC,MAAI,aAAa,oBAAoB,OAAQ,QAAO;AACpD,MAAI,CAAC,UAAU;AACb,UAAM,MAAM,MAAM,OAAO,2BAA2B;AACpD,eAAW,IAAI;AAAA,EACjB;AACA,cAAY,MAAM,SAAS,sBAAsB,QAAQ;AAAA,IACvD,UAAU;AAAA,EACZ,CAAC;AACD,oBAAkB;AAClB,cAAY;AACZ,SAAO;AACT;AAUA,eAAsB,kBAAmC;AACvD,MAAI,cAAc,QAAQ,oBAAoB,kBAAkB,EAAG,QAAO;AAC1E,QAAM,QAAQ,MAAM,OAAO,iBAAiB;AAC5C,cAAY,MAAM;AAClB,SAAO;AACT;AASA,SAAS,cAAc,QAAoB,eAAyB,MAA6B;AAC/F,QAAM,SAAS,OAAO;AACtB,QAAM,MAAM,OAAO,CAAC,EAAE;AACtB,QAAM,SAAS,IAAI,MAAM,GAAG,EAAE,KAAK,CAAC;AAEpC,MAAI,SAAS,OAAO;AAClB,aAAS,IAAI,GAAG,IAAI,KAAK,IAAK,QAAO,CAAC,IAAI,OAAO,CAAC,EAAE,CAAC;AAAA,EACvD,OAAO;AACL,QAAI,UAAU;AACd,aAAS,IAAI,GAAG,IAAI,QAAQ,KAAK;AAC/B,YAAM,IAAI,cAAc,CAAC;AACzB,iBAAW;AACX,eAAS,IAAI,GAAG,IAAI,KAAK,KAAK;AAC5B,eAAO,CAAC,KAAK,OAAO,CAAC,EAAE,CAAC,IAAI;AAAA,MAC9B;AAAA,IACF;AACA,aAAS,IAAI,GAAG,IAAI,KAAK,KAAK;AAC5B,aAAO,CAAC,KAAK,KAAK,IAAI,SAAS,IAAI;AAAA,IACrC;AAAA,EACF;AAGA,MAAI,OAAO;AACX,aAAW,KAAK,OAAQ,SAAQ,IAAI;AACpC,SAAO,KAAK,KAAK,IAAI;AACrB,SAAO,OAAO,IAAI,CAAC,MAAM,IAAI,KAAK,IAAI,MAAM,IAAI,CAAC;AACnD;AAMA,eAAsB,OAAO,MAAiC;AAC5D,QAAM,MAAM,MAAM,aAAa;AAC/B,QAAM,UAAU,oBAAoB;AACpC,QAAM,SAAS,MAAM,IAAI,MAAM,EAAE,SAAS,WAAW,KAAK,CAAC;AAI3D,MAAI,UAAU,OAAO,MAAM;AACzB,WAAO,MAAM,KAAK,OAAO,IAAoB;AAAA,EAC/C;AAGA,QAAM,SAAS;AACf,MAAI,OAAO,QAAQ,OAAO,KAAK,WAAW,GAAG;AAE3C,UAAM,SAAS,OAAO,KAAK,CAAC;AAC5B,UAAM,MAAM,OAAO,KAAK,CAAC;AACzB,UAAM,MAAkB,CAAC;AACzB,aAAS,IAAI,GAAG,IAAI,QAAQ,KAAK;AAC/B,YAAM,MAAgB,CAAC;AACvB,eAAS,IAAI,GAAG,IAAI,KAAK,KAAK;AAC5B,YAAI,KAAK,OAAO,KAAK,IAAI,MAAM,CAAC,CAAC;AAAA,MACnC;AACA,UAAI,KAAK,GAAG;AAAA,IACd;AACA,WAAO,cAAc,KAAK,IAAI,MAAM,MAAM,EAAE,KAAK,CAAC,GAAG,OAAO;AAAA,EAC9D;AAEA,QAAM,IAAI,MAAM,mCAAmC;AACrD;AAMA,eAAsB,YAA2B;AAC/C,QAAM,aAAa;AACrB;AAOO,SAAS,gBAAyB;AACvC,SAAO,cAAc;AACvB;AAKO,SAAS,iBAAiB,GAAa,GAAqB;AACjE,MAAI,MAAM;AACV,WAAS,IAAI,GAAG,IAAI,EAAE,QAAQ,IAAK,QAAO,EAAE,CAAC,IAAI,EAAE,CAAC;AACpD,SAAO;AACT;;;ACnMA,SAAS,MAAM,cAAc;AAC7B,SAAS,kBAAkB;AAC3B,YAAYC,SAAQ;AACpB,YAAYC,WAAU;;;ACcf,SAAS,SAAS,MAA6C,eAAgC;AACpG,SAAO,KAAK,UAAU,iBAAiB,KAAK,QAAQ,SAAS,aAAa,KAAK,KAAK,QAAQ,SAAS,GAAG;AAC1G;AAcO,SAAS,QAAQ,MAA6C,eAAgC;AACnG,SAAO,KAAK,UAAU,iBAAiB,KAAK,QAAQ,SAAS,aAAa,KAAK,KAAK,QAAQ,SAAS,GAAG;AAC1G;;;AC8FA,IAAM,aAAa;AACnB,IAAM,gBAAgB,MAAM,QAAQ,IAAI,mBAAmB;AAMpD,IAAM,kCAAN,cAA8C,MAAM;AAAA,EACzD,YACW,YACA,eACA,UACA,OACT;AACA;AAAA,MACE,eAAe,UAAU,YAAY,aAAa,gDACxB,KAAK,cAAc,QAAQ;AAAA;AAAA,IAMvD;AAbS;AACA;AACA;AACA;AAWT,SAAK,OAAO;AAAA,EACd;AAAA,EAfW;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAab;AAGA,eAAe,OAAO,QAAgBC,OAAc,MAAkC;AACpF,QAAM,MAAM,MAAM,MAAM,GAAG,UAAU,GAAGA,KAAI,IAAI;AAAA,IAC9C;AAAA,IACA,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,IAC9C,MAAM,OAAO,KAAK,UAAU,IAAI,IAAI;AAAA,EACtC,CAAC;AACD,QAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,MAAI,CAAC,IAAI,MAAO,KAAK,UAAU,KAAK,WAAW,QAAQ,KAAK,WAAW,gBAAiB;AACtF,UAAM,IAAI,MAAM,UAAU,MAAM,IAAIA,KAAI,YAAY,KAAK,SAAS,KAAK,UAAU,IAAI,CAAC,EAAE;AAAA,EAC1F;AACA,SAAO,KAAK;AACd;AAUA,eAAsB,aAA4B;AAChD,QAAM,MAAM,MAAM,MAAM,GAAG,UAAU,SAAS;AAC9C,MAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM,8BAA8B,IAAI,MAAM,EAAE;AACzE;AAGA,IAAI,mBAAmB;AAEvB,IAAM,sBAAsB,oBAAI,IAAqB;AAarD,eAAsB,iBAAiB,gBAAwC;AAC7E,QAAM,MAAM,kBAAkB,cAAc;AAC5C,MAAI,gBAAgB;AAClB,QAAI,oBAAoB,IAAI,GAAG,EAAG;AAAA,EACpC,OAAO;AACL,QAAI,iBAAkB;AAAA,EACxB;AACA,QAAM,YAAY,MAAM;AACtB,QAAI,eAAgB,qBAAoB,IAAI,KAAK,IAAI;AAAA,QAChD,oBAAmB;AAAA,EAC1B;AAEA,MAAI,WAAkC;AACtC,MAAI;AACF,eAAY,MAAM,OAAO,OAAO,gBAAgB,GAAG,EAAE;AAAA,EACvD,QAAQ;AAAA,EAER;AAEA,MAAI,UAAU;AAKZ,UAAM,gBAAgB,SAAS,QAAQ,QAAQ,SAAS;AACxD,QAAI,OAAO,kBAAkB,UAAU;AACrC,YAAM,WAAW,MAAM,gBAAgB;AACvC,UAAI,kBAAkB,UAAU;AAC9B,cAAM,IAAI,gCAAgC,KAAK,eAAe,UAAU,kBAAkB,CAAC;AAAA,MAC7F;AAAA,IACF;AACA,cAAU;AACV;AAAA,EACF;AAEA,MAAI;AAGF,UAAM,OAAO,MAAM,gBAAgB;AACnC,UAAM,OAAO,OAAO,gBAAgB,GAAG,IAAI;AAAA,MACzC,SAAS,EAAE,MAAM,UAAU,SAAS;AAAA,IACtC,CAAC;AAAA,EACH,SAAS,KAAK;AAEZ,QAAI,EAAE,eAAe,UAAU,CAAC,IAAI,QAAQ,SAAS,gBAAgB,EAAG,OAAM;AAAA,EAChF;AAEA,QAAM,OAAO,OAAO,gBAAgB,GAAG,UAAU;AAAA,IAC/C,YAAY;AAAA,IACZ,cAAc;AAAA,EAChB,CAAC;AAED,QAAM,OAAO,OAAO,gBAAgB,GAAG,UAAU;AAAA,IAC/C,YAAY;AAAA,IACZ,cAAc;AAAA,EAChB,CAAC;AAED,QAAM,OAAO,OAAO,gBAAgB,GAAG,UAAU;AAAA,IAC/C,YAAY;AAAA,IACZ,cAAc;AAAA,EAChB,CAAC;AAED,QAAM,OAAO,OAAO,gBAAgB,GAAG,UAAU;AAAA,IAC/C,YAAY;AAAA,IACZ,cAAc;AAAA,EAChB,CAAC;AACD,YAAU;AACZ;AAQA,eAAsB,aACpB,YACA,QAAQ,KAC4E;AACpF,QAAM,MAAiF,CAAC;AACxF,MAAI,SAAkB;AACtB,aAAS;AACP,UAAM,OAAgC,EAAE,cAAc,MAAM,aAAa,MAAM,MAAM;AACrF,QAAI,WAAW,UAAa,WAAW,KAAM,MAAK,SAAS;AAC3D,UAAM,MAAO,MAAM,OAAO,QAAQ,gBAAgB,UAAU,kBAAkB,IAAI;AAIlF,QAAI,KAAK,GAAG,IAAI,MAAM;AACtB,aAAS,IAAI;AACb,QAAI,WAAW,UAAa,WAAW,QAAQ,IAAI,OAAO,WAAW,EAAG;AAAA,EAC1E;AACA,SAAO;AACT;AAGA,eAAsB,eAAe,YAAqC;AACxE,QAAM,MAAO,MAAM,OAAO,QAAQ,gBAAgB,UAAU,iBAAiB,EAAE,OAAO,KAAK,CAAC;AAG5F,SAAO,IAAI;AACb;AAGA,eAAsB,iBAAiB,YAA4C;AACjF,MAAI;AACF,UAAM,OAAQ,MAAM,OAAO,OAAO,gBAAgB,UAAU,EAAE;AAG9D,WAAO,KAAK,QAAQ,QAAQ,SAAS,QAAQ;AAAA,EAC/C,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGA,eAAsB,oBAAoB,YAAoB,MAA6B;AACzF,QAAM,OAAO,OAAO,gBAAgB,UAAU,IAAI,EAAE,SAAS,EAAE,MAAM,UAAU,SAAS,EAAE,CAAC;AAC3F,aAAW,cAAc,CAAC,YAAY,QAAQ,UAAU,UAAU,GAAG;AACnE,UAAM,OAAO,OAAO,gBAAgB,UAAU,UAAU,EAAE,YAAY,cAAc,UAAU,CAAC;AAAA,EACjG;AACF;AAGA,eAAsB,gBACpB,YACA,QACe;AACf,QAAM,OAAO,OAAO,gBAAgB,UAAU,qBAAqB,EAAE,OAAO,CAAC;AAC/E;AAGO,SAAS,YAAY,MAAkB;AAC5C,SAAO;AAAA,IACL,IAAI,KAAK;AAAA,IACT,QAAQ,KAAK;AAAA,IACb,SAAS;AAAA,MACP,SAAS,KAAK;AAAA,MACd,UAAU,KAAK;AAAA,MACf,MAAM,KAAK;AAAA,MACX,SAAS,KAAK;AAAA,MACd,OAAO,KAAK;AAAA,MACZ,WAAW,KAAK;AAAA,MAChB,UAAU,KAAK;AAAA,MACf,MAAM,KAAK;AAAA;AAAA,MAEX,iBAAiB,KAAK,mBAAmB;AAAA,MACzC,eAAe,KAAK,iBAAiB,KAAK;AAAA;AAAA,MAE1C,mBAAmB,KAAK,UAAU,KAAK,qBAAqB,CAAC,CAAC;AAAA;AAAA,MAE9D,UAAU,KAAK,YAAY;AAAA,MAC3B,WAAW,KAAK,cAAc;AAAA;AAAA,MAE9B,QAAQ,KAAK,UAAU,CAAC;AAAA;AAAA,MAExB,WAAW,KAAK,aAAa;AAAA;AAAA,MAE7B,eAAe,KAAK,iBAAiB;AAAA;AAAA,MAErC,gBAAgB,KAAK,kBAAkB;AAAA,MACvC,UAAU,KAAK,YAAY;AAAA,MAC3B,gBAAgB,KAAK,kBAAkB,CAAC;AAAA,MACxC,iBAAiB,KAAK,mBAAmB;AAAA,MACzC,qBAAqB,KAAK,uBAAuB;AAAA,MACjD,UAAU,KAAK,YAAY,CAAC;AAAA;AAAA,MAE5B,WAAW,KAAK,aAAa;AAAA,MAC7B,aAAa,KAAK,eAAe;AAAA;AAAA,MAEjC,OAAO,KAAK,SAAS,KAAK;AAAA,MAC1B,SAAS,KAAK,WAAW,CAAC,KAAK,QAAQ;AAAA,MACvC,SAAS,KAAK,WAAW,CAAC,KAAK,QAAQ;AAAA,IACzC;AAAA,EACF;AACF;AAEO,SAAS,YAAY,OAAwF;AAClH,QAAM,IAAI,MAAM;AAChB,QAAM,YAAa,EAAE,aAAwB;AAG7C,MAAI,mBAAqC,CAAC;AAC1C,MAAI;AACF,UAAM,MAAM,EAAE;AACd,QAAI,OAAO,QAAQ,YAAY,IAAI,SAAS,GAAG;AAC7C,yBAAmB,KAAK,MAAM,GAAG;AAAA,IACnC,WAAW,MAAM,QAAQ,GAAG,GAAG;AAE7B,yBAAmB;AAAA,IACrB;AAAA,EACF,QAAQ;AACN,uBAAmB,CAAC;AAAA,EACtB;AAEA,SAAO;AAAA,IACL,IAAI,OAAO,MAAM,EAAE;AAAA,IACnB,SAAU,EAAE,WAAsB;AAAA,IAClC,UAAW,EAAE,YAAyB,CAAC;AAAA,IACvC,MAAO,EAAE,QAAqB,CAAC;AAAA,IAC/B,SAAU,EAAE,WAAsB;AAAA,IAClC,OAAQ,EAAE,SAAsB,CAAC;AAAA,IACjC;AAAA,IACA,UAAW,EAAE,YAAuB;AAAA,IACpC,WAAW,MAAM,UAAU,CAAC;AAAA,IAC5B,MAAO,EAAE,QAAmB;AAAA;AAAA,IAE5B,iBAAiB,OAAO,EAAE,oBAAoB,WAAW,EAAE,kBAAkB;AAAA,IAC7E,eAAgB,EAAE,iBAA4B;AAAA;AAAA,IAE9C,mBAAmB;AAAA;AAAA,IAEnB,UAAW,EAAE,YAAuB;AAAA,IACpC,WAAW,EAAE,cAAc;AAAA;AAAA,IAE3B,WAAa,EAAE,cAAyB,cAAc,cAAc;AAAA;AAAA,IAEpE,QAAQ,MAAM,QAAQ,EAAE,MAAM,IAAK,EAAE,SAAsB,CAAC;AAAA;AAAA,IAE5D,eAAe,EAAE,kBAAkB;AAAA;AAAA,IAEnC,gBACE,OAAO,EAAE,mBAAmB,YAAY,CAAC,UAAU,YAAY,UAAU,KAAK,EAAE,SAAS,EAAE,cAAc,IACpG,EAAE,iBACH;AAAA,IACN,UAAU,EAAE,aAAa;AAAA,IACzB,gBAAgB,MAAM,QAAQ,EAAE,cAAc,IACzC,EAAE,eAA6B,OAAO,CAAC,MAAmB,OAAO,MAAM,QAAQ,IAChF,CAAC;AAAA,IACL,iBAAiB,OAAO,EAAE,oBAAoB,WAAW,EAAE,kBAAkB;AAAA,IAC7E,qBAAqB,OAAO,EAAE,wBAAwB,WAAW,EAAE,sBAAsB;AAAA,IACzF,UAAU,MAAM,QAAQ,EAAE,QAAQ,IAC7B,EAAE,SAAuB,OAAO,CAAC,MAAmB,OAAO,MAAM,QAAQ,IAC1E,CAAC;AAAA;AAAA,IAEL,WAAW,EAAE,cAAc;AAAA,IAC3B,aAAa,EAAE,gBAAgB;AAAA;AAAA,IAE/B,OAAQ,EAAE,SAAqB,EAAE,YAAuB;AAAA,IACxD,SAAS,MAAM,QAAQ,EAAE,OAAO,IAAK,EAAE,UAAuB,CAAE,EAAE,YAAuB,MAAM;AAAA,IAC/F,SAAS,MAAM,QAAQ,EAAE,OAAO,IAAK,EAAE,UAAuB,CAAE,EAAE,YAAuB,MAAM;AAAA,EACjG;AACF;AAGA,SAAS,YAAY,SAAiB,SAAkB;AACtD,QAAM,OAAkB;AAAA,IACtB;AAAA,MACE,QAAQ;AAAA,QACN,EAAE,KAAK,YAAY,OAAO,EAAE,OAAO,QAAQ,EAAE;AAAA,QAC7C,EAAE,KAAK,YAAY,OAAO,EAAE,OAAO,SAAS,EAAE;AAAA,MAChD;AAAA,IACF;AAAA,EACF;AASA,MAAI,YAAY,QAAW;AACzB,SAAK,KAAK;AAAA,MACR,QAAQ,CAAC,EAAE,KAAK,YAAY,OAAO,EAAE,OAAO,QAAQ,EAAE,GAAG,EAAE,UAAU,EAAE,KAAK,WAAW,EAAE,CAAC;AAAA,IAC5F,CAAC;AAAA,EACH;AAEA,SAAO;AAAA,IACL;AAAA,IACA,UAAU,CAAC,EAAE,KAAK,aAAa,OAAO,EAAE,OAAO,MAAM,EAAE,CAAC;AAAA,EAC1D;AACF;AAUA,SAAS,SAAS,gBAAwB,gBAAgB,OAAO;AAC/D,QAAM,MAAM;AAEZ,WAAS,kBAAkB,SAAiB,SAAkB;AAC5D,QAAI,eAAe;AAKjB,YAAM,OAAkB,CAAC;AACzB,UAAI,YAAY,QAAW;AACzB,aAAK,KAAK;AAAA,UACR,QAAQ,CAAC,EAAE,KAAK,YAAY,OAAO,EAAE,OAAO,QAAQ,EAAE,GAAG,EAAE,UAAU,EAAE,KAAK,WAAW,EAAE,CAAC;AAAA,QAC5F,CAAC;AAAA,MACH;AACA,aAAO;AAAA,QACL,GAAI,KAAK,SAAS,KAAK,EAAE,KAAK;AAAA,QAC9B,UAAU,CAAC,EAAE,KAAK,aAAa,OAAO,EAAE,OAAO,MAAM,EAAE,CAAC;AAAA,MAC1D;AAAA,IACF;AACA,WAAO,YAAY,SAAS,OAAO;AAAA,EACrC;AAEA,SAAO;AAAA,IACL,MAAM,QAAQ,MAAiC;AAC7C,YAAM,iBAAiB,GAAG;AAC1B,YAAM,OAAO,OAAO,gBAAgB,GAAG,qBAAqB;AAAA,QAC1D,QAAQ,CAAC,YAAY,IAAI,CAAC;AAAA,MAC5B,CAAC;AAAA,IACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IASA,MAAM,QAAQ,IAAY,eAAoD;AAC5E,YAAM,iBAAiB,GAAG;AAC1B,UAAI;AACF,cAAM,SAAU,MAAM,OAAO,QAAQ,gBAAgB,GAAG,WAAW;AAAA,UACjE,KAAK,CAAC,EAAE;AAAA,UACR,cAAc;AAAA,UACd,aAAa;AAAA,QACf,CAAC;AACD,YAAI,CAAC,OAAO,OAAQ,QAAO;AAC3B,cAAM,OAAO,YAAY,OAAO,CAAC,CAAC;AAClC,YAAI,kBAAkB,UAAa,CAAC,QAAQ,MAAM,aAAa,EAAG,QAAO;AACzE,eAAO;AAAA,MACT,QAAQ;AACN,eAAO;AAAA,MACT;AAAA,IACF;AAAA,IAEA,MAAM,WAAW,MAAiC;AAChD,YAAM,iBAAiB,GAAG;AAC1B,YAAM,OAAO,OAAO,gBAAgB,GAAG,qBAAqB;AAAA,QAC1D,QAAQ,CAAC,YAAY,IAAI,CAAC;AAAA,MAC5B,CAAC;AAAA,IACH;AAAA,IAEA,MAAM,WAAW,MAAc,SAA6C;AAC1E,YAAM,iBAAiB,GAAG;AAC1B,YAAM,OAAO;AAAA,QACX,QAAQ;AAAA,UACN,MAAM;AAAA,YACJ,EAAE,KAAK,QAAQ,OAAO,EAAE,OAAO,KAAK,EAAE;AAAA,YACtC,EAAE,KAAK,aAAa,OAAO,EAAE,OAAO,KAAK,EAAE;AAAA,YAC3C,GAAI,gBACA,CAAC,IACD;AAAA,cACE;AAAA,gBACE,QAAQ;AAAA,kBACN,EAAE,KAAK,YAAY,OAAO,EAAE,OAAO,QAAQ,EAAE;AAAA,kBAC7C,EAAE,KAAK,YAAY,OAAO,EAAE,OAAO,SAAS,EAAE;AAAA,gBAChD;AAAA,cACF;AAAA,YACF;AAAA,UACN;AAAA,QACF;AAAA,QACA,cAAc;AAAA,QACd,aAAa;AAAA,QACb,OAAO;AAAA,MACT;AACA,YAAM,SAAU,MAAM,OAAO,QAAQ,gBAAgB,GAAG,kBAAkB,IAAI;AAG9E,UAAI,CAAC,OAAO,OAAO,OAAQ,QAAO;AAClC,aAAO,YAAY,OAAO,OAAO,CAAC,CAAC;AAAA,IACrC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAUA,MAAM,kBACJ,IACA,SACA,WACA,MACA,eACkB;AAClB,YAAM,iBAAiB,GAAG;AAC1B,UAAI,WAA8B;AAClC,UAAI,kBAAkB,QAAW;AAC/B,mBAAW,MAAM,KAAK,QAAQ,EAAE;AAChC,YAAI,YAAY,CAAC,SAAS,UAAU,aAAa,EAAG,QAAO;AAAA,MAC7D;AACA,YAAM,OAAO,OAAO,gBAAgB,GAAG,6BAA6B;AAAA,QAClE,QAAQ,CAAC,EAAE,IAAI,QAAQ,UAAU,CAAC;AAAA,MACpC,CAAC;AACD,YAAM,UAAmC,EAAE,SAAS,KAAK;AAOzD,UAAI,UAAU;AACZ,cAAM,UAA4B;AAAA,UAChC,GAAI,SAAS,qBAAqB,CAAC;AAAA,UACnC;AAAA,YACE,aAAa;AAAA,YACb,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,YACpC,YAAY,SAAS;AAAA,YACrB,YAAY,SAAS;AAAA,YACrB,SAAS,SAAS;AAAA,YAClB,SAAS,SAAS;AAAA,YAClB,QAAQ;AAAA,YACR,YAAY,SAAS;AAAA,UACvB;AAAA,QACF;AACA,gBAAQ,oBAAoB,KAAK,UAAU,OAAO;AAAA,MACpD;AACA,YAAM,OAAO,QAAQ,gBAAgB,GAAG,6BAA6B;AAAA,QACnE;AAAA,QACA,QAAQ,CAAC,EAAE;AAAA,MACb,CAAC;AACD,aAAO;AAAA,IACT;AAAA,IAEA,MAAM,iBACJ,WACA,MACA,SACA,iBAAiB,GACjB,SACwB;AACxB,YAAM,iBAAiB,GAAG;AAC1B,YAAM,SAAU,MAAM,OAAO,QAAQ,gBAAgB,GAAG,kBAAkB;AAAA,QACxE,QAAQ;AAAA,QACR,OAAO;AAAA,QACP,cAAc;AAAA,QACd,aAAa;AAAA,QACb,iBAAiB;AAAA,QACjB,QAAQ,kBAAkB,SAAS,OAAO;AAAA,MAC5C,CAAC;AAED,YAAM,eAAe,OAAO,IAAI,CAAC,OAAO;AAAA,QACtC,MAAM,YAAY,CAAC;AAAA,QACnB,OAAO,EAAE;AAAA,MACX,EAAE;AAEF,UAAI,aAAa,SAAS,GAAG;AAC3B,cAAM,OAAM,oBAAI,KAAK,GAAE,YAAY;AACnC,cAAM,MAAM,aAAa,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE;AAC7C,cAAM,UAAU,aAAa,IAAI,CAAC,OAAO;AAAA,UACvC,IAAI,EAAE,KAAK;AAAA,UACX,kBAAkB,EAAE,KAAK,mBAAmB,KAAK;AAAA,QACnD,EAAE;AACF,gBAAQ,IAAI;AAAA,UACV,OAAO,QAAQ,gBAAgB,GAAG,8BAA8B;AAAA,YAC9D,SAAS,EAAE,eAAe,IAAI;AAAA,YAC9B,QAAQ;AAAA,UACV,CAAC;AAAA,UACD,GAAG,QAAQ;AAAA,YAAI,CAAC,MACd,OAAO,QAAQ,gBAAgB,GAAG,8BAA8B;AAAA,cAC9D,SAAS,EAAE,iBAAiB,EAAE,gBAAgB;AAAA,cAC9C,QAAQ,CAAC,EAAE,EAAE;AAAA,YACf,CAAC;AAAA,UACH;AAAA,QACF,CAAC,EAAE,MAAM,CAAC,QAAiB;AACzB,kBAAQ,MAAM,2CAA4C,IAAc,OAAO,EAAE;AAAA,QACnF,CAAC;AACD,mBAAW,KAAK,cAAc;AAC5B,YAAE,KAAK,mBAAmB,EAAE,KAAK,mBAAmB,KAAK;AACzD,YAAE,KAAK,gBAAgB;AAAA,QACzB;AAAA,MACF;AAEA,aAAO;AAAA,IACT;AAAA,IAEA,MAAM,UAAU,SAAkB,SAAyC;AACzE,YAAM,iBAAiB,GAAG;AAC1B,YAAM,OAAgC;AAAA,QACpC,cAAc;AAAA,QACd,aAAa;AAAA,QACb,OAAO;AAAA,MACT;AACA,UAAI,QAAS,MAAK,SAAS,kBAAkB,SAAS,OAAO;AAE7D,YAAM,SAAU,MAAM,OAAO,QAAQ,gBAAgB,GAAG,kBAAkB,IAAI;AAG9E,aAAO,OAAO,OAAO,IAAI,WAAW;AAAA,IACtC;AAAA,IAEA,MAAM,WAAW,IAA2B;AAC1C,YAAM,iBAAiB,GAAG;AAC1B,YAAM,OAAO,QAAQ,gBAAgB,GAAG,kBAAkB;AAAA,QACxD,QAAQ,CAAC,EAAE;AAAA,MACb,CAAC;AAAA,IACH;AAAA;AAAA,IAGA,MAAM,eAAe,IAAY,eAA0C;AACzE,YAAM,iBAAiB,GAAG;AAC1B,UAAI,kBAAkB,QAAW;AAC/B,cAAM,WAAW,MAAM,KAAK,QAAQ,EAAE;AACtC,YAAI,YAAY,CAAC,SAAS,UAAU,aAAa,EAAG,QAAO;AAAA,MAC7D;AACA,YAAM,OAAO,QAAQ,gBAAgB,GAAG,6BAA6B;AAAA,QACnE,SAAS,EAAE,WAAW,MAAM;AAAA,QAC5B,QAAQ,CAAC,EAAE;AAAA,MACb,CAAC;AACD,aAAO;AAAA,IACT;AAAA,IAEA,MAAM,qBAAqB,YAAoB,SAAwC;AACrF,YAAM,iBAAiB,GAAG;AAC1B,YAAM,gBAA2B,CAAC,EAAE,KAAK,aAAa,OAAO,EAAE,OAAO,KAAK,EAAE,CAAC;AAC9E,UAAI,CAAC,eAAe;AAClB,sBAAc,KAAK;AAAA,UACjB,QAAQ;AAAA,YACN,EAAE,KAAK,YAAY,OAAO,EAAE,OAAO,QAAQ,EAAE;AAAA,YAC7C,EAAE,KAAK,YAAY,OAAO,EAAE,OAAO,SAAS,EAAE;AAAA,UAChD;AAAA,QACF,CAAC;AAAA,MACH;AACA,YAAM,OAAgC;AAAA,QACpC,QAAQ,EAAE,MAAM,cAAc;AAAA,QAC9B,cAAc;AAAA,QACd,aAAa;AAAA,QACb,OAAO;AAAA,MACT;AACA,YAAM,SAAU,MAAM,OAAO,QAAQ,gBAAgB,GAAG,kBAAkB,IAAI;AAG9E,aAAO,OAAO,OAAO,IAAI,WAAW,EAAE,OAAO,CAAC,MAAM,EAAE,UAAU,WAAW,UAAU,CAAC;AAAA,IACxF;AAAA,IAEA,MAAM,WAAW,SAAmC;AAClD,YAAM,iBAAiB,GAAG;AAC1B,YAAM,OAAgC,EAAE,OAAO,KAAK;AACpD,UAAI,QAAS,MAAK,SAAS,kBAAkB,OAAO;AACpD,YAAM,SAAU,MAAM,OAAO,QAAQ,gBAAgB,GAAG,iBAAiB,IAAI;AAC7E,aAAO,OAAO;AAAA,IAChB;AAAA,IAEA,MAAM,gBAAgB,IAAY,OAAgC;AAChE,YAAM,iBAAiB,GAAG;AAC1B,YAAM,OAAO,QAAQ,gBAAgB,GAAG,6BAA6B;AAAA,QACnE,SAAS,EAAE,MAAM;AAAA,QACjB,QAAQ,CAAC,EAAE;AAAA,MACb,CAAC;AAAA,IACH;AAAA,IAEA,MAAM,iBAAiB,IAAY,QAAgD;AACjF,YAAM,iBAAiB,GAAG;AAC1B,YAAM,OAAO,QAAQ,gBAAgB,GAAG,6BAA6B;AAAA,QACnE,SAAS;AAAA,QACT,QAAQ,CAAC,EAAE;AAAA,MACb,CAAC;AAAA,IACH;AAAA,IAEA,MAAM,sBAAsB,OAAe,OAAe,SAAgC;AACxF,YAAM,QAAQ,MAAM,KAAK,UAAU,OAAO;AAC1C,iBAAW,QAAQ,OAAO;AAKxB,YAAI,CAAC,SAAS,MAAM,OAAO,EAAG;AAC9B,YAAI,KAAK,MAAM,SAAS,KAAK,GAAG;AAC9B,gBAAM,WAAW,KAAK,MAAM,IAAI,CAAC,WAAY,WAAW,QAAQ,QAAQ,MAAO;AAC/E,gBAAM,gBAAgB,SAAS,OAAO,CAAC,WAAW,WAAW,KAAK,EAAE;AACpE,gBAAM,cAAc,MAAM,KAAK,IAAI,IAAI,aAAa,CAAC;AACrD,gBAAM,KAAK,gBAAgB,KAAK,IAAI,WAAW;AAAA,QACjD;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AASO,SAAS,qBAAqB,gBAAyB,gBAAgB,OAAuB;AACnG,SAAO,SAAS,kBAAkB,cAAc,GAAG,aAAa;AAClE;AAQA,eAAsB,QAAQ,IAAY,eAAoD;AAC5F,SAAO,SAAS,cAAc,CAAC,EAAE,QAAQ,IAAI,aAAa;AAC5D;AAEA,eAAsB,WAAW,MAAiC;AAChE,SAAO,SAAS,cAAc,CAAC,EAAE,WAAW,IAAI;AAClD;AA0BA,eAAsB,UAAU,SAAkB,SAAyC;AACzF,SAAO,SAAS,cAAc,CAAC,EAAE,UAAU,SAAS,OAAO;AAC7D;AAEA,eAAsB,WAAW,IAA2B;AAC1D,SAAO,SAAS,cAAc,CAAC,EAAE,WAAW,EAAE;AAChD;AAEA,eAAsB,eAAe,IAAY,eAA0C;AACzF,SAAO,SAAS,cAAc,CAAC,EAAE,eAAe,IAAI,aAAa;AACnE;AAcA,eAAsB,iBAAiB,IAAY,QAAgD;AACjG,SAAO,SAAS,cAAc,CAAC,EAAE,iBAAiB,IAAI,MAAM;AAC9D;;;ACz1BA,OAAO,eAAe;AACtB,OAAO,YAAY;;;ACwBnB,IAAM,SAAwB,QAAQ,IAAI,uBAAwC,OAAO,OAAO;AAIhG,IAAM,KAAoB;AAAA,EACxB,cAAc,CACZ,UACA,eACA,eACG;AAAA;AAAA;AAAA;AAAA,QAIC,QAAQ;AAAA,aACH,aAAa;AAAA;AAAA;AAAA;AAAA,EAIxB,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA+BV,aAAa,CACX,UACA,aACG;AAAA;AAAA,YAEK,QAAQ;AAAA,YACR,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAsBlB,gBAAgB,CACd,YACA,eACG;AAAA;AAAA,cAEO,UAAU;AAAA,cACV,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAiBtB,cAAc,CAAC,kBAAkB;AAAA;AAAA;AAAA;AAAA,EAIjC,aAAa;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA4Bf;AAIA,IAAM,KAAoB;AAAA,EACxB,cAAc,CAAC,UAAU,eAAe,eAAe;AAAA;AAAA;AAAA;AAAA,oBAIpD,QAAQ;AAAA,oBACR,aAAa;AAAA;AAAA;AAAA;AAAA,EAIhB,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA+BV,aAAa,CAAC,UAAU,aAAa;AAAA;AAAA,qBAEjC,QAAQ;AAAA,qBACR,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAsBZ,gBAAgB,CAAC,YAAY,eAAe;AAAA;AAAA,0BAExC,UAAU;AAAA,0BACV,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAiBd,cAAc,CAAC,kBAAkB;AAAA;AAAA;AAAA;AAAA,EAIjC,aAAa;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAyBf;AAIA,IAAM,YAAiD,EAAE,IAAI,GAAG;AAEzD,IAAM,IAAI,UAAU,MAAM;;;ADxNjC,IAAI,YAAuB,CAAC;AASrB,SAAS,aAAa,KAAsB;AACjD,cAAY,EAAE,GAAG,IAAI;AAIrB,oBAAkB,MAAM;AACxB,iBAAe,MAAM;AACvB;AAGA,IAAM,UAAU,oBAAI,IAAY;AAChC,SAAS,SAAS,KAAa,SAAuB;AACpD,MAAI,QAAQ,IAAI,GAAG,EAAG;AACtB,UAAQ,IAAI,GAAG;AACf,UAAQ,MAAM,OAAO;AACvB;AAMA,SAAS,gBAAgB,OAAgB,QAAgB;AAIvD,QAAM,MACJ,SAAS,WAAW,QAAQ,IAAI,4BAA4B,UAAU,QAAQ,YAAY,SAAY;AACxG,QAAM,KAAK,OAAO,QAAQ,IAAI,qBAAqB,UAAU,YAAY,aAAa,KAAK,EAAE,YAAY;AACzG,MAAI,MAAM,eAAe,MAAM,UAAU;AAGvC,aAAS,YAAY,CAAC,IAAI,gCAAgC,CAAC,8BAA8B;AAAA,EAC3F;AACA,SAAO;AACT;AAEA,SAAS,aAAa,OAAgB,QAAgB;AACpD,QAAM,SACJ,SAAS,WAAW,QAAQ,IAAI,yBAAyB,UAAU,QAAQ,SAAS,SAAY;AAClG,SACE,UACA,QAAQ,IAAI,kBACZ,UAAU,UACT,gBAAgB,IAAI,MAAM,WAAW,gBAAgB;AAE1D;AAEA,SAAS,eAAe,OAAgB,QAA4B;AAClE,QAAM,SACJ,SAAS,WAAW,QAAQ,IAAI,4BAA4B,UAAU,QAAQ,WAAW,SAAY;AACvG,SAAO,UAAU,QAAQ,IAAI,qBAAqB,UAAU,WAAW;AACzE;AAOA,SAAS,kBAA2B;AAClC,QAAM,OAAO,QAAQ,IAAI,sBAAsB,UAAU,YAAY,QAAQ,KAAK,EAAE,YAAY;AAChG,MAAI,QAAQ,SAAU,QAAO;AAC7B,MAAI,QAAQ,QAAQ;AAClB,aAAS,YAAY,GAAG,IAAI,sCAAsC,GAAG,eAAe;AAAA,EACtF;AACA,SAAO;AACT;AAEA,IAAM,qBAAqB;AAC3B,SAAS,mBAA2B;AAClC,QAAM,SAAS,OAAO,QAAQ,IAAI,gBAAgB;AAClD,MAAI,OAAO,SAAS,MAAM,KAAK,SAAS,EAAG,QAAO;AAClD,MAAI,UAAU,aAAa,UAAU,YAAY,EAAG,QAAO,UAAU;AACrE,SAAO;AACT;AAYA,IAAM,oBAAoB,oBAAI,IAAuB;AACrD,SAAS,UAAU,SAAwC;AACzD,QAAM,MAAM,WAAW;AACvB,MAAI,SAAS,kBAAkB,IAAI,GAAG;AACtC,MAAI,CAAC,QAAQ;AACX,aAAS,IAAI,UAAU;AAAA,MACrB,GAAI,QAAQ,IAAI,oBAAoB,EAAE,QAAQ,QAAQ,IAAI,iBAAiB;AAAA,MAC3E,GAAI,WAAW,EAAE,QAAQ;AAAA,MACzB,SAAS,iBAAiB;AAAA,IAC5B,CAAC;AACD,sBAAkB,IAAI,KAAK,MAAM;AAAA,EACnC;AACA,SAAO;AACT;AAEA,IAAM,iBAAiB,oBAAI,IAAoB;AAC/C,SAAS,OAAO,SAAqC;AACnD,QAAM,MAAM,WAAW;AACvB,MAAI,SAAS,eAAe,IAAI,GAAG;AACnC,MAAI,CAAC,QAAQ;AACX,aAAS,IAAI,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA,MAKlB,QAAQ,QAAQ,IAAI,oBAAoB,QAAQ,IAAI,kBAAkB;AAAA,MACtE,GAAI,WAAW,EAAE,QAAQ;AAAA,MACzB,SAAS,iBAAiB;AAAA,IAC5B,CAAC;AACD,mBAAe,IAAI,KAAK,MAAM;AAAA,EAChC;AACA,SAAO;AACT;AAGA,eAAsB,QAAQ,QAAgB,YAAY,KAAK,OAAgB,QAAgC;AAC7G,QAAM,WAAW,gBAAgB,IAAI;AACrC,QAAM,QAAQ,aAAa,IAAI;AAC/B,QAAM,UAAU,eAAe,IAAI;AAEnC,QAAM,aAAa,MAAM,SAAS,QAAQ,KAAK,MAAM,SAAS,WAAW;AACzE,QAAM,qBAAqB,aAAa,KAAK,IAAI,YAAY,GAAG,GAAI,IAAI;AACxE,MAAI;AACF,WAAO,aAAa,WAChB,MAAM,WAAW,QAAQ,OAAO,oBAAoB,OAAO,IAC3D,MAAM,cAAc,QAAQ,OAAO,oBAAoB,OAAO;AAAA,EACpE,SAAS,GAAG;AACV,YAAQ,MAAM,2BAA4B,EAAY,OAAO,EAAE;AAC/D,WAAO;AAAA,EACT;AACF;AAEA,eAAe,cACb,QACA,OACA,WACA,SACwB;AACxB,QAAM,OAAO,MAAM,UAAU,OAAO,EAAE,SAAS,OAAO;AAAA,IACpD;AAAA,IACA,YAAY;AAAA,IACZ,UAAU,CAAC,EAAE,MAAM,QAAQ,SAAS,OAAO,CAAC;AAAA,EAC9C,CAAC;AACD,aAAW,SAAS,KAAK,SAAS;AAChC,QAAI,MAAM,SAAS,OAAQ,QAAO,MAAM,KAAK,KAAK;AAAA,EACpD;AACA,SAAO;AACT;AAEA,eAAe,WACb,QACA,OACA,WACA,SACwB;AAMxB,QAAM,cAAc,OAAO,KAAK,KAAK,KAAK,MAAM,WAAW,OAAO;AAClE,QAAM,OAAO,MAAM,OAAO,OAAO,EAAE,KAAK,YAAY,OAAO;AAAA,IACzD;AAAA,IACA,GAAI,cAAc,EAAE,uBAAuB,UAAU,IAAI,EAAE,YAAY,UAAU;AAAA,IACjF,UAAU,CAAC,EAAE,MAAM,QAAQ,SAAS,OAAO,CAAC;AAAA,EAC9C,CAAC;AACD,SAAO,KAAK,QAAQ,CAAC,GAAG,SAAS,SAAS,KAAK,KAAK;AACtD;AAYA,SAAS,eAAe,KAAqB;AAC3C,SAAO,IACJ,QAAQ,8BAA8B,EAAE,EACxC,QAAQ,yEAAyE,EAAE,EACnF,KAAK;AACV;AAEA,SAAS,YAAY,KAAqB;AACxC,QAAM,eAAe,GAAG;AACxB,MAAI,IAAI,WAAW,KAAK,GAAG;AACzB,UAAM,QAAQ,IAAI,MAAM,IAAI;AAC5B,UAAM,MAAM;AACZ,QAAI,MAAM,MAAM,SAAS,CAAC,MAAM,MAAO,OAAM,IAAI;AACjD,UAAM,MAAM,KAAK,IAAI,EAAE,KAAK;AAAA,EAC9B;AAEA,MAAK,IAAI,WAAW,GAAG,KAAK,IAAI,SAAS,GAAG,KAAO,IAAI,WAAW,GAAG,KAAK,IAAI,SAAS,GAAG,GAAI;AAC5F,QAAI;AACF,YAAM,KAAK,MAAM,GAAG;AAAA,IACtB,QAAQ;AAAA,IAER;AAAA,EACF;AACA,SAAO;AACT;AASA,SAAS,eAAe,KAAkB;AACxC,QAAM,UAAU,YAAY,GAAG;AAC/B,MAAI;AACF,WAAO,KAAK,MAAM,OAAO;AAAA,EAC3B,SAAS,GAAG;AACV,UAAM,IAAI,QAAQ,MAAM,aAAa;AACrC,QAAI,EAAG,QAAO,KAAK,MAAM,EAAE,CAAC,CAAC;AAC7B,UAAM;AAAA,EACR;AACF;AAwBA,IAAM,mBAAmB,oBAAI,IAAY,CAAC,QAAQ,UAAU,KAAK,CAAC;AAElE,IAAM,mBAAmB,oBAAI,IAAY;AAAA,EACvC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAED,eAAsB,iBAAiB,SAAyC;AAC9E,QAAM,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAiCT,OAAO;AAEb,QAAM,MAAM,MAAM,QAAQ,QAAQ,GAAG;AACrC,MAAI,CAAC;AACH,WAAO;AAAA,MACL,UAAU,CAAC;AAAA,MACX,MAAM,CAAC;AAAA,MACP,SAAS;AAAA,MACT,UAAU;AAAA,MACV,WAAW;AAAA,MACX,QAAQ,CAAC;AAAA,MACT,YAAY;AAAA,IACd;AAEF,MAAI;AACF,UAAM,OAAO,eAAe,GAAG;AAC/B,UAAM,cAAc,OAAO,KAAK,aAAa,WAAW,KAAK,WAAW;AACxE,UAAM,WAAyB,iBAAiB,IAAI,WAAW,IAAK,cAA+B;AACnG,UAAM,YAAoC,KAAK,cAAc,cAAc,cAAc;AACzF,UAAM,SACJ,cAAc,eAAe,MAAM,QAAQ,KAAK,MAAM,IACjD,KAAK,OAAqB,OAAO,CAAC,MAAmB,OAAO,MAAM,QAAQ,IAC3E,CAAC;AACP,UAAM,gBAAgB,OAAO,KAAK,eAAe,WAAW,KAAK,aAAa;AAC9E,UAAM,aAAwC,iBAAiB,IAAI,aAAa,IAC3E,gBACD;AACJ,WAAO;AAAA,MACL,UAAU,MAAM,QAAQ,KAAK,QAAQ,IAAI,KAAK,WAAW,CAAC;AAAA,MAC1D,MAAM,MAAM,QAAQ,KAAK,IAAI,IAAI,KAAK,OAAO,CAAC;AAAA,MAC9C,SAAS,OAAO,KAAK,YAAY,WAAW,KAAK,UAAU;AAAA,MAC3D;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF,SAAS,GAAG;AACV,YAAQ,MAAM,0CAA2C,EAAY,OAAO,EAAE;AAC9E,WAAO;AAAA,MACL,UAAU,CAAC;AAAA,MACX,MAAM,CAAC;AAAA,MACP,SAAS;AAAA,MACT,UAAU;AAAA,MACV,WAAW;AAAA,MACX,QAAQ,CAAC;AAAA,MACT,YAAY;AAAA,IACd;AAAA,EACF;AACF;AAGA,eAAsB,cAAc,aAAqB,kBAA4C;AACnG,QAAM,SAAS;AAAA;AAAA;AAAA,UAGP,WAAW;AAAA,UACX,gBAAgB;AAExB,QAAM,MAAM,MAAM,QAAQ,QAAQ,EAAE;AACpC,MAAI,CAAC,IAAK,QAAO;AACjB,SAAO,IAAI,YAAY,EAAE,WAAW,KAAK;AAC3C;AAUA,eAAsB,gBACpB,UACA,eACA,kBAC4B;AAC5B,QAAM,aACJ,iBAAiB,SAAS,IAAI,iBAAiB,IAAI,CAAC,MAAM,IAAI,EAAE,GAAG,KAAK,EAAE,OAAO,EAAE,EAAE,KAAK,IAAI,IAAI;AAEpG,QAAM,SAAS,EAAE,aAAa,SAAS,MAAM,GAAG,GAAG,GAAG,cAAc,MAAM,GAAG,GAAG,GAAG,UAAU;AAE7F,MAAI;AAKF,UAAM,MAAM,MAAM,QAAQ,QAAQ,KAAK,gBAAgB,CAAC;AACxD,QAAI,CAAC,IAAK,QAAO,CAAC;AAGlB,UAAM,QAAQ,eAAe,GAAG,EAAE,MAAM,SAAS;AACjD,QAAI,CAAC,MAAO,QAAO,CAAC;AACpB,UAAM,SAAS,KAAK,MAAM,MAAM,CAAC,CAAC;AAClC,QAAI,CAAC,MAAM,QAAQ,MAAM,EAAG,QAAO,CAAC;AACpC,UAAM,MAAyB,CAAC;AAChC,eAAW,QAAQ,QAAQ;AACzB,UAAI,CAAC,QAAQ,OAAO,SAAS,SAAU;AACvC,YAAM,SAAS,KAAK;AACpB,UAAI,CAAC,CAAC,OAAO,UAAU,UAAU,MAAM,EAAE,SAAS,MAAM,EAAG;AAC3D,UAAI,WAAW,OAAQ;AACvB,YAAM,KAAsB;AAAA,QAC1B;AAAA,QACA,MAAM,OAAO,KAAK,SAAS,WAAW,KAAK,OAAO;AAAA,QAClD,QAAQ,OAAO,KAAK,WAAW,WAAW,KAAK,SAAS;AAAA,MAC1D;AACA,UAAI,OAAO,KAAK,gBAAgB,UAAU;AACxC,WAAG,cAAc,KAAK;AAAA,MACxB;AACA,UAAI,KAAK,EAAE;AAAA,IACb;AACA,WAAO,IAAI,MAAM,GAAG,CAAC;AAAA,EACvB,SAAS,GAAG;AACV,YAAQ,MAAM,kCAAmC,EAAY,OAAO,EAAE;AACtE,WAAO,CAAC;AAAA,EACV;AACF;AAGA,eAAsB,eACpB,UACA,UACoD;AACpD,QAAM,SAAS,EAAE,YAAY,UAAU,QAAQ;AAI/C,QAAM,MAAM,MAAM,QAAQ,QAAQ,KAAK,QAAQ;AAC/C,MAAI,CAAC,IAAK,QAAO,EAAE,aAAa,MAAM;AAEtC,MAAI;AACF,UAAM,OAAO,eAAe,GAAG;AAC/B,QAAI,OAAO,KAAK,gBAAgB,UAAW,QAAO,EAAE,aAAa,MAAM;AACvE,QAAI,KAAK,eAAe,OAAO,KAAK,WAAW,UAAU;AACvD,aAAO,EAAE,aAAa,MAAM,QAAQ,KAAK,OAAO;AAAA,IAClD;AACA,WAAO,EAAE,aAAa,MAAM;AAAA,EAC9B,SAAS,GAAG;AACV,YAAQ,MAAM,uCAAwC,EAAY,OAAO,EAAE;AAC3E,WAAO,EAAE,aAAa,MAAM;AAAA,EAC9B;AACF;AAOA,IAAM,wBAAwB,oBAAI,IAAY,CAAC,UAAU,YAAY,UAAU,KAAK,CAAC;AAErF,eAAsB,kBACpB,YACA,YAC0D;AAC1D,QAAM,SAAS,EAAE,eAAe,YAAY,UAAU;AAItD,QAAM,MAAM,MAAM,QAAQ,QAAQ,KAAK,QAAQ;AAC/C,MAAI,CAAC,IAAK,QAAO,EAAE,MAAM,MAAM;AAE/B,MAAI;AACF,UAAM,OAAO,eAAe,GAAG;AAC/B,UAAM,OAAsB,sBAAsB,IAAI,KAAK,IAAI,IAAK,KAAK,OAAyB;AAClG,WAAO;AAAA,MACL;AAAA,MACA,eAAe,OAAO,KAAK,kBAAkB,WAAW,KAAK,gBAAgB;AAAA,IAC/E;AAAA,EACF,SAAS,GAAG;AACV,YAAQ,MAAM,0CAA2C,EAAY,OAAO,EAAE;AAC9E,WAAO,EAAE,MAAM,MAAM;AAAA,EACvB;AACF;AAWA,eAAsB,cACpB,SACA,aACsB;AACtB,QAAM,YAAY,YAAY,IAAI,CAAC,MAAM,SAAS,EAAE,EAAE;AAAA,aAAgB,EAAE,OAAO,EAAE,EAAE,KAAK,IAAI;AAC5F,QAAM,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,yBAgBQ,OAAO;AAAA;AAAA;AAAA,EAG9B,SAAS;AAET,QAAM,MAAM,MAAM,QAAQ,QAAQ,GAAG;AACrC,MAAI,CAAC,IAAK,QAAO,EAAE,MAAM,MAAM,SAAS,MAAM,kBAAkB,OAAO,sBAAsB,CAAC,GAAG,cAAc,CAAC,EAAE;AAElH,MAAI;AACF,UAAM,OAAO,eAAe,GAAG;AAC/B,WAAO;AAAA,MACL,MAAM,MAAM,QAAQ,KAAK,IAAI,IAAI,KAAK,OAAO;AAAA,MAC7C,SAAS,OAAO,KAAK,YAAY,WAAW,KAAK,UAAU;AAAA,MAC3D,kBAAkB,OAAO,KAAK,sBAAsB,YAAY,KAAK,oBAAoB;AAAA,MACzF,sBAAsB,MAAM,QAAQ,KAAK,qBAAqB,IAAI,KAAK,sBAAsB,IAAI,MAAM,IAAI,CAAC;AAAA,MAC5G,cAAc,MAAM,QAAQ,KAAK,cAAc,IAAI,KAAK,eAAe,IAAI,MAAM,IAAI,CAAC;AAAA,IACxF;AAAA,EACF,SAAS,GAAG;AACV,YAAQ,MAAM,kCAAmC,EAAY,OAAO,EAAE;AACtE,WAAO,EAAE,MAAM,MAAM,SAAS,MAAM,kBAAkB,OAAO,sBAAsB,CAAC,GAAG,cAAc,CAAC,EAAE;AAAA,EAC1G;AACF;AAsCA,eAAsB,gBAAgB,UAA6C;AACjF,MAAI,SAAS,SAAS,EAAG,QAAO,CAAC;AACjC,QAAM,WAAW,SAAS,IAAI,CAAC,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,EAAE,EAAE,KAAK,IAAI;AAEhE,MAAI;AACF,UAAM,MAAM,MAAM,QAAQ,EAAE,aAAa,QAAQ,GAAG,KAAK,QAAQ;AACjE,QAAI,CAAC,IAAK,QAAO,CAAC;AAClB,UAAM,UAAU,eAAe,GAAG;AAClC,UAAM,QAAQ,QAAQ,MAAM,aAAa;AACzC,QAAI,CAAC,MAAO,QAAO,CAAC;AACpB,UAAM,SAAS,KAAK,MAAM,MAAM,CAAC,CAAC;AAClC,QAAI,CAAC,MAAM,QAAQ,MAAM,EAAG,QAAO,CAAC;AAEpC,UAAM,QAAwB,CAAC;AAC/B,UAAM,OAAO,oBAAI,IAAY;AAC7B,eAAW,QAAQ,QAAQ;AACzB,UAAI,CAAC,QAAQ,OAAO,SAAS,SAAU;AACvC,YAAM,EAAE,GAAG,EAAE,IAAI;AACjB,UAAI,OAAO,MAAM,YAAY,OAAO,MAAM,SAAU;AACpD,UAAI,CAAC,OAAO,UAAU,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,EAAG;AAElD,UAAI,IAAI,KAAK,IAAI,KAAK,KAAK,SAAS,UAAU,KAAK,SAAS,OAAQ;AACpE,UAAI,MAAM,EAAG;AACb,YAAM,MAAM,IAAI,IAAI,GAAG,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,IAAI,CAAC;AAC3C,UAAI,KAAK,IAAI,GAAG,EAAG;AACnB,WAAK,IAAI,GAAG;AAIZ,YAAM,SAAU,KAAkC;AAClD,YAAM,kBAAkB,WAAW,KAAK,WAAW,IAAK,SAAoB;AAE5E,YAAM,KAAK;AAAA,QACT;AAAA,QACA;AAAA,QACA,QAAQ,OAAQ,KAA8B,WAAW,WAAY,KAA4B,SAAS;AAAA,QAC1G;AAAA,MACF,CAAC;AAAA,IACH;AACA,WAAO;AAAA,EACT,SAAS,GAAG;AACV,YAAQ,MAAM,kCAAmC,EAAY,OAAO,EAAE;AACtE,WAAO,CAAC;AAAA,EACV;AACF;;;AE/pBA,YAAY,QAAQ;AACpB,YAAYC,WAAU;AAGtB,SAAS,cAAsB;AAC7B,SAAO,QAAQ,IAAI,yBAA8B,WAAK,WAAW,GAAG,mBAAmB;AACzF;AACA,IAAM,gBAAgB;AAOf,SAAS,cAAsB;AACpC,MAAI;AACF,UAAM,OAAO,KAAK,MAAS,gBAAa,YAAY,GAAG,OAAO,CAAC;AAC/D,WAAO,KAAK,SAAS;AAAA,EACvB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,oBAA4B;AAC1C,QAAM,QAAQ,YAAY,IAAI;AAC9B,EAAG,iBAAc,YAAY,GAAG,KAAK,UAAU,EAAE,OAAO,YAAW,oBAAI,KAAK,GAAE,YAAY,EAAE,CAAC,CAAC;AAC9F,SAAO;AACT;AAEO,SAAS,qBAA8B;AAC5C,QAAM,QAAQ,kBAAkB;AAChC,SAAO,QAAQ,kBAAkB;AACnC;;;ALpBA,SAAS,aAAa;AAOtB,IAAM,UAAU,CAAC,OAAuB,GAAG,QAAQ,WAAW,EAAE;AAKhE,IAAI,SAAuB;AAC3B,SAAS,WAAkB;AACzB,MAAI,CAAC,OAAQ,UAAS,IAAI,MAAM;AAChC,SAAO;AACT;AASO,SAAS,eAAe,MAAwB;AACrD,QAAM,aAAa,kBAAkB,KAAK,IAAI;AAC9C,MAAI,YAAY;AAEd,WAAO,SAAS,EACb,IAAI,MAAM,IAAI,EACd,IAAI,CAACC,OAAMA,GAAE,YAAY,EAAE,KAAK,CAAC,EACjC,OAAO,CAACA,OAAMA,GAAE,SAAS,KAAK,oBAAoB,KAAKA,EAAC,CAAC;AAAA,EAC9D;AACA,SAAO,MAAM,KAAK,KAAK,YAAY,EAAE,SAAS,QAAQ,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;AAC1E;AASO,SAAS,UAAU,OAAgC;AACxD,QAAM,MAAM,MAAM,IAAI,CAAC,MAAM,EAAE,EAAE;AACjC,QAAM,SAAS,MAAM,IAAI,CAAC,MAAM;AAC9B,UAAM,OAAO,CAAC,EAAE,SAAS,GAAG,EAAE,UAAU,GAAG,EAAE,IAAI,EAAE,KAAK,GAAG;AAC3D,WAAO,eAAe,IAAI;AAAA,EAC5B,CAAC;AAGD,QAAM,KAAK,oBAAI,IAAoB;AACnC,aAAW,UAAU,QAAQ;AAC3B,eAAWA,MAAK,IAAI,IAAI,MAAM,EAAG,IAAG,IAAIA,KAAI,GAAG,IAAIA,EAAC,KAAK,KAAK,CAAC;AAAA,EACjE;AACA,QAAM,IAAI,OAAO;AACjB,QAAM,MAAM,oBAAI,IAAoB;AACpC,KAAG,QAAQ,CAAC,MAAM,SAAS;AACzB,QAAI,IAAI,MAAM,KAAK,KAAK,IAAI,OAAO,QAAQ,OAAO,OAAO,CAAC,CAAC;AAAA,EAC7D,CAAC;AAED,QAAM,QAAQ,OAAO,OAAO,CAAC,GAAGA,OAAM,IAAIA,GAAE,QAAQ,CAAC,IAAI,KAAK,IAAI,GAAG,CAAC;AACtE,SAAO,EAAE,KAAK,QAAQ,KAAK,MAAM;AACnC;AAEO,SAAS,UAAU,OAAkB,aAAuB,KAAK,KAAK,IAAI,MAA0B;AACzG,QAAM,SAA6B,MAAM,IAAI,IAAI,CAAC,IAAI,MAAM;AAC1D,UAAM,MAAM,MAAM,OAAO,CAAC;AAC1B,UAAM,KAAK,IAAI;AACf,UAAM,KAAK,oBAAI,IAAoB;AACnC,eAAWA,MAAK,IAAK,IAAG,IAAIA,KAAI,GAAG,IAAIA,EAAC,KAAK,KAAK,CAAC;AAEnD,QAAI,QAAQ;AACZ,eAAWA,MAAK,aAAa;AAC3B,YAAM,IAAI,GAAG,IAAIA,EAAC,KAAK;AACvB,UAAI,MAAM,EAAG;AACb,YAAM,SAAS,MAAM,IAAI,IAAIA,EAAC,KAAK;AACnC,eAAS,UAAW,KAAK,KAAK,MAAO,IAAI,MAAM,IAAI,IAAI,KAAK,KAAK,MAAM;AAAA,IACzE;AACA,WAAO,CAAC,IAAI,KAAK;AAAA,EACnB,CAAC;AACD,SAAO,OAAO,KAAK,CAAC,GAAGC,OAAMA,GAAE,CAAC,IAAI,EAAE,CAAC,CAAC;AAC1C;AAGO,SAAS,SAAS,QAAkB,SAAmB,IAAI,IAAwB;AACxF,QAAM,SAAS,oBAAI,IAAoB;AACvC,SAAO,QAAQ,CAAC,IAAI,SAAS,OAAO,IAAI,KAAK,OAAO,IAAI,EAAE,KAAK,KAAK,KAAK,IAAI,OAAO,EAAE,CAAC;AACvF,UAAQ,QAAQ,CAAC,IAAI,SAAS,OAAO,IAAI,KAAK,OAAO,IAAI,EAAE,KAAK,KAAK,KAAK,IAAI,OAAO,EAAE,CAAC;AACxF,SAAO,MAAM,KAAK,OAAO,QAAQ,CAAC,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC;AAChE;AAGO,SAAS,eAAe,MAA6E;AAC1G,MAAI,OAAO,KAAK;AAChB,MAAI,KAAK,SAAS,OAAQ,SAAQ,MAAM,KAAK,SAAS,KAAK,GAAG;AAC9D,MAAI,KAAK,KAAK,OAAQ,SAAQ,MAAM,KAAK,KAAK,KAAK,GAAG;AACtD,MAAI,KAAK,QAAS,SAAQ,MAAM,KAAK;AACrC,SAAO;AACT;AAGA,IAAM,oBAAoB,CAAC,gBAAM,sBAAO,gBAAM,0BAAM;AAQ7C,SAAS,aAAa,SAAqC;AAChE,QAAM,UAAU,QAAQ,KAAK;AAC7B,MAAI,QAAQ,SAAS,IAAI;AACvB,WAAO,EAAE,IAAI,OAAO,WAAW,OAAO,QAAQ,iCAAQ,QAAQ,MAAM,4CAAc;AAAA,EACpF;AACA,QAAM,YAAY,kBAAkB,KAAK,CAAC,MAAM,QAAQ,SAAS,CAAC,CAAC;AACnE,SAAO,EAAE,IAAI,MAAM,UAAU;AAC/B;AAIA,SAAS,aAA6B;AACpC,SAAO,qBAAqB;AAC9B;AAGA,eAAsB,UACpB,SACA,UAAU,QACV,MASiB;AACjB,QAAM,QAAQ,MAAM,SAAS;AAC7B,QAAM,WAAW,MAAM,YAAY,CAAC;AACpC,QAAM,MAAM,MAAM,cAAc,WAAW;AAG3C,QAAM,UAAU,aAAa,OAAO;AACpC,MAAI,CAAC,QAAQ,IAAI;AACf,UAAM,IAAI,MAAM,uCAAmB,QAAQ,MAAM,EAAE;AAAA,EACrD;AAIA,QAAM,uBAAuB,UAAU,WAAW,WAAW;AAG7D,QAAM,OAAO,WAAW,KAAK,EAAE,OAAO,OAAO,EAAE,OAAO,KAAK;AAC3D,QAAM,iBAAiB,MAAM,IAAI,WAAW,MAAM,OAAO;AACzD,MAAI,gBAAgB;AAClB,YAAQ,IAAI,+CAA+C,eAAe,GAAG,MAAM,GAAG,CAAC,CAAC,GAAG;AAC3F,WAAO,eAAe;AAAA,EACxB;AAEA,UAAQ,IAAI,4BAA4B;AAGxC,QAAM,EAAE,UAAU,MAAM,SAAS,UAAU,WAAW,OAAO,IAAI,MAAM,iBAAiB,OAAO;AAC/F,UAAQ,IAAI,eAAe,SAAS,KAAK,IAAI,CAAC,EAAE;AAChD,UAAQ,IAAI,WAAW,KAAK,KAAK,IAAI,CAAC,EAAE;AACxC,UAAQ,IAAI,cAAc,OAAO,EAAE;AACnC,UAAQ,IAAI,eAAe,QAAQ,EAAE;AACrC,UAAQ,IAAI,gBAAgB,SAAS,EAAE;AACvC,UAAQ,IAAI,aAAa,OAAO,KAAK,IAAI,CAAC,EAAE;AAE5C,QAAM,aAAa,eAAe,EAAE,SAAS,UAAU,MAAM,QAAQ,CAAC;AACtE,QAAM,YAAY,MAAM,OAAO,UAAU;AAGzC,QAAM,WAAW,MAAM,IAAI,iBAAiB,WAAW,GAAG,SAAS,CAAG;AACtE,MAAI,SAAS,SAAS,KAAK,SAAS,CAAC,EAAE,SAAS,MAAM;AAIpD,QAAI,SAAS,SAAS,CAAC,EAAE,MAAM,OAAO,GAAG;AACvC,cAAQ,IAAI,oCAAoC,SAAS,CAAC,EAAE,MAAM,QAAQ,CAAC,CAAC,sBAAsB;AAClG,YAAM,IAAI,kBAAkB,SAAS,CAAC,EAAE,KAAK,IAAI,SAAS,WAAW,IAAI;AACzE,aAAO,SAAS,CAAC,EAAE,KAAK;AAAA,IAC1B;AACA,YAAQ;AAAA,MACN,+BAA+B,SAAS,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,CAAC,CAAC,uBAAuB,QAAQ,OAAO,CAAC;AAAA,IACvG;AAAA,EACF;AAGA,QAAM,eAAe,SAAS,SAAS,KAAK,SAAS,CAAC,EAAE,SAAS,QAAQ,SAAS,CAAC,EAAE,QAAQ;AAC7F,MAAI,cAAc;AAChB,YAAQ,IAAI,oCAAoC,SAAS,CAAC,EAAE,MAAM,QAAQ,CAAC,CAAC,+BAA+B;AAAA,EAC7G;AAGA,QAAM,UAAoB,UAAU,WAAW,CAAC,GAAG,IAAI,CAAC,OAAO;AAC/D,QAAM,UAAoB,CAAC,OAAO;AAElC,QAAM,OAAmB;AAAA,IACvB,IAAI,OAAO;AAAA,IACX;AAAA,IACA;AAAA,IACA,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,IAClC;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,OAAO,CAAC;AAAA,IACR,UAAU;AAAA,IACV;AAAA;AAAA,IAEA,iBAAiB;AAAA,IACjB,gBAAe,oBAAI,KAAK,GAAE,YAAY;AAAA;AAAA,IAEtC,mBAAmB,CAAC;AAAA;AAAA,IAEpB;AAAA,IACA,WAAW;AAAA;AAAA,IAEX;AAAA;AAAA,IAEA;AAAA;AAAA,IAEA,eAAe;AAAA;AAAA,IAEf,UAAU;AAAA;AAAA,IAEV,WAAW,QAAQ;AAAA,IACnB,aAAa;AAAA;AAAA,IAEb,OAAO;AAAA,IACP;AAAA,IACA;AAAA,EACF;AAGA,QAAM,IAAI,QAAQ,IAAI;AACtB,UAAQ,IAAI,gBAAgB,KAAK,EAAE,EAAE;AAGrC,MAAI;AACF,UAAM,QAAQ,MAAM,IAAI,WAAW,OAAO;AAC1C,QAAI,QAAQ,GAAG;AACb,YAAM,aAAa,MAAM,IAAI,iBAAiB,WAAW,GAAG,SAAS,CAAG;AAExE,YAAM,YAAsB,CAAC;AAC7B,YAAM,iBAA2B,CAAC;AAElC,iBAAW,EAAE,MAAM,MAAM,MAAM,KAAK,YAAY;AAC9C,YAAI,KAAK,OAAO,KAAK,GAAI;AACzB,YAAI,QAAQ,IAAK;AAEjB,gBAAQ,IAAI,eAAe,KAAK,GAAG,MAAM,GAAG,CAAC,CAAC,WAAW,MAAM,QAAQ,CAAC,CAAC,iBAAiB;AAC1F,cAAM,aAAa,MAAM,cAAc,SAAS,KAAK,OAAO;AAC5D,YAAI,YAAY;AACd,oBAAU,KAAK,KAAK,EAAE;AACtB,yBAAe,KAAK,KAAK,OAAO;AAChC,kBAAQ,IAAI,oBAAe;AAAA,QAC7B;AAAA,MACF;AAEA,UAAI,UAAU,SAAS,GAAG;AACxB,aAAK,QAAQ;AACb,cAAM,IAAI,WAAW,IAAI;AAKzB,mBAAW,OAAO,WAAW;AAC3B,gBAAM,SAAS,MAAM,IAAI,QAAQ,GAAG;AACpC,cAAI,UAAU,CAAC,OAAO,MAAM,SAAS,KAAK,EAAE,GAAG;AAC7C,gBAAI,CAAC,SAAS,QAAQ,OAAO,GAAG;AAC9B,sBAAQ,IAAI,yBAAyB,IAAI,MAAM,GAAG,CAAC,CAAC,mCAA8B,QAAQ,OAAO,CAAC,EAAE;AACpG;AAAA,YACF;AACA,mBAAO,MAAM,KAAK,KAAK,EAAE;AACzB,kBAAM,IAAI,WAAW,MAAM;AAAA,UAC7B;AAAA,QACF;AAGA,YAAI,mBAAmB,GAAG;AACxB,kBAAQ,IAAI,oDAAoD,KAAK,IAAI,UAAU,QAAQ,CAAC,CAAC,eAAe;AAC5G,qBAAW,OAAO,UAAU,MAAM,GAAG,CAAC,GAAG;AACvC,kBAAM,SAAS,MAAM,IAAI,QAAQ,GAAG;AACpC,gBAAI,CAAC,OAAQ;AAIb,gBAAI,CAAC,SAAS,QAAQ,OAAO,GAAG;AAC9B,sBAAQ,IAAI,oBAAoB,IAAI,MAAM,GAAG,CAAC,CAAC,2BAAsB,QAAQ,OAAO,CAAC,EAAE;AACvF;AAAA,YACF;AAMA,kBAAM,cAAsD,CAAC;AAC7D,uBAAW,QAAQ,OAAO,MAAM,MAAM,GAAG,CAAC,GAAG;AAC3C,kBAAI,SAAS,KAAK,GAAI;AACtB,oBAAM,KAAK,MAAM,IAAI,QAAQ,MAAM,OAAO;AAC1C,kBAAI,GAAI,aAAY,KAAK,EAAE,IAAI,GAAG,IAAI,SAAS,GAAG,QAAQ,CAAC;AAAA,YAC7D;AACA,wBAAY,KAAK,EAAE,IAAI,KAAK,IAAI,QAAQ,CAAC;AAEzC,kBAAM,UAAU,CAAC,GAAG,OAAO,IAAI;AAC/B,kBAAM,aAAa,OAAO;AAE1B,kBAAM;AAAA,cACJ,MAAM;AAAA,cACN,SAAS;AAAA,cACT;AAAA,cACA;AAAA,cACA;AAAA,YACF,IAAI,MAAM,cAAc,OAAO,SAAS,WAAW;AAEnD,gBAAI,UAAU;AAGd,gBAAI,YAAY,QAAQ,eAAe,MAAM;AAC3C,kBAAI,YAAY,KAAM,QAAO,OAAO;AACpC,kBAAI,eAAe,KAAM,QAAO,UAAU;AAE1C,qBAAO,oBAAoB,OAAO,qBAAqB,CAAC;AACxD,qBAAO,kBAAkB,KAAK;AAAA,gBAC5B,aAAa,KAAK;AAAA,gBAClB,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,gBACpC;AAAA,gBACA,YAAY,cAAc;AAAA,gBAC1B;AAAA,gBACA,SAAS,WAAW;AAAA,gBACpB,QAAQ;AAAA,cACV,CAAC;AACD,wBAAU;AAAA,YACZ;AAEA,gBAAI,cAAc;AAClB,gBAAI,kBAAkB;AAGtB,gBAAI,oBAAoB,qBAAqB,SAAS,GAAG;AAEvD,yBAAW,YAAY,sBAAsB;AAC3C,oBAAI,CAAC,KAAK,MAAM,SAAS,QAAQ,GAAG;AAClC,uBAAK,MAAM,KAAK,QAAQ;AACxB,gCAAc;AAAA,gBAChB;AAGA,sBAAM,SAAS,MAAM,IAAI,QAAQ,UAAU,OAAO;AAClD,oBAAI,UAAU,CAAC,OAAO,MAAM,SAAS,KAAK,EAAE,GAAG;AAG7C,sBAAI,SAAS,QAAQ,OAAO,GAAG;AAC7B,2BAAO,MAAM,KAAK,KAAK,EAAE;AACzB,0BAAM,IAAI,WAAW,MAAM;AAAA,kBAC7B,OAAO;AACL,4BAAQ,IAAI,qCAAqC,SAAS,MAAM,GAAG,CAAC,CAAC,8BAAyB;AAAA,kBAChG;AAAA,gBACF;AAAA,cACF;AAEA,kBAAI,aAAa,SAAS,GAAG;AAC3B,qBAAK,OAAO;AACZ,8BAAc;AACd,kCAAkB;AAAA,cACpB;AAGA,qBAAO,oBAAoB,OAAO,qBAAqB,CAAC;AACxD,qBAAO,kBAAkB,KAAK;AAAA,gBAC5B,aAAa,KAAK;AAAA,gBAClB,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,gBACpC,YAAY,OAAO;AAAA,gBACnB,YAAY,OAAO;AAAA,gBACnB,SAAS,CAAC,GAAG,OAAO,IAAI;AAAA,gBACxB,SAAS,CAAC,GAAG,OAAO,IAAI;AAAA,gBACxB,QAAQ;AAAA,gBACR;AAAA,gBACA,aAAa;AAAA,cACf,CAAC;AACD,wBAAU;AAAA,YACZ;AAEA,gBAAI,aAAa;AACf,kBAAI,iBAAiB;AACnB,qBAAK,YAAY,MAAM,OAAO,eAAe,IAAI,CAAC;AAAA,cACpD;AACA,oBAAM,IAAI,WAAW,IAAI;AAAA,YAC3B;AAEA,gBAAI,SAAS;AAEX,kBAAI,YAAY,QAAQ,eAAe,MAAM;AAC3C,uBAAO,YAAY,MAAM,OAAO,eAAe,MAAM,CAAC;AAAA,cACxD;AACA,oBAAM,IAAI,WAAW,MAAM;AAC3B,sBAAQ,IAAI,+BAA+B,IAAI,MAAM,GAAG,CAAC,CAAC,KAAK;AAAA,YACjE;AAAA,UACF;AAAA,QACF,OAAO;AACL,kBAAQ,IAAI,8DAA8D;AAAA,QAC5E;AAAA,MACF;AAAA,IACF;AAAA,EACF,SAAS,GAAG;AACV,YAAQ,MAAM,uCAAwC,EAAY,OAAO,EAAE;AAAA,EAC7E;AAEA,UAAQ,IAAI,sBAAsB,KAAK,EAAE,EAAE;AAC3C,SAAO,KAAK;AACd;AAgBA,eAAsB,YACpB,SACA,UAAU,QACV,MAMiB;AACjB,QAAM,QAAQ,MAAM,SAAS;AAC7B,QAAM,WAAW,MAAM,YAAY,CAAC;AACpC,QAAM,MAAM,MAAM,cAAc,WAAW;AAE3C,QAAM,UAAU,aAAa,OAAO;AACpC,MAAI,CAAC,QAAQ,IAAI;AACf,UAAM,IAAI,MAAM,uCAAmB,QAAQ,MAAM,EAAE;AAAA,EACrD;AAIA,QAAM,YAAY,MAAM,OAAO,OAAO;AACtC,QAAM,OAAM,oBAAI,KAAK,GAAE,YAAY;AAEnC,QAAM,OAAmB;AAAA,IACvB,IAAI,OAAO;AAAA,IACX;AAAA,IACA;AAAA,IACA,WAAW;AAAA,IACX,UAAU,CAAC;AAAA,IACX,MAAM,CAAC;AAAA,IACP,SAAS;AAAA,IACT;AAAA,IACA,OAAO,CAAC;AAAA,IACR,UAAU,UAAU,WAAW,WAAW;AAAA,IAC1C,MAAM,WAAW,KAAK,EAAE,OAAO,OAAO,EAAE,OAAO,KAAK;AAAA,IACpD,iBAAiB;AAAA,IACjB,eAAe;AAAA,IACf,mBAAmB,CAAC;AAAA,IACpB,UAAU;AAAA,IACV,WAAW;AAAA,IACX,WAAW;AAAA,IACX,QAAQ,CAAC;AAAA,IACT,eAAe;AAAA,IACf,UAAU;AAAA,IACV,WAAW,QAAQ;AAAA,IACnB,aAAa;AAAA,IACb,OAAO;AAAA,IACP,SAAS,UAAU,WAAW,CAAC,GAAG,IAAI,CAAC,OAAO;AAAA,IAC9C,SAAS,CAAC,OAAO;AAAA,EACnB;AAEA,QAAM,IAAI,QAAQ,IAAI;AACtB,SAAO,KAAK;AACd;AAkBA,eAAsB,aACpB,OACA,OAAO,GACP,UAAU,QACV,MAgByB;AACzB,QAAM,SAAS,MAAM,WAAW;AAChC,QAAM,UAAU,MAAM;AACtB,QAAM,kBAAkB,MAAM,mBAAmB;AACjD,QAAM,MAAM,MAAM,cAAc,WAAW;AAC3C,QAAM,QAAQ,MAAM,IAAI,WAAW,OAAO;AAC1C,MAAI,UAAU,EAAG,QAAO,CAAC;AAGzB,QAAM,iBAAiB,MAAM,OAAO,KAAK;AACzC,QAAM,IAAI,KAAK,IAAI,KAAK,IAAI,OAAO,GAAG,EAAE,GAAG,KAAK;AAChD,QAAM,aAAa,MAAM,IAAI,iBAAiB,gBAAgB,GAAG,SAAS,GAAK,OAAO;AAMtF,QAAM,WAAW,MAAM,IAAI,UAAU,SAAS,OAAO;AACrD,QAAM,YAAY,UAAU,QAAQ;AACpC,QAAM,cAAc,eAAe,KAAK;AACxC,QAAM,aAAa,UAAU,WAAW,WAAW,EAAE,MAAM,GAAG,CAAC;AAG/D,QAAM,SAAS;AAAA,IACb,WAAW,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE;AAAA,IAC/B,WAAW,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;AAAA,EAC5B;AAMA,QAAM,MAAM,KAAK,IAAI;AACrB,QAAM,UAAU,IAAI,IAAI,SAAS,IAAI,CAACC,OAAM,CAACA,GAAE,IAAIA,EAAC,CAAC,CAAC;AACtD,QAAM,gBAAoC,OAAO,IAAI,CAAC,CAAC,IAAI,QAAQ,MAAM;AACvE,UAAM,OAAO,QAAQ,IAAI,EAAE;AAC3B,QAAI,CAAC,KAAM,QAAO,CAAC,IAAI,QAAQ;AAE/B,QAAI,KAAK,cAAc,YAAa,QAAO,CAAC,IAAI,QAAQ;AACxD,UAAM,eAAe,IAAI,KAAK,KAAK,iBAAiB,KAAK,SAAS,EAAE,QAAQ;AAC5E,UAAM,WAAW,MAAM,gBAAgB;AACvC,UAAM,eAAe,IAAK,OAAO,KAAK,IAAI,KAAK,KAAK,mBAAmB,EAAE,KAAM,UAAU;AACzF,WAAO,CAAC,IAAI,WAAW,YAAY;AAAA,EACrC,CAAC;AACD,gBAAc,KAAK,CAAC,GAAG,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC;AAExC,QAAM,SAAS,cAAc,MAAM,GAAG,IAAI,EAAE,IAAI,CAAC,CAAC,EAAE,MAAM,EAAE;AAK5D,QAAM,eAAe;AACrB,QAAM,iBAAiB;AACvB,QAAM,aAAa,IAAI,IAAY,MAAM;AACzC,QAAM,WAA+C,SAAS,OAAO,IAAI,CAAC,QAAQ,EAAE,IAAI,KAAK,EAAE,EAAE,IAAI,CAAC;AACtG,QAAM,WAAqB,CAAC;AAE5B,SAAO,SAAS,SAAS,KAAK,SAAS,SAAS,gBAAgB;AAC9D,UAAM,OAAO,SAAS,MAAM;AAC5B,QAAI,KAAK,OAAO,aAAc;AAC9B,UAAM,OAAO,QAAQ,IAAI,KAAK,EAAE;AAChC,QAAI,CAAC,KAAM;AACX,eAAW,YAAY,KAAK,OAAO;AACjC,UAAI,WAAW,IAAI,QAAQ,EAAG;AAC9B,iBAAW,IAAI,QAAQ;AAEvB,YAAM,SAAS,QAAQ,IAAI,QAAQ;AACnC,UAAI,CAAC,UAAU,OAAO,cAAc,MAAO;AAE3C,UAAI,kBAAkB,KAAK,OAAO,WAAW;AAC3C,cAAM,MAAM,iBAAiB,gBAAgB,OAAO,SAAS;AAC7D,YAAI,MAAM,gBAAiB;AAAA,MAC7B;AACA,eAAS,KAAK,QAAQ;AACtB,eAAS,KAAK,EAAE,IAAI,UAAU,KAAK,KAAK,MAAM,EAAE,CAAC;AACjD,UAAI,SAAS,UAAU,eAAgB;AAAA,IACzC;AAAA,EACF;AAGA,QAAM,eAAe,MAAM;AAC3B,QAAM,iBACJ,gBAAgB,aAAa,SAAS,IAClC,OAAO,OAAO,CAAC,OAAO;AACpB,UAAM,OAAO,QAAQ,IAAI,EAAE;AAC3B,QAAI,CAAC,KAAM,QAAO;AAClB,QAAI,KAAK,cAAc,YAAa,QAAO;AAC3C,WAAO,aAAa,MAAM,CAACF,OAAM,KAAK,OAAO,IAAI,CAAC,MAAM,EAAE,YAAY,CAAC,EAAE,SAASA,GAAE,YAAY,CAAC,CAAC;AAAA,EACpG,CAAC,IACD;AAGN,QAAM,YAAY,IAAI,IAAI,WAAW,IAAI,CAAC,MAAM,CAAC,EAAE,KAAK,IAAI,EAAE,KAAK,CAAC,CAAC;AACrE,QAAM,SAAS,IAAI,IAAI,cAAc,IAAI,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,CAAC;AAEtE,QAAM,UAA0B,CAAC;AACjC,aAAW,MAAM,CAAC,GAAG,gBAAgB,GAAG,QAAQ,GAAG;AACjD,UAAM,OAAO,QAAQ,IAAI,EAAE;AAC3B,QAAI,CAAC,KAAM;AACX,YAAQ,KAAK;AAAA,MACX,IAAI,KAAK;AAAA,MACT,SAAS,KAAK;AAAA,MACd,SAAS,KAAK;AAAA,MACd,MAAM,KAAK;AAAA,MACX,UAAU,KAAK;AAAA,MACf,OAAO,KAAK;AAAA,MACZ,WAAW,KAAK;AAAA,MAChB,YAAY,UAAU,IAAI,EAAE,KAAK;AAAA,MACjC,KAAK,OAAO,IAAI,EAAE,KAAK;AAAA,MACvB,QAAQ,KAAK,UAAU,CAAC;AAAA,MACxB,WAAW,KAAK,aAAa;AAAA,IAC/B,CAAC;AAAA,EACH;AAEA,SAAO;AACT;AAGA,eAAsB,aAAa,UAAU,QAAQ,YAAyD;AAC5G,QAAM,MAAM,cAAc,WAAW;AACrC,QAAM,QAAQ,MAAM,IAAI,WAAW,OAAO;AAC1C,SAAO,EAAE,MAAM;AACjB;AAKA,SAAS,MAAM,IAA2B;AACxC,SAAO,IAAI,QAAQ,CAACG,aAAY,WAAWA,UAAS,EAAE,CAAC;AACzD;AAWA,eAAsB,kBAAkB,SAAiB,YAA8C;AACrG,QAAM,MAAM,cAAc,WAAW;AACrC,QAAM,SAAQ,oBAAI,KAAK,GAAE,YAAY,EAAE,MAAM,GAAG,EAAE;AAClD,QAAM,WAAW,MAAM,IAAI,qBAAqB,OAAO,OAAO;AAG9D,QAAM,QAAQ,SAAS,OAAO,CAAC,MAAM,EAAE,aAAa,QAAQ;AAG5D,QAAM,eAAe,MAAM,OAAO,CAAC,MAAM,EAAE,kBAAkB,IAAI;AACjE,MAAI,eAAe;AAEnB,aAAW,eAAe,cAAc;AAEtC,QAAI,UAAU;AACd,QAAI,eAAkC;AACtC,eAAW,SAAS,OAAO;AACzB,UAAI,MAAM,OAAO,YAAY,GAAI;AACjC,UAAI,MAAM,cAAe;AACzB,UAAI,CAAC,MAAM,UAAU,UAAU,CAAC,YAAY,UAAU,OAAQ;AAC9D,YAAM,MAAM,iBAAiB,YAAY,WAAW,MAAM,SAAS;AACnE,UAAI,MAAM,SAAS;AACjB,kBAAU;AACV,uBAAe;AAAA,MACjB;AAAA,IACF;AAEA,QAAI,CAAC,cAAc;AACjB,YAAM,IAAI,iBAAiB,YAAY,IAAI,EAAE,eAAe,MAAM,CAAC;AACnE;AAAA,IACF;AAEA,UAAM,WAAW,MAAM,kBAAkB,aAAa,SAAS,YAAY,OAAO;AAClF,YAAQ;AAAA,MACN,+BAA+B,YAAY,GAAG,MAAM,GAAG,CAAC,CAAC,WAAM,aAAa,GAAG,MAAM,GAAG,CAAC,CAAC,KAAK,SAAS,IAAI;AAAA,IAC9G;AAEA,QAAI,SAAS,SAAS,UAAU;AAC9B,YAAM,aAAa,aAAa,qBAAqB,CAAC;AACtD,iBAAW,KAAK;AAAA,QACd,aAAa,YAAY;AAAA,QACzB,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,QACpC,YAAY,aAAa;AAAA,QACzB,YAAY,aAAa;AAAA,QACzB,SAAS,CAAC,GAAG,aAAa,IAAI;AAAA,QAC9B,SAAS,CAAC,GAAG,aAAa,IAAI;AAAA,QAC9B,QAAQ;AAAA,MACV,CAAC;AACD,YAAM,gBAAgB,SAAS,iBAAiB,YAAY;AAC5D,YAAM,eAAe,MAAM,OAAO,eAAe,EAAE,GAAG,cAAc,SAAS,cAAc,CAAC,CAAC;AAC7F,YAAM,UAAU,WAAW,KAAK,EAAE,OAAO,aAAa,EAAE,OAAO,KAAK;AACpE,YAAM,IAAI,kBAAkB,aAAa,IAAI,eAAe,cAAc,OAAO;AACjF,YAAM,IAAI,iBAAiB,aAAa,IAAI;AAAA,QAC1C,mBAAmB,KAAK,UAAU,UAAU;AAAA,QAC5C,gBAAgB;AAAA,MAClB,CAAC;AACD,YAAM,IAAI,WAAW,YAAY,EAAE;AACnC;AAAA,IACF,WAAW,SAAS,SAAS,YAAY;AACvC,YAAM,IAAI,iBAAiB,YAAY,IAAI,EAAE,eAAe,OAAO,UAAU,MAAM,gBAAgB,WAAW,CAAC;AAC/G,YAAM,IAAI,iBAAiB,aAAa,IAAI,EAAE,UAAU,MAAM,gBAAgB,WAAW,CAAC;AAAA,IAC5F,WAAW,SAAS,SAAS,UAAU;AACrC,YAAM,aAAa,aAAa,qBAAqB,CAAC;AACtD,iBAAW,KAAK;AAAA,QACd,aAAa,YAAY;AAAA,QACzB,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,QACpC,YAAY,aAAa;AAAA,QACzB,YAAY,aAAa;AAAA,QACzB,SAAS,CAAC,GAAG,aAAa,IAAI;AAAA,QAC9B,SAAS,CAAC,GAAG,aAAa,IAAI;AAAA,QAC9B,QAAQ;AAAA,MACV,CAAC;AACD,YAAM,gBAAgB,SAAS,iBAAiB,GAAG,aAAa,OAAO,SAAI,YAAY,OAAO;AAC9F,YAAM,eAAe,MAAM,OAAO,eAAe,EAAE,GAAG,cAAc,SAAS,cAAc,CAAC,CAAC;AAC7F,YAAM,UAAU,WAAW,KAAK,EAAE,OAAO,aAAa,EAAE,OAAO,KAAK;AACpE,YAAM,IAAI,kBAAkB,aAAa,IAAI,eAAe,cAAc,OAAO;AACjF,YAAM,IAAI,iBAAiB,aAAa,IAAI;AAAA,QAC1C,mBAAmB,KAAK,UAAU,UAAU;AAAA,QAC5C,gBAAgB;AAAA,MAClB,CAAC;AACD,YAAM,IAAI,WAAW,YAAY,EAAE;AACnC;AAAA,IACF,OAAO;AAEL,YAAM,IAAI,iBAAiB,YAAY,IAAI,EAAE,eAAe,OAAO,gBAAgB,MAAM,CAAC;AAAA,IAC5F;AAEA,UAAM,MAAM,GAAG;AAAA,EACjB;AAIA,MAAI,MAAM,SAAS,EAAG,QAAO;AAI7B,QAAM,aAAa,IAAI,IAAI,aAAa,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;AAQxD,QAAM,QAAmB,CAAC;AAC1B,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,QAAI,WAAW,IAAI,MAAM,CAAC,EAAE,EAAE,EAAG;AACjC,aAAS,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACzC,UAAI,WAAW,IAAI,MAAM,CAAC,EAAE,EAAE,EAAG;AACjC,UAAI,CAAC,MAAM,CAAC,EAAE,UAAU,UAAU,CAAC,MAAM,CAAC,EAAE,UAAU,OAAQ;AAC9D,YAAM,MAAM,iBAAiB,MAAM,CAAC,EAAE,WAAW,MAAM,CAAC,EAAE,SAAS;AACnE,UAAI,OAAO,KAAK;AACd,cAAM,KAAK,EAAE,GAAG,GAAG,IAAI,CAAC;AAAA,MAC1B;AAAA,IACF;AAAA,EACF;AAEA,MAAI,MAAM,WAAW,EAAG,QAAO;AAG/B,QAAM,KAAK,CAAC,GAAG,MAAM,EAAE,MAAM,EAAE,GAAG;AAClC,QAAM,WAAW,MAAM,MAAM,GAAG,EAAE;AAGlC,QAAM,aAAa,oBAAI,IAAY;AACnC,MAAI,cAAc;AAElB,aAAW,EAAE,GAAG,EAAE,KAAK,UAAU;AAC/B,UAAM,QAAQ,MAAM,CAAC;AACrB,UAAM,QAAQ,MAAM,CAAC;AAGrB,QAAI,WAAW,IAAI,MAAM,EAAE,KAAK,WAAW,IAAI,MAAM,EAAE,EAAG;AAE1D,UAAM,SAAS,MAAM,eAAe,MAAM,SAAS,MAAM,OAAO;AAEhE,QAAI,OAAO,eAAe,OAAO,QAAQ;AAEvC,YAAM,CAAC,UAAU,QAAQ,IAAI,MAAM,QAAQ,UAAU,MAAM,QAAQ,SAAS,CAAC,OAAO,KAAK,IAAI,CAAC,OAAO,KAAK;AAE1G,YAAM,eAAe,MAAM,OAAO,OAAO,MAAM;AAC/C,YAAM,UAAU,WAAW,KAAK,EAAE,OAAO,OAAO,MAAM,EAAE,OAAO,KAAK;AACpE,YAAM,IAAI,kBAAkB,SAAS,IAAI,OAAO,QAAQ,cAAc,OAAO;AAC7E,YAAM,IAAI,WAAW,SAAS,EAAE;AAChC,iBAAW,IAAI,SAAS,EAAE;AAC1B;AAAA,IACF;AAGA,UAAM,MAAM,GAAG;AAAA,EACjB;AAEA,SAAO,eAAe;AACxB;AAOA,eAAsB,oBAAoB,SAAiB,QAAc,YAA8C;AACrH,QAAM,MAAM,cAAc,WAAW;AACrC,QAAM,MAAM;AAAA,IACV,MAAM,CAAC,QAAiB,SAAS,OAAO,KAAK,GAAG,IAAI,QAAQ,IAAI,GAAG;AAAA,IACnE,MAAM,CAAC,QAAiB,SAAS,OAAO,KAAK,GAAG,IAAI,QAAQ,KAAK,GAAG;AAAA,IACpE,OAAO,CAAC,QAAiB,SAAS,OAAO,MAAM,GAAG,IAAI,QAAQ,MAAM,GAAG;AAAA,EACzE;AAEA,MAAI,KAAK,uDAAuD,OAAO,EAAE;AAGzE,QAAM,WAAW,MAAM,IAAI,UAAU,OAAO;AAE5C,QAAM,WAAW,SAAS,OAAO,CAAC,MAAM,EAAE,aAAa,QAAQ;AAC/D,MAAI;AAAA,IACF,0BAA0B,SAAS,MAAM,0BAA0B,SAAS,SAAS,SAAS,MAAM;AAAA,EACtG;AAIA,QAAM,SAAS,oBAAI,IAA0B;AAC7C,aAAW,QAAQ,UAAU;AAC3B,QAAI,KAAK,cAAc,YAAa;AACpC,UAAM,WAAW,KAAK,YAAY;AAClC,QAAI,CAAC,OAAO,IAAI,QAAQ,GAAG;AACzB,aAAO,IAAI,UAAU,CAAC,CAAC;AAAA,IACzB;AACA,WAAO,IAAI,QAAQ,EAAG,KAAK,IAAI;AAAA,EACjC;AAQA,QAAM,aAA8B,CAAC;AAErC,aAAW,CAAC,UAAU,UAAU,KAAK,OAAO,QAAQ,GAAG;AACrD,QAAI,KAAK,6BAA6B,QAAQ,SAAS,WAAW,MAAM,SAAS;AACjF,aAAS,IAAI,GAAG,IAAI,WAAW,QAAQ,KAAK;AAC1C,eAAS,IAAI,IAAI,GAAG,IAAI,WAAW,QAAQ,KAAK;AAC9C,cAAM,QAAQ,WAAW,CAAC;AAC1B,cAAM,QAAQ,WAAW,CAAC;AAC1B,YAAI,CAAC,MAAM,UAAU,UAAU,CAAC,MAAM,UAAU,OAAQ;AACxD,cAAM,MAAM,iBAAiB,MAAM,WAAW,MAAM,SAAS;AAC7D,YAAI,OAAO,MAAM;AACf,qBAAW,KAAK,EAAE,OAAO,OAAO,YAAY,IAAI,CAAC;AAAA,QACnD;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAGA,aAAW,KAAK,CAAC,GAAG,MAAM,EAAE,aAAa,EAAE,UAAU;AACrD,QAAM,WAAW,WAAW,MAAM,GAAG,EAAE;AACvC,MAAI;AAAA,IACF,yBAAyB,WAAW,MAAM,4DAA4D,SAAS,MAAM;AAAA,EACvH;AAEA,QAAM,eAAe,oBAAI,IAAY;AACrC,MAAI,cAAc;AAGlB,WAAS,eAAe,QAAgB,QAAgB,eAAuB;AAC7E,UAAM,SAAc,WAAK,WAAW,GAAG,MAAM;AAC7C,UAAM,UAAe,WAAK,QAAQ,sBAAsB;AACxD,UAAM,aAAY,oBAAI,KAAK,GAAE,YAAY;AACzC,UAAM,SAAS,IAAI,SAAS,4BAA4B,MAAM,iBAAiB,MAAM,oBAAoB,cAAc,MAAM;AAAA;AAE7H,QAAI;AACF,MAAG,cAAU,QAAQ,EAAE,WAAW,KAAK,CAAC;AACxC,MAAG,mBAAe,SAAS,QAAQ,MAAM;AAAA,IAC3C,SAAS,KAAK;AACZ,UAAI,MAAM,wCAAyC,IAAc,OAAO,EAAE;AAAA,IAC5E;AAAA,EACF;AAGA,aAAW,EAAE,OAAO,OAAO,WAAW,KAAK,UAAU;AACnD,QAAI,aAAa,IAAI,MAAM,EAAE,KAAK,aAAa,IAAI,MAAM,EAAE,GAAG;AAC5D,UAAI;AAAA,QACF,kCAAkC,MAAM,GAAG,MAAM,GAAG,CAAC,CAAC,KAAK,MAAM,GAAG,MAAM,GAAG,CAAC,CAAC;AAAA,MACjF;AACA;AAAA,IACF;AAEA,QAAI;AAAA,MACF,oCAAoC,MAAM,GAAG,MAAM,GAAG,CAAC,CAAC,KAAK,MAAM,GAAG,MAAM,GAAG,CAAC,CAAC,cAAc,WAAW,QAAQ,CAAC,CAAC;AAAA,IACtH;AACA,UAAM,gBAAgB,MAAM,eAAe,MAAM,SAAS,MAAM,OAAO;AAEvE,QAAI,cAAc,eAAe,cAAc,QAAQ;AACrD,UAAI,KAAK,2BAA2B;AAGpC,YAAM,CAAC,UAAU,QAAQ,IAAI,MAAM,QAAQ,UAAU,MAAM,QAAQ,SAAS,CAAC,OAAO,KAAK,IAAI,CAAC,OAAO,KAAK;AAE1G,UAAI;AAAA,QACF,kBAAkB,SAAS,GAAG,MAAM,GAAG,CAAC,CAAC,UAAU,SAAS,QAAQ,MAAM,gBAAgB,SAAS,GAAG,MAAM,GAAG,CAAC,CAAC,UAAU,SAAS,QAAQ,MAAM;AAAA,MACpJ;AAEA,YAAM,aAAa,SAAS;AAC5B,YAAM,UAAU,CAAC,GAAG,SAAS,IAAI;AAGjC,eAAS,UAAU,cAAc;AAIjC,eAAS,OAAO,MAAM,KAAK,oBAAI,IAAI,CAAC,GAAG,SAAS,MAAM,GAAG,SAAS,IAAI,CAAC,CAAC;AACxE,eAAS,WAAW,MAAM,KAAK,oBAAI,IAAI,CAAC,GAAG,SAAS,UAAU,GAAG,SAAS,QAAQ,CAAC,CAAC;AAGpF,eAAS,QAAQ,MAAM,KAAK,oBAAI,IAAI,CAAC,GAAG,SAAS,OAAO,GAAG,SAAS,KAAK,CAAC,CAAC,EAAE;AAAA,QAC3E,CAAC,OAAO,OAAO,SAAS,MAAM,OAAO,SAAS;AAAA,MAChD;AAGA,eAAS,mBAAmB,SAAS,mBAAmB,MAAM,SAAS,mBAAmB;AAG1F,YAAM,iBAAiB,IAAI,KAAK,SAAS,iBAAiB,SAAS,SAAS,EAAE,QAAQ;AACtF,YAAM,iBAAiB,IAAI,KAAK,SAAS,iBAAiB,SAAS,SAAS,EAAE,QAAQ;AACtF,eAAS,gBACP,kBAAkB,iBACd,SAAS,iBAAiB,SAAS,YACnC,SAAS,iBAAiB,SAAS;AAGzC,YAAM,YAAY,eAAe,QAAQ;AACzC,eAAS,YAAY,MAAM,OAAO,SAAS;AAC3C,eAAS,OAAO,WAAW,KAAK,EAAE,OAAO,SAAS,OAAO,EAAE,OAAO,KAAK;AAGvE,eAAS,oBAAoB,SAAS,qBAAqB,CAAC;AAC5D,eAAS,kBAAkB,KAAK;AAAA,QAC9B,aAAa,SAAS;AAAA,QACtB,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,QACpC;AAAA,QACA,YAAY,SAAS;AAAA,QACrB;AAAA,QACA,SAAS,SAAS;AAAA,QAClB,QAAQ;AAAA,MACV,CAAC;AAGD,YAAM,IAAI,WAAW,QAAQ;AAG7B,YAAM,IAAI,eAAe,SAAS,EAAE;AAGpC,YAAM,IAAI,sBAAsB,SAAS,IAAI,SAAS,IAAI,OAAO;AAGjE,qBAAe,SAAS,IAAI,SAAS,IAAI,SAAS,OAAO;AAEzD,mBAAa,IAAI,SAAS,EAAE;AAC5B,mBAAa,IAAI,SAAS,EAAE;AAC5B;AAAA,IACF,OAAO;AACL,UAAI,KAAK,kCAAkC;AAAA,IAC7C;AAGA,UAAM,MAAM,GAAG;AAAA,EACjB;AAEA,MAAI,KAAK,uDAAuD,WAAW,SAAS;AACpF,SAAO;AACT;AAmBA,SAAS,oBAAoB,UAAuC;AAClE,QAAM,OAAO,QAAQ,IAAI,sBAAsB,YAAY,UAAU,KAAK,EAAE,YAAY;AACxF,SAAO,QAAQ,SAAS,SAAS;AACnC;AAGA,IAAM,sBAAsB;AAuB5B,eAAsB,cACpB,SACA,MAQ8B;AAC9B,QAAM,MAAM,MAAM,cAAc,WAAW;AAC3C,QAAM,QAAQ,MAAM,UAAU;AAC9B,QAAM,OAAO,oBAAoB,MAAM,IAAI;AAC3C,QAAM,MAAM,MAAM,QAAQ,SAAS,CAAC,MAAc,QAAQ,IAAI,CAAC;AAE/D,QAAM,MAAM,MAAM,IAAI,UAAU,OAAO;AAGvC,QAAM,QAAQ,IAAI,OAAO,CAAC,MAAM,EAAE,aAAa,YAAY,EAAE,cAAc,eAAe,EAAE,cAAc,KAAK;AAE/G,QAAM,SAAS,oBAAI,IAA0B;AAC7C,aAAW,KAAK,OAAO;AACrB,UAAM,IAAI,EAAE,YAAY;AACxB,QAAI,CAAC,OAAO,IAAI,CAAC,EAAG,QAAO,IAAI,GAAG,CAAC,CAAC;AACpC,WAAO,IAAI,CAAC,EAAG,KAAK,CAAC;AAAA,EACvB;AAEA,MAAI,aAAa;AACjB,MAAI,UAAU;AAEd,MAAI,iBAAiB;AACrB,MAAI,iBAAiB;AAErB,aAAW,CAAC,UAAU,UAAU,KAAK,OAAO,QAAQ,GAAG;AAGrD,eAAW,KAAK,CAAC,GAAG,MAAM,KAAK,MAAM,EAAE,SAAS,IAAI,KAAK,MAAM,EAAE,SAAS,CAAC;AAE3E,aAAS,QAAQ,GAAG,QAAQ,WAAW,QAAQ,SAAS,qBAAqB;AAC3E,YAAM,QAAQ,WAAW,MAAM,OAAO,QAAQ,mBAAmB;AACjE,UAAI,MAAM,SAAS,EAAG;AAYtB,UAAI,CAAC,SAAS,MAAM,MAAM,CAAC,MAAM,EAAE,mBAAmB,GAAG;AACvD;AACA;AAAA,MACF;AACA;AAEA,YAAM,QAAQ,MAAM,gBAAgB,MAAM,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC;AAC/D,iBAAW,EAAE,GAAG,GAAG,QAAQ,gBAAgB,KAAK,OAAO;AACrD,cAAM,QAAQ,MAAM,CAAC;AACrB,cAAM,QAAQ,MAAM,CAAC;AACrB,YAAI,CAAC,SAAS,CAAC,MAAO;AACtB;AAIA,cAAM,IAAI,iBAAiB,MAAM,IAAI;AAAA,UACnC,UAAU;AAAA,UACV,gBAAgB;AAAA,UAChB,gBAAgB,MAAM,KAAK,oBAAI,IAAI,CAAC,GAAI,MAAM,kBAAkB,CAAC,GAAI,MAAM,EAAE,CAAC,CAAC;AAAA,UAC/E,iBAAiB;AAAA,QACnB,CAAC;AACD,cAAM,IAAI,iBAAiB,MAAM,IAAI;AAAA,UACnC,UAAU;AAAA,UACV,gBAAgB;AAAA,UAChB,gBAAgB,MAAM,KAAK,oBAAI,IAAI,CAAC,GAAI,MAAM,kBAAkB,CAAC,GAAI,MAAM,EAAE,CAAC,CAAC;AAAA,UAC/E,iBAAiB;AAAA,QACnB,CAAC;AACD,YAAI,cAAc,QAAQ,KAAK,MAAM,GAAG,MAAM,GAAG,CAAC,CAAC,WAAM,MAAM,GAAG,MAAM,GAAG,CAAC,CAAC,WAAM,MAAM,EAAE;AAE3F,YAAI,SAAS,QAAQ;AAMnB,gBAAM,aAAa,oBAAoB,IAAI,QAAQ,oBAAoB,IAAI,QAAQ;AACnF,cAAI,CAAC,YAAY;AAIf,gBAAI,oFAA+E;AAAA,UACrF,OAAO;AACL,kBAAM,KAAK,MAAM,IAAI,eAAe,WAAW,IAAI,OAAO;AAC1D,gBAAI,IAAI;AACN;AACA,kBAAI,+CAA+C,WAAW,GAAG,MAAM,GAAG,CAAC,CAAC,EAAE;AAAA,YAChF;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAIA,YAAM,aAAY,oBAAI,KAAK,GAAE,YAAY;AACzC,iBAAW,KAAK,OAAO;AACrB,cAAM,IAAI,iBAAiB,EAAE,IAAI,EAAE,qBAAqB,UAAU,CAAC;AAAA,MACrE;AAAA,IACF;AAAA,EACF;AAEA;AAAA,IACE,cAAc,cAAc,uBAAuB,cAAc,wBAC5D,UAAU,mBAAmB,OAAO;AAAA,EAC3C;AACA,SAAO,EAAE,SAAS,MAAM,QAAQ,YAAY,SAAS,gBAAgB,eAAe;AACtF;;;AM3pCA,YAAYC,SAAQ;AACpB,YAAYC,WAAU;AAKtB,IAAMC,UAAwB,QAAQ,IAAI,uBAAwC,OAAO,OAAO;AAahG,eAAsB,eAAe,SAA4C;AAC/E,QAAM,QAAQ,MAAM,UAAU,OAAO;AACrC,QAAM,MAAM,KAAK,IAAI;AACrB,QAAM,gBAAgB,IAAI,KAAK,KAAK,KAAK;AACzC,QAAM,UAA4B,CAAC;AAEnC,aAAW,QAAQ,OAAO;AAIxB,QAAI,CAAC,SAAS,MAAM,OAAO,EAAG;AAE9B,UAAM,UAA8B,CAAC;AAErC,QAAI,KAAK,QAAQ,KAAK,EAAE,SAAS,IAAI;AACnC,cAAQ,KAAK,WAAW;AAAA,IAC1B;AAEA,QAAI,KAAK,cAAc,MAAM;AAC3B,YAAM,YAAY,IAAI,KAAK,KAAK,SAAS,EAAE,QAAQ;AACnD,UAAI,MAAM,YAAY,eAAe;AACnC,gBAAQ,KAAK,mBAAmB;AAAA,MAClC;AAAA,IACF;AAEA,QAAI,KAAK,aAAa,MAAM;AAC1B,cAAQ,KAAK,kBAAkB;AAAA,IACjC;AAEA,QAAI,QAAQ,SAAS,GAAG;AACtB,UAAI,CAAC,KAAK,aAAa;AACrB,cAAM,iBAAiB,KAAK,IAAI,EAAE,aAAa,KAAK,CAAC;AAAA,MACvD;AACA,cAAQ,KAAK,EAAE,MAAM,QAAQ,CAAC;AAAA,IAChC;AAAA,EACF;AAEA,SAAO;AACT;AAIA,IAAM,qBAAqB,QAAQ,IAAI,mBAAmB,QAAQ,IAAI;AAEtE,SAAS,gBAAgB,KAAqB;AAC5C,MAAI,MAAM;AACV,MAAI;AACF,UAAM,QAAW,gBAAY,GAAG;AAChC,eAAW,KAAK,OAAO;AACrB,YAAM,QAAQ,EAAE,MAAM,8BAA8B;AACpD,UAAI,OAAO;AACT,cAAM,IAAI,SAAS,MAAM,CAAC,GAAG,EAAE;AAC/B,YAAI,IAAI,IAAK,OAAM;AAAA,MACrB;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO,MAAM;AACf;AAEA,SAAS,YAAY,GAA6B;AAChD,MAAIA,YAAW,MAAM;AACnB,YAAQ,GAAG;AAAA,MACT,KAAK;AACH,eAAO;AAAA,MACT,KAAK;AACH,eAAO;AAAA,MACT,KAAK;AACH,eAAO;AAAA,IACX;AAAA,EACF;AACA,UAAQ,GAAG;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,EACX;AACF;AAEA,SAAS,cAAc,SAAqC;AAC1D,MAAI,QAAQ,SAAS,WAAW,EAAG,QAAO;AAC1C,MAAI,QAAQ,SAAS,mBAAmB,EAAG,QAAO;AAClD,SAAO;AACT;AAEA,eAAsB,oBAAoB,SAAiB,YAAsC;AAQ/F,QAAM,OAAY,cAAQ,kBAAkB;AAC5C,MAAI;AACJ,MAAI;AACJ,MAAI,YAAY;AACd,UAAM,OAAY,eAAS,UAAU;AACrC,QAAI,SAAS,cAAc,SAAS,MAAM,SAAS,OAAO,SAAS,MAAM;AACvE,YAAM,IAAI,MAAM,wGAAuC,UAAU,EAAE;AAAA,IACrE;AACA,eAAgB,WAAK,MAAM,IAAI;AAC/B,aAAS;AAAA,EACX,OAAO;AACL,aAAS,gBAAgB,IAAI;AAC7B,eAAgB,WAAK,MAAM,oBAAoB,MAAM,KAAK;AAAA,EAC5D;AAEA,QAAM,QAAQ,MAAM,eAAe,OAAO;AAE1C,QAAM,OAAM,oBAAI,KAAK,GAAE,YAAY,EAAE,MAAM,GAAG,EAAE;AAChD,QAAM,QAAkB,CAAC;AAEzB,QAAM,QAAQA,YAAW,OAAO,mCAAe;AAC/C,QAAM,WAAWA,YAAW,OAAO,6BAAS;AAC5C,QAAM,aAAaA,YAAW,OAAO,UAAK,MAAM,MAAM,0CAAY,GAAG,MAAM,MAAM;AACjF,QAAM,YACJA,YAAW,OAAO,mFAAkB;AAEtC,QAAM,KAAK,KAAK,KAAK,iBAAY,UAAU,QAAQ,EAAE;AACrD,QAAM,KAAK,EAAE;AACb,QAAM,KAAK,KAAK,QAAQ,SAAI,GAAG,MAAM,UAAU,EAAE;AACjD,QAAM,KAAK,KAAK,SAAS,EAAE;AAC3B,QAAM,KAAK,EAAE;AAEb,MAAI,MAAM,WAAW,GAAG;AACtB,UAAM,KAAKA,YAAW,OAAO,wEAAiB,oCAA+B;AAAA,EAC/E;AAQA,QAAM,OAAO,IAAI,IAAI,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,KAAK,IAAI,GAAG,IAAI,CAAC,CAAC;AAC7D,QAAM,gBAAgB,oBAAI,IAAY;AACtC,QAAM,YAAsB,CAAC;AAC7B,aAAW,EAAE,KAAK,KAAK,OAAO;AAC5B,eAAW,WAAW,KAAK,kBAAkB,CAAC,GAAG;AAC/C,YAAM,QAAQ,KAAK,IAAI,OAAO;AAC9B,UAAI,CAAC,MAAO;AACZ,YAAM,MAAM,KAAK,KAAK,UAAU,GAAG,KAAK,EAAE,IAAI,OAAO,KAAK,GAAG,OAAO,IAAI,KAAK,EAAE;AAC/E,UAAI,cAAc,IAAI,GAAG,EAAG;AAC5B,oBAAc,IAAI,GAAG;AAGrB,YAAM,CAAC,OAAO,KAAK,IAAI,KAAK,MAAM,KAAK,SAAS,KAAK,KAAK,MAAM,MAAM,SAAS,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,OAAO,IAAI;AAC/G,YAAMC,MAAKD,YAAW;AACtB,gBAAU,KAAK,iBAAUC,MAAK,iBAAO,UAAU,MAAM,MAAM,YAAY,SAAS,EAAE;AAClF,UAAI,MAAM,iBAAiB;AACzB,kBAAU,KAAK,KAAKA,MAAK,6BAAS,KAAK,YAAO,MAAM,eAAe,EAAE;AACrE,kBAAU,KAAK,EAAE;AAAA,MACnB;AACA,gBAAU,KAAK,OAAOA,MAAK,iBAAO,MAAM,MAAMA,MAAK,iBAAO,SAAS,IAAI;AACvE,gBAAU,KAAK,qBAAqB;AACpC,gBAAU,KAAK,aAAa,MAAM,UAAU,MAAM,GAAG,EAAE,CAAC,MAAM,MAAM,QAAQ,QAAQ,OAAO,GAAG,CAAC,IAAI;AACnG,gBAAU,KAAK,aAAa,MAAM,UAAU,MAAM,GAAG,EAAE,CAAC,MAAM,MAAM,QAAQ,QAAQ,OAAO,GAAG,CAAC,IAAI;AACnG,gBAAU,KAAK,EAAE;AACjB,gBAAU,KAAK,QAAQ,MAAM,EAAE,IAAI;AACnC,gBAAU,KAAK,QAAQ,MAAM,EAAE,IAAI;AACnC,gBAAU,KAAK,EAAE;AACjB,gBAAU;AAAA,QACRA,MACI,sHACA;AAAA,MACN;AACA,gBAAU,KAAKA,MAAK,4EAA0B,iDAAkC;AAChF,gBAAU,KAAKA,MAAK,2EAAuB,sDAA0C;AACrF,gBAAU,KAAK,EAAE;AACjB,gBAAU,KAAK,KAAK;AACpB,gBAAU,KAAK,EAAE;AAAA,IACnB;AAAA,EACF;AACA,MAAI,UAAU,SAAS,GAAG;AACxB,UAAM,KAAKD,YAAW,OAAO,kGAAuB,gDAA2C;AAC/F,UAAM,KAAK,EAAE;AACb,UAAM,KAAK,GAAG,SAAS;AACvB,UAAM,KAAKA,YAAW,OAAO,gCAAY,gBAAgB;AACzD,UAAM,KAAK,EAAE;AAAA,EACf;AAEA,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,UAAM,EAAE,MAAM,QAAQ,IAAI,MAAM,CAAC;AACjC,UAAM,QAAQ,cAAc,OAAO;AACnC,UAAM,YAAY,QAAQ,IAAI,WAAW,EAAE,KAAK,QAAG;AAEnD,UAAM,aAAaA,YAAW,OAAO,iBAAO;AAC5C,UAAM,eAAeA,YAAW,OAAO,iBAAO;AAC9C,UAAM,UAAUA,YAAW,OAAO,uBAAQ;AAC1C,UAAM,WAAWA,YAAW,OAAO,iBAAO;AAC1C,UAAM,YAAYA,YAAW,OAAO,iBAAO;AAC3C,UAAM,eAAeA,YAAW,OAAO,iBAAO;AAC9C,UAAM,cAAcA,YAAW,OAAO,iBAAO;AAE7C,UAAM,KAAK,QAAQ,IAAI,CAAC,KAAK,KAAK,MAAM,KAAK,YAAY,SAAS,EAAE;AACpE,UAAM,KAAK,KAAK,KAAK,EAAE,IAAI;AAC3B,UAAM,KAAK,EAAE;AACb,UAAM,KAAK,KAAK,UAAU,YAAO,SAAS,EAAE;AAC5C,UAAM,KAAK,EAAE;AACb,UAAM,KAAK,KAAK,YAAY,UAAK;AACjC,UAAM,KAAK,KAAK;AAChB,UAAM,KAAK,KAAK,OAAO;AACvB,UAAM,KAAK,KAAK;AAChB,UAAM,KAAK,EAAE;AACb,UAAM,KAAK,KAAK,OAAO,YAAO,KAAK,SAAS,KAAK,IAAI,CAAC,EAAE;AACxD,UAAM,KAAK,KAAK,QAAQ,YAAO,KAAK,KAAK,KAAK,IAAI,CAAC,EAAE;AACrD,UAAM,KAAK,EAAE;AACb,UAAM,KAAK,gBAAW,SAAS,EAAE;AACjC,UAAM,KAAK,mBAAY,YAAY,EAAE;AACrC,UAAM,KAAK,yBAAa,WAAW,EAAE;AACrC,UAAM,KAAK,EAAE;AACb,UAAM,KAAK,KAAK;AAChB,UAAM,KAAK,EAAE;AAAA,EACf;AAEA,EAAG,cAAe,cAAQ,QAAQ,GAAG,EAAE,WAAW,KAAK,CAAC;AACxD,EAAG,kBAAc,UAAU,MAAM,KAAK,IAAI,GAAG,MAAM;AAEnD,SAAO;AACT;;;ACvMA,SAAS,qBAAqB,GAAwB;AACpD,SAAO,EAAE,SAAS,WAAW,KAAK,EAAE,KAAK,WAAW;AACtD;AAEA,eAAsB,kBAAkB,MAUb;AACzB,QAAM,EAAE,MAAM,GAAG,IAAI;AACrB,QAAM,gBAAgB,KAAK,kBAAkB;AAC7C,QAAM,SAAS,KAAK,WAAW;AAC/B,QAAM,MAAM,KAAK,QAAQ,SAAS,CAAC,MAAc,QAAQ,IAAI,CAAC;AAC9D,QAAM,OAAO,KAAK,QAAQ,SAAS,CAAC,MAAc,QAAQ,KAAK,CAAC;AAEhE,MAAI,SAAS,GAAI,OAAM,IAAI,MAAM,wDAAwD,IAAI,IAAI;AAEjG,QAAM,QAAQ,kBAAkB;AAChC,QAAM,YAAY,MAAM,gBAAgB;AACxC,QAAM,YAAY,MAAM,iBAAiB,IAAI;AAC7C,MAAI,cAAc,KAAM,OAAM,IAAI,MAAM,+BAA+B,IAAI,kBAAkB;AAE7F,QAAM,SAAS,MAAM,aAAa,IAAI;AACtC,QAAM,QAAQ,OAAO,IAAI,WAAW;AACpC,QAAM,iBAAiB,MAAM,OAAO,oBAAoB,EAAE;AAE1D;AAAA,IACE,aAAa,IAAI,KAAK,SAAS,MAAM,MAAM,MAAM,kBAAa,EAAE,KAAK,SAAS,MAAM,KAAK,MACpF,cAAc;AAAA,EACrB;AAEA,MAAI,QAAQ;AACV,QAAI,wEAAmE;AACvE,WAAO,EAAE,OAAO,MAAM,QAAQ,gBAAgB,WAAW,GAAG,UAAU,GAAG,WAAW,WAAW,OAAO,QAAQ,KAAK;AAAA,EACrH;AAMA,QAAM,oBAAoB,MAAM,iBAAiB,EAAE;AACnD,MAAI,sBAAsB,MAAM;AAC9B,UAAM,oBAAoB,IAAI,SAAS;AACvC,QAAI,qBAAqB,EAAE,OAAO,SAAS,GAAG;AAAA,EAChD,OAAO;AACL,QAAI,sBAAsB,WAAW;AACnC,YAAM,IAAI,MAAM,oBAAoB,EAAE,eAAe,iBAAiB,4BAA4B,SAAS,GAAG;AAAA,IAChH;AACA,UAAM,gBAAgB,MAAM,eAAe,EAAE;AAC7C,QAAI,gBAAgB,GAAG;AACrB,YAAM,IAAI,MAAM,oBAAoB,EAAE,mBAAmB,aAAa,oCAAoC;AAAA,IAC5G;AAAA,EACF;AAEA,MAAI,YAAY;AAChB,MAAI,WAAW;AACf,QAAM,QAAQ;AACd,MAAI,SAAoF,CAAC;AAEzF,QAAM,QAAQ,YAAY;AACxB,QAAI,CAAC,OAAO,OAAQ;AACpB,UAAM,gBAAgB,IAAI,MAAM;AAChC,gBAAY,OAAO;AACnB,aAAS,CAAC;AAAA,EACZ;AAEA,aAAW,QAAQ,OAAO;AACxB,QAAI,iBAAiB,qBAAqB,IAAI,GAAG;AAC/C,UAAI;AACF,cAAM,QAAQ,MAAM,iBAAiB,KAAK,OAAO;AAIjD,YAAI,KAAK,SAAS,WAAW,EAAG,MAAK,WAAW,MAAM;AACtD,YAAI,KAAK,KAAK,WAAW,EAAG,MAAK,OAAO,MAAM;AAC9C,YAAI,CAAC,KAAK,QAAS,MAAK,UAAU,MAAM;AACxC;AAAA,MACF,SAAS,GAAG;AACV,aAAK,mCAAmC,KAAK,GAAG,MAAM,GAAG,CAAC,CAAC,0BAAsB,EAAY,OAAO,EAAE;AAAA,MACxG;AAAA,IACF;AAEA,UAAM,QAAQ,YAAY,EAAE,GAAG,MAAM,WAAW,MAAM,OAAO,eAAe,IAAI,CAAC,EAAE,CAAC;AACpF,WAAO,KAAK,KAA2E;AACvF,QAAI,OAAO,UAAU,OAAO;AAC1B,YAAM,MAAM;AACZ,UAAI,aAAa,QAAQ,IAAI,MAAM,MAAM,EAAE;AAAA,IAC7C;AAAA,EACF;AACA,QAAM,MAAM;AAEZ,QAAM,aAAa,MAAM,eAAe,EAAE;AAC1C,MAAI,eAAe,MAAM,QAAQ;AAC/B,SAAK,0BAA0B,UAAU,gCAAgC,MAAM,MAAM,gCAA2B;AAAA,EAClH;AAEA;AAAA,IACE,mBAAmB,QAAQ,cAAc,SAAS,mBAC5C,IAAI,qDAAgD,EAAE;AAAA,EAC9D;AAEA,SAAO,EAAE,OAAO,MAAM,QAAQ,gBAAgB,WAAW,UAAU,WAAW,WAAW,OAAO,QAAQ,MAAM;AAChH;;;AC9GO,IAAM,8BAA8B;AAGpC,SAAS,wBAAwB,UAA2B;AACjE,QAAM,SAAS,OAAO,QAAQ,IAAI,wBAAwB;AAC1D,MAAI,OAAO,SAAS,MAAM,KAAK,UAAU,EAAG,QAAO;AACnD,MAAI,aAAa,UAAa,OAAO,SAAS,QAAQ,KAAK,YAAY,EAAG,QAAO;AACjF,SAAO;AACT;AAWO,SAAS,wBACd,cACA,iBACA,eACS;AAGT,MAAI,CAAC,cAAc,UAAU,CAAC,iBAAiB,OAAQ,QAAO;AAC9D,MAAI,aAAa,WAAW,gBAAgB,OAAQ,QAAO;AAC3D,SAAO,iBAAiB,cAAc,eAAe,KAAK,wBAAwB,aAAa;AACjG;","names":["basename","fs","path","path","path","t","b","n","resolve","fs","path","LOCALE","zh"]}
|
|
1
|
+
{"version":3,"sources":["../src/quality.ts","../src/crud-guard.ts"],"sourcesContent":["/**\n * quality.ts — Memory quality scanning and review batch generation (Story 31)\n */\n\nimport * as fs from 'fs'\nimport * as path from 'path'\nimport { listNotes, patchNotePayload, type MemoryNote } from './storage.js'\nimport { canWrite } from './auth.js'\nimport type { PromptLocale } from './prompts.js'\n\nconst LOCALE: PromptLocale = (process.env.AMEM_PROMPT_LOCALE as PromptLocale) === 'zh' ? 'zh' : 'en'\n\n// ── Types ─────────────────────────────────────────────────────────────────────\n\nexport interface LowQualityItem {\n note: MemoryNote\n reasons: LowQualityReason[]\n}\n\nexport type LowQualityReason = 'too_short' | 'expired_ephemeral' | 'pending_conflict'\n\n// ── scanLowQuality ────────────────────────────────────────────────────────────\n\nexport async function scanLowQuality(agentId: string): Promise<LowQualityItem[]> {\n const notes = await listNotes(agentId)\n const now = Date.now()\n const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000\n const results: LowQualityItem[] = []\n\n for (const note of notes) {\n // Story 33: listNotes also returns SHARED notes owned by other agents. Quality\n // enforcement marks notes low_quality, so scan only what this agent may write —\n // flagging a note we cannot act on would be noise, and patching it a violation.\n if (!canWrite(note, agentId)) continue\n\n const reasons: LowQualityReason[] = []\n\n if (note.content.trim().length < 10) {\n reasons.push('too_short')\n }\n\n if (note.ephemeral === true) {\n const createdAt = new Date(note.timestamp).getTime()\n if (now - createdAt > SEVEN_DAYS_MS) {\n reasons.push('expired_ephemeral')\n }\n }\n\n if (note.conflict === true) {\n reasons.push('pending_conflict')\n }\n\n if (reasons.length > 0) {\n if (!note.low_quality) {\n await patchNotePayload(note.id, { low_quality: true })\n }\n results.push({ note, reasons })\n }\n }\n\n return results\n}\n\n// ── generateReviewBatch ───────────────────────────────────────────────────────\n\nconst DEFAULT_OUTPUT_DIR = process.env.AMEM_REVIEW_DIR || process.cwd()\n\nfunction nextBatchNumber(dir: string): number {\n let max = 0\n try {\n const files = fs.readdirSync(dir)\n for (const f of files) {\n const match = f.match(/^amem-review-batch(\\d+)\\.md$/)\n if (match) {\n const n = parseInt(match[1], 10)\n if (n > max) max = n\n }\n }\n } catch {\n // dir doesn't exist yet\n }\n return max + 1\n}\n\nfunction reasonLabel(r: LowQualityReason): string {\n if (LOCALE === 'zh') {\n switch (r) {\n case 'too_short':\n return '内容过短(<10字)'\n case 'expired_ephemeral':\n return '临时记忆已过期(>7天)'\n case 'pending_conflict':\n return '存在冲突标记'\n }\n }\n switch (r) {\n case 'too_short':\n return 'Content too short (<10 chars)'\n case 'expired_ephemeral':\n return 'Ephemeral memory expired (>7 days)'\n case 'pending_conflict':\n return 'Pending conflict flag'\n }\n}\n\nfunction severityBadge(reasons: LowQualityReason[]): string {\n if (reasons.includes('too_short')) return '🔴 LOW'\n if (reasons.includes('expired_ephemeral')) return '🟡 EXPIRED'\n return '🟠 CONFLICT'\n}\n\nexport async function generateReviewBatch(agentId: string, outputPath?: string): Promise<string> {\n // outputPath is a bare filename, not a path. It arrives from the\n // memory_quality_scan tool, so a prompt-injected agent could otherwise hand\n // us an absolute path or a ../ traversal and overwrite any file the process\n // can write (CodeQL js/path-injection). path.basename() strips every\n // directory component, so the write can only ever land in the review root;\n // we reject anything that carried a directory part loudly rather than\n // silently rewriting it. Operators choose the root with AMEM_REVIEW_DIR.\n const root = path.resolve(DEFAULT_OUTPUT_DIR)\n let filePath: string\n let batchN: number\n if (outputPath) {\n const name = path.basename(outputPath)\n if (name !== outputPath || name === '' || name === '.' || name === '..') {\n throw new Error(`[quality] outputPath 必须是纯文件名(不含目录): ${outputPath}`)\n }\n filePath = path.join(root, name)\n batchN = 0\n } else {\n batchN = nextBatchNumber(root)\n filePath = path.join(root, `amem-review-batch${batchN}.md`)\n }\n\n const items = await scanLowQuality(agentId)\n\n const now = new Date().toISOString().slice(0, 10)\n const lines: string[] = []\n\n const title = LOCALE === 'zh' ? 'A-MEM 质量审核' : 'A-MEM Quality Review'\n const genLabel = LOCALE === 'zh' ? '生成时间' : 'Generated'\n const countLabel = LOCALE === 'zh' ? `共 ${items.length} 条低质量条目` : `${items.length} low-quality item(s)`\n const applyHint =\n LOCALE === 'zh' ? '勾选后交给助手处理这些条目' : 'Tick your choices, then ask the assistant to act on them'\n\n lines.push(`# ${title} — Batch ${batchN || 'custom'}`)\n lines.push('')\n lines.push(`> ${genLabel}:${now} | ${countLabel}`)\n lines.push(`> ${applyHint}`)\n lines.push('')\n\n if (items.length === 0) {\n lines.push(LOCALE === 'zh' ? '✅ 没有发现低质量条目。' : '✅ No low-quality items found.')\n }\n\n // ── Story 43: conflicts render as ONE decision, not two entries ────────────\n // A contradiction involves a PAIR. Listing each note separately forces the\n // reviewer to find both, reconstruct that they belong together, then tick two\n // boxes — which is the single biggest source of review friction. Shown side by\n // side with timestamps, the reason, and a recommendation, it is one glance and\n // one tick. Each note still gets its own entry below for the apply tool.\n const byId = new Map(items.map((it) => [it.note.id, it.note]))\n const renderedPairs = new Set<string>()\n const pairLines: string[] = []\n for (const { note } of items) {\n for (const otherId of note.conflicts_with ?? []) {\n const other = byId.get(otherId)\n if (!other) continue\n const key = note.id < otherId ? `${note.id}:${otherId}` : `${otherId}:${note.id}`\n if (renderedPairs.has(key)) continue\n renderedPairs.add(key)\n\n // Newer first — the later statement is usually the current one.\n const [newer, older] = Date.parse(note.timestamp) >= Date.parse(other.timestamp) ? [note, other] : [other, note]\n const zh = LOCALE === 'zh'\n pairLines.push(`### 🟠 ${zh ? '冲突' : 'CONFLICT'} | ${newer.category || 'General'}`)\n if (newer.conflict_reason) {\n pairLines.push(`**${zh ? '判定理由' : 'Why'}:** ${newer.conflict_reason}`)\n pairLines.push('')\n }\n pairLines.push(`| | ${zh ? '时间' : 'When'} | ${zh ? '内容' : 'Content'} |`)\n pairLines.push('| :-- | :-- | :-- |')\n pairLines.push(`| **A** | ${newer.timestamp.slice(0, 10)} | ${newer.content.replace(/\\n/g, ' ')} |`)\n pairLines.push(`| **B** | ${older.timestamp.slice(0, 10)} | ${older.content.replace(/\\n/g, ' ')} |`)\n pairLines.push('')\n pairLines.push(`\\`A: ${newer.id}\\``)\n pairLines.push(`\\`B: ${older.id}\\``)\n pairLines.push('')\n pairLines.push(\n zh\n ? `- [ ] ✅ **A 是当前状态,停用 B**(推荐:A 更新)`\n : `- [ ] ✅ **A is current — retire B** (recommended: A is newer)`\n )\n pairLines.push(zh ? `- [ ] ↩️ B 是当前状态,停用 A` : `- [ ] ↩️ B is current — retire A`)\n pairLines.push(zh ? `- [ ] 🤝 两者都成立(误判)` : `- [ ] 🤝 Both hold — not a contradiction`)\n pairLines.push('')\n pairLines.push('---')\n pairLines.push('')\n }\n }\n if (pairLines.length > 0) {\n lines.push(LOCALE === 'zh' ? '## 冲突(成对,一个冲突一个决定)' : '## Conflicts (paired — one decision each)')\n lines.push('')\n lines.push(...pairLines)\n lines.push(LOCALE === 'zh' ? '## 其余条目' : '## Other items')\n lines.push('')\n }\n\n for (let i = 0; i < items.length; i++) {\n const { note, reasons } = items[i]\n const badge = severityBadge(reasons)\n const reasonStr = reasons.map(reasonLabel).join('、')\n\n const issueLabel = LOCALE === 'zh' ? '问题' : 'Issue'\n const contentLabel = LOCALE === 'zh' ? '内容' : 'Content'\n const kwLabel = LOCALE === 'zh' ? '关键词' : 'Keywords'\n const tagLabel = LOCALE === 'zh' ? '标签' : 'Tags'\n const keepLabel = LOCALE === 'zh' ? '保留' : 'Keep'\n const rewriteLabel = LOCALE === 'zh' ? '改写' : 'Rewrite'\n const deleteLabel = LOCALE === 'zh' ? '删除' : 'Delete'\n\n lines.push(`### [${i + 1}] ${badge} | ${note.category || 'General'}`)\n lines.push(`\\`${note.id}\\``)\n lines.push('')\n lines.push(`**${issueLabel}:** ${reasonStr}`)\n lines.push('')\n lines.push(`**${contentLabel}:**`)\n lines.push('```')\n lines.push(note.content)\n lines.push('```')\n lines.push('')\n lines.push(`**${kwLabel}:** ${note.keywords.join(', ')}`)\n lines.push(`**${tagLabel}:** ${note.tags.join(', ')}`)\n lines.push('')\n lines.push(`- [ ] ✅ ${keepLabel}`)\n lines.push(`- [ ] 🔧 ${rewriteLabel}`)\n lines.push(`- [ ] 🗑️ ${deleteLabel}`)\n lines.push('')\n lines.push('---')\n lines.push('')\n }\n\n fs.mkdirSync(path.dirname(filePath), { recursive: true })\n fs.writeFileSync(filePath, lines.join('\\n'), 'utf8')\n\n return filePath\n}\n","/**\n * crud-guard.ts — write-safety policy for the agent_end CRUD decision (Story 41).\n *\n * The CRUD step hands the LLM a numbered list of candidate memories and asks it\n * to pick one to UPDATE or DELETE. Picking the WRONG number is the engine's only\n * silent, unrecoverable failure:\n *\n * - DELETE is already safe — `invalidateNote` is a soft delete (is_active=false).\n * - UPDATE is not — `updateNoteContent` overwrites content + embedding in place.\n *\n * An out-of-range index is harmless (`memories[bad]` is undefined and the caller\n * skips it). The dangerous case is an in-range but WRONG index: both are valid\n * array positions, so nothing structural catches it, and the access protocol\n * (Story 33/36) does not either — the caller usually does own the note it is\n * about to clobber.\n *\n * This is a documented failure class, not a hypothetical: mem0 removed its own\n * CRUD step in part because \"overwrites sometimes erased key information from the\n * original fact\", and Memory-R1 exists because vanilla LLMs mis-classify additive\n * facts as contradictions. The risk scales inversely with model capability, and\n * the memories that reach this step have already survived hash and vector dedup —\n * i.e. they are the HARDEST subset, exactly where a cheap model is least reliable.\n *\n * The rule below is the architectural answer to that, rather than paying for a\n * bigger model: before overwriting a memory, check that the replacement text is\n * at least plausibly ABOUT that memory. A mis-targeted UPDATE rewrites a note\n * with content that has nothing to do with it, which is cheap to detect — both\n * embeddings are already in hand, so this costs one dot product and no LLM call.\n */\nimport { cosineSimilarity } from './embedding.js'\n\n/**\n * Similarity floor for accepting an UPDATE target.\n *\n * Heuristic, not empirically tuned: it sits just above the 0.3 bar the engine\n * already uses for \"these two notes are related at all\", because a legitimate\n * CRUD UPDATE is often a correction or contradiction (\"drinks tea\" → \"switched to\n * coffee\") that is related but not near-identical. Set it too high and real\n * corrections get downgraded; too low and the guard does nothing.\n *\n * Failing this check is SAFE by construction — the caller inserts the fact as a\n * new memory instead of overwriting, and scheduled consolidation can merge later.\n * So the cost of a false positive is a duplicate, and the cost of a false\n * negative is a destroyed memory. Bias accordingly: raise it for cheaper models.\n */\nexport const DEFAULT_CRUD_UPDATE_MIN_SIM = 0.35\n\n/** Resolve the threshold: env var wins, then an explicit override, then default. */\nexport function resolveCrudUpdateMinSim(override?: number): number {\n const envVal = Number(process.env.AMEM_CRUD_UPDATE_MIN_SIM)\n if (Number.isFinite(envVal) && envVal >= 0) return envVal\n if (override !== undefined && Number.isFinite(override) && override >= 0) return override\n return DEFAULT_CRUD_UPDATE_MIN_SIM\n}\n\n/**\n * May `newEmbedding`'s fact overwrite the memory `targetEmbedding` belongs to?\n *\n * True when the replacement is plausibly about the same thing. False means the\n * LLM most likely named the wrong index — the caller should insert instead of\n * overwrite, never throw.\n *\n * Both vectors are L2-normalized by `encode`, so this is a dot product.\n */\nexport function isPlausibleUpdateTarget(\n newEmbedding: number[],\n targetEmbedding: number[],\n minSimilarity?: number\n): boolean {\n // A missing or malformed vector is not evidence of a good target. Refuse\n // rather than let a degenerate similarity wave the overwrite through.\n if (!newEmbedding?.length || !targetEmbedding?.length) return false\n if (newEmbedding.length !== targetEmbedding.length) return false\n return cosineSimilarity(newEmbedding, targetEmbedding) >= resolveCrudUpdateMinSim(minSimilarity)\n}\n"],"mappings":"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","names":[]}
|