@ai-agent-forge/plugin-memory 0.85.2 → 0.85.4

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.
@@ -1 +1 @@
1
- {"version":3,"file":"capability.d.ts","sourceRoot":"","sources":["../src/capability.ts"],"names":[],"mappings":"AAyFA,OAAO,KAAK,EAEX,0BAA0B,EAC1B,YAAY,EACZ,yBAAyB,EAEzB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACN,KAAK,aAAa,EAClB,KAAK,sBAAsB,EAG3B,MAAM,yBAAyB,CAAC;AAMjC,OAAO,EACN,uBAAuB,EACvB,KAAK,0BAA0B,EAC/B,KAAK,wBAAwB,EAC7B,KAAK,YAAY,EACjB,KAAK,yBAAyB,EAC9B,KAAK,gCAAgC,GACrC,MAAM,yBAAyB,CAAC;AAuFjC,eAAO,MAAM,kBAAkB;;;;;;;;;;CAMrB,CAAC;AAEX;;;GAGG;AACH,eAAO,MAAM,yBAAyB,2BAA2B,CAAC;AAmBlE;;;;GAIG;AACH,eAAO,MAAM,6BAA6B,0LACyI,CAAC;AAgCpL;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,qRACoP,CAAC;AAWpR;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,eAAe,CAAC;AAyFjD;;;;;;;GAOG;AACH,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,CAAC;IACvC,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;CACjC;AAKD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAChC,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,GAAG,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAe,EAC/D,YAAY,CAAC,EAAE,MAAM,GACnB,sBAAsB,CA0BxB;AAED,qDAA8B;AAC9B,MAAM,MAAM,sBAAsB,GAAG,IAAI,GAAG,KAAK,CAAC;AAIlD;;;GAGG;AACH,MAAM,WAAW,yBAAyB;IACzC,QAAQ,CAAC,OAAO,EAAE,sBAAsB,CAAC;IACzC,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;CAClC;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CACnC,eAAe,EAAE,MAAM,GAAG,SAAS,EACnC,GAAG,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAe,GAC7D,yBAAyB,CAW3B;AAED,0FAA0F;AAC1F,MAAM,WAAW,uBAAuB;IACvC,oFAAkF;IAClF,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;;;OAKG;IACH,QAAQ,CAAC,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAC3C,uGAAyD;IACzD,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,CAAC;IACvC,8FAAoE;IACpE,QAAQ,CAAC,yBAAyB,CAAC,EAAE,MAAM,CAAC;IAC5C;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE;QACjB,QAAQ,CAAC,IAAI,CAAC,EAAE,YAAY,CAAC;QAC7B;;;;WAIG;QACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;QAC/B;;WAEG;QACH,QAAQ,CAAC,OAAO,CAAC,EAAE,sBAAsB,CAAC;KAC1C,CAAC;IACF;;;;OAIG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,0BAA0B,CAAC;IACvD;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,yBAAyB,CAAC;CAC7C;AAMD,wGAA2D;AAC3D,MAAM,MAAM,uBAAuB,GAAG,UAAU,GAAG,OAAO,GAAG,UAAU,GAAG,QAAQ,CAAC;AAEnF,+FAAoD;AACpD,MAAM,WAAW,uBAAuB;IACvC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CACpC;AAqYD;;;;;;;GAOG;AACH,wBAAgB,6BAA6B,CAC5C,OAAO,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,EACpD,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,GAC1C,MAAM,EAAE,CAeV;AAiRD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,sBAAsB,CACrC,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE,uBAAuB,EAChC,QAAQ,CAAC,EAAE,0BAA0B,GACnC,SAAS,sBAAsB,EAAE,CAGnC;AAED,sFAAsF;AACtF,MAAM,WAAW,+BAA+B;IAC/C,QAAQ,CAAC,aAAa,EAAE,SAAS,sBAAsB,EAAE,CAAC;IAC1D,qFAAqF;IACrF,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAClC,WAAW,IAAI,KAAK,GAAG,uBAAuB,CAAC;IAC/C;;;OAGG;IACH,eAAe,IAAI,OAAO,CAAC,OAAO,GAAG,uBAAuB,CAAC,CAAC;CAC9D;AAED;;;;;GAKG;AACH,wBAAgB,oCAAoC,CACnD,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE,uBAAuB,EAChC,QAAQ,EAAE,0BAA0B,GAClC,+BAA+B,CAiBjC","sourcesContent":["/**\n * First-party builtin memory capability (统一修复轮 A, 方案系统设计 §6.1 + §11,\n * 方案实施计划 §11 A 路) — the model-facing write/recall/list/forget surface\n * over the M5 Memory Foundation.\n *\n * Every operation is suite-scoped by construction: the suiteId comes from the\n * session's suite binding entry (SUITE_SESSION_ENTRY_TYPE, written by the sdk\n * at session creation) and is NEVER a tool parameter, so the model cannot\n * widen or switch its memory domain (防越权). Sessions without a binding fall\n * back to the \"legacy\" domain with a diagnostic. Writes go through the\n * scheduler facade (observation → candidate → committed) plus a durable JSONL\n * ledger append (src/memory/ledger.ts, the store's persistent replica); recall\n * runs through the memory-recall-agent with the suiteId passthrough filter and\n * the egress gate; forget goes through the canonical purge gate\n * (user-immediate) and journal, physically rewriting the ledger (forget 后真\n * 物理删除).\n *\n * Domain resolution is LAZY, per tool call (统一修复轮 B1 复审修复): the sdk\n * appends the suite binding entry only after `new AgentSession` returns (the\n * constructor's synchronous runtime build already runs this factory), so a\n * factory-time resolution always saw an empty branch and every new session\n * fell back to the legacy domain. Instead, each tool handler resolves the\n * domain at call time — the binding entry is on the branch by then (creation\n * appends it; resumed sessions carry it in their history) — and the per-domain\n * ledger is created, loaded, and replayed into the store on the first call in\n * that domain (Map<domain, ledger> cache, no memoization of the domain\n * itself). The store stays one per-capability-instance singleton across\n * domains: the atom `suiteId` field carries the read boundary; the ledger is\n * only the per-domain durable replica.\n *\n * Concurrency envelope (统一修复轮 H1, #7 深度修复; WP-F 关洞修复): ledger\n * appends AND the forget path's full-file rewrite serialize competing\n * processes through the ledger's `.lock` sibling file (exclusive create +\n * poll, 5 s timeout, stale-lock stealing — src/memory/ledger.ts), and both\n * write paths run open → write → fsync → close, so a hard crash cannot lose\n * a write that returned to its caller. A lock timeout or IO/fsync failure\n * throws out of the tool handler, so the model sees the error instead of a\n * silently lost write. Remaining boundaries: the lock is atomic only on\n * local filesystems; a rewrite publishes the calling process's in-memory\n * view without merging rows another process appended after its last load\n * (the forget caller owns that reconciliation); fsync does not cover\n * directory entries, so a crash before rewrite's rename leaves the previous\n * file intact; an append whose fsync failed stays unacknowledged though it\n * may already be readable — the loader's duplicate tolerance covers an\n * unacknowledged retry.\n *\n * Global mode gate (2.4d6 S1, 记忆系统语义化重设计 §3): `memory.mode` is\n * `\"off\"` (default) | `\"light\"` | `\"full\"`; off means the whole memory family\n * is NOT registered (resolveBuiltinCapabilities skips the plugin; the factory\n * itself returns zero registrations for direct callers). light/full run an\n * enabling state machine (local checks → model load/download → smoke →\n * ready; failure → failed with a four-option action list and the effective\n * mode falls back to off for this instance).\n *\n * Semantic recall (2.4d6 S2, 设计 §4): memory_recall has exactly one\n * retrieval path — embed → KNN depth50 + FTS depth50 → RRF k=60 top20 →\n * cross-encoder rerank → store lookup (owner/suite/scope/expiry). The legacy\n * tag-wordform bridge (query-token → tag-facet anchors, recency fallback\n * page) is retired as a retrieval semantic; tags still ride the FTS body and\n * the memory_list management face. Degradation ladder (never thrown at the\n * model): rerank failure → RRF order; vector failure → FTS-only with\n * recency ordering; FTS/index failure → KNN-only; both channels down →\n * recency enumeration over the domain; total failure → degraded packet.\n * `memoryIdEquals`-style primary-key lookup survives as the exact-id fast\n * path (reference semantics, not retrieval).\n *\n * Semantic write merge (2.4d6 S3, 设计 §5 + 实施计划 S3 标定定版): in mode\n * \"full\" every memory_write probes the projection BEFORE the canonical commit\n * (embed + same-owner KNN top-5) because atoms are immutable — the outcome's\n * relations must ride the atom's one commit. Merge condition (calibrated on\n * bge-m3): cos ≥ 0.72, or 0.55 ≤ cos < 0.72 with a case-insensitive tag\n * overlap. A merge commits the NEW atom with a one-way `supersedes` relation\n * to the neighbor (the old entry keeps its memoryId and stays addressable via\n * primary-key lookup) while the projection upserts the new row and removes the\n * superseded one; startup reconciliation never re-adds superseded rows.\n * 0.55 ≤ cos < 0.72 without a tag overlap is the conflict band: both entries\n * stay, and the new atom carries one-way `conflict` relations. Below that (or\n * no neighbor): a plain new entry. bge-small-zh similarity distributions\n * overlap (标定: 余弦不可用作去重信号) → light mode never probes and stays\n * byte-identical to the pre-S3 behavior. Recall filters superseded entries\n * after the store lookup (only while their successor is among the candidates,\n * so forgetting the successor revives the old entry). The Foundation\n * (owner, observationId) idempotency is untouched: replays never consume a\n * probe outcome.\n */\nimport { existsSync, readdirSync, statSync } from \"node:fs\";\nimport { statfs } from \"node:fs/promises\";\nimport { createRequire } from \"node:module\";\nimport { dirname, join } from \"node:path\";\nimport type {\n\tJsonValue,\n\tMemoryCapabilityOverrideV1,\n\tMemoryModeV1,\n\tMemoryStorageComponentsV1,\n\tMemoryVectorComponentsOverrideV1,\n} from \"@agent-forge/plugin-sdk\";\nimport {\n\ttype CapabilityAPI,\n\ttype DisposableRegistration,\n\tEXPERIMENTAL_PUBLIC_API_VERSION,\n\tMEMORY_RECALL_TOOL_NAME,\n} from \"@agent-forge/plugin-sdk\";\nimport { Type } from \"typebox\";\nimport { purgeZeroByteCacheArtifacts } from \"./memory/model-cache-hygiene.ts\";\n\n// 记忆公共契约的权威定义处已是 plugin-sdk(D-075 S4-4 第一批迁入);此处 re-export\n// 维持宿主历史导出面(宿主第二批收口后由宿主 re-export SDK)。\nexport {\n\tMEMORY_RECALL_TOOL_NAME,\n\ttype MemoryCapabilityOverrideV1,\n\ttype MemoryEnablingOverrideV1,\n\ttype MemoryModeV1,\n\ttype MemoryStorageComponentsV1,\n\ttype MemoryVectorComponentsOverrideV1,\n} from \"@agent-forge/plugin-sdk\";\n\nimport { loadAssistantPreferenceCard } from \"./memory/assistant-card.ts\";\nimport { createMemoryCandidateMachine } from \"./memory/candidates.ts\";\nimport type { MemoryEgressFactInfoV1 } from \"./memory/egress-policy.ts\";\nimport { createMemoryEgressPolicy } from \"./memory/egress-policy.ts\";\nimport type { MemoryEmbeddingProviderV1 } from \"./memory/embedding-provider.ts\";\nimport type { MemoryRerankerV1 } from \"./memory/embedding-reranker.ts\";\nimport {\n\tbuildMemoryAtomV1,\n\ttype MemoryAtomV1,\n\ttype MemoryPreferenceEnvelopeV1,\n\ttype MemoryRelationV1,\n\ttype MemorySuiteFilterStatsV1,\n} from \"./memory/foundation.ts\";\nimport { createDurableMemoryLedger, type DurableMemoryLedgerV1 } from \"./memory/ledger.ts\";\nimport { createMemoryLifecycleManager } from \"./memory/lifecycle.ts\";\nimport {\n\tcreateMemoryNetwork,\n\tcreateRelationDiffusionAdapter,\n\ttype MemoryNetworkExpansionV1,\n} from \"./memory/memory-network.ts\";\nimport { createPreferenceDisambiguator, type PreferenceDisambiguatorV1 } from \"./memory/preference-disambiguator.ts\";\nimport { promotePreferenceToUserDefault } from \"./memory/preference-promotion.ts\";\nimport {\n\tcreatePreferenceResolver,\n\ttype PreferenceContextV1,\n\ttype PreferenceResolverV1,\n} from \"./memory/preference-resolver.ts\";\nimport { createMemoryPurgeGate, createMemoryReplicaRegistry, type MemoryReplicaV1 } from \"./memory/purge.ts\";\nimport { createMemoryPurgeJournal } from \"./memory/purge-journal.ts\";\nimport { createMemoryRecallAgent } from \"./memory/recall-agent.ts\";\nimport { createMemoryRecallIndex } from \"./memory/recall-index.ts\";\nimport type { MemoryRecallPacketV1 } from \"./memory/recall-packet.ts\";\nimport { createMemoryScheduler } from \"./memory/scheduler.ts\";\nimport { createMemorySchedulerApi } from \"./memory/scheduler-api.ts\";\nimport { createFirstPartyRetentionRegistry, createMemoryStore } from \"./memory/store.ts\";\nimport {\n\tforgetDomainMemory,\n\tlistDomainMemories,\n\ttype SuiteForgetAuthorizationRefV1,\n\ttype SuiteMemoryDomainV1,\n\ttype SuiteMemoryListEntryV1,\n\tsuiteMemoryDomainName,\n} from \"./memory/suite-memory.ts\";\nimport { MEMORY_CONTRACT_VERSION_V2 } from \"./memory/transfer.ts\";\nimport type { MemoryVectorHitV1, MemoryVectorIndexV1 } from \"./memory/vector-index.ts\";\n\n// ---------------------------------------------------------------------------\n// Host lifecycle catalog references (D-075 S4-4 第一批迁包改写): the embedded\n// capability imported the event definitions from the host's lifecycle catalog\n// and re-defined them idempotently. The plugin now subscribes through the\n// plain `{id, version}` reference form (observability.ts 同构先例): the host's\n// bootstrap lifecycle plugin owns the definitions, and a host that has not\n// defined the event rejects the registration structurally (plugin-load\n// failure under D-028, never a session failure). The data shapes below are\n// structural narrowings of the catalog payloads — only the fields the bash\n// error-lesson capture reads.\n// ---------------------------------------------------------------------------\n\n/** `tool.execution.start@1` (host lifecycle catalog). */\nconst TOOL_EXECUTION_START_EVENT_ID = \"tool.execution.start\";\nconst TOOL_EXECUTION_START_EVENT_VERSION = 1;\n/** `tool.result@1` (host lifecycle catalog). */\nconst TOOL_RESULT_EVENT_ID = \"tool.result\";\nconst TOOL_RESULT_EVENT_VERSION = 1;\n\n/** Structural narrowing of the catalog's `ToolExecutionStartEventDataV1`. */\ninterface MemoryToolExecutionStartEventDataV1 {\n\treadonly version: 1;\n\treadonly sessionId: string;\n\treadonly toolCallId: string;\n\treadonly toolName: string;\n}\n\n/** Structural narrowing of the catalog's `ToolResultEventDataV1` (text blocks only). */\ninterface MemoryToolResultEventDataV1 {\n\treadonly version: 1;\n\treadonly sessionId: string;\n\treadonly toolCallId: string;\n\treadonly toolName: string;\n\treadonly isError: boolean;\n\treadonly message: {\n\t\treadonly content: readonly ({ readonly type: \"text\"; readonly text: string } | { readonly type: \"image\" })[];\n\t};\n}\n\nexport const capabilityManifest = {\n\tid: \"agent-forge.builtin.memory\",\n\tversion: \"0.1.0\",\n\tapiVersion: EXPERIMENTAL_PUBLIC_API_VERSION,\n\trequiredCapabilities: [\"session\", \"events\", \"tools\"],\n\tprovides: [{ id: \"memory.scheduler\", version: 1, kind: \"service\" }],\n} as const;\n\n/**\n * Marker text of the auto-recall injection message (B1 记忆自动注入, D-071 增补\n * 裁决自 sdk 迁入; pinned by test/memory-sdk-e2e.test.ts).\n */\nexport const MEMORY_AUTO_RECALL_MARKER = \"<auto_recalled_memory>\";\n\n/**\n * Upper bound on facts pulled into one auto-recall injection (B1 记忆自动注入).\n * The injection rides every request of the turn, so it stays a compact hint —\n * deep recall is what memory_recall is for.\n */\nconst MEMORY_AUTO_RECALL_LIMIT = 3;\n\n/**\n * Auto-recall rerank floor (错误教训与召回质量护栏设计 §2.1): reranked\n * candidates scoring below it are dropped from the auto-recall injection\n * (`score < minScore`). bge-reranker outputs uncalibrated logits that are only\n * order-preserving (见 embedding-reranker.ts 头注), so 0 (\"positive logit =\n * relevant direction\") is the conservative initial gate — tunable with real\n * model eval evidence (只改常量 + eval 证据).\n */\nconst MEMORY_AUTO_RECALL_MIN_RERANK_SCORE = 0;\n\n/**\n * Tail line of every auto-recall injection block (错误教训与召回质量护栏设计\n * §2.1): tells the model the block was recalled rather than written by the\n * user and may be outdated. Pinned verbatim by test/memory-error-lessons.test.ts.\n */\nexport const MEMORY_AUTO_RECALL_DISCLAIMER =\n\t\"(Recalled automatically from persistent memory for this request — not written by the user. Recalled items may be outdated; verify against current state before relying on them.)\";\n\n/**\n * Upper bound on the error message embedded in one memory auto-recall log\n * line: provider/store errors can carry arbitrary payload text, and the log\n * channel is for locating the failure, not for echoing it.\n */\nconst MEMORY_AUTO_RECALL_ERROR_MESSAGE_LIMIT = 200;\n\n// ---------------------------------------------------------------------------\n// bash 失败教训自动沉淀 (错误教训与召回质量护栏设计 §2.2): 确定性事实模板,\n// 无模型解读; 风暴由签名去重 + 每实例上限兜住; 7 天保留档到期失去召回资格。\n// ---------------------------------------------------------------------------\n\n/** 每实例教训写入上限 (§2.2 风暴闸门): 首个超限事件 warn 一次, 之后静默跳过。 */\nconst MEMORY_LESSON_MAX_PER_SESSION = 10;\n/** 教训 excerpt 字符上限 (错误文本尾部, 按码点切分不劈代理对)。 */\nconst MEMORY_LESSON_ERROR_EXCERPT_MAX_CHARS = 300;\n/** toolCallId→command 配对映射容量 (仅 bash, FIFO)。 */\nconst MEMORY_LESSON_TOOLCALL_MAP_CAPACITY = 32;\n/** 教训开关环境变量 (context 显式值 > env > 默认 on; 非法值取默认并 warn)。 */\nconst MEMORY_LESSONS_ENV = \"AGENT_FORGE_MEMORY_LESSONS\";\n/** 教训 tags (§2.2): 随 FTS body 可检索。 */\nconst MEMORY_LESSON_TAGS: readonly string[] = [\"auto-lesson\", \"bash\"];\n/** 教训提交的保留档 (first-party retention registry 既有 \"short\" = 7 天)。 */\nconst MEMORY_LESSON_RETENTION_MODE_ID = \"short\";\n/**\n * bash 失败退出码的既有后缀 (core/tools/bash.ts: 输出文本 + 该状态行); 解析\n * 不到 (timeout/abort 等) 记 unknown, 仍沉淀。\n */\nconst MEMORY_LESSON_EXIT_CODE_PATTERN = /Command exited with code (\\d+)\\s*$/;\n\n/**\n * Memory tools guide(记忆 runtime 注入面,原 system-prompt.ts MEMORY_TOOL_GUIDE,\n * D-071 迁移):the model-facing hint that the persistent memory tools exist.\n * It rides memory_recall's promptGuidelines so the text lives with the plugin\n * that owns the tools (宪法 §3) and renders whenever the tool face is active.\n */\nexport const MEMORY_TOOLS_GUIDE =\n\t\"Memory: You have persistent memory tools (memory_write, memory_recall, memory_list, memory_forget). Use memory_recall to look up user preferences or project facts before answering. Use memory_write to save important decisions, preferences, or findings for future sessions.\";\n\n/**\n * Session protocol key of the suite binding entry (方案设计 §10.1). The value\n * mirrors the authoritative `SUITE_SESSION_ENTRY_TYPE` in\n * src/profiles/suite-loader.ts — pinned by a deterministic test in\n * test/capabilities-builtin.test.ts. Declared locally so the capability module\n * graph stays free of the assembler's bundled-loader dependency chain.\n */\nconst SUITE_SESSION_ENTRY_TYPE = \"suite\";\n\n/**\n * Owner (isolation principal) of every atom this capability writes. The\n * Foundation owner is the store/facade isolation identity; this product ships\n * one local user per agentDir, so one stable owner id partitions the ledger\n * directory `<agentDir>/memory/<owner>/ledger-<suiteId|\"legacy\">.jsonl`.\n */\nexport const BUILTIN_MEMORY_OWNER = \"local-user\";\n\n/** Contract version written by this capability (@2 atoms carry suiteId). */\nconst MEMORY_CONTRACT_VERSION = MEMORY_CONTRACT_VERSION_V2;\n\nconst TAG_FACET_NAMESPACE = \"builtin.memory\";\nconst EVENT_TYPE = \"memory.changed\";\nconst MAX_TAGS = 8;\nconst DEFAULT_RECALL_LIMIT = 5;\nconst DEFAULT_LIST_LIMIT = 20;\n/** Bounded supplementary relation references per recall (记忆网络接线). */\nconst MAX_RELATED_REFS = 20;\n\n// ---------------------------------------------------------------------------\n// 语义写入合并 (2.4d6 S3, 实施计划 S3 阈值标定定版)。full 档独有: light 档\n// (bge-small-zh) 重复变体/同主题/无关三组余弦分布完全重叠, 余弦不可用作去重\n// 信号 → 不探测不合并 (行为与 S3 之前逐字节一致); off 档整体不注册。\n// ---------------------------------------------------------------------------\n\n/** 合并下限 (bge-m3 实测: 重复变体 0.664–0.833, 同主题 ≤0.652 → 0.72 零误合并)。 */\nconst SEMANTIC_MERGE_T_HIGH = 0.72;\n/** 冲突带下限 (bge-m3 实测: 无关语句最高 0.450 → 0.55 以下零越界)。 */\nconst SEMANTIC_MERGE_T_LOW = 0.55;\n/** 写入探测的同 owner KNN 近邻数 (标定定版: top-5)。 */\nconst SEMANTIC_MERGE_KNN_LIMIT = 5;\n/** supersedes/conflict 关系的 capability 命名空间 (与 tag facet 同域)。 */\nconst SEMANTIC_RELATION_NAMESPACE = \"builtin.memory\";\n\n// ---------------------------------------------------------------------------\n// 语义召回管线 (2.4d6 S2, 记忆系统语义化重设计 §4) 与全局档位 (S1, §3)。\n// 召回唯一路径: embed → KNN+FTS → RRF → 精排 → store 回查; tag 词面 bridge\n// 已退役 (tags 仅入 FTS body 与管理面)。off 档 = 整个 capability 不注册。\n// ---------------------------------------------------------------------------\n\n/** memory_write 内容长度契约 (POC 报告 §7): reranker 512-token 上下文足够。 */\nconst MEMORY_CONTENT_MAX_LENGTH = 700;\n/** FTS / KNN 通道各自取回深度 (定版管线: depth 50)。 */\nconst HYBRID_CHANNEL_DEPTH = 50;\n/** RRF 常数, 与 vector-index 组件 queryFts 打分口径一致。 */\nconst HYBRID_RRF_K = 60;\n/** 融合候选池上限 (定版管线: top20 → 精排 → top limit)。 */\nconst HYBRID_FUSION_POOL = 20;\n/** 单次启动对账 embed+upsert 上限, 超出留待下次启动继续。 */\nconst RECONCILE_MAX_UPSERTS = 2000;\n/** 开启前磁盘余量下限 (设计 §3.2 checking): light ≥ 500MB, full ≥ 2GB。 */\nconst DISK_HEADROOM_MIN_BYTES: Readonly<Record<Exclude<MemoryModeV1, \"off\">, number>> = {\n\tlight: 500 * 1024 * 1024,\n\tfull: 2 * 1024 * 1024 * 1024,\n};\n/**\n * 模型文件大小下限 (防截断/空文件的 sanity 下限, 非精确体积): 高档 embedding\n * (bge-m3 q8) 数百 MB → 下限 256MB, 精排器 (bge-reranker-base q8) ~280MB →\n * 下限 64MB, 低档 (bge-small-zh q8) ~24MB → 下限 8MB。真实校验由加载冒烟承担。\n */\nconst MODEL_MIN_WEIGHT_BYTES: Readonly<Record<Exclude<MemoryModeV1, \"off\">, number>> = {\n\tlight: 8 * 1024 * 1024,\n\tfull: 256 * 1024 * 1024,\n};\nconst RERANKER_MIN_WEIGHT_BYTES = 64 * 1024 * 1024;\n/** 自定义镜像源环境变量 (失败行动清单第一项指向它)。 */\nconst HF_MIRRORS_ENV = \"AGENT_FORGE_HF_MIRRORS\";\n/** enabling 加载/下载期间的周期性进度日志间隔。 */\nconst ENABLING_PROGRESS_LOG_INTERVAL_MS = 15_000;\n\n/** 两档预设 (实施计划 §2): light = 纯中文强项低配档, full = 跨语言大规模默认档。 */\ninterface MemoryVectorPresetV1 {\n\treadonly embeddingModelId: string;\n\treadonly dimensions: number;\n\t/** 查询侧指令前缀 (bge-small-zh 系查询侧指令由调用方负责添加; 写入侧不加)。 */\n\treadonly queryPrefix: string;\n}\n\nconst VECTOR_PRESET_LIGHT: MemoryVectorPresetV1 = {\n\tembeddingModelId: \"Xenova/bge-small-zh-v1.5\",\n\tdimensions: 512,\n\tqueryPrefix: \"为这个句子生成表示以用于检索相关文章:\",\n};\nconst VECTOR_PRESET_FULL: MemoryVectorPresetV1 = {\n\tembeddingModelId: \"Xenova/bge-m3\",\n\tdimensions: 1024,\n\tqueryPrefix: \"\",\n};\n/** 两档共用精排模型 (POC 报告 §8: 官方 Xenova 转换版唯一实测可用)。 */\nconst VECTOR_RERANKER_MODEL_ID = \"Xenova/bge-reranker-base\";\nconst VECTOR_DB_DIRNAME = \"vector\";\nconst VECTOR_MODEL_CACHE_DIRNAME = \"model-cache\";\n/** 镜像序列 (实施计划 §7): 直连 huggingface.co 超时时回落 hf-mirror.com。 */\nconst VECTOR_REMOTE_HOSTS: readonly string[] = [\"https://huggingface.co\", \"https://hf-mirror.com\"];\n\n/**\n * 解析后的全局模式 (2.4d6 S1)。`invalidEnvValue` 非空 = 配置的环境值非法,\n * 已 fail-closed 回 off, 调用方负责以此输出一次 warn; `invalidSettingsValue`\n * 非空 = settings.json 的 `memory.mode` 非法, 同为 fail-closed off 并输出一次\n * 诊断; `legacyEnvUsed` = 模式由旧别名 `AGENT_FORGE_MEMORY_VECTOR` 决定 (兼容\n * 期: 仅当 `AGENT_FORGE_MEMORY_MODE` 与 settings 均未定档时生效, 一个发布周期\n * 后移除)。\n */\nexport interface MemoryModeResolutionV1 {\n\treadonly mode: MemoryModeV1;\n\treadonly invalidEnvValue?: string;\n\treadonly invalidSettingsValue?: string;\n\treadonly legacyEnvUsed?: boolean;\n}\n\nconst isMemoryModeV1 = (value: string): value is MemoryModeV1 =>\n\tvalue === \"off\" || value === \"light\" || value === \"full\";\n\n/**\n * memory.mode 解析 (2.4d6 S1)。优先级: context 显式值 → 环境变量\n * `AGENT_FORGE_MEMORY_MODE` → settings.json `memory.mode` (settingsMode 参, 持\n * 久配置面) → 旧别名 `AGENT_FORGE_MEMORY_VECTOR` (兼容期) → 默认 off。env 是\n * 临时覆盖、settings 是持久配置 (env > 文件, 沿用本仓 suite 解析先例)。任何\n * 非法配置值 fail-closed 为 off 并在结果中携带原值供调用方告警一次, 绝不静默\n * 改档或意外下载。\n */\nexport function resolveMemoryMode(\n\texplicitMode: string | undefined,\n\tenv: Readonly<Record<string, string | undefined>> = process.env,\n\tsettingsMode?: string,\n): MemoryModeResolutionV1 {\n\tif (explicitMode !== undefined) {\n\t\tif (isMemoryModeV1(explicitMode)) return { mode: explicitMode };\n\t\treturn { mode: \"off\", invalidEnvValue: explicitMode };\n\t}\n\tconst configured = env.AGENT_FORGE_MEMORY_MODE?.trim().toLowerCase();\n\tif (configured !== undefined && configured !== \"\") {\n\t\tif (isMemoryModeV1(configured)) return { mode: configured };\n\t\treturn { mode: \"off\", invalidEnvValue: configured };\n\t}\n\t// 持久配置面: settings.json memory.mode。设置但非法同样 fail-closed, 不再\n\t// 回落旧别名 (与非法 env 的先例一致)。\n\tconst fromSettings = settingsMode?.trim().toLowerCase();\n\tif (fromSettings !== undefined && fromSettings !== \"\") {\n\t\tif (isMemoryModeV1(fromSettings)) return { mode: fromSettings };\n\t\treturn { mode: \"off\", invalidSettingsValue: fromSettings };\n\t}\n\t// 兼容期别名: AGENT_FORGE_MEMORY_VECTOR (混合检索工程化的旧开关), 语义并入\n\t// AGENT_FORGE_MEMORY_MODE, 仅当新变量与 settings 均未定档时生效; 一个发布\n\t// 周期后删除。\n\tconst legacy = env.AGENT_FORGE_MEMORY_VECTOR?.trim().toLowerCase();\n\tif (legacy !== undefined && legacy !== \"\") {\n\t\tif (isMemoryModeV1(legacy)) return { mode: legacy, legacyEnvUsed: true };\n\t\treturn { mode: \"off\", invalidEnvValue: legacy, legacyEnvUsed: true };\n\t}\n\treturn { mode: \"off\" };\n}\n\n/** bash 失败教训捕获开关取值 (§2.2)。 */\nexport type MemoryLessonsSettingV1 = \"on\" | \"off\";\n\nconst isMemoryLessonsSettingV1 = (value: string): value is MemoryLessonsSettingV1 => value === \"on\" || value === \"off\";\n\n/**\n * lessons 开关解析结果: `invalidEnvValue` 非空 = 配置值非法, 已 fail 回默认 on,\n * 调用方负责以此告警一次。\n */\nexport interface MemoryLessonsResolutionV1 {\n\treadonly lessons: MemoryLessonsSettingV1;\n\treadonly invalidEnvValue?: string;\n}\n\n/**\n * context.memory.lessons 解析 (§2.2, resolveMemoryMode 同构): 显式值 → 环境变量\n * `AGENT_FORGE_MEMORY_LESSONS` → 默认 on。任何非法配置值回默认并在结果中携带\n * 原值供调用方告警一次, 绝不静默改开关。\n */\nexport function resolveMemoryLessons(\n\texplicitLessons: string | undefined,\n\tenv: Readonly<Record<string, string | undefined>> = process.env,\n): MemoryLessonsResolutionV1 {\n\tif (explicitLessons !== undefined) {\n\t\tif (isMemoryLessonsSettingV1(explicitLessons)) return { lessons: explicitLessons };\n\t\treturn { lessons: \"on\", invalidEnvValue: explicitLessons };\n\t}\n\tconst configured = env[MEMORY_LESSONS_ENV]?.trim().toLowerCase();\n\tif (configured !== undefined && configured !== \"\") {\n\t\tif (isMemoryLessonsSettingV1(configured)) return { lessons: configured };\n\t\treturn { lessons: \"on\", invalidEnvValue: configured };\n\t}\n\treturn { lessons: \"on\" };\n}\n\n/** Host-provided factory context; resolved by resolveBuiltinCapabilities (builtin.ts). */\nexport interface MemoryCapabilityContext {\n\t/** Agent home directory — roots the durable ledger under `<agentDir>/memory/`. */\n\treadonly agentDir: string;\n\t/**\n\t * 宿主已解析的 onnxruntime-node 入口绝对路径 (D-075 安装店重依赖可见性):\n\t * 安装店副本无 node_modules, 插件内 createRequire 解析不到; 宿主装载器把\n\t * hostDependencyPaths 注入 manifest config, entry 提取后经本字段传入。\n\t * 缺省 (源码/工作区直载) 走插件内双通道自解析。\n\t */\n\treadonly hostOnnxRuntimeEntryPath?: string;\n\t/** 同上——@lancedb/lancedb 入口路径 (向量索引运行时按绝对路径原生 import)。 */\n\treadonly hostLancedbEntryPath?: string;\n\t/** 同上——@huggingface/transformers 入口路径 (embedding/reranker 运行时用)。 */\n\treadonly hostTransformersEntryPath?: string;\n\t/**\n\t * 记忆全局档位 (2.4d6 S1)。缺省解析顺序: 本字段 → 环境变量\n\t * `AGENT_FORGE_MEMORY_MODE` → {@link MemoryCapabilityContext.memory.settingsMode}\n\t * → 旧别名 `AGENT_FORGE_MEMORY_VECTOR` (兼容期) → 默认 off (整个家族不注册)。\n\t * 装配方 (sdk) 可经本字段显式定档。\n\t */\n\treadonly memory?: {\n\t\treadonly mode?: MemoryModeV1;\n\t\t/**\n\t\t * settings.json `memory.mode` 的原样值 (持久配置面, 优先级低于 env);\n\t\t * 类型面只允许合法档位, 手改文件的非法值原样透传并由\n\t\t * {@link resolveMemoryMode} fail-closed 处理。\n\t\t */\n\t\treadonly settingsMode?: string;\n\t\t/**\n\t\t * bash 失败教训捕获开关 (§2.2): 缺省解析顺序见 {@link resolveMemoryLessons}。\n\t\t */\n\t\treadonly lessons?: MemoryLessonsSettingV1;\n\t};\n\t/**\n\t * 测试专用注入 (架构宪法: deterministic mock AI 门禁)。经装配上下文携带时,\n\t * 装配方原样透传给工厂; 与 {@link createCapabilityWithVectorComponents}\n\t * 的第三参语义一致, 第三参优先。\n\t */\n\treadonly vectorComponents?: MemoryCapabilityOverrideV1;\n\t/**\n\t * 记忆底层组件注入 (扩展位盘点 A1/A2 的宿主装配通道): store/ledger 工厂与\n\t * 向量组件公共契约, 缺省成员走内置默认实现。装配方经\n\t * `CreateAgentSessionOptions.memoryCapability.storage` 携带。\n\t */\n\treadonly storage?: MemoryStorageComponentsV1;\n}\n\nconst isVectorComponentsOverride = (\n\toverride: MemoryCapabilityOverrideV1,\n): override is MemoryVectorComponentsOverrideV1 => \"embedProvider\" in override && \"vectorIndex\" in override;\n\n/** capability 级状态机 (设计 §3.2): off 只出现在模式关闭 (零注册, 不可观测)。 */\nexport type MemoryCapabilityStateV1 = \"enabling\" | \"ready\" | \"degraded\" | \"failed\";\n\n/** enabling 失败的结构化结果: 原因 + 行动清单 (硬性要求, 设计 §3.2)。 */\nexport interface MemoryEnablingFailureV1 {\n\treadonly reason: string;\n\treadonly actions: readonly string[];\n}\n\ninterface ActiveVectorChannelV1 {\n\treadonly embed: MemoryEmbeddingProviderV1;\n\t/** undefined = 精排不可用 (跳过精排, 直接用 RRF 序)。 */\n\treadonly reranker: MemoryRerankerV1 | undefined;\n\treadonly index: MemoryVectorIndexV1;\n\treadonly queryPrefix: string;\n\t/** 预设 embedding modelId — 对账时与投影存储的 modelId 比较, 不一致即清空重建。 */\n\treadonly embeddingModelId: string;\n}\n\ninterface MemoryFact {\n\treadonly memoryId: string;\n\treadonly statement: string;\n\treadonly confidence: number;\n}\n\nfunction assertNonEmptyString(value: unknown, label: string): asserts value is string {\n\tif (typeof value !== \"string\" || value.trim().length === 0) throw new Error(`${label} must be a non-empty string`);\n}\n\nfunction isPlainRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * 双约定工具输入读取(对齐 subagent-delegate 的 objectInput):capability invoke\n * 面单参约定下第一参即输入(第二参是宿主注入的 toolExecutionContext),legacy 5 参\n * 约定(toolCallId 字符串打头)下输入在第二参。生产装配恒向 runtime invoke 面注入\n * toolExecutionContext 作为 execute 第二参(公共契约),因此\"第二参非 undefined 即\n * 输入\"的判定会把 context 误当输入(content/query 等读成 undefined)。改按形状判定:\n * 第一参是普通对象即取第一参,否则看第二参;两者皆非对象时返回空记录,交由各工具\n * 的字段断言报错。\n */\nfunction memoryToolInput(first: unknown, second: unknown): Record<string, unknown> {\n\tconst primary = isPlainRecord(first) ? first : second;\n\treturn isPlainRecord(primary) ? primary : {};\n}\n\n/** FNV-1a 32-bit — deterministic identity from the given value (code-memory 同构). */\nfunction identityHash(value: string): string {\n\tlet hash = 0x811c9dc5;\n\tfor (let index = 0; index < value.length; index += 1) {\n\t\thash ^= value.charCodeAt(index);\n\t\thash = Math.imul(hash, 0x01000193);\n\t}\n\treturn (hash >>> 0).toString(36);\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n\treturn value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\n\n/**\n * Text of the branch's LAST user message — the auto-recall query and per-turn\n * cache key (B1 记忆自动注入). Read from the persisted branch, not the\n * request-time snapshot: request-time injections (the sdk's background\n * delegation digest) ride the transformContext input as user-role blocks and\n * would otherwise hijack the query, while the branch never sees them.\n */\nfunction lastBranchUserText(api: CapabilityAPI): string | undefined {\n\tconst entries = api.session?.getBranchEntries() ?? [];\n\tfor (let index = entries.length - 1; index >= 0; index -= 1) {\n\t\tconst entry = entries[index];\n\t\tif (entry.type !== \"message\" || entry.message?.role !== \"user\") continue;\n\t\tconst content: unknown = entry.message.content;\n\t\tif (typeof content === \"string\") return content;\n\t\tif (!Array.isArray(content)) continue;\n\t\tconst parts: string[] = [];\n\t\tfor (const item of content) {\n\t\t\tif (isPlainObject(item) && item.type === \"text\" && typeof item.text === \"string\") parts.push(item.text);\n\t\t}\n\t\treturn parts.join(\"\\n\");\n\t}\n\treturn undefined;\n}\n\n/**\n * Inserts the recall injection right after the last user message of the\n * request snapshot (B1 记忆自动注入): the recalled context is turn-scoped, so\n * it belongs at the head of the current turn, ahead of any assistant/\n * toolResult round already in flight. Purely a request-time view — the\n * persisted session history stays untouched.\n */\nfunction injectAfterLastUserMessage(messages: readonly unknown[], injection: unknown): readonly unknown[] {\n\tfor (let index = messages.length - 1; index >= 0; index -= 1) {\n\t\tif ((messages[index] as { readonly role?: string } | undefined)?.role === \"user\") {\n\t\t\treturn [...messages.slice(0, index + 1), injection, ...messages.slice(index + 1)];\n\t\t}\n\t}\n\treturn messages;\n}\n\n/**\n * Tail of `text` whose UTF-16 length never exceeds `limit`, cut on code-point\n * boundaries so a surrogate pair never splits. The excerpt budget is measured\n * in UTF-16 units (`String.length`, the same measure as the 700-char content\n * contract), so counting code points here would let astral-plane tails double\n * the rendered excerpt and break the cap.\n */\nfunction tailWithinUtf16Budget(text: string, limit: number): string {\n\tif (limit <= 0) return \"\";\n\tif (text.length <= limit) return text;\n\tconst codePoints = Array.from(text);\n\tlet units = 0;\n\tlet start = codePoints.length;\n\twhile (start > 0 && units + codePoints[start - 1].length <= limit) {\n\t\tunits += codePoints[start - 1].length;\n\t\tstart -= 1;\n\t}\n\treturn codePoints.slice(start).join(\"\");\n}\n\n/** bash 失败退出码解析 (core/tools/bash.ts 的既有后缀); 解析不到 (timeout/abort) = \"unknown\"。 */\nfunction lessonExitCodeOf(errorText: string): string {\n\treturn MEMORY_LESSON_EXIT_CODE_PATTERN.exec(errorText)?.[1] ?? \"unknown\";\n}\n\n/** Concatenated text of a toolResult message's text blocks (images skipped). */\nfunction toolResultTextOf(content: MemoryToolResultEventDataV1[\"message\"][\"content\"]): string {\n\tconst parts: string[] = [];\n\tfor (const block of content) {\n\t\tif (block.type === \"text\") parts.push(block.text);\n\t}\n\treturn parts.join(\"\\n\");\n}\n\n/**\n * 教训内容 (§2.2 确定性事实模板, 无模型解读): `Command failed: \\`<command>\\`\n * exited with code <N>. Output tail: <excerpt>`。excerpt 取错误文本尾部, 预算\n * 按 UTF-16 单元计 ({@link MEMORY_LESSON_ERROR_EXCERPT_MAX_CHARS} 与 700 上限\n * 同一口径); 超长只压缩 excerpt (excerpt 压到 0 仍超限的极端长命令再压 command\n * 尾部, 保持模板可解析)。\n */\nfunction buildLessonContent(command: string, exitCode: string, errorText: string): string {\n\tconst render = (commandPart: string, excerpt: string): string =>\n\t\t`Command failed: \\`${commandPart}\\` exited with code ${exitCode}. Output tail: ${excerpt}`;\n\tconst excerptLimit = Math.min(\n\t\tMEMORY_LESSON_ERROR_EXCERPT_MAX_CHARS,\n\t\tMEMORY_CONTENT_MAX_LENGTH - render(command, \"\").length,\n\t);\n\tif (excerptLimit > 0) return render(command, tailWithinUtf16Budget(errorText, excerptLimit));\n\tconst commandBudget = MEMORY_CONTENT_MAX_LENGTH - render(\"\", \"\").length;\n\treturn render(command.slice(0, Math.max(0, commandBudget)), \"\");\n}\n\n/**\n * Suite binding entry ids already warned about (统一修复轮终审 minor 3):\n * resolveSessionDomain runs on EVERY memory tool call, so without this\n * module-level set one malformed binding entry would repeat the warning on\n * each call and flood the log.\n */\nconst malformedBindingWarned = new Set<string>();\n\n/**\n * Resolves the session's suite binding domain from the branch entries: the\n * LAST suite entry wins (the sdk appends one at session creation). A binding\n * with a malformed payload counts as unbound — the legacy fallback is\n * fail-safe (its atoms are invisible to every real suite) and a diagnostic is\n * logged when the logger capability is available. Called per tool call —\n * never captured at factory time (统一修复轮 B1: the binding entry is appended\n * after the session constructor ran this factory).\n */\nfunction resolveSessionDomain(api: CapabilityAPI): SuiteMemoryDomainV1 {\n\tconst entries = api.session?.getBranchEntries() ?? [];\n\tfor (let index = entries.length - 1; index >= 0; index -= 1) {\n\t\tconst entry = entries[index];\n\t\tif (entry.type !== \"custom\" || entry.customType !== SUITE_SESSION_ENTRY_TYPE) continue;\n\t\tconst data = entry.data;\n\t\tconst suiteId = isPlainObject(data) ? data.suiteId : undefined;\n\t\tif (typeof suiteId === \"string\" && suiteId.trim() !== \"\") {\n\t\t\treturn { kind: \"suite\", suiteId };\n\t\t}\n\t\tif (!malformedBindingWarned.has(entry.id)) {\n\t\t\tmalformedBindingWarned.add(entry.id);\n\t\t\tapi.logger?.warn(\"suite binding entry has a malformed payload; falling back to the legacy memory domain\", {\n\t\t\t\tentryId: entry.id,\n\t\t\t});\n\t\t}\n\t\treturn { kind: \"legacy\" };\n\t}\n\treturn { kind: \"legacy\" };\n}\n\n/** Recency-first payload projection: unwraps the `{ statement }` payload convention. */\nfunction statementOf(payload: unknown): string {\n\tif (isPlainObject(payload) && typeof payload.statement === \"string\") return payload.statement;\n\treturn JSON.stringify(payload);\n}\n\n/** The egress gate re-derives statements as payload JSON; unwrap for the model face. */\nfunction unwrapEgressStatement(statement: string): string {\n\ttry {\n\t\tconst parsed: unknown = JSON.parse(statement);\n\t\tif (isPlainObject(parsed) && typeof parsed.statement === \"string\") return parsed.statement;\n\t} catch {\n\t\t// Not payload JSON — surface it verbatim.\n\t}\n\treturn statement;\n}\n\n/**\n * 三通道 RRF 融合 (定版管线): `score(id) += 1/(k + rank + 1)` 对各通道排名累加,\n * 取 top {@link HYBRID_FUSION_POOL}。分数并列时以 memoryId 升序打破, 保证确定性。\n */\nfunction rrfFuseRankings(rankings: readonly (readonly string[])[]): readonly string[] {\n\tconst scores = new Map<string, number>();\n\tfor (const ranking of rankings) {\n\t\tfor (const [rank, memoryId] of ranking.entries()) {\n\t\t\tscores.set(memoryId, (scores.get(memoryId) ?? 0) + 1 / (HYBRID_RRF_K + rank + 1));\n\t\t}\n\t}\n\treturn [...scores.entries()]\n\t\t.sort((left, right) => right[1] - left[1] || (left[0] < right[0] ? -1 : 1))\n\t\t.map(([memoryId]) => memoryId)\n\t\t.slice(0, HYBRID_FUSION_POOL);\n}\n\n/** 语义合并判定结论 (投影探测产物; relations 随新 atom 一次性提交, atom 不可变)。 */\ntype SemanticMergePlanV1 =\n\t| { readonly kind: \"merge\"; readonly targetMemoryId: string }\n\t| { readonly kind: \"conflict\"; readonly conflictMemoryIds: readonly string[] }\n\t| { readonly kind: \"new\" };\n\n/** 探测产物: 判定结论 + 新 statement 的嵌入 (投影 upsert 复用同一向量)。 */\ninterface SemanticMergeOutcomeV1 {\n\treadonly plan: SemanticMergePlanV1;\n\treadonly vector: readonly number[];\n}\n\n/** 探测到的近邻 (canonical 回查后): 既有 tags + 与新向量的余弦。 */\ninterface SemanticMergeNeighborV1 {\n\treadonly memoryId: string;\n\treadonly tags: readonly string[];\n\treadonly cosine: number;\n}\n\n/** 嵌入向量已 L2 归一化 (provider 契约), 点积即余弦。 */\nfunction cosineSimilarity(left: readonly number[], right: readonly number[]): number {\n\tlet dot = 0;\n\tfor (const [axis, value] of left.entries()) dot += value * (right[axis] ?? 0);\n\treturn dot;\n}\n\n/** tags 重叠判定 (至少一个共同 tag, trim 后大小写不敏感)。 */\nfunction tagsOverlapIgnoreCase(left: readonly string[], right: readonly string[]): boolean {\n\tif (left.length === 0 || right.length === 0) return false;\n\tconst rightSet = new Set(right.map((tag) => tag.trim().toLowerCase()));\n\treturn left.some((tag) => rightSet.has(tag.trim().toLowerCase()));\n}\n\n/**\n * 语义合并判定 (实施计划 S3 标定定版, 三分支):\n * - 合并: cos ≥ 0.72, 或 0.55 ≤ cos < 0.72 且新 tags 与既有 tags 有重叠\n * (复合条件救回跨语言改写 — 余弦偏低但 tags 同源; 取首个满足条件的近邻);\n * - 冲突带: 0.55 ≤ cos < 0.72 且无 tag 重叠 → 双存 + conflict 关系\n * (单向声明在新 atom 上; 正确性优先于存储省略, 宁可双存不错杀);\n * - 低相似/无近邻: 普通新条目。\n * 近邻按 KNN 序传入。\n */\nfunction decideSemanticMergePlan(input: {\n\treadonly newTags: readonly string[];\n\treadonly neighbors: readonly SemanticMergeNeighborV1[];\n}): SemanticMergePlanV1 {\n\tfor (const neighbor of input.neighbors) {\n\t\tconst merged =\n\t\t\tneighbor.cosine >= SEMANTIC_MERGE_T_HIGH ||\n\t\t\t(neighbor.cosine >= SEMANTIC_MERGE_T_LOW && tagsOverlapIgnoreCase(input.newTags, neighbor.tags));\n\t\tif (merged) return { kind: \"merge\", targetMemoryId: neighbor.memoryId };\n\t}\n\tconst conflictMemoryIds = input.neighbors\n\t\t.filter((neighbor) => neighbor.cosine >= SEMANTIC_MERGE_T_LOW)\n\t\t.map((neighbor) => neighbor.memoryId);\n\treturn conflictMemoryIds.length > 0 ? { kind: \"conflict\", conflictMemoryIds } : { kind: \"new\" };\n}\n\n/**\n * 判定结论 → 新 atom 的 relations (canonical 侧合并/冲突痕迹)。canonical\n * atom 不可变且 Foundation 无\"更新 payload\"操作, 关系只在新 atom 上单向声明\n * (可发现语义由此满足): supersedes 指向被合并的旧条目, conflict 指向冲突带\n * 近邻。ids 由 (kind, target, content) 确定性派生。\n */\nfunction semanticMergeRelations(plan: SemanticMergePlanV1, content: string): readonly MemoryRelationV1[] {\n\tconst relation = (kind: \"supersedes\" | \"conflict\", targetMemoryId: string): MemoryRelationV1 => {\n\t\tconst relationId = `rel-${identityHash(`${kind}\\u0000${targetMemoryId}\\u0000${content}`)}`;\n\t\treturn {\n\t\t\trelationId,\n\t\t\tnamespace: SEMANTIC_RELATION_NAMESPACE,\n\t\t\tschemaVersion: 1,\n\t\t\tkind,\n\t\t\ttargetMemoryId,\n\t\t\trelationRevision: `r-${identityHash(relationId)}`,\n\t\t};\n\t};\n\tif (plan.kind === \"merge\") return [relation(\"supersedes\", plan.targetMemoryId)];\n\tif (plan.kind === \"conflict\") return plan.conflictMemoryIds.map((target) => relation(\"conflict\", target));\n\treturn [];\n}\n\n/** 档位预设: mode → 模型组合。off 在进入装配前已被门控拦截。 */\nfunction vectorPresetForMode(mode: Exclude<MemoryModeV1, \"off\">): MemoryVectorPresetV1 {\n\treturn mode === \"light\" ? VECTOR_PRESET_LIGHT : VECTOR_PRESET_FULL;\n}\n\n/**\n * 自定义镜像源 (失败行动清单第一项): `AGENT_FORGE_HF_MIRRORS` 逗号分隔 host\n * 列表, 排在默认镜像序列之前优先尝试。\n */\nfunction remoteHostsForEnabling(): readonly string[] {\n\tconst custom = process.env[HF_MIRRORS_ENV]\n\t\t?.split(\",\")\n\t\t.map((host) => host.trim())\n\t\t.filter((host) => host !== \"\");\n\treturn custom === undefined || custom.length === 0 ? VECTOR_REMOTE_HOSTS : [...custom, ...VECTOR_REMOTE_HOSTS];\n}\n\n/**\n * 本地检查 1: native .node sidecar 可解析 (设计 §3.2 checking)。布局:\n * onnxruntime-node/bin 下的 napi-vN/<platform>/<arch> 原生模块 — 缺失 = 安装\n * 损坏。容错: 不钉死 napi 版本号, 逐目录扫描。\n *\n * 解析基点双通道: jiti(安装店加载缝)注入的 `require` 走宿主 alias 表\n * (宿主 node_modules 副本, 店内无依赖树); 纯 ESM(源码/工作区直载)下裸\n * `require` 未定义(typeof 探测不抛 ReferenceError), 回落原生 createRequire。\n */\nfunction resolveOnnxRuntimeEntryPath(): string {\n\tif (typeof require === \"function\") {\n\t\ttry {\n\t\t\treturn require.resolve(\"onnxruntime-node\");\n\t\t} catch {\n\t\t\t// jiti 解析失败(alias 未覆盖等)→ 继续走原生解析再报结构化错误。\n\t\t}\n\t}\n\treturn createRequire(import.meta.url).resolve(\"onnxruntime-node\");\n}\n\nfunction nativeBinaryCheck(hostEntryPath?: string): string | undefined {\n\t// 两种失败要区分 (D-075 安装店路径修复后前者成为真实可达路径):\n\t// (a) 宿主未注入入口路径且插件位置自解析不到 — 不是安装损坏, 行动是宿主侧\n\t// 安装重依赖后重启会话, 不是重装插件;\n\t// (b) 路径存在但原生二进制缺失/损坏 — 保留损坏类文案。\n\tlet entryPath: string;\n\tif (hostEntryPath === undefined) {\n\t\ttry {\n\t\t\tentryPath = resolveOnnxRuntimeEntryPath();\n\t\t} catch (error) {\n\t\t\tconst detail = error instanceof Error ? error.message : String(error);\n\t\t\treturn `onnxruntime-node is not provided by the host installation and could not be resolved from the plugin location (${detail}); to enable memory features, install onnxruntime-node in the host project (npm i onnxruntime-node) and restart the session`;\n\t\t}\n\t} else {\n\t\tentryPath = hostEntryPath;\n\t}\n\ttry {\n\t\tlet packageDir = dirname(entryPath);\n\t\twhile (packageDir !== dirname(packageDir) && !existsSync(join(packageDir, \"package.json\"))) {\n\t\t\tpackageDir = dirname(packageDir);\n\t\t}\n\t\tconst binDir = join(packageDir, \"bin\");\n\t\tif (!existsSync(binDir)) return `onnxruntime native binary directory not found: ${binDir}`;\n\t\tfor (const napiEntry of readdirSync(binDir, { withFileTypes: true })) {\n\t\t\tif (!napiEntry.isDirectory() || !napiEntry.name.startsWith(\"napi-v\")) continue;\n\t\t\tconst platformDir = join(binDir, napiEntry.name, process.platform, process.arch);\n\t\t\tif (!existsSync(platformDir)) continue;\n\t\t\tconst binaries = readdirSync(platformDir).filter((file) => file.endsWith(\".node\"));\n\t\t\tif (binaries.length > 0) return undefined;\n\t\t}\n\t\treturn `onnxruntime native binary for ${process.platform}/${process.arch} not found under ${binDir}`;\n\t} catch (error) {\n\t\treturn `onnxruntime native binary could not be resolved (${error instanceof Error ? error.message : String(error)}); the installation looks damaged — reinstall the package`;\n\t}\n}\n\n/**\n * 重依赖自解析默认实现 (missingHeavyDependencyModules 生产接线): 双通道与\n * {@link resolveOnnxRuntimeEntryPath} 一致 — jiti(安装店加载缝)注入的\n * `require` 走宿主 alias 表; 纯 ESM(源码/工作区直载)下回落原生 createRequire。\n */\nfunction resolveHeavyDependencyEntryPath(specifier: string): string {\n\tif (typeof require === \"function\") {\n\t\ttry {\n\t\t\treturn require.resolve(specifier);\n\t\t} catch {\n\t\t\t// jiti 解析失败(alias 未覆盖等)→ 继续走原生解析。\n\t\t}\n\t}\n\treturn createRequire(import.meta.url).resolve(specifier);\n}\n\n/**\n * 本地检查 1b: 重依赖供给 (设计 §3.2 checking)。`@lancedb/lancedb` 与\n * `@huggingface/transformers` 逐个判定 — entries 提供宿主入口路径 = 可用\n * (不触自解析); 未提供时 resolveModule 自解析成功 = 可用, 抛错 = 计入缺失。\n * 返回缺失 specifier 列表 (空 = 通过)。纯函数零 IO; 导出仅供测试 (与宿主侧\n * buildHostDependencyAliases 同一模式), 生产接线传插件位置自解析 — 把加载步\n * 的 \"Cannot find module\" 泛化包装前置成精准缺供文案, 省掉模型下载等前置工作。\n */\nexport function missingHeavyDependencyModules(\n\tentries: { lancedb?: string; transformers?: string },\n\tresolveModule: (specifier: string) => string,\n): string[] {\n\tconst missing: string[] = [];\n\tconst supplies: readonly (readonly [specifier: string, entryPath: string | undefined])[] = [\n\t\t[\"@lancedb/lancedb\", entries.lancedb],\n\t\t[\"@huggingface/transformers\", entries.transformers],\n\t];\n\tfor (const [specifier, entryPath] of supplies) {\n\t\tif (entryPath !== undefined) continue;\n\t\ttry {\n\t\t\tresolveModule(specifier);\n\t\t} catch {\n\t\t\tmissing.push(specifier);\n\t\t}\n\t}\n\treturn missing;\n}\n\n/**\n * transformers.js 缓存布局的容错存在性检查 (设计 §3.2 手动导入路径):\n * `<cacheDir>/<modelId>/` 下需有非空 tokenizer 文件与 `onnx/` 权重文件。\n * 返回 undefined = 检查通过; \"missing\" = 目录/文件缺失或仅剩 0 字节残骸\n * (失败下载产物, 清理后进入下载阶段, 见 model-cache-hygiene); 其他字符串 =\n * 文件存在但可疑 (截断/非空过小), 属失败。\n */\nfunction modelFilesCheck(cacheDir: string, modelId: string, minWeightBytes: number): string | undefined {\n\tconst modelDir = join(cacheDir, modelId);\n\tif (!existsSync(modelDir)) return \"missing\";\n\t// tokenizer 只剩 0 字节文件 = 失败下载残骸 (真 tokenizer.json 不可能为空):\n\t// 清掉按缺失处理可重下; 非空文件不动 (手动导入布局不受影响)。\n\tconst tokenizerNames = [\"tokenizer.json\", \"tokenizer.model\"].filter((name) => existsSync(join(modelDir, name)));\n\tconst hasTokenizer = tokenizerNames.some((name) => statSync(join(modelDir, name)).size > 0);\n\tif (!hasTokenizer) {\n\t\tif (tokenizerNames.length > 0) purgeZeroByteCacheArtifacts(modelDir);\n\t\treturn \"missing\";\n\t}\n\tconst onnxDir = join(modelDir, \"onnx\");\n\tif (!existsSync(onnxDir)) return `onnx weight directory missing under ${modelDir}`;\n\tconst weights = readdirSync(onnxDir).filter((name) => name.includes(\".onnx\"));\n\tif (weights.length === 0) return `no onnx weight file under ${onnxDir}`;\n\tconst largest = Math.max(...weights.map((name) => statSync(join(onnxDir, name)).size));\n\tif (largest < minWeightBytes) {\n\t\treturn `largest onnx weight under ${onnxDir} is ${largest} bytes, below the expected minimum ${minWeightBytes} — the download looks truncated`;\n\t}\n\treturn undefined;\n}\n\n/**\n * 本地检查 3: 磁盘余量 (设计 §3.2 checking, node:fs statfs, 零新依赖)。\n * 以 agentDir 所在卷为准 (cacheDir 同卷创建)。\n */\nasync function diskHeadroomCheck(agentDir: string, minimumBytes: number): Promise<string | undefined> {\n\tlet freeBytes: number;\n\ttry {\n\t\tconst stats = await statfs(agentDir);\n\t\tfreeBytes = stats.bfree * stats.bsize;\n\t} catch (error) {\n\t\treturn `disk free space could not be determined for ${agentDir} (${error instanceof Error ? error.message : String(error)})`;\n\t}\n\tif (freeBytes < minimumBytes) {\n\t\treturn `disk free space is ${freeBytes} bytes, below the required ${minimumBytes}`;\n\t}\n\treturn undefined;\n}\n\n/** enabling 失败的五选一行动清单 (设计 §3.2 硬性要求)。 */\nfunction enablingActionList(modelCacheDir: string, currentMode: MemoryModeV1): readonly string[] {\n\treturn [\n\t\t`Switch to a reachable mirror: set ${HF_MIRRORS_ENV} to a comma-separated host list (tried before the defaults)`,\n\t\t`Install the heavy dependencies in the host project (npm i onnxruntime-node @lancedb/lancedb @huggingface/transformers) so the host can supply them via hostDependencyPaths, then restart the session`,\n\t\t`Download the models manually and place them under ${modelCacheDir} (layout: <modelId>/tokenizer.json + <modelId>/onnx/model*.onnx)`,\n\t\tcurrentMode === \"full\"\n\t\t\t? 'Downgrade to memory.mode \"light\" (much smaller models)'\n\t\t\t: 'Keep memory.mode \"light\" and retry with a reachable mirror',\n\t\t'Give up: leave memory.mode \"off\" (the default) so the memory family stays unregistered',\n\t];\n}\n\n/** One unresolvably competing preference, surfaced verbatim instead of a silent pick. */\ninterface RecallConflictV1 {\n\treadonly subject: MemoryPreferenceEnvelopeV1[\"subject\"];\n\treadonly key: string;\n\treadonly candidates: readonly {\n\t\treadonly memoryId: string;\n\t\treadonly statement: string;\n\t\treadonly value: JsonValue;\n\t\treadonly evidenceClass: MemoryPreferenceEnvelopeV1[\"evidence\"][\"class\"];\n\t\treadonly confidence: number;\n\t}[];\n\treadonly explanation: readonly string[];\n}\n\n/**\n * Parses the optional recall context (scenes/projects/tasks) into a preference\n * resolution context. Returns undefined when absent so the no-context recall\n * path stays byte-identical to the pre-disambiguation behavior.\n */\nfunction parseRecallContext(value: unknown): PreferenceContextV1 | undefined {\n\tif (value === undefined) return undefined;\n\tif (!isPlainObject(value)) throw new Error(\"context must be an object\");\n\tconst stringArray = (field: \"scenes\" | \"projects\" | \"tasks\"): readonly string[] | undefined => {\n\t\tconst entries: unknown = value[field];\n\t\tif (entries === undefined) return undefined;\n\t\tif (!Array.isArray(entries)) throw new Error(`context.${field} must be an array of strings`);\n\t\tfor (const entry of entries) {\n\t\t\tif (typeof entry !== \"string\") throw new Error(`context.${field} entries must be strings`);\n\t\t}\n\t\treturn entries;\n\t};\n\tconst scenes = stringArray(\"scenes\");\n\tconst projects = stringArray(\"projects\");\n\tconst tasks = stringArray(\"tasks\");\n\treturn {\n\t\t...(scenes === undefined ? {} : { scenes }),\n\t\t...(projects === undefined ? {} : { projects }),\n\t\t...(tasks === undefined ? {} : { tasks }),\n\t};\n}\n\n/**\n * Context-scoped preference post-processing over the recalled facts: preference\n * atoms compete per (subject, key); the resolver drops out-of-scope preferences\n * and deterministically ranks the rest, and an unconfirmed ranking goes through\n * the scope-specificity disambiguator. Whatever remains unresolvable is\n * returned as an explicit conflict block — the recall never silently picks\n * between competing stored values. Non-preference facts pass through untouched.\n */\nfunction applyPreferenceContext(input: {\n\treadonly facts: readonly MemoryFact[];\n\treadonly context: PreferenceContextV1;\n\treadonly resolver: PreferenceResolverV1;\n\treadonly disambiguator: PreferenceDisambiguatorV1;\n\treadonly atomOf: (memoryId: string) => MemoryAtomV1 | undefined;\n}): { readonly facts: readonly MemoryFact[]; readonly conflicts: readonly RecallConflictV1[] } {\n\tconst { facts, context, resolver, disambiguator, atomOf } = input;\n\tconst preferenceAtoms: MemoryAtomV1[] = [];\n\tconst atomsByGroup = new Map<string, MemoryAtomV1[]>();\n\tconst preferenceMemoryIds = new Set<string>();\n\tfor (const fact of facts) {\n\t\tconst atom = atomOf(fact.memoryId);\n\t\tif (atom === undefined || atom.memoryKind !== \"preference\" || atom.preference === undefined) continue;\n\t\tpreferenceMemoryIds.add(fact.memoryId);\n\t\tpreferenceAtoms.push(atom);\n\t\tconst groupKey = `${atom.preference.subject}\\u0000${atom.preference.key}`;\n\t\tconst group = atomsByGroup.get(groupKey) ?? [];\n\t\tgroup.push(atom);\n\t\tatomsByGroup.set(groupKey, group);\n\t}\n\tif (preferenceMemoryIds.size === 0) return { facts, conflicts: [] };\n\n\tconst keptMemoryIds = new Set<string>();\n\tconst conflicts: RecallConflictV1[] = [];\n\tfor (const decision of resolver.resolve(preferenceAtoms, context)) {\n\t\tconst groupAtoms = atomsByGroup.get(`${decision.subject}\\u0000${decision.key}`) ?? [];\n\t\tif (decision.status === \"applied\") {\n\t\t\tif (decision.winnerMemoryId === undefined) {\n\t\t\t\tthrow new Error(`applied preference decision without a winnerMemoryId: ${JSON.stringify(decision)}`);\n\t\t\t}\n\t\t\tkeptMemoryIds.add(decision.winnerMemoryId);\n\t\t\tcontinue;\n\t\t}\n\t\tif (decision.status === \"instruction_override\") {\n\t\t\t// No stored fact survives a current-instruction override (this slice has\n\t\t\t// no input source for currentInstructions, so the branch stays inert).\n\t\t\tcontinue;\n\t\t}\n\t\tif (decision.status === \"conflict_unconfirmed\") {\n\t\t\tconst outcome = disambiguator.disambiguate(\n\t\t\t\tgroupAtoms.map((atom) => ({\n\t\t\t\t\tmemoryId: atom.memoryId,\n\t\t\t\t\tpreferredValue: atom.preference!.preferredValue,\n\t\t\t\t\tscope: atom.preference!.scope,\n\t\t\t\t\tevidenceClass: atom.preference!.evidence.class,\n\t\t\t\t\tapplicabilityConfidence: atom.preference!.applicabilityConfidence,\n\t\t\t\t})),\n\t\t\t\tcontext,\n\t\t\t);\n\t\t\tif (outcome.status === \"resolved\") {\n\t\t\t\tkeptMemoryIds.add(outcome.winnerMemoryId);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconflicts.push({\n\t\t\t\tsubject: decision.subject,\n\t\t\t\tkey: decision.key,\n\t\t\t\tcandidates: groupAtoms.map((atom) => {\n\t\t\t\t\tconst preference = atom.preference!;\n\t\t\t\t\treturn {\n\t\t\t\t\t\tmemoryId: atom.memoryId,\n\t\t\t\t\t\tstatement: unwrapEgressStatement(statementOf(atom.payload)),\n\t\t\t\t\t\tvalue: preference.preferredValue,\n\t\t\t\t\t\tevidenceClass: preference.evidence.class,\n\t\t\t\t\t\tconfidence: preference.applicabilityConfidence,\n\t\t\t\t\t};\n\t\t\t\t}),\n\t\t\t\texplanation: [...outcome.explanation],\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\t\tthrow new Error(`unexpected preference decision status: ${JSON.stringify(decision)}`);\n\t}\n\n\treturn {\n\t\tfacts: facts.filter((fact) => !preferenceMemoryIds.has(fact.memoryId) || keptMemoryIds.has(fact.memoryId)),\n\t\tconflicts,\n\t};\n}\n\nfunction tagFacetsOf(atom: MemoryAtomV1): string[] {\n\treturn atom.facets\n\t\t.filter((facet) => facet.namespace === TAG_FACET_NAMESPACE && facet.key === \"tag\")\n\t\t.map((facet) => facet.value);\n}\n\nfunction entryToToolEntry(entry: SuiteMemoryListEntryV1): Record<string, unknown> {\n\treturn {\n\t\tmemoryId: entry.memoryId,\n\t\tkind: entry.memoryKind,\n\t\tstatement: statementOf(entry.payload),\n\t\toccurredAt: entry.occurredAt,\n\t\t...(entry.preference === undefined\n\t\t\t? {}\n\t\t\t: {\n\t\t\t\t\tpreference: {\n\t\t\t\t\t\tsubject: entry.preference.subject,\n\t\t\t\t\t\tkey: entry.preference.key,\n\t\t\t\t\t\tscopeLevel: entry.preference.scopeLevel,\n\t\t\t\t\t\tcrossSuiteVisible: entry.preference.crossSuiteVisible,\n\t\t\t\t\t},\n\t\t\t\t}),\n\t};\n}\n\nconst writeSchema = Type.Object({\n\tcontent: Type.String({\n\t\tdescription: \"The memory content: one self-contained fact or preference statement\",\n\t\tmaxLength: MEMORY_CONTENT_MAX_LENGTH,\n\t}),\n\tkind: Type.Union([Type.Literal(\"fact\"), Type.Literal(\"preference\")], {\n\t\tdescription: \"fact = a durable piece of information; preference = a user preference (requires subject)\",\n\t}),\n\tsubject: Type.Optional(\n\t\tType.Union([Type.Literal(\"user\"), Type.Literal(\"project\"), Type.Literal(\"task\"), Type.Literal(\"environment\")], {\n\t\t\tdescription: \"Required when kind is preference: whose preference this is\",\n\t\t}),\n\t),\n\ttags: Type.Optional(\n\t\tType.Array(Type.String({ minLength: 1 }), {\n\t\t\tdescription:\n\t\t\t\t\"Optional short keywords; stored with the memory, full-text searchable and shown in memory_list; at most 8\",\n\t\t\tmaxItems: MAX_TAGS,\n\t\t}),\n\t),\n});\n\nconst recallSchema = Type.Object({\n\tquery: Type.String({\n\t\tdescription:\n\t\t\t\"Natural-language query; recall fuses semantic (vector) and full-text (BM25) matching and reranks the result. An exact memoryId retrieves that memory directly\",\n\t}),\n\tlimit: Type.Optional(\n\t\tType.Integer({ description: \"Maximum number of memories to return (1-100)\", minimum: 1, maximum: 100 }),\n\t),\n\tcontext: Type.Optional(\n\t\tType.Object(\n\t\t\t{\n\t\t\t\tscenes: Type.Optional(Type.Array(Type.String())),\n\t\t\t\tprojects: Type.Optional(Type.Array(Type.String())),\n\t\t\t\ttasks: Type.Optional(Type.Array(Type.String())),\n\t\t\t},\n\t\t\t{\n\t\t\t\tdescription:\n\t\t\t\t\t\"Current scope context; when present, recalled preferences are resolved against it (out-of-scope preferences drop, competing values resolve by scope specificity or surface as explicit conflicts)\",\n\t\t\t},\n\t\t),\n\t),\n});\n\nconst listSchema = Type.Object({\n\tlimit: Type.Optional(\n\t\tType.Integer({ description: \"Maximum number of entries to list (1-100)\", minimum: 1, maximum: 100 }),\n\t),\n});\n\nconst forgetSchema = Type.Object({\n\tmemoryId: Type.String({\n\t\tdescription: \"The memoryId of the memory to forget, as returned by memory_write or memory_list\",\n\t}),\n});\n\n/**\n * Creates the builtin memory capability: four tools (memory_write,\n * memory_recall, memory_list, memory_forget) over a per-session Foundation\n * stack whose durable replica is the per-domain JSONL ledger under\n * `<agentDir>/memory/<owner>/ledger-<domain>.jsonl`, resolved lazily at each\n * tool call (统一修复轮 B1).\n *\n * `memory.mode` gates the whole family (2.4d6 S1): a resolved mode of \"off\"\n * registers NOTHING (zero tools, zero commands, zero events) — the factory\n * returns an empty registration list and logs the one-line disabled\n * declaration. resolveBuiltinCapabilities intercepts earlier at the assembly\n * level (设计 §3.2 \"禁用 = 不注册\").\n *\n * `override` is the test-only seam (deterministic mock AI 门禁): full vector\n * components skip the real transformers/lancedb assembly entirely; enabling\n * seams keep the real assembly but replace the transformers loader.\n */\nexport function createMemoryCapability(\n\tapi: CapabilityAPI,\n\tcontext: MemoryCapabilityContext,\n\toverride?: MemoryCapabilityOverrideV1,\n): readonly DisposableRegistration[] {\n\tconst harness = createMemoryCapabilityHarness(api, context, override);\n\treturn harness.registrations;\n}\n\n/** Test-only harness over the capability: registration list plus state-work hooks. */\nexport interface MemoryVectorCapabilityHarnessV1 {\n\treadonly registrations: readonly DisposableRegistration[];\n\t/** Resolves after every vector op queued so far (reconcile/upsert/purge) settled. */\n\tsettleVectorWork(): Promise<void>;\n\tvectorState(): \"off\" | MemoryCapabilityStateV1;\n\t/**\n\t * Resolves once the enabling attempt settles (immediately for injected\n\t * components): ready, or the structured failure (reason + action list).\n\t */\n\tenablingOutcome(): Promise<\"ready\" | MemoryEnablingFailureV1>;\n}\n\n/**\n * Test-only assembly variant: the capability with an injected override (full\n * vector components skip real assembly; enabling seams redirect the model\n * loader on the real path). Public tool names, schemas, and return shapes are\n * identical to {@link createMemoryCapability}.\n */\nexport function createCapabilityWithVectorComponents(\n\tapi: CapabilityAPI,\n\tcontext: MemoryCapabilityContext,\n\toverride: MemoryCapabilityOverrideV1,\n): MemoryVectorCapabilityHarnessV1 {\n\t// Tool-face test stubs hand-write partial CapabilityAPIs that predate the\n\t// D-071 auto-recall hook; default registerLoopHook to a no-op so those\n\t// tests keep exercising just the tool face. Real CapabilityAPIs always\n\t// carry the method (production path never hits this default) — the `in`\n\t// check is runtime-only because the stubs bypass the declared type.\n\tconst apiWithHooks: CapabilityAPI =\n\t\t\"registerLoopHook\" in api\n\t\t\t? api\n\t\t\t: Object.assign(Object.create(null) as CapabilityAPI, api, {\n\t\t\t\t\tregisterLoopHook: (): DisposableRegistration => ({\n\t\t\t\t\t\tid: \"test:noop-loop-hook\",\n\t\t\t\t\t\tkind: \"loop-hook\",\n\t\t\t\t\t\tdispose() {},\n\t\t\t\t\t}),\n\t\t\t\t});\n\treturn createMemoryCapabilityHarness(apiWithHooks, context, override);\n}\n\nfunction createMemoryCapabilityHarness(\n\tapi: CapabilityAPI,\n\tcontext: MemoryCapabilityContext,\n\toverride?: MemoryCapabilityOverrideV1,\n): MemoryVectorCapabilityHarnessV1 {\n\tassertNonEmptyString(context.agentDir, \"MemoryCapabilityContext.agentDir\");\n\tconst now = (): number => Date.now();\n\tconst sessionId = api.session?.getSessionId() ?? \"no-session\";\n\n\t// 全局档位解析 (2.4d6 S1): context 显式 → env → settings.json → 默认 off。\n\t// 非法 env/settings 值 fail-closed 回 off 并各告警一次; off = 家族整体不注册\n\t// (零工具零命令零事件)。\n\tconst modeResolution = resolveMemoryMode(context.memory?.mode, undefined, context.memory?.settingsMode);\n\tif (modeResolution.invalidEnvValue !== undefined) {\n\t\tapi.logger?.warn(\n\t\t\t`invalid AGENT_FORGE_MEMORY_MODE value \"${modeResolution.invalidEnvValue}\"; memory stays off (fail-closed)`,\n\t\t);\n\t}\n\tif (modeResolution.invalidSettingsValue !== undefined) {\n\t\tapi.logger?.warn(\n\t\t\t`invalid memory.mode setting \"${modeResolution.invalidSettingsValue}\"; memory stays off (fail-closed)`,\n\t\t);\n\t}\n\tif (modeResolution.mode === \"off\") {\n\t\tapi.logger?.info(\"memory disabled (mode=off)\");\n\t\treturn {\n\t\t\tregistrations: [],\n\t\t\tsettleVectorWork: async () => {},\n\t\t\tvectorState: () => \"off\",\n\t\t\tenablingOutcome: async () => \"ready\",\n\t\t};\n\t}\n\tconst memoryMode = modeResolution.mode;\n\tconst vectorComponentsOverride: MemoryCapabilityOverrideV1 | undefined =\n\t\toverride ?? context.vectorComponents ?? context.storage?.vectorComponents;\n\tconst injectedComponents =\n\t\tvectorComponentsOverride !== undefined && isVectorComponentsOverride(vectorComponentsOverride)\n\t\t\t? vectorComponentsOverride\n\t\t\t: undefined;\n\tconst enablingOverride =\n\t\tvectorComponentsOverride !== undefined && !isVectorComponentsOverride(vectorComponentsOverride)\n\t\t\t? vectorComponentsOverride\n\t\t\t: undefined;\n\n\t// Store: one per-capability-instance singleton. Atoms of every domain this\n\t// session touches live here together — the atom `suiteId` field carries the\n\t// read boundary (suite queries filter through it; the ledger is only the\n\t// per-domain durable replica). The injected storeFactory (扩展位 A1) replaces\n\t// the default store wholesale: the implementation owns retention semantics.\n\tconst retentionRegistry = createFirstPartyRetentionRegistry();\n\tconst store =\n\t\tcontext.storage?.storeFactory?.({\n\t\t\tagentDir: context.agentDir,\n\t\t\towner: BUILTIN_MEMORY_OWNER,\n\t\t\tnow,\n\t\t}) ?? createMemoryStore({ retentionRegistry, now });\n\n\t// Durable replica per domain, created and loaded lazily on the first tool\n\t// call in that domain (统一修复轮 B1 复审修复): the sdk appends the suite\n\t// binding entry only after the session constructor ran this factory, so the\n\t// domain (and with it the ledger to replay) is only known at call time.\n\t// Write identity is deterministic: observationId `memory_write:<sessionId>:<n>`\n\t// hashes into the memoryId. The counter is instance-local, so a resumed session\n\t// re-derives its floor from the replayed ledger (resolveDomain): a persisted\n\t// observationId of this session marks its sequence as taken — the store rejects\n\t// a regenerated memoryId (canonical atoms are immutable), which would fail every\n\t// replayed-sequence write after resume (write → dispose → resume → write).\n\tconst writeSequencePrefix = `memory_write:${sessionId}:`;\n\tconst writeSequenceOf = (observationId: string): number | undefined => {\n\t\tif (!observationId.startsWith(writeSequencePrefix)) return undefined;\n\t\tconst sequence = observationId.slice(writeSequencePrefix.length);\n\t\treturn /^\\d+$/.test(sequence) ? Number(sequence) : undefined;\n\t};\n\tlet writeSequence = 0;\n\n\tconst domainLedgers = new Map<string, DurableMemoryLedgerV1>();\n\tconst resolveDomain = (): {\n\t\treadonly domain: SuiteMemoryDomainV1;\n\t\treadonly domainName: string;\n\t\treadonly ledger: DurableMemoryLedgerV1;\n\t} => {\n\t\tconst domain = resolveSessionDomain(api);\n\t\tconst domainName = suiteMemoryDomainName(domain);\n\t\tlet ledger = domainLedgers.get(domainName);\n\t\tif (ledger === undefined) {\n\t\t\tledger =\n\t\t\t\tcontext.storage?.ledgerFactory?.({\n\t\t\t\t\tagentDir: context.agentDir,\n\t\t\t\t\towner: BUILTIN_MEMORY_OWNER,\n\t\t\t\t\tdomain: domainName,\n\t\t\t\t\tnow,\n\t\t\t\t}) ??\n\t\t\t\tcreateDurableMemoryLedger({\n\t\t\t\t\tpath: join(context.agentDir, \"memory\", BUILTIN_MEMORY_OWNER, `ledger-${domainName}.jsonl`),\n\t\t\t\t\tnow,\n\t\t\t\t});\n\t\t\tif (domain.kind === \"legacy\") {\n\t\t\t\tapi.logger?.info(\"session has no suite binding; memory falls back to the legacy domain\", {\n\t\t\t\t\tsessionId: api.session?.getSessionId() ?? \"unknown\",\n\t\t\t\t});\n\t\t\t}\n\t\t\t// First load in this domain: replay the durable atoms into the store.\n\t\t\t// Sequences persisted by earlier instances of this session raise the\n\t\t\t// write-sequence floor before any new write can regenerate their ids.\n\t\t\tconst replay = ledger.load();\n\t\t\tfor (const atom of replay.atoms) {\n\t\t\t\tconst persistedSequence = writeSequenceOf(atom.observationId);\n\t\t\t\tif (persistedSequence !== undefined && persistedSequence > writeSequence) writeSequence = persistedSequence;\n\t\t\t\ttry {\n\t\t\t\t\tstore.commit(atom);\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// A ledger atom the current store cannot accept (e.g. an unregistered\n\t\t\t\t\t// retention mode) must not fail the session — skip with a diagnostic.\n\t\t\t\t\tapi.logger?.warn(\"memory ledger atom could not be replayed into the store\", {\n\t\t\t\t\t\tmemoryId: atom.memoryId,\n\t\t\t\t\t\terror: error instanceof Error ? error.message : String(error),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (replay.corruptedLines > 0 || replay.duplicateSkipped > 0 || replay.unreadable) {\n\t\t\t\tapi.logger?.warn(\"memory ledger loaded with diagnostics\", {\n\t\t\t\t\tdomain: domainName,\n\t\t\t\t\tcorruptedLines: replay.corruptedLines,\n\t\t\t\t\tduplicateSkipped: replay.duplicateSkipped,\n\t\t\t\t\tunreadable: replay.unreadable,\n\t\t\t\t});\n\t\t\t}\n\t\t\tdomainLedgers.set(domainName, ledger);\n\t\t\t// 启动对账: 首个域的 ledger 重放完成后一次性触发 (fire-and-forget,\n\t\t\t// 不阻塞本工具调用; 失败只影响向量通道, 不影响锚点通道)。\n\t\t\tvectorReconcileOnce();\n\t\t}\n\t\treturn { domain, domainName, ledger };\n\t};\n\n\tconst machine = createMemoryCandidateMachine({ store, now });\n\tconst lifecycle = createMemoryLifecycleManager({\n\t\tpolicy: {\n\t\t\tpolicyVersion: \"lifecycle@1\",\n\t\t\thalfLifeMs: { session: 86_400_000, cycle: 604_800_000, \"long-term\": 2_592_000_000 },\n\t\t\tstaleAfterMs: { session: 172_800_000, cycle: 1_209_600_000, \"long-term\": 5_184_000_000 },\n\t\t\tarchiveAfterMs: { session: 604_800_000, cycle: 5_184_000_000, \"long-term\": 15_552_000_000 },\n\t\t\tbaselineAttention: 0.5,\n\t\t},\n\t\tnow,\n\t});\n\tconst scheduler = createMemoryScheduler({ now });\n\tscheduler.acquireForInstance({ instanceId: sessionId, owner: BUILTIN_MEMORY_OWNER });\n\t// 非工具召回路径设施 (2.4d6 S4 家族收口取证结论): 生产工具召回 (memory_recall\n\t// 工具与 auto-recall 钩子, 后者直连同一 recallExecute, D-071 自 sdk 迁入)\n\t// 只走 recallSemantically 语义管线; index → recallAgent →\n\t// schedulerApi.recall 这条链在本 capability 内不再被工具面调用 (schedulerApi\n\t// 的生产使用仅剩写入路径的 submitObservation/submitCandidate)。保留原因:\n\t// MemorySchedulerApiV1 facade 契约 (recall + egress 门)、recall-agent 与\n\t// recall-index 是公共 API (src/index.ts 导出) 和 memory-testkit 之上的测试\n\t// 设施 (scheduler-api / memory-suite-scope / coverage-branch-memory-recall\n\t// 等测试的直接消费者) — 有存续消费者, 非死代码。\n\tconst index = createMemoryRecallIndex({ store });\n\tconst recallAgent = createMemoryRecallAgent({\n\t\tscheduler,\n\t\tindex,\n\t\tpolicyDefaults: {\n\t\t\tcandidateBudget: 1000,\n\t\t\tmodelInspectionLimit: 20,\n\t\t\tfinalResultLimit: 20,\n\t\t\tmaxRelationHops: 2,\n\t\t\tmaxModelCalls: 3,\n\t\t\tmaxOutputTokens: 4000,\n\t\t\tmaxOutputBytes: 16_000,\n\t\t\tmaxRounds: 3,\n\t\t},\n\t\thostLimits: {\n\t\t\tcandidateBudget: 5000,\n\t\t\tmodelInspectionLimit: 100,\n\t\t\tfinalResultLimit: 100,\n\t\t\tmaxRelationHops: 5,\n\t\t\tmaxModelCalls: 10,\n\t\t\tmaxOutputTokens: 100_000,\n\t\t\tmaxOutputBytes: 1_000_000,\n\t\t\tmaxRounds: 10,\n\t\t},\n\t\tnow,\n\t});\n\tconst replicaRegistry = createMemoryReplicaRegistry();\n\treplicaRegistry.register({\n\t\treplicaId: \"memory-ledger\",\n\t\tkind: \"canonical\",\n\t\townerScope: BUILTIN_MEMORY_OWNER,\n\t\tcontractVersion: MEMORY_CONTRACT_VERSION,\n\t\thealthy: true,\n\t} satisfies MemoryReplicaV1);\n\t// 向量投影副本 (混合检索工程化, D-035): 恒注册 — purge 批次以向量索引确认为\n\t// 副本契约的一部分; 通道 off/disabled 时 index 分派为 no-op (索引不存在,\n\t// 无行可清), 行为见 vectorPurgeMemories。\n\treplicaRegistry.register({\n\t\treplicaId: \"memory-vector-index\",\n\t\tkind: \"index\",\n\t\tcontractVersion: MEMORY_CONTRACT_VERSION,\n\t\townerScope: BUILTIN_MEMORY_OWNER,\n\t\thealthy: true,\n\t} satisfies MemoryReplicaV1);\n\tconst purgeGate = createMemoryPurgeGate({ registry: replicaRegistry, now });\n\tconst purgeJournal = createMemoryPurgeJournal();\n\tconst egressPolicy = createMemoryEgressPolicy({\n\t\tpolicyVersion: \"egress@1\",\n\t\tallowedSourceKinds: [\"tool\", \"entry\"],\n\t\tredactedPayloadFields: [],\n\t});\n\t// schedulerApi.recall 在生产工具面无调用点 (角色见上方\"非工具召回路径设施\"\n\t// 注释); 本 capability 的生产使用仅 submitObservation/submitCandidate (写入)。\n\tconst schedulerApi = createMemorySchedulerApi({\n\t\trecallAgent,\n\t\tmachine,\n\t\tlifecycle,\n\t\tpurgeGate,\n\t\tegressPolicy,\n\t\tfactInfoLookup: (memoryId) => {\n\t\t\tconst record = store.get(memoryId, { owner: BUILTIN_MEMORY_OWNER });\n\t\t\tif (!record) return undefined;\n\t\t\treturn {\n\t\t\t\tsourceRefs: record.atom.sourceRefs,\n\t\t\t\tpayload: record.atom.payload as Record<string, unknown>,\n\t\t\t};\n\t\t},\n\t});\n\t// Preference resolution/disambiguation over context-scoped recalls (2.4c/2.4d2):\n\t// stateless and deterministic — one instance per capability suffices.\n\tconst preferenceResolver = createPreferenceResolver();\n\tconst preferenceDisambiguator = createPreferenceDisambiguator();\n\t// Relation diffusion over the recall seeds (记忆网络接线, 2.4d4): a factory-\n\t// scoped deterministic network with one enabled one-hop adapter; disabled\n\t// adapters yield an explicit `disabled` result instead of silent degradation.\n\tconst memoryNetwork = createMemoryNetwork({\n\t\tadapters: [createRelationDiffusionAdapter({ maxHops: 1 })],\n\t});\n\n\t// ---- 语义召回通道 (2.4d6 S1/S2; 召回唯一检索路径) ----\n\t// 状态机 (设计 §3.2): enabling = 检查/加载进行中 (惰性触发: 首次向量操作\n\t// 时真正执行); ready = 可用; degraded = ready 后运行期故障 (可自愈回\n\t// ready); failed = enabling 失败, 本实例内终态 (mode 回退 off, 记忆工具\n\t// 返回携带行动清单的不可用 packet, 不再重试)。所有写侧操作经 vectorQueue\n\t// 串行化, 避免 reconcile 与写入埋点在 LanceDB 建表上竞态; recall 等待\n\t// enabling 单飞完成, 不与后台对账并发读索引。transformers/lancedb 的动态\n\t// import 只出现在 vectorEnsure 的装配分支里 (type-position 引类型), 未\n\t// 触发时不加载任何重依赖。\n\tlet vectorState: MemoryCapabilityStateV1 = \"enabling\";\n\tlet vectorChannel: ActiveVectorChannelV1 | undefined;\n\tlet enablingFailure: MemoryEnablingFailureV1 | undefined;\n\tlet enablingSettled: (() => void) | undefined;\n\tconst enablingSettledPromise = new Promise<\"ready\" | MemoryEnablingFailureV1>((resolve) => {\n\t\tenablingSettled = () => resolve(enablingFailure ?? \"ready\");\n\t});\n\tif (injectedComponents !== undefined) {\n\t\t// 测试注入: 直接 ready, 跳过真实装配与档位解析。\n\t\tvectorState = \"ready\";\n\t\tvectorChannel = {\n\t\t\tembed: injectedComponents.embedProvider,\n\t\t\treranker: injectedComponents.reranker,\n\t\t\tindex: injectedComponents.vectorIndex,\n\t\t\tqueryPrefix: injectedComponents.queryPrefix ?? \"\",\n\t\t\tembeddingModelId: injectedComponents.embedProvider.modelId,\n\t\t};\n\t\tenablingSettled?.();\n\t}\n\tconst vectorEnabled = (): boolean => vectorState !== \"failed\";\n\tlet vectorInitPromise: Promise<ActiveVectorChannelV1 | undefined> | undefined;\n\tlet vectorQueue: Promise<unknown> = Promise.resolve();\n\t/** 精排组件失败标记: reranker 加载/调用失败后本实例内跳过精排 (RRF 序)。 */\n\tlet vectorRerankerUnavailable = false;\n\t/** 投影写/清失败的一次性降级告警标记 (重复失败由启动对账修复, 不刷屏)。 */\n\tlet vectorDegradedWarned = false;\n\n\tconst vectorEnqueue = <T>(operation: () => Promise<T>): Promise<T> => {\n\t\tconst next = vectorQueue.then(operation, operation);\n\t\t// 队尾永不满仓 reject: 前序失败不阻断后续操作, 错误由操作自己消化。\n\t\tvectorQueue = next.then(\n\t\t\t() => undefined,\n\t\t\t() => undefined,\n\t\t);\n\t\treturn next;\n\t};\n\n\t/**\n\t * 开启状态机 (设计 §3.2, 惰性单飞): 本地检查 (native .node 可解析 → 重依赖\n\t * 供给 (lancedb/transformers: 宿主入口路径或插件位置自解析) → 模型文件存在\n\t * 与大小 → 磁盘余量) → transformers 加载 (自带下载, 进度周期性\n\t * 落一行日志) → 冒烟 (embed 一条 + getStoredModelId 与档位比对) → ready。\n\t * 任一步失败 → failed (原因 + 五选一行动清单), mode 记录回退 off, 本实例\n\t * 不再重试。\n\t */\n\tconst vectorEnsure = (): Promise<ActiveVectorChannelV1 | undefined> => {\n\t\tif (vectorState === \"ready\" || vectorState === \"degraded\") return Promise.resolve(vectorChannel);\n\t\tif (vectorState === \"failed\") return Promise.resolve(undefined);\n\t\tif (vectorInitPromise !== undefined) return vectorInitPromise;\n\t\tvectorInitPromise = (async (): Promise<ActiveVectorChannelV1 | undefined> => {\n\t\t\tconst preset = vectorPresetForMode(memoryMode);\n\t\t\tconst modelCacheDir = join(context.agentDir, \"memory\", VECTOR_MODEL_CACHE_DIRNAME);\n\t\t\tconst failEnabling = (reason: string): undefined => {\n\t\t\t\tvectorState = \"failed\";\n\t\t\t\tenablingFailure = { reason, actions: enablingActionList(modelCacheDir, memoryMode) };\n\t\t\t\tvectorInitPromise = undefined;\n\t\t\t\tapi.logger?.warn(`memory enabling failed: ${reason} — actions: ${enablingFailure.actions.join(\" | \")}`, {\n\t\t\t\t\tmode: memoryMode,\n\t\t\t\t});\n\t\t\t\tenablingSettled?.();\n\t\t\t\treturn undefined;\n\t\t\t};\n\t\t\tapi.logger?.info(`memory enabling started (mode=${memoryMode}, embedding=${preset.embeddingModelId})`);\n\t\t\t// 检查 1: native .node sidecar (纯本地, 零网络)。宿主注入路径优先\n\t\t\t// (安装店副本自解析不可达, 见 MemoryCapabilityContext.hostOnnxRuntimeEntryPath)。\n\t\t\tconst nativeProblem = nativeBinaryCheck(context.hostOnnxRuntimeEntryPath);\n\t\t\tif (nativeProblem !== undefined) return failEnabling(nativeProblem);\n\t\t\t// 检查 1b: 重依赖供给。宿主入口路径优先; 未提供时插件位置自解析,\n\t\t\t// 两者都不可达 = 原本要到加载步才以 \"Cannot find module\" 泛化包装\n\t\t\t// 失败的根因, 前置到模型下载之前以 onnx 缺供分支同构的精准文案报告。\n\t\t\t// 路径已提供但包损坏的真实失败仍走加载步, 由泛化包装保留原始错误。\n\t\t\tconst missingHeavyDependencies = missingHeavyDependencyModules(\n\t\t\t\t{ lancedb: context.hostLancedbEntryPath, transformers: context.hostTransformersEntryPath },\n\t\t\t\tresolveHeavyDependencyEntryPath,\n\t\t\t);\n\t\t\tif (missingHeavyDependencies.length > 0) {\n\t\t\t\treturn failEnabling(\n\t\t\t\t\t`${missingHeavyDependencies.join(\", \")} are not provided by the host installation and could not be resolved from the plugin location; to enable memory features, install them in the host project (npm i ${missingHeavyDependencies.join(\" \")}) and restart the session`,\n\t\t\t\t);\n\t\t\t}\n\t\t\t// 检查 2: 模型文件 (缺失 = 进入下载; 存在但截断 = 失败提示手动导入)。\n\t\t\tconst embeddingProblem = modelFilesCheck(\n\t\t\t\tmodelCacheDir,\n\t\t\t\tpreset.embeddingModelId,\n\t\t\t\tMODEL_MIN_WEIGHT_BYTES[memoryMode],\n\t\t\t);\n\t\t\tconst rerankerProblem = modelFilesCheck(modelCacheDir, VECTOR_RERANKER_MODEL_ID, RERANKER_MIN_WEIGHT_BYTES);\n\t\t\tconst modelsMissing =\n\t\t\t\tembeddingProblem === \"missing\" || rerankerProblem === \"missing\"\n\t\t\t\t\t? `${preset.embeddingModelId}${rerankerProblem === \"missing\" ? ` + ${VECTOR_RERANKER_MODEL_ID}` : \"\"}`\n\t\t\t\t\t: undefined;\n\t\t\tif (embeddingProblem !== undefined && embeddingProblem !== \"missing\") return failEnabling(embeddingProblem);\n\t\t\tif (rerankerProblem !== undefined && rerankerProblem !== \"missing\") return failEnabling(rerankerProblem);\n\t\t\tif (modelsMissing !== undefined) {\n\t\t\t\tapi.logger?.info(\n\t\t\t\t\t`memory enabling: model files missing (${modelsMissing}); downloading via configured mirrors`,\n\t\t\t\t);\n\t\t\t}\n\t\t\t// 检查 3: 磁盘余量 (light ≥ 500MB / full ≥ 2GB)。\n\t\t\tconst diskProblem = await diskHeadroomCheck(context.agentDir, DISK_HEADROOM_MIN_BYTES[memoryMode]);\n\t\t\tif (diskProblem !== undefined) return failEnabling(diskProblem);\n\t\t\t// 加载: transformers/lancedb 的动态 import 都在这些模块内部, 这里只做\n\t\t\t// 模块级懒加载, 保持未触发会话零重依赖。进度周期性落一行 (下载可达\n\t\t\t// 数百 MB), 完成后清除。\n\t\t\tconst progressTimer = setInterval(() => {\n\t\t\t\tapi.logger?.info(\"memory enabling still in progress (downloading/loading models)\");\n\t\t\t}, ENABLING_PROGRESS_LOG_INTERVAL_MS);\n\t\t\tif (typeof progressTimer === \"object\" && \"unref\" in progressTimer) progressTimer.unref();\n\t\t\ttry {\n\t\t\t\tconst [vectorIndexModule, embeddingModule, rerankerModule] = await Promise.all([\n\t\t\t\t\timport(\"./memory/vector-index.ts\"),\n\t\t\t\t\timport(\"./memory/embedding-provider.ts\"),\n\t\t\t\t\timport(\"./memory/embedding-reranker.ts\"),\n\t\t\t\t]);\n\t\t\t\tconst index = await vectorIndexModule.createMemoryVectorIndex({\n\t\t\t\t\tdbPath: join(context.agentDir, \"memory\", VECTOR_DB_DIRNAME),\n\t\t\t\t\tdimensions: preset.dimensions,\n\t\t\t\t\t...(context.hostLancedbEntryPath === undefined ? {} : { moduleEntryPath: context.hostLancedbEntryPath }),\n\t\t\t\t});\n\t\t\t\tconst remoteHosts = remoteHostsForEnabling();\n\t\t\t\tconst channel: ActiveVectorChannelV1 = {\n\t\t\t\t\tembed: embeddingModule.createTransformersEmbeddingProvider({\n\t\t\t\t\t\tmodelId: preset.embeddingModelId,\n\t\t\t\t\t\tdimensions: preset.dimensions,\n\t\t\t\t\t\tcacheDir: modelCacheDir,\n\t\t\t\t\t\tremoteHosts,\n\t\t\t\t\t\t...(context.hostTransformersEntryPath === undefined\n\t\t\t\t\t\t\t? {}\n\t\t\t\t\t\t\t: { moduleEntryPath: context.hostTransformersEntryPath }),\n\t\t\t\t\t\t...(enablingOverride?.embeddingLoadImpl === undefined\n\t\t\t\t\t\t\t? {}\n\t\t\t\t\t\t\t: { loadImpl: enablingOverride.embeddingLoadImpl }),\n\t\t\t\t\t}),\n\t\t\t\t\treranker: rerankerModule.createTransformersReranker({\n\t\t\t\t\t\tmodelId: VECTOR_RERANKER_MODEL_ID,\n\t\t\t\t\t\tcacheDir: modelCacheDir,\n\t\t\t\t\t\tremoteHosts,\n\t\t\t\t\t\t...(context.hostTransformersEntryPath === undefined\n\t\t\t\t\t\t\t? {}\n\t\t\t\t\t\t\t: { moduleEntryPath: context.hostTransformersEntryPath }),\n\t\t\t\t\t\t...(enablingOverride?.rerankerLoadImpl === undefined\n\t\t\t\t\t\t\t? {}\n\t\t\t\t\t\t\t: { loadImpl: enablingOverride.rerankerLoadImpl }),\n\t\t\t\t\t}),\n\t\t\t\t\tindex,\n\t\t\t\t\tqueryPrefix: preset.queryPrefix,\n\t\t\t\t\tembeddingModelId: preset.embeddingModelId,\n\t\t\t\t};\n\t\t\t\t// 冒烟: embed 一条 (同时完成模型加载) + 投影档位比对。\n\t\t\t\tawait channel.embed.embed([\"memory enabling smoke test\"]);\n\t\t\t\tif (channel.embed.modelId !== preset.embeddingModelId) {\n\t\t\t\t\treturn failEnabling(\n\t\t\t\t\t\t`smoke check failed: embedding modelId ${channel.embed.modelId} does not match the ${memoryMode} preset ${preset.embeddingModelId}`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tconst storedModelId = await channel.index.getStoredModelId();\n\t\t\t\tif (storedModelId !== undefined && storedModelId !== preset.embeddingModelId) {\n\t\t\t\t\tapi.logger?.warn(\n\t\t\t\t\t\t`memory projection was built for ${storedModelId}, preset is ${preset.embeddingModelId}; startup reconciliation will rebuild it`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tvectorChannel = channel;\n\t\t\t\tvectorState = \"ready\";\n\t\t\t\tapi.logger?.info(`memory ready (mode=${memoryMode}, embedding=${preset.embeddingModelId})`);\n\t\t\t\tenablingSettled?.();\n\t\t\t\treturn channel;\n\t\t\t} catch (error) {\n\t\t\t\treturn failEnabling(\n\t\t\t\t\t`model load or smoke check failed (${error instanceof Error ? error.message : String(error)})`,\n\t\t\t\t);\n\t\t\t} finally {\n\t\t\t\tclearInterval(progressTimer);\n\t\t\t}\n\t\t})();\n\t\treturn vectorInitPromise;\n\t};\n\n\t/**\n\t * 写入埋点 (canonical 已提交后): fire-and-forget, 不阻塞工具返回、不抛出。\n\t * embed/upsert 失败记一次 degraded, 该条由启动对账补齐 — canonical 与工具\n\t * 返回不受影响 (向量投影只是 index 副本)。full 档语义合并 (S3) 时携带探测\n\t * 产物: 复用探测已算出的新 statement 向量; 合并命中在 upsert 新行后移除被\n\t * supersedes 的旧行 (upsert 新 id 是插入而非覆盖, 旧行必须显式移除, 失败走\n\t * 同一 degraded 告警 — 残行由召回侧 superseded 过滤 + 对账跳过回填兜底)。\n\t */\n\tconst vectorUpsertAtom = (atom: MemoryAtomV1, mergeOutcome?: SemanticMergeOutcomeV1): void => {\n\t\tif (!vectorEnabled()) return;\n\t\tvoid vectorEnqueue(async () => {\n\t\t\tconst channel = await vectorEnsure();\n\t\t\tif (channel === undefined) return;\n\t\t\ttry {\n\t\t\t\tconst statement = statementOf(atom.payload);\n\t\t\t\tconst [vector] =\n\t\t\t\t\tmergeOutcome === undefined ? await channel.embed.embed([statement]) : [mergeOutcome.vector];\n\t\t\t\tawait channel.index.upsert([\n\t\t\t\t\t{\n\t\t\t\t\t\tmemoryId: atom.memoryId,\n\t\t\t\t\t\towner: BUILTIN_MEMORY_OWNER,\n\t\t\t\t\t\tmodelId: channel.embed.modelId,\n\t\t\t\t\t\tstatement,\n\t\t\t\t\t\ttags: tagFacetsOf(atom),\n\t\t\t\t\t\tvector,\n\t\t\t\t\t},\n\t\t\t\t]);\n\t\t\t\tif (mergeOutcome?.plan.kind === \"merge\") {\n\t\t\t\t\tawait channel.index.remove([mergeOutcome.plan.targetMemoryId]);\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tif (!vectorDegradedWarned) {\n\t\t\t\t\tvectorDegradedWarned = true;\n\t\t\t\t\tapi.logger?.warn(\n\t\t\t\t\t\t\"memory vector projection write failed; the entry is rebuilt by startup reconciliation\",\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tmemoryId: atom.memoryId,\n\t\t\t\t\t\t\terror: error instanceof Error ? error.message : String(error),\n\t\t\t\t\t\t},\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t};\n\n\t/**\n\t * 当前 store 内被 supersedes 关系指向的 memoryId 集 (S3): 写入探测跳过已\n\t * 继任的近邻 (其继任条目才是当前事实), 启动对账跳过回填 (投影只维护未继任\n\t * 条目)。O(n) 扫描, 与 recall 关系扩散建图同量级。\n\t */\n\tconst supersededMemoryIds = (): ReadonlySet<string> => {\n\t\tconst superseded = new Set<string>();\n\t\tfor (const record of store.list({ owner: BUILTIN_MEMORY_OWNER })) {\n\t\t\tfor (const relation of record.atom.relations) {\n\t\t\t\tif (relation.kind === \"supersedes\" && relation.targetMemoryId !== undefined) {\n\t\t\t\t\tsuperseded.add(relation.targetMemoryId);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn superseded;\n\t};\n\n\t/** 语义合并探测失败的一次性降级告警标记 (失败按普通新条目写入, 不刷屏)。 */\n\tlet semanticMergeProbeWarned = false;\n\n\t/**\n\t * 写入语义合并探测 (S3, full 档独有; 在 canonical 提交前执行 — supersedes/\n\t * conflict 关系必须随新 atom 一次性提交, atom 不可变)。同一 vectorQueue 串\n\t * 行域内: embed(statement) → 同 owner KNN top-5 → canonical 回查 (同域可见、\n\t * 未被继任) → 重嵌各近邻的 canonical statement 计算余弦 (阈值作用在 canonical\n\t * 文本的向量上, 不依赖可能陈旧的投影 body) → 三分支判定。时序保证\"排除自身\n\t * observationId 条目\": 新 atom 尚未提交, 其 memoryId 不可能在投影中;\n\t * observationId 重放 (deduped) 消费不到探测产物, 幂等零副作用。探测失败不\n\t * 阻塞写入 (降级为普通新条目并告警一次); enabling 未落定/failed 不等待 —\n\t * 写入不得被模型加载阻塞。\n\t */\n\tconst semanticMergeProbe = (input: {\n\t\treadonly statement: string;\n\t\treadonly newTags: readonly string[];\n\t\treadonly domain: SuiteMemoryDomainV1;\n\t}): Promise<SemanticMergeOutcomeV1 | undefined> => {\n\t\tif (vectorState === \"enabling\" || vectorState === \"failed\") return Promise.resolve(undefined);\n\t\treturn vectorEnqueue(async (): Promise<SemanticMergeOutcomeV1 | undefined> => {\n\t\t\tconst channel = await vectorEnsure();\n\t\t\tif (channel === undefined) return undefined;\n\t\t\ttry {\n\t\t\t\tconst [vector] = await channel.embed.embed([input.statement]);\n\t\t\t\tconst hits = await channel.index.queryKnn(vector, {\n\t\t\t\t\towner: BUILTIN_MEMORY_OWNER,\n\t\t\t\t\tlimit: SEMANTIC_MERGE_KNN_LIMIT,\n\t\t\t\t});\n\t\t\t\tif (hits.length === 0) return { plan: { kind: \"new\" }, vector };\n\t\t\t\tconst superseded = supersededMemoryIds();\n\t\t\t\tconst neighborAtoms: MemoryAtomV1[] = [];\n\t\t\t\tfor (const hit of hits) {\n\t\t\t\t\tconst record = store.get(hit.memoryId, {\n\t\t\t\t\t\towner: BUILTIN_MEMORY_OWNER,\n\t\t\t\t\t\t...(input.domain.kind === \"suite\" ? { suiteId: input.domain.suiteId } : {}),\n\t\t\t\t\t});\n\t\t\t\t\tif (record === undefined) continue;\n\t\t\t\t\t// 域隔离: 合并只发生在同域条目之间 (suite 写入不得 supersedes 跨\n\t\t\t\t\t// suite/跨域条目 — 那会移除别域召回所需的投影行; promoted 的跨\n\t\t\t\t\t// suite 可见偏好 canonical 归属其原 suite, 同样不作为合并目标)。\n\t\t\t\t\tif (\n\t\t\t\t\t\tinput.domain.kind === \"suite\"\n\t\t\t\t\t\t\t? record.atom.suiteId !== input.domain.suiteId\n\t\t\t\t\t\t\t: record.atom.suiteId !== undefined\n\t\t\t\t\t) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tif (superseded.has(record.atom.memoryId)) continue;\n\t\t\t\t\tneighborAtoms.push(record.atom);\n\t\t\t\t}\n\t\t\t\tif (neighborAtoms.length === 0) return { plan: { kind: \"new\" }, vector };\n\t\t\t\tconst neighborVectors = await channel.embed.embed(neighborAtoms.map((atom) => statementOf(atom.payload)));\n\t\t\t\tconst neighbors: SemanticMergeNeighborV1[] = neighborAtoms.map((atom, position) => ({\n\t\t\t\t\tmemoryId: atom.memoryId,\n\t\t\t\t\ttags: tagFacetsOf(atom),\n\t\t\t\t\tcosine: cosineSimilarity(vector, neighborVectors[position] ?? []),\n\t\t\t\t}));\n\t\t\t\treturn { plan: decideSemanticMergePlan({ newTags: input.newTags, neighbors }), vector };\n\t\t\t} catch (error) {\n\t\t\t\tif (!semanticMergeProbeWarned) {\n\t\t\t\t\tsemanticMergeProbeWarned = true;\n\t\t\t\t\tapi.logger?.warn(\"memory semantic merge probe failed; the entry is written as a new memory\", {\n\t\t\t\t\t\terror: error instanceof Error ? error.message : String(error),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t});\n\t};\n\n\t/**\n\t * purge 的 index 副本分派: 通道非 ready/degraded 时 no-op 且不报错 (enabling\n\t * 未完成/failed = 索引无本会话写入的行, 残行由 store 回查兜底不可出线)。\n\t * remove 异步且失败只记 degraded: 残行永远过不了 recall 的 store 回查, 不构\n\t * 成出线泄漏。\n\t */\n\tconst vectorPurgeMemories = (memoryIds: readonly string[]): void => {\n\t\tif (vectorState !== \"ready\" && vectorState !== \"degraded\") return;\n\t\tvoid vectorEnqueue(async () => {\n\t\t\tconst channel = vectorChannel;\n\t\t\tif (channel === undefined) return;\n\t\t\ttry {\n\t\t\t\tawait channel.index.remove(memoryIds);\n\t\t\t} catch (error) {\n\t\t\t\tif (!vectorDegradedWarned) {\n\t\t\t\t\tvectorDegradedWarned = true;\n\t\t\t\t\tapi.logger?.warn(\n\t\t\t\t\t\t\"memory vector projection purge failed; stale rows stay unreachable via the store lookup\",\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\terror: error instanceof Error ? error.message : String(error),\n\t\t\t\t\t\t},\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t};\n\n\t/**\n\t * 启动对账 (一次性, capability 初始化语义内; fire-and-forget 不阻塞首工具):\n\t * 1) 投影 modelId 与预设不一致 (含 undefined 且表非空) → 清空向量表;\n\t * 2) diff store 全量 memoryId vs 投影, 缺失的分批 embed+upsert, 单次上限\n\t * {@link RECONCILE_MAX_UPSERTS}, 超出记 degraded 下次启动继续。对账失败不\n\t * 阻塞 capability 启动, 状态转 degraded (运行期故障, 可自愈: 后续向量操作\n\t * 重新触发补齐语义, canonical 面不受影响)。\n\t */\n\tlet vectorReconcileStarted = false;\n\tconst vectorReconcileOnce = (): void => {\n\t\tif (!vectorEnabled() || vectorReconcileStarted) return;\n\t\tvectorReconcileStarted = true;\n\t\tvoid vectorEnqueue(async () => {\n\t\t\tconst channel = await vectorEnsure();\n\t\t\tif (channel === undefined) return;\n\t\t\ttry {\n\t\t\t\tconst storedModelId = await channel.index.getStoredModelId();\n\t\t\t\tif (storedModelId !== channel.embeddingModelId) {\n\t\t\t\t\tconst stale = await channel.index.listMemoryIds();\n\t\t\t\t\tif (stale.size > 0) await channel.index.remove([...stale]);\n\t\t\t\t}\n\t\t\t\tconst indexed = await channel.index.listMemoryIds();\n\t\t\t\t// 被继任的旧条目不回填投影 (S3): 合并已移除其行, 对账的重放/重建\n\t\t\t\t// 不得复活 (召回可见性由继任条目承担, canonical 侧保留可直查)。\n\t\t\t\tconst superseded = supersededMemoryIds();\n\t\t\t\tconst missing: MemoryAtomV1[] = [];\n\t\t\t\tfor (const record of store.list({ owner: BUILTIN_MEMORY_OWNER })) {\n\t\t\t\t\tif (superseded.has(record.atom.memoryId)) continue;\n\t\t\t\t\tif (!indexed.has(record.atom.memoryId)) missing.push(record.atom);\n\t\t\t\t}\n\t\t\t\tif (missing.length > RECONCILE_MAX_UPSERTS) {\n\t\t\t\t\tapi.logger?.warn(\"memory vector reconciliation capped; remaining entries rebuild on the next startup\", {\n\t\t\t\t\t\ttotal: missing.length,\n\t\t\t\t\t\tcap: RECONCILE_MAX_UPSERTS,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tconst batch = missing.slice(0, RECONCILE_MAX_UPSERTS);\n\t\t\t\tif (batch.length === 0) return;\n\t\t\t\tconst statements = batch.map((atom) => statementOf(atom.payload));\n\t\t\t\tconst vectors = await channel.embed.embed(statements);\n\t\t\t\tawait channel.index.upsert(\n\t\t\t\t\tbatch.map((atom, position) => ({\n\t\t\t\t\t\tmemoryId: atom.memoryId,\n\t\t\t\t\t\towner: BUILTIN_MEMORY_OWNER,\n\t\t\t\t\t\tmodelId: channel.embed.modelId,\n\t\t\t\t\t\tstatement: statements[position],\n\t\t\t\t\t\ttags: tagFacetsOf(atom),\n\t\t\t\t\t\tvector: vectors[position],\n\t\t\t\t\t})),\n\t\t\t\t);\n\t\t\t} catch (error) {\n\t\t\t\tvectorState = \"degraded\";\n\t\t\t\tapi.logger?.warn(\n\t\t\t\t\t\"memory vector reconciliation failed; the channel is degraded (canonical recall degrades, self-heals on the next successful channel op)\",\n\t\t\t\t\t{\n\t\t\t\t\t\terror: error instanceof Error ? error.message : String(error),\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t}\n\t\t});\n\t};\n\n\tconst buildWriteAtom = (input: {\n\t\treadonly content: string;\n\t\treadonly kind: \"fact\" | \"preference\";\n\t\treadonly subject?: string;\n\t\treadonly tags?: readonly string[];\n\t\t/** 语义合并 (S3) 判定产生的 supersedes/conflict 关系; 缺省 = 空关系。 */\n\t\treadonly relations?: readonly MemoryRelationV1[];\n\t\t/** 写入来源标记; 缺省 = 模型工具面。 */\n\t\treadonly writeReason?: string;\n\t\treadonly domain: SuiteMemoryDomainV1;\n\t}): MemoryAtomV1 => {\n\t\twriteSequence += 1;\n\t\tconst observationId = `memory_write:${sessionId}:${writeSequence}`;\n\t\tconst memoryId = `mem-${identityHash(`${BUILTIN_MEMORY_OWNER}\\u0000${observationId}`)}`;\n\t\tconst occurredAt = new Date(now()).toISOString();\n\t\tconst sourceRef = { kind: \"tool\" as const, id: `memory_write:${observationId}` };\n\t\tconst tags = (input.tags ?? []).map((tag) => tag.trim()).filter((tag) => tag !== \"\");\n\t\tconst facets = tags.map((tag) => ({\n\t\t\tnamespace: TAG_FACET_NAMESPACE,\n\t\t\tschemaVersion: 1,\n\t\t\tkey: \"tag\",\n\t\t\tvalue: tag,\n\t\t}));\n\t\tconst preferenceEnvelope: MemoryPreferenceEnvelopeV1 | undefined =\n\t\t\tinput.kind === \"preference\"\n\t\t\t\t? {\n\t\t\t\t\t\tsubject: input.subject as MemoryPreferenceEnvelopeV1[\"subject\"],\n\t\t\t\t\t\tkey: \"statement\",\n\t\t\t\t\t\tpreferredValue: input.content,\n\t\t\t\t\t\tscope: { level: \"profile-private\" },\n\t\t\t\t\t\tevidence: { class: \"explicit\", sourceRefs: [sourceRef], confidence: 1 },\n\t\t\t\t\t\tapplicabilityConfidence: 1,\n\t\t\t\t\t}\n\t\t\t\t: undefined;\n\t\treturn buildMemoryAtomV1({\n\t\t\tmemoryId,\n\t\t\tcontractVersion: MEMORY_CONTRACT_VERSION,\n\t\t\tschemaVersion: 1,\n\t\t\tscope: \"long-term\",\n\t\t\tretentionMode: \"long\",\n\t\t\towner: BUILTIN_MEMORY_OWNER,\n\t\t\tprofileId: capabilityManifest.id,\n\t\t\t...(input.domain.kind === \"suite\" ? { suiteId: input.domain.suiteId } : {}),\n\t\t\tretentionPolicyVersion: \"retention@1\",\n\t\t\tmemoryKind: input.kind === \"preference\" ? \"preference\" : \"fact\",\n\t\t\tpayload: { statement: input.content },\n\t\t\t...(preferenceEnvelope === undefined ? {} : { preference: preferenceEnvelope }),\n\t\t\toccurredAt,\n\t\t\trecordedAt: occurredAt,\n\t\t\tsourceRefs: [sourceRef],\n\t\t\tobservationId,\n\t\t\tsessionRefs: [sessionId],\n\t\t\tagentInstanceRefs: [],\n\t\t\tprojectRefs: [],\n\t\t\tsubjectRefs: [],\n\t\t\tfacets,\n\t\t\trelations: input.relations ?? [],\n\t\t\tconfidence: 1,\n\t\t\timportance: 0.5,\n\t\t\tevidenceClass: \"explicit\",\n\t\t\tcontentRevision: `c-${identityHash(input.content)}`,\n\t\t\twriteReason: input.writeReason ?? `${capabilityManifest.id}/memory_write`,\n\t\t});\n\t};\n\n\tconst writeTool = api.registerTool({\n\t\tname: \"memory_write\",\n\t\tlabel: \"Write memory\",\n\t\tdescription:\n\t\t\t\"Persist one durable memory (a fact or a user preference) scoped to the current suite; it stays recallable across sessions until forgotten\",\n\t\tparameters: writeSchema,\n\t\texecute: async (first: unknown, second?: unknown) => {\n\t\t\tconst input = memoryToolInput(first, second);\n\t\t\tassertNonEmptyString(input.content, \"content\");\n\t\t\t// 长度契约 (混合检索工程化 §5): 超长显式拒绝并提示拆分, 而非静默截断。\n\t\t\tif (input.content.length > MEMORY_CONTENT_MAX_LENGTH) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`content is ${input.content.length} characters; the memory content limit is ${MEMORY_CONTENT_MAX_LENGTH} — split it into multiple self-contained memories`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (input.kind !== \"fact\" && input.kind !== \"preference\")\n\t\t\t\tthrow new Error('kind must be \"fact\" or \"preference\"');\n\t\t\tif (input.kind === \"preference\") {\n\t\t\t\tassertNonEmptyString(input.subject, 'subject (required when kind is \"preference\")');\n\t\t\t}\n\t\t\tif (input.subject !== undefined) assertNonEmptyString(input.subject, \"subject\");\n\t\t\tif (input.tags !== undefined) {\n\t\t\t\tif (!Array.isArray(input.tags) || input.tags.length > MAX_TAGS) {\n\t\t\t\t\tthrow new Error(`tags must be an array of at most ${MAX_TAGS} strings`);\n\t\t\t\t}\n\t\t\t\tfor (const tag of input.tags) assertNonEmptyString(tag, \"tags entry\");\n\t\t\t}\n\t\t\tconst { domain, domainName, ledger } = resolveDomain();\n\t\t\t// 语义合并探测 (2.4d6 S3, full 档独有): light 档完全跳过 (写向量投影\n\t\t\t// 不查近邻, 行为与现状一致)。探测在 canonical 提交前执行, 判定决定新\n\t\t\t// atom 的 supersedes/conflict relations (atom 不可变, 随提交一次写入);\n\t\t\t// 探测失败降级为普通新条目 (告警一次, 不阻塞写入)。\n\t\t\tconst mergeOutcome =\n\t\t\t\tmemoryMode === \"full\"\n\t\t\t\t\t? await semanticMergeProbe({\n\t\t\t\t\t\t\tstatement: input.content,\n\t\t\t\t\t\t\tnewTags: input.tags ?? [],\n\t\t\t\t\t\t\tdomain,\n\t\t\t\t\t\t})\n\t\t\t\t\t: undefined;\n\t\t\tconst atom = buildWriteAtom({\n\t\t\t\tcontent: input.content,\n\t\t\t\tkind: input.kind,\n\t\t\t\tdomain,\n\t\t\t\t...(input.subject === undefined ? {} : { subject: input.subject }),\n\t\t\t\t...(input.tags === undefined ? {} : { tags: input.tags }),\n\t\t\t\t...(mergeOutcome === undefined\n\t\t\t\t\t? {}\n\t\t\t\t\t: { relations: semanticMergeRelations(mergeOutcome.plan, input.content) }),\n\t\t\t});\n\t\t\tconst observation = schedulerApi.submitObservation({\n\t\t\t\towner: BUILTIN_MEMORY_OWNER,\n\t\t\t\tobservationId: atom.observationId,\n\t\t\t\tdraft: atom,\n\t\t\t});\n\t\t\tif (observation.state === \"rejected\") {\n\t\t\t\tthrow new Error(`memory rejected: ${observation.reason ?? \"rejected\"}`);\n\t\t\t}\n\t\t\tconst candidate = schedulerApi.submitCandidate({\n\t\t\t\tobservationId: atom.observationId,\n\t\t\t\towner: BUILTIN_MEMORY_OWNER,\n\t\t\t});\n\t\t\tif (candidate.status === \"rejected\") throw new Error(`memory rejected: ${candidate.reason ?? \"rejected\"}`);\n\t\t\tif (candidate.status === \"committed\") {\n\t\t\t\tledger.append(atom);\n\t\t\t\t// 向量投影埋点 (混合检索工程化): canonical 已提交, 投影写入异步旁路,\n\t\t\t\t// 失败不阻塞不抛出 (vectorUpsertAtom 内部消化)。full 档携带语义合并\n\t\t\t\t// 探测产物 (复用探测向量; 合并命中同时移除被 supersedes 的旧行)。\n\t\t\t\tvectorUpsertAtom(atom, mergeOutcome);\n\t\t\t}\n\t\t\tawait api.publish({\n\t\t\t\ttype: EVENT_TYPE,\n\t\t\t\tversion: 1,\n\t\t\t\tcorrelationId: capabilityManifest.id,\n\t\t\t\tdata: { action: \"written\", memoryId: candidate.memoryId, memoryKind: atom.memoryKind, domain: domainName },\n\t\t\t});\n\t\t\t// 偏好晋升触发 (B2): the /promote capability command is the production\n\t\t\t// caller of promotePreferenceToUserDefault; the tip is what tells the\n\t\t\t// model (so it can tell the user) that the cross-suite promotion path\n\t\t\t// exists. Facts have no promotion path — no tip.\n\t\t\treturn {\n\t\t\t\tmemoryId: candidate.memoryId,\n\t\t\t\tkind: atom.memoryKind,\n\t\t\t\tdomain: domainName,\n\t\t\t\t...(atom.memoryKind === \"preference\"\n\t\t\t\t\t? {\n\t\t\t\t\t\t\ttip: \"[tip] If this is a general preference (not project-specific), the user can promote it across all suites using /promote.\",\n\t\t\t\t\t\t}\n\t\t\t\t\t: {}),\n\t\t\t};\n\t\t},\n\t});\n\n\t/**\n\t * 语义召回管线 (2.4d6 S2, 设计 §4) — 唯一检索路径: embed(查询, light 加 BGE\n\t * 前缀) → KNN depth50 + FTS depth50 → RRF k=60 top20 → 精排 → store 回查\n\t * (owner/suite/scope/过期, 单一可见性路径, 不足 limit 不回填)。降级层级\n\t * (每次降级一条结构化日志, 不抛给模型, 状态转 degraded 可自愈):\n\t * 精排失败 → RRF 序; 向量失败 (embed/KNN) → FTS-only + recency 排序;\n\t * FTS 失败 → KNN-only + recency 排序; 双通道皆败 → 域内 recency 枚举\n\t * (store.list, 列表语义保留); 枚举也失败 → degraded packet (调用方 catch)。\n\t */\n\n\t/** 一次语义召回的中间候选: atom + 融合/降级序位 (精排并列打破用)。 */\n\tinterface RecallCandidateV1 {\n\t\treadonly atom: MemoryAtomV1;\n\t\treadonly position: number;\n\t}\n\n\t/** 出线事实行 (含精排可选分): score 缺席 = 该路径未产生精排分。 */\n\tinterface ScoredRecallFactV1 {\n\t\treadonly memoryId: string;\n\t\treadonly statement: string;\n\t\treadonly confidence: number;\n\t\treadonly position: number;\n\t\treadonly score?: number;\n\t}\n\n\ttype SemanticRecallOutcomeV1 =\n\t\t| {\n\t\t\t\treadonly kind: \"served\";\n\t\t\t\treadonly facts: readonly MemoryFact[];\n\t\t\t\treadonly candidateCount: number;\n\t\t\t\treadonly suiteFilter: MemorySuiteFilterStatsV1;\n\t\t }\n\t\t| { readonly kind: \"blocked\"; readonly reason: string }\n\t\t| { readonly kind: \"unavailable\"; readonly message: string };\n\n\t/** recency 排序 (occurredAt 降序, memoryId 升序打破并列 — 与 recall index 同口径)。 */\n\tconst recencyOrdered = (entries: readonly RecallCandidateV1[]): readonly RecallCandidateV1[] =>\n\t\t[...entries]\n\t\t\t.sort(\n\t\t\t\t(left, right) =>\n\t\t\t\t\tDate.parse(right.atom.occurredAt) - Date.parse(left.atom.occurredAt) ||\n\t\t\t\t\t(left.atom.memoryId < right.atom.memoryId ? -1 : 1),\n\t\t\t)\n\t\t\t.map((entry, position) => ({ atom: entry.atom, position }));\n\n\t/**\n\t * superseded 过滤 (S3-B, store 回查后 / 精排前): 被某条在场候选的 supersedes\n\t * 关系指向的旧条目不再出线 — 合并后同义查询只出新条目。\"新条目在场\"以候选\n\t * 集为界: 继任条目被遗忘 (purge/过期) 后其关系随之消失, 旧条目自然恢复出线。\n\t * memoryId 主键直查不经此过滤 (引用不失效), memory_list 管理面不过滤\n\t * (nothing silently dropped — 用户可显式 forget 旧条目)。\n\t */\n\tconst filterSupersededCandidates = (candidates: readonly RecallCandidateV1[]): readonly RecallCandidateV1[] => {\n\t\tconst superseded = new Set<string>();\n\t\tfor (const entry of candidates) {\n\t\t\tfor (const relation of entry.atom.relations) {\n\t\t\t\tif (relation.kind === \"supersedes\" && relation.targetMemoryId !== undefined) {\n\t\t\t\t\tsuperseded.add(relation.targetMemoryId);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (superseded.size === 0) return candidates;\n\t\treturn candidates.filter((entry) => !superseded.has(entry.atom.memoryId));\n\t};\n\n\t/**\n\t * store 回查 (正确性关键): 候选 id 逐条走与旧锚点通道相同的可见性单一路径\n\t * (owner + suite 读边界 + 过期, 单一路径); suite 场景下不可见 id 按 legacy/\n\t * 外套件区分计数 (suiteFilter 可观测), 不可见者丢弃且不回填 (诚实分页)。\n\t */\n\tconst lookupCandidates = (\n\t\trankedIds: readonly (readonly [memoryId: string, position: number])[],\n\t\tsuiteId: string | undefined,\n\t): { readonly candidates: readonly RecallCandidateV1[]; readonly suiteFilter: MemorySuiteFilterStatsV1 } => {\n\t\tconst candidates: RecallCandidateV1[] = [];\n\t\tlet legacySkipped = 0;\n\t\tlet foreignSuiteSkipped = 0;\n\t\tfor (const [memoryId, position] of rankedIds) {\n\t\t\tconst record = store.get(memoryId, {\n\t\t\t\towner: BUILTIN_MEMORY_OWNER,\n\t\t\t\t...(suiteId === undefined ? {} : { suiteId }),\n\t\t\t});\n\t\t\tif (record !== undefined) {\n\t\t\t\tcandidates.push({ atom: record.atom, position });\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (suiteId !== undefined) {\n\t\t\t\tconst unfiltered = store.get(memoryId, { owner: BUILTIN_MEMORY_OWNER });\n\t\t\t\tif (unfiltered !== undefined) {\n\t\t\t\t\tif (unfiltered.atom.suiteId === undefined) legacySkipped += 1;\n\t\t\t\t\telse foreignSuiteSkipped += 1;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn { candidates, suiteFilter: { legacySkipped, foreignSuiteSkipped } };\n\t};\n\n\t/**\n\t * egress 门重放 (设计 §11 MUST run): 对最终候选统一执行, 与旧锚点 packet 同\n\t * 一 gate、同一 factInfo 来源 (store); blocked 即 fail-closed, 该次 recall\n\t * 返回 failed packet (来源未登记的事实永不越过出界面)。\n\t */\n\tconst egressGate = (candidates: readonly RecallCandidateV1[]) => {\n\t\tconst factInfo: Record<string, MemoryEgressFactInfoV1 | undefined> = {};\n\t\tfor (const entry of candidates) {\n\t\t\tfactInfo[entry.atom.memoryId] = {\n\t\t\t\tsourceRefs: entry.atom.sourceRefs,\n\t\t\t\tpayload: entry.atom.payload as Record<string, unknown>,\n\t\t\t};\n\t\t}\n\t\treturn egressPolicy.apply(\n\t\t\t{\n\t\t\t\toperationId: `memory_recall:${sessionId}:${identityHash(candidates.map((entry) => entry.atom.memoryId).join(\"\\u0000\"))}`,\n\t\t\t\tstatus: \"completed\",\n\t\t\t\tfacts: candidates.map((entry) => ({\n\t\t\t\t\tmemoryId: entry.atom.memoryId,\n\t\t\t\t\tcontentRevision: entry.atom.contentRevision,\n\t\t\t\t\tstatement: JSON.stringify(entry.atom.payload),\n\t\t\t\t\tconfidence: entry.atom.confidence,\n\t\t\t\t})),\n\t\t\t\tqueryPlan: { lookupUsed: false, filtersApplied: [], scorerVersion: \"semantic-rrf@1\" },\n\t\t\t\tattempts: 0,\n\t\t\t\tnarrowingHints: [],\n\t\t\t\tomittedCount: 0,\n\t\t\t\ttruncated: false,\n\t\t\t\ttruncationScope: \"none\",\n\t\t\t\tbudget: {\n\t\t\t\t\tcandidateBudget: 0,\n\t\t\t\t\tmodelInspectionLimit: 0,\n\t\t\t\t\tfinalResultLimit: 0,\n\t\t\t\t\tused: { candidateCount: 0, modelInspectedCount: 0, finalResultCount: 0 },\n\t\t\t\t},\n\t\t\t} satisfies MemoryRecallPacketV1,\n\t\t\tfactInfo,\n\t\t);\n\t};\n\n\tconst recallSemantically = async (input: {\n\t\treadonly query: string;\n\t\treadonly domain: SuiteMemoryDomainV1;\n\t\treadonly limit: number;\n\t\t/**\n\t\t * 自动召回质量门槛 (错误教训与召回质量护栏设计 §2.1, 内部执行缝):\n\t\t * memory_recall 工具 schema 不含该参数, 显式召回行为不变; auto-recall\n\t\t * 钩子经内部闭包恒传。仅约束精排实际产生 score 的候选。\n\t\t */\n\t\treadonly minScore?: number;\n\t}): Promise<SemanticRecallOutcomeV1> => {\n\t\t// 读侧先等写侧队列落定 (启动对账 + 待处理的投影写入), 保证 recall 与\n\t\t// 仓库/投影的线性一致: 本调用之前提交的记忆要么在 store 要么已补进投影,\n\t\t// 不会与后台对账/埋点竞态。\n\t\tawait vectorQueue;\n\t\tconst channel = await vectorEnsure();\n\t\tif (channel === undefined) {\n\t\t\treturn {\n\t\t\t\tkind: \"unavailable\",\n\t\t\t\tmessage:\n\t\t\t\t\tenablingFailure === undefined\n\t\t\t\t\t\t? `memory channel is unavailable (state=${vectorState})`\n\t\t\t\t\t\t: `memory enabling failed: ${enablingFailure.reason} — actions: ${enablingFailure.actions.join(\" | \")}`,\n\t\t\t};\n\t\t}\n\t\tconst suiteId = input.domain.kind === \"suite\" ? input.domain.suiteId : undefined;\n\t\tlet vectorFailed = false;\n\t\tlet ftsFailed = false;\n\t\tlet knnHits: readonly MemoryVectorHitV1[] | undefined;\n\t\tlet ftsHits: readonly MemoryVectorHitV1[] | undefined;\n\t\ttry {\n\t\t\tconst [queryVector] = await channel.embed.embed([channel.queryPrefix + input.query]);\n\t\t\tknnHits = await channel.index.queryKnn(queryVector, {\n\t\t\t\towner: BUILTIN_MEMORY_OWNER,\n\t\t\t\tlimit: HYBRID_CHANNEL_DEPTH,\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tvectorFailed = true;\n\t\t\tapi.logger?.warn(\"memory recall degraded: the vector channel failed; continuing on the FTS channel\", {\n\t\t\t\terror: error instanceof Error ? error.message : String(error),\n\t\t\t});\n\t\t}\n\t\ttry {\n\t\t\tftsHits = await channel.index.queryFts(input.query, {\n\t\t\t\towner: BUILTIN_MEMORY_OWNER,\n\t\t\t\tlimit: HYBRID_CHANNEL_DEPTH,\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tftsFailed = true;\n\t\t\tapi.logger?.warn(\"memory recall degraded: the FTS channel failed\", {\n\t\t\t\terror: error instanceof Error ? error.message : String(error),\n\t\t\t});\n\t\t}\n\n\t\tlet candidates: readonly RecallCandidateV1[];\n\t\tlet suiteFilter: MemorySuiteFilterStatsV1 = { legacySkipped: 0, foreignSuiteSkipped: 0 };\n\t\tlet channelDegraded = false;\n\t\tif (!vectorFailed && !ftsFailed && knnHits !== undefined && ftsHits !== undefined) {\n\t\t\t// 主路径: 双通道 RRF k=60 融合 top20。\n\t\t\tconst pool = rrfFuseRankings([ftsHits.map((hit) => hit.memoryId), knnHits.map((hit) => hit.memoryId)]);\n\t\t\tconst lookup = lookupCandidates(\n\t\t\t\tpool.map((memoryId, position) => [memoryId, position] as const),\n\t\t\t\tsuiteId,\n\t\t\t);\n\t\t\tcandidates = lookup.candidates;\n\t\t\tsuiteFilter = lookup.suiteFilter;\n\t\t} else if (ftsHits !== undefined) {\n\t\t\t// 向量失败: FTS-only + recency 排序。\n\t\t\tchannelDegraded = true;\n\t\t\tconst lookup = lookupCandidates(\n\t\t\t\tftsHits.map((hit, position) => [hit.memoryId, position] as const),\n\t\t\t\tsuiteId,\n\t\t\t);\n\t\t\tcandidates = recencyOrdered(lookup.candidates);\n\t\t\tsuiteFilter = lookup.suiteFilter;\n\t\t} else if (knnHits !== undefined) {\n\t\t\t// FTS 失败: KNN-only + recency 排序 (对称降级)。\n\t\t\tchannelDegraded = true;\n\t\t\tconst lookup = lookupCandidates(\n\t\t\t\tknnHits.map((hit, position) => [hit.memoryId, position] as const),\n\t\t\t\tsuiteId,\n\t\t\t);\n\t\t\tcandidates = recencyOrdered(lookup.candidates);\n\t\t\tsuiteFilter = lookup.suiteFilter;\n\t\t} else {\n\t\t\t// 双通道皆败: 域内 recency 枚举兜底 (store.list, 列表语义保留)。\n\t\t\tchannelDegraded = true;\n\t\t\tconst page = listDomainMemories({ store }, { owner: BUILTIN_MEMORY_OWNER, domain: input.domain });\n\t\t\tconst domainCandidates: RecallCandidateV1[] = [];\n\t\t\tfor (const entry of page.entries) {\n\t\t\t\tconst record = store.get(entry.memoryId, { owner: BUILTIN_MEMORY_OWNER });\n\t\t\t\tif (record !== undefined) domainCandidates.push({ atom: record.atom, position: 0 });\n\t\t\t}\n\t\t\tcandidates = recencyOrdered(domainCandidates);\n\t\t}\n\t\t// 运行期通道故障 → degraded (可自愈: 下一次全通 recall 回 ready)。\n\t\tif (channelDegraded) vectorState = \"degraded\";\n\t\telse if (vectorState === \"degraded\") vectorState = \"ready\";\n\n\t\t// superseded 过滤 (S3-B): 回查后、egress/精排前 (主路径与全部降级路径\n\t\t// 统一收敛于此 — 被继任的旧条目任何通道都不再出线)。\n\t\tcandidates = filterSupersededCandidates(candidates);\n\n\t\tif (candidates.length === 0) {\n\t\t\treturn { kind: \"served\", facts: [], candidateCount: 0, suiteFilter };\n\t\t}\n\t\tconst egressOutcome = egressGate(candidates);\n\t\tif (egressOutcome.status === \"blocked\") return { kind: \"blocked\", reason: egressOutcome.reason };\n\t\tconst statementByMemoryId = new Map(egressOutcome.packet.facts.map((fact) => [fact.memoryId, fact.statement]));\n\t\tlet ordered: readonly ScoredRecallFactV1[] = candidates.map((entry) => ({\n\t\t\tmemoryId: entry.atom.memoryId,\n\t\t\tstatement: unwrapEgressStatement(statementByMemoryId.get(entry.atom.memoryId) ?? \"\"),\n\t\t\tconfidence: entry.atom.confidence,\n\t\t\tposition: entry.position,\n\t\t}));\n\t\t// 精排 (仅主融合路径: 降级页的排序即其降级语义)。reranker 不可用或调用\n\t\t// 失败 → 保持 RRF 序 (精排失败 → RRF 序), 各记一次结构化日志。\n\t\tconst reranker = channel.reranker;\n\t\tif (!channelDegraded && reranker !== undefined && !vectorRerankerUnavailable) {\n\t\t\ttry {\n\t\t\t\tconst scores = await reranker.rerank(\n\t\t\t\t\tinput.query,\n\t\t\t\t\tordered.map((fact) => fact.statement),\n\t\t\t\t);\n\t\t\t\tordered = ordered\n\t\t\t\t\t.map((fact, position) => ({ ...fact, score: scores[position] }))\n\t\t\t\t\t.sort((left, right) => (right.score ?? 0) - (left.score ?? 0) || left.position - right.position);\n\t\t\t} catch (error) {\n\t\t\t\tvectorRerankerUnavailable = true;\n\t\t\t\tapi.logger?.warn(\"memory reranker failed; recall continues on the RRF order\", {\n\t\t\t\t\terror: error instanceof Error ? error.message : String(error),\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\t// minScore 门槛 (§2.1): 仅精排实际产生 score 的候选参与过滤 (score <\n\t\t// minScore 丢弃), 过滤先于 limit 截取; 降级路径与无 score 的候选不受影响。\n\t\tconst minScore = input.minScore;\n\t\tconst eligible =\n\t\t\tminScore === undefined\n\t\t\t\t? ordered\n\t\t\t\t: ordered.filter((fact) => fact.score === undefined || fact.score >= minScore);\n\t\treturn {\n\t\t\tkind: \"served\",\n\t\t\tfacts: eligible\n\t\t\t\t.slice(0, input.limit)\n\t\t\t\t.map(({ memoryId, statement, confidence }) => ({ memoryId, statement, confidence })),\n\t\t\tcandidateCount: ordered.length,\n\t\t\tsuiteFilter,\n\t\t};\n\t};\n\n\t// 提取为具名闭包: 除模型工具面外, 下方的 auto-recall 钩子直接调用同一 execute\n\t// (单参输入约定, memoryToolInput 取第一参), 不经 runtime.invokeTool —— 钩子\n\t// 随插件注册/卸载, 与工具面同生命周期, 替换工具面不会重接自动召回。\n\tconst recallExecute = async (first: unknown, second?: unknown): Promise<unknown> => {\n\t\tconst input = memoryToolInput(first, second);\n\t\tassertNonEmptyString(input.query, \"query\");\n\t\tconst rawLimit: unknown = input.limit;\n\t\tconst limit =\n\t\t\trawLimit === undefined\n\t\t\t\t? DEFAULT_RECALL_LIMIT\n\t\t\t\t: ((): number => {\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\ttypeof rawLimit !== \"number\" ||\n\t\t\t\t\t\t\t!Number.isSafeInteger(rawLimit) ||\n\t\t\t\t\t\t\trawLimit < 1 ||\n\t\t\t\t\t\t\trawLimit > 100\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tthrow new Error(\"limit must be an integer between 1 and 100\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn rawLimit;\n\t\t\t\t\t})();\n\t\t// 内部执行缝 (§2.1): auto-recall 闭包经本字段恒传质量门槛; memory_recall\n\t\t// 工具 schema 不含该参数 (工具面恒 undefined), 显式召回行为不变。\n\t\tconst rawMinScore: unknown = input.minScore;\n\t\tconst minScore =\n\t\t\trawMinScore === undefined\n\t\t\t\t? undefined\n\t\t\t\t: ((): number => {\n\t\t\t\t\t\tif (typeof rawMinScore !== \"number\" || !Number.isFinite(rawMinScore)) {\n\t\t\t\t\t\t\tthrow new Error(\"minScore must be a finite number\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn rawMinScore;\n\t\t\t\t\t})();\n\t\tconst context = parseRecallContext(input.context);\n\t\tconst { domain } = resolveDomain();\n\n\t\t// enabling 失败 = 本实例终态 (mode 记录回退 off): 记忆工具返回携带原因\n\t\t// 与五选一行动清单的不可用 packet (设计 §3.2 硬性要求), 不再重试。\n\t\tif (vectorState === \"failed\") {\n\t\t\tconst failure = enablingFailure ?? { reason: \"memory enabling failed\", actions: [] as const };\n\t\t\treturn {\n\t\t\t\tstatus: \"unavailable\",\n\t\t\t\tstate: \"failed\",\n\t\t\t\tfacts: [] as MemoryFact[],\n\t\t\t\terror: `memory enabling failed: ${failure.reason}`,\n\t\t\t\tactions: failure.actions,\n\t\t\t};\n\t\t}\n\n\t\t// 主键快路径 (memoryIdEquals 语义保留, 幂等/引用而非检索): query 恰为本\n\t\t// 域可见 memoryId 时直接取该条, 不依赖向量通道 (enabling 期间也可用)。\n\t\tlet outcome: SemanticRecallOutcomeV1;\n\t\tconst directRecord = store.get(input.query.trim(), {\n\t\t\towner: BUILTIN_MEMORY_OWNER,\n\t\t\t...(domain.kind === \"suite\" ? { suiteId: domain.suiteId } : {}),\n\t\t});\n\t\tif (directRecord !== undefined) {\n\t\t\tconst gate = egressGate([{ atom: directRecord.atom, position: 0 }]);\n\t\t\tif (gate.status === \"blocked\") {\n\t\t\t\treturn { status: \"failed\", facts: [] as MemoryFact[], error: `egress blocked: ${gate.reason}` };\n\t\t\t}\n\t\t\tconst statementByMemoryId = new Map(gate.packet.facts.map((fact) => [fact.memoryId, fact.statement]));\n\t\t\toutcome = {\n\t\t\t\tkind: \"served\",\n\t\t\t\tfacts: [\n\t\t\t\t\t{\n\t\t\t\t\t\tmemoryId: directRecord.atom.memoryId,\n\t\t\t\t\t\tstatement: unwrapEgressStatement(statementByMemoryId.get(directRecord.atom.memoryId) ?? \"\"),\n\t\t\t\t\t\tconfidence: directRecord.atom.confidence,\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t\tcandidateCount: 1,\n\t\t\t\tsuiteFilter: { legacySkipped: 0, foreignSuiteSkipped: 0 },\n\t\t\t};\n\t\t} else {\n\t\t\ttry {\n\t\t\t\toutcome = await recallSemantically({\n\t\t\t\t\tquery: input.query,\n\t\t\t\t\tdomain,\n\t\t\t\t\tlimit,\n\t\t\t\t\t...(minScore === undefined ? {} : { minScore }),\n\t\t\t\t});\n\t\t\t} catch (error) {\n\t\t\t\t// 全败 (枚举兜底也失败) → degraded packet: 结构化返回, 不抛给模型。\n\t\t\t\tvectorState = \"degraded\";\n\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\tapi.logger?.warn(\"memory recall degraded: every channel failed; returning a degraded packet\", {\n\t\t\t\t\terror: message,\n\t\t\t\t});\n\t\t\t\treturn { status: \"failed\", facts: [] as MemoryFact[], error: message };\n\t\t\t}\n\t\t}\n\t\tif (outcome.kind === \"blocked\") {\n\t\t\treturn { status: \"failed\", facts: [] as MemoryFact[], error: `egress blocked: ${outcome.reason}` };\n\t\t}\n\t\tif (outcome.kind === \"unavailable\") {\n\t\t\treturn {\n\t\t\t\tstatus: \"unavailable\",\n\t\t\t\tstate: vectorState,\n\t\t\t\tfacts: [] as MemoryFact[],\n\t\t\t\terror: outcome.message,\n\t\t\t};\n\t\t}\n\t\tconst facts = outcome.facts;\n\t\tconst resolved =\n\t\t\tcontext === undefined\n\t\t\t\t? undefined\n\t\t\t\t: applyPreferenceContext({\n\t\t\t\t\t\tfacts,\n\t\t\t\t\t\tcontext,\n\t\t\t\t\t\tresolver: preferenceResolver,\n\t\t\t\t\t\tdisambiguator: preferenceDisambiguator,\n\t\t\t\t\t\tatomOf: (memoryId) => store.get(memoryId, { owner: BUILTIN_MEMORY_OWNER })?.atom,\n\t\t\t\t\t});\n\t\t// 偏好消歧先于关系扩散: the diffusion seeds are the atoms the user\n\t\t// actually sees — preference facts dropped by the conflict resolution\n\t\t// never diffuse their relations.\n\t\tconst finalFacts = resolved === undefined ? facts : resolved.facts;\n\t\tconst seeds: MemoryAtomV1[] = [];\n\t\tfor (const fact of finalFacts) {\n\t\t\tconst atom = store.get(fact.memoryId, { owner: BUILTIN_MEMORY_OWNER })?.atom;\n\t\t\tif (atom !== undefined) seeds.push(atom);\n\t\t}\n\t\t// The traversal graph is the recall domain's visible atom set (same\n\t\t// suite read boundary). Diffusion targets outside this set are dropped,\n\t\t// so relations never leak across suites.\n\t\tconst domainMemoryIds = new Set<string>();\n\t\tconst graph: MemoryAtomV1[] = [];\n\t\tfor (const entry of listDomainMemories({ store }, { owner: BUILTIN_MEMORY_OWNER, domain }).entries) {\n\t\t\tdomainMemoryIds.add(entry.memoryId);\n\t\t\tconst atom = store.get(entry.memoryId, { owner: BUILTIN_MEMORY_OWNER })?.atom;\n\t\t\tif (atom !== undefined) graph.push(atom);\n\t\t}\n\t\tconst related: MemoryNetworkExpansionV1[] = [];\n\t\tlet relatedOmittedCount = 0;\n\t\tconst seenRelatedMemoryIds = new Set<string>();\n\t\tfor (const adapterResult of memoryNetwork.expand({ seeds, graph })) {\n\t\t\tif (adapterResult.status !== \"completed\") continue;\n\t\t\tfor (const expansion of adapterResult.related) {\n\t\t\t\tif (!domainMemoryIds.has(expansion.memoryId)) continue;\n\t\t\t\tif (seenRelatedMemoryIds.has(expansion.memoryId)) continue;\n\t\t\t\tseenRelatedMemoryIds.add(expansion.memoryId);\n\t\t\t\tif (related.length >= MAX_RELATED_REFS) {\n\t\t\t\t\trelatedOmittedCount += 1;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\trelated.push(expansion);\n\t\t\t}\n\t\t}\n\t\treturn {\n\t\t\tstatus: finalFacts.length === 0 ? \"empty\" : \"completed\",\n\t\t\tfacts: finalFacts,\n\t\t\t...(resolved !== undefined && resolved.conflicts.length > 0 ? { conflicts: resolved.conflicts } : {}),\n\t\t\t...(domain.kind === \"suite\" ? { suiteFilter: outcome.suiteFilter } : {}),\n\t\t\tomittedCount: Math.max(0, outcome.candidateCount - finalFacts.length),\n\t\t\ttruncated: false,\n\t\t\t// References only — no statement/payload egress; the model fetches\n\t\t\t// content via memory_list or another memory_recall.\n\t\t\t...(related.length > 0\n\t\t\t\t? {\n\t\t\t\t\t\trelated: related.map((expansion) => ({\n\t\t\t\t\t\t\tmemoryId: expansion.memoryId,\n\t\t\t\t\t\t\trelationKind: expansion.relationKind,\n\t\t\t\t\t\t\thop: expansion.hop,\n\t\t\t\t\t\t\tweight: expansion.weight,\n\t\t\t\t\t\t\tvia: expansion.via,\n\t\t\t\t\t\t})),\n\t\t\t\t\t}\n\t\t\t\t: {}),\n\t\t\t...(relatedOmittedCount > 0 ? { relatedOmittedCount } : {}),\n\t\t};\n\t};\n\tconst recallTool = api.registerTool({\n\t\tname: MEMORY_RECALL_TOOL_NAME,\n\t\tlabel: \"Recall memories\",\n\t\tpromptGuidelines: [MEMORY_TOOLS_GUIDE],\n\t\tdescription:\n\t\t\t\"Search this suite's durable memories with semantic recall: vector and full-text channels fuse (RRF) and the result is reranked; an exact memoryId retrieves that memory directly; the result may include related memory references (ids and relation metadata only — recall or list a referenced memory again for its content)\",\n\t\tparameters: recallSchema,\n\t\texecute: recallExecute,\n\t});\n\n\t// ---- B1 记忆自动注入 (auto recall; D-071 增补裁决: 自 sdk transformContext 迁入) ----\n\t// transformContext 链上的插件环节: 无事实可注入时返回 undefined 保持输入。工厂\n\t// 执行 ⟺ memory 家族注册 (mode=off 时整体不注册), 钩子随插件卸载自动摘除, 无需\n\t// 再检查工具在位。失败降级 (recall throw / 契约漂移): 不注入、不阻塞请求, 记\n\t// 一条有界 api.logger warn。\n\tconst autoRecallBlock = async (query: string): Promise<string> => {\n\t\ttry {\n\t\t\t// §2.1: 自动召回恒传质量门槛 (内部闭包, 不经工具 schema)。\n\t\t\tconst result: unknown = await recallExecute({\n\t\t\t\tquery,\n\t\t\t\tlimit: MEMORY_AUTO_RECALL_LIMIT,\n\t\t\t\tminScore: MEMORY_AUTO_RECALL_MIN_RERANK_SCORE,\n\t\t\t});\n\t\t\t// Defensive contract-drift checks: recallExecute is this factory's own\n\t\t\t// closure, so both branches are unreachable today — they exist in case\n\t\t\t// the recall body ever grows an adapter seam (then add a test seam for\n\t\t\t// them; until then there is no honest way to drive them from a test).\n\t\t\tif (result === null || typeof result !== \"object\") {\n\t\t\t\tapi.logger?.warn(\"memory auto-recall failed: memory_recall returned a non-object result (contract drift)\");\n\t\t\t\treturn \"\";\n\t\t\t}\n\t\t\tconst facts = (result as { facts?: unknown }).facts;\n\t\t\tif (!Array.isArray(facts)) {\n\t\t\t\tapi.logger?.warn(\n\t\t\t\t\t'memory auto-recall failed: memory_recall returned a non-array \"facts\" field (contract drift)',\n\t\t\t\t);\n\t\t\t\treturn \"\";\n\t\t\t}\n\t\t\tconst statements: string[] = [];\n\t\t\tfor (const fact of facts) {\n\t\t\t\tif (fact !== null && typeof fact === \"object\") {\n\t\t\t\t\tconst statement = (fact as { statement?: unknown }).statement;\n\t\t\t\t\tif (typeof statement === \"string\" && statement.trim() !== \"\") statements.push(statement);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (statements.length === 0) return \"\";\n\t\t\treturn [\n\t\t\t\tMEMORY_AUTO_RECALL_MARKER,\n\t\t\t\t...statements.map((statement) => `- ${statement}`),\n\t\t\t\t\"</auto_recalled_memory>\",\n\t\t\t\tMEMORY_AUTO_RECALL_DISCLAIMER,\n\t\t\t].join(\"\\n\");\n\t\t} catch (error) {\n\t\t\t// Bound by code points, not UTF-16 code units, so the cut never splits a\n\t\t\t// surrogate pair into a lone surrogate.\n\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\tconst codePoints = Array.from(message);\n\t\t\tconst bounded =\n\t\t\t\tcodePoints.length <= MEMORY_AUTO_RECALL_ERROR_MESSAGE_LIMIT\n\t\t\t\t\t? message\n\t\t\t\t\t: `${codePoints.slice(0, MEMORY_AUTO_RECALL_ERROR_MESSAGE_LIMIT).join(\"\")}…`;\n\t\t\tapi.logger?.warn(`memory auto-recall failed: memory_recall threw: ${bounded}`);\n\t\t\treturn \"\";\n\t\t}\n\t};\n\t// Per-session cache keyed by the last user message text: one user turn runs\n\t// at most one recall; the remembered block rides every model round of that\n\t// turn unchanged, so request-time injections stay idempotent across tool\n\t// loops. An empty block means \"no injection for this turn\" (no facts, or\n\t// recall failure — the recall is an enhancement face and must never block\n\t// the user's request).\n\tconst autoRecall: { lastQuery: string | undefined; block: string } = { lastQuery: undefined, block: \"\" };\n\tconst autoRecallHook = api.registerLoopHook(\"transformContext\", async (messages) => {\n\t\tconst userText = lastBranchUserText(api);\n\t\tif (userText === undefined || userText.trim() === \"\") return undefined;\n\t\tif (autoRecall.lastQuery !== userText) {\n\t\t\tautoRecall.lastQuery = userText;\n\t\t\tautoRecall.block = await autoRecallBlock(userText);\n\t\t}\n\t\tif (autoRecall.block === \"\") return undefined;\n\t\treturn injectAfterLastUserMessage(messages, {\n\t\t\trole: \"user\",\n\t\t\tcontent: autoRecall.block,\n\t\t\ttimestamp: now(),\n\t\t});\n\t});\n\n\t// ---- assistant 偏好卡注入 (统一修复轮 A 交付3; D-075 S4-4 第二批随拆包迁入) ----\n\t// 宿主 sdk 曾在 system prompt 的 persona 段后注入本卡; 公共插件 API 无 system\n\t// prompt 面, 迁入后经 transformContext 循环钩子注入 (最后一条 user 消息之后,\n\t// 与 auto-recall 同一请求时视图)。卡文本与读边界语义逐字保留 (宿主\n\t// memory-assistant-card.test.ts pin 的格式): own-suite 偏好 + 已晋升 user-default\n\t// 偏好, legacy 原子不可见。会话期一卡: 首次模型轮次惰性读取并缓存 (绑定入口在\n\t// 会话构造后才写入, 工厂时间读不到 domain — 与 B1 同因), 后续轮次复用。判定\n\t// \"assistant 定向\" 用 builtin 默认方案 id (宿主 builtin suite 事实: assistant 方\n\t// 案的 orientation 即 assistant); 自定义方案的 orientation 宿主未上公共通道,\n\t// 不覆盖。IO 错误降级为一条 api.logger warn + 无注入 (插件无 suite 诊断通道,\n\t// warn 即对等物), 绝不阻塞请求。\n\tconst assistantCard: { loaded: boolean; text: string | undefined } = { loaded: false, text: undefined };\n\tconst preferenceCardHook = api.registerLoopHook(\"transformContext\", async (messages) => {\n\t\tif (!assistantCard.loaded) {\n\t\t\tassistantCard.loaded = true;\n\t\t\tconst domain = resolveSessionDomain(api);\n\t\t\tif (domain.kind === \"suite\" && domain.suiteId === \"assistant\") {\n\t\t\t\ttry {\n\t\t\t\t\tassistantCard.text = loadAssistantPreferenceCard({\n\t\t\t\t\t\tagentDir: context.agentDir,\n\t\t\t\t\t\towner: BUILTIN_MEMORY_OWNER,\n\t\t\t\t\t\tsuiteId: domain.suiteId,\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\t\tapi.logger?.warn(`assistant preference card could not be loaded: ${message}`);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (assistantCard.text === undefined) return undefined;\n\t\treturn injectAfterLastUserMessage(messages, {\n\t\t\trole: \"user\",\n\t\t\tcontent: assistantCard.text,\n\t\t\ttimestamp: now(),\n\t\t});\n\t});\n\n\t// ---- bash 失败教训自动沉淀 (错误教训与召回质量护栏设计 §2.2) ----\n\t// 家族内独立开关 (默认 on): mode=off 已在工厂入口零注册; lessons=off 时本块\n\t// 不订阅任何事件 (无观察者即无捕获)。两个事件的公开载荷都不含工具入参, 命令\n\t// 字符串在 tool.execution.start 时经会话分支的 assistant toolCall 块回读\n\t// (与 lastBranchUserText 同一数据来源), 以 toolCallId 配对到 tool.result。\n\tconst lessonsResolution = resolveMemoryLessons(context.memory?.lessons);\n\tif (lessonsResolution.invalidEnvValue !== undefined) {\n\t\tapi.logger?.warn(\n\t\t\t`invalid ${MEMORY_LESSONS_ENV} value \"${lessonsResolution.invalidEnvValue}\"; bash error-lesson capture stays on`,\n\t\t);\n\t}\n\t// lifecycle 在 CapabilityAPI 类型上是必选成员(experimental),但最小宿主可以\n\t// 不带该面;教训是增强面,与 auto-recall 同款降级:缺面时跳过注册、记一条\n\t// 有界 warn,不影响记忆家族其余工具面。\n\tconst lifecycleReady = api.lifecycle !== undefined;\n\tif (lessonsResolution.lessons === \"on\" && !lifecycleReady) {\n\t\tapi.logger?.warn(\"memory bash-lesson capture unavailable: host exposes no lifecycle face\");\n\t}\n\tconst lessonRegistrations: DisposableRegistration[] = [];\n\tif (lessonsResolution.lessons === \"on\" && lifecycleReady) {\n\t\t/** (command, exitCode) 签名去重 (§2.2 风暴闸门): 同签名重复失败只写首条。 */\n\t\tconst lessonSignatures = new Set<string>();\n\t\tlet lessonsWritten = 0;\n\t\tlet lessonCapWarned = false;\n\t\t/** toolCallId → command 配对映射 (仅 bash, 容量 FIFO)。 */\n\t\tconst bashCommandsByToolCallId = new Map<string, string>();\n\n\t\tconst rememberBashCommand = (toolCallId: string, command: string): void => {\n\t\t\tif (\n\t\t\t\t!bashCommandsByToolCallId.has(toolCallId) &&\n\t\t\t\tbashCommandsByToolCallId.size >= MEMORY_LESSON_TOOLCALL_MAP_CAPACITY\n\t\t\t) {\n\t\t\t\tconst oldest = bashCommandsByToolCallId.keys().next();\n\t\t\t\tif (oldest.done !== true) bashCommandsByToolCallId.delete(oldest.value);\n\t\t\t}\n\t\t\tbashCommandsByToolCallId.set(toolCallId, command);\n\t\t};\n\n\t\t/**\n\t\t * 从会话分支回读该 toolCall 的 bash 命令 (assistant toolCall 块, 最新\n\t\t * 优先): assistant 消息在工具执行前已入分支, tool.execution.start 时可查。\n\t\t */\n\t\tconst bashCommandForToolCall = (toolCallId: string): string | undefined => {\n\t\t\tconst entries = api.session?.getBranchEntries() ?? [];\n\t\t\tfor (let index = entries.length - 1; index >= 0; index -= 1) {\n\t\t\t\tconst entry = entries[index];\n\t\t\t\tif (entry.type !== \"message\" || entry.message?.role !== \"assistant\") continue;\n\t\t\t\tconst content: unknown = entry.message.content;\n\t\t\t\tif (!Array.isArray(content)) continue;\n\t\t\t\tfor (const item of content) {\n\t\t\t\t\tif (!isPlainObject(item) || item.type !== \"toolCall\" || item.id !== toolCallId) continue;\n\t\t\t\t\tconst args = item.arguments;\n\t\t\t\t\tconst command = isPlainObject(args) ? args.command : undefined;\n\t\t\t\t\tif (typeof command === \"string\" && command.trim() !== \"\") return command;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn undefined;\n\t\t};\n\n\t\t/**\n\t\t * 内部直写 (与 /promote 同构): ledger 追加持久副本, store.commit 以\n\t\t * retentionModeId \"short\" 覆盖保留档 (7 天到期失去召回资格, 不物理清除),\n\t\t * 向量投影异步埋点。教训不是模型工具调用, 不经 candidate 状态机。\n\t\t */\n\t\tconst writeLesson = (command: string, exitCode: string, errorText: string): void => {\n\t\t\tconst signature = `${command}\\u0000${exitCode}`;\n\t\t\tif (lessonSignatures.has(signature)) return;\n\t\t\tif (lessonsWritten >= MEMORY_LESSON_MAX_PER_SESSION) {\n\t\t\t\tif (!lessonCapWarned) {\n\t\t\t\t\tlessonCapWarned = true;\n\t\t\t\t\tapi.logger?.warn(\n\t\t\t\t\t\t`memory bash-lesson cap reached (${MEMORY_LESSON_MAX_PER_SESSION} per instance); further failures are not captured`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst { domain, ledger } = resolveDomain();\n\t\t\tconst atom = buildWriteAtom({\n\t\t\t\tcontent: buildLessonContent(command, exitCode, errorText),\n\t\t\t\tkind: \"fact\",\n\t\t\t\ttags: [...MEMORY_LESSON_TAGS],\n\t\t\t\tdomain,\n\t\t\t\twriteReason: `${capabilityManifest.id}/auto-lesson`,\n\t\t\t});\n\t\t\tledger.append(atom);\n\t\t\tstore.commit(atom, { retentionModeId: MEMORY_LESSON_RETENTION_MODE_ID });\n\t\t\tvectorUpsertAtom(atom);\n\t\t\tlessonsWritten += 1;\n\t\t\tlessonSignatures.add(signature);\n\t\t};\n\n\t\tlessonRegistrations.push(\n\t\t\tapi.lifecycle.registerObserve<MemoryToolExecutionStartEventDataV1>(\n\t\t\t\tTOOL_EXECUTION_START_EVENT_ID,\n\t\t\t\t(event) => {\n\t\t\t\t\tif (event.data.toolName !== \"bash\") return;\n\t\t\t\t\tconst command = bashCommandForToolCall(event.data.toolCallId);\n\t\t\t\t\tif (command !== undefined) rememberBashCommand(event.data.toolCallId, command);\n\t\t\t\t},\n\t\t\t\tTOOL_EXECUTION_START_EVENT_VERSION,\n\t\t\t),\n\t\t);\n\t\tlessonRegistrations.push(\n\t\t\tapi.lifecycle.registerObserve<MemoryToolResultEventDataV1>(\n\t\t\t\tTOOL_RESULT_EVENT_ID,\n\t\t\t\t(event) => {\n\t\t\t\t\tconst data = event.data;\n\t\t\t\t\tif (data.toolName !== \"bash\" || !data.isError) return;\n\t\t\t\t\tconst command = bashCommandsByToolCallId.get(data.toolCallId);\n\t\t\t\t\tbashCommandsByToolCallId.delete(data.toolCallId);\n\t\t\t\t\tif (command === undefined) {\n\t\t\t\t\t\t// 配对失败 (start 未命中/映射溢出): 不写教训, 记一条有界 debug。\n\t\t\t\t\t\tapi.logger?.debug(\"memory bash-lesson capture skipped: no command paired with the failed toolCall\", {\n\t\t\t\t\t\t\ttoolCallId: data.toolCallId,\n\t\t\t\t\t\t});\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tconst errorText = toolResultTextOf(data.message.content);\n\t\t\t\t\twriteLesson(command, lessonExitCodeOf(errorText), errorText);\n\t\t\t\t},\n\t\t\t\tTOOL_RESULT_EVENT_VERSION,\n\t\t\t),\n\t\t);\n\t}\n\n\tconst listTool = api.registerTool({\n\t\tname: \"memory_list\",\n\t\tlabel: \"List memories\",\n\t\tdescription: \"List this suite's durable memories (the explicit management surface for personal data)\",\n\t\tparameters: listSchema,\n\t\texecute: async (first: unknown, second?: unknown) => {\n\t\t\tconst input = memoryToolInput(first, second);\n\t\t\tconst rawLimit: unknown = input.limit;\n\t\t\tconst limit =\n\t\t\t\trawLimit === undefined\n\t\t\t\t\t? DEFAULT_LIST_LIMIT\n\t\t\t\t\t: ((): number => {\n\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\ttypeof rawLimit !== \"number\" ||\n\t\t\t\t\t\t\t\t!Number.isSafeInteger(rawLimit) ||\n\t\t\t\t\t\t\t\trawLimit < 1 ||\n\t\t\t\t\t\t\t\trawLimit > 100\n\t\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t\tthrow new Error(\"limit must be an integer between 1 and 100\");\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn rawLimit;\n\t\t\t\t\t\t})();\n\t\t\tconst { domain } = resolveDomain();\n\t\t\tconst page = listDomainMemories({ store }, { owner: BUILTIN_MEMORY_OWNER, domain });\n\t\t\treturn {\n\t\t\t\tdomain: page.domain,\n\t\t\t\tentries: page.entries.slice(0, limit).map(entryToToolEntry),\n\t\t\t\ttotal: page.entries.length,\n\t\t\t\tpreferenceCount: page.preferenceCount,\n\t\t\t\tfactCount: page.factCount,\n\t\t\t\tfilter: page.filter,\n\t\t\t};\n\t\t},\n\t});\n\n\tconst forgetTool = api.registerTool({\n\t\tname: \"memory_forget\",\n\t\tlabel: \"Forget memory\",\n\t\tdescription: \"Physically delete one memory of this suite by memoryId (purge-gated, journalled, irreversible)\",\n\t\tparameters: forgetSchema,\n\t\texecute: async (first: unknown, second?: unknown) => {\n\t\t\tconst input = memoryToolInput(first, second);\n\t\t\tassertNonEmptyString(input.memoryId, \"memoryId\");\n\t\t\tconst { domain, domainName, ledger } = resolveDomain();\n\t\t\tconst authorizationRef: SuiteForgetAuthorizationRefV1 = {\n\t\t\t\tmode: \"user-immediate\",\n\t\t\t\tissuedAt: now(),\n\t\t\t\tissuedBy: capabilityManifest.id,\n\t\t\t\tconfirmationRef: `memory_forget:${sessionId}:${input.memoryId}`,\n\t\t\t};\n\t\t\tconst result = forgetDomainMemory(\n\t\t\t\t{\n\t\t\t\t\tstore,\n\t\t\t\t\tpurgeGate,\n\t\t\t\t\tpurgeJournal,\n\t\t\t\t\t// index 副本分派 (混合检索工程化): canonical 走下方原回调 (行为零\n\t\t\t\t\t// 变化), 向量投影由 suite-memory 的 executePurgeBatch 按副本路由到这里。\n\t\t\t\t\tpurgeIndexReplica: vectorPurgeMemories,\n\t\t\t\t\tnow,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\towner: BUILTIN_MEMORY_OWNER,\n\t\t\t\t\tdomain,\n\t\t\t\t\tmemoryId: input.memoryId,\n\t\t\t\t\tauthorizedBy: `${capabilityManifest.id}:memory_forget`,\n\t\t\t\t\tauthorizationRef,\n\t\t\t\t},\n\t\t\t\t(memoryIds) => {\n\t\t\t\t\tconst purged = new Set(memoryIds);\n\t\t\t\t\t// Physical replica purge: rewrite the durable ledger without the\n\t\t\t\t\t// purged atoms, then evict them from the in-memory store so the\n\t\t\t\t\t// running process cannot recall them either (both idempotent for\n\t\t\t\t\t// purge_eligible retries).\n\t\t\t\t\tledger.rewrite(ledger.atomsSnapshot().filter((atom) => !purged.has(atom.memoryId)));\n\t\t\t\t\tfor (const memoryId of memoryIds) store.evict(memoryId);\n\t\t\t\t},\n\t\t\t);\n\t\t\tif (result.status === \"completed\") {\n\t\t\t\tawait api.publish({\n\t\t\t\t\ttype: EVENT_TYPE,\n\t\t\t\t\tversion: 1,\n\t\t\t\t\tcorrelationId: capabilityManifest.id,\n\t\t\t\t\tdata: { action: \"forgotten\", memoryId: input.memoryId, domain: domainName, batchId: result.batchId },\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn { status: result.status, memoryId: input.memoryId };\n\t\t},\n\t});\n\n\t/** Canonical memoryId of the promoted copy of one preference (promotePreferenceToUserDefault suffix). */\n\tconst userDefaultMemoryId = (memoryId: string): string => `${memoryId}-user-default`;\n\n\t/**\n\t * /promote (偏好晋升触发, B2): the production caller of\n\t * {@link promotePreferenceToUserDefault}. Promotes one of THIS session's\n\t * preferences to the user-default scope: the promoted canonical atom is\n\t * appended to the session domain's durable ledger and committed to the\n\t * store, so every suite's preference-card read (which applies the suite\n\t * read boundary across all of the owner's ledgers) sees it from now on.\n\t * The original atom is never rewritten. Errors (unknown id, non-preference,\n\t * already promoted) surface as error results, never as thrown failures —\n\t * a slash command must not crash the host.\n\t */\n\tconst promoteCommand = api.registerCommand({\n\t\tname: \"promote\",\n\t\tdescription: \"Promote a memory preference to the user-default scope (visible in every suite)\",\n\t\targumentHint: \"<memoryId>\",\n\t\texecute: async (args: string) => {\n\t\t\tconst memoryId = args.trim();\n\t\t\tif (memoryId === \"\") {\n\t\t\t\treturn {\n\t\t\t\t\tstatus: \"error\" as const,\n\t\t\t\t\tmessage: \"Usage: /promote <memoryId> — run memory_list for this suite's memory ids\",\n\t\t\t\t};\n\t\t\t}\n\t\t\tconst record = store.get(memoryId, { owner: BUILTIN_MEMORY_OWNER });\n\t\t\tif (!record) {\n\t\t\t\treturn {\n\t\t\t\t\tstatus: \"error\" as const,\n\t\t\t\t\tmessage: `No memory found with id ${memoryId}; run memory_list for this suite's memory ids`,\n\t\t\t\t};\n\t\t\t}\n\t\t\tconst atom = record.atom;\n\t\t\tif (atom.memoryKind !== \"preference\" || atom.preference === undefined) {\n\t\t\t\treturn {\n\t\t\t\t\tstatus: \"error\" as const,\n\t\t\t\t\tmessage: `${memoryId} is a fact, not a preference; only preferences can be promoted`,\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (store.get(userDefaultMemoryId(memoryId), { owner: BUILTIN_MEMORY_OWNER }) !== undefined) {\n\t\t\t\treturn {\n\t\t\t\t\tstatus: \"error\" as const,\n\t\t\t\t\tmessage: `Preference ${memoryId} is already promoted to the user-default scope`,\n\t\t\t\t};\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst promoted = promotePreferenceToUserDefault({\n\t\t\t\t\tatom,\n\t\t\t\t\tauthorizedBy: \"user:/promote\",\n\t\t\t\t\tconfirmedAt: new Date(now()).toISOString(),\n\t\t\t\t});\n\t\t\t\tconst { domainName, ledger } = resolveDomain();\n\t\t\t\tledger.append(promoted);\n\t\t\t\tstore.commit(promoted);\n\t\t\t\t// 向量投影埋点: 与 memory_write 同一异步旁路 (失败不阻塞命令返回)。\n\t\t\t\tvectorUpsertAtom(promoted);\n\t\t\t\treturn {\n\t\t\t\t\tstatus: \"success\" as const,\n\t\t\t\t\tmessage: `Promoted \"${unwrapEgressStatement(statementOf(promoted.payload))}\" to the user-default scope; it is now visible in every suite`,\n\t\t\t\t\tdata: { memoryId: promoted.memoryId, sourceMemoryId: memoryId, domain: domainName },\n\t\t\t\t};\n\t\t\t} catch (error) {\n\t\t\t\treturn { status: \"error\" as const, message: error instanceof Error ? error.message : String(error) };\n\t\t\t}\n\t\t},\n\t});\n\n\t// memory.scheduler@1 plugin capability (宪法 §3 显式声明面): a declarative\n\t// marker only — the scheduler enforcement itself (per-instance single\n\t// lease, acquired above) stays carried by the host builtin implementation.\n\t// The declaration anchors \"this suite's memory scheduling is provided by\n\t// this plugin\" so the code Profile assembly check (requiresMemoryScheduler)\n\t// and third-party replacement detection have an explicit surface:\n\t// list()-visible, a same-id double provide fails explicitly (no override\n\t// priority), and after dispose a replacement provider can take the id.\n\tconst schedulerCapability = api.capabilities.provide({ id: \"memory.scheduler\", version: 1, kind: \"service\" }, () =>\n\t\tObject.freeze({\n\t\t\tid: \"memory.scheduler\",\n\t\t\tversion: 1,\n\t\t\towner: \"agent-forge.builtin.memory\",\n\t\t\tleaseScope: \"per-agent-instance\",\n\t\t}),\n\t);\n\n\tconst registrations = [\n\t\twriteTool,\n\t\trecallTool,\n\t\tlistTool,\n\t\tforgetTool,\n\t\tpromoteCommand,\n\t\tschedulerCapability,\n\t\tautoRecallHook,\n\t\tpreferenceCardHook,\n\t\t...lessonRegistrations,\n\t];\n\treturn {\n\t\tregistrations,\n\t\tsettleVectorWork: async () => {\n\t\t\tawait vectorQueue;\n\t\t},\n\t\tvectorState: (): \"off\" | MemoryCapabilityStateV1 => vectorState,\n\t\t// 触发惰性 enabling (已注入组件时已就绪) 并等待其落定: ready 或结构化失败。\n\t\tenablingOutcome: async () => {\n\t\t\tif (vectorState === \"enabling\") void vectorEnsure();\n\t\t\tawait enablingSettledPromise;\n\t\t\treturn enablingFailure ?? \"ready\";\n\t\t},\n\t};\n}\n"]}
1
+ {"version":3,"file":"capability.d.ts","sourceRoot":"","sources":["../src/capability.ts"],"names":[],"mappings":"AAyFA,OAAO,KAAK,EAEX,0BAA0B,EAC1B,YAAY,EACZ,yBAAyB,EAEzB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACN,KAAK,aAAa,EAClB,KAAK,sBAAsB,EAG3B,MAAM,yBAAyB,CAAC;AAMjC,OAAO,EACN,uBAAuB,EACvB,KAAK,0BAA0B,EAC/B,KAAK,wBAAwB,EAC7B,KAAK,YAAY,EACjB,KAAK,yBAAyB,EAC9B,KAAK,gCAAgC,GACrC,MAAM,yBAAyB,CAAC;AAuFjC,eAAO,MAAM,kBAAkB;;;;;;;;;;CAMrB,CAAC;AAEX;;;GAGG;AACH,eAAO,MAAM,yBAAyB,2BAA2B,CAAC;AAmBlE;;;;GAIG;AACH,eAAO,MAAM,6BAA6B,0LACyI,CAAC;AAgCpL;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,qRACoP,CAAC;AAWpR;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,eAAe,CAAC;AAyFjD;;;;;;;GAOG;AACH,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,CAAC;IACvC,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;CACjC;AAKD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAChC,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,GAAG,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAe,EAC/D,YAAY,CAAC,EAAE,MAAM,GACnB,sBAAsB,CA0BxB;AAED,qDAA8B;AAC9B,MAAM,MAAM,sBAAsB,GAAG,IAAI,GAAG,KAAK,CAAC;AAIlD;;;GAGG;AACH,MAAM,WAAW,yBAAyB;IACzC,QAAQ,CAAC,OAAO,EAAE,sBAAsB,CAAC;IACzC,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;CAClC;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CACnC,eAAe,EAAE,MAAM,GAAG,SAAS,EACnC,GAAG,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAe,GAC7D,yBAAyB,CAW3B;AAED,mGAAmG;AACnG,MAAM,WAAW,uBAAuB;IACvC,oFAAkF;IAClF,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;;;OAKG;IACH,QAAQ,CAAC,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAC3C,uGAAyD;IACzD,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,CAAC;IACvC,8FAAoE;IACpE,QAAQ,CAAC,yBAAyB,CAAC,EAAE,MAAM,CAAC;IAC5C;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE;QACjB,QAAQ,CAAC,IAAI,CAAC,EAAE,YAAY,CAAC;QAC7B;;;;WAIG;QACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;QAC/B;;WAEG;QACH,QAAQ,CAAC,OAAO,CAAC,EAAE,sBAAsB,CAAC;KAC1C,CAAC;IACF;;;;OAIG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,0BAA0B,CAAC;IACvD;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,yBAAyB,CAAC;CAC7C;AAMD,wGAA2D;AAC3D,MAAM,MAAM,uBAAuB,GAAG,UAAU,GAAG,OAAO,GAAG,UAAU,GAAG,QAAQ,CAAC;AAEnF,+FAAoD;AACpD,MAAM,WAAW,uBAAuB;IACvC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CACpC;AAqYD;;;;;;;GAOG;AACH,wBAAgB,6BAA6B,CAC5C,OAAO,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,EACpD,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,GAC1C,MAAM,EAAE,CAeV;AAiRD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,sBAAsB,CACrC,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE,uBAAuB,EAChC,QAAQ,CAAC,EAAE,0BAA0B,GACnC,SAAS,sBAAsB,EAAE,CAGnC;AAED,sFAAsF;AACtF,MAAM,WAAW,+BAA+B;IAC/C,QAAQ,CAAC,aAAa,EAAE,SAAS,sBAAsB,EAAE,CAAC;IAC1D,qFAAqF;IACrF,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAClC,WAAW,IAAI,KAAK,GAAG,uBAAuB,CAAC;IAC/C;;;OAGG;IACH,eAAe,IAAI,OAAO,CAAC,OAAO,GAAG,uBAAuB,CAAC,CAAC;CAC9D;AAED;;;;;GAKG;AACH,wBAAgB,oCAAoC,CACnD,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE,uBAAuB,EAChC,QAAQ,EAAE,0BAA0B,GAClC,+BAA+B,CAiBjC","sourcesContent":["/**\n * First-party builtin memory capability (统一修复轮 A, 方案系统设计 §6.1 + §11,\n * 方案实施计划 §11 A 路) — the model-facing write/recall/list/forget surface\n * over the M5 Memory Foundation.\n *\n * Every operation is suite-scoped by construction: the suiteId comes from the\n * session's suite binding entry (SUITE_SESSION_ENTRY_TYPE, written by the sdk\n * at session creation) and is NEVER a tool parameter, so the model cannot\n * widen or switch its memory domain (防越权). Sessions without a binding fall\n * back to the \"legacy\" domain with a diagnostic. Writes go through the\n * scheduler facade (observation → candidate → committed) plus a durable JSONL\n * ledger append (src/memory/ledger.ts, the store's persistent replica); recall\n * runs through the memory-recall-agent with the suiteId passthrough filter and\n * the egress gate; forget goes through the canonical purge gate\n * (user-immediate) and journal, physically rewriting the ledger (forget 后真\n * 物理删除).\n *\n * Domain resolution is LAZY, per tool call (统一修复轮 B1 复审修复): the sdk\n * appends the suite binding entry only after `new AgentSession` returns (the\n * constructor's synchronous runtime build already runs this factory), so a\n * factory-time resolution always saw an empty branch and every new session\n * fell back to the legacy domain. Instead, each tool handler resolves the\n * domain at call time — the binding entry is on the branch by then (creation\n * appends it; resumed sessions carry it in their history) — and the per-domain\n * ledger is created, loaded, and replayed into the store on the first call in\n * that domain (Map<domain, ledger> cache, no memoization of the domain\n * itself). The store stays one per-capability-instance singleton across\n * domains: the atom `suiteId` field carries the read boundary; the ledger is\n * only the per-domain durable replica.\n *\n * Concurrency envelope (统一修复轮 H1, #7 深度修复; WP-F 关洞修复): ledger\n * appends AND the forget path's full-file rewrite serialize competing\n * processes through the ledger's `.lock` sibling file (exclusive create +\n * poll, 5 s timeout, stale-lock stealing — src/memory/ledger.ts), and both\n * write paths run open → write → fsync → close, so a hard crash cannot lose\n * a write that returned to its caller. A lock timeout or IO/fsync failure\n * throws out of the tool handler, so the model sees the error instead of a\n * silently lost write. Remaining boundaries: the lock is atomic only on\n * local filesystems; a rewrite publishes the calling process's in-memory\n * view without merging rows another process appended after its last load\n * (the forget caller owns that reconciliation); fsync does not cover\n * directory entries, so a crash before rewrite's rename leaves the previous\n * file intact; an append whose fsync failed stays unacknowledged though it\n * may already be readable — the loader's duplicate tolerance covers an\n * unacknowledged retry.\n *\n * Global mode gate (2.4d6 S1, 记忆系统语义化重设计 §3): `memory.mode` is\n * `\"off\"` (default) | `\"light\"` | `\"full\"`; off means the whole memory family\n * is NOT registered (the suite membership skips the plugin; the factory\n * itself returns zero registrations for direct callers). light/full run an\n * enabling state machine (local checks → model load/download → smoke →\n * ready; failure → failed with a four-option action list and the effective\n * mode falls back to off for this instance).\n *\n * Semantic recall (2.4d6 S2, 设计 §4): memory_recall has exactly one\n * retrieval path — embed → KNN depth50 + FTS depth50 → RRF k=60 top20 →\n * cross-encoder rerank → store lookup (owner/suite/scope/expiry). The legacy\n * tag-wordform bridge (query-token → tag-facet anchors, recency fallback\n * page) is retired as a retrieval semantic; tags still ride the FTS body and\n * the memory_list management face. Degradation ladder (never thrown at the\n * model): rerank failure → RRF order; vector failure → FTS-only with\n * recency ordering; FTS/index failure → KNN-only; both channels down →\n * recency enumeration over the domain; total failure → degraded packet.\n * `memoryIdEquals`-style primary-key lookup survives as the exact-id fast\n * path (reference semantics, not retrieval).\n *\n * Semantic write merge (2.4d6 S3, 设计 §5 + 实施计划 S3 标定定版): in mode\n * \"full\" every memory_write probes the projection BEFORE the canonical commit\n * (embed + same-owner KNN top-5) because atoms are immutable — the outcome's\n * relations must ride the atom's one commit. Merge condition (calibrated on\n * bge-m3): cos ≥ 0.72, or 0.55 ≤ cos < 0.72 with a case-insensitive tag\n * overlap. A merge commits the NEW atom with a one-way `supersedes` relation\n * to the neighbor (the old entry keeps its memoryId and stays addressable via\n * primary-key lookup) while the projection upserts the new row and removes the\n * superseded one; startup reconciliation never re-adds superseded rows.\n * 0.55 ≤ cos < 0.72 without a tag overlap is the conflict band: both entries\n * stay, and the new atom carries one-way `conflict` relations. Below that (or\n * no neighbor): a plain new entry. bge-small-zh similarity distributions\n * overlap (标定: 余弦不可用作去重信号) → light mode never probes and stays\n * byte-identical to the pre-S3 behavior. Recall filters superseded entries\n * after the store lookup (only while their successor is among the candidates,\n * so forgetting the successor revives the old entry). The Foundation\n * (owner, observationId) idempotency is untouched: replays never consume a\n * probe outcome.\n */\nimport { existsSync, readdirSync, statSync } from \"node:fs\";\nimport { statfs } from \"node:fs/promises\";\nimport { createRequire } from \"node:module\";\nimport { dirname, join } from \"node:path\";\nimport type {\n\tJsonValue,\n\tMemoryCapabilityOverrideV1,\n\tMemoryModeV1,\n\tMemoryStorageComponentsV1,\n\tMemoryVectorComponentsOverrideV1,\n} from \"@agent-forge/plugin-sdk\";\nimport {\n\ttype CapabilityAPI,\n\ttype DisposableRegistration,\n\tEXPERIMENTAL_PUBLIC_API_VERSION,\n\tMEMORY_RECALL_TOOL_NAME,\n} from \"@agent-forge/plugin-sdk\";\nimport { Type } from \"typebox\";\nimport { purgeZeroByteCacheArtifacts } from \"./memory/model-cache-hygiene.ts\";\n\n// 记忆公共契约的权威定义处已是 plugin-sdk(D-075 S4-4 第一批迁入);此处 re-export\n// 维持宿主历史导出面(宿主第二批收口后由宿主 re-export SDK)。\nexport {\n\tMEMORY_RECALL_TOOL_NAME,\n\ttype MemoryCapabilityOverrideV1,\n\ttype MemoryEnablingOverrideV1,\n\ttype MemoryModeV1,\n\ttype MemoryStorageComponentsV1,\n\ttype MemoryVectorComponentsOverrideV1,\n} from \"@agent-forge/plugin-sdk\";\n\nimport { loadAssistantPreferenceCard } from \"./memory/assistant-card.ts\";\nimport { createMemoryCandidateMachine } from \"./memory/candidates.ts\";\nimport type { MemoryEgressFactInfoV1 } from \"./memory/egress-policy.ts\";\nimport { createMemoryEgressPolicy } from \"./memory/egress-policy.ts\";\nimport type { MemoryEmbeddingProviderV1 } from \"./memory/embedding-provider.ts\";\nimport type { MemoryRerankerV1 } from \"./memory/embedding-reranker.ts\";\nimport {\n\tbuildMemoryAtomV1,\n\ttype MemoryAtomV1,\n\ttype MemoryPreferenceEnvelopeV1,\n\ttype MemoryRelationV1,\n\ttype MemorySuiteFilterStatsV1,\n} from \"./memory/foundation.ts\";\nimport { createDurableMemoryLedger, type DurableMemoryLedgerV1 } from \"./memory/ledger.ts\";\nimport { createMemoryLifecycleManager } from \"./memory/lifecycle.ts\";\nimport {\n\tcreateMemoryNetwork,\n\tcreateRelationDiffusionAdapter,\n\ttype MemoryNetworkExpansionV1,\n} from \"./memory/memory-network.ts\";\nimport { createPreferenceDisambiguator, type PreferenceDisambiguatorV1 } from \"./memory/preference-disambiguator.ts\";\nimport { promotePreferenceToUserDefault } from \"./memory/preference-promotion.ts\";\nimport {\n\tcreatePreferenceResolver,\n\ttype PreferenceContextV1,\n\ttype PreferenceResolverV1,\n} from \"./memory/preference-resolver.ts\";\nimport { createMemoryPurgeGate, createMemoryReplicaRegistry, type MemoryReplicaV1 } from \"./memory/purge.ts\";\nimport { createMemoryPurgeJournal } from \"./memory/purge-journal.ts\";\nimport { createMemoryRecallAgent } from \"./memory/recall-agent.ts\";\nimport { createMemoryRecallIndex } from \"./memory/recall-index.ts\";\nimport type { MemoryRecallPacketV1 } from \"./memory/recall-packet.ts\";\nimport { createMemoryScheduler } from \"./memory/scheduler.ts\";\nimport { createMemorySchedulerApi } from \"./memory/scheduler-api.ts\";\nimport { createFirstPartyRetentionRegistry, createMemoryStore } from \"./memory/store.ts\";\nimport {\n\tforgetDomainMemory,\n\tlistDomainMemories,\n\ttype SuiteForgetAuthorizationRefV1,\n\ttype SuiteMemoryDomainV1,\n\ttype SuiteMemoryListEntryV1,\n\tsuiteMemoryDomainName,\n} from \"./memory/suite-memory.ts\";\nimport { MEMORY_CONTRACT_VERSION_V2 } from \"./memory/transfer.ts\";\nimport type { MemoryVectorHitV1, MemoryVectorIndexV1 } from \"./memory/vector-index.ts\";\n\n// ---------------------------------------------------------------------------\n// Host lifecycle catalog references (D-075 S4-4 第一批迁包改写): the embedded\n// capability imported the event definitions from the host's lifecycle catalog\n// and re-defined them idempotently. The plugin now subscribes through the\n// plain `{id, version}` reference form (observability.ts 同构先例): the host's\n// bootstrap lifecycle plugin owns the definitions, and a host that has not\n// defined the event rejects the registration structurally (plugin-load\n// failure under D-028, never a session failure). The data shapes below are\n// structural narrowings of the catalog payloads — only the fields the bash\n// error-lesson capture reads.\n// ---------------------------------------------------------------------------\n\n/** `tool.execution.start@1` (host lifecycle catalog). */\nconst TOOL_EXECUTION_START_EVENT_ID = \"tool.execution.start\";\nconst TOOL_EXECUTION_START_EVENT_VERSION = 1;\n/** `tool.result@1` (host lifecycle catalog). */\nconst TOOL_RESULT_EVENT_ID = \"tool.result\";\nconst TOOL_RESULT_EVENT_VERSION = 1;\n\n/** Structural narrowing of the catalog's `ToolExecutionStartEventDataV1`. */\ninterface MemoryToolExecutionStartEventDataV1 {\n\treadonly version: 1;\n\treadonly sessionId: string;\n\treadonly toolCallId: string;\n\treadonly toolName: string;\n}\n\n/** Structural narrowing of the catalog's `ToolResultEventDataV1` (text blocks only). */\ninterface MemoryToolResultEventDataV1 {\n\treadonly version: 1;\n\treadonly sessionId: string;\n\treadonly toolCallId: string;\n\treadonly toolName: string;\n\treadonly isError: boolean;\n\treadonly message: {\n\t\treadonly content: readonly ({ readonly type: \"text\"; readonly text: string } | { readonly type: \"image\" })[];\n\t};\n}\n\nexport const capabilityManifest = {\n\tid: \"agent-forge.builtin.memory\",\n\tversion: \"0.1.0\",\n\tapiVersion: EXPERIMENTAL_PUBLIC_API_VERSION,\n\trequiredCapabilities: [\"session\", \"events\", \"tools\"],\n\tprovides: [{ id: \"memory.scheduler\", version: 1, kind: \"service\" }],\n} as const;\n\n/**\n * Marker text of the auto-recall injection message (B1 记忆自动注入, D-071 增补\n * 裁决自 sdk 迁入; pinned by test/memory-sdk-e2e.test.ts).\n */\nexport const MEMORY_AUTO_RECALL_MARKER = \"<auto_recalled_memory>\";\n\n/**\n * Upper bound on facts pulled into one auto-recall injection (B1 记忆自动注入).\n * The injection rides every request of the turn, so it stays a compact hint —\n * deep recall is what memory_recall is for.\n */\nconst MEMORY_AUTO_RECALL_LIMIT = 3;\n\n/**\n * Auto-recall rerank floor (错误教训与召回质量护栏设计 §2.1): reranked\n * candidates scoring below it are dropped from the auto-recall injection\n * (`score < minScore`). bge-reranker outputs uncalibrated logits that are only\n * order-preserving (见 embedding-reranker.ts 头注), so 0 (\"positive logit =\n * relevant direction\") is the conservative initial gate — tunable with real\n * model eval evidence (只改常量 + eval 证据).\n */\nconst MEMORY_AUTO_RECALL_MIN_RERANK_SCORE = 0;\n\n/**\n * Tail line of every auto-recall injection block (错误教训与召回质量护栏设计\n * §2.1): tells the model the block was recalled rather than written by the\n * user and may be outdated. Pinned verbatim by test/memory-error-lessons.test.ts.\n */\nexport const MEMORY_AUTO_RECALL_DISCLAIMER =\n\t\"(Recalled automatically from persistent memory for this request — not written by the user. Recalled items may be outdated; verify against current state before relying on them.)\";\n\n/**\n * Upper bound on the error message embedded in one memory auto-recall log\n * line: provider/store errors can carry arbitrary payload text, and the log\n * channel is for locating the failure, not for echoing it.\n */\nconst MEMORY_AUTO_RECALL_ERROR_MESSAGE_LIMIT = 200;\n\n// ---------------------------------------------------------------------------\n// bash 失败教训自动沉淀 (错误教训与召回质量护栏设计 §2.2): 确定性事实模板,\n// 无模型解读; 风暴由签名去重 + 每实例上限兜住; 7 天保留档到期失去召回资格。\n// ---------------------------------------------------------------------------\n\n/** 每实例教训写入上限 (§2.2 风暴闸门): 首个超限事件 warn 一次, 之后静默跳过。 */\nconst MEMORY_LESSON_MAX_PER_SESSION = 10;\n/** 教训 excerpt 字符上限 (错误文本尾部, 按码点切分不劈代理对)。 */\nconst MEMORY_LESSON_ERROR_EXCERPT_MAX_CHARS = 300;\n/** toolCallId→command 配对映射容量 (仅 bash, FIFO)。 */\nconst MEMORY_LESSON_TOOLCALL_MAP_CAPACITY = 32;\n/** 教训开关环境变量 (context 显式值 > env > 默认 on; 非法值取默认并 warn)。 */\nconst MEMORY_LESSONS_ENV = \"AGENT_FORGE_MEMORY_LESSONS\";\n/** 教训 tags (§2.2): 随 FTS body 可检索。 */\nconst MEMORY_LESSON_TAGS: readonly string[] = [\"auto-lesson\", \"bash\"];\n/** 教训提交的保留档 (first-party retention registry 既有 \"short\" = 7 天)。 */\nconst MEMORY_LESSON_RETENTION_MODE_ID = \"short\";\n/**\n * bash 失败退出码的既有后缀 (core/tools/bash.ts: 输出文本 + 该状态行); 解析\n * 不到 (timeout/abort 等) 记 unknown, 仍沉淀。\n */\nconst MEMORY_LESSON_EXIT_CODE_PATTERN = /Command exited with code (\\d+)\\s*$/;\n\n/**\n * Memory tools guide(记忆 runtime 注入面,原 system-prompt.ts MEMORY_TOOL_GUIDE,\n * D-071 迁移):the model-facing hint that the persistent memory tools exist.\n * It rides memory_recall's promptGuidelines so the text lives with the plugin\n * that owns the tools (宪法 §3) and renders whenever the tool face is active.\n */\nexport const MEMORY_TOOLS_GUIDE =\n\t\"Memory: You have persistent memory tools (memory_write, memory_recall, memory_list, memory_forget). Use memory_recall to look up user preferences or project facts before answering. Use memory_write to save important decisions, preferences, or findings for future sessions.\";\n\n/**\n * Session protocol key of the suite binding entry (方案设计 §10.1). The value\n * mirrors the authoritative `SUITE_SESSION_ENTRY_TYPE` in\n * src/profiles/suite-loader.ts — pinned by a deterministic test in\n * test/capabilities-builtin.test.ts. Declared locally so the capability module\n * graph stays free of the assembler's bundled-loader dependency chain.\n */\nconst SUITE_SESSION_ENTRY_TYPE = \"suite\";\n\n/**\n * Owner (isolation principal) of every atom this capability writes. The\n * Foundation owner is the store/facade isolation identity; this product ships\n * one local user per agentDir, so one stable owner id partitions the ledger\n * directory `<agentDir>/memory/<owner>/ledger-<suiteId|\"legacy\">.jsonl`.\n */\nexport const BUILTIN_MEMORY_OWNER = \"local-user\";\n\n/** Contract version written by this capability (@2 atoms carry suiteId). */\nconst MEMORY_CONTRACT_VERSION = MEMORY_CONTRACT_VERSION_V2;\n\nconst TAG_FACET_NAMESPACE = \"builtin.memory\";\nconst EVENT_TYPE = \"memory.changed\";\nconst MAX_TAGS = 8;\nconst DEFAULT_RECALL_LIMIT = 5;\nconst DEFAULT_LIST_LIMIT = 20;\n/** Bounded supplementary relation references per recall (记忆网络接线). */\nconst MAX_RELATED_REFS = 20;\n\n// ---------------------------------------------------------------------------\n// 语义写入合并 (2.4d6 S3, 实施计划 S3 阈值标定定版)。full 档独有: light 档\n// (bge-small-zh) 重复变体/同主题/无关三组余弦分布完全重叠, 余弦不可用作去重\n// 信号 → 不探测不合并 (行为与 S3 之前逐字节一致); off 档整体不注册。\n// ---------------------------------------------------------------------------\n\n/** 合并下限 (bge-m3 实测: 重复变体 0.664–0.833, 同主题 ≤0.652 → 0.72 零误合并)。 */\nconst SEMANTIC_MERGE_T_HIGH = 0.72;\n/** 冲突带下限 (bge-m3 实测: 无关语句最高 0.450 → 0.55 以下零越界)。 */\nconst SEMANTIC_MERGE_T_LOW = 0.55;\n/** 写入探测的同 owner KNN 近邻数 (标定定版: top-5)。 */\nconst SEMANTIC_MERGE_KNN_LIMIT = 5;\n/** supersedes/conflict 关系的 capability 命名空间 (与 tag facet 同域)。 */\nconst SEMANTIC_RELATION_NAMESPACE = \"builtin.memory\";\n\n// ---------------------------------------------------------------------------\n// 语义召回管线 (2.4d6 S2, 记忆系统语义化重设计 §4) 与全局档位 (S1, §3)。\n// 召回唯一路径: embed → KNN+FTS → RRF → 精排 → store 回查; tag 词面 bridge\n// 已退役 (tags 仅入 FTS body 与管理面)。off 档 = 整个 capability 不注册。\n// ---------------------------------------------------------------------------\n\n/** memory_write 内容长度契约 (POC 报告 §7): reranker 512-token 上下文足够。 */\nconst MEMORY_CONTENT_MAX_LENGTH = 700;\n/** FTS / KNN 通道各自取回深度 (定版管线: depth 50)。 */\nconst HYBRID_CHANNEL_DEPTH = 50;\n/** RRF 常数, 与 vector-index 组件 queryFts 打分口径一致。 */\nconst HYBRID_RRF_K = 60;\n/** 融合候选池上限 (定版管线: top20 → 精排 → top limit)。 */\nconst HYBRID_FUSION_POOL = 20;\n/** 单次启动对账 embed+upsert 上限, 超出留待下次启动继续。 */\nconst RECONCILE_MAX_UPSERTS = 2000;\n/** 开启前磁盘余量下限 (设计 §3.2 checking): light ≥ 500MB, full ≥ 2GB。 */\nconst DISK_HEADROOM_MIN_BYTES: Readonly<Record<Exclude<MemoryModeV1, \"off\">, number>> = {\n\tlight: 500 * 1024 * 1024,\n\tfull: 2 * 1024 * 1024 * 1024,\n};\n/**\n * 模型文件大小下限 (防截断/空文件的 sanity 下限, 非精确体积): 高档 embedding\n * (bge-m3 q8) 数百 MB → 下限 256MB, 精排器 (bge-reranker-base q8) ~280MB →\n * 下限 64MB, 低档 (bge-small-zh q8) ~24MB → 下限 8MB。真实校验由加载冒烟承担。\n */\nconst MODEL_MIN_WEIGHT_BYTES: Readonly<Record<Exclude<MemoryModeV1, \"off\">, number>> = {\n\tlight: 8 * 1024 * 1024,\n\tfull: 256 * 1024 * 1024,\n};\nconst RERANKER_MIN_WEIGHT_BYTES = 64 * 1024 * 1024;\n/** 自定义镜像源环境变量 (失败行动清单第一项指向它)。 */\nconst HF_MIRRORS_ENV = \"AGENT_FORGE_HF_MIRRORS\";\n/** enabling 加载/下载期间的周期性进度日志间隔。 */\nconst ENABLING_PROGRESS_LOG_INTERVAL_MS = 15_000;\n\n/** 两档预设 (实施计划 §2): light = 纯中文强项低配档, full = 跨语言大规模默认档。 */\ninterface MemoryVectorPresetV1 {\n\treadonly embeddingModelId: string;\n\treadonly dimensions: number;\n\t/** 查询侧指令前缀 (bge-small-zh 系查询侧指令由调用方负责添加; 写入侧不加)。 */\n\treadonly queryPrefix: string;\n}\n\nconst VECTOR_PRESET_LIGHT: MemoryVectorPresetV1 = {\n\tembeddingModelId: \"Xenova/bge-small-zh-v1.5\",\n\tdimensions: 512,\n\tqueryPrefix: \"为这个句子生成表示以用于检索相关文章:\",\n};\nconst VECTOR_PRESET_FULL: MemoryVectorPresetV1 = {\n\tembeddingModelId: \"Xenova/bge-m3\",\n\tdimensions: 1024,\n\tqueryPrefix: \"\",\n};\n/** 两档共用精排模型 (POC 报告 §8: 官方 Xenova 转换版唯一实测可用)。 */\nconst VECTOR_RERANKER_MODEL_ID = \"Xenova/bge-reranker-base\";\nconst VECTOR_DB_DIRNAME = \"vector\";\nconst VECTOR_MODEL_CACHE_DIRNAME = \"model-cache\";\n/** 镜像序列 (实施计划 §7): 直连 huggingface.co 超时时回落 hf-mirror.com。 */\nconst VECTOR_REMOTE_HOSTS: readonly string[] = [\"https://huggingface.co\", \"https://hf-mirror.com\"];\n\n/**\n * 解析后的全局模式 (2.4d6 S1)。`invalidEnvValue` 非空 = 配置的环境值非法,\n * 已 fail-closed 回 off, 调用方负责以此输出一次 warn; `invalidSettingsValue`\n * 非空 = settings.json 的 `memory.mode` 非法, 同为 fail-closed off 并输出一次\n * 诊断; `legacyEnvUsed` = 模式由旧别名 `AGENT_FORGE_MEMORY_VECTOR` 决定 (兼容\n * 期: 仅当 `AGENT_FORGE_MEMORY_MODE` 与 settings 均未定档时生效, 一个发布周期\n * 后移除)。\n */\nexport interface MemoryModeResolutionV1 {\n\treadonly mode: MemoryModeV1;\n\treadonly invalidEnvValue?: string;\n\treadonly invalidSettingsValue?: string;\n\treadonly legacyEnvUsed?: boolean;\n}\n\nconst isMemoryModeV1 = (value: string): value is MemoryModeV1 =>\n\tvalue === \"off\" || value === \"light\" || value === \"full\";\n\n/**\n * memory.mode 解析 (2.4d6 S1)。优先级: context 显式值 → 环境变量\n * `AGENT_FORGE_MEMORY_MODE` → settings.json `memory.mode` (settingsMode 参, 持\n * 久配置面) → 旧别名 `AGENT_FORGE_MEMORY_VECTOR` (兼容期) → 默认 off。env 是\n * 临时覆盖、settings 是持久配置 (env > 文件, 沿用本仓 suite 解析先例)。任何\n * 非法配置值 fail-closed 为 off 并在结果中携带原值供调用方告警一次, 绝不静默\n * 改档或意外下载。\n */\nexport function resolveMemoryMode(\n\texplicitMode: string | undefined,\n\tenv: Readonly<Record<string, string | undefined>> = process.env,\n\tsettingsMode?: string,\n): MemoryModeResolutionV1 {\n\tif (explicitMode !== undefined) {\n\t\tif (isMemoryModeV1(explicitMode)) return { mode: explicitMode };\n\t\treturn { mode: \"off\", invalidEnvValue: explicitMode };\n\t}\n\tconst configured = env.AGENT_FORGE_MEMORY_MODE?.trim().toLowerCase();\n\tif (configured !== undefined && configured !== \"\") {\n\t\tif (isMemoryModeV1(configured)) return { mode: configured };\n\t\treturn { mode: \"off\", invalidEnvValue: configured };\n\t}\n\t// 持久配置面: settings.json memory.mode。设置但非法同样 fail-closed, 不再\n\t// 回落旧别名 (与非法 env 的先例一致)。\n\tconst fromSettings = settingsMode?.trim().toLowerCase();\n\tif (fromSettings !== undefined && fromSettings !== \"\") {\n\t\tif (isMemoryModeV1(fromSettings)) return { mode: fromSettings };\n\t\treturn { mode: \"off\", invalidSettingsValue: fromSettings };\n\t}\n\t// 兼容期别名: AGENT_FORGE_MEMORY_VECTOR (混合检索工程化的旧开关), 语义并入\n\t// AGENT_FORGE_MEMORY_MODE, 仅当新变量与 settings 均未定档时生效; 一个发布\n\t// 周期后删除。\n\tconst legacy = env.AGENT_FORGE_MEMORY_VECTOR?.trim().toLowerCase();\n\tif (legacy !== undefined && legacy !== \"\") {\n\t\tif (isMemoryModeV1(legacy)) return { mode: legacy, legacyEnvUsed: true };\n\t\treturn { mode: \"off\", invalidEnvValue: legacy, legacyEnvUsed: true };\n\t}\n\treturn { mode: \"off\" };\n}\n\n/** bash 失败教训捕获开关取值 (§2.2)。 */\nexport type MemoryLessonsSettingV1 = \"on\" | \"off\";\n\nconst isMemoryLessonsSettingV1 = (value: string): value is MemoryLessonsSettingV1 => value === \"on\" || value === \"off\";\n\n/**\n * lessons 开关解析结果: `invalidEnvValue` 非空 = 配置值非法, 已 fail 回默认 on,\n * 调用方负责以此告警一次。\n */\nexport interface MemoryLessonsResolutionV1 {\n\treadonly lessons: MemoryLessonsSettingV1;\n\treadonly invalidEnvValue?: string;\n}\n\n/**\n * context.memory.lessons 解析 (§2.2, resolveMemoryMode 同构): 显式值 → 环境变量\n * `AGENT_FORGE_MEMORY_LESSONS` → 默认 on。任何非法配置值回默认并在结果中携带\n * 原值供调用方告警一次, 绝不静默改开关。\n */\nexport function resolveMemoryLessons(\n\texplicitLessons: string | undefined,\n\tenv: Readonly<Record<string, string | undefined>> = process.env,\n): MemoryLessonsResolutionV1 {\n\tif (explicitLessons !== undefined) {\n\t\tif (isMemoryLessonsSettingV1(explicitLessons)) return { lessons: explicitLessons };\n\t\treturn { lessons: \"on\", invalidEnvValue: explicitLessons };\n\t}\n\tconst configured = env[MEMORY_LESSONS_ENV]?.trim().toLowerCase();\n\tif (configured !== undefined && configured !== \"\") {\n\t\tif (isMemoryLessonsSettingV1(configured)) return { lessons: configured };\n\t\treturn { lessons: \"on\", invalidEnvValue: configured };\n\t}\n\treturn { lessons: \"on\" };\n}\n\n/** Host-provided factory context; provided by the host session assembly (sdk suite membership). */\nexport interface MemoryCapabilityContext {\n\t/** Agent home directory — roots the durable ledger under `<agentDir>/memory/`. */\n\treadonly agentDir: string;\n\t/**\n\t * 宿主已解析的 onnxruntime-node 入口绝对路径 (D-075 安装店重依赖可见性):\n\t * 安装店副本无 node_modules, 插件内 createRequire 解析不到; 宿主装载器把\n\t * hostDependencyPaths 注入 manifest config, entry 提取后经本字段传入。\n\t * 缺省 (源码/工作区直载) 走插件内双通道自解析。\n\t */\n\treadonly hostOnnxRuntimeEntryPath?: string;\n\t/** 同上——@lancedb/lancedb 入口路径 (向量索引运行时按绝对路径原生 import)。 */\n\treadonly hostLancedbEntryPath?: string;\n\t/** 同上——@huggingface/transformers 入口路径 (embedding/reranker 运行时用)。 */\n\treadonly hostTransformersEntryPath?: string;\n\t/**\n\t * 记忆全局档位 (2.4d6 S1)。缺省解析顺序: 本字段 → 环境变量\n\t * `AGENT_FORGE_MEMORY_MODE` → {@link MemoryCapabilityContext.memory.settingsMode}\n\t * → 旧别名 `AGENT_FORGE_MEMORY_VECTOR` (兼容期) → 默认 off (整个家族不注册)。\n\t * 装配方 (sdk) 可经本字段显式定档。\n\t */\n\treadonly memory?: {\n\t\treadonly mode?: MemoryModeV1;\n\t\t/**\n\t\t * settings.json `memory.mode` 的原样值 (持久配置面, 优先级低于 env);\n\t\t * 类型面只允许合法档位, 手改文件的非法值原样透传并由\n\t\t * {@link resolveMemoryMode} fail-closed 处理。\n\t\t */\n\t\treadonly settingsMode?: string;\n\t\t/**\n\t\t * bash 失败教训捕获开关 (§2.2): 缺省解析顺序见 {@link resolveMemoryLessons}。\n\t\t */\n\t\treadonly lessons?: MemoryLessonsSettingV1;\n\t};\n\t/**\n\t * 测试专用注入 (架构宪法: deterministic mock AI 门禁)。经装配上下文携带时,\n\t * 装配方原样透传给工厂; 与 {@link createCapabilityWithVectorComponents}\n\t * 的第三参语义一致, 第三参优先。\n\t */\n\treadonly vectorComponents?: MemoryCapabilityOverrideV1;\n\t/**\n\t * 记忆底层组件注入 (扩展位盘点 A1/A2 的宿主装配通道): store/ledger 工厂与\n\t * 向量组件公共契约, 缺省成员走内置默认实现。装配方经\n\t * `CreateAgentSessionOptions.memoryCapability.storage` 携带。\n\t */\n\treadonly storage?: MemoryStorageComponentsV1;\n}\n\nconst isVectorComponentsOverride = (\n\toverride: MemoryCapabilityOverrideV1,\n): override is MemoryVectorComponentsOverrideV1 => \"embedProvider\" in override && \"vectorIndex\" in override;\n\n/** capability 级状态机 (设计 §3.2): off 只出现在模式关闭 (零注册, 不可观测)。 */\nexport type MemoryCapabilityStateV1 = \"enabling\" | \"ready\" | \"degraded\" | \"failed\";\n\n/** enabling 失败的结构化结果: 原因 + 行动清单 (硬性要求, 设计 §3.2)。 */\nexport interface MemoryEnablingFailureV1 {\n\treadonly reason: string;\n\treadonly actions: readonly string[];\n}\n\ninterface ActiveVectorChannelV1 {\n\treadonly embed: MemoryEmbeddingProviderV1;\n\t/** undefined = 精排不可用 (跳过精排, 直接用 RRF 序)。 */\n\treadonly reranker: MemoryRerankerV1 | undefined;\n\treadonly index: MemoryVectorIndexV1;\n\treadonly queryPrefix: string;\n\t/** 预设 embedding modelId — 对账时与投影存储的 modelId 比较, 不一致即清空重建。 */\n\treadonly embeddingModelId: string;\n}\n\ninterface MemoryFact {\n\treadonly memoryId: string;\n\treadonly statement: string;\n\treadonly confidence: number;\n}\n\nfunction assertNonEmptyString(value: unknown, label: string): asserts value is string {\n\tif (typeof value !== \"string\" || value.trim().length === 0) throw new Error(`${label} must be a non-empty string`);\n}\n\nfunction isPlainRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * 双约定工具输入读取(对齐 subagent-delegate 的 objectInput):capability invoke\n * 面单参约定下第一参即输入(第二参是宿主注入的 toolExecutionContext),legacy 5 参\n * 约定(toolCallId 字符串打头)下输入在第二参。生产装配恒向 runtime invoke 面注入\n * toolExecutionContext 作为 execute 第二参(公共契约),因此\"第二参非 undefined 即\n * 输入\"的判定会把 context 误当输入(content/query 等读成 undefined)。改按形状判定:\n * 第一参是普通对象即取第一参,否则看第二参;两者皆非对象时返回空记录,交由各工具\n * 的字段断言报错。\n */\nfunction memoryToolInput(first: unknown, second: unknown): Record<string, unknown> {\n\tconst primary = isPlainRecord(first) ? first : second;\n\treturn isPlainRecord(primary) ? primary : {};\n}\n\n/** FNV-1a 32-bit — deterministic identity from the given value (code-memory 同构). */\nfunction identityHash(value: string): string {\n\tlet hash = 0x811c9dc5;\n\tfor (let index = 0; index < value.length; index += 1) {\n\t\thash ^= value.charCodeAt(index);\n\t\thash = Math.imul(hash, 0x01000193);\n\t}\n\treturn (hash >>> 0).toString(36);\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n\treturn value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\n\n/**\n * Text of the branch's LAST user message — the auto-recall query and per-turn\n * cache key (B1 记忆自动注入). Read from the persisted branch, not the\n * request-time snapshot: request-time injections (the sdk's background\n * delegation digest) ride the transformContext input as user-role blocks and\n * would otherwise hijack the query, while the branch never sees them.\n */\nfunction lastBranchUserText(api: CapabilityAPI): string | undefined {\n\tconst entries = api.session?.getBranchEntries() ?? [];\n\tfor (let index = entries.length - 1; index >= 0; index -= 1) {\n\t\tconst entry = entries[index];\n\t\tif (entry.type !== \"message\" || entry.message?.role !== \"user\") continue;\n\t\tconst content: unknown = entry.message.content;\n\t\tif (typeof content === \"string\") return content;\n\t\tif (!Array.isArray(content)) continue;\n\t\tconst parts: string[] = [];\n\t\tfor (const item of content) {\n\t\t\tif (isPlainObject(item) && item.type === \"text\" && typeof item.text === \"string\") parts.push(item.text);\n\t\t}\n\t\treturn parts.join(\"\\n\");\n\t}\n\treturn undefined;\n}\n\n/**\n * Inserts the recall injection right after the last user message of the\n * request snapshot (B1 记忆自动注入): the recalled context is turn-scoped, so\n * it belongs at the head of the current turn, ahead of any assistant/\n * toolResult round already in flight. Purely a request-time view — the\n * persisted session history stays untouched.\n */\nfunction injectAfterLastUserMessage(messages: readonly unknown[], injection: unknown): readonly unknown[] {\n\tfor (let index = messages.length - 1; index >= 0; index -= 1) {\n\t\tif ((messages[index] as { readonly role?: string } | undefined)?.role === \"user\") {\n\t\t\treturn [...messages.slice(0, index + 1), injection, ...messages.slice(index + 1)];\n\t\t}\n\t}\n\treturn messages;\n}\n\n/**\n * Tail of `text` whose UTF-16 length never exceeds `limit`, cut on code-point\n * boundaries so a surrogate pair never splits. The excerpt budget is measured\n * in UTF-16 units (`String.length`, the same measure as the 700-char content\n * contract), so counting code points here would let astral-plane tails double\n * the rendered excerpt and break the cap.\n */\nfunction tailWithinUtf16Budget(text: string, limit: number): string {\n\tif (limit <= 0) return \"\";\n\tif (text.length <= limit) return text;\n\tconst codePoints = Array.from(text);\n\tlet units = 0;\n\tlet start = codePoints.length;\n\twhile (start > 0 && units + codePoints[start - 1].length <= limit) {\n\t\tunits += codePoints[start - 1].length;\n\t\tstart -= 1;\n\t}\n\treturn codePoints.slice(start).join(\"\");\n}\n\n/** bash 失败退出码解析 (core/tools/bash.ts 的既有后缀); 解析不到 (timeout/abort) = \"unknown\"。 */\nfunction lessonExitCodeOf(errorText: string): string {\n\treturn MEMORY_LESSON_EXIT_CODE_PATTERN.exec(errorText)?.[1] ?? \"unknown\";\n}\n\n/** Concatenated text of a toolResult message's text blocks (images skipped). */\nfunction toolResultTextOf(content: MemoryToolResultEventDataV1[\"message\"][\"content\"]): string {\n\tconst parts: string[] = [];\n\tfor (const block of content) {\n\t\tif (block.type === \"text\") parts.push(block.text);\n\t}\n\treturn parts.join(\"\\n\");\n}\n\n/**\n * 教训内容 (§2.2 确定性事实模板, 无模型解读): `Command failed: \\`<command>\\`\n * exited with code <N>. Output tail: <excerpt>`。excerpt 取错误文本尾部, 预算\n * 按 UTF-16 单元计 ({@link MEMORY_LESSON_ERROR_EXCERPT_MAX_CHARS} 与 700 上限\n * 同一口径); 超长只压缩 excerpt (excerpt 压到 0 仍超限的极端长命令再压 command\n * 尾部, 保持模板可解析)。\n */\nfunction buildLessonContent(command: string, exitCode: string, errorText: string): string {\n\tconst render = (commandPart: string, excerpt: string): string =>\n\t\t`Command failed: \\`${commandPart}\\` exited with code ${exitCode}. Output tail: ${excerpt}`;\n\tconst excerptLimit = Math.min(\n\t\tMEMORY_LESSON_ERROR_EXCERPT_MAX_CHARS,\n\t\tMEMORY_CONTENT_MAX_LENGTH - render(command, \"\").length,\n\t);\n\tif (excerptLimit > 0) return render(command, tailWithinUtf16Budget(errorText, excerptLimit));\n\tconst commandBudget = MEMORY_CONTENT_MAX_LENGTH - render(\"\", \"\").length;\n\treturn render(command.slice(0, Math.max(0, commandBudget)), \"\");\n}\n\n/**\n * Suite binding entry ids already warned about (统一修复轮终审 minor 3):\n * resolveSessionDomain runs on EVERY memory tool call, so without this\n * module-level set one malformed binding entry would repeat the warning on\n * each call and flood the log.\n */\nconst malformedBindingWarned = new Set<string>();\n\n/**\n * Resolves the session's suite binding domain from the branch entries: the\n * LAST suite entry wins (the sdk appends one at session creation). A binding\n * with a malformed payload counts as unbound — the legacy fallback is\n * fail-safe (its atoms are invisible to every real suite) and a diagnostic is\n * logged when the logger capability is available. Called per tool call —\n * never captured at factory time (统一修复轮 B1: the binding entry is appended\n * after the session constructor ran this factory).\n */\nfunction resolveSessionDomain(api: CapabilityAPI): SuiteMemoryDomainV1 {\n\tconst entries = api.session?.getBranchEntries() ?? [];\n\tfor (let index = entries.length - 1; index >= 0; index -= 1) {\n\t\tconst entry = entries[index];\n\t\tif (entry.type !== \"custom\" || entry.customType !== SUITE_SESSION_ENTRY_TYPE) continue;\n\t\tconst data = entry.data;\n\t\tconst suiteId = isPlainObject(data) ? data.suiteId : undefined;\n\t\tif (typeof suiteId === \"string\" && suiteId.trim() !== \"\") {\n\t\t\treturn { kind: \"suite\", suiteId };\n\t\t}\n\t\tif (!malformedBindingWarned.has(entry.id)) {\n\t\t\tmalformedBindingWarned.add(entry.id);\n\t\t\tapi.logger?.warn(\"suite binding entry has a malformed payload; falling back to the legacy memory domain\", {\n\t\t\t\tentryId: entry.id,\n\t\t\t});\n\t\t}\n\t\treturn { kind: \"legacy\" };\n\t}\n\treturn { kind: \"legacy\" };\n}\n\n/** Recency-first payload projection: unwraps the `{ statement }` payload convention. */\nfunction statementOf(payload: unknown): string {\n\tif (isPlainObject(payload) && typeof payload.statement === \"string\") return payload.statement;\n\treturn JSON.stringify(payload);\n}\n\n/** The egress gate re-derives statements as payload JSON; unwrap for the model face. */\nfunction unwrapEgressStatement(statement: string): string {\n\ttry {\n\t\tconst parsed: unknown = JSON.parse(statement);\n\t\tif (isPlainObject(parsed) && typeof parsed.statement === \"string\") return parsed.statement;\n\t} catch {\n\t\t// Not payload JSON — surface it verbatim.\n\t}\n\treturn statement;\n}\n\n/**\n * 三通道 RRF 融合 (定版管线): `score(id) += 1/(k + rank + 1)` 对各通道排名累加,\n * 取 top {@link HYBRID_FUSION_POOL}。分数并列时以 memoryId 升序打破, 保证确定性。\n */\nfunction rrfFuseRankings(rankings: readonly (readonly string[])[]): readonly string[] {\n\tconst scores = new Map<string, number>();\n\tfor (const ranking of rankings) {\n\t\tfor (const [rank, memoryId] of ranking.entries()) {\n\t\t\tscores.set(memoryId, (scores.get(memoryId) ?? 0) + 1 / (HYBRID_RRF_K + rank + 1));\n\t\t}\n\t}\n\treturn [...scores.entries()]\n\t\t.sort((left, right) => right[1] - left[1] || (left[0] < right[0] ? -1 : 1))\n\t\t.map(([memoryId]) => memoryId)\n\t\t.slice(0, HYBRID_FUSION_POOL);\n}\n\n/** 语义合并判定结论 (投影探测产物; relations 随新 atom 一次性提交, atom 不可变)。 */\ntype SemanticMergePlanV1 =\n\t| { readonly kind: \"merge\"; readonly targetMemoryId: string }\n\t| { readonly kind: \"conflict\"; readonly conflictMemoryIds: readonly string[] }\n\t| { readonly kind: \"new\" };\n\n/** 探测产物: 判定结论 + 新 statement 的嵌入 (投影 upsert 复用同一向量)。 */\ninterface SemanticMergeOutcomeV1 {\n\treadonly plan: SemanticMergePlanV1;\n\treadonly vector: readonly number[];\n}\n\n/** 探测到的近邻 (canonical 回查后): 既有 tags + 与新向量的余弦。 */\ninterface SemanticMergeNeighborV1 {\n\treadonly memoryId: string;\n\treadonly tags: readonly string[];\n\treadonly cosine: number;\n}\n\n/** 嵌入向量已 L2 归一化 (provider 契约), 点积即余弦。 */\nfunction cosineSimilarity(left: readonly number[], right: readonly number[]): number {\n\tlet dot = 0;\n\tfor (const [axis, value] of left.entries()) dot += value * (right[axis] ?? 0);\n\treturn dot;\n}\n\n/** tags 重叠判定 (至少一个共同 tag, trim 后大小写不敏感)。 */\nfunction tagsOverlapIgnoreCase(left: readonly string[], right: readonly string[]): boolean {\n\tif (left.length === 0 || right.length === 0) return false;\n\tconst rightSet = new Set(right.map((tag) => tag.trim().toLowerCase()));\n\treturn left.some((tag) => rightSet.has(tag.trim().toLowerCase()));\n}\n\n/**\n * 语义合并判定 (实施计划 S3 标定定版, 三分支):\n * - 合并: cos ≥ 0.72, 或 0.55 ≤ cos < 0.72 且新 tags 与既有 tags 有重叠\n * (复合条件救回跨语言改写 — 余弦偏低但 tags 同源; 取首个满足条件的近邻);\n * - 冲突带: 0.55 ≤ cos < 0.72 且无 tag 重叠 → 双存 + conflict 关系\n * (单向声明在新 atom 上; 正确性优先于存储省略, 宁可双存不错杀);\n * - 低相似/无近邻: 普通新条目。\n * 近邻按 KNN 序传入。\n */\nfunction decideSemanticMergePlan(input: {\n\treadonly newTags: readonly string[];\n\treadonly neighbors: readonly SemanticMergeNeighborV1[];\n}): SemanticMergePlanV1 {\n\tfor (const neighbor of input.neighbors) {\n\t\tconst merged =\n\t\t\tneighbor.cosine >= SEMANTIC_MERGE_T_HIGH ||\n\t\t\t(neighbor.cosine >= SEMANTIC_MERGE_T_LOW && tagsOverlapIgnoreCase(input.newTags, neighbor.tags));\n\t\tif (merged) return { kind: \"merge\", targetMemoryId: neighbor.memoryId };\n\t}\n\tconst conflictMemoryIds = input.neighbors\n\t\t.filter((neighbor) => neighbor.cosine >= SEMANTIC_MERGE_T_LOW)\n\t\t.map((neighbor) => neighbor.memoryId);\n\treturn conflictMemoryIds.length > 0 ? { kind: \"conflict\", conflictMemoryIds } : { kind: \"new\" };\n}\n\n/**\n * 判定结论 → 新 atom 的 relations (canonical 侧合并/冲突痕迹)。canonical\n * atom 不可变且 Foundation 无\"更新 payload\"操作, 关系只在新 atom 上单向声明\n * (可发现语义由此满足): supersedes 指向被合并的旧条目, conflict 指向冲突带\n * 近邻。ids 由 (kind, target, content) 确定性派生。\n */\nfunction semanticMergeRelations(plan: SemanticMergePlanV1, content: string): readonly MemoryRelationV1[] {\n\tconst relation = (kind: \"supersedes\" | \"conflict\", targetMemoryId: string): MemoryRelationV1 => {\n\t\tconst relationId = `rel-${identityHash(`${kind}\\u0000${targetMemoryId}\\u0000${content}`)}`;\n\t\treturn {\n\t\t\trelationId,\n\t\t\tnamespace: SEMANTIC_RELATION_NAMESPACE,\n\t\t\tschemaVersion: 1,\n\t\t\tkind,\n\t\t\ttargetMemoryId,\n\t\t\trelationRevision: `r-${identityHash(relationId)}`,\n\t\t};\n\t};\n\tif (plan.kind === \"merge\") return [relation(\"supersedes\", plan.targetMemoryId)];\n\tif (plan.kind === \"conflict\") return plan.conflictMemoryIds.map((target) => relation(\"conflict\", target));\n\treturn [];\n}\n\n/** 档位预设: mode → 模型组合。off 在进入装配前已被门控拦截。 */\nfunction vectorPresetForMode(mode: Exclude<MemoryModeV1, \"off\">): MemoryVectorPresetV1 {\n\treturn mode === \"light\" ? VECTOR_PRESET_LIGHT : VECTOR_PRESET_FULL;\n}\n\n/**\n * 自定义镜像源 (失败行动清单第一项): `AGENT_FORGE_HF_MIRRORS` 逗号分隔 host\n * 列表, 排在默认镜像序列之前优先尝试。\n */\nfunction remoteHostsForEnabling(): readonly string[] {\n\tconst custom = process.env[HF_MIRRORS_ENV]\n\t\t?.split(\",\")\n\t\t.map((host) => host.trim())\n\t\t.filter((host) => host !== \"\");\n\treturn custom === undefined || custom.length === 0 ? VECTOR_REMOTE_HOSTS : [...custom, ...VECTOR_REMOTE_HOSTS];\n}\n\n/**\n * 本地检查 1: native .node sidecar 可解析 (设计 §3.2 checking)。布局:\n * onnxruntime-node/bin 下的 napi-vN/<platform>/<arch> 原生模块 — 缺失 = 安装\n * 损坏。容错: 不钉死 napi 版本号, 逐目录扫描。\n *\n * 解析基点双通道: jiti(安装店加载缝)注入的 `require` 走宿主 alias 表\n * (宿主 node_modules 副本, 店内无依赖树); 纯 ESM(源码/工作区直载)下裸\n * `require` 未定义(typeof 探测不抛 ReferenceError), 回落原生 createRequire。\n */\nfunction resolveOnnxRuntimeEntryPath(): string {\n\tif (typeof require === \"function\") {\n\t\ttry {\n\t\t\treturn require.resolve(\"onnxruntime-node\");\n\t\t} catch {\n\t\t\t// jiti 解析失败(alias 未覆盖等)→ 继续走原生解析再报结构化错误。\n\t\t}\n\t}\n\treturn createRequire(import.meta.url).resolve(\"onnxruntime-node\");\n}\n\nfunction nativeBinaryCheck(hostEntryPath?: string): string | undefined {\n\t// 两种失败要区分 (D-075 安装店路径修复后前者成为真实可达路径):\n\t// (a) 宿主未注入入口路径且插件位置自解析不到 — 不是安装损坏, 行动是宿主侧\n\t// 安装重依赖后重启会话, 不是重装插件;\n\t// (b) 路径存在但原生二进制缺失/损坏 — 保留损坏类文案。\n\tlet entryPath: string;\n\tif (hostEntryPath === undefined) {\n\t\ttry {\n\t\t\tentryPath = resolveOnnxRuntimeEntryPath();\n\t\t} catch (error) {\n\t\t\tconst detail = error instanceof Error ? error.message : String(error);\n\t\t\treturn `onnxruntime-node is not provided by the host installation and could not be resolved from the plugin location (${detail}); to enable memory features, install onnxruntime-node in the host project (npm i onnxruntime-node) and restart the session`;\n\t\t}\n\t} else {\n\t\tentryPath = hostEntryPath;\n\t}\n\ttry {\n\t\tlet packageDir = dirname(entryPath);\n\t\twhile (packageDir !== dirname(packageDir) && !existsSync(join(packageDir, \"package.json\"))) {\n\t\t\tpackageDir = dirname(packageDir);\n\t\t}\n\t\tconst binDir = join(packageDir, \"bin\");\n\t\tif (!existsSync(binDir)) return `onnxruntime native binary directory not found: ${binDir}`;\n\t\tfor (const napiEntry of readdirSync(binDir, { withFileTypes: true })) {\n\t\t\tif (!napiEntry.isDirectory() || !napiEntry.name.startsWith(\"napi-v\")) continue;\n\t\t\tconst platformDir = join(binDir, napiEntry.name, process.platform, process.arch);\n\t\t\tif (!existsSync(platformDir)) continue;\n\t\t\tconst binaries = readdirSync(platformDir).filter((file) => file.endsWith(\".node\"));\n\t\t\tif (binaries.length > 0) return undefined;\n\t\t}\n\t\treturn `onnxruntime native binary for ${process.platform}/${process.arch} not found under ${binDir}`;\n\t} catch (error) {\n\t\treturn `onnxruntime native binary could not be resolved (${error instanceof Error ? error.message : String(error)}); the installation looks damaged — reinstall the package`;\n\t}\n}\n\n/**\n * 重依赖自解析默认实现 (missingHeavyDependencyModules 生产接线): 双通道与\n * {@link resolveOnnxRuntimeEntryPath} 一致 — jiti(安装店加载缝)注入的\n * `require` 走宿主 alias 表; 纯 ESM(源码/工作区直载)下回落原生 createRequire。\n */\nfunction resolveHeavyDependencyEntryPath(specifier: string): string {\n\tif (typeof require === \"function\") {\n\t\ttry {\n\t\t\treturn require.resolve(specifier);\n\t\t} catch {\n\t\t\t// jiti 解析失败(alias 未覆盖等)→ 继续走原生解析。\n\t\t}\n\t}\n\treturn createRequire(import.meta.url).resolve(specifier);\n}\n\n/**\n * 本地检查 1b: 重依赖供给 (设计 §3.2 checking)。`@lancedb/lancedb` 与\n * `@huggingface/transformers` 逐个判定 — entries 提供宿主入口路径 = 可用\n * (不触自解析); 未提供时 resolveModule 自解析成功 = 可用, 抛错 = 计入缺失。\n * 返回缺失 specifier 列表 (空 = 通过)。纯函数零 IO; 导出仅供测试 (与宿主侧\n * buildHostDependencyAliases 同一模式), 生产接线传插件位置自解析 — 把加载步\n * 的 \"Cannot find module\" 泛化包装前置成精准缺供文案, 省掉模型下载等前置工作。\n */\nexport function missingHeavyDependencyModules(\n\tentries: { lancedb?: string; transformers?: string },\n\tresolveModule: (specifier: string) => string,\n): string[] {\n\tconst missing: string[] = [];\n\tconst supplies: readonly (readonly [specifier: string, entryPath: string | undefined])[] = [\n\t\t[\"@lancedb/lancedb\", entries.lancedb],\n\t\t[\"@huggingface/transformers\", entries.transformers],\n\t];\n\tfor (const [specifier, entryPath] of supplies) {\n\t\tif (entryPath !== undefined) continue;\n\t\ttry {\n\t\t\tresolveModule(specifier);\n\t\t} catch {\n\t\t\tmissing.push(specifier);\n\t\t}\n\t}\n\treturn missing;\n}\n\n/**\n * transformers.js 缓存布局的容错存在性检查 (设计 §3.2 手动导入路径):\n * `<cacheDir>/<modelId>/` 下需有非空 tokenizer 文件与 `onnx/` 权重文件。\n * 返回 undefined = 检查通过; \"missing\" = 目录/文件缺失或仅剩 0 字节残骸\n * (失败下载产物, 清理后进入下载阶段, 见 model-cache-hygiene); 其他字符串 =\n * 文件存在但可疑 (截断/非空过小), 属失败。\n */\nfunction modelFilesCheck(cacheDir: string, modelId: string, minWeightBytes: number): string | undefined {\n\tconst modelDir = join(cacheDir, modelId);\n\tif (!existsSync(modelDir)) return \"missing\";\n\t// tokenizer 只剩 0 字节文件 = 失败下载残骸 (真 tokenizer.json 不可能为空):\n\t// 清掉按缺失处理可重下; 非空文件不动 (手动导入布局不受影响)。\n\tconst tokenizerNames = [\"tokenizer.json\", \"tokenizer.model\"].filter((name) => existsSync(join(modelDir, name)));\n\tconst hasTokenizer = tokenizerNames.some((name) => statSync(join(modelDir, name)).size > 0);\n\tif (!hasTokenizer) {\n\t\tif (tokenizerNames.length > 0) purgeZeroByteCacheArtifacts(modelDir);\n\t\treturn \"missing\";\n\t}\n\tconst onnxDir = join(modelDir, \"onnx\");\n\tif (!existsSync(onnxDir)) return `onnx weight directory missing under ${modelDir}`;\n\tconst weights = readdirSync(onnxDir).filter((name) => name.includes(\".onnx\"));\n\tif (weights.length === 0) return `no onnx weight file under ${onnxDir}`;\n\tconst largest = Math.max(...weights.map((name) => statSync(join(onnxDir, name)).size));\n\tif (largest < minWeightBytes) {\n\t\treturn `largest onnx weight under ${onnxDir} is ${largest} bytes, below the expected minimum ${minWeightBytes} — the download looks truncated`;\n\t}\n\treturn undefined;\n}\n\n/**\n * 本地检查 3: 磁盘余量 (设计 §3.2 checking, node:fs statfs, 零新依赖)。\n * 以 agentDir 所在卷为准 (cacheDir 同卷创建)。\n */\nasync function diskHeadroomCheck(agentDir: string, minimumBytes: number): Promise<string | undefined> {\n\tlet freeBytes: number;\n\ttry {\n\t\tconst stats = await statfs(agentDir);\n\t\tfreeBytes = stats.bfree * stats.bsize;\n\t} catch (error) {\n\t\treturn `disk free space could not be determined for ${agentDir} (${error instanceof Error ? error.message : String(error)})`;\n\t}\n\tif (freeBytes < minimumBytes) {\n\t\treturn `disk free space is ${freeBytes} bytes, below the required ${minimumBytes}`;\n\t}\n\treturn undefined;\n}\n\n/** enabling 失败的五选一行动清单 (设计 §3.2 硬性要求)。 */\nfunction enablingActionList(modelCacheDir: string, currentMode: MemoryModeV1): readonly string[] {\n\treturn [\n\t\t`Switch to a reachable mirror: set ${HF_MIRRORS_ENV} to a comma-separated host list (tried before the defaults)`,\n\t\t`Install the heavy dependencies in the host project (npm i onnxruntime-node @lancedb/lancedb @huggingface/transformers) so the host can supply them via hostDependencyPaths, then restart the session`,\n\t\t`Download the models manually and place them under ${modelCacheDir} (layout: <modelId>/tokenizer.json + <modelId>/onnx/model*.onnx)`,\n\t\tcurrentMode === \"full\"\n\t\t\t? 'Downgrade to memory.mode \"light\" (much smaller models)'\n\t\t\t: 'Keep memory.mode \"light\" and retry with a reachable mirror',\n\t\t'Give up: leave memory.mode \"off\" (the default) so the memory family stays unregistered',\n\t];\n}\n\n/** One unresolvably competing preference, surfaced verbatim instead of a silent pick. */\ninterface RecallConflictV1 {\n\treadonly subject: MemoryPreferenceEnvelopeV1[\"subject\"];\n\treadonly key: string;\n\treadonly candidates: readonly {\n\t\treadonly memoryId: string;\n\t\treadonly statement: string;\n\t\treadonly value: JsonValue;\n\t\treadonly evidenceClass: MemoryPreferenceEnvelopeV1[\"evidence\"][\"class\"];\n\t\treadonly confidence: number;\n\t}[];\n\treadonly explanation: readonly string[];\n}\n\n/**\n * Parses the optional recall context (scenes/projects/tasks) into a preference\n * resolution context. Returns undefined when absent so the no-context recall\n * path stays byte-identical to the pre-disambiguation behavior.\n */\nfunction parseRecallContext(value: unknown): PreferenceContextV1 | undefined {\n\tif (value === undefined) return undefined;\n\tif (!isPlainObject(value)) throw new Error(\"context must be an object\");\n\tconst stringArray = (field: \"scenes\" | \"projects\" | \"tasks\"): readonly string[] | undefined => {\n\t\tconst entries: unknown = value[field];\n\t\tif (entries === undefined) return undefined;\n\t\tif (!Array.isArray(entries)) throw new Error(`context.${field} must be an array of strings`);\n\t\tfor (const entry of entries) {\n\t\t\tif (typeof entry !== \"string\") throw new Error(`context.${field} entries must be strings`);\n\t\t}\n\t\treturn entries;\n\t};\n\tconst scenes = stringArray(\"scenes\");\n\tconst projects = stringArray(\"projects\");\n\tconst tasks = stringArray(\"tasks\");\n\treturn {\n\t\t...(scenes === undefined ? {} : { scenes }),\n\t\t...(projects === undefined ? {} : { projects }),\n\t\t...(tasks === undefined ? {} : { tasks }),\n\t};\n}\n\n/**\n * Context-scoped preference post-processing over the recalled facts: preference\n * atoms compete per (subject, key); the resolver drops out-of-scope preferences\n * and deterministically ranks the rest, and an unconfirmed ranking goes through\n * the scope-specificity disambiguator. Whatever remains unresolvable is\n * returned as an explicit conflict block — the recall never silently picks\n * between competing stored values. Non-preference facts pass through untouched.\n */\nfunction applyPreferenceContext(input: {\n\treadonly facts: readonly MemoryFact[];\n\treadonly context: PreferenceContextV1;\n\treadonly resolver: PreferenceResolverV1;\n\treadonly disambiguator: PreferenceDisambiguatorV1;\n\treadonly atomOf: (memoryId: string) => MemoryAtomV1 | undefined;\n}): { readonly facts: readonly MemoryFact[]; readonly conflicts: readonly RecallConflictV1[] } {\n\tconst { facts, context, resolver, disambiguator, atomOf } = input;\n\tconst preferenceAtoms: MemoryAtomV1[] = [];\n\tconst atomsByGroup = new Map<string, MemoryAtomV1[]>();\n\tconst preferenceMemoryIds = new Set<string>();\n\tfor (const fact of facts) {\n\t\tconst atom = atomOf(fact.memoryId);\n\t\tif (atom === undefined || atom.memoryKind !== \"preference\" || atom.preference === undefined) continue;\n\t\tpreferenceMemoryIds.add(fact.memoryId);\n\t\tpreferenceAtoms.push(atom);\n\t\tconst groupKey = `${atom.preference.subject}\\u0000${atom.preference.key}`;\n\t\tconst group = atomsByGroup.get(groupKey) ?? [];\n\t\tgroup.push(atom);\n\t\tatomsByGroup.set(groupKey, group);\n\t}\n\tif (preferenceMemoryIds.size === 0) return { facts, conflicts: [] };\n\n\tconst keptMemoryIds = new Set<string>();\n\tconst conflicts: RecallConflictV1[] = [];\n\tfor (const decision of resolver.resolve(preferenceAtoms, context)) {\n\t\tconst groupAtoms = atomsByGroup.get(`${decision.subject}\\u0000${decision.key}`) ?? [];\n\t\tif (decision.status === \"applied\") {\n\t\t\tif (decision.winnerMemoryId === undefined) {\n\t\t\t\tthrow new Error(`applied preference decision without a winnerMemoryId: ${JSON.stringify(decision)}`);\n\t\t\t}\n\t\t\tkeptMemoryIds.add(decision.winnerMemoryId);\n\t\t\tcontinue;\n\t\t}\n\t\tif (decision.status === \"instruction_override\") {\n\t\t\t// No stored fact survives a current-instruction override (this slice has\n\t\t\t// no input source for currentInstructions, so the branch stays inert).\n\t\t\tcontinue;\n\t\t}\n\t\tif (decision.status === \"conflict_unconfirmed\") {\n\t\t\tconst outcome = disambiguator.disambiguate(\n\t\t\t\tgroupAtoms.map((atom) => ({\n\t\t\t\t\tmemoryId: atom.memoryId,\n\t\t\t\t\tpreferredValue: atom.preference!.preferredValue,\n\t\t\t\t\tscope: atom.preference!.scope,\n\t\t\t\t\tevidenceClass: atom.preference!.evidence.class,\n\t\t\t\t\tapplicabilityConfidence: atom.preference!.applicabilityConfidence,\n\t\t\t\t})),\n\t\t\t\tcontext,\n\t\t\t);\n\t\t\tif (outcome.status === \"resolved\") {\n\t\t\t\tkeptMemoryIds.add(outcome.winnerMemoryId);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconflicts.push({\n\t\t\t\tsubject: decision.subject,\n\t\t\t\tkey: decision.key,\n\t\t\t\tcandidates: groupAtoms.map((atom) => {\n\t\t\t\t\tconst preference = atom.preference!;\n\t\t\t\t\treturn {\n\t\t\t\t\t\tmemoryId: atom.memoryId,\n\t\t\t\t\t\tstatement: unwrapEgressStatement(statementOf(atom.payload)),\n\t\t\t\t\t\tvalue: preference.preferredValue,\n\t\t\t\t\t\tevidenceClass: preference.evidence.class,\n\t\t\t\t\t\tconfidence: preference.applicabilityConfidence,\n\t\t\t\t\t};\n\t\t\t\t}),\n\t\t\t\texplanation: [...outcome.explanation],\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\t\tthrow new Error(`unexpected preference decision status: ${JSON.stringify(decision)}`);\n\t}\n\n\treturn {\n\t\tfacts: facts.filter((fact) => !preferenceMemoryIds.has(fact.memoryId) || keptMemoryIds.has(fact.memoryId)),\n\t\tconflicts,\n\t};\n}\n\nfunction tagFacetsOf(atom: MemoryAtomV1): string[] {\n\treturn atom.facets\n\t\t.filter((facet) => facet.namespace === TAG_FACET_NAMESPACE && facet.key === \"tag\")\n\t\t.map((facet) => facet.value);\n}\n\nfunction entryToToolEntry(entry: SuiteMemoryListEntryV1): Record<string, unknown> {\n\treturn {\n\t\tmemoryId: entry.memoryId,\n\t\tkind: entry.memoryKind,\n\t\tstatement: statementOf(entry.payload),\n\t\toccurredAt: entry.occurredAt,\n\t\t...(entry.preference === undefined\n\t\t\t? {}\n\t\t\t: {\n\t\t\t\t\tpreference: {\n\t\t\t\t\t\tsubject: entry.preference.subject,\n\t\t\t\t\t\tkey: entry.preference.key,\n\t\t\t\t\t\tscopeLevel: entry.preference.scopeLevel,\n\t\t\t\t\t\tcrossSuiteVisible: entry.preference.crossSuiteVisible,\n\t\t\t\t\t},\n\t\t\t\t}),\n\t};\n}\n\nconst writeSchema = Type.Object({\n\tcontent: Type.String({\n\t\tdescription: \"The memory content: one self-contained fact or preference statement\",\n\t\tmaxLength: MEMORY_CONTENT_MAX_LENGTH,\n\t}),\n\tkind: Type.Union([Type.Literal(\"fact\"), Type.Literal(\"preference\")], {\n\t\tdescription: \"fact = a durable piece of information; preference = a user preference (requires subject)\",\n\t}),\n\tsubject: Type.Optional(\n\t\tType.Union([Type.Literal(\"user\"), Type.Literal(\"project\"), Type.Literal(\"task\"), Type.Literal(\"environment\")], {\n\t\t\tdescription: \"Required when kind is preference: whose preference this is\",\n\t\t}),\n\t),\n\ttags: Type.Optional(\n\t\tType.Array(Type.String({ minLength: 1 }), {\n\t\t\tdescription:\n\t\t\t\t\"Optional short keywords; stored with the memory, full-text searchable and shown in memory_list; at most 8\",\n\t\t\tmaxItems: MAX_TAGS,\n\t\t}),\n\t),\n});\n\nconst recallSchema = Type.Object({\n\tquery: Type.String({\n\t\tdescription:\n\t\t\t\"Natural-language query; recall fuses semantic (vector) and full-text (BM25) matching and reranks the result. An exact memoryId retrieves that memory directly\",\n\t}),\n\tlimit: Type.Optional(\n\t\tType.Integer({ description: \"Maximum number of memories to return (1-100)\", minimum: 1, maximum: 100 }),\n\t),\n\tcontext: Type.Optional(\n\t\tType.Object(\n\t\t\t{\n\t\t\t\tscenes: Type.Optional(Type.Array(Type.String())),\n\t\t\t\tprojects: Type.Optional(Type.Array(Type.String())),\n\t\t\t\ttasks: Type.Optional(Type.Array(Type.String())),\n\t\t\t},\n\t\t\t{\n\t\t\t\tdescription:\n\t\t\t\t\t\"Current scope context; when present, recalled preferences are resolved against it (out-of-scope preferences drop, competing values resolve by scope specificity or surface as explicit conflicts)\",\n\t\t\t},\n\t\t),\n\t),\n});\n\nconst listSchema = Type.Object({\n\tlimit: Type.Optional(\n\t\tType.Integer({ description: \"Maximum number of entries to list (1-100)\", minimum: 1, maximum: 100 }),\n\t),\n});\n\nconst forgetSchema = Type.Object({\n\tmemoryId: Type.String({\n\t\tdescription: \"The memoryId of the memory to forget, as returned by memory_write or memory_list\",\n\t}),\n});\n\n/**\n * Creates the builtin memory capability: four tools (memory_write,\n * memory_recall, memory_list, memory_forget) over a per-session Foundation\n * stack whose durable replica is the per-domain JSONL ledger under\n * `<agentDir>/memory/<owner>/ledger-<domain>.jsonl`, resolved lazily at each\n * tool call (统一修复轮 B1).\n *\n * `memory.mode` gates the whole family (2.4d6 S1): a resolved mode of \"off\"\n * registers NOTHING (zero tools, zero commands, zero events) — the factory\n * returns an empty registration list and logs the one-line disabled\n * declaration. suite capability membership decides earlier at the assembly\n * level (设计 §3.2 \"禁用 = 不注册\").\n *\n * `override` is the test-only seam (deterministic mock AI 门禁): full vector\n * components skip the real transformers/lancedb assembly entirely; enabling\n * seams keep the real assembly but replace the transformers loader.\n */\nexport function createMemoryCapability(\n\tapi: CapabilityAPI,\n\tcontext: MemoryCapabilityContext,\n\toverride?: MemoryCapabilityOverrideV1,\n): readonly DisposableRegistration[] {\n\tconst harness = createMemoryCapabilityHarness(api, context, override);\n\treturn harness.registrations;\n}\n\n/** Test-only harness over the capability: registration list plus state-work hooks. */\nexport interface MemoryVectorCapabilityHarnessV1 {\n\treadonly registrations: readonly DisposableRegistration[];\n\t/** Resolves after every vector op queued so far (reconcile/upsert/purge) settled. */\n\tsettleVectorWork(): Promise<void>;\n\tvectorState(): \"off\" | MemoryCapabilityStateV1;\n\t/**\n\t * Resolves once the enabling attempt settles (immediately for injected\n\t * components): ready, or the structured failure (reason + action list).\n\t */\n\tenablingOutcome(): Promise<\"ready\" | MemoryEnablingFailureV1>;\n}\n\n/**\n * Test-only assembly variant: the capability with an injected override (full\n * vector components skip real assembly; enabling seams redirect the model\n * loader on the real path). Public tool names, schemas, and return shapes are\n * identical to {@link createMemoryCapability}.\n */\nexport function createCapabilityWithVectorComponents(\n\tapi: CapabilityAPI,\n\tcontext: MemoryCapabilityContext,\n\toverride: MemoryCapabilityOverrideV1,\n): MemoryVectorCapabilityHarnessV1 {\n\t// Tool-face test stubs hand-write partial CapabilityAPIs that predate the\n\t// D-071 auto-recall hook; default registerLoopHook to a no-op so those\n\t// tests keep exercising just the tool face. Real CapabilityAPIs always\n\t// carry the method (production path never hits this default) — the `in`\n\t// check is runtime-only because the stubs bypass the declared type.\n\tconst apiWithHooks: CapabilityAPI =\n\t\t\"registerLoopHook\" in api\n\t\t\t? api\n\t\t\t: Object.assign(Object.create(null) as CapabilityAPI, api, {\n\t\t\t\t\tregisterLoopHook: (): DisposableRegistration => ({\n\t\t\t\t\t\tid: \"test:noop-loop-hook\",\n\t\t\t\t\t\tkind: \"loop-hook\",\n\t\t\t\t\t\tdispose() {},\n\t\t\t\t\t}),\n\t\t\t\t});\n\treturn createMemoryCapabilityHarness(apiWithHooks, context, override);\n}\n\nfunction createMemoryCapabilityHarness(\n\tapi: CapabilityAPI,\n\tcontext: MemoryCapabilityContext,\n\toverride?: MemoryCapabilityOverrideV1,\n): MemoryVectorCapabilityHarnessV1 {\n\tassertNonEmptyString(context.agentDir, \"MemoryCapabilityContext.agentDir\");\n\tconst now = (): number => Date.now();\n\tconst sessionId = api.session?.getSessionId() ?? \"no-session\";\n\n\t// 全局档位解析 (2.4d6 S1): context 显式 → env → settings.json → 默认 off。\n\t// 非法 env/settings 值 fail-closed 回 off 并各告警一次; off = 家族整体不注册\n\t// (零工具零命令零事件)。\n\tconst modeResolution = resolveMemoryMode(context.memory?.mode, undefined, context.memory?.settingsMode);\n\tif (modeResolution.invalidEnvValue !== undefined) {\n\t\tapi.logger?.warn(\n\t\t\t`invalid AGENT_FORGE_MEMORY_MODE value \"${modeResolution.invalidEnvValue}\"; memory stays off (fail-closed)`,\n\t\t);\n\t}\n\tif (modeResolution.invalidSettingsValue !== undefined) {\n\t\tapi.logger?.warn(\n\t\t\t`invalid memory.mode setting \"${modeResolution.invalidSettingsValue}\"; memory stays off (fail-closed)`,\n\t\t);\n\t}\n\tif (modeResolution.mode === \"off\") {\n\t\tapi.logger?.info(\"memory disabled (mode=off)\");\n\t\treturn {\n\t\t\tregistrations: [],\n\t\t\tsettleVectorWork: async () => {},\n\t\t\tvectorState: () => \"off\",\n\t\t\tenablingOutcome: async () => \"ready\",\n\t\t};\n\t}\n\tconst memoryMode = modeResolution.mode;\n\tconst vectorComponentsOverride: MemoryCapabilityOverrideV1 | undefined =\n\t\toverride ?? context.vectorComponents ?? context.storage?.vectorComponents;\n\tconst injectedComponents =\n\t\tvectorComponentsOverride !== undefined && isVectorComponentsOverride(vectorComponentsOverride)\n\t\t\t? vectorComponentsOverride\n\t\t\t: undefined;\n\tconst enablingOverride =\n\t\tvectorComponentsOverride !== undefined && !isVectorComponentsOverride(vectorComponentsOverride)\n\t\t\t? vectorComponentsOverride\n\t\t\t: undefined;\n\n\t// Store: one per-capability-instance singleton. Atoms of every domain this\n\t// session touches live here together — the atom `suiteId` field carries the\n\t// read boundary (suite queries filter through it; the ledger is only the\n\t// per-domain durable replica). The injected storeFactory (扩展位 A1) replaces\n\t// the default store wholesale: the implementation owns retention semantics.\n\tconst retentionRegistry = createFirstPartyRetentionRegistry();\n\tconst store =\n\t\tcontext.storage?.storeFactory?.({\n\t\t\tagentDir: context.agentDir,\n\t\t\towner: BUILTIN_MEMORY_OWNER,\n\t\t\tnow,\n\t\t}) ?? createMemoryStore({ retentionRegistry, now });\n\n\t// Durable replica per domain, created and loaded lazily on the first tool\n\t// call in that domain (统一修复轮 B1 复审修复): the sdk appends the suite\n\t// binding entry only after the session constructor ran this factory, so the\n\t// domain (and with it the ledger to replay) is only known at call time.\n\t// Write identity is deterministic: observationId `memory_write:<sessionId>:<n>`\n\t// hashes into the memoryId. The counter is instance-local, so a resumed session\n\t// re-derives its floor from the replayed ledger (resolveDomain): a persisted\n\t// observationId of this session marks its sequence as taken — the store rejects\n\t// a regenerated memoryId (canonical atoms are immutable), which would fail every\n\t// replayed-sequence write after resume (write → dispose → resume → write).\n\tconst writeSequencePrefix = `memory_write:${sessionId}:`;\n\tconst writeSequenceOf = (observationId: string): number | undefined => {\n\t\tif (!observationId.startsWith(writeSequencePrefix)) return undefined;\n\t\tconst sequence = observationId.slice(writeSequencePrefix.length);\n\t\treturn /^\\d+$/.test(sequence) ? Number(sequence) : undefined;\n\t};\n\tlet writeSequence = 0;\n\n\tconst domainLedgers = new Map<string, DurableMemoryLedgerV1>();\n\tconst resolveDomain = (): {\n\t\treadonly domain: SuiteMemoryDomainV1;\n\t\treadonly domainName: string;\n\t\treadonly ledger: DurableMemoryLedgerV1;\n\t} => {\n\t\tconst domain = resolveSessionDomain(api);\n\t\tconst domainName = suiteMemoryDomainName(domain);\n\t\tlet ledger = domainLedgers.get(domainName);\n\t\tif (ledger === undefined) {\n\t\t\tledger =\n\t\t\t\tcontext.storage?.ledgerFactory?.({\n\t\t\t\t\tagentDir: context.agentDir,\n\t\t\t\t\towner: BUILTIN_MEMORY_OWNER,\n\t\t\t\t\tdomain: domainName,\n\t\t\t\t\tnow,\n\t\t\t\t}) ??\n\t\t\t\tcreateDurableMemoryLedger({\n\t\t\t\t\tpath: join(context.agentDir, \"memory\", BUILTIN_MEMORY_OWNER, `ledger-${domainName}.jsonl`),\n\t\t\t\t\tnow,\n\t\t\t\t});\n\t\t\tif (domain.kind === \"legacy\") {\n\t\t\t\tapi.logger?.info(\"session has no suite binding; memory falls back to the legacy domain\", {\n\t\t\t\t\tsessionId: api.session?.getSessionId() ?? \"unknown\",\n\t\t\t\t});\n\t\t\t}\n\t\t\t// First load in this domain: replay the durable atoms into the store.\n\t\t\t// Sequences persisted by earlier instances of this session raise the\n\t\t\t// write-sequence floor before any new write can regenerate their ids.\n\t\t\tconst replay = ledger.load();\n\t\t\tfor (const atom of replay.atoms) {\n\t\t\t\tconst persistedSequence = writeSequenceOf(atom.observationId);\n\t\t\t\tif (persistedSequence !== undefined && persistedSequence > writeSequence) writeSequence = persistedSequence;\n\t\t\t\ttry {\n\t\t\t\t\tstore.commit(atom);\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// A ledger atom the current store cannot accept (e.g. an unregistered\n\t\t\t\t\t// retention mode) must not fail the session — skip with a diagnostic.\n\t\t\t\t\tapi.logger?.warn(\"memory ledger atom could not be replayed into the store\", {\n\t\t\t\t\t\tmemoryId: atom.memoryId,\n\t\t\t\t\t\terror: error instanceof Error ? error.message : String(error),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (replay.corruptedLines > 0 || replay.duplicateSkipped > 0 || replay.unreadable) {\n\t\t\t\tapi.logger?.warn(\"memory ledger loaded with diagnostics\", {\n\t\t\t\t\tdomain: domainName,\n\t\t\t\t\tcorruptedLines: replay.corruptedLines,\n\t\t\t\t\tduplicateSkipped: replay.duplicateSkipped,\n\t\t\t\t\tunreadable: replay.unreadable,\n\t\t\t\t});\n\t\t\t}\n\t\t\tdomainLedgers.set(domainName, ledger);\n\t\t\t// 启动对账: 首个域的 ledger 重放完成后一次性触发 (fire-and-forget,\n\t\t\t// 不阻塞本工具调用; 失败只影响向量通道, 不影响锚点通道)。\n\t\t\tvectorReconcileOnce();\n\t\t}\n\t\treturn { domain, domainName, ledger };\n\t};\n\n\tconst machine = createMemoryCandidateMachine({ store, now });\n\tconst lifecycle = createMemoryLifecycleManager({\n\t\tpolicy: {\n\t\t\tpolicyVersion: \"lifecycle@1\",\n\t\t\thalfLifeMs: { session: 86_400_000, cycle: 604_800_000, \"long-term\": 2_592_000_000 },\n\t\t\tstaleAfterMs: { session: 172_800_000, cycle: 1_209_600_000, \"long-term\": 5_184_000_000 },\n\t\t\tarchiveAfterMs: { session: 604_800_000, cycle: 5_184_000_000, \"long-term\": 15_552_000_000 },\n\t\t\tbaselineAttention: 0.5,\n\t\t},\n\t\tnow,\n\t});\n\tconst scheduler = createMemoryScheduler({ now });\n\tscheduler.acquireForInstance({ instanceId: sessionId, owner: BUILTIN_MEMORY_OWNER });\n\t// 非工具召回路径设施 (2.4d6 S4 家族收口取证结论): 生产工具召回 (memory_recall\n\t// 工具与 auto-recall 钩子, 后者直连同一 recallExecute, D-071 自 sdk 迁入)\n\t// 只走 recallSemantically 语义管线; index → recallAgent →\n\t// schedulerApi.recall 这条链在本 capability 内不再被工具面调用 (schedulerApi\n\t// 的生产使用仅剩写入路径的 submitObservation/submitCandidate)。保留原因:\n\t// MemorySchedulerApiV1 facade 契约 (recall + egress 门)、recall-agent 与\n\t// recall-index 是公共 API (src/index.ts 导出) 和 memory-testkit 之上的测试\n\t// 设施 (scheduler-api / memory-suite-scope / coverage-branch-memory-recall\n\t// 等测试的直接消费者) — 有存续消费者, 非死代码。\n\tconst index = createMemoryRecallIndex({ store });\n\tconst recallAgent = createMemoryRecallAgent({\n\t\tscheduler,\n\t\tindex,\n\t\tpolicyDefaults: {\n\t\t\tcandidateBudget: 1000,\n\t\t\tmodelInspectionLimit: 20,\n\t\t\tfinalResultLimit: 20,\n\t\t\tmaxRelationHops: 2,\n\t\t\tmaxModelCalls: 3,\n\t\t\tmaxOutputTokens: 4000,\n\t\t\tmaxOutputBytes: 16_000,\n\t\t\tmaxRounds: 3,\n\t\t},\n\t\thostLimits: {\n\t\t\tcandidateBudget: 5000,\n\t\t\tmodelInspectionLimit: 100,\n\t\t\tfinalResultLimit: 100,\n\t\t\tmaxRelationHops: 5,\n\t\t\tmaxModelCalls: 10,\n\t\t\tmaxOutputTokens: 100_000,\n\t\t\tmaxOutputBytes: 1_000_000,\n\t\t\tmaxRounds: 10,\n\t\t},\n\t\tnow,\n\t});\n\tconst replicaRegistry = createMemoryReplicaRegistry();\n\treplicaRegistry.register({\n\t\treplicaId: \"memory-ledger\",\n\t\tkind: \"canonical\",\n\t\townerScope: BUILTIN_MEMORY_OWNER,\n\t\tcontractVersion: MEMORY_CONTRACT_VERSION,\n\t\thealthy: true,\n\t} satisfies MemoryReplicaV1);\n\t// 向量投影副本 (混合检索工程化, D-035): 恒注册 — purge 批次以向量索引确认为\n\t// 副本契约的一部分; 通道 off/disabled 时 index 分派为 no-op (索引不存在,\n\t// 无行可清), 行为见 vectorPurgeMemories。\n\treplicaRegistry.register({\n\t\treplicaId: \"memory-vector-index\",\n\t\tkind: \"index\",\n\t\tcontractVersion: MEMORY_CONTRACT_VERSION,\n\t\townerScope: BUILTIN_MEMORY_OWNER,\n\t\thealthy: true,\n\t} satisfies MemoryReplicaV1);\n\tconst purgeGate = createMemoryPurgeGate({ registry: replicaRegistry, now });\n\tconst purgeJournal = createMemoryPurgeJournal();\n\tconst egressPolicy = createMemoryEgressPolicy({\n\t\tpolicyVersion: \"egress@1\",\n\t\tallowedSourceKinds: [\"tool\", \"entry\"],\n\t\tredactedPayloadFields: [],\n\t});\n\t// schedulerApi.recall 在生产工具面无调用点 (角色见上方\"非工具召回路径设施\"\n\t// 注释); 本 capability 的生产使用仅 submitObservation/submitCandidate (写入)。\n\tconst schedulerApi = createMemorySchedulerApi({\n\t\trecallAgent,\n\t\tmachine,\n\t\tlifecycle,\n\t\tpurgeGate,\n\t\tegressPolicy,\n\t\tfactInfoLookup: (memoryId) => {\n\t\t\tconst record = store.get(memoryId, { owner: BUILTIN_MEMORY_OWNER });\n\t\t\tif (!record) return undefined;\n\t\t\treturn {\n\t\t\t\tsourceRefs: record.atom.sourceRefs,\n\t\t\t\tpayload: record.atom.payload as Record<string, unknown>,\n\t\t\t};\n\t\t},\n\t});\n\t// Preference resolution/disambiguation over context-scoped recalls (2.4c/2.4d2):\n\t// stateless and deterministic — one instance per capability suffices.\n\tconst preferenceResolver = createPreferenceResolver();\n\tconst preferenceDisambiguator = createPreferenceDisambiguator();\n\t// Relation diffusion over the recall seeds (记忆网络接线, 2.4d4): a factory-\n\t// scoped deterministic network with one enabled one-hop adapter; disabled\n\t// adapters yield an explicit `disabled` result instead of silent degradation.\n\tconst memoryNetwork = createMemoryNetwork({\n\t\tadapters: [createRelationDiffusionAdapter({ maxHops: 1 })],\n\t});\n\n\t// ---- 语义召回通道 (2.4d6 S1/S2; 召回唯一检索路径) ----\n\t// 状态机 (设计 §3.2): enabling = 检查/加载进行中 (惰性触发: 首次向量操作\n\t// 时真正执行); ready = 可用; degraded = ready 后运行期故障 (可自愈回\n\t// ready); failed = enabling 失败, 本实例内终态 (mode 回退 off, 记忆工具\n\t// 返回携带行动清单的不可用 packet, 不再重试)。所有写侧操作经 vectorQueue\n\t// 串行化, 避免 reconcile 与写入埋点在 LanceDB 建表上竞态; recall 等待\n\t// enabling 单飞完成, 不与后台对账并发读索引。transformers/lancedb 的动态\n\t// import 只出现在 vectorEnsure 的装配分支里 (type-position 引类型), 未\n\t// 触发时不加载任何重依赖。\n\tlet vectorState: MemoryCapabilityStateV1 = \"enabling\";\n\tlet vectorChannel: ActiveVectorChannelV1 | undefined;\n\tlet enablingFailure: MemoryEnablingFailureV1 | undefined;\n\tlet enablingSettled: (() => void) | undefined;\n\tconst enablingSettledPromise = new Promise<\"ready\" | MemoryEnablingFailureV1>((resolve) => {\n\t\tenablingSettled = () => resolve(enablingFailure ?? \"ready\");\n\t});\n\tif (injectedComponents !== undefined) {\n\t\t// 测试注入: 直接 ready, 跳过真实装配与档位解析。\n\t\tvectorState = \"ready\";\n\t\tvectorChannel = {\n\t\t\tembed: injectedComponents.embedProvider,\n\t\t\treranker: injectedComponents.reranker,\n\t\t\tindex: injectedComponents.vectorIndex,\n\t\t\tqueryPrefix: injectedComponents.queryPrefix ?? \"\",\n\t\t\tembeddingModelId: injectedComponents.embedProvider.modelId,\n\t\t};\n\t\tenablingSettled?.();\n\t}\n\tconst vectorEnabled = (): boolean => vectorState !== \"failed\";\n\tlet vectorInitPromise: Promise<ActiveVectorChannelV1 | undefined> | undefined;\n\tlet vectorQueue: Promise<unknown> = Promise.resolve();\n\t/** 精排组件失败标记: reranker 加载/调用失败后本实例内跳过精排 (RRF 序)。 */\n\tlet vectorRerankerUnavailable = false;\n\t/** 投影写/清失败的一次性降级告警标记 (重复失败由启动对账修复, 不刷屏)。 */\n\tlet vectorDegradedWarned = false;\n\n\tconst vectorEnqueue = <T>(operation: () => Promise<T>): Promise<T> => {\n\t\tconst next = vectorQueue.then(operation, operation);\n\t\t// 队尾永不满仓 reject: 前序失败不阻断后续操作, 错误由操作自己消化。\n\t\tvectorQueue = next.then(\n\t\t\t() => undefined,\n\t\t\t() => undefined,\n\t\t);\n\t\treturn next;\n\t};\n\n\t/**\n\t * 开启状态机 (设计 §3.2, 惰性单飞): 本地检查 (native .node 可解析 → 重依赖\n\t * 供给 (lancedb/transformers: 宿主入口路径或插件位置自解析) → 模型文件存在\n\t * 与大小 → 磁盘余量) → transformers 加载 (自带下载, 进度周期性\n\t * 落一行日志) → 冒烟 (embed 一条 + getStoredModelId 与档位比对) → ready。\n\t * 任一步失败 → failed (原因 + 五选一行动清单), mode 记录回退 off, 本实例\n\t * 不再重试。\n\t */\n\tconst vectorEnsure = (): Promise<ActiveVectorChannelV1 | undefined> => {\n\t\tif (vectorState === \"ready\" || vectorState === \"degraded\") return Promise.resolve(vectorChannel);\n\t\tif (vectorState === \"failed\") return Promise.resolve(undefined);\n\t\tif (vectorInitPromise !== undefined) return vectorInitPromise;\n\t\tvectorInitPromise = (async (): Promise<ActiveVectorChannelV1 | undefined> => {\n\t\t\tconst preset = vectorPresetForMode(memoryMode);\n\t\t\tconst modelCacheDir = join(context.agentDir, \"memory\", VECTOR_MODEL_CACHE_DIRNAME);\n\t\t\tconst failEnabling = (reason: string): undefined => {\n\t\t\t\tvectorState = \"failed\";\n\t\t\t\tenablingFailure = { reason, actions: enablingActionList(modelCacheDir, memoryMode) };\n\t\t\t\tvectorInitPromise = undefined;\n\t\t\t\tapi.logger?.warn(`memory enabling failed: ${reason} — actions: ${enablingFailure.actions.join(\" | \")}`, {\n\t\t\t\t\tmode: memoryMode,\n\t\t\t\t});\n\t\t\t\tenablingSettled?.();\n\t\t\t\treturn undefined;\n\t\t\t};\n\t\t\tapi.logger?.info(`memory enabling started (mode=${memoryMode}, embedding=${preset.embeddingModelId})`);\n\t\t\t// 检查 1: native .node sidecar (纯本地, 零网络)。宿主注入路径优先\n\t\t\t// (安装店副本自解析不可达, 见 MemoryCapabilityContext.hostOnnxRuntimeEntryPath)。\n\t\t\tconst nativeProblem = nativeBinaryCheck(context.hostOnnxRuntimeEntryPath);\n\t\t\tif (nativeProblem !== undefined) return failEnabling(nativeProblem);\n\t\t\t// 检查 1b: 重依赖供给。宿主入口路径优先; 未提供时插件位置自解析,\n\t\t\t// 两者都不可达 = 原本要到加载步才以 \"Cannot find module\" 泛化包装\n\t\t\t// 失败的根因, 前置到模型下载之前以 onnx 缺供分支同构的精准文案报告。\n\t\t\t// 路径已提供但包损坏的真实失败仍走加载步, 由泛化包装保留原始错误。\n\t\t\tconst missingHeavyDependencies = missingHeavyDependencyModules(\n\t\t\t\t{ lancedb: context.hostLancedbEntryPath, transformers: context.hostTransformersEntryPath },\n\t\t\t\tresolveHeavyDependencyEntryPath,\n\t\t\t);\n\t\t\tif (missingHeavyDependencies.length > 0) {\n\t\t\t\treturn failEnabling(\n\t\t\t\t\t`${missingHeavyDependencies.join(\", \")} are not provided by the host installation and could not be resolved from the plugin location; to enable memory features, install them in the host project (npm i ${missingHeavyDependencies.join(\" \")}) and restart the session`,\n\t\t\t\t);\n\t\t\t}\n\t\t\t// 检查 2: 模型文件 (缺失 = 进入下载; 存在但截断 = 失败提示手动导入)。\n\t\t\tconst embeddingProblem = modelFilesCheck(\n\t\t\t\tmodelCacheDir,\n\t\t\t\tpreset.embeddingModelId,\n\t\t\t\tMODEL_MIN_WEIGHT_BYTES[memoryMode],\n\t\t\t);\n\t\t\tconst rerankerProblem = modelFilesCheck(modelCacheDir, VECTOR_RERANKER_MODEL_ID, RERANKER_MIN_WEIGHT_BYTES);\n\t\t\tconst modelsMissing =\n\t\t\t\tembeddingProblem === \"missing\" || rerankerProblem === \"missing\"\n\t\t\t\t\t? `${preset.embeddingModelId}${rerankerProblem === \"missing\" ? ` + ${VECTOR_RERANKER_MODEL_ID}` : \"\"}`\n\t\t\t\t\t: undefined;\n\t\t\tif (embeddingProblem !== undefined && embeddingProblem !== \"missing\") return failEnabling(embeddingProblem);\n\t\t\tif (rerankerProblem !== undefined && rerankerProblem !== \"missing\") return failEnabling(rerankerProblem);\n\t\t\tif (modelsMissing !== undefined) {\n\t\t\t\tapi.logger?.info(\n\t\t\t\t\t`memory enabling: model files missing (${modelsMissing}); downloading via configured mirrors`,\n\t\t\t\t);\n\t\t\t}\n\t\t\t// 检查 3: 磁盘余量 (light ≥ 500MB / full ≥ 2GB)。\n\t\t\tconst diskProblem = await diskHeadroomCheck(context.agentDir, DISK_HEADROOM_MIN_BYTES[memoryMode]);\n\t\t\tif (diskProblem !== undefined) return failEnabling(diskProblem);\n\t\t\t// 加载: transformers/lancedb 的动态 import 都在这些模块内部, 这里只做\n\t\t\t// 模块级懒加载, 保持未触发会话零重依赖。进度周期性落一行 (下载可达\n\t\t\t// 数百 MB), 完成后清除。\n\t\t\tconst progressTimer = setInterval(() => {\n\t\t\t\tapi.logger?.info(\"memory enabling still in progress (downloading/loading models)\");\n\t\t\t}, ENABLING_PROGRESS_LOG_INTERVAL_MS);\n\t\t\tif (typeof progressTimer === \"object\" && \"unref\" in progressTimer) progressTimer.unref();\n\t\t\ttry {\n\t\t\t\tconst [vectorIndexModule, embeddingModule, rerankerModule] = await Promise.all([\n\t\t\t\t\timport(\"./memory/vector-index.ts\"),\n\t\t\t\t\timport(\"./memory/embedding-provider.ts\"),\n\t\t\t\t\timport(\"./memory/embedding-reranker.ts\"),\n\t\t\t\t]);\n\t\t\t\tconst index = await vectorIndexModule.createMemoryVectorIndex({\n\t\t\t\t\tdbPath: join(context.agentDir, \"memory\", VECTOR_DB_DIRNAME),\n\t\t\t\t\tdimensions: preset.dimensions,\n\t\t\t\t\t...(context.hostLancedbEntryPath === undefined ? {} : { moduleEntryPath: context.hostLancedbEntryPath }),\n\t\t\t\t});\n\t\t\t\tconst remoteHosts = remoteHostsForEnabling();\n\t\t\t\tconst channel: ActiveVectorChannelV1 = {\n\t\t\t\t\tembed: embeddingModule.createTransformersEmbeddingProvider({\n\t\t\t\t\t\tmodelId: preset.embeddingModelId,\n\t\t\t\t\t\tdimensions: preset.dimensions,\n\t\t\t\t\t\tcacheDir: modelCacheDir,\n\t\t\t\t\t\tremoteHosts,\n\t\t\t\t\t\t...(context.hostTransformersEntryPath === undefined\n\t\t\t\t\t\t\t? {}\n\t\t\t\t\t\t\t: { moduleEntryPath: context.hostTransformersEntryPath }),\n\t\t\t\t\t\t...(enablingOverride?.embeddingLoadImpl === undefined\n\t\t\t\t\t\t\t? {}\n\t\t\t\t\t\t\t: { loadImpl: enablingOverride.embeddingLoadImpl }),\n\t\t\t\t\t}),\n\t\t\t\t\treranker: rerankerModule.createTransformersReranker({\n\t\t\t\t\t\tmodelId: VECTOR_RERANKER_MODEL_ID,\n\t\t\t\t\t\tcacheDir: modelCacheDir,\n\t\t\t\t\t\tremoteHosts,\n\t\t\t\t\t\t...(context.hostTransformersEntryPath === undefined\n\t\t\t\t\t\t\t? {}\n\t\t\t\t\t\t\t: { moduleEntryPath: context.hostTransformersEntryPath }),\n\t\t\t\t\t\t...(enablingOverride?.rerankerLoadImpl === undefined\n\t\t\t\t\t\t\t? {}\n\t\t\t\t\t\t\t: { loadImpl: enablingOverride.rerankerLoadImpl }),\n\t\t\t\t\t}),\n\t\t\t\t\tindex,\n\t\t\t\t\tqueryPrefix: preset.queryPrefix,\n\t\t\t\t\tembeddingModelId: preset.embeddingModelId,\n\t\t\t\t};\n\t\t\t\t// 冒烟: embed 一条 (同时完成模型加载) + 投影档位比对。\n\t\t\t\tawait channel.embed.embed([\"memory enabling smoke test\"]);\n\t\t\t\tif (channel.embed.modelId !== preset.embeddingModelId) {\n\t\t\t\t\treturn failEnabling(\n\t\t\t\t\t\t`smoke check failed: embedding modelId ${channel.embed.modelId} does not match the ${memoryMode} preset ${preset.embeddingModelId}`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tconst storedModelId = await channel.index.getStoredModelId();\n\t\t\t\tif (storedModelId !== undefined && storedModelId !== preset.embeddingModelId) {\n\t\t\t\t\tapi.logger?.warn(\n\t\t\t\t\t\t`memory projection was built for ${storedModelId}, preset is ${preset.embeddingModelId}; startup reconciliation will rebuild it`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tvectorChannel = channel;\n\t\t\t\tvectorState = \"ready\";\n\t\t\t\tapi.logger?.info(`memory ready (mode=${memoryMode}, embedding=${preset.embeddingModelId})`);\n\t\t\t\tenablingSettled?.();\n\t\t\t\treturn channel;\n\t\t\t} catch (error) {\n\t\t\t\treturn failEnabling(\n\t\t\t\t\t`model load or smoke check failed (${error instanceof Error ? error.message : String(error)})`,\n\t\t\t\t);\n\t\t\t} finally {\n\t\t\t\tclearInterval(progressTimer);\n\t\t\t}\n\t\t})();\n\t\treturn vectorInitPromise;\n\t};\n\n\t/**\n\t * 写入埋点 (canonical 已提交后): fire-and-forget, 不阻塞工具返回、不抛出。\n\t * embed/upsert 失败记一次 degraded, 该条由启动对账补齐 — canonical 与工具\n\t * 返回不受影响 (向量投影只是 index 副本)。full 档语义合并 (S3) 时携带探测\n\t * 产物: 复用探测已算出的新 statement 向量; 合并命中在 upsert 新行后移除被\n\t * supersedes 的旧行 (upsert 新 id 是插入而非覆盖, 旧行必须显式移除, 失败走\n\t * 同一 degraded 告警 — 残行由召回侧 superseded 过滤 + 对账跳过回填兜底)。\n\t */\n\tconst vectorUpsertAtom = (atom: MemoryAtomV1, mergeOutcome?: SemanticMergeOutcomeV1): void => {\n\t\tif (!vectorEnabled()) return;\n\t\tvoid vectorEnqueue(async () => {\n\t\t\tconst channel = await vectorEnsure();\n\t\t\tif (channel === undefined) return;\n\t\t\ttry {\n\t\t\t\tconst statement = statementOf(atom.payload);\n\t\t\t\tconst [vector] =\n\t\t\t\t\tmergeOutcome === undefined ? await channel.embed.embed([statement]) : [mergeOutcome.vector];\n\t\t\t\tawait channel.index.upsert([\n\t\t\t\t\t{\n\t\t\t\t\t\tmemoryId: atom.memoryId,\n\t\t\t\t\t\towner: BUILTIN_MEMORY_OWNER,\n\t\t\t\t\t\tmodelId: channel.embed.modelId,\n\t\t\t\t\t\tstatement,\n\t\t\t\t\t\ttags: tagFacetsOf(atom),\n\t\t\t\t\t\tvector,\n\t\t\t\t\t},\n\t\t\t\t]);\n\t\t\t\tif (mergeOutcome?.plan.kind === \"merge\") {\n\t\t\t\t\tawait channel.index.remove([mergeOutcome.plan.targetMemoryId]);\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tif (!vectorDegradedWarned) {\n\t\t\t\t\tvectorDegradedWarned = true;\n\t\t\t\t\tapi.logger?.warn(\n\t\t\t\t\t\t\"memory vector projection write failed; the entry is rebuilt by startup reconciliation\",\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tmemoryId: atom.memoryId,\n\t\t\t\t\t\t\terror: error instanceof Error ? error.message : String(error),\n\t\t\t\t\t\t},\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t};\n\n\t/**\n\t * 当前 store 内被 supersedes 关系指向的 memoryId 集 (S3): 写入探测跳过已\n\t * 继任的近邻 (其继任条目才是当前事实), 启动对账跳过回填 (投影只维护未继任\n\t * 条目)。O(n) 扫描, 与 recall 关系扩散建图同量级。\n\t */\n\tconst supersededMemoryIds = (): ReadonlySet<string> => {\n\t\tconst superseded = new Set<string>();\n\t\tfor (const record of store.list({ owner: BUILTIN_MEMORY_OWNER })) {\n\t\t\tfor (const relation of record.atom.relations) {\n\t\t\t\tif (relation.kind === \"supersedes\" && relation.targetMemoryId !== undefined) {\n\t\t\t\t\tsuperseded.add(relation.targetMemoryId);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn superseded;\n\t};\n\n\t/** 语义合并探测失败的一次性降级告警标记 (失败按普通新条目写入, 不刷屏)。 */\n\tlet semanticMergeProbeWarned = false;\n\n\t/**\n\t * 写入语义合并探测 (S3, full 档独有; 在 canonical 提交前执行 — supersedes/\n\t * conflict 关系必须随新 atom 一次性提交, atom 不可变)。同一 vectorQueue 串\n\t * 行域内: embed(statement) → 同 owner KNN top-5 → canonical 回查 (同域可见、\n\t * 未被继任) → 重嵌各近邻的 canonical statement 计算余弦 (阈值作用在 canonical\n\t * 文本的向量上, 不依赖可能陈旧的投影 body) → 三分支判定。时序保证\"排除自身\n\t * observationId 条目\": 新 atom 尚未提交, 其 memoryId 不可能在投影中;\n\t * observationId 重放 (deduped) 消费不到探测产物, 幂等零副作用。探测失败不\n\t * 阻塞写入 (降级为普通新条目并告警一次); enabling 未落定/failed 不等待 —\n\t * 写入不得被模型加载阻塞。\n\t */\n\tconst semanticMergeProbe = (input: {\n\t\treadonly statement: string;\n\t\treadonly newTags: readonly string[];\n\t\treadonly domain: SuiteMemoryDomainV1;\n\t}): Promise<SemanticMergeOutcomeV1 | undefined> => {\n\t\tif (vectorState === \"enabling\" || vectorState === \"failed\") return Promise.resolve(undefined);\n\t\treturn vectorEnqueue(async (): Promise<SemanticMergeOutcomeV1 | undefined> => {\n\t\t\tconst channel = await vectorEnsure();\n\t\t\tif (channel === undefined) return undefined;\n\t\t\ttry {\n\t\t\t\tconst [vector] = await channel.embed.embed([input.statement]);\n\t\t\t\tconst hits = await channel.index.queryKnn(vector, {\n\t\t\t\t\towner: BUILTIN_MEMORY_OWNER,\n\t\t\t\t\tlimit: SEMANTIC_MERGE_KNN_LIMIT,\n\t\t\t\t});\n\t\t\t\tif (hits.length === 0) return { plan: { kind: \"new\" }, vector };\n\t\t\t\tconst superseded = supersededMemoryIds();\n\t\t\t\tconst neighborAtoms: MemoryAtomV1[] = [];\n\t\t\t\tfor (const hit of hits) {\n\t\t\t\t\tconst record = store.get(hit.memoryId, {\n\t\t\t\t\t\towner: BUILTIN_MEMORY_OWNER,\n\t\t\t\t\t\t...(input.domain.kind === \"suite\" ? { suiteId: input.domain.suiteId } : {}),\n\t\t\t\t\t});\n\t\t\t\t\tif (record === undefined) continue;\n\t\t\t\t\t// 域隔离: 合并只发生在同域条目之间 (suite 写入不得 supersedes 跨\n\t\t\t\t\t// suite/跨域条目 — 那会移除别域召回所需的投影行; promoted 的跨\n\t\t\t\t\t// suite 可见偏好 canonical 归属其原 suite, 同样不作为合并目标)。\n\t\t\t\t\tif (\n\t\t\t\t\t\tinput.domain.kind === \"suite\"\n\t\t\t\t\t\t\t? record.atom.suiteId !== input.domain.suiteId\n\t\t\t\t\t\t\t: record.atom.suiteId !== undefined\n\t\t\t\t\t) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tif (superseded.has(record.atom.memoryId)) continue;\n\t\t\t\t\tneighborAtoms.push(record.atom);\n\t\t\t\t}\n\t\t\t\tif (neighborAtoms.length === 0) return { plan: { kind: \"new\" }, vector };\n\t\t\t\tconst neighborVectors = await channel.embed.embed(neighborAtoms.map((atom) => statementOf(atom.payload)));\n\t\t\t\tconst neighbors: SemanticMergeNeighborV1[] = neighborAtoms.map((atom, position) => ({\n\t\t\t\t\tmemoryId: atom.memoryId,\n\t\t\t\t\ttags: tagFacetsOf(atom),\n\t\t\t\t\tcosine: cosineSimilarity(vector, neighborVectors[position] ?? []),\n\t\t\t\t}));\n\t\t\t\treturn { plan: decideSemanticMergePlan({ newTags: input.newTags, neighbors }), vector };\n\t\t\t} catch (error) {\n\t\t\t\tif (!semanticMergeProbeWarned) {\n\t\t\t\t\tsemanticMergeProbeWarned = true;\n\t\t\t\t\tapi.logger?.warn(\"memory semantic merge probe failed; the entry is written as a new memory\", {\n\t\t\t\t\t\terror: error instanceof Error ? error.message : String(error),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t});\n\t};\n\n\t/**\n\t * purge 的 index 副本分派: 通道非 ready/degraded 时 no-op 且不报错 (enabling\n\t * 未完成/failed = 索引无本会话写入的行, 残行由 store 回查兜底不可出线)。\n\t * remove 异步且失败只记 degraded: 残行永远过不了 recall 的 store 回查, 不构\n\t * 成出线泄漏。\n\t */\n\tconst vectorPurgeMemories = (memoryIds: readonly string[]): void => {\n\t\tif (vectorState !== \"ready\" && vectorState !== \"degraded\") return;\n\t\tvoid vectorEnqueue(async () => {\n\t\t\tconst channel = vectorChannel;\n\t\t\tif (channel === undefined) return;\n\t\t\ttry {\n\t\t\t\tawait channel.index.remove(memoryIds);\n\t\t\t} catch (error) {\n\t\t\t\tif (!vectorDegradedWarned) {\n\t\t\t\t\tvectorDegradedWarned = true;\n\t\t\t\t\tapi.logger?.warn(\n\t\t\t\t\t\t\"memory vector projection purge failed; stale rows stay unreachable via the store lookup\",\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\terror: error instanceof Error ? error.message : String(error),\n\t\t\t\t\t\t},\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t};\n\n\t/**\n\t * 启动对账 (一次性, capability 初始化语义内; fire-and-forget 不阻塞首工具):\n\t * 1) 投影 modelId 与预设不一致 (含 undefined 且表非空) → 清空向量表;\n\t * 2) diff store 全量 memoryId vs 投影, 缺失的分批 embed+upsert, 单次上限\n\t * {@link RECONCILE_MAX_UPSERTS}, 超出记 degraded 下次启动继续。对账失败不\n\t * 阻塞 capability 启动, 状态转 degraded (运行期故障, 可自愈: 后续向量操作\n\t * 重新触发补齐语义, canonical 面不受影响)。\n\t */\n\tlet vectorReconcileStarted = false;\n\tconst vectorReconcileOnce = (): void => {\n\t\tif (!vectorEnabled() || vectorReconcileStarted) return;\n\t\tvectorReconcileStarted = true;\n\t\tvoid vectorEnqueue(async () => {\n\t\t\tconst channel = await vectorEnsure();\n\t\t\tif (channel === undefined) return;\n\t\t\ttry {\n\t\t\t\tconst storedModelId = await channel.index.getStoredModelId();\n\t\t\t\tif (storedModelId !== channel.embeddingModelId) {\n\t\t\t\t\tconst stale = await channel.index.listMemoryIds();\n\t\t\t\t\tif (stale.size > 0) await channel.index.remove([...stale]);\n\t\t\t\t}\n\t\t\t\tconst indexed = await channel.index.listMemoryIds();\n\t\t\t\t// 被继任的旧条目不回填投影 (S3): 合并已移除其行, 对账的重放/重建\n\t\t\t\t// 不得复活 (召回可见性由继任条目承担, canonical 侧保留可直查)。\n\t\t\t\tconst superseded = supersededMemoryIds();\n\t\t\t\tconst missing: MemoryAtomV1[] = [];\n\t\t\t\tfor (const record of store.list({ owner: BUILTIN_MEMORY_OWNER })) {\n\t\t\t\t\tif (superseded.has(record.atom.memoryId)) continue;\n\t\t\t\t\tif (!indexed.has(record.atom.memoryId)) missing.push(record.atom);\n\t\t\t\t}\n\t\t\t\tif (missing.length > RECONCILE_MAX_UPSERTS) {\n\t\t\t\t\tapi.logger?.warn(\"memory vector reconciliation capped; remaining entries rebuild on the next startup\", {\n\t\t\t\t\t\ttotal: missing.length,\n\t\t\t\t\t\tcap: RECONCILE_MAX_UPSERTS,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tconst batch = missing.slice(0, RECONCILE_MAX_UPSERTS);\n\t\t\t\tif (batch.length === 0) return;\n\t\t\t\tconst statements = batch.map((atom) => statementOf(atom.payload));\n\t\t\t\tconst vectors = await channel.embed.embed(statements);\n\t\t\t\tawait channel.index.upsert(\n\t\t\t\t\tbatch.map((atom, position) => ({\n\t\t\t\t\t\tmemoryId: atom.memoryId,\n\t\t\t\t\t\towner: BUILTIN_MEMORY_OWNER,\n\t\t\t\t\t\tmodelId: channel.embed.modelId,\n\t\t\t\t\t\tstatement: statements[position],\n\t\t\t\t\t\ttags: tagFacetsOf(atom),\n\t\t\t\t\t\tvector: vectors[position],\n\t\t\t\t\t})),\n\t\t\t\t);\n\t\t\t} catch (error) {\n\t\t\t\tvectorState = \"degraded\";\n\t\t\t\tapi.logger?.warn(\n\t\t\t\t\t\"memory vector reconciliation failed; the channel is degraded (canonical recall degrades, self-heals on the next successful channel op)\",\n\t\t\t\t\t{\n\t\t\t\t\t\terror: error instanceof Error ? error.message : String(error),\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t}\n\t\t});\n\t};\n\n\tconst buildWriteAtom = (input: {\n\t\treadonly content: string;\n\t\treadonly kind: \"fact\" | \"preference\";\n\t\treadonly subject?: string;\n\t\treadonly tags?: readonly string[];\n\t\t/** 语义合并 (S3) 判定产生的 supersedes/conflict 关系; 缺省 = 空关系。 */\n\t\treadonly relations?: readonly MemoryRelationV1[];\n\t\t/** 写入来源标记; 缺省 = 模型工具面。 */\n\t\treadonly writeReason?: string;\n\t\treadonly domain: SuiteMemoryDomainV1;\n\t}): MemoryAtomV1 => {\n\t\twriteSequence += 1;\n\t\tconst observationId = `memory_write:${sessionId}:${writeSequence}`;\n\t\tconst memoryId = `mem-${identityHash(`${BUILTIN_MEMORY_OWNER}\\u0000${observationId}`)}`;\n\t\tconst occurredAt = new Date(now()).toISOString();\n\t\tconst sourceRef = { kind: \"tool\" as const, id: `memory_write:${observationId}` };\n\t\tconst tags = (input.tags ?? []).map((tag) => tag.trim()).filter((tag) => tag !== \"\");\n\t\tconst facets = tags.map((tag) => ({\n\t\t\tnamespace: TAG_FACET_NAMESPACE,\n\t\t\tschemaVersion: 1,\n\t\t\tkey: \"tag\",\n\t\t\tvalue: tag,\n\t\t}));\n\t\tconst preferenceEnvelope: MemoryPreferenceEnvelopeV1 | undefined =\n\t\t\tinput.kind === \"preference\"\n\t\t\t\t? {\n\t\t\t\t\t\tsubject: input.subject as MemoryPreferenceEnvelopeV1[\"subject\"],\n\t\t\t\t\t\tkey: \"statement\",\n\t\t\t\t\t\tpreferredValue: input.content,\n\t\t\t\t\t\tscope: { level: \"profile-private\" },\n\t\t\t\t\t\tevidence: { class: \"explicit\", sourceRefs: [sourceRef], confidence: 1 },\n\t\t\t\t\t\tapplicabilityConfidence: 1,\n\t\t\t\t\t}\n\t\t\t\t: undefined;\n\t\treturn buildMemoryAtomV1({\n\t\t\tmemoryId,\n\t\t\tcontractVersion: MEMORY_CONTRACT_VERSION,\n\t\t\tschemaVersion: 1,\n\t\t\tscope: \"long-term\",\n\t\t\tretentionMode: \"long\",\n\t\t\towner: BUILTIN_MEMORY_OWNER,\n\t\t\tprofileId: capabilityManifest.id,\n\t\t\t...(input.domain.kind === \"suite\" ? { suiteId: input.domain.suiteId } : {}),\n\t\t\tretentionPolicyVersion: \"retention@1\",\n\t\t\tmemoryKind: input.kind === \"preference\" ? \"preference\" : \"fact\",\n\t\t\tpayload: { statement: input.content },\n\t\t\t...(preferenceEnvelope === undefined ? {} : { preference: preferenceEnvelope }),\n\t\t\toccurredAt,\n\t\t\trecordedAt: occurredAt,\n\t\t\tsourceRefs: [sourceRef],\n\t\t\tobservationId,\n\t\t\tsessionRefs: [sessionId],\n\t\t\tagentInstanceRefs: [],\n\t\t\tprojectRefs: [],\n\t\t\tsubjectRefs: [],\n\t\t\tfacets,\n\t\t\trelations: input.relations ?? [],\n\t\t\tconfidence: 1,\n\t\t\timportance: 0.5,\n\t\t\tevidenceClass: \"explicit\",\n\t\t\tcontentRevision: `c-${identityHash(input.content)}`,\n\t\t\twriteReason: input.writeReason ?? `${capabilityManifest.id}/memory_write`,\n\t\t});\n\t};\n\n\tconst writeTool = api.registerTool({\n\t\tname: \"memory_write\",\n\t\tlabel: \"Write memory\",\n\t\tdescription:\n\t\t\t\"Persist one durable memory (a fact or a user preference) scoped to the current suite; it stays recallable across sessions until forgotten\",\n\t\tparameters: writeSchema,\n\t\texecute: async (first: unknown, second?: unknown) => {\n\t\t\tconst input = memoryToolInput(first, second);\n\t\t\tassertNonEmptyString(input.content, \"content\");\n\t\t\t// 长度契约 (混合检索工程化 §5): 超长显式拒绝并提示拆分, 而非静默截断。\n\t\t\tif (input.content.length > MEMORY_CONTENT_MAX_LENGTH) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`content is ${input.content.length} characters; the memory content limit is ${MEMORY_CONTENT_MAX_LENGTH} — split it into multiple self-contained memories`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (input.kind !== \"fact\" && input.kind !== \"preference\")\n\t\t\t\tthrow new Error('kind must be \"fact\" or \"preference\"');\n\t\t\tif (input.kind === \"preference\") {\n\t\t\t\tassertNonEmptyString(input.subject, 'subject (required when kind is \"preference\")');\n\t\t\t}\n\t\t\tif (input.subject !== undefined) assertNonEmptyString(input.subject, \"subject\");\n\t\t\tif (input.tags !== undefined) {\n\t\t\t\tif (!Array.isArray(input.tags) || input.tags.length > MAX_TAGS) {\n\t\t\t\t\tthrow new Error(`tags must be an array of at most ${MAX_TAGS} strings`);\n\t\t\t\t}\n\t\t\t\tfor (const tag of input.tags) assertNonEmptyString(tag, \"tags entry\");\n\t\t\t}\n\t\t\tconst { domain, domainName, ledger } = resolveDomain();\n\t\t\t// 语义合并探测 (2.4d6 S3, full 档独有): light 档完全跳过 (写向量投影\n\t\t\t// 不查近邻, 行为与现状一致)。探测在 canonical 提交前执行, 判定决定新\n\t\t\t// atom 的 supersedes/conflict relations (atom 不可变, 随提交一次写入);\n\t\t\t// 探测失败降级为普通新条目 (告警一次, 不阻塞写入)。\n\t\t\tconst mergeOutcome =\n\t\t\t\tmemoryMode === \"full\"\n\t\t\t\t\t? await semanticMergeProbe({\n\t\t\t\t\t\t\tstatement: input.content,\n\t\t\t\t\t\t\tnewTags: input.tags ?? [],\n\t\t\t\t\t\t\tdomain,\n\t\t\t\t\t\t})\n\t\t\t\t\t: undefined;\n\t\t\tconst atom = buildWriteAtom({\n\t\t\t\tcontent: input.content,\n\t\t\t\tkind: input.kind,\n\t\t\t\tdomain,\n\t\t\t\t...(input.subject === undefined ? {} : { subject: input.subject }),\n\t\t\t\t...(input.tags === undefined ? {} : { tags: input.tags }),\n\t\t\t\t...(mergeOutcome === undefined\n\t\t\t\t\t? {}\n\t\t\t\t\t: { relations: semanticMergeRelations(mergeOutcome.plan, input.content) }),\n\t\t\t});\n\t\t\tconst observation = schedulerApi.submitObservation({\n\t\t\t\towner: BUILTIN_MEMORY_OWNER,\n\t\t\t\tobservationId: atom.observationId,\n\t\t\t\tdraft: atom,\n\t\t\t});\n\t\t\tif (observation.state === \"rejected\") {\n\t\t\t\tthrow new Error(`memory rejected: ${observation.reason ?? \"rejected\"}`);\n\t\t\t}\n\t\t\tconst candidate = schedulerApi.submitCandidate({\n\t\t\t\tobservationId: atom.observationId,\n\t\t\t\towner: BUILTIN_MEMORY_OWNER,\n\t\t\t});\n\t\t\tif (candidate.status === \"rejected\") throw new Error(`memory rejected: ${candidate.reason ?? \"rejected\"}`);\n\t\t\tif (candidate.status === \"committed\") {\n\t\t\t\tledger.append(atom);\n\t\t\t\t// 向量投影埋点 (混合检索工程化): canonical 已提交, 投影写入异步旁路,\n\t\t\t\t// 失败不阻塞不抛出 (vectorUpsertAtom 内部消化)。full 档携带语义合并\n\t\t\t\t// 探测产物 (复用探测向量; 合并命中同时移除被 supersedes 的旧行)。\n\t\t\t\tvectorUpsertAtom(atom, mergeOutcome);\n\t\t\t}\n\t\t\tawait api.publish({\n\t\t\t\ttype: EVENT_TYPE,\n\t\t\t\tversion: 1,\n\t\t\t\tcorrelationId: capabilityManifest.id,\n\t\t\t\tdata: { action: \"written\", memoryId: candidate.memoryId, memoryKind: atom.memoryKind, domain: domainName },\n\t\t\t});\n\t\t\t// 偏好晋升触发 (B2): the /promote capability command is the production\n\t\t\t// caller of promotePreferenceToUserDefault; the tip is what tells the\n\t\t\t// model (so it can tell the user) that the cross-suite promotion path\n\t\t\t// exists. Facts have no promotion path — no tip.\n\t\t\treturn {\n\t\t\t\tmemoryId: candidate.memoryId,\n\t\t\t\tkind: atom.memoryKind,\n\t\t\t\tdomain: domainName,\n\t\t\t\t...(atom.memoryKind === \"preference\"\n\t\t\t\t\t? {\n\t\t\t\t\t\t\ttip: \"[tip] If this is a general preference (not project-specific), the user can promote it across all suites using /promote.\",\n\t\t\t\t\t\t}\n\t\t\t\t\t: {}),\n\t\t\t};\n\t\t},\n\t});\n\n\t/**\n\t * 语义召回管线 (2.4d6 S2, 设计 §4) — 唯一检索路径: embed(查询, light 加 BGE\n\t * 前缀) → KNN depth50 + FTS depth50 → RRF k=60 top20 → 精排 → store 回查\n\t * (owner/suite/scope/过期, 单一可见性路径, 不足 limit 不回填)。降级层级\n\t * (每次降级一条结构化日志, 不抛给模型, 状态转 degraded 可自愈):\n\t * 精排失败 → RRF 序; 向量失败 (embed/KNN) → FTS-only + recency 排序;\n\t * FTS 失败 → KNN-only + recency 排序; 双通道皆败 → 域内 recency 枚举\n\t * (store.list, 列表语义保留); 枚举也失败 → degraded packet (调用方 catch)。\n\t */\n\n\t/** 一次语义召回的中间候选: atom + 融合/降级序位 (精排并列打破用)。 */\n\tinterface RecallCandidateV1 {\n\t\treadonly atom: MemoryAtomV1;\n\t\treadonly position: number;\n\t}\n\n\t/** 出线事实行 (含精排可选分): score 缺席 = 该路径未产生精排分。 */\n\tinterface ScoredRecallFactV1 {\n\t\treadonly memoryId: string;\n\t\treadonly statement: string;\n\t\treadonly confidence: number;\n\t\treadonly position: number;\n\t\treadonly score?: number;\n\t}\n\n\ttype SemanticRecallOutcomeV1 =\n\t\t| {\n\t\t\t\treadonly kind: \"served\";\n\t\t\t\treadonly facts: readonly MemoryFact[];\n\t\t\t\treadonly candidateCount: number;\n\t\t\t\treadonly suiteFilter: MemorySuiteFilterStatsV1;\n\t\t }\n\t\t| { readonly kind: \"blocked\"; readonly reason: string }\n\t\t| { readonly kind: \"unavailable\"; readonly message: string };\n\n\t/** recency 排序 (occurredAt 降序, memoryId 升序打破并列 — 与 recall index 同口径)。 */\n\tconst recencyOrdered = (entries: readonly RecallCandidateV1[]): readonly RecallCandidateV1[] =>\n\t\t[...entries]\n\t\t\t.sort(\n\t\t\t\t(left, right) =>\n\t\t\t\t\tDate.parse(right.atom.occurredAt) - Date.parse(left.atom.occurredAt) ||\n\t\t\t\t\t(left.atom.memoryId < right.atom.memoryId ? -1 : 1),\n\t\t\t)\n\t\t\t.map((entry, position) => ({ atom: entry.atom, position }));\n\n\t/**\n\t * superseded 过滤 (S3-B, store 回查后 / 精排前): 被某条在场候选的 supersedes\n\t * 关系指向的旧条目不再出线 — 合并后同义查询只出新条目。\"新条目在场\"以候选\n\t * 集为界: 继任条目被遗忘 (purge/过期) 后其关系随之消失, 旧条目自然恢复出线。\n\t * memoryId 主键直查不经此过滤 (引用不失效), memory_list 管理面不过滤\n\t * (nothing silently dropped — 用户可显式 forget 旧条目)。\n\t */\n\tconst filterSupersededCandidates = (candidates: readonly RecallCandidateV1[]): readonly RecallCandidateV1[] => {\n\t\tconst superseded = new Set<string>();\n\t\tfor (const entry of candidates) {\n\t\t\tfor (const relation of entry.atom.relations) {\n\t\t\t\tif (relation.kind === \"supersedes\" && relation.targetMemoryId !== undefined) {\n\t\t\t\t\tsuperseded.add(relation.targetMemoryId);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (superseded.size === 0) return candidates;\n\t\treturn candidates.filter((entry) => !superseded.has(entry.atom.memoryId));\n\t};\n\n\t/**\n\t * store 回查 (正确性关键): 候选 id 逐条走与旧锚点通道相同的可见性单一路径\n\t * (owner + suite 读边界 + 过期, 单一路径); suite 场景下不可见 id 按 legacy/\n\t * 外套件区分计数 (suiteFilter 可观测), 不可见者丢弃且不回填 (诚实分页)。\n\t */\n\tconst lookupCandidates = (\n\t\trankedIds: readonly (readonly [memoryId: string, position: number])[],\n\t\tsuiteId: string | undefined,\n\t): { readonly candidates: readonly RecallCandidateV1[]; readonly suiteFilter: MemorySuiteFilterStatsV1 } => {\n\t\tconst candidates: RecallCandidateV1[] = [];\n\t\tlet legacySkipped = 0;\n\t\tlet foreignSuiteSkipped = 0;\n\t\tfor (const [memoryId, position] of rankedIds) {\n\t\t\tconst record = store.get(memoryId, {\n\t\t\t\towner: BUILTIN_MEMORY_OWNER,\n\t\t\t\t...(suiteId === undefined ? {} : { suiteId }),\n\t\t\t});\n\t\t\tif (record !== undefined) {\n\t\t\t\tcandidates.push({ atom: record.atom, position });\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (suiteId !== undefined) {\n\t\t\t\tconst unfiltered = store.get(memoryId, { owner: BUILTIN_MEMORY_OWNER });\n\t\t\t\tif (unfiltered !== undefined) {\n\t\t\t\t\tif (unfiltered.atom.suiteId === undefined) legacySkipped += 1;\n\t\t\t\t\telse foreignSuiteSkipped += 1;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn { candidates, suiteFilter: { legacySkipped, foreignSuiteSkipped } };\n\t};\n\n\t/**\n\t * egress 门重放 (设计 §11 MUST run): 对最终候选统一执行, 与旧锚点 packet 同\n\t * 一 gate、同一 factInfo 来源 (store); blocked 即 fail-closed, 该次 recall\n\t * 返回 failed packet (来源未登记的事实永不越过出界面)。\n\t */\n\tconst egressGate = (candidates: readonly RecallCandidateV1[]) => {\n\t\tconst factInfo: Record<string, MemoryEgressFactInfoV1 | undefined> = {};\n\t\tfor (const entry of candidates) {\n\t\t\tfactInfo[entry.atom.memoryId] = {\n\t\t\t\tsourceRefs: entry.atom.sourceRefs,\n\t\t\t\tpayload: entry.atom.payload as Record<string, unknown>,\n\t\t\t};\n\t\t}\n\t\treturn egressPolicy.apply(\n\t\t\t{\n\t\t\t\toperationId: `memory_recall:${sessionId}:${identityHash(candidates.map((entry) => entry.atom.memoryId).join(\"\\u0000\"))}`,\n\t\t\t\tstatus: \"completed\",\n\t\t\t\tfacts: candidates.map((entry) => ({\n\t\t\t\t\tmemoryId: entry.atom.memoryId,\n\t\t\t\t\tcontentRevision: entry.atom.contentRevision,\n\t\t\t\t\tstatement: JSON.stringify(entry.atom.payload),\n\t\t\t\t\tconfidence: entry.atom.confidence,\n\t\t\t\t})),\n\t\t\t\tqueryPlan: { lookupUsed: false, filtersApplied: [], scorerVersion: \"semantic-rrf@1\" },\n\t\t\t\tattempts: 0,\n\t\t\t\tnarrowingHints: [],\n\t\t\t\tomittedCount: 0,\n\t\t\t\ttruncated: false,\n\t\t\t\ttruncationScope: \"none\",\n\t\t\t\tbudget: {\n\t\t\t\t\tcandidateBudget: 0,\n\t\t\t\t\tmodelInspectionLimit: 0,\n\t\t\t\t\tfinalResultLimit: 0,\n\t\t\t\t\tused: { candidateCount: 0, modelInspectedCount: 0, finalResultCount: 0 },\n\t\t\t\t},\n\t\t\t} satisfies MemoryRecallPacketV1,\n\t\t\tfactInfo,\n\t\t);\n\t};\n\n\tconst recallSemantically = async (input: {\n\t\treadonly query: string;\n\t\treadonly domain: SuiteMemoryDomainV1;\n\t\treadonly limit: number;\n\t\t/**\n\t\t * 自动召回质量门槛 (错误教训与召回质量护栏设计 §2.1, 内部执行缝):\n\t\t * memory_recall 工具 schema 不含该参数, 显式召回行为不变; auto-recall\n\t\t * 钩子经内部闭包恒传。仅约束精排实际产生 score 的候选。\n\t\t */\n\t\treadonly minScore?: number;\n\t}): Promise<SemanticRecallOutcomeV1> => {\n\t\t// 读侧先等写侧队列落定 (启动对账 + 待处理的投影写入), 保证 recall 与\n\t\t// 仓库/投影的线性一致: 本调用之前提交的记忆要么在 store 要么已补进投影,\n\t\t// 不会与后台对账/埋点竞态。\n\t\tawait vectorQueue;\n\t\tconst channel = await vectorEnsure();\n\t\tif (channel === undefined) {\n\t\t\treturn {\n\t\t\t\tkind: \"unavailable\",\n\t\t\t\tmessage:\n\t\t\t\t\tenablingFailure === undefined\n\t\t\t\t\t\t? `memory channel is unavailable (state=${vectorState})`\n\t\t\t\t\t\t: `memory enabling failed: ${enablingFailure.reason} — actions: ${enablingFailure.actions.join(\" | \")}`,\n\t\t\t};\n\t\t}\n\t\tconst suiteId = input.domain.kind === \"suite\" ? input.domain.suiteId : undefined;\n\t\tlet vectorFailed = false;\n\t\tlet ftsFailed = false;\n\t\tlet knnHits: readonly MemoryVectorHitV1[] | undefined;\n\t\tlet ftsHits: readonly MemoryVectorHitV1[] | undefined;\n\t\ttry {\n\t\t\tconst [queryVector] = await channel.embed.embed([channel.queryPrefix + input.query]);\n\t\t\tknnHits = await channel.index.queryKnn(queryVector, {\n\t\t\t\towner: BUILTIN_MEMORY_OWNER,\n\t\t\t\tlimit: HYBRID_CHANNEL_DEPTH,\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tvectorFailed = true;\n\t\t\tapi.logger?.warn(\"memory recall degraded: the vector channel failed; continuing on the FTS channel\", {\n\t\t\t\terror: error instanceof Error ? error.message : String(error),\n\t\t\t});\n\t\t}\n\t\ttry {\n\t\t\tftsHits = await channel.index.queryFts(input.query, {\n\t\t\t\towner: BUILTIN_MEMORY_OWNER,\n\t\t\t\tlimit: HYBRID_CHANNEL_DEPTH,\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tftsFailed = true;\n\t\t\tapi.logger?.warn(\"memory recall degraded: the FTS channel failed\", {\n\t\t\t\terror: error instanceof Error ? error.message : String(error),\n\t\t\t});\n\t\t}\n\n\t\tlet candidates: readonly RecallCandidateV1[];\n\t\tlet suiteFilter: MemorySuiteFilterStatsV1 = { legacySkipped: 0, foreignSuiteSkipped: 0 };\n\t\tlet channelDegraded = false;\n\t\tif (!vectorFailed && !ftsFailed && knnHits !== undefined && ftsHits !== undefined) {\n\t\t\t// 主路径: 双通道 RRF k=60 融合 top20。\n\t\t\tconst pool = rrfFuseRankings([ftsHits.map((hit) => hit.memoryId), knnHits.map((hit) => hit.memoryId)]);\n\t\t\tconst lookup = lookupCandidates(\n\t\t\t\tpool.map((memoryId, position) => [memoryId, position] as const),\n\t\t\t\tsuiteId,\n\t\t\t);\n\t\t\tcandidates = lookup.candidates;\n\t\t\tsuiteFilter = lookup.suiteFilter;\n\t\t} else if (ftsHits !== undefined) {\n\t\t\t// 向量失败: FTS-only + recency 排序。\n\t\t\tchannelDegraded = true;\n\t\t\tconst lookup = lookupCandidates(\n\t\t\t\tftsHits.map((hit, position) => [hit.memoryId, position] as const),\n\t\t\t\tsuiteId,\n\t\t\t);\n\t\t\tcandidates = recencyOrdered(lookup.candidates);\n\t\t\tsuiteFilter = lookup.suiteFilter;\n\t\t} else if (knnHits !== undefined) {\n\t\t\t// FTS 失败: KNN-only + recency 排序 (对称降级)。\n\t\t\tchannelDegraded = true;\n\t\t\tconst lookup = lookupCandidates(\n\t\t\t\tknnHits.map((hit, position) => [hit.memoryId, position] as const),\n\t\t\t\tsuiteId,\n\t\t\t);\n\t\t\tcandidates = recencyOrdered(lookup.candidates);\n\t\t\tsuiteFilter = lookup.suiteFilter;\n\t\t} else {\n\t\t\t// 双通道皆败: 域内 recency 枚举兜底 (store.list, 列表语义保留)。\n\t\t\tchannelDegraded = true;\n\t\t\tconst page = listDomainMemories({ store }, { owner: BUILTIN_MEMORY_OWNER, domain: input.domain });\n\t\t\tconst domainCandidates: RecallCandidateV1[] = [];\n\t\t\tfor (const entry of page.entries) {\n\t\t\t\tconst record = store.get(entry.memoryId, { owner: BUILTIN_MEMORY_OWNER });\n\t\t\t\tif (record !== undefined) domainCandidates.push({ atom: record.atom, position: 0 });\n\t\t\t}\n\t\t\tcandidates = recencyOrdered(domainCandidates);\n\t\t}\n\t\t// 运行期通道故障 → degraded (可自愈: 下一次全通 recall 回 ready)。\n\t\tif (channelDegraded) vectorState = \"degraded\";\n\t\telse if (vectorState === \"degraded\") vectorState = \"ready\";\n\n\t\t// superseded 过滤 (S3-B): 回查后、egress/精排前 (主路径与全部降级路径\n\t\t// 统一收敛于此 — 被继任的旧条目任何通道都不再出线)。\n\t\tcandidates = filterSupersededCandidates(candidates);\n\n\t\tif (candidates.length === 0) {\n\t\t\treturn { kind: \"served\", facts: [], candidateCount: 0, suiteFilter };\n\t\t}\n\t\tconst egressOutcome = egressGate(candidates);\n\t\tif (egressOutcome.status === \"blocked\") return { kind: \"blocked\", reason: egressOutcome.reason };\n\t\tconst statementByMemoryId = new Map(egressOutcome.packet.facts.map((fact) => [fact.memoryId, fact.statement]));\n\t\tlet ordered: readonly ScoredRecallFactV1[] = candidates.map((entry) => ({\n\t\t\tmemoryId: entry.atom.memoryId,\n\t\t\tstatement: unwrapEgressStatement(statementByMemoryId.get(entry.atom.memoryId) ?? \"\"),\n\t\t\tconfidence: entry.atom.confidence,\n\t\t\tposition: entry.position,\n\t\t}));\n\t\t// 精排 (仅主融合路径: 降级页的排序即其降级语义)。reranker 不可用或调用\n\t\t// 失败 → 保持 RRF 序 (精排失败 → RRF 序), 各记一次结构化日志。\n\t\tconst reranker = channel.reranker;\n\t\tif (!channelDegraded && reranker !== undefined && !vectorRerankerUnavailable) {\n\t\t\ttry {\n\t\t\t\tconst scores = await reranker.rerank(\n\t\t\t\t\tinput.query,\n\t\t\t\t\tordered.map((fact) => fact.statement),\n\t\t\t\t);\n\t\t\t\tordered = ordered\n\t\t\t\t\t.map((fact, position) => ({ ...fact, score: scores[position] }))\n\t\t\t\t\t.sort((left, right) => (right.score ?? 0) - (left.score ?? 0) || left.position - right.position);\n\t\t\t} catch (error) {\n\t\t\t\tvectorRerankerUnavailable = true;\n\t\t\t\tapi.logger?.warn(\"memory reranker failed; recall continues on the RRF order\", {\n\t\t\t\t\terror: error instanceof Error ? error.message : String(error),\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\t// minScore 门槛 (§2.1): 仅精排实际产生 score 的候选参与过滤 (score <\n\t\t// minScore 丢弃), 过滤先于 limit 截取; 降级路径与无 score 的候选不受影响。\n\t\tconst minScore = input.minScore;\n\t\tconst eligible =\n\t\t\tminScore === undefined\n\t\t\t\t? ordered\n\t\t\t\t: ordered.filter((fact) => fact.score === undefined || fact.score >= minScore);\n\t\treturn {\n\t\t\tkind: \"served\",\n\t\t\tfacts: eligible\n\t\t\t\t.slice(0, input.limit)\n\t\t\t\t.map(({ memoryId, statement, confidence }) => ({ memoryId, statement, confidence })),\n\t\t\tcandidateCount: ordered.length,\n\t\t\tsuiteFilter,\n\t\t};\n\t};\n\n\t// 提取为具名闭包: 除模型工具面外, 下方的 auto-recall 钩子直接调用同一 execute\n\t// (单参输入约定, memoryToolInput 取第一参), 不经 runtime.invokeTool —— 钩子\n\t// 随插件注册/卸载, 与工具面同生命周期, 替换工具面不会重接自动召回。\n\tconst recallExecute = async (first: unknown, second?: unknown): Promise<unknown> => {\n\t\tconst input = memoryToolInput(first, second);\n\t\tassertNonEmptyString(input.query, \"query\");\n\t\tconst rawLimit: unknown = input.limit;\n\t\tconst limit =\n\t\t\trawLimit === undefined\n\t\t\t\t? DEFAULT_RECALL_LIMIT\n\t\t\t\t: ((): number => {\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\ttypeof rawLimit !== \"number\" ||\n\t\t\t\t\t\t\t!Number.isSafeInteger(rawLimit) ||\n\t\t\t\t\t\t\trawLimit < 1 ||\n\t\t\t\t\t\t\trawLimit > 100\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tthrow new Error(\"limit must be an integer between 1 and 100\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn rawLimit;\n\t\t\t\t\t})();\n\t\t// 内部执行缝 (§2.1): auto-recall 闭包经本字段恒传质量门槛; memory_recall\n\t\t// 工具 schema 不含该参数 (工具面恒 undefined), 显式召回行为不变。\n\t\tconst rawMinScore: unknown = input.minScore;\n\t\tconst minScore =\n\t\t\trawMinScore === undefined\n\t\t\t\t? undefined\n\t\t\t\t: ((): number => {\n\t\t\t\t\t\tif (typeof rawMinScore !== \"number\" || !Number.isFinite(rawMinScore)) {\n\t\t\t\t\t\t\tthrow new Error(\"minScore must be a finite number\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn rawMinScore;\n\t\t\t\t\t})();\n\t\tconst context = parseRecallContext(input.context);\n\t\tconst { domain } = resolveDomain();\n\n\t\t// enabling 失败 = 本实例终态 (mode 记录回退 off): 记忆工具返回携带原因\n\t\t// 与五选一行动清单的不可用 packet (设计 §3.2 硬性要求), 不再重试。\n\t\tif (vectorState === \"failed\") {\n\t\t\tconst failure = enablingFailure ?? { reason: \"memory enabling failed\", actions: [] as const };\n\t\t\treturn {\n\t\t\t\tstatus: \"unavailable\",\n\t\t\t\tstate: \"failed\",\n\t\t\t\tfacts: [] as MemoryFact[],\n\t\t\t\terror: `memory enabling failed: ${failure.reason}`,\n\t\t\t\tactions: failure.actions,\n\t\t\t};\n\t\t}\n\n\t\t// 主键快路径 (memoryIdEquals 语义保留, 幂等/引用而非检索): query 恰为本\n\t\t// 域可见 memoryId 时直接取该条, 不依赖向量通道 (enabling 期间也可用)。\n\t\tlet outcome: SemanticRecallOutcomeV1;\n\t\tconst directRecord = store.get(input.query.trim(), {\n\t\t\towner: BUILTIN_MEMORY_OWNER,\n\t\t\t...(domain.kind === \"suite\" ? { suiteId: domain.suiteId } : {}),\n\t\t});\n\t\tif (directRecord !== undefined) {\n\t\t\tconst gate = egressGate([{ atom: directRecord.atom, position: 0 }]);\n\t\t\tif (gate.status === \"blocked\") {\n\t\t\t\treturn { status: \"failed\", facts: [] as MemoryFact[], error: `egress blocked: ${gate.reason}` };\n\t\t\t}\n\t\t\tconst statementByMemoryId = new Map(gate.packet.facts.map((fact) => [fact.memoryId, fact.statement]));\n\t\t\toutcome = {\n\t\t\t\tkind: \"served\",\n\t\t\t\tfacts: [\n\t\t\t\t\t{\n\t\t\t\t\t\tmemoryId: directRecord.atom.memoryId,\n\t\t\t\t\t\tstatement: unwrapEgressStatement(statementByMemoryId.get(directRecord.atom.memoryId) ?? \"\"),\n\t\t\t\t\t\tconfidence: directRecord.atom.confidence,\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t\tcandidateCount: 1,\n\t\t\t\tsuiteFilter: { legacySkipped: 0, foreignSuiteSkipped: 0 },\n\t\t\t};\n\t\t} else {\n\t\t\ttry {\n\t\t\t\toutcome = await recallSemantically({\n\t\t\t\t\tquery: input.query,\n\t\t\t\t\tdomain,\n\t\t\t\t\tlimit,\n\t\t\t\t\t...(minScore === undefined ? {} : { minScore }),\n\t\t\t\t});\n\t\t\t} catch (error) {\n\t\t\t\t// 全败 (枚举兜底也失败) → degraded packet: 结构化返回, 不抛给模型。\n\t\t\t\tvectorState = \"degraded\";\n\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\tapi.logger?.warn(\"memory recall degraded: every channel failed; returning a degraded packet\", {\n\t\t\t\t\terror: message,\n\t\t\t\t});\n\t\t\t\treturn { status: \"failed\", facts: [] as MemoryFact[], error: message };\n\t\t\t}\n\t\t}\n\t\tif (outcome.kind === \"blocked\") {\n\t\t\treturn { status: \"failed\", facts: [] as MemoryFact[], error: `egress blocked: ${outcome.reason}` };\n\t\t}\n\t\tif (outcome.kind === \"unavailable\") {\n\t\t\treturn {\n\t\t\t\tstatus: \"unavailable\",\n\t\t\t\tstate: vectorState,\n\t\t\t\tfacts: [] as MemoryFact[],\n\t\t\t\terror: outcome.message,\n\t\t\t};\n\t\t}\n\t\tconst facts = outcome.facts;\n\t\tconst resolved =\n\t\t\tcontext === undefined\n\t\t\t\t? undefined\n\t\t\t\t: applyPreferenceContext({\n\t\t\t\t\t\tfacts,\n\t\t\t\t\t\tcontext,\n\t\t\t\t\t\tresolver: preferenceResolver,\n\t\t\t\t\t\tdisambiguator: preferenceDisambiguator,\n\t\t\t\t\t\tatomOf: (memoryId) => store.get(memoryId, { owner: BUILTIN_MEMORY_OWNER })?.atom,\n\t\t\t\t\t});\n\t\t// 偏好消歧先于关系扩散: the diffusion seeds are the atoms the user\n\t\t// actually sees — preference facts dropped by the conflict resolution\n\t\t// never diffuse their relations.\n\t\tconst finalFacts = resolved === undefined ? facts : resolved.facts;\n\t\tconst seeds: MemoryAtomV1[] = [];\n\t\tfor (const fact of finalFacts) {\n\t\t\tconst atom = store.get(fact.memoryId, { owner: BUILTIN_MEMORY_OWNER })?.atom;\n\t\t\tif (atom !== undefined) seeds.push(atom);\n\t\t}\n\t\t// The traversal graph is the recall domain's visible atom set (same\n\t\t// suite read boundary). Diffusion targets outside this set are dropped,\n\t\t// so relations never leak across suites.\n\t\tconst domainMemoryIds = new Set<string>();\n\t\tconst graph: MemoryAtomV1[] = [];\n\t\tfor (const entry of listDomainMemories({ store }, { owner: BUILTIN_MEMORY_OWNER, domain }).entries) {\n\t\t\tdomainMemoryIds.add(entry.memoryId);\n\t\t\tconst atom = store.get(entry.memoryId, { owner: BUILTIN_MEMORY_OWNER })?.atom;\n\t\t\tif (atom !== undefined) graph.push(atom);\n\t\t}\n\t\tconst related: MemoryNetworkExpansionV1[] = [];\n\t\tlet relatedOmittedCount = 0;\n\t\tconst seenRelatedMemoryIds = new Set<string>();\n\t\tfor (const adapterResult of memoryNetwork.expand({ seeds, graph })) {\n\t\t\tif (adapterResult.status !== \"completed\") continue;\n\t\t\tfor (const expansion of adapterResult.related) {\n\t\t\t\tif (!domainMemoryIds.has(expansion.memoryId)) continue;\n\t\t\t\tif (seenRelatedMemoryIds.has(expansion.memoryId)) continue;\n\t\t\t\tseenRelatedMemoryIds.add(expansion.memoryId);\n\t\t\t\tif (related.length >= MAX_RELATED_REFS) {\n\t\t\t\t\trelatedOmittedCount += 1;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\trelated.push(expansion);\n\t\t\t}\n\t\t}\n\t\treturn {\n\t\t\tstatus: finalFacts.length === 0 ? \"empty\" : \"completed\",\n\t\t\tfacts: finalFacts,\n\t\t\t...(resolved !== undefined && resolved.conflicts.length > 0 ? { conflicts: resolved.conflicts } : {}),\n\t\t\t...(domain.kind === \"suite\" ? { suiteFilter: outcome.suiteFilter } : {}),\n\t\t\tomittedCount: Math.max(0, outcome.candidateCount - finalFacts.length),\n\t\t\ttruncated: false,\n\t\t\t// References only — no statement/payload egress; the model fetches\n\t\t\t// content via memory_list or another memory_recall.\n\t\t\t...(related.length > 0\n\t\t\t\t? {\n\t\t\t\t\t\trelated: related.map((expansion) => ({\n\t\t\t\t\t\t\tmemoryId: expansion.memoryId,\n\t\t\t\t\t\t\trelationKind: expansion.relationKind,\n\t\t\t\t\t\t\thop: expansion.hop,\n\t\t\t\t\t\t\tweight: expansion.weight,\n\t\t\t\t\t\t\tvia: expansion.via,\n\t\t\t\t\t\t})),\n\t\t\t\t\t}\n\t\t\t\t: {}),\n\t\t\t...(relatedOmittedCount > 0 ? { relatedOmittedCount } : {}),\n\t\t};\n\t};\n\tconst recallTool = api.registerTool({\n\t\tname: MEMORY_RECALL_TOOL_NAME,\n\t\tlabel: \"Recall memories\",\n\t\tpromptGuidelines: [MEMORY_TOOLS_GUIDE],\n\t\tdescription:\n\t\t\t\"Search this suite's durable memories with semantic recall: vector and full-text channels fuse (RRF) and the result is reranked; an exact memoryId retrieves that memory directly; the result may include related memory references (ids and relation metadata only — recall or list a referenced memory again for its content)\",\n\t\tparameters: recallSchema,\n\t\texecute: recallExecute,\n\t});\n\n\t// ---- B1 记忆自动注入 (auto recall; D-071 增补裁决: 自 sdk transformContext 迁入) ----\n\t// transformContext 链上的插件环节: 无事实可注入时返回 undefined 保持输入。工厂\n\t// 执行 ⟺ memory 家族注册 (mode=off 时整体不注册), 钩子随插件卸载自动摘除, 无需\n\t// 再检查工具在位。失败降级 (recall throw / 契约漂移): 不注入、不阻塞请求, 记\n\t// 一条有界 api.logger warn。\n\tconst autoRecallBlock = async (query: string): Promise<string> => {\n\t\ttry {\n\t\t\t// §2.1: 自动召回恒传质量门槛 (内部闭包, 不经工具 schema)。\n\t\t\tconst result: unknown = await recallExecute({\n\t\t\t\tquery,\n\t\t\t\tlimit: MEMORY_AUTO_RECALL_LIMIT,\n\t\t\t\tminScore: MEMORY_AUTO_RECALL_MIN_RERANK_SCORE,\n\t\t\t});\n\t\t\t// Defensive contract-drift checks: recallExecute is this factory's own\n\t\t\t// closure, so both branches are unreachable today — they exist in case\n\t\t\t// the recall body ever grows an adapter seam (then add a test seam for\n\t\t\t// them; until then there is no honest way to drive them from a test).\n\t\t\tif (result === null || typeof result !== \"object\") {\n\t\t\t\tapi.logger?.warn(\"memory auto-recall failed: memory_recall returned a non-object result (contract drift)\");\n\t\t\t\treturn \"\";\n\t\t\t}\n\t\t\tconst facts = (result as { facts?: unknown }).facts;\n\t\t\tif (!Array.isArray(facts)) {\n\t\t\t\tapi.logger?.warn(\n\t\t\t\t\t'memory auto-recall failed: memory_recall returned a non-array \"facts\" field (contract drift)',\n\t\t\t\t);\n\t\t\t\treturn \"\";\n\t\t\t}\n\t\t\tconst statements: string[] = [];\n\t\t\tfor (const fact of facts) {\n\t\t\t\tif (fact !== null && typeof fact === \"object\") {\n\t\t\t\t\tconst statement = (fact as { statement?: unknown }).statement;\n\t\t\t\t\tif (typeof statement === \"string\" && statement.trim() !== \"\") statements.push(statement);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (statements.length === 0) return \"\";\n\t\t\treturn [\n\t\t\t\tMEMORY_AUTO_RECALL_MARKER,\n\t\t\t\t...statements.map((statement) => `- ${statement}`),\n\t\t\t\t\"</auto_recalled_memory>\",\n\t\t\t\tMEMORY_AUTO_RECALL_DISCLAIMER,\n\t\t\t].join(\"\\n\");\n\t\t} catch (error) {\n\t\t\t// Bound by code points, not UTF-16 code units, so the cut never splits a\n\t\t\t// surrogate pair into a lone surrogate.\n\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\tconst codePoints = Array.from(message);\n\t\t\tconst bounded =\n\t\t\t\tcodePoints.length <= MEMORY_AUTO_RECALL_ERROR_MESSAGE_LIMIT\n\t\t\t\t\t? message\n\t\t\t\t\t: `${codePoints.slice(0, MEMORY_AUTO_RECALL_ERROR_MESSAGE_LIMIT).join(\"\")}…`;\n\t\t\tapi.logger?.warn(`memory auto-recall failed: memory_recall threw: ${bounded}`);\n\t\t\treturn \"\";\n\t\t}\n\t};\n\t// Per-session cache keyed by the last user message text: one user turn runs\n\t// at most one recall; the remembered block rides every model round of that\n\t// turn unchanged, so request-time injections stay idempotent across tool\n\t// loops. An empty block means \"no injection for this turn\" (no facts, or\n\t// recall failure — the recall is an enhancement face and must never block\n\t// the user's request).\n\tconst autoRecall: { lastQuery: string | undefined; block: string } = { lastQuery: undefined, block: \"\" };\n\tconst autoRecallHook = api.registerLoopHook(\"transformContext\", async (messages) => {\n\t\tconst userText = lastBranchUserText(api);\n\t\tif (userText === undefined || userText.trim() === \"\") return undefined;\n\t\tif (autoRecall.lastQuery !== userText) {\n\t\t\tautoRecall.lastQuery = userText;\n\t\t\tautoRecall.block = await autoRecallBlock(userText);\n\t\t}\n\t\tif (autoRecall.block === \"\") return undefined;\n\t\treturn injectAfterLastUserMessage(messages, {\n\t\t\trole: \"user\",\n\t\t\tcontent: autoRecall.block,\n\t\t\ttimestamp: now(),\n\t\t});\n\t});\n\n\t// ---- assistant 偏好卡注入 (统一修复轮 A 交付3; D-075 S4-4 第二批随拆包迁入) ----\n\t// 宿主 sdk 曾在 system prompt 的 persona 段后注入本卡; 公共插件 API 无 system\n\t// prompt 面, 迁入后经 transformContext 循环钩子注入 (最后一条 user 消息之后,\n\t// 与 auto-recall 同一请求时视图)。卡文本与读边界语义逐字保留 (宿主\n\t// memory-assistant-card.test.ts pin 的格式): own-suite 偏好 + 已晋升 user-default\n\t// 偏好, legacy 原子不可见。会话期一卡: 首次模型轮次惰性读取并缓存 (绑定入口在\n\t// 会话构造后才写入, 工厂时间读不到 domain — 与 B1 同因), 后续轮次复用。判定\n\t// \"assistant 定向\" 用 builtin 默认方案 id (宿主 builtin suite 事实: assistant 方\n\t// 案的 orientation 即 assistant); 自定义方案的 orientation 宿主未上公共通道,\n\t// 不覆盖。IO 错误降级为一条 api.logger warn + 无注入 (插件无 suite 诊断通道,\n\t// warn 即对等物), 绝不阻塞请求。\n\tconst assistantCard: { loaded: boolean; text: string | undefined } = { loaded: false, text: undefined };\n\tconst preferenceCardHook = api.registerLoopHook(\"transformContext\", async (messages) => {\n\t\tif (!assistantCard.loaded) {\n\t\t\tassistantCard.loaded = true;\n\t\t\tconst domain = resolveSessionDomain(api);\n\t\t\tif (domain.kind === \"suite\" && domain.suiteId === \"assistant\") {\n\t\t\t\ttry {\n\t\t\t\t\tassistantCard.text = loadAssistantPreferenceCard({\n\t\t\t\t\t\tagentDir: context.agentDir,\n\t\t\t\t\t\towner: BUILTIN_MEMORY_OWNER,\n\t\t\t\t\t\tsuiteId: domain.suiteId,\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\t\tapi.logger?.warn(`assistant preference card could not be loaded: ${message}`);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (assistantCard.text === undefined) return undefined;\n\t\treturn injectAfterLastUserMessage(messages, {\n\t\t\trole: \"user\",\n\t\t\tcontent: assistantCard.text,\n\t\t\ttimestamp: now(),\n\t\t});\n\t});\n\n\t// ---- bash 失败教训自动沉淀 (错误教训与召回质量护栏设计 §2.2) ----\n\t// 家族内独立开关 (默认 on): mode=off 已在工厂入口零注册; lessons=off 时本块\n\t// 不订阅任何事件 (无观察者即无捕获)。两个事件的公开载荷都不含工具入参, 命令\n\t// 字符串在 tool.execution.start 时经会话分支的 assistant toolCall 块回读\n\t// (与 lastBranchUserText 同一数据来源), 以 toolCallId 配对到 tool.result。\n\tconst lessonsResolution = resolveMemoryLessons(context.memory?.lessons);\n\tif (lessonsResolution.invalidEnvValue !== undefined) {\n\t\tapi.logger?.warn(\n\t\t\t`invalid ${MEMORY_LESSONS_ENV} value \"${lessonsResolution.invalidEnvValue}\"; bash error-lesson capture stays on`,\n\t\t);\n\t}\n\t// lifecycle 在 CapabilityAPI 类型上是必选成员(experimental),但最小宿主可以\n\t// 不带该面;教训是增强面,与 auto-recall 同款降级:缺面时跳过注册、记一条\n\t// 有界 warn,不影响记忆家族其余工具面。\n\tconst lifecycleReady = api.lifecycle !== undefined;\n\tif (lessonsResolution.lessons === \"on\" && !lifecycleReady) {\n\t\tapi.logger?.warn(\"memory bash-lesson capture unavailable: host exposes no lifecycle face\");\n\t}\n\tconst lessonRegistrations: DisposableRegistration[] = [];\n\tif (lessonsResolution.lessons === \"on\" && lifecycleReady) {\n\t\t/** (command, exitCode) 签名去重 (§2.2 风暴闸门): 同签名重复失败只写首条。 */\n\t\tconst lessonSignatures = new Set<string>();\n\t\tlet lessonsWritten = 0;\n\t\tlet lessonCapWarned = false;\n\t\t/** toolCallId → command 配对映射 (仅 bash, 容量 FIFO)。 */\n\t\tconst bashCommandsByToolCallId = new Map<string, string>();\n\n\t\tconst rememberBashCommand = (toolCallId: string, command: string): void => {\n\t\t\tif (\n\t\t\t\t!bashCommandsByToolCallId.has(toolCallId) &&\n\t\t\t\tbashCommandsByToolCallId.size >= MEMORY_LESSON_TOOLCALL_MAP_CAPACITY\n\t\t\t) {\n\t\t\t\tconst oldest = bashCommandsByToolCallId.keys().next();\n\t\t\t\tif (oldest.done !== true) bashCommandsByToolCallId.delete(oldest.value);\n\t\t\t}\n\t\t\tbashCommandsByToolCallId.set(toolCallId, command);\n\t\t};\n\n\t\t/**\n\t\t * 从会话分支回读该 toolCall 的 bash 命令 (assistant toolCall 块, 最新\n\t\t * 优先): assistant 消息在工具执行前已入分支, tool.execution.start 时可查。\n\t\t */\n\t\tconst bashCommandForToolCall = (toolCallId: string): string | undefined => {\n\t\t\tconst entries = api.session?.getBranchEntries() ?? [];\n\t\t\tfor (let index = entries.length - 1; index >= 0; index -= 1) {\n\t\t\t\tconst entry = entries[index];\n\t\t\t\tif (entry.type !== \"message\" || entry.message?.role !== \"assistant\") continue;\n\t\t\t\tconst content: unknown = entry.message.content;\n\t\t\t\tif (!Array.isArray(content)) continue;\n\t\t\t\tfor (const item of content) {\n\t\t\t\t\tif (!isPlainObject(item) || item.type !== \"toolCall\" || item.id !== toolCallId) continue;\n\t\t\t\t\tconst args = item.arguments;\n\t\t\t\t\tconst command = isPlainObject(args) ? args.command : undefined;\n\t\t\t\t\tif (typeof command === \"string\" && command.trim() !== \"\") return command;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn undefined;\n\t\t};\n\n\t\t/**\n\t\t * 内部直写 (与 /promote 同构): ledger 追加持久副本, store.commit 以\n\t\t * retentionModeId \"short\" 覆盖保留档 (7 天到期失去召回资格, 不物理清除),\n\t\t * 向量投影异步埋点。教训不是模型工具调用, 不经 candidate 状态机。\n\t\t */\n\t\tconst writeLesson = (command: string, exitCode: string, errorText: string): void => {\n\t\t\tconst signature = `${command}\\u0000${exitCode}`;\n\t\t\tif (lessonSignatures.has(signature)) return;\n\t\t\tif (lessonsWritten >= MEMORY_LESSON_MAX_PER_SESSION) {\n\t\t\t\tif (!lessonCapWarned) {\n\t\t\t\t\tlessonCapWarned = true;\n\t\t\t\t\tapi.logger?.warn(\n\t\t\t\t\t\t`memory bash-lesson cap reached (${MEMORY_LESSON_MAX_PER_SESSION} per instance); further failures are not captured`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst { domain, ledger } = resolveDomain();\n\t\t\tconst atom = buildWriteAtom({\n\t\t\t\tcontent: buildLessonContent(command, exitCode, errorText),\n\t\t\t\tkind: \"fact\",\n\t\t\t\ttags: [...MEMORY_LESSON_TAGS],\n\t\t\t\tdomain,\n\t\t\t\twriteReason: `${capabilityManifest.id}/auto-lesson`,\n\t\t\t});\n\t\t\tledger.append(atom);\n\t\t\tstore.commit(atom, { retentionModeId: MEMORY_LESSON_RETENTION_MODE_ID });\n\t\t\tvectorUpsertAtom(atom);\n\t\t\tlessonsWritten += 1;\n\t\t\tlessonSignatures.add(signature);\n\t\t};\n\n\t\tlessonRegistrations.push(\n\t\t\tapi.lifecycle.registerObserve<MemoryToolExecutionStartEventDataV1>(\n\t\t\t\tTOOL_EXECUTION_START_EVENT_ID,\n\t\t\t\t(event) => {\n\t\t\t\t\tif (event.data.toolName !== \"bash\") return;\n\t\t\t\t\tconst command = bashCommandForToolCall(event.data.toolCallId);\n\t\t\t\t\tif (command !== undefined) rememberBashCommand(event.data.toolCallId, command);\n\t\t\t\t},\n\t\t\t\tTOOL_EXECUTION_START_EVENT_VERSION,\n\t\t\t),\n\t\t);\n\t\tlessonRegistrations.push(\n\t\t\tapi.lifecycle.registerObserve<MemoryToolResultEventDataV1>(\n\t\t\t\tTOOL_RESULT_EVENT_ID,\n\t\t\t\t(event) => {\n\t\t\t\t\tconst data = event.data;\n\t\t\t\t\tif (data.toolName !== \"bash\" || !data.isError) return;\n\t\t\t\t\tconst command = bashCommandsByToolCallId.get(data.toolCallId);\n\t\t\t\t\tbashCommandsByToolCallId.delete(data.toolCallId);\n\t\t\t\t\tif (command === undefined) {\n\t\t\t\t\t\t// 配对失败 (start 未命中/映射溢出): 不写教训, 记一条有界 debug。\n\t\t\t\t\t\tapi.logger?.debug(\"memory bash-lesson capture skipped: no command paired with the failed toolCall\", {\n\t\t\t\t\t\t\ttoolCallId: data.toolCallId,\n\t\t\t\t\t\t});\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tconst errorText = toolResultTextOf(data.message.content);\n\t\t\t\t\twriteLesson(command, lessonExitCodeOf(errorText), errorText);\n\t\t\t\t},\n\t\t\t\tTOOL_RESULT_EVENT_VERSION,\n\t\t\t),\n\t\t);\n\t}\n\n\tconst listTool = api.registerTool({\n\t\tname: \"memory_list\",\n\t\tlabel: \"List memories\",\n\t\tdescription: \"List this suite's durable memories (the explicit management surface for personal data)\",\n\t\tparameters: listSchema,\n\t\texecute: async (first: unknown, second?: unknown) => {\n\t\t\tconst input = memoryToolInput(first, second);\n\t\t\tconst rawLimit: unknown = input.limit;\n\t\t\tconst limit =\n\t\t\t\trawLimit === undefined\n\t\t\t\t\t? DEFAULT_LIST_LIMIT\n\t\t\t\t\t: ((): number => {\n\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\ttypeof rawLimit !== \"number\" ||\n\t\t\t\t\t\t\t\t!Number.isSafeInteger(rawLimit) ||\n\t\t\t\t\t\t\t\trawLimit < 1 ||\n\t\t\t\t\t\t\t\trawLimit > 100\n\t\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t\tthrow new Error(\"limit must be an integer between 1 and 100\");\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn rawLimit;\n\t\t\t\t\t\t})();\n\t\t\tconst { domain } = resolveDomain();\n\t\t\tconst page = listDomainMemories({ store }, { owner: BUILTIN_MEMORY_OWNER, domain });\n\t\t\treturn {\n\t\t\t\tdomain: page.domain,\n\t\t\t\tentries: page.entries.slice(0, limit).map(entryToToolEntry),\n\t\t\t\ttotal: page.entries.length,\n\t\t\t\tpreferenceCount: page.preferenceCount,\n\t\t\t\tfactCount: page.factCount,\n\t\t\t\tfilter: page.filter,\n\t\t\t};\n\t\t},\n\t});\n\n\tconst forgetTool = api.registerTool({\n\t\tname: \"memory_forget\",\n\t\tlabel: \"Forget memory\",\n\t\tdescription: \"Physically delete one memory of this suite by memoryId (purge-gated, journalled, irreversible)\",\n\t\tparameters: forgetSchema,\n\t\texecute: async (first: unknown, second?: unknown) => {\n\t\t\tconst input = memoryToolInput(first, second);\n\t\t\tassertNonEmptyString(input.memoryId, \"memoryId\");\n\t\t\tconst { domain, domainName, ledger } = resolveDomain();\n\t\t\tconst authorizationRef: SuiteForgetAuthorizationRefV1 = {\n\t\t\t\tmode: \"user-immediate\",\n\t\t\t\tissuedAt: now(),\n\t\t\t\tissuedBy: capabilityManifest.id,\n\t\t\t\tconfirmationRef: `memory_forget:${sessionId}:${input.memoryId}`,\n\t\t\t};\n\t\t\tconst result = forgetDomainMemory(\n\t\t\t\t{\n\t\t\t\t\tstore,\n\t\t\t\t\tpurgeGate,\n\t\t\t\t\tpurgeJournal,\n\t\t\t\t\t// index 副本分派 (混合检索工程化): canonical 走下方原回调 (行为零\n\t\t\t\t\t// 变化), 向量投影由 suite-memory 的 executePurgeBatch 按副本路由到这里。\n\t\t\t\t\tpurgeIndexReplica: vectorPurgeMemories,\n\t\t\t\t\tnow,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\towner: BUILTIN_MEMORY_OWNER,\n\t\t\t\t\tdomain,\n\t\t\t\t\tmemoryId: input.memoryId,\n\t\t\t\t\tauthorizedBy: `${capabilityManifest.id}:memory_forget`,\n\t\t\t\t\tauthorizationRef,\n\t\t\t\t},\n\t\t\t\t(memoryIds) => {\n\t\t\t\t\tconst purged = new Set(memoryIds);\n\t\t\t\t\t// Physical replica purge: rewrite the durable ledger without the\n\t\t\t\t\t// purged atoms, then evict them from the in-memory store so the\n\t\t\t\t\t// running process cannot recall them either (both idempotent for\n\t\t\t\t\t// purge_eligible retries).\n\t\t\t\t\tledger.rewrite(ledger.atomsSnapshot().filter((atom) => !purged.has(atom.memoryId)));\n\t\t\t\t\tfor (const memoryId of memoryIds) store.evict(memoryId);\n\t\t\t\t},\n\t\t\t);\n\t\t\tif (result.status === \"completed\") {\n\t\t\t\tawait api.publish({\n\t\t\t\t\ttype: EVENT_TYPE,\n\t\t\t\t\tversion: 1,\n\t\t\t\t\tcorrelationId: capabilityManifest.id,\n\t\t\t\t\tdata: { action: \"forgotten\", memoryId: input.memoryId, domain: domainName, batchId: result.batchId },\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn { status: result.status, memoryId: input.memoryId };\n\t\t},\n\t});\n\n\t/** Canonical memoryId of the promoted copy of one preference (promotePreferenceToUserDefault suffix). */\n\tconst userDefaultMemoryId = (memoryId: string): string => `${memoryId}-user-default`;\n\n\t/**\n\t * /promote (偏好晋升触发, B2): the production caller of\n\t * {@link promotePreferenceToUserDefault}. Promotes one of THIS session's\n\t * preferences to the user-default scope: the promoted canonical atom is\n\t * appended to the session domain's durable ledger and committed to the\n\t * store, so every suite's preference-card read (which applies the suite\n\t * read boundary across all of the owner's ledgers) sees it from now on.\n\t * The original atom is never rewritten. Errors (unknown id, non-preference,\n\t * already promoted) surface as error results, never as thrown failures —\n\t * a slash command must not crash the host.\n\t */\n\tconst promoteCommand = api.registerCommand({\n\t\tname: \"promote\",\n\t\tdescription: \"Promote a memory preference to the user-default scope (visible in every suite)\",\n\t\targumentHint: \"<memoryId>\",\n\t\texecute: async (args: string) => {\n\t\t\tconst memoryId = args.trim();\n\t\t\tif (memoryId === \"\") {\n\t\t\t\treturn {\n\t\t\t\t\tstatus: \"error\" as const,\n\t\t\t\t\tmessage: \"Usage: /promote <memoryId> — run memory_list for this suite's memory ids\",\n\t\t\t\t};\n\t\t\t}\n\t\t\tconst record = store.get(memoryId, { owner: BUILTIN_MEMORY_OWNER });\n\t\t\tif (!record) {\n\t\t\t\treturn {\n\t\t\t\t\tstatus: \"error\" as const,\n\t\t\t\t\tmessage: `No memory found with id ${memoryId}; run memory_list for this suite's memory ids`,\n\t\t\t\t};\n\t\t\t}\n\t\t\tconst atom = record.atom;\n\t\t\tif (atom.memoryKind !== \"preference\" || atom.preference === undefined) {\n\t\t\t\treturn {\n\t\t\t\t\tstatus: \"error\" as const,\n\t\t\t\t\tmessage: `${memoryId} is a fact, not a preference; only preferences can be promoted`,\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (store.get(userDefaultMemoryId(memoryId), { owner: BUILTIN_MEMORY_OWNER }) !== undefined) {\n\t\t\t\treturn {\n\t\t\t\t\tstatus: \"error\" as const,\n\t\t\t\t\tmessage: `Preference ${memoryId} is already promoted to the user-default scope`,\n\t\t\t\t};\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst promoted = promotePreferenceToUserDefault({\n\t\t\t\t\tatom,\n\t\t\t\t\tauthorizedBy: \"user:/promote\",\n\t\t\t\t\tconfirmedAt: new Date(now()).toISOString(),\n\t\t\t\t});\n\t\t\t\tconst { domainName, ledger } = resolveDomain();\n\t\t\t\tledger.append(promoted);\n\t\t\t\tstore.commit(promoted);\n\t\t\t\t// 向量投影埋点: 与 memory_write 同一异步旁路 (失败不阻塞命令返回)。\n\t\t\t\tvectorUpsertAtom(promoted);\n\t\t\t\treturn {\n\t\t\t\t\tstatus: \"success\" as const,\n\t\t\t\t\tmessage: `Promoted \"${unwrapEgressStatement(statementOf(promoted.payload))}\" to the user-default scope; it is now visible in every suite`,\n\t\t\t\t\tdata: { memoryId: promoted.memoryId, sourceMemoryId: memoryId, domain: domainName },\n\t\t\t\t};\n\t\t\t} catch (error) {\n\t\t\t\treturn { status: \"error\" as const, message: error instanceof Error ? error.message : String(error) };\n\t\t\t}\n\t\t},\n\t});\n\n\t// memory.scheduler@1 plugin capability (宪法 §3 显式声明面): a declarative\n\t// marker only — the scheduler enforcement itself (per-instance single\n\t// lease, acquired above) stays carried by the host builtin implementation.\n\t// The declaration anchors \"this suite's memory scheduling is provided by\n\t// this plugin\" so the code Profile assembly check (requiresMemoryScheduler)\n\t// and third-party replacement detection have an explicit surface:\n\t// list()-visible, a same-id double provide fails explicitly (no override\n\t// priority), and after dispose a replacement provider can take the id.\n\tconst schedulerCapability = api.capabilities.provide({ id: \"memory.scheduler\", version: 1, kind: \"service\" }, () =>\n\t\tObject.freeze({\n\t\t\tid: \"memory.scheduler\",\n\t\t\tversion: 1,\n\t\t\towner: \"agent-forge.builtin.memory\",\n\t\t\tleaseScope: \"per-agent-instance\",\n\t\t}),\n\t);\n\n\tconst registrations = [\n\t\twriteTool,\n\t\trecallTool,\n\t\tlistTool,\n\t\tforgetTool,\n\t\tpromoteCommand,\n\t\tschedulerCapability,\n\t\tautoRecallHook,\n\t\tpreferenceCardHook,\n\t\t...lessonRegistrations,\n\t];\n\treturn {\n\t\tregistrations,\n\t\tsettleVectorWork: async () => {\n\t\t\tawait vectorQueue;\n\t\t},\n\t\tvectorState: (): \"off\" | MemoryCapabilityStateV1 => vectorState,\n\t\t// 触发惰性 enabling (已注入组件时已就绪) 并等待其落定: ready 或结构化失败。\n\t\tenablingOutcome: async () => {\n\t\t\tif (vectorState === \"enabling\") void vectorEnsure();\n\t\t\tawait enablingSettledPromise;\n\t\t\treturn enablingFailure ?? \"ready\";\n\t\t},\n\t};\n}\n"]}