@ai-agent-forge/plugin-memory 0.85.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/README.md +65 -0
  2. package/agent-forge.json +11 -0
  3. package/dist/capability.d.ts +182 -0
  4. package/dist/capability.d.ts.map +1 -0
  5. package/dist/capability.js +2565 -0
  6. package/dist/capability.js.map +1 -0
  7. package/dist/entry.d.ts +36 -0
  8. package/dist/entry.d.ts.map +1 -0
  9. package/dist/entry.js +154 -0
  10. package/dist/entry.js.map +1 -0
  11. package/dist/index.d.ts +49 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +49 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/memory/assistant-card.d.ts +31 -0
  16. package/dist/memory/assistant-card.d.ts.map +1 -0
  17. package/dist/memory/assistant-card.js +108 -0
  18. package/dist/memory/assistant-card.js.map +1 -0
  19. package/dist/memory/candidates.d.ts +65 -0
  20. package/dist/memory/candidates.d.ts.map +1 -0
  21. package/dist/memory/candidates.js +100 -0
  22. package/dist/memory/candidates.js.map +1 -0
  23. package/dist/memory/code-memory.d.ts +89 -0
  24. package/dist/memory/code-memory.d.ts.map +1 -0
  25. package/dist/memory/code-memory.js +104 -0
  26. package/dist/memory/code-memory.js.map +1 -0
  27. package/dist/memory/compaction-sequencer.d.ts +63 -0
  28. package/dist/memory/compaction-sequencer.d.ts.map +1 -0
  29. package/dist/memory/compaction-sequencer.js +129 -0
  30. package/dist/memory/compaction-sequencer.js.map +1 -0
  31. package/dist/memory/continuation.d.ts +44 -0
  32. package/dist/memory/continuation.d.ts.map +1 -0
  33. package/dist/memory/continuation.js +49 -0
  34. package/dist/memory/continuation.js.map +1 -0
  35. package/dist/memory/curation.d.ts +58 -0
  36. package/dist/memory/curation.d.ts.map +1 -0
  37. package/dist/memory/curation.js +68 -0
  38. package/dist/memory/curation.js.map +1 -0
  39. package/dist/memory/egress-policy.d.ts +50 -0
  40. package/dist/memory/egress-policy.d.ts.map +1 -0
  41. package/dist/memory/egress-policy.js +71 -0
  42. package/dist/memory/egress-policy.js.map +1 -0
  43. package/dist/memory/embedding-provider.d.ts +70 -0
  44. package/dist/memory/embedding-provider.d.ts.map +1 -0
  45. package/dist/memory/embedding-provider.js +164 -0
  46. package/dist/memory/embedding-provider.js.map +1 -0
  47. package/dist/memory/embedding-reranker.d.ts +56 -0
  48. package/dist/memory/embedding-reranker.d.ts.map +1 -0
  49. package/dist/memory/embedding-reranker.js +109 -0
  50. package/dist/memory/embedding-reranker.js.map +1 -0
  51. package/dist/memory/foundation.d.ts +168 -0
  52. package/dist/memory/foundation.d.ts.map +1 -0
  53. package/dist/memory/foundation.js +487 -0
  54. package/dist/memory/foundation.js.map +1 -0
  55. package/dist/memory/host-module-import.d.ts +25 -0
  56. package/dist/memory/host-module-import.d.ts.map +1 -0
  57. package/dist/memory/host-module-import.js +41 -0
  58. package/dist/memory/host-module-import.js.map +1 -0
  59. package/dist/memory/ledger.d.ts +58 -0
  60. package/dist/memory/ledger.d.ts.map +1 -0
  61. package/dist/memory/ledger.js +315 -0
  62. package/dist/memory/ledger.js.map +1 -0
  63. package/dist/memory/lifecycle.d.ts +124 -0
  64. package/dist/memory/lifecycle.d.ts.map +1 -0
  65. package/dist/memory/lifecycle.js +201 -0
  66. package/dist/memory/lifecycle.js.map +1 -0
  67. package/dist/memory/memory-network.d.ts +55 -0
  68. package/dist/memory/memory-network.d.ts.map +1 -0
  69. package/dist/memory/memory-network.js +70 -0
  70. package/dist/memory/memory-network.js.map +1 -0
  71. package/dist/memory/model-cache-hygiene.d.ts +18 -0
  72. package/dist/memory/model-cache-hygiene.d.ts.map +1 -0
  73. package/dist/memory/model-cache-hygiene.js +38 -0
  74. package/dist/memory/model-cache-hygiene.js.map +1 -0
  75. package/dist/memory/preference-disambiguator.d.ts +43 -0
  76. package/dist/memory/preference-disambiguator.d.ts.map +1 -0
  77. package/dist/memory/preference-disambiguator.js +81 -0
  78. package/dist/memory/preference-disambiguator.js.map +1 -0
  79. package/dist/memory/preference-lifecycle.d.ts +66 -0
  80. package/dist/memory/preference-lifecycle.d.ts.map +1 -0
  81. package/dist/memory/preference-lifecycle.js +129 -0
  82. package/dist/memory/preference-lifecycle.js.map +1 -0
  83. package/dist/memory/preference-promotion.d.ts +87 -0
  84. package/dist/memory/preference-promotion.d.ts.map +1 -0
  85. package/dist/memory/preference-promotion.js +102 -0
  86. package/dist/memory/preference-promotion.js.map +1 -0
  87. package/dist/memory/preference-resolver.d.ts +44 -0
  88. package/dist/memory/preference-resolver.d.ts.map +1 -0
  89. package/dist/memory/preference-resolver.js +107 -0
  90. package/dist/memory/preference-resolver.js.map +1 -0
  91. package/dist/memory/purge-journal.d.ts +76 -0
  92. package/dist/memory/purge-journal.d.ts.map +1 -0
  93. package/dist/memory/purge-journal.js +130 -0
  94. package/dist/memory/purge-journal.js.map +1 -0
  95. package/dist/memory/purge.d.ts +90 -0
  96. package/dist/memory/purge.d.ts.map +1 -0
  97. package/dist/memory/purge.js +138 -0
  98. package/dist/memory/purge.js.map +1 -0
  99. package/dist/memory/recall-agent.d.ts +84 -0
  100. package/dist/memory/recall-agent.d.ts.map +1 -0
  101. package/dist/memory/recall-agent.js +199 -0
  102. package/dist/memory/recall-agent.js.map +1 -0
  103. package/dist/memory/recall-index.d.ts +87 -0
  104. package/dist/memory/recall-index.d.ts.map +1 -0
  105. package/dist/memory/recall-index.js +222 -0
  106. package/dist/memory/recall-index.js.map +1 -0
  107. package/dist/memory/recall-packet.d.ts +121 -0
  108. package/dist/memory/recall-packet.d.ts.map +1 -0
  109. package/dist/memory/recall-packet.js +156 -0
  110. package/dist/memory/recall-packet.js.map +1 -0
  111. package/dist/memory/scheduler-api.d.ts +99 -0
  112. package/dist/memory/scheduler-api.d.ts.map +1 -0
  113. package/dist/memory/scheduler-api.js +93 -0
  114. package/dist/memory/scheduler-api.js.map +1 -0
  115. package/dist/memory/scheduler.d.ts +55 -0
  116. package/dist/memory/scheduler.d.ts.map +1 -0
  117. package/dist/memory/scheduler.js +91 -0
  118. package/dist/memory/scheduler.js.map +1 -0
  119. package/dist/memory/store.d.ts +107 -0
  120. package/dist/memory/store.d.ts.map +1 -0
  121. package/dist/memory/store.js +208 -0
  122. package/dist/memory/store.js.map +1 -0
  123. package/dist/memory/suite-memory.d.ts +208 -0
  124. package/dist/memory/suite-memory.d.ts.map +1 -0
  125. package/dist/memory/suite-memory.js +288 -0
  126. package/dist/memory/suite-memory.js.map +1 -0
  127. package/dist/memory/transfer.d.ts +142 -0
  128. package/dist/memory/transfer.d.ts.map +1 -0
  129. package/dist/memory/transfer.js +210 -0
  130. package/dist/memory/transfer.js.map +1 -0
  131. package/dist/memory/vector-index.d.ts +39 -0
  132. package/dist/memory/vector-index.d.ts.map +1 -0
  133. package/dist/memory/vector-index.js +136 -0
  134. package/dist/memory/vector-index.js.map +1 -0
  135. package/dist/memory/write-budget.d.ts +33 -0
  136. package/dist/memory/write-budget.d.ts.map +1 -0
  137. package/dist/memory/write-budget.js +45 -0
  138. package/dist/memory/write-budget.js.map +1 -0
  139. package/dist/testing/memory-testkit.d.ts +149 -0
  140. package/dist/testing/memory-testkit.d.ts.map +1 -0
  141. package/dist/testing/memory-testkit.js +438 -0
  142. package/dist/testing/memory-testkit.js.map +1 -0
  143. package/dist/utils/sync-sleep.d.ts +2 -0
  144. package/dist/utils/sync-sleep.d.ts.map +1 -0
  145. package/dist/utils/sync-sleep.js +11 -0
  146. package/dist/utils/sync-sleep.js.map +1 -0
  147. package/package.json +56 -0
  148. package/plugin.json +10 -0
@@ -0,0 +1,2565 @@
1
+ /**
2
+ * First-party builtin memory capability (统一修复轮 A, 方案系统设计 §6.1 + §11,
3
+ * 方案实施计划 §11 A 路) — the model-facing write/recall/list/forget surface
4
+ * over the M5 Memory Foundation.
5
+ *
6
+ * Every operation is suite-scoped by construction: the suiteId comes from the
7
+ * session's suite binding entry (SUITE_SESSION_ENTRY_TYPE, written by the sdk
8
+ * at session creation) and is NEVER a tool parameter, so the model cannot
9
+ * widen or switch its memory domain (防越权). Sessions without a binding fall
10
+ * back to the "legacy" domain with a diagnostic. Writes go through the
11
+ * scheduler facade (observation → candidate → committed) plus a durable JSONL
12
+ * ledger append (src/memory/ledger.ts, the store's persistent replica); recall
13
+ * runs through the memory-recall-agent with the suiteId passthrough filter and
14
+ * the egress gate; forget goes through the canonical purge gate
15
+ * (user-immediate) and journal, physically rewriting the ledger (forget 后真
16
+ * 物理删除).
17
+ *
18
+ * Domain resolution is LAZY, per tool call (统一修复轮 B1 复审修复): the sdk
19
+ * appends the suite binding entry only after `new AgentSession` returns (the
20
+ * constructor's synchronous runtime build already runs this factory), so a
21
+ * factory-time resolution always saw an empty branch and every new session
22
+ * fell back to the legacy domain. Instead, each tool handler resolves the
23
+ * domain at call time — the binding entry is on the branch by then (creation
24
+ * appends it; resumed sessions carry it in their history) — and the per-domain
25
+ * ledger is created, loaded, and replayed into the store on the first call in
26
+ * that domain (Map<domain, ledger> cache, no memoization of the domain
27
+ * itself). The store stays one per-capability-instance singleton across
28
+ * domains: the atom `suiteId` field carries the read boundary; the ledger is
29
+ * only the per-domain durable replica.
30
+ *
31
+ * Concurrency envelope (统一修复轮 H1, #7 深度修复; WP-F 关洞修复): ledger
32
+ * appends AND the forget path's full-file rewrite serialize competing
33
+ * processes through the ledger's `.lock` sibling file (exclusive create +
34
+ * poll, 5 s timeout, stale-lock stealing — src/memory/ledger.ts), and both
35
+ * write paths run open → write → fsync → close, so a hard crash cannot lose
36
+ * a write that returned to its caller. A lock timeout or IO/fsync failure
37
+ * throws out of the tool handler, so the model sees the error instead of a
38
+ * silently lost write. Remaining boundaries: the lock is atomic only on
39
+ * local filesystems; a rewrite publishes the calling process's in-memory
40
+ * view without merging rows another process appended after its last load
41
+ * (the forget caller owns that reconciliation); fsync does not cover
42
+ * directory entries, so a crash before rewrite's rename leaves the previous
43
+ * file intact; an append whose fsync failed stays unacknowledged though it
44
+ * may already be readable — the loader's duplicate tolerance covers an
45
+ * unacknowledged retry.
46
+ *
47
+ * Global mode gate (2.4d6 S1, 记忆系统语义化重设计 §3): `memory.mode` is
48
+ * `"off"` (default) | `"light"` | `"full"`; off means the whole memory family
49
+ * is NOT registered (resolveBuiltinCapabilities skips the plugin; the factory
50
+ * itself returns zero registrations for direct callers). light/full run an
51
+ * enabling state machine (local checks → model load/download → smoke →
52
+ * ready; failure → failed with a four-option action list and the effective
53
+ * mode falls back to off for this instance).
54
+ *
55
+ * Semantic recall (2.4d6 S2, 设计 §4): memory_recall has exactly one
56
+ * retrieval path — embed → KNN depth50 + FTS depth50 → RRF k=60 top20 →
57
+ * cross-encoder rerank → store lookup (owner/suite/scope/expiry). The legacy
58
+ * tag-wordform bridge (query-token → tag-facet anchors, recency fallback
59
+ * page) is retired as a retrieval semantic; tags still ride the FTS body and
60
+ * the memory_list management face. Degradation ladder (never thrown at the
61
+ * model): rerank failure → RRF order; vector failure → FTS-only with
62
+ * recency ordering; FTS/index failure → KNN-only; both channels down →
63
+ * recency enumeration over the domain; total failure → degraded packet.
64
+ * `memoryIdEquals`-style primary-key lookup survives as the exact-id fast
65
+ * path (reference semantics, not retrieval).
66
+ *
67
+ * Semantic write merge (2.4d6 S3, 设计 §5 + 实施计划 S3 标定定版): in mode
68
+ * "full" every memory_write probes the projection BEFORE the canonical commit
69
+ * (embed + same-owner KNN top-5) because atoms are immutable — the outcome's
70
+ * relations must ride the atom's one commit. Merge condition (calibrated on
71
+ * bge-m3): cos ≥ 0.72, or 0.55 ≤ cos < 0.72 with a case-insensitive tag
72
+ * overlap. A merge commits the NEW atom with a one-way `supersedes` relation
73
+ * to the neighbor (the old entry keeps its memoryId and stays addressable via
74
+ * primary-key lookup) while the projection upserts the new row and removes the
75
+ * superseded one; startup reconciliation never re-adds superseded rows.
76
+ * 0.55 ≤ cos < 0.72 without a tag overlap is the conflict band: both entries
77
+ * stay, and the new atom carries one-way `conflict` relations. Below that (or
78
+ * no neighbor): a plain new entry. bge-small-zh similarity distributions
79
+ * overlap (标定: 余弦不可用作去重信号) → light mode never probes and stays
80
+ * byte-identical to the pre-S3 behavior. Recall filters superseded entries
81
+ * after the store lookup (only while their successor is among the candidates,
82
+ * so forgetting the successor revives the old entry). The Foundation
83
+ * (owner, observationId) idempotency is untouched: replays never consume a
84
+ * probe outcome.
85
+ */
86
+ import { existsSync, readdirSync, statSync } from "node:fs";
87
+ import { statfs } from "node:fs/promises";
88
+ import { createRequire } from "node:module";
89
+ import { dirname, join } from "node:path";
90
+ import { EXPERIMENTAL_PUBLIC_API_VERSION, MEMORY_RECALL_TOOL_NAME, } from "@agent-forge/plugin-sdk";
91
+ import { Type } from "typebox";
92
+ import { purgeZeroByteCacheArtifacts } from "./memory/model-cache-hygiene.js";
93
+ // 记忆公共契约的权威定义处已是 plugin-sdk(D-075 S4-4 第一批迁入);此处 re-export
94
+ // 维持宿主历史导出面(宿主第二批收口后由宿主 re-export SDK)。
95
+ export { MEMORY_RECALL_TOOL_NAME, } from "@agent-forge/plugin-sdk";
96
+ import { loadAssistantPreferenceCard } from "./memory/assistant-card.js";
97
+ import { createMemoryCandidateMachine } from "./memory/candidates.js";
98
+ import { createMemoryEgressPolicy } from "./memory/egress-policy.js";
99
+ import { buildMemoryAtomV1, } from "./memory/foundation.js";
100
+ import { createDurableMemoryLedger } from "./memory/ledger.js";
101
+ import { createMemoryLifecycleManager } from "./memory/lifecycle.js";
102
+ import { createMemoryNetwork, createRelationDiffusionAdapter, } from "./memory/memory-network.js";
103
+ import { createPreferenceDisambiguator } from "./memory/preference-disambiguator.js";
104
+ import { promotePreferenceToUserDefault } from "./memory/preference-promotion.js";
105
+ import { createPreferenceResolver, } from "./memory/preference-resolver.js";
106
+ import { createMemoryPurgeGate, createMemoryReplicaRegistry } from "./memory/purge.js";
107
+ import { createMemoryPurgeJournal } from "./memory/purge-journal.js";
108
+ import { createMemoryRecallAgent } from "./memory/recall-agent.js";
109
+ import { createMemoryRecallIndex } from "./memory/recall-index.js";
110
+ import { createMemoryScheduler } from "./memory/scheduler.js";
111
+ import { createMemorySchedulerApi } from "./memory/scheduler-api.js";
112
+ import { createFirstPartyRetentionRegistry, createMemoryStore } from "./memory/store.js";
113
+ import { forgetDomainMemory, listDomainMemories, suiteMemoryDomainName, } from "./memory/suite-memory.js";
114
+ import { MEMORY_CONTRACT_VERSION_V2 } from "./memory/transfer.js";
115
+ // ---------------------------------------------------------------------------
116
+ // Host lifecycle catalog references (D-075 S4-4 第一批迁包改写): the embedded
117
+ // capability imported the event definitions from the host's lifecycle catalog
118
+ // and re-defined them idempotently. The plugin now subscribes through the
119
+ // plain `{id, version}` reference form (observability.ts 同构先例): the host's
120
+ // bootstrap lifecycle plugin owns the definitions, and a host that has not
121
+ // defined the event rejects the registration structurally (plugin-load
122
+ // failure under D-028, never a session failure). The data shapes below are
123
+ // structural narrowings of the catalog payloads — only the fields the bash
124
+ // error-lesson capture reads.
125
+ // ---------------------------------------------------------------------------
126
+ /** `tool.execution.start@1` (host lifecycle catalog). */
127
+ const TOOL_EXECUTION_START_EVENT_ID = "tool.execution.start";
128
+ const TOOL_EXECUTION_START_EVENT_VERSION = 1;
129
+ /** `tool.result@1` (host lifecycle catalog). */
130
+ const TOOL_RESULT_EVENT_ID = "tool.result";
131
+ const TOOL_RESULT_EVENT_VERSION = 1;
132
+ export const capabilityManifest = {
133
+ id: "agent-forge.builtin.memory",
134
+ version: "0.1.0",
135
+ apiVersion: EXPERIMENTAL_PUBLIC_API_VERSION,
136
+ requiredCapabilities: ["session", "events", "tools"],
137
+ provides: [{ id: "memory.scheduler", version: 1, kind: "service" }],
138
+ };
139
+ /**
140
+ * Marker text of the auto-recall injection message (B1 记忆自动注入, D-071 增补
141
+ * 裁决自 sdk 迁入; pinned by test/memory-sdk-e2e.test.ts).
142
+ */
143
+ export const MEMORY_AUTO_RECALL_MARKER = "<auto_recalled_memory>";
144
+ /**
145
+ * Upper bound on facts pulled into one auto-recall injection (B1 记忆自动注入).
146
+ * The injection rides every request of the turn, so it stays a compact hint —
147
+ * deep recall is what memory_recall is for.
148
+ */
149
+ const MEMORY_AUTO_RECALL_LIMIT = 3;
150
+ /**
151
+ * Auto-recall rerank floor (错误教训与召回质量护栏设计 §2.1): reranked
152
+ * candidates scoring below it are dropped from the auto-recall injection
153
+ * (`score < minScore`). bge-reranker outputs uncalibrated logits that are only
154
+ * order-preserving (见 embedding-reranker.ts 头注), so 0 ("positive logit =
155
+ * relevant direction") is the conservative initial gate — tunable with real
156
+ * model eval evidence (只改常量 + eval 证据).
157
+ */
158
+ const MEMORY_AUTO_RECALL_MIN_RERANK_SCORE = 0;
159
+ /**
160
+ * Tail line of every auto-recall injection block (错误教训与召回质量护栏设计
161
+ * §2.1): tells the model the block was recalled rather than written by the
162
+ * user and may be outdated. Pinned verbatim by test/memory-error-lessons.test.ts.
163
+ */
164
+ export const MEMORY_AUTO_RECALL_DISCLAIMER = "(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.)";
165
+ /**
166
+ * Upper bound on the error message embedded in one memory auto-recall log
167
+ * line: provider/store errors can carry arbitrary payload text, and the log
168
+ * channel is for locating the failure, not for echoing it.
169
+ */
170
+ const MEMORY_AUTO_RECALL_ERROR_MESSAGE_LIMIT = 200;
171
+ // ---------------------------------------------------------------------------
172
+ // bash 失败教训自动沉淀 (错误教训与召回质量护栏设计 §2.2): 确定性事实模板,
173
+ // 无模型解读; 风暴由签名去重 + 每实例上限兜住; 7 天保留档到期失去召回资格。
174
+ // ---------------------------------------------------------------------------
175
+ /** 每实例教训写入上限 (§2.2 风暴闸门): 首个超限事件 warn 一次, 之后静默跳过。 */
176
+ const MEMORY_LESSON_MAX_PER_SESSION = 10;
177
+ /** 教训 excerpt 字符上限 (错误文本尾部, 按码点切分不劈代理对)。 */
178
+ const MEMORY_LESSON_ERROR_EXCERPT_MAX_CHARS = 300;
179
+ /** toolCallId→command 配对映射容量 (仅 bash, FIFO)。 */
180
+ const MEMORY_LESSON_TOOLCALL_MAP_CAPACITY = 32;
181
+ /** 教训开关环境变量 (context 显式值 > env > 默认 on; 非法值取默认并 warn)。 */
182
+ const MEMORY_LESSONS_ENV = "AGENT_FORGE_MEMORY_LESSONS";
183
+ /** 教训 tags (§2.2): 随 FTS body 可检索。 */
184
+ const MEMORY_LESSON_TAGS = ["auto-lesson", "bash"];
185
+ /** 教训提交的保留档 (first-party retention registry 既有 "short" = 7 天)。 */
186
+ const MEMORY_LESSON_RETENTION_MODE_ID = "short";
187
+ /**
188
+ * bash 失败退出码的既有后缀 (core/tools/bash.ts: 输出文本 + 该状态行); 解析
189
+ * 不到 (timeout/abort 等) 记 unknown, 仍沉淀。
190
+ */
191
+ const MEMORY_LESSON_EXIT_CODE_PATTERN = /Command exited with code (\d+)\s*$/;
192
+ /**
193
+ * Memory tools guide(记忆 runtime 注入面,原 system-prompt.ts MEMORY_TOOL_GUIDE,
194
+ * D-071 迁移):the model-facing hint that the persistent memory tools exist.
195
+ * It rides memory_recall's promptGuidelines so the text lives with the plugin
196
+ * that owns the tools (宪法 §3) and renders whenever the tool face is active.
197
+ */
198
+ export const MEMORY_TOOLS_GUIDE = "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.";
199
+ /**
200
+ * Session protocol key of the suite binding entry (方案设计 §10.1). The value
201
+ * mirrors the authoritative `SUITE_SESSION_ENTRY_TYPE` in
202
+ * src/profiles/suite-loader.ts — pinned by a deterministic test in
203
+ * test/capabilities-builtin.test.ts. Declared locally so the capability module
204
+ * graph stays free of the assembler's bundled-loader dependency chain.
205
+ */
206
+ const SUITE_SESSION_ENTRY_TYPE = "suite";
207
+ /**
208
+ * Owner (isolation principal) of every atom this capability writes. The
209
+ * Foundation owner is the store/facade isolation identity; this product ships
210
+ * one local user per agentDir, so one stable owner id partitions the ledger
211
+ * directory `<agentDir>/memory/<owner>/ledger-<suiteId|"legacy">.jsonl`.
212
+ */
213
+ export const BUILTIN_MEMORY_OWNER = "local-user";
214
+ /** Contract version written by this capability (@2 atoms carry suiteId). */
215
+ const MEMORY_CONTRACT_VERSION = MEMORY_CONTRACT_VERSION_V2;
216
+ const TAG_FACET_NAMESPACE = "builtin.memory";
217
+ const EVENT_TYPE = "memory.changed";
218
+ const MAX_TAGS = 8;
219
+ const DEFAULT_RECALL_LIMIT = 5;
220
+ const DEFAULT_LIST_LIMIT = 20;
221
+ /** Bounded supplementary relation references per recall (记忆网络接线). */
222
+ const MAX_RELATED_REFS = 20;
223
+ // ---------------------------------------------------------------------------
224
+ // 语义写入合并 (2.4d6 S3, 实施计划 S3 阈值标定定版)。full 档独有: light 档
225
+ // (bge-small-zh) 重复变体/同主题/无关三组余弦分布完全重叠, 余弦不可用作去重
226
+ // 信号 → 不探测不合并 (行为与 S3 之前逐字节一致); off 档整体不注册。
227
+ // ---------------------------------------------------------------------------
228
+ /** 合并下限 (bge-m3 实测: 重复变体 0.664–0.833, 同主题 ≤0.652 → 0.72 零误合并)。 */
229
+ const SEMANTIC_MERGE_T_HIGH = 0.72;
230
+ /** 冲突带下限 (bge-m3 实测: 无关语句最高 0.450 → 0.55 以下零越界)。 */
231
+ const SEMANTIC_MERGE_T_LOW = 0.55;
232
+ /** 写入探测的同 owner KNN 近邻数 (标定定版: top-5)。 */
233
+ const SEMANTIC_MERGE_KNN_LIMIT = 5;
234
+ /** supersedes/conflict 关系的 capability 命名空间 (与 tag facet 同域)。 */
235
+ const SEMANTIC_RELATION_NAMESPACE = "builtin.memory";
236
+ // ---------------------------------------------------------------------------
237
+ // 语义召回管线 (2.4d6 S2, 记忆系统语义化重设计 §4) 与全局档位 (S1, §3)。
238
+ // 召回唯一路径: embed → KNN+FTS → RRF → 精排 → store 回查; tag 词面 bridge
239
+ // 已退役 (tags 仅入 FTS body 与管理面)。off 档 = 整个 capability 不注册。
240
+ // ---------------------------------------------------------------------------
241
+ /** memory_write 内容长度契约 (POC 报告 §7): reranker 512-token 上下文足够。 */
242
+ const MEMORY_CONTENT_MAX_LENGTH = 700;
243
+ /** FTS / KNN 通道各自取回深度 (定版管线: depth 50)。 */
244
+ const HYBRID_CHANNEL_DEPTH = 50;
245
+ /** RRF 常数, 与 vector-index 组件 queryFts 打分口径一致。 */
246
+ const HYBRID_RRF_K = 60;
247
+ /** 融合候选池上限 (定版管线: top20 → 精排 → top limit)。 */
248
+ const HYBRID_FUSION_POOL = 20;
249
+ /** 单次启动对账 embed+upsert 上限, 超出留待下次启动继续。 */
250
+ const RECONCILE_MAX_UPSERTS = 2000;
251
+ /** 开启前磁盘余量下限 (设计 §3.2 checking): light ≥ 500MB, full ≥ 2GB。 */
252
+ const DISK_HEADROOM_MIN_BYTES = {
253
+ light: 500 * 1024 * 1024,
254
+ full: 2 * 1024 * 1024 * 1024,
255
+ };
256
+ /**
257
+ * 模型文件大小下限 (防截断/空文件的 sanity 下限, 非精确体积): 高档 embedding
258
+ * (bge-m3 q8) 数百 MB → 下限 256MB, 精排器 (bge-reranker-base q8) ~280MB →
259
+ * 下限 64MB, 低档 (bge-small-zh q8) ~24MB → 下限 8MB。真实校验由加载冒烟承担。
260
+ */
261
+ const MODEL_MIN_WEIGHT_BYTES = {
262
+ light: 8 * 1024 * 1024,
263
+ full: 256 * 1024 * 1024,
264
+ };
265
+ const RERANKER_MIN_WEIGHT_BYTES = 64 * 1024 * 1024;
266
+ /** 自定义镜像源环境变量 (失败行动清单第一项指向它)。 */
267
+ const HF_MIRRORS_ENV = "AGENT_FORGE_HF_MIRRORS";
268
+ /** enabling 加载/下载期间的周期性进度日志间隔。 */
269
+ const ENABLING_PROGRESS_LOG_INTERVAL_MS = 15_000;
270
+ const VECTOR_PRESET_LIGHT = {
271
+ embeddingModelId: "Xenova/bge-small-zh-v1.5",
272
+ dimensions: 512,
273
+ queryPrefix: "为这个句子生成表示以用于检索相关文章:",
274
+ };
275
+ const VECTOR_PRESET_FULL = {
276
+ embeddingModelId: "Xenova/bge-m3",
277
+ dimensions: 1024,
278
+ queryPrefix: "",
279
+ };
280
+ /** 两档共用精排模型 (POC 报告 §8: 官方 Xenova 转换版唯一实测可用)。 */
281
+ const VECTOR_RERANKER_MODEL_ID = "Xenova/bge-reranker-base";
282
+ const VECTOR_DB_DIRNAME = "vector";
283
+ const VECTOR_MODEL_CACHE_DIRNAME = "model-cache";
284
+ /** 镜像序列 (实施计划 §7): 直连 huggingface.co 超时时回落 hf-mirror.com。 */
285
+ const VECTOR_REMOTE_HOSTS = ["https://huggingface.co", "https://hf-mirror.com"];
286
+ const isMemoryModeV1 = (value) => value === "off" || value === "light" || value === "full";
287
+ /**
288
+ * memory.mode 解析 (2.4d6 S1)。优先级: context 显式值 → 环境变量
289
+ * `AGENT_FORGE_MEMORY_MODE` → settings.json `memory.mode` (settingsMode 参, 持
290
+ * 久配置面) → 旧别名 `AGENT_FORGE_MEMORY_VECTOR` (兼容期) → 默认 off。env 是
291
+ * 临时覆盖、settings 是持久配置 (env > 文件, 沿用本仓 suite 解析先例)。任何
292
+ * 非法配置值 fail-closed 为 off 并在结果中携带原值供调用方告警一次, 绝不静默
293
+ * 改档或意外下载。
294
+ */
295
+ export function resolveMemoryMode(explicitMode, env = process.env, settingsMode) {
296
+ if (explicitMode !== undefined) {
297
+ if (isMemoryModeV1(explicitMode))
298
+ return { mode: explicitMode };
299
+ return { mode: "off", invalidEnvValue: explicitMode };
300
+ }
301
+ const configured = env.AGENT_FORGE_MEMORY_MODE?.trim().toLowerCase();
302
+ if (configured !== undefined && configured !== "") {
303
+ if (isMemoryModeV1(configured))
304
+ return { mode: configured };
305
+ return { mode: "off", invalidEnvValue: configured };
306
+ }
307
+ // 持久配置面: settings.json memory.mode。设置但非法同样 fail-closed, 不再
308
+ // 回落旧别名 (与非法 env 的先例一致)。
309
+ const fromSettings = settingsMode?.trim().toLowerCase();
310
+ if (fromSettings !== undefined && fromSettings !== "") {
311
+ if (isMemoryModeV1(fromSettings))
312
+ return { mode: fromSettings };
313
+ return { mode: "off", invalidSettingsValue: fromSettings };
314
+ }
315
+ // 兼容期别名: AGENT_FORGE_MEMORY_VECTOR (混合检索工程化的旧开关), 语义并入
316
+ // AGENT_FORGE_MEMORY_MODE, 仅当新变量与 settings 均未定档时生效; 一个发布
317
+ // 周期后删除。
318
+ const legacy = env.AGENT_FORGE_MEMORY_VECTOR?.trim().toLowerCase();
319
+ if (legacy !== undefined && legacy !== "") {
320
+ if (isMemoryModeV1(legacy))
321
+ return { mode: legacy, legacyEnvUsed: true };
322
+ return { mode: "off", invalidEnvValue: legacy, legacyEnvUsed: true };
323
+ }
324
+ return { mode: "off" };
325
+ }
326
+ const isMemoryLessonsSettingV1 = (value) => value === "on" || value === "off";
327
+ /**
328
+ * context.memory.lessons 解析 (§2.2, resolveMemoryMode 同构): 显式值 → 环境变量
329
+ * `AGENT_FORGE_MEMORY_LESSONS` → 默认 on。任何非法配置值回默认并在结果中携带
330
+ * 原值供调用方告警一次, 绝不静默改开关。
331
+ */
332
+ export function resolveMemoryLessons(explicitLessons, env = process.env) {
333
+ if (explicitLessons !== undefined) {
334
+ if (isMemoryLessonsSettingV1(explicitLessons))
335
+ return { lessons: explicitLessons };
336
+ return { lessons: "on", invalidEnvValue: explicitLessons };
337
+ }
338
+ const configured = env[MEMORY_LESSONS_ENV]?.trim().toLowerCase();
339
+ if (configured !== undefined && configured !== "") {
340
+ if (isMemoryLessonsSettingV1(configured))
341
+ return { lessons: configured };
342
+ return { lessons: "on", invalidEnvValue: configured };
343
+ }
344
+ return { lessons: "on" };
345
+ }
346
+ const isVectorComponentsOverride = (override) => "embedProvider" in override && "vectorIndex" in override;
347
+ function assertNonEmptyString(value, label) {
348
+ if (typeof value !== "string" || value.trim().length === 0)
349
+ throw new Error(`${label} must be a non-empty string`);
350
+ }
351
+ function isPlainRecord(value) {
352
+ return typeof value === "object" && value !== null && !Array.isArray(value);
353
+ }
354
+ /**
355
+ * 双约定工具输入读取(对齐 subagent-delegate 的 objectInput):capability invoke
356
+ * 面单参约定下第一参即输入(第二参是宿主注入的 toolExecutionContext),legacy 5 参
357
+ * 约定(toolCallId 字符串打头)下输入在第二参。生产装配恒向 runtime invoke 面注入
358
+ * toolExecutionContext 作为 execute 第二参(公共契约),因此"第二参非 undefined 即
359
+ * 输入"的判定会把 context 误当输入(content/query 等读成 undefined)。改按形状判定:
360
+ * 第一参是普通对象即取第一参,否则看第二参;两者皆非对象时返回空记录,交由各工具
361
+ * 的字段断言报错。
362
+ */
363
+ function memoryToolInput(first, second) {
364
+ const primary = isPlainRecord(first) ? first : second;
365
+ return isPlainRecord(primary) ? primary : {};
366
+ }
367
+ /** FNV-1a 32-bit — deterministic identity from the given value (code-memory 同构). */
368
+ function identityHash(value) {
369
+ let hash = 0x811c9dc5;
370
+ for (let index = 0; index < value.length; index += 1) {
371
+ hash ^= value.charCodeAt(index);
372
+ hash = Math.imul(hash, 0x01000193);
373
+ }
374
+ return (hash >>> 0).toString(36);
375
+ }
376
+ function isPlainObject(value) {
377
+ return value !== null && typeof value === "object" && !Array.isArray(value);
378
+ }
379
+ /**
380
+ * Text of the branch's LAST user message — the auto-recall query and per-turn
381
+ * cache key (B1 记忆自动注入). Read from the persisted branch, not the
382
+ * request-time snapshot: request-time injections (the sdk's background
383
+ * delegation digest) ride the transformContext input as user-role blocks and
384
+ * would otherwise hijack the query, while the branch never sees them.
385
+ */
386
+ function lastBranchUserText(api) {
387
+ const entries = api.session?.getBranchEntries() ?? [];
388
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
389
+ const entry = entries[index];
390
+ if (entry.type !== "message" || entry.message?.role !== "user")
391
+ continue;
392
+ const content = entry.message.content;
393
+ if (typeof content === "string")
394
+ return content;
395
+ if (!Array.isArray(content))
396
+ continue;
397
+ const parts = [];
398
+ for (const item of content) {
399
+ if (isPlainObject(item) && item.type === "text" && typeof item.text === "string")
400
+ parts.push(item.text);
401
+ }
402
+ return parts.join("\n");
403
+ }
404
+ return undefined;
405
+ }
406
+ /**
407
+ * Inserts the recall injection right after the last user message of the
408
+ * request snapshot (B1 记忆自动注入): the recalled context is turn-scoped, so
409
+ * it belongs at the head of the current turn, ahead of any assistant/
410
+ * toolResult round already in flight. Purely a request-time view — the
411
+ * persisted session history stays untouched.
412
+ */
413
+ function injectAfterLastUserMessage(messages, injection) {
414
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
415
+ if (messages[index]?.role === "user") {
416
+ return [...messages.slice(0, index + 1), injection, ...messages.slice(index + 1)];
417
+ }
418
+ }
419
+ return messages;
420
+ }
421
+ /**
422
+ * Tail of `text` whose UTF-16 length never exceeds `limit`, cut on code-point
423
+ * boundaries so a surrogate pair never splits. The excerpt budget is measured
424
+ * in UTF-16 units (`String.length`, the same measure as the 700-char content
425
+ * contract), so counting code points here would let astral-plane tails double
426
+ * the rendered excerpt and break the cap.
427
+ */
428
+ function tailWithinUtf16Budget(text, limit) {
429
+ if (limit <= 0)
430
+ return "";
431
+ if (text.length <= limit)
432
+ return text;
433
+ const codePoints = Array.from(text);
434
+ let units = 0;
435
+ let start = codePoints.length;
436
+ while (start > 0 && units + codePoints[start - 1].length <= limit) {
437
+ units += codePoints[start - 1].length;
438
+ start -= 1;
439
+ }
440
+ return codePoints.slice(start).join("");
441
+ }
442
+ /** bash 失败退出码解析 (core/tools/bash.ts 的既有后缀); 解析不到 (timeout/abort) = "unknown"。 */
443
+ function lessonExitCodeOf(errorText) {
444
+ return MEMORY_LESSON_EXIT_CODE_PATTERN.exec(errorText)?.[1] ?? "unknown";
445
+ }
446
+ /** Concatenated text of a toolResult message's text blocks (images skipped). */
447
+ function toolResultTextOf(content) {
448
+ const parts = [];
449
+ for (const block of content) {
450
+ if (block.type === "text")
451
+ parts.push(block.text);
452
+ }
453
+ return parts.join("\n");
454
+ }
455
+ /**
456
+ * 教训内容 (§2.2 确定性事实模板, 无模型解读): `Command failed: \`<command>\`
457
+ * exited with code <N>. Output tail: <excerpt>`。excerpt 取错误文本尾部, 预算
458
+ * 按 UTF-16 单元计 ({@link MEMORY_LESSON_ERROR_EXCERPT_MAX_CHARS} 与 700 上限
459
+ * 同一口径); 超长只压缩 excerpt (excerpt 压到 0 仍超限的极端长命令再压 command
460
+ * 尾部, 保持模板可解析)。
461
+ */
462
+ function buildLessonContent(command, exitCode, errorText) {
463
+ const render = (commandPart, excerpt) => `Command failed: \`${commandPart}\` exited with code ${exitCode}. Output tail: ${excerpt}`;
464
+ const excerptLimit = Math.min(MEMORY_LESSON_ERROR_EXCERPT_MAX_CHARS, MEMORY_CONTENT_MAX_LENGTH - render(command, "").length);
465
+ if (excerptLimit > 0)
466
+ return render(command, tailWithinUtf16Budget(errorText, excerptLimit));
467
+ const commandBudget = MEMORY_CONTENT_MAX_LENGTH - render("", "").length;
468
+ return render(command.slice(0, Math.max(0, commandBudget)), "");
469
+ }
470
+ /**
471
+ * Suite binding entry ids already warned about (统一修复轮终审 minor 3):
472
+ * resolveSessionDomain runs on EVERY memory tool call, so without this
473
+ * module-level set one malformed binding entry would repeat the warning on
474
+ * each call and flood the log.
475
+ */
476
+ const malformedBindingWarned = new Set();
477
+ /**
478
+ * Resolves the session's suite binding domain from the branch entries: the
479
+ * LAST suite entry wins (the sdk appends one at session creation). A binding
480
+ * with a malformed payload counts as unbound — the legacy fallback is
481
+ * fail-safe (its atoms are invisible to every real suite) and a diagnostic is
482
+ * logged when the logger capability is available. Called per tool call —
483
+ * never captured at factory time (统一修复轮 B1: the binding entry is appended
484
+ * after the session constructor ran this factory).
485
+ */
486
+ function resolveSessionDomain(api) {
487
+ const entries = api.session?.getBranchEntries() ?? [];
488
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
489
+ const entry = entries[index];
490
+ if (entry.type !== "custom" || entry.customType !== SUITE_SESSION_ENTRY_TYPE)
491
+ continue;
492
+ const data = entry.data;
493
+ const suiteId = isPlainObject(data) ? data.suiteId : undefined;
494
+ if (typeof suiteId === "string" && suiteId.trim() !== "") {
495
+ return { kind: "suite", suiteId };
496
+ }
497
+ if (!malformedBindingWarned.has(entry.id)) {
498
+ malformedBindingWarned.add(entry.id);
499
+ api.logger?.warn("suite binding entry has a malformed payload; falling back to the legacy memory domain", {
500
+ entryId: entry.id,
501
+ });
502
+ }
503
+ return { kind: "legacy" };
504
+ }
505
+ return { kind: "legacy" };
506
+ }
507
+ /** Recency-first payload projection: unwraps the `{ statement }` payload convention. */
508
+ function statementOf(payload) {
509
+ if (isPlainObject(payload) && typeof payload.statement === "string")
510
+ return payload.statement;
511
+ return JSON.stringify(payload);
512
+ }
513
+ /** The egress gate re-derives statements as payload JSON; unwrap for the model face. */
514
+ function unwrapEgressStatement(statement) {
515
+ try {
516
+ const parsed = JSON.parse(statement);
517
+ if (isPlainObject(parsed) && typeof parsed.statement === "string")
518
+ return parsed.statement;
519
+ }
520
+ catch {
521
+ // Not payload JSON — surface it verbatim.
522
+ }
523
+ return statement;
524
+ }
525
+ /**
526
+ * 三通道 RRF 融合 (定版管线): `score(id) += 1/(k + rank + 1)` 对各通道排名累加,
527
+ * 取 top {@link HYBRID_FUSION_POOL}。分数并列时以 memoryId 升序打破, 保证确定性。
528
+ */
529
+ function rrfFuseRankings(rankings) {
530
+ const scores = new Map();
531
+ for (const ranking of rankings) {
532
+ for (const [rank, memoryId] of ranking.entries()) {
533
+ scores.set(memoryId, (scores.get(memoryId) ?? 0) + 1 / (HYBRID_RRF_K + rank + 1));
534
+ }
535
+ }
536
+ return [...scores.entries()]
537
+ .sort((left, right) => right[1] - left[1] || (left[0] < right[0] ? -1 : 1))
538
+ .map(([memoryId]) => memoryId)
539
+ .slice(0, HYBRID_FUSION_POOL);
540
+ }
541
+ /** 嵌入向量已 L2 归一化 (provider 契约), 点积即余弦。 */
542
+ function cosineSimilarity(left, right) {
543
+ let dot = 0;
544
+ for (const [axis, value] of left.entries())
545
+ dot += value * (right[axis] ?? 0);
546
+ return dot;
547
+ }
548
+ /** tags 重叠判定 (至少一个共同 tag, trim 后大小写不敏感)。 */
549
+ function tagsOverlapIgnoreCase(left, right) {
550
+ if (left.length === 0 || right.length === 0)
551
+ return false;
552
+ const rightSet = new Set(right.map((tag) => tag.trim().toLowerCase()));
553
+ return left.some((tag) => rightSet.has(tag.trim().toLowerCase()));
554
+ }
555
+ /**
556
+ * 语义合并判定 (实施计划 S3 标定定版, 三分支):
557
+ * - 合并: cos ≥ 0.72, 或 0.55 ≤ cos < 0.72 且新 tags 与既有 tags 有重叠
558
+ * (复合条件救回跨语言改写 — 余弦偏低但 tags 同源; 取首个满足条件的近邻);
559
+ * - 冲突带: 0.55 ≤ cos < 0.72 且无 tag 重叠 → 双存 + conflict 关系
560
+ * (单向声明在新 atom 上; 正确性优先于存储省略, 宁可双存不错杀);
561
+ * - 低相似/无近邻: 普通新条目。
562
+ * 近邻按 KNN 序传入。
563
+ */
564
+ function decideSemanticMergePlan(input) {
565
+ for (const neighbor of input.neighbors) {
566
+ const merged = neighbor.cosine >= SEMANTIC_MERGE_T_HIGH ||
567
+ (neighbor.cosine >= SEMANTIC_MERGE_T_LOW && tagsOverlapIgnoreCase(input.newTags, neighbor.tags));
568
+ if (merged)
569
+ return { kind: "merge", targetMemoryId: neighbor.memoryId };
570
+ }
571
+ const conflictMemoryIds = input.neighbors
572
+ .filter((neighbor) => neighbor.cosine >= SEMANTIC_MERGE_T_LOW)
573
+ .map((neighbor) => neighbor.memoryId);
574
+ return conflictMemoryIds.length > 0 ? { kind: "conflict", conflictMemoryIds } : { kind: "new" };
575
+ }
576
+ /**
577
+ * 判定结论 → 新 atom 的 relations (canonical 侧合并/冲突痕迹)。canonical
578
+ * atom 不可变且 Foundation 无"更新 payload"操作, 关系只在新 atom 上单向声明
579
+ * (可发现语义由此满足): supersedes 指向被合并的旧条目, conflict 指向冲突带
580
+ * 近邻。ids 由 (kind, target, content) 确定性派生。
581
+ */
582
+ function semanticMergeRelations(plan, content) {
583
+ const relation = (kind, targetMemoryId) => {
584
+ const relationId = `rel-${identityHash(`${kind}\u0000${targetMemoryId}\u0000${content}`)}`;
585
+ return {
586
+ relationId,
587
+ namespace: SEMANTIC_RELATION_NAMESPACE,
588
+ schemaVersion: 1,
589
+ kind,
590
+ targetMemoryId,
591
+ relationRevision: `r-${identityHash(relationId)}`,
592
+ };
593
+ };
594
+ if (plan.kind === "merge")
595
+ return [relation("supersedes", plan.targetMemoryId)];
596
+ if (plan.kind === "conflict")
597
+ return plan.conflictMemoryIds.map((target) => relation("conflict", target));
598
+ return [];
599
+ }
600
+ /** 档位预设: mode → 模型组合。off 在进入装配前已被门控拦截。 */
601
+ function vectorPresetForMode(mode) {
602
+ return mode === "light" ? VECTOR_PRESET_LIGHT : VECTOR_PRESET_FULL;
603
+ }
604
+ /**
605
+ * 自定义镜像源 (失败行动清单第一项): `AGENT_FORGE_HF_MIRRORS` 逗号分隔 host
606
+ * 列表, 排在默认镜像序列之前优先尝试。
607
+ */
608
+ function remoteHostsForEnabling() {
609
+ const custom = process.env[HF_MIRRORS_ENV]
610
+ ?.split(",")
611
+ .map((host) => host.trim())
612
+ .filter((host) => host !== "");
613
+ return custom === undefined || custom.length === 0 ? VECTOR_REMOTE_HOSTS : [...custom, ...VECTOR_REMOTE_HOSTS];
614
+ }
615
+ /**
616
+ * 本地检查 1: native .node sidecar 可解析 (设计 §3.2 checking)。布局:
617
+ * onnxruntime-node/bin 下的 napi-vN/<platform>/<arch> 原生模块 — 缺失 = 安装
618
+ * 损坏。容错: 不钉死 napi 版本号, 逐目录扫描。
619
+ *
620
+ * 解析基点双通道: jiti(安装店加载缝)注入的 `require` 走宿主 alias 表
621
+ * (宿主 node_modules 副本, 店内无依赖树); 纯 ESM(源码/工作区直载)下裸
622
+ * `require` 未定义(typeof 探测不抛 ReferenceError), 回落原生 createRequire。
623
+ */
624
+ function resolveOnnxRuntimeEntryPath() {
625
+ if (typeof require === "function") {
626
+ try {
627
+ return require.resolve("onnxruntime-node");
628
+ }
629
+ catch {
630
+ // jiti 解析失败(alias 未覆盖等)→ 继续走原生解析再报结构化错误。
631
+ }
632
+ }
633
+ return createRequire(import.meta.url).resolve("onnxruntime-node");
634
+ }
635
+ function nativeBinaryCheck(hostEntryPath) {
636
+ // 两种失败要区分 (D-075 安装店路径修复后前者成为真实可达路径):
637
+ // (a) 宿主未注入入口路径且插件位置自解析不到 — 不是安装损坏, 行动是宿主侧
638
+ // 安装重依赖后重启会话, 不是重装插件;
639
+ // (b) 路径存在但原生二进制缺失/损坏 — 保留损坏类文案。
640
+ let entryPath;
641
+ if (hostEntryPath === undefined) {
642
+ try {
643
+ entryPath = resolveOnnxRuntimeEntryPath();
644
+ }
645
+ catch (error) {
646
+ const detail = error instanceof Error ? error.message : String(error);
647
+ return `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`;
648
+ }
649
+ }
650
+ else {
651
+ entryPath = hostEntryPath;
652
+ }
653
+ try {
654
+ let packageDir = dirname(entryPath);
655
+ while (packageDir !== dirname(packageDir) && !existsSync(join(packageDir, "package.json"))) {
656
+ packageDir = dirname(packageDir);
657
+ }
658
+ const binDir = join(packageDir, "bin");
659
+ if (!existsSync(binDir))
660
+ return `onnxruntime native binary directory not found: ${binDir}`;
661
+ for (const napiEntry of readdirSync(binDir, { withFileTypes: true })) {
662
+ if (!napiEntry.isDirectory() || !napiEntry.name.startsWith("napi-v"))
663
+ continue;
664
+ const platformDir = join(binDir, napiEntry.name, process.platform, process.arch);
665
+ if (!existsSync(platformDir))
666
+ continue;
667
+ const binaries = readdirSync(platformDir).filter((file) => file.endsWith(".node"));
668
+ if (binaries.length > 0)
669
+ return undefined;
670
+ }
671
+ return `onnxruntime native binary for ${process.platform}/${process.arch} not found under ${binDir}`;
672
+ }
673
+ catch (error) {
674
+ return `onnxruntime native binary could not be resolved (${error instanceof Error ? error.message : String(error)}); the installation looks damaged — reinstall the package`;
675
+ }
676
+ }
677
+ /**
678
+ * 重依赖自解析默认实现 (missingHeavyDependencyModules 生产接线): 双通道与
679
+ * {@link resolveOnnxRuntimeEntryPath} 一致 — jiti(安装店加载缝)注入的
680
+ * `require` 走宿主 alias 表; 纯 ESM(源码/工作区直载)下回落原生 createRequire。
681
+ */
682
+ function resolveHeavyDependencyEntryPath(specifier) {
683
+ if (typeof require === "function") {
684
+ try {
685
+ return require.resolve(specifier);
686
+ }
687
+ catch {
688
+ // jiti 解析失败(alias 未覆盖等)→ 继续走原生解析。
689
+ }
690
+ }
691
+ return createRequire(import.meta.url).resolve(specifier);
692
+ }
693
+ /**
694
+ * 本地检查 1b: 重依赖供给 (设计 §3.2 checking)。`@lancedb/lancedb` 与
695
+ * `@huggingface/transformers` 逐个判定 — entries 提供宿主入口路径 = 可用
696
+ * (不触自解析); 未提供时 resolveModule 自解析成功 = 可用, 抛错 = 计入缺失。
697
+ * 返回缺失 specifier 列表 (空 = 通过)。纯函数零 IO; 导出仅供测试 (与宿主侧
698
+ * buildHostDependencyAliases 同一模式), 生产接线传插件位置自解析 — 把加载步
699
+ * 的 "Cannot find module" 泛化包装前置成精准缺供文案, 省掉模型下载等前置工作。
700
+ */
701
+ export function missingHeavyDependencyModules(entries, resolveModule) {
702
+ const missing = [];
703
+ const supplies = [
704
+ ["@lancedb/lancedb", entries.lancedb],
705
+ ["@huggingface/transformers", entries.transformers],
706
+ ];
707
+ for (const [specifier, entryPath] of supplies) {
708
+ if (entryPath !== undefined)
709
+ continue;
710
+ try {
711
+ resolveModule(specifier);
712
+ }
713
+ catch {
714
+ missing.push(specifier);
715
+ }
716
+ }
717
+ return missing;
718
+ }
719
+ /**
720
+ * transformers.js 缓存布局的容错存在性检查 (设计 §3.2 手动导入路径):
721
+ * `<cacheDir>/<modelId>/` 下需有非空 tokenizer 文件与 `onnx/` 权重文件。
722
+ * 返回 undefined = 检查通过; "missing" = 目录/文件缺失或仅剩 0 字节残骸
723
+ * (失败下载产物, 清理后进入下载阶段, 见 model-cache-hygiene); 其他字符串 =
724
+ * 文件存在但可疑 (截断/非空过小), 属失败。
725
+ */
726
+ function modelFilesCheck(cacheDir, modelId, minWeightBytes) {
727
+ const modelDir = join(cacheDir, modelId);
728
+ if (!existsSync(modelDir))
729
+ return "missing";
730
+ // tokenizer 只剩 0 字节文件 = 失败下载残骸 (真 tokenizer.json 不可能为空):
731
+ // 清掉按缺失处理可重下; 非空文件不动 (手动导入布局不受影响)。
732
+ const tokenizerNames = ["tokenizer.json", "tokenizer.model"].filter((name) => existsSync(join(modelDir, name)));
733
+ const hasTokenizer = tokenizerNames.some((name) => statSync(join(modelDir, name)).size > 0);
734
+ if (!hasTokenizer) {
735
+ if (tokenizerNames.length > 0)
736
+ purgeZeroByteCacheArtifacts(modelDir);
737
+ return "missing";
738
+ }
739
+ const onnxDir = join(modelDir, "onnx");
740
+ if (!existsSync(onnxDir))
741
+ return `onnx weight directory missing under ${modelDir}`;
742
+ const weights = readdirSync(onnxDir).filter((name) => name.includes(".onnx"));
743
+ if (weights.length === 0)
744
+ return `no onnx weight file under ${onnxDir}`;
745
+ const largest = Math.max(...weights.map((name) => statSync(join(onnxDir, name)).size));
746
+ if (largest < minWeightBytes) {
747
+ return `largest onnx weight under ${onnxDir} is ${largest} bytes, below the expected minimum ${minWeightBytes} — the download looks truncated`;
748
+ }
749
+ return undefined;
750
+ }
751
+ /**
752
+ * 本地检查 3: 磁盘余量 (设计 §3.2 checking, node:fs statfs, 零新依赖)。
753
+ * 以 agentDir 所在卷为准 (cacheDir 同卷创建)。
754
+ */
755
+ async function diskHeadroomCheck(agentDir, minimumBytes) {
756
+ let freeBytes;
757
+ try {
758
+ const stats = await statfs(agentDir);
759
+ freeBytes = stats.bfree * stats.bsize;
760
+ }
761
+ catch (error) {
762
+ return `disk free space could not be determined for ${agentDir} (${error instanceof Error ? error.message : String(error)})`;
763
+ }
764
+ if (freeBytes < minimumBytes) {
765
+ return `disk free space is ${freeBytes} bytes, below the required ${minimumBytes}`;
766
+ }
767
+ return undefined;
768
+ }
769
+ /** enabling 失败的五选一行动清单 (设计 §3.2 硬性要求)。 */
770
+ function enablingActionList(modelCacheDir, currentMode) {
771
+ return [
772
+ `Switch to a reachable mirror: set ${HF_MIRRORS_ENV} to a comma-separated host list (tried before the defaults)`,
773
+ `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`,
774
+ `Download the models manually and place them under ${modelCacheDir} (layout: <modelId>/tokenizer.json + <modelId>/onnx/model*.onnx)`,
775
+ currentMode === "full"
776
+ ? 'Downgrade to memory.mode "light" (much smaller models)'
777
+ : 'Keep memory.mode "light" and retry with a reachable mirror',
778
+ 'Give up: leave memory.mode "off" (the default) so the memory family stays unregistered',
779
+ ];
780
+ }
781
+ /**
782
+ * Parses the optional recall context (scenes/projects/tasks) into a preference
783
+ * resolution context. Returns undefined when absent so the no-context recall
784
+ * path stays byte-identical to the pre-disambiguation behavior.
785
+ */
786
+ function parseRecallContext(value) {
787
+ if (value === undefined)
788
+ return undefined;
789
+ if (!isPlainObject(value))
790
+ throw new Error("context must be an object");
791
+ const stringArray = (field) => {
792
+ const entries = value[field];
793
+ if (entries === undefined)
794
+ return undefined;
795
+ if (!Array.isArray(entries))
796
+ throw new Error(`context.${field} must be an array of strings`);
797
+ for (const entry of entries) {
798
+ if (typeof entry !== "string")
799
+ throw new Error(`context.${field} entries must be strings`);
800
+ }
801
+ return entries;
802
+ };
803
+ const scenes = stringArray("scenes");
804
+ const projects = stringArray("projects");
805
+ const tasks = stringArray("tasks");
806
+ return {
807
+ ...(scenes === undefined ? {} : { scenes }),
808
+ ...(projects === undefined ? {} : { projects }),
809
+ ...(tasks === undefined ? {} : { tasks }),
810
+ };
811
+ }
812
+ /**
813
+ * Context-scoped preference post-processing over the recalled facts: preference
814
+ * atoms compete per (subject, key); the resolver drops out-of-scope preferences
815
+ * and deterministically ranks the rest, and an unconfirmed ranking goes through
816
+ * the scope-specificity disambiguator. Whatever remains unresolvable is
817
+ * returned as an explicit conflict block — the recall never silently picks
818
+ * between competing stored values. Non-preference facts pass through untouched.
819
+ */
820
+ function applyPreferenceContext(input) {
821
+ const { facts, context, resolver, disambiguator, atomOf } = input;
822
+ const preferenceAtoms = [];
823
+ const atomsByGroup = new Map();
824
+ const preferenceMemoryIds = new Set();
825
+ for (const fact of facts) {
826
+ const atom = atomOf(fact.memoryId);
827
+ if (atom === undefined || atom.memoryKind !== "preference" || atom.preference === undefined)
828
+ continue;
829
+ preferenceMemoryIds.add(fact.memoryId);
830
+ preferenceAtoms.push(atom);
831
+ const groupKey = `${atom.preference.subject}\u0000${atom.preference.key}`;
832
+ const group = atomsByGroup.get(groupKey) ?? [];
833
+ group.push(atom);
834
+ atomsByGroup.set(groupKey, group);
835
+ }
836
+ if (preferenceMemoryIds.size === 0)
837
+ return { facts, conflicts: [] };
838
+ const keptMemoryIds = new Set();
839
+ const conflicts = [];
840
+ for (const decision of resolver.resolve(preferenceAtoms, context)) {
841
+ const groupAtoms = atomsByGroup.get(`${decision.subject}\u0000${decision.key}`) ?? [];
842
+ if (decision.status === "applied") {
843
+ if (decision.winnerMemoryId === undefined) {
844
+ throw new Error(`applied preference decision without a winnerMemoryId: ${JSON.stringify(decision)}`);
845
+ }
846
+ keptMemoryIds.add(decision.winnerMemoryId);
847
+ continue;
848
+ }
849
+ if (decision.status === "instruction_override") {
850
+ // No stored fact survives a current-instruction override (this slice has
851
+ // no input source for currentInstructions, so the branch stays inert).
852
+ continue;
853
+ }
854
+ if (decision.status === "conflict_unconfirmed") {
855
+ const outcome = disambiguator.disambiguate(groupAtoms.map((atom) => ({
856
+ memoryId: atom.memoryId,
857
+ preferredValue: atom.preference.preferredValue,
858
+ scope: atom.preference.scope,
859
+ evidenceClass: atom.preference.evidence.class,
860
+ applicabilityConfidence: atom.preference.applicabilityConfidence,
861
+ })), context);
862
+ if (outcome.status === "resolved") {
863
+ keptMemoryIds.add(outcome.winnerMemoryId);
864
+ continue;
865
+ }
866
+ conflicts.push({
867
+ subject: decision.subject,
868
+ key: decision.key,
869
+ candidates: groupAtoms.map((atom) => {
870
+ const preference = atom.preference;
871
+ return {
872
+ memoryId: atom.memoryId,
873
+ statement: unwrapEgressStatement(statementOf(atom.payload)),
874
+ value: preference.preferredValue,
875
+ evidenceClass: preference.evidence.class,
876
+ confidence: preference.applicabilityConfidence,
877
+ };
878
+ }),
879
+ explanation: [...outcome.explanation],
880
+ });
881
+ continue;
882
+ }
883
+ throw new Error(`unexpected preference decision status: ${JSON.stringify(decision)}`);
884
+ }
885
+ return {
886
+ facts: facts.filter((fact) => !preferenceMemoryIds.has(fact.memoryId) || keptMemoryIds.has(fact.memoryId)),
887
+ conflicts,
888
+ };
889
+ }
890
+ function tagFacetsOf(atom) {
891
+ return atom.facets
892
+ .filter((facet) => facet.namespace === TAG_FACET_NAMESPACE && facet.key === "tag")
893
+ .map((facet) => facet.value);
894
+ }
895
+ function entryToToolEntry(entry) {
896
+ return {
897
+ memoryId: entry.memoryId,
898
+ kind: entry.memoryKind,
899
+ statement: statementOf(entry.payload),
900
+ occurredAt: entry.occurredAt,
901
+ ...(entry.preference === undefined
902
+ ? {}
903
+ : {
904
+ preference: {
905
+ subject: entry.preference.subject,
906
+ key: entry.preference.key,
907
+ scopeLevel: entry.preference.scopeLevel,
908
+ crossSuiteVisible: entry.preference.crossSuiteVisible,
909
+ },
910
+ }),
911
+ };
912
+ }
913
+ const writeSchema = Type.Object({
914
+ content: Type.String({
915
+ description: "The memory content: one self-contained fact or preference statement",
916
+ maxLength: MEMORY_CONTENT_MAX_LENGTH,
917
+ }),
918
+ kind: Type.Union([Type.Literal("fact"), Type.Literal("preference")], {
919
+ description: "fact = a durable piece of information; preference = a user preference (requires subject)",
920
+ }),
921
+ subject: Type.Optional(Type.Union([Type.Literal("user"), Type.Literal("project"), Type.Literal("task"), Type.Literal("environment")], {
922
+ description: "Required when kind is preference: whose preference this is",
923
+ })),
924
+ tags: Type.Optional(Type.Array(Type.String({ minLength: 1 }), {
925
+ description: "Optional short keywords; stored with the memory, full-text searchable and shown in memory_list; at most 8",
926
+ maxItems: MAX_TAGS,
927
+ })),
928
+ });
929
+ const recallSchema = Type.Object({
930
+ query: Type.String({
931
+ description: "Natural-language query; recall fuses semantic (vector) and full-text (BM25) matching and reranks the result. An exact memoryId retrieves that memory directly",
932
+ }),
933
+ limit: Type.Optional(Type.Integer({ description: "Maximum number of memories to return (1-100)", minimum: 1, maximum: 100 })),
934
+ context: Type.Optional(Type.Object({
935
+ scenes: Type.Optional(Type.Array(Type.String())),
936
+ projects: Type.Optional(Type.Array(Type.String())),
937
+ tasks: Type.Optional(Type.Array(Type.String())),
938
+ }, {
939
+ description: "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)",
940
+ })),
941
+ });
942
+ const listSchema = Type.Object({
943
+ limit: Type.Optional(Type.Integer({ description: "Maximum number of entries to list (1-100)", minimum: 1, maximum: 100 })),
944
+ });
945
+ const forgetSchema = Type.Object({
946
+ memoryId: Type.String({
947
+ description: "The memoryId of the memory to forget, as returned by memory_write or memory_list",
948
+ }),
949
+ });
950
+ /**
951
+ * Creates the builtin memory capability: four tools (memory_write,
952
+ * memory_recall, memory_list, memory_forget) over a per-session Foundation
953
+ * stack whose durable replica is the per-domain JSONL ledger under
954
+ * `<agentDir>/memory/<owner>/ledger-<domain>.jsonl`, resolved lazily at each
955
+ * tool call (统一修复轮 B1).
956
+ *
957
+ * `memory.mode` gates the whole family (2.4d6 S1): a resolved mode of "off"
958
+ * registers NOTHING (zero tools, zero commands, zero events) — the factory
959
+ * returns an empty registration list and logs the one-line disabled
960
+ * declaration. resolveBuiltinCapabilities intercepts earlier at the assembly
961
+ * level (设计 §3.2 "禁用 = 不注册").
962
+ *
963
+ * `override` is the test-only seam (deterministic mock AI 门禁): full vector
964
+ * components skip the real transformers/lancedb assembly entirely; enabling
965
+ * seams keep the real assembly but replace the transformers loader.
966
+ */
967
+ export function createMemoryCapability(api, context, override) {
968
+ const harness = createMemoryCapabilityHarness(api, context, override);
969
+ return harness.registrations;
970
+ }
971
+ /**
972
+ * Test-only assembly variant: the capability with an injected override (full
973
+ * vector components skip real assembly; enabling seams redirect the model
974
+ * loader on the real path). Public tool names, schemas, and return shapes are
975
+ * identical to {@link createMemoryCapability}.
976
+ */
977
+ export function createCapabilityWithVectorComponents(api, context, override) {
978
+ // Tool-face test stubs hand-write partial CapabilityAPIs that predate the
979
+ // D-071 auto-recall hook; default registerLoopHook to a no-op so those
980
+ // tests keep exercising just the tool face. Real CapabilityAPIs always
981
+ // carry the method (production path never hits this default) — the `in`
982
+ // check is runtime-only because the stubs bypass the declared type.
983
+ const apiWithHooks = "registerLoopHook" in api
984
+ ? api
985
+ : Object.assign(Object.create(null), api, {
986
+ registerLoopHook: () => ({
987
+ id: "test:noop-loop-hook",
988
+ kind: "loop-hook",
989
+ dispose() { },
990
+ }),
991
+ });
992
+ return createMemoryCapabilityHarness(apiWithHooks, context, override);
993
+ }
994
+ function createMemoryCapabilityHarness(api, context, override) {
995
+ assertNonEmptyString(context.agentDir, "MemoryCapabilityContext.agentDir");
996
+ const now = () => Date.now();
997
+ const sessionId = api.session?.getSessionId() ?? "no-session";
998
+ // 全局档位解析 (2.4d6 S1): context 显式 → env → settings.json → 默认 off。
999
+ // 非法 env/settings 值 fail-closed 回 off 并各告警一次; off = 家族整体不注册
1000
+ // (零工具零命令零事件)。
1001
+ const modeResolution = resolveMemoryMode(context.memory?.mode, undefined, context.memory?.settingsMode);
1002
+ if (modeResolution.invalidEnvValue !== undefined) {
1003
+ api.logger?.warn(`invalid AGENT_FORGE_MEMORY_MODE value "${modeResolution.invalidEnvValue}"; memory stays off (fail-closed)`);
1004
+ }
1005
+ if (modeResolution.invalidSettingsValue !== undefined) {
1006
+ api.logger?.warn(`invalid memory.mode setting "${modeResolution.invalidSettingsValue}"; memory stays off (fail-closed)`);
1007
+ }
1008
+ if (modeResolution.mode === "off") {
1009
+ api.logger?.info("memory disabled (mode=off)");
1010
+ return {
1011
+ registrations: [],
1012
+ settleVectorWork: async () => { },
1013
+ vectorState: () => "off",
1014
+ enablingOutcome: async () => "ready",
1015
+ };
1016
+ }
1017
+ const memoryMode = modeResolution.mode;
1018
+ const vectorComponentsOverride = override ?? context.vectorComponents ?? context.storage?.vectorComponents;
1019
+ const injectedComponents = vectorComponentsOverride !== undefined && isVectorComponentsOverride(vectorComponentsOverride)
1020
+ ? vectorComponentsOverride
1021
+ : undefined;
1022
+ const enablingOverride = vectorComponentsOverride !== undefined && !isVectorComponentsOverride(vectorComponentsOverride)
1023
+ ? vectorComponentsOverride
1024
+ : undefined;
1025
+ // Store: one per-capability-instance singleton. Atoms of every domain this
1026
+ // session touches live here together — the atom `suiteId` field carries the
1027
+ // read boundary (suite queries filter through it; the ledger is only the
1028
+ // per-domain durable replica). The injected storeFactory (扩展位 A1) replaces
1029
+ // the default store wholesale: the implementation owns retention semantics.
1030
+ const retentionRegistry = createFirstPartyRetentionRegistry();
1031
+ const store = context.storage?.storeFactory?.({
1032
+ agentDir: context.agentDir,
1033
+ owner: BUILTIN_MEMORY_OWNER,
1034
+ now,
1035
+ }) ?? createMemoryStore({ retentionRegistry, now });
1036
+ // Durable replica per domain, created and loaded lazily on the first tool
1037
+ // call in that domain (统一修复轮 B1 复审修复): the sdk appends the suite
1038
+ // binding entry only after the session constructor ran this factory, so the
1039
+ // domain (and with it the ledger to replay) is only known at call time.
1040
+ // Write identity is deterministic: observationId `memory_write:<sessionId>:<n>`
1041
+ // hashes into the memoryId. The counter is instance-local, so a resumed session
1042
+ // re-derives its floor from the replayed ledger (resolveDomain): a persisted
1043
+ // observationId of this session marks its sequence as taken — the store rejects
1044
+ // a regenerated memoryId (canonical atoms are immutable), which would fail every
1045
+ // replayed-sequence write after resume (write → dispose → resume → write).
1046
+ const writeSequencePrefix = `memory_write:${sessionId}:`;
1047
+ const writeSequenceOf = (observationId) => {
1048
+ if (!observationId.startsWith(writeSequencePrefix))
1049
+ return undefined;
1050
+ const sequence = observationId.slice(writeSequencePrefix.length);
1051
+ return /^\d+$/.test(sequence) ? Number(sequence) : undefined;
1052
+ };
1053
+ let writeSequence = 0;
1054
+ const domainLedgers = new Map();
1055
+ const resolveDomain = () => {
1056
+ const domain = resolveSessionDomain(api);
1057
+ const domainName = suiteMemoryDomainName(domain);
1058
+ let ledger = domainLedgers.get(domainName);
1059
+ if (ledger === undefined) {
1060
+ ledger =
1061
+ context.storage?.ledgerFactory?.({
1062
+ agentDir: context.agentDir,
1063
+ owner: BUILTIN_MEMORY_OWNER,
1064
+ domain: domainName,
1065
+ now,
1066
+ }) ??
1067
+ createDurableMemoryLedger({
1068
+ path: join(context.agentDir, "memory", BUILTIN_MEMORY_OWNER, `ledger-${domainName}.jsonl`),
1069
+ now,
1070
+ });
1071
+ if (domain.kind === "legacy") {
1072
+ api.logger?.info("session has no suite binding; memory falls back to the legacy domain", {
1073
+ sessionId: api.session?.getSessionId() ?? "unknown",
1074
+ });
1075
+ }
1076
+ // First load in this domain: replay the durable atoms into the store.
1077
+ // Sequences persisted by earlier instances of this session raise the
1078
+ // write-sequence floor before any new write can regenerate their ids.
1079
+ const replay = ledger.load();
1080
+ for (const atom of replay.atoms) {
1081
+ const persistedSequence = writeSequenceOf(atom.observationId);
1082
+ if (persistedSequence !== undefined && persistedSequence > writeSequence)
1083
+ writeSequence = persistedSequence;
1084
+ try {
1085
+ store.commit(atom);
1086
+ }
1087
+ catch (error) {
1088
+ // A ledger atom the current store cannot accept (e.g. an unregistered
1089
+ // retention mode) must not fail the session — skip with a diagnostic.
1090
+ api.logger?.warn("memory ledger atom could not be replayed into the store", {
1091
+ memoryId: atom.memoryId,
1092
+ error: error instanceof Error ? error.message : String(error),
1093
+ });
1094
+ }
1095
+ }
1096
+ if (replay.corruptedLines > 0 || replay.duplicateSkipped > 0 || replay.unreadable) {
1097
+ api.logger?.warn("memory ledger loaded with diagnostics", {
1098
+ domain: domainName,
1099
+ corruptedLines: replay.corruptedLines,
1100
+ duplicateSkipped: replay.duplicateSkipped,
1101
+ unreadable: replay.unreadable,
1102
+ });
1103
+ }
1104
+ domainLedgers.set(domainName, ledger);
1105
+ // 启动对账: 首个域的 ledger 重放完成后一次性触发 (fire-and-forget,
1106
+ // 不阻塞本工具调用; 失败只影响向量通道, 不影响锚点通道)。
1107
+ vectorReconcileOnce();
1108
+ }
1109
+ return { domain, domainName, ledger };
1110
+ };
1111
+ const machine = createMemoryCandidateMachine({ store, now });
1112
+ const lifecycle = createMemoryLifecycleManager({
1113
+ policy: {
1114
+ policyVersion: "lifecycle@1",
1115
+ halfLifeMs: { session: 86_400_000, cycle: 604_800_000, "long-term": 2_592_000_000 },
1116
+ staleAfterMs: { session: 172_800_000, cycle: 1_209_600_000, "long-term": 5_184_000_000 },
1117
+ archiveAfterMs: { session: 604_800_000, cycle: 5_184_000_000, "long-term": 15_552_000_000 },
1118
+ baselineAttention: 0.5,
1119
+ },
1120
+ now,
1121
+ });
1122
+ const scheduler = createMemoryScheduler({ now });
1123
+ scheduler.acquireForInstance({ instanceId: sessionId, owner: BUILTIN_MEMORY_OWNER });
1124
+ // 非工具召回路径设施 (2.4d6 S4 家族收口取证结论): 生产工具召回 (memory_recall
1125
+ // 工具与 auto-recall 钩子, 后者直连同一 recallExecute, D-071 自 sdk 迁入)
1126
+ // 只走 recallSemantically 语义管线; index → recallAgent →
1127
+ // schedulerApi.recall 这条链在本 capability 内不再被工具面调用 (schedulerApi
1128
+ // 的生产使用仅剩写入路径的 submitObservation/submitCandidate)。保留原因:
1129
+ // MemorySchedulerApiV1 facade 契约 (recall + egress 门)、recall-agent 与
1130
+ // recall-index 是公共 API (src/index.ts 导出) 和 memory-testkit 之上的测试
1131
+ // 设施 (scheduler-api / memory-suite-scope / coverage-branch-memory-recall
1132
+ // 等测试的直接消费者) — 有存续消费者, 非死代码。
1133
+ const index = createMemoryRecallIndex({ store });
1134
+ const recallAgent = createMemoryRecallAgent({
1135
+ scheduler,
1136
+ index,
1137
+ policyDefaults: {
1138
+ candidateBudget: 1000,
1139
+ modelInspectionLimit: 20,
1140
+ finalResultLimit: 20,
1141
+ maxRelationHops: 2,
1142
+ maxModelCalls: 3,
1143
+ maxOutputTokens: 4000,
1144
+ maxOutputBytes: 16_000,
1145
+ maxRounds: 3,
1146
+ },
1147
+ hostLimits: {
1148
+ candidateBudget: 5000,
1149
+ modelInspectionLimit: 100,
1150
+ finalResultLimit: 100,
1151
+ maxRelationHops: 5,
1152
+ maxModelCalls: 10,
1153
+ maxOutputTokens: 100_000,
1154
+ maxOutputBytes: 1_000_000,
1155
+ maxRounds: 10,
1156
+ },
1157
+ now,
1158
+ });
1159
+ const replicaRegistry = createMemoryReplicaRegistry();
1160
+ replicaRegistry.register({
1161
+ replicaId: "memory-ledger",
1162
+ kind: "canonical",
1163
+ ownerScope: BUILTIN_MEMORY_OWNER,
1164
+ contractVersion: MEMORY_CONTRACT_VERSION,
1165
+ healthy: true,
1166
+ });
1167
+ // 向量投影副本 (混合检索工程化, D-035): 恒注册 — purge 批次以向量索引确认为
1168
+ // 副本契约的一部分; 通道 off/disabled 时 index 分派为 no-op (索引不存在,
1169
+ // 无行可清), 行为见 vectorPurgeMemories。
1170
+ replicaRegistry.register({
1171
+ replicaId: "memory-vector-index",
1172
+ kind: "index",
1173
+ contractVersion: MEMORY_CONTRACT_VERSION,
1174
+ ownerScope: BUILTIN_MEMORY_OWNER,
1175
+ healthy: true,
1176
+ });
1177
+ const purgeGate = createMemoryPurgeGate({ registry: replicaRegistry, now });
1178
+ const purgeJournal = createMemoryPurgeJournal();
1179
+ const egressPolicy = createMemoryEgressPolicy({
1180
+ policyVersion: "egress@1",
1181
+ allowedSourceKinds: ["tool", "entry"],
1182
+ redactedPayloadFields: [],
1183
+ });
1184
+ // schedulerApi.recall 在生产工具面无调用点 (角色见上方"非工具召回路径设施"
1185
+ // 注释); 本 capability 的生产使用仅 submitObservation/submitCandidate (写入)。
1186
+ const schedulerApi = createMemorySchedulerApi({
1187
+ recallAgent,
1188
+ machine,
1189
+ lifecycle,
1190
+ purgeGate,
1191
+ egressPolicy,
1192
+ factInfoLookup: (memoryId) => {
1193
+ const record = store.get(memoryId, { owner: BUILTIN_MEMORY_OWNER });
1194
+ if (!record)
1195
+ return undefined;
1196
+ return {
1197
+ sourceRefs: record.atom.sourceRefs,
1198
+ payload: record.atom.payload,
1199
+ };
1200
+ },
1201
+ });
1202
+ // Preference resolution/disambiguation over context-scoped recalls (2.4c/2.4d2):
1203
+ // stateless and deterministic — one instance per capability suffices.
1204
+ const preferenceResolver = createPreferenceResolver();
1205
+ const preferenceDisambiguator = createPreferenceDisambiguator();
1206
+ // Relation diffusion over the recall seeds (记忆网络接线, 2.4d4): a factory-
1207
+ // scoped deterministic network with one enabled one-hop adapter; disabled
1208
+ // adapters yield an explicit `disabled` result instead of silent degradation.
1209
+ const memoryNetwork = createMemoryNetwork({
1210
+ adapters: [createRelationDiffusionAdapter({ maxHops: 1 })],
1211
+ });
1212
+ // ---- 语义召回通道 (2.4d6 S1/S2; 召回唯一检索路径) ----
1213
+ // 状态机 (设计 §3.2): enabling = 检查/加载进行中 (惰性触发: 首次向量操作
1214
+ // 时真正执行); ready = 可用; degraded = ready 后运行期故障 (可自愈回
1215
+ // ready); failed = enabling 失败, 本实例内终态 (mode 回退 off, 记忆工具
1216
+ // 返回携带行动清单的不可用 packet, 不再重试)。所有写侧操作经 vectorQueue
1217
+ // 串行化, 避免 reconcile 与写入埋点在 LanceDB 建表上竞态; recall 等待
1218
+ // enabling 单飞完成, 不与后台对账并发读索引。transformers/lancedb 的动态
1219
+ // import 只出现在 vectorEnsure 的装配分支里 (type-position 引类型), 未
1220
+ // 触发时不加载任何重依赖。
1221
+ let vectorState = "enabling";
1222
+ let vectorChannel;
1223
+ let enablingFailure;
1224
+ let enablingSettled;
1225
+ const enablingSettledPromise = new Promise((resolve) => {
1226
+ enablingSettled = () => resolve(enablingFailure ?? "ready");
1227
+ });
1228
+ if (injectedComponents !== undefined) {
1229
+ // 测试注入: 直接 ready, 跳过真实装配与档位解析。
1230
+ vectorState = "ready";
1231
+ vectorChannel = {
1232
+ embed: injectedComponents.embedProvider,
1233
+ reranker: injectedComponents.reranker,
1234
+ index: injectedComponents.vectorIndex,
1235
+ queryPrefix: injectedComponents.queryPrefix ?? "",
1236
+ embeddingModelId: injectedComponents.embedProvider.modelId,
1237
+ };
1238
+ enablingSettled?.();
1239
+ }
1240
+ const vectorEnabled = () => vectorState !== "failed";
1241
+ let vectorInitPromise;
1242
+ let vectorQueue = Promise.resolve();
1243
+ /** 精排组件失败标记: reranker 加载/调用失败后本实例内跳过精排 (RRF 序)。 */
1244
+ let vectorRerankerUnavailable = false;
1245
+ /** 投影写/清失败的一次性降级告警标记 (重复失败由启动对账修复, 不刷屏)。 */
1246
+ let vectorDegradedWarned = false;
1247
+ const vectorEnqueue = (operation) => {
1248
+ const next = vectorQueue.then(operation, operation);
1249
+ // 队尾永不满仓 reject: 前序失败不阻断后续操作, 错误由操作自己消化。
1250
+ vectorQueue = next.then(() => undefined, () => undefined);
1251
+ return next;
1252
+ };
1253
+ /**
1254
+ * 开启状态机 (设计 §3.2, 惰性单飞): 本地检查 (native .node 可解析 → 重依赖
1255
+ * 供给 (lancedb/transformers: 宿主入口路径或插件位置自解析) → 模型文件存在
1256
+ * 与大小 → 磁盘余量) → transformers 加载 (自带下载, 进度周期性
1257
+ * 落一行日志) → 冒烟 (embed 一条 + getStoredModelId 与档位比对) → ready。
1258
+ * 任一步失败 → failed (原因 + 五选一行动清单), mode 记录回退 off, 本实例
1259
+ * 不再重试。
1260
+ */
1261
+ const vectorEnsure = () => {
1262
+ if (vectorState === "ready" || vectorState === "degraded")
1263
+ return Promise.resolve(vectorChannel);
1264
+ if (vectorState === "failed")
1265
+ return Promise.resolve(undefined);
1266
+ if (vectorInitPromise !== undefined)
1267
+ return vectorInitPromise;
1268
+ vectorInitPromise = (async () => {
1269
+ const preset = vectorPresetForMode(memoryMode);
1270
+ const modelCacheDir = join(context.agentDir, "memory", VECTOR_MODEL_CACHE_DIRNAME);
1271
+ const failEnabling = (reason) => {
1272
+ vectorState = "failed";
1273
+ enablingFailure = { reason, actions: enablingActionList(modelCacheDir, memoryMode) };
1274
+ vectorInitPromise = undefined;
1275
+ api.logger?.warn(`memory enabling failed: ${reason} — actions: ${enablingFailure.actions.join(" | ")}`, {
1276
+ mode: memoryMode,
1277
+ });
1278
+ enablingSettled?.();
1279
+ return undefined;
1280
+ };
1281
+ api.logger?.info(`memory enabling started (mode=${memoryMode}, embedding=${preset.embeddingModelId})`);
1282
+ // 检查 1: native .node sidecar (纯本地, 零网络)。宿主注入路径优先
1283
+ // (安装店副本自解析不可达, 见 MemoryCapabilityContext.hostOnnxRuntimeEntryPath)。
1284
+ const nativeProblem = nativeBinaryCheck(context.hostOnnxRuntimeEntryPath);
1285
+ if (nativeProblem !== undefined)
1286
+ return failEnabling(nativeProblem);
1287
+ // 检查 1b: 重依赖供给。宿主入口路径优先; 未提供时插件位置自解析,
1288
+ // 两者都不可达 = 原本要到加载步才以 "Cannot find module" 泛化包装
1289
+ // 失败的根因, 前置到模型下载之前以 onnx 缺供分支同构的精准文案报告。
1290
+ // 路径已提供但包损坏的真实失败仍走加载步, 由泛化包装保留原始错误。
1291
+ const missingHeavyDependencies = missingHeavyDependencyModules({ lancedb: context.hostLancedbEntryPath, transformers: context.hostTransformersEntryPath }, resolveHeavyDependencyEntryPath);
1292
+ if (missingHeavyDependencies.length > 0) {
1293
+ return failEnabling(`${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`);
1294
+ }
1295
+ // 检查 2: 模型文件 (缺失 = 进入下载; 存在但截断 = 失败提示手动导入)。
1296
+ const embeddingProblem = modelFilesCheck(modelCacheDir, preset.embeddingModelId, MODEL_MIN_WEIGHT_BYTES[memoryMode]);
1297
+ const rerankerProblem = modelFilesCheck(modelCacheDir, VECTOR_RERANKER_MODEL_ID, RERANKER_MIN_WEIGHT_BYTES);
1298
+ const modelsMissing = embeddingProblem === "missing" || rerankerProblem === "missing"
1299
+ ? `${preset.embeddingModelId}${rerankerProblem === "missing" ? ` + ${VECTOR_RERANKER_MODEL_ID}` : ""}`
1300
+ : undefined;
1301
+ if (embeddingProblem !== undefined && embeddingProblem !== "missing")
1302
+ return failEnabling(embeddingProblem);
1303
+ if (rerankerProblem !== undefined && rerankerProblem !== "missing")
1304
+ return failEnabling(rerankerProblem);
1305
+ if (modelsMissing !== undefined) {
1306
+ api.logger?.info(`memory enabling: model files missing (${modelsMissing}); downloading via configured mirrors`);
1307
+ }
1308
+ // 检查 3: 磁盘余量 (light ≥ 500MB / full ≥ 2GB)。
1309
+ const diskProblem = await diskHeadroomCheck(context.agentDir, DISK_HEADROOM_MIN_BYTES[memoryMode]);
1310
+ if (diskProblem !== undefined)
1311
+ return failEnabling(diskProblem);
1312
+ // 加载: transformers/lancedb 的动态 import 都在这些模块内部, 这里只做
1313
+ // 模块级懒加载, 保持未触发会话零重依赖。进度周期性落一行 (下载可达
1314
+ // 数百 MB), 完成后清除。
1315
+ const progressTimer = setInterval(() => {
1316
+ api.logger?.info("memory enabling still in progress (downloading/loading models)");
1317
+ }, ENABLING_PROGRESS_LOG_INTERVAL_MS);
1318
+ if (typeof progressTimer === "object" && "unref" in progressTimer)
1319
+ progressTimer.unref();
1320
+ try {
1321
+ const [vectorIndexModule, embeddingModule, rerankerModule] = await Promise.all([
1322
+ import("./memory/vector-index.js"),
1323
+ import("./memory/embedding-provider.js"),
1324
+ import("./memory/embedding-reranker.js"),
1325
+ ]);
1326
+ const index = await vectorIndexModule.createMemoryVectorIndex({
1327
+ dbPath: join(context.agentDir, "memory", VECTOR_DB_DIRNAME),
1328
+ dimensions: preset.dimensions,
1329
+ ...(context.hostLancedbEntryPath === undefined ? {} : { moduleEntryPath: context.hostLancedbEntryPath }),
1330
+ });
1331
+ const remoteHosts = remoteHostsForEnabling();
1332
+ const channel = {
1333
+ embed: embeddingModule.createTransformersEmbeddingProvider({
1334
+ modelId: preset.embeddingModelId,
1335
+ dimensions: preset.dimensions,
1336
+ cacheDir: modelCacheDir,
1337
+ remoteHosts,
1338
+ ...(context.hostTransformersEntryPath === undefined
1339
+ ? {}
1340
+ : { moduleEntryPath: context.hostTransformersEntryPath }),
1341
+ ...(enablingOverride?.embeddingLoadImpl === undefined
1342
+ ? {}
1343
+ : { loadImpl: enablingOverride.embeddingLoadImpl }),
1344
+ }),
1345
+ reranker: rerankerModule.createTransformersReranker({
1346
+ modelId: VECTOR_RERANKER_MODEL_ID,
1347
+ cacheDir: modelCacheDir,
1348
+ remoteHosts,
1349
+ ...(context.hostTransformersEntryPath === undefined
1350
+ ? {}
1351
+ : { moduleEntryPath: context.hostTransformersEntryPath }),
1352
+ ...(enablingOverride?.rerankerLoadImpl === undefined
1353
+ ? {}
1354
+ : { loadImpl: enablingOverride.rerankerLoadImpl }),
1355
+ }),
1356
+ index,
1357
+ queryPrefix: preset.queryPrefix,
1358
+ embeddingModelId: preset.embeddingModelId,
1359
+ };
1360
+ // 冒烟: embed 一条 (同时完成模型加载) + 投影档位比对。
1361
+ await channel.embed.embed(["memory enabling smoke test"]);
1362
+ if (channel.embed.modelId !== preset.embeddingModelId) {
1363
+ return failEnabling(`smoke check failed: embedding modelId ${channel.embed.modelId} does not match the ${memoryMode} preset ${preset.embeddingModelId}`);
1364
+ }
1365
+ const storedModelId = await channel.index.getStoredModelId();
1366
+ if (storedModelId !== undefined && storedModelId !== preset.embeddingModelId) {
1367
+ api.logger?.warn(`memory projection was built for ${storedModelId}, preset is ${preset.embeddingModelId}; startup reconciliation will rebuild it`);
1368
+ }
1369
+ vectorChannel = channel;
1370
+ vectorState = "ready";
1371
+ api.logger?.info(`memory ready (mode=${memoryMode}, embedding=${preset.embeddingModelId})`);
1372
+ enablingSettled?.();
1373
+ return channel;
1374
+ }
1375
+ catch (error) {
1376
+ return failEnabling(`model load or smoke check failed (${error instanceof Error ? error.message : String(error)})`);
1377
+ }
1378
+ finally {
1379
+ clearInterval(progressTimer);
1380
+ }
1381
+ })();
1382
+ return vectorInitPromise;
1383
+ };
1384
+ /**
1385
+ * 写入埋点 (canonical 已提交后): fire-and-forget, 不阻塞工具返回、不抛出。
1386
+ * embed/upsert 失败记一次 degraded, 该条由启动对账补齐 — canonical 与工具
1387
+ * 返回不受影响 (向量投影只是 index 副本)。full 档语义合并 (S3) 时携带探测
1388
+ * 产物: 复用探测已算出的新 statement 向量; 合并命中在 upsert 新行后移除被
1389
+ * supersedes 的旧行 (upsert 新 id 是插入而非覆盖, 旧行必须显式移除, 失败走
1390
+ * 同一 degraded 告警 — 残行由召回侧 superseded 过滤 + 对账跳过回填兜底)。
1391
+ */
1392
+ const vectorUpsertAtom = (atom, mergeOutcome) => {
1393
+ if (!vectorEnabled())
1394
+ return;
1395
+ void vectorEnqueue(async () => {
1396
+ const channel = await vectorEnsure();
1397
+ if (channel === undefined)
1398
+ return;
1399
+ try {
1400
+ const statement = statementOf(atom.payload);
1401
+ const [vector] = mergeOutcome === undefined ? await channel.embed.embed([statement]) : [mergeOutcome.vector];
1402
+ await channel.index.upsert([
1403
+ {
1404
+ memoryId: atom.memoryId,
1405
+ owner: BUILTIN_MEMORY_OWNER,
1406
+ modelId: channel.embed.modelId,
1407
+ statement,
1408
+ tags: tagFacetsOf(atom),
1409
+ vector,
1410
+ },
1411
+ ]);
1412
+ if (mergeOutcome?.plan.kind === "merge") {
1413
+ await channel.index.remove([mergeOutcome.plan.targetMemoryId]);
1414
+ }
1415
+ }
1416
+ catch (error) {
1417
+ if (!vectorDegradedWarned) {
1418
+ vectorDegradedWarned = true;
1419
+ api.logger?.warn("memory vector projection write failed; the entry is rebuilt by startup reconciliation", {
1420
+ memoryId: atom.memoryId,
1421
+ error: error instanceof Error ? error.message : String(error),
1422
+ });
1423
+ }
1424
+ }
1425
+ });
1426
+ };
1427
+ /**
1428
+ * 当前 store 内被 supersedes 关系指向的 memoryId 集 (S3): 写入探测跳过已
1429
+ * 继任的近邻 (其继任条目才是当前事实), 启动对账跳过回填 (投影只维护未继任
1430
+ * 条目)。O(n) 扫描, 与 recall 关系扩散建图同量级。
1431
+ */
1432
+ const supersededMemoryIds = () => {
1433
+ const superseded = new Set();
1434
+ for (const record of store.list({ owner: BUILTIN_MEMORY_OWNER })) {
1435
+ for (const relation of record.atom.relations) {
1436
+ if (relation.kind === "supersedes" && relation.targetMemoryId !== undefined) {
1437
+ superseded.add(relation.targetMemoryId);
1438
+ }
1439
+ }
1440
+ }
1441
+ return superseded;
1442
+ };
1443
+ /** 语义合并探测失败的一次性降级告警标记 (失败按普通新条目写入, 不刷屏)。 */
1444
+ let semanticMergeProbeWarned = false;
1445
+ /**
1446
+ * 写入语义合并探测 (S3, full 档独有; 在 canonical 提交前执行 — supersedes/
1447
+ * conflict 关系必须随新 atom 一次性提交, atom 不可变)。同一 vectorQueue 串
1448
+ * 行域内: embed(statement) → 同 owner KNN top-5 → canonical 回查 (同域可见、
1449
+ * 未被继任) → 重嵌各近邻的 canonical statement 计算余弦 (阈值作用在 canonical
1450
+ * 文本的向量上, 不依赖可能陈旧的投影 body) → 三分支判定。时序保证"排除自身
1451
+ * observationId 条目": 新 atom 尚未提交, 其 memoryId 不可能在投影中;
1452
+ * observationId 重放 (deduped) 消费不到探测产物, 幂等零副作用。探测失败不
1453
+ * 阻塞写入 (降级为普通新条目并告警一次); enabling 未落定/failed 不等待 —
1454
+ * 写入不得被模型加载阻塞。
1455
+ */
1456
+ const semanticMergeProbe = (input) => {
1457
+ if (vectorState === "enabling" || vectorState === "failed")
1458
+ return Promise.resolve(undefined);
1459
+ return vectorEnqueue(async () => {
1460
+ const channel = await vectorEnsure();
1461
+ if (channel === undefined)
1462
+ return undefined;
1463
+ try {
1464
+ const [vector] = await channel.embed.embed([input.statement]);
1465
+ const hits = await channel.index.queryKnn(vector, {
1466
+ owner: BUILTIN_MEMORY_OWNER,
1467
+ limit: SEMANTIC_MERGE_KNN_LIMIT,
1468
+ });
1469
+ if (hits.length === 0)
1470
+ return { plan: { kind: "new" }, vector };
1471
+ const superseded = supersededMemoryIds();
1472
+ const neighborAtoms = [];
1473
+ for (const hit of hits) {
1474
+ const record = store.get(hit.memoryId, {
1475
+ owner: BUILTIN_MEMORY_OWNER,
1476
+ ...(input.domain.kind === "suite" ? { suiteId: input.domain.suiteId } : {}),
1477
+ });
1478
+ if (record === undefined)
1479
+ continue;
1480
+ // 域隔离: 合并只发生在同域条目之间 (suite 写入不得 supersedes 跨
1481
+ // suite/跨域条目 — 那会移除别域召回所需的投影行; promoted 的跨
1482
+ // suite 可见偏好 canonical 归属其原 suite, 同样不作为合并目标)。
1483
+ if (input.domain.kind === "suite"
1484
+ ? record.atom.suiteId !== input.domain.suiteId
1485
+ : record.atom.suiteId !== undefined) {
1486
+ continue;
1487
+ }
1488
+ if (superseded.has(record.atom.memoryId))
1489
+ continue;
1490
+ neighborAtoms.push(record.atom);
1491
+ }
1492
+ if (neighborAtoms.length === 0)
1493
+ return { plan: { kind: "new" }, vector };
1494
+ const neighborVectors = await channel.embed.embed(neighborAtoms.map((atom) => statementOf(atom.payload)));
1495
+ const neighbors = neighborAtoms.map((atom, position) => ({
1496
+ memoryId: atom.memoryId,
1497
+ tags: tagFacetsOf(atom),
1498
+ cosine: cosineSimilarity(vector, neighborVectors[position] ?? []),
1499
+ }));
1500
+ return { plan: decideSemanticMergePlan({ newTags: input.newTags, neighbors }), vector };
1501
+ }
1502
+ catch (error) {
1503
+ if (!semanticMergeProbeWarned) {
1504
+ semanticMergeProbeWarned = true;
1505
+ api.logger?.warn("memory semantic merge probe failed; the entry is written as a new memory", {
1506
+ error: error instanceof Error ? error.message : String(error),
1507
+ });
1508
+ }
1509
+ return undefined;
1510
+ }
1511
+ });
1512
+ };
1513
+ /**
1514
+ * purge 的 index 副本分派: 通道非 ready/degraded 时 no-op 且不报错 (enabling
1515
+ * 未完成/failed = 索引无本会话写入的行, 残行由 store 回查兜底不可出线)。
1516
+ * remove 异步且失败只记 degraded: 残行永远过不了 recall 的 store 回查, 不构
1517
+ * 成出线泄漏。
1518
+ */
1519
+ const vectorPurgeMemories = (memoryIds) => {
1520
+ if (vectorState !== "ready" && vectorState !== "degraded")
1521
+ return;
1522
+ void vectorEnqueue(async () => {
1523
+ const channel = vectorChannel;
1524
+ if (channel === undefined)
1525
+ return;
1526
+ try {
1527
+ await channel.index.remove(memoryIds);
1528
+ }
1529
+ catch (error) {
1530
+ if (!vectorDegradedWarned) {
1531
+ vectorDegradedWarned = true;
1532
+ api.logger?.warn("memory vector projection purge failed; stale rows stay unreachable via the store lookup", {
1533
+ error: error instanceof Error ? error.message : String(error),
1534
+ });
1535
+ }
1536
+ }
1537
+ });
1538
+ };
1539
+ /**
1540
+ * 启动对账 (一次性, capability 初始化语义内; fire-and-forget 不阻塞首工具):
1541
+ * 1) 投影 modelId 与预设不一致 (含 undefined 且表非空) → 清空向量表;
1542
+ * 2) diff store 全量 memoryId vs 投影, 缺失的分批 embed+upsert, 单次上限
1543
+ * {@link RECONCILE_MAX_UPSERTS}, 超出记 degraded 下次启动继续。对账失败不
1544
+ * 阻塞 capability 启动, 状态转 degraded (运行期故障, 可自愈: 后续向量操作
1545
+ * 重新触发补齐语义, canonical 面不受影响)。
1546
+ */
1547
+ let vectorReconcileStarted = false;
1548
+ const vectorReconcileOnce = () => {
1549
+ if (!vectorEnabled() || vectorReconcileStarted)
1550
+ return;
1551
+ vectorReconcileStarted = true;
1552
+ void vectorEnqueue(async () => {
1553
+ const channel = await vectorEnsure();
1554
+ if (channel === undefined)
1555
+ return;
1556
+ try {
1557
+ const storedModelId = await channel.index.getStoredModelId();
1558
+ if (storedModelId !== channel.embeddingModelId) {
1559
+ const stale = await channel.index.listMemoryIds();
1560
+ if (stale.size > 0)
1561
+ await channel.index.remove([...stale]);
1562
+ }
1563
+ const indexed = await channel.index.listMemoryIds();
1564
+ // 被继任的旧条目不回填投影 (S3): 合并已移除其行, 对账的重放/重建
1565
+ // 不得复活 (召回可见性由继任条目承担, canonical 侧保留可直查)。
1566
+ const superseded = supersededMemoryIds();
1567
+ const missing = [];
1568
+ for (const record of store.list({ owner: BUILTIN_MEMORY_OWNER })) {
1569
+ if (superseded.has(record.atom.memoryId))
1570
+ continue;
1571
+ if (!indexed.has(record.atom.memoryId))
1572
+ missing.push(record.atom);
1573
+ }
1574
+ if (missing.length > RECONCILE_MAX_UPSERTS) {
1575
+ api.logger?.warn("memory vector reconciliation capped; remaining entries rebuild on the next startup", {
1576
+ total: missing.length,
1577
+ cap: RECONCILE_MAX_UPSERTS,
1578
+ });
1579
+ }
1580
+ const batch = missing.slice(0, RECONCILE_MAX_UPSERTS);
1581
+ if (batch.length === 0)
1582
+ return;
1583
+ const statements = batch.map((atom) => statementOf(atom.payload));
1584
+ const vectors = await channel.embed.embed(statements);
1585
+ await channel.index.upsert(batch.map((atom, position) => ({
1586
+ memoryId: atom.memoryId,
1587
+ owner: BUILTIN_MEMORY_OWNER,
1588
+ modelId: channel.embed.modelId,
1589
+ statement: statements[position],
1590
+ tags: tagFacetsOf(atom),
1591
+ vector: vectors[position],
1592
+ })));
1593
+ }
1594
+ catch (error) {
1595
+ vectorState = "degraded";
1596
+ api.logger?.warn("memory vector reconciliation failed; the channel is degraded (canonical recall degrades, self-heals on the next successful channel op)", {
1597
+ error: error instanceof Error ? error.message : String(error),
1598
+ });
1599
+ }
1600
+ });
1601
+ };
1602
+ const buildWriteAtom = (input) => {
1603
+ writeSequence += 1;
1604
+ const observationId = `memory_write:${sessionId}:${writeSequence}`;
1605
+ const memoryId = `mem-${identityHash(`${BUILTIN_MEMORY_OWNER}\u0000${observationId}`)}`;
1606
+ const occurredAt = new Date(now()).toISOString();
1607
+ const sourceRef = { kind: "tool", id: `memory_write:${observationId}` };
1608
+ const tags = (input.tags ?? []).map((tag) => tag.trim()).filter((tag) => tag !== "");
1609
+ const facets = tags.map((tag) => ({
1610
+ namespace: TAG_FACET_NAMESPACE,
1611
+ schemaVersion: 1,
1612
+ key: "tag",
1613
+ value: tag,
1614
+ }));
1615
+ const preferenceEnvelope = input.kind === "preference"
1616
+ ? {
1617
+ subject: input.subject,
1618
+ key: "statement",
1619
+ preferredValue: input.content,
1620
+ scope: { level: "profile-private" },
1621
+ evidence: { class: "explicit", sourceRefs: [sourceRef], confidence: 1 },
1622
+ applicabilityConfidence: 1,
1623
+ }
1624
+ : undefined;
1625
+ return buildMemoryAtomV1({
1626
+ memoryId,
1627
+ contractVersion: MEMORY_CONTRACT_VERSION,
1628
+ schemaVersion: 1,
1629
+ scope: "long-term",
1630
+ retentionMode: "long",
1631
+ owner: BUILTIN_MEMORY_OWNER,
1632
+ profileId: capabilityManifest.id,
1633
+ ...(input.domain.kind === "suite" ? { suiteId: input.domain.suiteId } : {}),
1634
+ retentionPolicyVersion: "retention@1",
1635
+ memoryKind: input.kind === "preference" ? "preference" : "fact",
1636
+ payload: { statement: input.content },
1637
+ ...(preferenceEnvelope === undefined ? {} : { preference: preferenceEnvelope }),
1638
+ occurredAt,
1639
+ recordedAt: occurredAt,
1640
+ sourceRefs: [sourceRef],
1641
+ observationId,
1642
+ sessionRefs: [sessionId],
1643
+ agentInstanceRefs: [],
1644
+ projectRefs: [],
1645
+ subjectRefs: [],
1646
+ facets,
1647
+ relations: input.relations ?? [],
1648
+ confidence: 1,
1649
+ importance: 0.5,
1650
+ evidenceClass: "explicit",
1651
+ contentRevision: `c-${identityHash(input.content)}`,
1652
+ writeReason: input.writeReason ?? `${capabilityManifest.id}/memory_write`,
1653
+ });
1654
+ };
1655
+ const writeTool = api.registerTool({
1656
+ name: "memory_write",
1657
+ label: "Write memory",
1658
+ description: "Persist one durable memory (a fact or a user preference) scoped to the current suite; it stays recallable across sessions until forgotten",
1659
+ parameters: writeSchema,
1660
+ execute: async (first, second) => {
1661
+ const input = memoryToolInput(first, second);
1662
+ assertNonEmptyString(input.content, "content");
1663
+ // 长度契约 (混合检索工程化 §5): 超长显式拒绝并提示拆分, 而非静默截断。
1664
+ if (input.content.length > MEMORY_CONTENT_MAX_LENGTH) {
1665
+ throw new Error(`content is ${input.content.length} characters; the memory content limit is ${MEMORY_CONTENT_MAX_LENGTH} — split it into multiple self-contained memories`);
1666
+ }
1667
+ if (input.kind !== "fact" && input.kind !== "preference")
1668
+ throw new Error('kind must be "fact" or "preference"');
1669
+ if (input.kind === "preference") {
1670
+ assertNonEmptyString(input.subject, 'subject (required when kind is "preference")');
1671
+ }
1672
+ if (input.subject !== undefined)
1673
+ assertNonEmptyString(input.subject, "subject");
1674
+ if (input.tags !== undefined) {
1675
+ if (!Array.isArray(input.tags) || input.tags.length > MAX_TAGS) {
1676
+ throw new Error(`tags must be an array of at most ${MAX_TAGS} strings`);
1677
+ }
1678
+ for (const tag of input.tags)
1679
+ assertNonEmptyString(tag, "tags entry");
1680
+ }
1681
+ const { domain, domainName, ledger } = resolveDomain();
1682
+ // 语义合并探测 (2.4d6 S3, full 档独有): light 档完全跳过 (写向量投影
1683
+ // 不查近邻, 行为与现状一致)。探测在 canonical 提交前执行, 判定决定新
1684
+ // atom 的 supersedes/conflict relations (atom 不可变, 随提交一次写入);
1685
+ // 探测失败降级为普通新条目 (告警一次, 不阻塞写入)。
1686
+ const mergeOutcome = memoryMode === "full"
1687
+ ? await semanticMergeProbe({
1688
+ statement: input.content,
1689
+ newTags: input.tags ?? [],
1690
+ domain,
1691
+ })
1692
+ : undefined;
1693
+ const atom = buildWriteAtom({
1694
+ content: input.content,
1695
+ kind: input.kind,
1696
+ domain,
1697
+ ...(input.subject === undefined ? {} : { subject: input.subject }),
1698
+ ...(input.tags === undefined ? {} : { tags: input.tags }),
1699
+ ...(mergeOutcome === undefined
1700
+ ? {}
1701
+ : { relations: semanticMergeRelations(mergeOutcome.plan, input.content) }),
1702
+ });
1703
+ const observation = schedulerApi.submitObservation({
1704
+ owner: BUILTIN_MEMORY_OWNER,
1705
+ observationId: atom.observationId,
1706
+ draft: atom,
1707
+ });
1708
+ if (observation.state === "rejected") {
1709
+ throw new Error(`memory rejected: ${observation.reason ?? "rejected"}`);
1710
+ }
1711
+ const candidate = schedulerApi.submitCandidate({
1712
+ observationId: atom.observationId,
1713
+ owner: BUILTIN_MEMORY_OWNER,
1714
+ });
1715
+ if (candidate.status === "rejected")
1716
+ throw new Error(`memory rejected: ${candidate.reason ?? "rejected"}`);
1717
+ if (candidate.status === "committed") {
1718
+ ledger.append(atom);
1719
+ // 向量投影埋点 (混合检索工程化): canonical 已提交, 投影写入异步旁路,
1720
+ // 失败不阻塞不抛出 (vectorUpsertAtom 内部消化)。full 档携带语义合并
1721
+ // 探测产物 (复用探测向量; 合并命中同时移除被 supersedes 的旧行)。
1722
+ vectorUpsertAtom(atom, mergeOutcome);
1723
+ }
1724
+ await api.publish({
1725
+ type: EVENT_TYPE,
1726
+ version: 1,
1727
+ correlationId: capabilityManifest.id,
1728
+ data: { action: "written", memoryId: candidate.memoryId, memoryKind: atom.memoryKind, domain: domainName },
1729
+ });
1730
+ // 偏好晋升触发 (B2): the /promote capability command is the production
1731
+ // caller of promotePreferenceToUserDefault; the tip is what tells the
1732
+ // model (so it can tell the user) that the cross-suite promotion path
1733
+ // exists. Facts have no promotion path — no tip.
1734
+ return {
1735
+ memoryId: candidate.memoryId,
1736
+ kind: atom.memoryKind,
1737
+ domain: domainName,
1738
+ ...(atom.memoryKind === "preference"
1739
+ ? {
1740
+ tip: "[tip] If this is a general preference (not project-specific), the user can promote it across all suites using /promote.",
1741
+ }
1742
+ : {}),
1743
+ };
1744
+ },
1745
+ });
1746
+ /** recency 排序 (occurredAt 降序, memoryId 升序打破并列 — 与 recall index 同口径)。 */
1747
+ const recencyOrdered = (entries) => [...entries]
1748
+ .sort((left, right) => Date.parse(right.atom.occurredAt) - Date.parse(left.atom.occurredAt) ||
1749
+ (left.atom.memoryId < right.atom.memoryId ? -1 : 1))
1750
+ .map((entry, position) => ({ atom: entry.atom, position }));
1751
+ /**
1752
+ * superseded 过滤 (S3-B, store 回查后 / 精排前): 被某条在场候选的 supersedes
1753
+ * 关系指向的旧条目不再出线 — 合并后同义查询只出新条目。"新条目在场"以候选
1754
+ * 集为界: 继任条目被遗忘 (purge/过期) 后其关系随之消失, 旧条目自然恢复出线。
1755
+ * memoryId 主键直查不经此过滤 (引用不失效), memory_list 管理面不过滤
1756
+ * (nothing silently dropped — 用户可显式 forget 旧条目)。
1757
+ */
1758
+ const filterSupersededCandidates = (candidates) => {
1759
+ const superseded = new Set();
1760
+ for (const entry of candidates) {
1761
+ for (const relation of entry.atom.relations) {
1762
+ if (relation.kind === "supersedes" && relation.targetMemoryId !== undefined) {
1763
+ superseded.add(relation.targetMemoryId);
1764
+ }
1765
+ }
1766
+ }
1767
+ if (superseded.size === 0)
1768
+ return candidates;
1769
+ return candidates.filter((entry) => !superseded.has(entry.atom.memoryId));
1770
+ };
1771
+ /**
1772
+ * store 回查 (正确性关键): 候选 id 逐条走与旧锚点通道相同的可见性单一路径
1773
+ * (owner + suite 读边界 + 过期, 单一路径); suite 场景下不可见 id 按 legacy/
1774
+ * 外套件区分计数 (suiteFilter 可观测), 不可见者丢弃且不回填 (诚实分页)。
1775
+ */
1776
+ const lookupCandidates = (rankedIds, suiteId) => {
1777
+ const candidates = [];
1778
+ let legacySkipped = 0;
1779
+ let foreignSuiteSkipped = 0;
1780
+ for (const [memoryId, position] of rankedIds) {
1781
+ const record = store.get(memoryId, {
1782
+ owner: BUILTIN_MEMORY_OWNER,
1783
+ ...(suiteId === undefined ? {} : { suiteId }),
1784
+ });
1785
+ if (record !== undefined) {
1786
+ candidates.push({ atom: record.atom, position });
1787
+ continue;
1788
+ }
1789
+ if (suiteId !== undefined) {
1790
+ const unfiltered = store.get(memoryId, { owner: BUILTIN_MEMORY_OWNER });
1791
+ if (unfiltered !== undefined) {
1792
+ if (unfiltered.atom.suiteId === undefined)
1793
+ legacySkipped += 1;
1794
+ else
1795
+ foreignSuiteSkipped += 1;
1796
+ }
1797
+ }
1798
+ }
1799
+ return { candidates, suiteFilter: { legacySkipped, foreignSuiteSkipped } };
1800
+ };
1801
+ /**
1802
+ * egress 门重放 (设计 §11 MUST run): 对最终候选统一执行, 与旧锚点 packet 同
1803
+ * 一 gate、同一 factInfo 来源 (store); blocked 即 fail-closed, 该次 recall
1804
+ * 返回 failed packet (来源未登记的事实永不越过出界面)。
1805
+ */
1806
+ const egressGate = (candidates) => {
1807
+ const factInfo = {};
1808
+ for (const entry of candidates) {
1809
+ factInfo[entry.atom.memoryId] = {
1810
+ sourceRefs: entry.atom.sourceRefs,
1811
+ payload: entry.atom.payload,
1812
+ };
1813
+ }
1814
+ return egressPolicy.apply({
1815
+ operationId: `memory_recall:${sessionId}:${identityHash(candidates.map((entry) => entry.atom.memoryId).join("\u0000"))}`,
1816
+ status: "completed",
1817
+ facts: candidates.map((entry) => ({
1818
+ memoryId: entry.atom.memoryId,
1819
+ contentRevision: entry.atom.contentRevision,
1820
+ statement: JSON.stringify(entry.atom.payload),
1821
+ confidence: entry.atom.confidence,
1822
+ })),
1823
+ queryPlan: { lookupUsed: false, filtersApplied: [], scorerVersion: "semantic-rrf@1" },
1824
+ attempts: 0,
1825
+ narrowingHints: [],
1826
+ omittedCount: 0,
1827
+ truncated: false,
1828
+ truncationScope: "none",
1829
+ budget: {
1830
+ candidateBudget: 0,
1831
+ modelInspectionLimit: 0,
1832
+ finalResultLimit: 0,
1833
+ used: { candidateCount: 0, modelInspectedCount: 0, finalResultCount: 0 },
1834
+ },
1835
+ }, factInfo);
1836
+ };
1837
+ const recallSemantically = async (input) => {
1838
+ // 读侧先等写侧队列落定 (启动对账 + 待处理的投影写入), 保证 recall 与
1839
+ // 仓库/投影的线性一致: 本调用之前提交的记忆要么在 store 要么已补进投影,
1840
+ // 不会与后台对账/埋点竞态。
1841
+ await vectorQueue;
1842
+ const channel = await vectorEnsure();
1843
+ if (channel === undefined) {
1844
+ return {
1845
+ kind: "unavailable",
1846
+ message: enablingFailure === undefined
1847
+ ? `memory channel is unavailable (state=${vectorState})`
1848
+ : `memory enabling failed: ${enablingFailure.reason} — actions: ${enablingFailure.actions.join(" | ")}`,
1849
+ };
1850
+ }
1851
+ const suiteId = input.domain.kind === "suite" ? input.domain.suiteId : undefined;
1852
+ let vectorFailed = false;
1853
+ let ftsFailed = false;
1854
+ let knnHits;
1855
+ let ftsHits;
1856
+ try {
1857
+ const [queryVector] = await channel.embed.embed([channel.queryPrefix + input.query]);
1858
+ knnHits = await channel.index.queryKnn(queryVector, {
1859
+ owner: BUILTIN_MEMORY_OWNER,
1860
+ limit: HYBRID_CHANNEL_DEPTH,
1861
+ });
1862
+ }
1863
+ catch (error) {
1864
+ vectorFailed = true;
1865
+ api.logger?.warn("memory recall degraded: the vector channel failed; continuing on the FTS channel", {
1866
+ error: error instanceof Error ? error.message : String(error),
1867
+ });
1868
+ }
1869
+ try {
1870
+ ftsHits = await channel.index.queryFts(input.query, {
1871
+ owner: BUILTIN_MEMORY_OWNER,
1872
+ limit: HYBRID_CHANNEL_DEPTH,
1873
+ });
1874
+ }
1875
+ catch (error) {
1876
+ ftsFailed = true;
1877
+ api.logger?.warn("memory recall degraded: the FTS channel failed", {
1878
+ error: error instanceof Error ? error.message : String(error),
1879
+ });
1880
+ }
1881
+ let candidates;
1882
+ let suiteFilter = { legacySkipped: 0, foreignSuiteSkipped: 0 };
1883
+ let channelDegraded = false;
1884
+ if (!vectorFailed && !ftsFailed && knnHits !== undefined && ftsHits !== undefined) {
1885
+ // 主路径: 双通道 RRF k=60 融合 top20。
1886
+ const pool = rrfFuseRankings([ftsHits.map((hit) => hit.memoryId), knnHits.map((hit) => hit.memoryId)]);
1887
+ const lookup = lookupCandidates(pool.map((memoryId, position) => [memoryId, position]), suiteId);
1888
+ candidates = lookup.candidates;
1889
+ suiteFilter = lookup.suiteFilter;
1890
+ }
1891
+ else if (ftsHits !== undefined) {
1892
+ // 向量失败: FTS-only + recency 排序。
1893
+ channelDegraded = true;
1894
+ const lookup = lookupCandidates(ftsHits.map((hit, position) => [hit.memoryId, position]), suiteId);
1895
+ candidates = recencyOrdered(lookup.candidates);
1896
+ suiteFilter = lookup.suiteFilter;
1897
+ }
1898
+ else if (knnHits !== undefined) {
1899
+ // FTS 失败: KNN-only + recency 排序 (对称降级)。
1900
+ channelDegraded = true;
1901
+ const lookup = lookupCandidates(knnHits.map((hit, position) => [hit.memoryId, position]), suiteId);
1902
+ candidates = recencyOrdered(lookup.candidates);
1903
+ suiteFilter = lookup.suiteFilter;
1904
+ }
1905
+ else {
1906
+ // 双通道皆败: 域内 recency 枚举兜底 (store.list, 列表语义保留)。
1907
+ channelDegraded = true;
1908
+ const page = listDomainMemories({ store }, { owner: BUILTIN_MEMORY_OWNER, domain: input.domain });
1909
+ const domainCandidates = [];
1910
+ for (const entry of page.entries) {
1911
+ const record = store.get(entry.memoryId, { owner: BUILTIN_MEMORY_OWNER });
1912
+ if (record !== undefined)
1913
+ domainCandidates.push({ atom: record.atom, position: 0 });
1914
+ }
1915
+ candidates = recencyOrdered(domainCandidates);
1916
+ }
1917
+ // 运行期通道故障 → degraded (可自愈: 下一次全通 recall 回 ready)。
1918
+ if (channelDegraded)
1919
+ vectorState = "degraded";
1920
+ else if (vectorState === "degraded")
1921
+ vectorState = "ready";
1922
+ // superseded 过滤 (S3-B): 回查后、egress/精排前 (主路径与全部降级路径
1923
+ // 统一收敛于此 — 被继任的旧条目任何通道都不再出线)。
1924
+ candidates = filterSupersededCandidates(candidates);
1925
+ if (candidates.length === 0) {
1926
+ return { kind: "served", facts: [], candidateCount: 0, suiteFilter };
1927
+ }
1928
+ const egressOutcome = egressGate(candidates);
1929
+ if (egressOutcome.status === "blocked")
1930
+ return { kind: "blocked", reason: egressOutcome.reason };
1931
+ const statementByMemoryId = new Map(egressOutcome.packet.facts.map((fact) => [fact.memoryId, fact.statement]));
1932
+ let ordered = candidates.map((entry) => ({
1933
+ memoryId: entry.atom.memoryId,
1934
+ statement: unwrapEgressStatement(statementByMemoryId.get(entry.atom.memoryId) ?? ""),
1935
+ confidence: entry.atom.confidence,
1936
+ position: entry.position,
1937
+ }));
1938
+ // 精排 (仅主融合路径: 降级页的排序即其降级语义)。reranker 不可用或调用
1939
+ // 失败 → 保持 RRF 序 (精排失败 → RRF 序), 各记一次结构化日志。
1940
+ const reranker = channel.reranker;
1941
+ if (!channelDegraded && reranker !== undefined && !vectorRerankerUnavailable) {
1942
+ try {
1943
+ const scores = await reranker.rerank(input.query, ordered.map((fact) => fact.statement));
1944
+ ordered = ordered
1945
+ .map((fact, position) => ({ ...fact, score: scores[position] }))
1946
+ .sort((left, right) => (right.score ?? 0) - (left.score ?? 0) || left.position - right.position);
1947
+ }
1948
+ catch (error) {
1949
+ vectorRerankerUnavailable = true;
1950
+ api.logger?.warn("memory reranker failed; recall continues on the RRF order", {
1951
+ error: error instanceof Error ? error.message : String(error),
1952
+ });
1953
+ }
1954
+ }
1955
+ // minScore 门槛 (§2.1): 仅精排实际产生 score 的候选参与过滤 (score <
1956
+ // minScore 丢弃), 过滤先于 limit 截取; 降级路径与无 score 的候选不受影响。
1957
+ const minScore = input.minScore;
1958
+ const eligible = minScore === undefined
1959
+ ? ordered
1960
+ : ordered.filter((fact) => fact.score === undefined || fact.score >= minScore);
1961
+ return {
1962
+ kind: "served",
1963
+ facts: eligible
1964
+ .slice(0, input.limit)
1965
+ .map(({ memoryId, statement, confidence }) => ({ memoryId, statement, confidence })),
1966
+ candidateCount: ordered.length,
1967
+ suiteFilter,
1968
+ };
1969
+ };
1970
+ // 提取为具名闭包: 除模型工具面外, 下方的 auto-recall 钩子直接调用同一 execute
1971
+ // (单参输入约定, memoryToolInput 取第一参), 不经 runtime.invokeTool —— 钩子
1972
+ // 随插件注册/卸载, 与工具面同生命周期, 替换工具面不会重接自动召回。
1973
+ const recallExecute = async (first, second) => {
1974
+ const input = memoryToolInput(first, second);
1975
+ assertNonEmptyString(input.query, "query");
1976
+ const rawLimit = input.limit;
1977
+ const limit = rawLimit === undefined
1978
+ ? DEFAULT_RECALL_LIMIT
1979
+ : (() => {
1980
+ if (typeof rawLimit !== "number" ||
1981
+ !Number.isSafeInteger(rawLimit) ||
1982
+ rawLimit < 1 ||
1983
+ rawLimit > 100) {
1984
+ throw new Error("limit must be an integer between 1 and 100");
1985
+ }
1986
+ return rawLimit;
1987
+ })();
1988
+ // 内部执行缝 (§2.1): auto-recall 闭包经本字段恒传质量门槛; memory_recall
1989
+ // 工具 schema 不含该参数 (工具面恒 undefined), 显式召回行为不变。
1990
+ const rawMinScore = input.minScore;
1991
+ const minScore = rawMinScore === undefined
1992
+ ? undefined
1993
+ : (() => {
1994
+ if (typeof rawMinScore !== "number" || !Number.isFinite(rawMinScore)) {
1995
+ throw new Error("minScore must be a finite number");
1996
+ }
1997
+ return rawMinScore;
1998
+ })();
1999
+ const context = parseRecallContext(input.context);
2000
+ const { domain } = resolveDomain();
2001
+ // enabling 失败 = 本实例终态 (mode 记录回退 off): 记忆工具返回携带原因
2002
+ // 与五选一行动清单的不可用 packet (设计 §3.2 硬性要求), 不再重试。
2003
+ if (vectorState === "failed") {
2004
+ const failure = enablingFailure ?? { reason: "memory enabling failed", actions: [] };
2005
+ return {
2006
+ status: "unavailable",
2007
+ state: "failed",
2008
+ facts: [],
2009
+ error: `memory enabling failed: ${failure.reason}`,
2010
+ actions: failure.actions,
2011
+ };
2012
+ }
2013
+ // 主键快路径 (memoryIdEquals 语义保留, 幂等/引用而非检索): query 恰为本
2014
+ // 域可见 memoryId 时直接取该条, 不依赖向量通道 (enabling 期间也可用)。
2015
+ let outcome;
2016
+ const directRecord = store.get(input.query.trim(), {
2017
+ owner: BUILTIN_MEMORY_OWNER,
2018
+ ...(domain.kind === "suite" ? { suiteId: domain.suiteId } : {}),
2019
+ });
2020
+ if (directRecord !== undefined) {
2021
+ const gate = egressGate([{ atom: directRecord.atom, position: 0 }]);
2022
+ if (gate.status === "blocked") {
2023
+ return { status: "failed", facts: [], error: `egress blocked: ${gate.reason}` };
2024
+ }
2025
+ const statementByMemoryId = new Map(gate.packet.facts.map((fact) => [fact.memoryId, fact.statement]));
2026
+ outcome = {
2027
+ kind: "served",
2028
+ facts: [
2029
+ {
2030
+ memoryId: directRecord.atom.memoryId,
2031
+ statement: unwrapEgressStatement(statementByMemoryId.get(directRecord.atom.memoryId) ?? ""),
2032
+ confidence: directRecord.atom.confidence,
2033
+ },
2034
+ ],
2035
+ candidateCount: 1,
2036
+ suiteFilter: { legacySkipped: 0, foreignSuiteSkipped: 0 },
2037
+ };
2038
+ }
2039
+ else {
2040
+ try {
2041
+ outcome = await recallSemantically({
2042
+ query: input.query,
2043
+ domain,
2044
+ limit,
2045
+ ...(minScore === undefined ? {} : { minScore }),
2046
+ });
2047
+ }
2048
+ catch (error) {
2049
+ // 全败 (枚举兜底也失败) → degraded packet: 结构化返回, 不抛给模型。
2050
+ vectorState = "degraded";
2051
+ const message = error instanceof Error ? error.message : String(error);
2052
+ api.logger?.warn("memory recall degraded: every channel failed; returning a degraded packet", {
2053
+ error: message,
2054
+ });
2055
+ return { status: "failed", facts: [], error: message };
2056
+ }
2057
+ }
2058
+ if (outcome.kind === "blocked") {
2059
+ return { status: "failed", facts: [], error: `egress blocked: ${outcome.reason}` };
2060
+ }
2061
+ if (outcome.kind === "unavailable") {
2062
+ return {
2063
+ status: "unavailable",
2064
+ state: vectorState,
2065
+ facts: [],
2066
+ error: outcome.message,
2067
+ };
2068
+ }
2069
+ const facts = outcome.facts;
2070
+ const resolved = context === undefined
2071
+ ? undefined
2072
+ : applyPreferenceContext({
2073
+ facts,
2074
+ context,
2075
+ resolver: preferenceResolver,
2076
+ disambiguator: preferenceDisambiguator,
2077
+ atomOf: (memoryId) => store.get(memoryId, { owner: BUILTIN_MEMORY_OWNER })?.atom,
2078
+ });
2079
+ // 偏好消歧先于关系扩散: the diffusion seeds are the atoms the user
2080
+ // actually sees — preference facts dropped by the conflict resolution
2081
+ // never diffuse their relations.
2082
+ const finalFacts = resolved === undefined ? facts : resolved.facts;
2083
+ const seeds = [];
2084
+ for (const fact of finalFacts) {
2085
+ const atom = store.get(fact.memoryId, { owner: BUILTIN_MEMORY_OWNER })?.atom;
2086
+ if (atom !== undefined)
2087
+ seeds.push(atom);
2088
+ }
2089
+ // The traversal graph is the recall domain's visible atom set (same
2090
+ // suite read boundary). Diffusion targets outside this set are dropped,
2091
+ // so relations never leak across suites.
2092
+ const domainMemoryIds = new Set();
2093
+ const graph = [];
2094
+ for (const entry of listDomainMemories({ store }, { owner: BUILTIN_MEMORY_OWNER, domain }).entries) {
2095
+ domainMemoryIds.add(entry.memoryId);
2096
+ const atom = store.get(entry.memoryId, { owner: BUILTIN_MEMORY_OWNER })?.atom;
2097
+ if (atom !== undefined)
2098
+ graph.push(atom);
2099
+ }
2100
+ const related = [];
2101
+ let relatedOmittedCount = 0;
2102
+ const seenRelatedMemoryIds = new Set();
2103
+ for (const adapterResult of memoryNetwork.expand({ seeds, graph })) {
2104
+ if (adapterResult.status !== "completed")
2105
+ continue;
2106
+ for (const expansion of adapterResult.related) {
2107
+ if (!domainMemoryIds.has(expansion.memoryId))
2108
+ continue;
2109
+ if (seenRelatedMemoryIds.has(expansion.memoryId))
2110
+ continue;
2111
+ seenRelatedMemoryIds.add(expansion.memoryId);
2112
+ if (related.length >= MAX_RELATED_REFS) {
2113
+ relatedOmittedCount += 1;
2114
+ continue;
2115
+ }
2116
+ related.push(expansion);
2117
+ }
2118
+ }
2119
+ return {
2120
+ status: finalFacts.length === 0 ? "empty" : "completed",
2121
+ facts: finalFacts,
2122
+ ...(resolved !== undefined && resolved.conflicts.length > 0 ? { conflicts: resolved.conflicts } : {}),
2123
+ ...(domain.kind === "suite" ? { suiteFilter: outcome.suiteFilter } : {}),
2124
+ omittedCount: Math.max(0, outcome.candidateCount - finalFacts.length),
2125
+ truncated: false,
2126
+ // References only — no statement/payload egress; the model fetches
2127
+ // content via memory_list or another memory_recall.
2128
+ ...(related.length > 0
2129
+ ? {
2130
+ related: related.map((expansion) => ({
2131
+ memoryId: expansion.memoryId,
2132
+ relationKind: expansion.relationKind,
2133
+ hop: expansion.hop,
2134
+ weight: expansion.weight,
2135
+ via: expansion.via,
2136
+ })),
2137
+ }
2138
+ : {}),
2139
+ ...(relatedOmittedCount > 0 ? { relatedOmittedCount } : {}),
2140
+ };
2141
+ };
2142
+ const recallTool = api.registerTool({
2143
+ name: MEMORY_RECALL_TOOL_NAME,
2144
+ label: "Recall memories",
2145
+ promptGuidelines: [MEMORY_TOOLS_GUIDE],
2146
+ description: "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)",
2147
+ parameters: recallSchema,
2148
+ execute: recallExecute,
2149
+ });
2150
+ // ---- B1 记忆自动注入 (auto recall; D-071 增补裁决: 自 sdk transformContext 迁入) ----
2151
+ // transformContext 链上的插件环节: 无事实可注入时返回 undefined 保持输入。工厂
2152
+ // 执行 ⟺ memory 家族注册 (mode=off 时整体不注册), 钩子随插件卸载自动摘除, 无需
2153
+ // 再检查工具在位。失败降级 (recall throw / 契约漂移): 不注入、不阻塞请求, 记
2154
+ // 一条有界 api.logger warn。
2155
+ const autoRecallBlock = async (query) => {
2156
+ try {
2157
+ // §2.1: 自动召回恒传质量门槛 (内部闭包, 不经工具 schema)。
2158
+ const result = await recallExecute({
2159
+ query,
2160
+ limit: MEMORY_AUTO_RECALL_LIMIT,
2161
+ minScore: MEMORY_AUTO_RECALL_MIN_RERANK_SCORE,
2162
+ });
2163
+ // Defensive contract-drift checks: recallExecute is this factory's own
2164
+ // closure, so both branches are unreachable today — they exist in case
2165
+ // the recall body ever grows an adapter seam (then add a test seam for
2166
+ // them; until then there is no honest way to drive them from a test).
2167
+ if (result === null || typeof result !== "object") {
2168
+ api.logger?.warn("memory auto-recall failed: memory_recall returned a non-object result (contract drift)");
2169
+ return "";
2170
+ }
2171
+ const facts = result.facts;
2172
+ if (!Array.isArray(facts)) {
2173
+ api.logger?.warn('memory auto-recall failed: memory_recall returned a non-array "facts" field (contract drift)');
2174
+ return "";
2175
+ }
2176
+ const statements = [];
2177
+ for (const fact of facts) {
2178
+ if (fact !== null && typeof fact === "object") {
2179
+ const statement = fact.statement;
2180
+ if (typeof statement === "string" && statement.trim() !== "")
2181
+ statements.push(statement);
2182
+ }
2183
+ }
2184
+ if (statements.length === 0)
2185
+ return "";
2186
+ return [
2187
+ MEMORY_AUTO_RECALL_MARKER,
2188
+ ...statements.map((statement) => `- ${statement}`),
2189
+ "</auto_recalled_memory>",
2190
+ MEMORY_AUTO_RECALL_DISCLAIMER,
2191
+ ].join("\n");
2192
+ }
2193
+ catch (error) {
2194
+ // Bound by code points, not UTF-16 code units, so the cut never splits a
2195
+ // surrogate pair into a lone surrogate.
2196
+ const message = error instanceof Error ? error.message : String(error);
2197
+ const codePoints = Array.from(message);
2198
+ const bounded = codePoints.length <= MEMORY_AUTO_RECALL_ERROR_MESSAGE_LIMIT
2199
+ ? message
2200
+ : `${codePoints.slice(0, MEMORY_AUTO_RECALL_ERROR_MESSAGE_LIMIT).join("")}…`;
2201
+ api.logger?.warn(`memory auto-recall failed: memory_recall threw: ${bounded}`);
2202
+ return "";
2203
+ }
2204
+ };
2205
+ // Per-session cache keyed by the last user message text: one user turn runs
2206
+ // at most one recall; the remembered block rides every model round of that
2207
+ // turn unchanged, so request-time injections stay idempotent across tool
2208
+ // loops. An empty block means "no injection for this turn" (no facts, or
2209
+ // recall failure — the recall is an enhancement face and must never block
2210
+ // the user's request).
2211
+ const autoRecall = { lastQuery: undefined, block: "" };
2212
+ const autoRecallHook = api.registerLoopHook("transformContext", async (messages) => {
2213
+ const userText = lastBranchUserText(api);
2214
+ if (userText === undefined || userText.trim() === "")
2215
+ return undefined;
2216
+ if (autoRecall.lastQuery !== userText) {
2217
+ autoRecall.lastQuery = userText;
2218
+ autoRecall.block = await autoRecallBlock(userText);
2219
+ }
2220
+ if (autoRecall.block === "")
2221
+ return undefined;
2222
+ return injectAfterLastUserMessage(messages, {
2223
+ role: "user",
2224
+ content: autoRecall.block,
2225
+ timestamp: now(),
2226
+ });
2227
+ });
2228
+ // ---- assistant 偏好卡注入 (统一修复轮 A 交付3; D-075 S4-4 第二批随拆包迁入) ----
2229
+ // 宿主 sdk 曾在 system prompt 的 persona 段后注入本卡; 公共插件 API 无 system
2230
+ // prompt 面, 迁入后经 transformContext 循环钩子注入 (最后一条 user 消息之后,
2231
+ // 与 auto-recall 同一请求时视图)。卡文本与读边界语义逐字保留 (宿主
2232
+ // memory-assistant-card.test.ts pin 的格式): own-suite 偏好 + 已晋升 user-default
2233
+ // 偏好, legacy 原子不可见。会话期一卡: 首次模型轮次惰性读取并缓存 (绑定入口在
2234
+ // 会话构造后才写入, 工厂时间读不到 domain — 与 B1 同因), 后续轮次复用。判定
2235
+ // "assistant 定向" 用 builtin 默认方案 id (宿主 builtin suite 事实: assistant 方
2236
+ // 案的 orientation 即 assistant); 自定义方案的 orientation 宿主未上公共通道,
2237
+ // 不覆盖。IO 错误降级为一条 api.logger warn + 无注入 (插件无 suite 诊断通道,
2238
+ // warn 即对等物), 绝不阻塞请求。
2239
+ const assistantCard = { loaded: false, text: undefined };
2240
+ const preferenceCardHook = api.registerLoopHook("transformContext", async (messages) => {
2241
+ if (!assistantCard.loaded) {
2242
+ assistantCard.loaded = true;
2243
+ const domain = resolveSessionDomain(api);
2244
+ if (domain.kind === "suite" && domain.suiteId === "assistant") {
2245
+ try {
2246
+ assistantCard.text = loadAssistantPreferenceCard({
2247
+ agentDir: context.agentDir,
2248
+ owner: BUILTIN_MEMORY_OWNER,
2249
+ suiteId: domain.suiteId,
2250
+ });
2251
+ }
2252
+ catch (error) {
2253
+ const message = error instanceof Error ? error.message : String(error);
2254
+ api.logger?.warn(`assistant preference card could not be loaded: ${message}`);
2255
+ }
2256
+ }
2257
+ }
2258
+ if (assistantCard.text === undefined)
2259
+ return undefined;
2260
+ return injectAfterLastUserMessage(messages, {
2261
+ role: "user",
2262
+ content: assistantCard.text,
2263
+ timestamp: now(),
2264
+ });
2265
+ });
2266
+ // ---- bash 失败教训自动沉淀 (错误教训与召回质量护栏设计 §2.2) ----
2267
+ // 家族内独立开关 (默认 on): mode=off 已在工厂入口零注册; lessons=off 时本块
2268
+ // 不订阅任何事件 (无观察者即无捕获)。两个事件的公开载荷都不含工具入参, 命令
2269
+ // 字符串在 tool.execution.start 时经会话分支的 assistant toolCall 块回读
2270
+ // (与 lastBranchUserText 同一数据来源), 以 toolCallId 配对到 tool.result。
2271
+ const lessonsResolution = resolveMemoryLessons(context.memory?.lessons);
2272
+ if (lessonsResolution.invalidEnvValue !== undefined) {
2273
+ api.logger?.warn(`invalid ${MEMORY_LESSONS_ENV} value "${lessonsResolution.invalidEnvValue}"; bash error-lesson capture stays on`);
2274
+ }
2275
+ // lifecycle 在 CapabilityAPI 类型上是必选成员(experimental),但最小宿主可以
2276
+ // 不带该面;教训是增强面,与 auto-recall 同款降级:缺面时跳过注册、记一条
2277
+ // 有界 warn,不影响记忆家族其余工具面。
2278
+ const lifecycleReady = api.lifecycle !== undefined;
2279
+ if (lessonsResolution.lessons === "on" && !lifecycleReady) {
2280
+ api.logger?.warn("memory bash-lesson capture unavailable: host exposes no lifecycle face");
2281
+ }
2282
+ const lessonRegistrations = [];
2283
+ if (lessonsResolution.lessons === "on" && lifecycleReady) {
2284
+ /** (command, exitCode) 签名去重 (§2.2 风暴闸门): 同签名重复失败只写首条。 */
2285
+ const lessonSignatures = new Set();
2286
+ let lessonsWritten = 0;
2287
+ let lessonCapWarned = false;
2288
+ /** toolCallId → command 配对映射 (仅 bash, 容量 FIFO)。 */
2289
+ const bashCommandsByToolCallId = new Map();
2290
+ const rememberBashCommand = (toolCallId, command) => {
2291
+ if (!bashCommandsByToolCallId.has(toolCallId) &&
2292
+ bashCommandsByToolCallId.size >= MEMORY_LESSON_TOOLCALL_MAP_CAPACITY) {
2293
+ const oldest = bashCommandsByToolCallId.keys().next();
2294
+ if (oldest.done !== true)
2295
+ bashCommandsByToolCallId.delete(oldest.value);
2296
+ }
2297
+ bashCommandsByToolCallId.set(toolCallId, command);
2298
+ };
2299
+ /**
2300
+ * 从会话分支回读该 toolCall 的 bash 命令 (assistant toolCall 块, 最新
2301
+ * 优先): assistant 消息在工具执行前已入分支, tool.execution.start 时可查。
2302
+ */
2303
+ const bashCommandForToolCall = (toolCallId) => {
2304
+ const entries = api.session?.getBranchEntries() ?? [];
2305
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
2306
+ const entry = entries[index];
2307
+ if (entry.type !== "message" || entry.message?.role !== "assistant")
2308
+ continue;
2309
+ const content = entry.message.content;
2310
+ if (!Array.isArray(content))
2311
+ continue;
2312
+ for (const item of content) {
2313
+ if (!isPlainObject(item) || item.type !== "toolCall" || item.id !== toolCallId)
2314
+ continue;
2315
+ const args = item.arguments;
2316
+ const command = isPlainObject(args) ? args.command : undefined;
2317
+ if (typeof command === "string" && command.trim() !== "")
2318
+ return command;
2319
+ }
2320
+ }
2321
+ return undefined;
2322
+ };
2323
+ /**
2324
+ * 内部直写 (与 /promote 同构): ledger 追加持久副本, store.commit 以
2325
+ * retentionModeId "short" 覆盖保留档 (7 天到期失去召回资格, 不物理清除),
2326
+ * 向量投影异步埋点。教训不是模型工具调用, 不经 candidate 状态机。
2327
+ */
2328
+ const writeLesson = (command, exitCode, errorText) => {
2329
+ const signature = `${command}\u0000${exitCode}`;
2330
+ if (lessonSignatures.has(signature))
2331
+ return;
2332
+ if (lessonsWritten >= MEMORY_LESSON_MAX_PER_SESSION) {
2333
+ if (!lessonCapWarned) {
2334
+ lessonCapWarned = true;
2335
+ api.logger?.warn(`memory bash-lesson cap reached (${MEMORY_LESSON_MAX_PER_SESSION} per instance); further failures are not captured`);
2336
+ }
2337
+ return;
2338
+ }
2339
+ const { domain, ledger } = resolveDomain();
2340
+ const atom = buildWriteAtom({
2341
+ content: buildLessonContent(command, exitCode, errorText),
2342
+ kind: "fact",
2343
+ tags: [...MEMORY_LESSON_TAGS],
2344
+ domain,
2345
+ writeReason: `${capabilityManifest.id}/auto-lesson`,
2346
+ });
2347
+ ledger.append(atom);
2348
+ store.commit(atom, { retentionModeId: MEMORY_LESSON_RETENTION_MODE_ID });
2349
+ vectorUpsertAtom(atom);
2350
+ lessonsWritten += 1;
2351
+ lessonSignatures.add(signature);
2352
+ };
2353
+ lessonRegistrations.push(api.lifecycle.registerObserve(TOOL_EXECUTION_START_EVENT_ID, (event) => {
2354
+ if (event.data.toolName !== "bash")
2355
+ return;
2356
+ const command = bashCommandForToolCall(event.data.toolCallId);
2357
+ if (command !== undefined)
2358
+ rememberBashCommand(event.data.toolCallId, command);
2359
+ }, TOOL_EXECUTION_START_EVENT_VERSION));
2360
+ lessonRegistrations.push(api.lifecycle.registerObserve(TOOL_RESULT_EVENT_ID, (event) => {
2361
+ const data = event.data;
2362
+ if (data.toolName !== "bash" || !data.isError)
2363
+ return;
2364
+ const command = bashCommandsByToolCallId.get(data.toolCallId);
2365
+ bashCommandsByToolCallId.delete(data.toolCallId);
2366
+ if (command === undefined) {
2367
+ // 配对失败 (start 未命中/映射溢出): 不写教训, 记一条有界 debug。
2368
+ api.logger?.debug("memory bash-lesson capture skipped: no command paired with the failed toolCall", {
2369
+ toolCallId: data.toolCallId,
2370
+ });
2371
+ return;
2372
+ }
2373
+ const errorText = toolResultTextOf(data.message.content);
2374
+ writeLesson(command, lessonExitCodeOf(errorText), errorText);
2375
+ }, TOOL_RESULT_EVENT_VERSION));
2376
+ }
2377
+ const listTool = api.registerTool({
2378
+ name: "memory_list",
2379
+ label: "List memories",
2380
+ description: "List this suite's durable memories (the explicit management surface for personal data)",
2381
+ parameters: listSchema,
2382
+ execute: async (first, second) => {
2383
+ const input = memoryToolInput(first, second);
2384
+ const rawLimit = input.limit;
2385
+ const limit = rawLimit === undefined
2386
+ ? DEFAULT_LIST_LIMIT
2387
+ : (() => {
2388
+ if (typeof rawLimit !== "number" ||
2389
+ !Number.isSafeInteger(rawLimit) ||
2390
+ rawLimit < 1 ||
2391
+ rawLimit > 100) {
2392
+ throw new Error("limit must be an integer between 1 and 100");
2393
+ }
2394
+ return rawLimit;
2395
+ })();
2396
+ const { domain } = resolveDomain();
2397
+ const page = listDomainMemories({ store }, { owner: BUILTIN_MEMORY_OWNER, domain });
2398
+ return {
2399
+ domain: page.domain,
2400
+ entries: page.entries.slice(0, limit).map(entryToToolEntry),
2401
+ total: page.entries.length,
2402
+ preferenceCount: page.preferenceCount,
2403
+ factCount: page.factCount,
2404
+ filter: page.filter,
2405
+ };
2406
+ },
2407
+ });
2408
+ const forgetTool = api.registerTool({
2409
+ name: "memory_forget",
2410
+ label: "Forget memory",
2411
+ description: "Physically delete one memory of this suite by memoryId (purge-gated, journalled, irreversible)",
2412
+ parameters: forgetSchema,
2413
+ execute: async (first, second) => {
2414
+ const input = memoryToolInput(first, second);
2415
+ assertNonEmptyString(input.memoryId, "memoryId");
2416
+ const { domain, domainName, ledger } = resolveDomain();
2417
+ const authorizationRef = {
2418
+ mode: "user-immediate",
2419
+ issuedAt: now(),
2420
+ issuedBy: capabilityManifest.id,
2421
+ confirmationRef: `memory_forget:${sessionId}:${input.memoryId}`,
2422
+ };
2423
+ const result = forgetDomainMemory({
2424
+ store,
2425
+ purgeGate,
2426
+ purgeJournal,
2427
+ // index 副本分派 (混合检索工程化): canonical 走下方原回调 (行为零
2428
+ // 变化), 向量投影由 suite-memory 的 executePurgeBatch 按副本路由到这里。
2429
+ purgeIndexReplica: vectorPurgeMemories,
2430
+ now,
2431
+ }, {
2432
+ owner: BUILTIN_MEMORY_OWNER,
2433
+ domain,
2434
+ memoryId: input.memoryId,
2435
+ authorizedBy: `${capabilityManifest.id}:memory_forget`,
2436
+ authorizationRef,
2437
+ }, (memoryIds) => {
2438
+ const purged = new Set(memoryIds);
2439
+ // Physical replica purge: rewrite the durable ledger without the
2440
+ // purged atoms, then evict them from the in-memory store so the
2441
+ // running process cannot recall them either (both idempotent for
2442
+ // purge_eligible retries).
2443
+ ledger.rewrite(ledger.atomsSnapshot().filter((atom) => !purged.has(atom.memoryId)));
2444
+ for (const memoryId of memoryIds)
2445
+ store.evict(memoryId);
2446
+ });
2447
+ if (result.status === "completed") {
2448
+ await api.publish({
2449
+ type: EVENT_TYPE,
2450
+ version: 1,
2451
+ correlationId: capabilityManifest.id,
2452
+ data: { action: "forgotten", memoryId: input.memoryId, domain: domainName, batchId: result.batchId },
2453
+ });
2454
+ }
2455
+ return { status: result.status, memoryId: input.memoryId };
2456
+ },
2457
+ });
2458
+ /** Canonical memoryId of the promoted copy of one preference (promotePreferenceToUserDefault suffix). */
2459
+ const userDefaultMemoryId = (memoryId) => `${memoryId}-user-default`;
2460
+ /**
2461
+ * /promote (偏好晋升触发, B2): the production caller of
2462
+ * {@link promotePreferenceToUserDefault}. Promotes one of THIS session's
2463
+ * preferences to the user-default scope: the promoted canonical atom is
2464
+ * appended to the session domain's durable ledger and committed to the
2465
+ * store, so every suite's preference-card read (which applies the suite
2466
+ * read boundary across all of the owner's ledgers) sees it from now on.
2467
+ * The original atom is never rewritten. Errors (unknown id, non-preference,
2468
+ * already promoted) surface as error results, never as thrown failures —
2469
+ * a slash command must not crash the host.
2470
+ */
2471
+ const promoteCommand = api.registerCommand({
2472
+ name: "promote",
2473
+ description: "Promote a memory preference to the user-default scope (visible in every suite)",
2474
+ argumentHint: "<memoryId>",
2475
+ execute: async (args) => {
2476
+ const memoryId = args.trim();
2477
+ if (memoryId === "") {
2478
+ return {
2479
+ status: "error",
2480
+ message: "Usage: /promote <memoryId> — run memory_list for this suite's memory ids",
2481
+ };
2482
+ }
2483
+ const record = store.get(memoryId, { owner: BUILTIN_MEMORY_OWNER });
2484
+ if (!record) {
2485
+ return {
2486
+ status: "error",
2487
+ message: `No memory found with id ${memoryId}; run memory_list for this suite's memory ids`,
2488
+ };
2489
+ }
2490
+ const atom = record.atom;
2491
+ if (atom.memoryKind !== "preference" || atom.preference === undefined) {
2492
+ return {
2493
+ status: "error",
2494
+ message: `${memoryId} is a fact, not a preference; only preferences can be promoted`,
2495
+ };
2496
+ }
2497
+ if (store.get(userDefaultMemoryId(memoryId), { owner: BUILTIN_MEMORY_OWNER }) !== undefined) {
2498
+ return {
2499
+ status: "error",
2500
+ message: `Preference ${memoryId} is already promoted to the user-default scope`,
2501
+ };
2502
+ }
2503
+ try {
2504
+ const promoted = promotePreferenceToUserDefault({
2505
+ atom,
2506
+ authorizedBy: "user:/promote",
2507
+ confirmedAt: new Date(now()).toISOString(),
2508
+ });
2509
+ const { domainName, ledger } = resolveDomain();
2510
+ ledger.append(promoted);
2511
+ store.commit(promoted);
2512
+ // 向量投影埋点: 与 memory_write 同一异步旁路 (失败不阻塞命令返回)。
2513
+ vectorUpsertAtom(promoted);
2514
+ return {
2515
+ status: "success",
2516
+ message: `Promoted "${unwrapEgressStatement(statementOf(promoted.payload))}" to the user-default scope; it is now visible in every suite`,
2517
+ data: { memoryId: promoted.memoryId, sourceMemoryId: memoryId, domain: domainName },
2518
+ };
2519
+ }
2520
+ catch (error) {
2521
+ return { status: "error", message: error instanceof Error ? error.message : String(error) };
2522
+ }
2523
+ },
2524
+ });
2525
+ // memory.scheduler@1 plugin capability (宪法 §3 显式声明面): a declarative
2526
+ // marker only — the scheduler enforcement itself (per-instance single
2527
+ // lease, acquired above) stays carried by the host builtin implementation.
2528
+ // The declaration anchors "this suite's memory scheduling is provided by
2529
+ // this plugin" so the code Profile assembly check (requiresMemoryScheduler)
2530
+ // and third-party replacement detection have an explicit surface:
2531
+ // list()-visible, a same-id double provide fails explicitly (no override
2532
+ // priority), and after dispose a replacement provider can take the id.
2533
+ const schedulerCapability = api.capabilities.provide({ id: "memory.scheduler", version: 1, kind: "service" }, () => Object.freeze({
2534
+ id: "memory.scheduler",
2535
+ version: 1,
2536
+ owner: "agent-forge.builtin.memory",
2537
+ leaseScope: "per-agent-instance",
2538
+ }));
2539
+ const registrations = [
2540
+ writeTool,
2541
+ recallTool,
2542
+ listTool,
2543
+ forgetTool,
2544
+ promoteCommand,
2545
+ schedulerCapability,
2546
+ autoRecallHook,
2547
+ preferenceCardHook,
2548
+ ...lessonRegistrations,
2549
+ ];
2550
+ return {
2551
+ registrations,
2552
+ settleVectorWork: async () => {
2553
+ await vectorQueue;
2554
+ },
2555
+ vectorState: () => vectorState,
2556
+ // 触发惰性 enabling (已注入组件时已就绪) 并等待其落定: ready 或结构化失败。
2557
+ enablingOutcome: async () => {
2558
+ if (vectorState === "enabling")
2559
+ void vectorEnsure();
2560
+ await enablingSettledPromise;
2561
+ return enablingFailure ?? "ready";
2562
+ },
2563
+ };
2564
+ }
2565
+ //# sourceMappingURL=capability.js.map