@ai-agent-forge/plugin-memory 0.88.1 → 0.88.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -4
- package/agent-forge.json +1 -1
- package/bin/agent-forge-memory-inference.js +8 -0
- package/dist/capability.d.ts +42 -59
- package/dist/capability.d.ts.map +1 -1
- package/dist/capability.js +263 -2055
- package/dist/capability.js.map +1 -1
- package/dist/entry.d.ts +11 -0
- package/dist/entry.d.ts.map +1 -1
- package/dist/entry.js +38 -2
- package/dist/entry.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/inference-bin.d.ts +21 -0
- package/dist/inference-bin.d.ts.map +1 -0
- package/dist/inference-bin.js +85 -0
- package/dist/inference-bin.js.map +1 -0
- package/dist/memory/command-face.d.ts +86 -0
- package/dist/memory/command-face.d.ts.map +1 -0
- package/dist/memory/command-face.js +377 -0
- package/dist/memory/command-face.js.map +1 -0
- package/dist/memory/embedding-provider.d.ts.map +1 -1
- package/dist/memory/embedding-provider.js +14 -2
- package/dist/memory/embedding-provider.js.map +1 -1
- package/dist/memory/embedding-reranker.d.ts.map +1 -1
- package/dist/memory/embedding-reranker.js +14 -2
- package/dist/memory/embedding-reranker.js.map +1 -1
- package/dist/memory/enabling.d.ts +104 -0
- package/dist/memory/enabling.d.ts.map +1 -0
- package/dist/memory/enabling.js +321 -0
- package/dist/memory/enabling.js.map +1 -0
- package/dist/memory/hooks.d.ts +110 -0
- package/dist/memory/hooks.d.ts.map +1 -0
- package/dist/memory/hooks.js +478 -0
- package/dist/memory/hooks.js.map +1 -0
- package/dist/memory/inference-channel.d.ts +33 -0
- package/dist/memory/inference-channel.d.ts.map +1 -0
- package/dist/memory/inference-channel.js +101 -0
- package/dist/memory/inference-channel.js.map +1 -0
- package/dist/memory/inference-client.d.ts +25 -0
- package/dist/memory/inference-client.d.ts.map +1 -0
- package/dist/memory/inference-client.js +216 -0
- package/dist/memory/inference-client.js.map +1 -0
- package/dist/memory/inference-protocol.d.ts +95 -0
- package/dist/memory/inference-protocol.d.ts.map +1 -0
- package/dist/memory/inference-protocol.js +27 -0
- package/dist/memory/inference-protocol.js.map +1 -0
- package/dist/memory/inference-server.d.ts +70 -0
- package/dist/memory/inference-server.d.ts.map +1 -0
- package/dist/memory/inference-server.js +395 -0
- package/dist/memory/inference-server.js.map +1 -0
- package/dist/memory/inference-service-main.d.ts +31 -0
- package/dist/memory/inference-service-main.d.ts.map +1 -0
- package/dist/memory/inference-service-main.js +89 -0
- package/dist/memory/inference-service-main.js.map +1 -0
- package/dist/memory/ledger.d.ts +15 -0
- package/dist/memory/ledger.d.ts.map +1 -1
- package/dist/memory/ledger.js +35 -2
- package/dist/memory/ledger.js.map +1 -1
- package/dist/memory/model-cache-hygiene.d.ts +10 -0
- package/dist/memory/model-cache-hygiene.d.ts.map +1 -1
- package/dist/memory/model-cache-hygiene.js +15 -0
- package/dist/memory/model-cache-hygiene.js.map +1 -1
- package/dist/memory/preference-promotion.d.ts +0 -14
- package/dist/memory/preference-promotion.d.ts.map +1 -1
- package/dist/memory/preference-promotion.js +17 -0
- package/dist/memory/preference-promotion.js.map +1 -1
- package/dist/memory/recall-pipeline.d.ts +85 -0
- package/dist/memory/recall-pipeline.d.ts.map +1 -0
- package/dist/memory/recall-pipeline.js +678 -0
- package/dist/memory/recall-pipeline.js.map +1 -0
- package/dist/memory/retention-sweep.d.ts +59 -0
- package/dist/memory/retention-sweep.d.ts.map +1 -0
- package/dist/memory/retention-sweep.js +103 -0
- package/dist/memory/retention-sweep.js.map +1 -0
- package/dist/memory/store.d.ts +17 -2
- package/dist/memory/store.d.ts.map +1 -1
- package/dist/memory/store.js +18 -2
- package/dist/memory/store.js.map +1 -1
- package/dist/memory/vector-channel.d.ts +120 -0
- package/dist/memory/vector-channel.d.ts.map +1 -0
- package/dist/memory/vector-channel.js +495 -0
- package/dist/memory/vector-channel.js.map +1 -0
- package/dist/memory/vector-projection.d.ts +87 -0
- package/dist/memory/vector-projection.d.ts.map +1 -0
- package/dist/memory/vector-projection.js +215 -0
- package/dist/memory/vector-projection.js.map +1 -0
- package/dist/memory/write-face.d.ts +128 -0
- package/dist/memory/write-face.d.ts.map +1 -0
- package/dist/memory/write-face.js +504 -0
- package/dist/memory/write-face.js.map +1 -0
- package/dist/testing/memory-testkit.d.ts +2 -0
- package/dist/testing/memory-testkit.d.ts.map +1 -1
- package/dist/testing/memory-testkit.js +1 -0
- package/dist/testing/memory-testkit.js.map +1 -1
- package/package.json +7 -3
- package/plugin.json +1 -1
package/dist/capability.js
CHANGED
|
@@ -83,52 +83,51 @@
|
|
|
83
83
|
* (owner, observationId) idempotency is untouched: replays never consume a
|
|
84
84
|
* probe outcome.
|
|
85
85
|
*/
|
|
86
|
-
import {
|
|
87
|
-
import {
|
|
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";
|
|
86
|
+
import { join } from "node:path";
|
|
87
|
+
import { EXPERIMENTAL_PUBLIC_API_VERSION, } from "@agent-forge/plugin-sdk";
|
|
93
88
|
// 记忆公共契约的权威定义处已是 plugin-sdk(D-075 S4-4 第一批迁入);此处 re-export
|
|
94
89
|
// 维持宿主历史导出面(宿主第二批收口后由宿主 re-export SDK)。
|
|
95
90
|
export { MEMORY_RECALL_TOOL_NAME, } from "@agent-forge/plugin-sdk";
|
|
96
|
-
|
|
91
|
+
// 拆分战役 slice 1/3b: 征询持久化(enabling.ts 征询域)与向量常量/状态类型
|
|
92
|
+
// (vector-channel.ts)随模块迁出,经 re-export 维持 capability.ts 的历史
|
|
93
|
+
// 导出面(index.ts `export *` 与 entry.ts / inference-service-main.ts 的
|
|
94
|
+
// 既有 import 不变;vector-channel.ts 不进 index.ts,slice 2 先例)。
|
|
95
|
+
export { MEMORY_AGENT_DIR_CONFIG_RELATIVE_PATH, MEMORY_CONSENT_DECLINED_VALUE, writeMemoryConsentDeclined, writeMemorySettingsMode, } from "./memory/enabling.js";
|
|
96
|
+
// 拆分战役 slice 7: auto-recall 注入标记/免责行与 lessons 开关解析随钩子面迁入
|
|
97
|
+
// hooks.ts, 经 re-export 维持 capability.ts 的历史导出面 (index.ts `export *`
|
|
98
|
+
// 与既有 import 不变; hooks.ts 本体不进 index.ts, recall-pipeline.ts 先例)。
|
|
99
|
+
export { MEMORY_AUTO_RECALL_DEADLINE_MS, MEMORY_AUTO_RECALL_DISCLAIMER, MEMORY_AUTO_RECALL_MARKER, resolveMemoryLessons, } from "./memory/hooks.js";
|
|
100
|
+
// 拆分战役 slice 4: MEMORY_TOOLS_GUIDE 随召回管线迁入 recall-pipeline.ts, 经
|
|
101
|
+
// re-export 维持 capability.ts 的历史导出面 (宿主与测试的既有 import 不变;
|
|
102
|
+
// recall-pipeline.ts 本体不进 index.ts, vector-channel.ts slice 2 先例)。
|
|
103
|
+
export { MEMORY_TOOLS_GUIDE } from "./memory/recall-pipeline.js";
|
|
104
|
+
export { VECTOR_DB_DIRNAME, VECTOR_MODEL_CACHE_DIRNAME, VECTOR_RERANKER_MODEL_ID, } from "./memory/vector-channel.js";
|
|
105
|
+
// 拆分战役 slice 5: 档位自动升级阈值随写入面迁入 write-face.ts, 经 re-export
|
|
106
|
+
// 维持 capability.ts 的历史导出面。
|
|
107
|
+
export { MEMORY_TIER_UPGRADE_STORE_THRESHOLD } from "./memory/write-face.js";
|
|
97
108
|
import { createMemoryCandidateMachine } from "./memory/candidates.js";
|
|
109
|
+
import { createMemoryCommandFace } from "./memory/command-face.js";
|
|
98
110
|
import { createMemoryEgressPolicy } from "./memory/egress-policy.js";
|
|
99
|
-
import {
|
|
100
|
-
import { createDurableMemoryLedger } from "./memory/ledger.js";
|
|
111
|
+
import { createMemoryHooks } from "./memory/hooks.js";
|
|
112
|
+
import { createDurableMemoryLedger, scanOwnerLedgerMemoryIds } from "./memory/ledger.js";
|
|
101
113
|
import { createMemoryLifecycleManager } from "./memory/lifecycle.js";
|
|
102
|
-
import { createMemoryNetwork, createRelationDiffusionAdapter
|
|
114
|
+
import { createMemoryNetwork, createRelationDiffusionAdapter } from "./memory/memory-network.js";
|
|
103
115
|
import { createPreferenceDisambiguator } from "./memory/preference-disambiguator.js";
|
|
104
|
-
import {
|
|
105
|
-
import { createPreferenceResolver, } from "./memory/preference-resolver.js";
|
|
116
|
+
import { createPreferenceResolver } from "./memory/preference-resolver.js";
|
|
106
117
|
import { createMemoryPurgeGate, createMemoryReplicaRegistry } from "./memory/purge.js";
|
|
107
118
|
import { createMemoryPurgeJournal } from "./memory/purge-journal.js";
|
|
108
119
|
import { createMemoryRecallAgent } from "./memory/recall-agent.js";
|
|
109
120
|
import { createMemoryRecallIndex } from "./memory/recall-index.js";
|
|
121
|
+
import { createMemoryRecallPipeline } from "./memory/recall-pipeline.js";
|
|
122
|
+
import { sweepExpiredMemories } from "./memory/retention-sweep.js";
|
|
110
123
|
import { createMemoryScheduler } from "./memory/scheduler.js";
|
|
111
124
|
import { createMemorySchedulerApi } from "./memory/scheduler-api.js";
|
|
112
125
|
import { createFirstPartyRetentionRegistry, createMemoryStore } from "./memory/store.js";
|
|
113
|
-
import {
|
|
126
|
+
import { suiteMemoryDomainName } from "./memory/suite-memory.js";
|
|
114
127
|
import { MEMORY_CONTRACT_VERSION_V2 } from "./memory/transfer.js";
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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;
|
|
128
|
+
import { createMemoryVectorChannel } from "./memory/vector-channel.js";
|
|
129
|
+
import { createMemoryVectorProjection } from "./memory/vector-projection.js";
|
|
130
|
+
import { createMemoryWriteFace, TAG_FACET_NAMESPACE } from "./memory/write-face.js";
|
|
132
131
|
export const capabilityManifest = {
|
|
133
132
|
id: "agent-forge.builtin.memory",
|
|
134
133
|
version: "0.1.0",
|
|
@@ -136,158 +135,12 @@ export const capabilityManifest = {
|
|
|
136
135
|
requiredCapabilities: ["session", "events", "tools"],
|
|
137
136
|
provides: [{ id: "memory.scheduler", version: 1, kind: "service" }],
|
|
138
137
|
};
|
|
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
138
|
const isMemoryModeV1 = (value) => value === "off" || value === "light" || value === "full";
|
|
287
139
|
/**
|
|
288
140
|
* memory.mode 解析 (2.4d6 S1)。优先级: context 显式值 → 环境变量
|
|
289
141
|
* `AGENT_FORGE_MEMORY_MODE` → settings.json `memory.mode` (settingsMode 参, 持
|
|
290
|
-
* 久配置面) → 旧别名 `AGENT_FORGE_MEMORY_VECTOR` (兼容期) → 默认
|
|
142
|
+
* 久配置面) → 旧别名 `AGENT_FORGE_MEMORY_VECTOR` (兼容期) → 默认 light
|
|
143
|
+
* (2026-10-05 用户裁决翻转默认 off→light, 对齐主流编程助手开箱即用方向)。env 是
|
|
291
144
|
* 临时覆盖、settings 是持久配置 (env > 文件, 沿用本仓 suite 解析先例)。任何
|
|
292
145
|
* 非法配置值 fail-closed 为 off 并在结果中携带原值供调用方告警一次, 绝不静默
|
|
293
146
|
* 改档或意外下载。
|
|
@@ -321,29 +174,20 @@ export function resolveMemoryMode(explicitMode, env = process.env, settingsMode)
|
|
|
321
174
|
return { mode: legacy, legacyEnvUsed: true };
|
|
322
175
|
return { mode: "off", invalidEnvValue: legacy, legacyEnvUsed: true };
|
|
323
176
|
}
|
|
324
|
-
return { mode: "
|
|
177
|
+
return { mode: "light" };
|
|
325
178
|
}
|
|
326
|
-
const
|
|
179
|
+
const isVectorComponentsOverride = (override) => "embedProvider" in override && "vectorIndex" in override;
|
|
327
180
|
/**
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
*
|
|
181
|
+
* 默认档判定 (征询拒绝只抑制默认档): 显式值、env、settings、旧别名四层全部
|
|
182
|
+
* 未定档时, 模式来自出厂默认档 (light)。与 resolveMemoryMode 的优先级链同构,
|
|
183
|
+
* 不改 resolver 返回面。
|
|
331
184
|
*/
|
|
332
|
-
export function
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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" };
|
|
185
|
+
export function memoryModeSourceIsDefault(explicitMode, env, settingsMode) {
|
|
186
|
+
return (explicitMode === undefined &&
|
|
187
|
+
(env.AGENT_FORGE_MEMORY_MODE?.trim() ?? "") === "" &&
|
|
188
|
+
(env.AGENT_FORGE_MEMORY_VECTOR?.trim() ?? "") === "" &&
|
|
189
|
+
(settingsMode?.trim() ?? "") === "");
|
|
345
190
|
}
|
|
346
|
-
const isVectorComponentsOverride = (override) => "embedProvider" in override && "vectorIndex" in override;
|
|
347
191
|
function assertNonEmptyString(value, label) {
|
|
348
192
|
if (typeof value !== "string" || value.trim().length === 0)
|
|
349
193
|
throw new Error(`${label} must be a non-empty string`);
|
|
@@ -377,96 +221,22 @@ function isPlainObject(value) {
|
|
|
377
221
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
378
222
|
}
|
|
379
223
|
/**
|
|
380
|
-
*
|
|
381
|
-
*
|
|
382
|
-
*
|
|
383
|
-
*
|
|
384
|
-
*
|
|
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.
|
|
224
|
+
* Session protocol key of the suite binding entry (方案设计 §10.1). The value
|
|
225
|
+
* mirrors the authoritative `SUITE_SESSION_ENTRY_TYPE` in
|
|
226
|
+
* src/suites/suite-loader.ts — pinned by a deterministic test in
|
|
227
|
+
* test/capabilities-builtin.test.ts. Declared locally so the capability module
|
|
228
|
+
* graph stays free of the assembler's bundled-loader dependency chain.
|
|
427
229
|
*/
|
|
428
|
-
|
|
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
|
-
}
|
|
230
|
+
const SUITE_SESSION_ENTRY_TYPE = "suite";
|
|
455
231
|
/**
|
|
456
|
-
*
|
|
457
|
-
*
|
|
458
|
-
*
|
|
459
|
-
*
|
|
460
|
-
* 尾部, 保持模板可解析)。
|
|
232
|
+
* Owner (isolation principal) of every atom this capability writes. The
|
|
233
|
+
* Foundation owner is the store/facade isolation identity; this product ships
|
|
234
|
+
* one local user per agentDir, so one stable owner id partitions the ledger
|
|
235
|
+
* directory `<agentDir>/memory/<owner>/ledger-<suiteId|"legacy">.jsonl`.
|
|
461
236
|
*/
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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
|
-
}
|
|
237
|
+
export const BUILTIN_MEMORY_OWNER = "local-user";
|
|
238
|
+
/** Contract version written by this capability (@2 atoms carry suiteId). */
|
|
239
|
+
const MEMORY_CONTRACT_VERSION = MEMORY_CONTRACT_VERSION_V2;
|
|
470
240
|
/**
|
|
471
241
|
* Suite binding entry ids already warned about (统一修复轮终审 minor 3):
|
|
472
242
|
* resolveSessionDomain runs on EVERY memory tool call, so without this
|
|
@@ -522,431 +292,11 @@ function unwrapEgressStatement(statement) {
|
|
|
522
292
|
}
|
|
523
293
|
return statement;
|
|
524
294
|
}
|
|
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
295
|
function tagFacetsOf(atom) {
|
|
891
296
|
return atom.facets
|
|
892
297
|
.filter((facet) => facet.namespace === TAG_FACET_NAMESPACE && facet.key === "tag")
|
|
893
298
|
.map((facet) => facet.value);
|
|
894
299
|
}
|
|
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
300
|
/**
|
|
951
301
|
* Creates the builtin memory capability: four tools (memory_write,
|
|
952
302
|
* memory_recall, memory_list, memory_forget) over a per-session Foundation
|
|
@@ -1012,6 +362,7 @@ function createMemoryCapabilityHarness(api, context, override) {
|
|
|
1012
362
|
settleVectorWork: async () => { },
|
|
1013
363
|
vectorState: () => "off",
|
|
1014
364
|
enablingOutcome: async () => "ready",
|
|
365
|
+
teardown: () => { },
|
|
1015
366
|
};
|
|
1016
367
|
}
|
|
1017
368
|
const memoryMode = modeResolution.mode;
|
|
@@ -1043,14 +394,6 @@ function createMemoryCapabilityHarness(api, context, override) {
|
|
|
1043
394
|
// observationId of this session marks its sequence as taken — the store rejects
|
|
1044
395
|
// a regenerated memoryId (canonical atoms are immutable), which would fail every
|
|
1045
396
|
// 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
397
|
const domainLedgers = new Map();
|
|
1055
398
|
const resolveDomain = () => {
|
|
1056
399
|
const domain = resolveSessionDomain(api);
|
|
@@ -1078,9 +421,9 @@ function createMemoryCapabilityHarness(api, context, override) {
|
|
|
1078
421
|
// write-sequence floor before any new write can regenerate their ids.
|
|
1079
422
|
const replay = ledger.load();
|
|
1080
423
|
for (const atom of replay.atoms) {
|
|
1081
|
-
const persistedSequence = writeSequenceOf(atom.observationId);
|
|
1082
|
-
if (persistedSequence !== undefined
|
|
1083
|
-
|
|
424
|
+
const persistedSequence = writeFace.writeSequenceOf(atom.observationId);
|
|
425
|
+
if (persistedSequence !== undefined)
|
|
426
|
+
writeFace.advanceWriteSequenceFloor(persistedSequence);
|
|
1084
427
|
try {
|
|
1085
428
|
store.commit(atom);
|
|
1086
429
|
}
|
|
@@ -1104,7 +447,34 @@ function createMemoryCapabilityHarness(api, context, override) {
|
|
|
1104
447
|
domainLedgers.set(domainName, ledger);
|
|
1105
448
|
// 启动对账: 首个域的 ledger 重放完成后一次性触发 (fire-and-forget,
|
|
1106
449
|
// 不阻塞本工具调用; 失败只影响向量通道, 不影响锚点通道)。
|
|
1107
|
-
|
|
450
|
+
vectorProjection.reconcileOnce();
|
|
451
|
+
// 机会式保留清扫 (M5, 记忆系统设计 §3.3): 首载重放完成后对本域已过期
|
|
452
|
+
// 原子执行一次受控物理清理 (retention-expiry 授权, 最旧优先 ≤200 条,
|
|
453
|
+
// ledger 重写 + 向量行删除 + store 逐出)。同步调用且 sweep 自吞错——
|
|
454
|
+
// 装载绝不因清扫失败失败; 非干净结果只记一条有界 warn。
|
|
455
|
+
const sweepLedger = ledger;
|
|
456
|
+
const sweep = sweepExpiredMemories({
|
|
457
|
+
store,
|
|
458
|
+
purgeGate,
|
|
459
|
+
purgeJournal,
|
|
460
|
+
purgeIndexReplica: vectorProjection.purgeMemories,
|
|
461
|
+
purgeCanonicalReplica: (memoryIds) => {
|
|
462
|
+
const purged = new Set(memoryIds);
|
|
463
|
+
sweepLedger.rewrite(sweepLedger.atomsSnapshot().filter((atom) => !purged.has(atom.memoryId)));
|
|
464
|
+
for (const id of memoryIds)
|
|
465
|
+
store.evict(id);
|
|
466
|
+
},
|
|
467
|
+
authorizedBy: `${capabilityManifest.id}:retention-sweep`,
|
|
468
|
+
now,
|
|
469
|
+
}, { owner: BUILTIN_MEMORY_OWNER, domain });
|
|
470
|
+
if (sweep.status === "failed" || sweep.status === "purge_eligible" || sweep.reason !== undefined) {
|
|
471
|
+
api.logger?.warn("memory retention sweep did not finish clean; expired atoms stay purge-eligible and are retried on the next domain load", {
|
|
472
|
+
domain: domainName,
|
|
473
|
+
status: sweep.status,
|
|
474
|
+
...(sweep.reason === undefined ? {} : { reason: sweep.reason }),
|
|
475
|
+
...(sweep.failedReplicas === undefined ? {} : { failedReplicas: sweep.failedReplicas }),
|
|
476
|
+
});
|
|
477
|
+
}
|
|
1108
478
|
}
|
|
1109
479
|
return { domain, domainName, ledger };
|
|
1110
480
|
};
|
|
@@ -1166,7 +536,7 @@ function createMemoryCapabilityHarness(api, context, override) {
|
|
|
1166
536
|
});
|
|
1167
537
|
// 向量投影副本 (混合检索工程化, D-035): 恒注册 — purge 批次以向量索引确认为
|
|
1168
538
|
// 副本契约的一部分; 通道 off/disabled 时 index 分派为 no-op (索引不存在,
|
|
1169
|
-
// 无行可清), 行为见
|
|
539
|
+
// 无行可清), 行为见 vectorProjection.purgeMemories。
|
|
1170
540
|
replicaRegistry.register({
|
|
1171
541
|
replicaId: "memory-vector-index",
|
|
1172
542
|
kind: "index",
|
|
@@ -1209,1357 +579,195 @@ function createMemoryCapabilityHarness(api, context, override) {
|
|
|
1209
579
|
const memoryNetwork = createMemoryNetwork({
|
|
1210
580
|
adapters: [createRelationDiffusionAdapter({ maxHops: 1 })],
|
|
1211
581
|
});
|
|
1212
|
-
// ----
|
|
1213
|
-
//
|
|
1214
|
-
//
|
|
1215
|
-
//
|
|
1216
|
-
//
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
582
|
+
// ---- 语义召回通道状态机 (2.4d6 S1/S2; 拆分战役 slice 3b) ----
|
|
583
|
+
// vectorEnsure 状态转换、装配分支 (服务/本地/下载/征询门) 与 dispose 闩锁
|
|
584
|
+
// 已迁至 memory/vector-channel.ts: 共享闭包状态收拢为 channel 对象, 经 deps
|
|
585
|
+
// 显式注入, 降级/投影经下方 vectorProjection (slice 3a) 上报。以下调用点
|
|
586
|
+
// 全部薄委托。
|
|
587
|
+
const vectorChannelCtl = createMemoryVectorChannel({
|
|
588
|
+
agentDir: context.agentDir,
|
|
589
|
+
memoryMode,
|
|
590
|
+
logger: api.logger,
|
|
591
|
+
hostOnnxRuntimeEntryPath: context.hostOnnxRuntimeEntryPath,
|
|
592
|
+
hostLancedbEntryPath: context.hostLancedbEntryPath,
|
|
593
|
+
hostTransformersEntryPath: context.hostTransformersEntryPath,
|
|
594
|
+
inferenceServicePort: context.inferenceService?.port,
|
|
595
|
+
inferenceServiceSpawnImpl: context.inferenceService?.spawnImpl,
|
|
596
|
+
inferenceServiceModelsInPlace: context.inferenceService?.modelsInPlace,
|
|
597
|
+
enablingOverride,
|
|
598
|
+
injectedComponents,
|
|
599
|
+
// vectorProjection 声明在本对象之后; 委托箭头只在 download 运行期触达,
|
|
600
|
+
// 与下方 supersededMemoryIds 依赖同构 (构造期不执行)。
|
|
601
|
+
resetReconcile: () => vectorProjection.resetReconcile(),
|
|
602
|
+
reconcileOnce: () => vectorProjection.reconcileOnce(),
|
|
1227
603
|
});
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
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
|
-
};
|
|
604
|
+
// 写入侧向量投影管线 (拆分战役 slice 3a): 串行队列、upsert/purge 埋点与启动
|
|
605
|
+
// 对账搬至 memory/vector-projection.ts; 状态机归 memory/vector-channel.ts
|
|
606
|
+
// (slice 3b), 投影经只读 deps 使用, degraded 转换经 markDegraded 回调上报。
|
|
607
|
+
const vectorProjection = createMemoryVectorProjection({
|
|
608
|
+
vectorEnabled: () => vectorChannelCtl.enabled(),
|
|
609
|
+
ensureChannel: () => vectorChannelCtl.ensure(),
|
|
610
|
+
readyChannel: () => vectorChannelCtl.readyChannel(),
|
|
611
|
+
isChannelActive: () => {
|
|
612
|
+
const channelState = vectorChannelCtl.rawState();
|
|
613
|
+
return channelState === "ready" || channelState === "degraded";
|
|
1744
614
|
},
|
|
615
|
+
isDisposed: () => vectorChannelCtl.isDisposed(),
|
|
616
|
+
// 反向清理的跨进程守卫 (2026-10-07 终审): 只读扫描 owner 全部域账本的
|
|
617
|
+
// memoryId 并集 (盘面扫描, 不触碰进程内 ledger 快照)。
|
|
618
|
+
refreshCanonicalMemoryIds: () => scanOwnerLedgerMemoryIds({ agentDir: context.agentDir, owner: BUILTIN_MEMORY_OWNER }),
|
|
619
|
+
markDegraded: () => vectorChannelCtl.markDegraded(),
|
|
620
|
+
logger: api.logger,
|
|
621
|
+
owner: BUILTIN_MEMORY_OWNER,
|
|
622
|
+
store,
|
|
623
|
+
// writeFace 声明在本对象之后; 委托箭头只在对账运行期触达 (构造期不执行)。
|
|
624
|
+
supersededMemoryIds: () => writeFace.supersededMemoryIds(),
|
|
625
|
+
statementOf,
|
|
626
|
+
tagFacetsOf,
|
|
1745
627
|
});
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
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
|
-
});
|
|
628
|
+
// ---- 写入面 (拆分战役 slice 5) ----
|
|
629
|
+
// memory_write 工具、语义合并探测、写入 atom 构造与写后自动升档旁路已迁至
|
|
630
|
+
// memory/write-face.ts: 写序计数/探测降级告警闩/升档闩归 write-face 工厂
|
|
631
|
+
// 所有, 经 deps 显式注入; 状态机与写侧投影经对象只读消费。supersededMemoryIds
|
|
632
|
+
// 供上方 vectorProjection 的对账 deps 消费 (构造期不执行, 依赖同构)。
|
|
633
|
+
const writeFace = createMemoryWriteFace({
|
|
634
|
+
logger: api.logger,
|
|
635
|
+
registerTool: (definition) => api.registerTool(definition),
|
|
636
|
+
publish: api.publish,
|
|
637
|
+
owner: BUILTIN_MEMORY_OWNER,
|
|
638
|
+
sessionId,
|
|
639
|
+
manifest: capabilityManifest,
|
|
640
|
+
contractVersion: MEMORY_CONTRACT_VERSION,
|
|
641
|
+
memoryMode,
|
|
642
|
+
agentDir: context.agentDir,
|
|
643
|
+
now,
|
|
644
|
+
store,
|
|
645
|
+
schedulerApi,
|
|
646
|
+
vectorProjection,
|
|
647
|
+
vectorChannelCtl,
|
|
648
|
+
resolveDomain,
|
|
649
|
+
assertNonEmptyString,
|
|
650
|
+
memoryToolInput,
|
|
651
|
+
identityHash,
|
|
652
|
+
statementOf,
|
|
653
|
+
tagFacetsOf,
|
|
2265
654
|
});
|
|
2266
|
-
// ----
|
|
2267
|
-
//
|
|
2268
|
-
//
|
|
2269
|
-
//
|
|
2270
|
-
//
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
api.logger
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
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
|
-
},
|
|
655
|
+
// ---- 语义召回管线 (拆分战役 slice 4) ----
|
|
656
|
+
// 双通道查询、RRF 融合、egress 门、语义召回与召回工具已迁至
|
|
657
|
+
// memory/recall-pipeline.ts: 状态机经 vectorChannelCtl 只读面消费
|
|
658
|
+
// (warming/ensure/征询/失败/degraded), 写侧投影的读侧等待经 settleProjection,
|
|
659
|
+
// capability 模块级纯函数经 deps 注入避免双头定义。recallTool 进 registrations,
|
|
660
|
+
// recallExecute 同时被钩子面 (slice 7) 直连。
|
|
661
|
+
const recallPipeline = createMemoryRecallPipeline({
|
|
662
|
+
logger: api.logger,
|
|
663
|
+
owner: BUILTIN_MEMORY_OWNER,
|
|
664
|
+
sessionId,
|
|
665
|
+
store,
|
|
666
|
+
resolveDomain,
|
|
667
|
+
egressPolicy,
|
|
668
|
+
preferenceResolver,
|
|
669
|
+
preferenceDisambiguator,
|
|
670
|
+
memoryNetwork,
|
|
671
|
+
warming: () => vectorChannelCtl.warming(),
|
|
672
|
+
ensureChannel: () => vectorChannelCtl.ensure(),
|
|
673
|
+
declined: () => vectorChannelCtl.declined(),
|
|
674
|
+
consentAsk: () => vectorChannelCtl.consentAsk(),
|
|
675
|
+
failure: () => vectorChannelCtl.failure(),
|
|
676
|
+
rawState: () => vectorChannelCtl.rawState(),
|
|
677
|
+
markDegraded: () => vectorChannelCtl.markDegraded(),
|
|
678
|
+
recoverDegraded: () => vectorChannelCtl.recoverDegraded(),
|
|
679
|
+
settleProjection: () => vectorProjection.settle(),
|
|
680
|
+
memoryToolInput,
|
|
681
|
+
assertNonEmptyString,
|
|
682
|
+
isPlainObject,
|
|
683
|
+
identityHash,
|
|
684
|
+
statementOf,
|
|
685
|
+
unwrapEgressStatement,
|
|
686
|
+
registerTool: (definition) => api.registerTool(definition),
|
|
2407
687
|
});
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
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
|
-
},
|
|
688
|
+
// ---- 命令面 (拆分战役 slice 6) ----
|
|
689
|
+
// memory_list/forget 工具、/memory 与 /promote 命令、memory_enable 征询执行
|
|
690
|
+
// 面与 memory.scheduler 声明已迁至 memory/command-face.ts: 列表/删除/晋升/
|
|
691
|
+
// 征询的命令执行面经 deps 显式注入, decline/download 仍薄委托 vectorChannelCtl
|
|
692
|
+
// (slice 3b 先例); 升档声明状态经 writeFace 的只读面消费。
|
|
693
|
+
const commandFace = createMemoryCommandFace({
|
|
694
|
+
registerTool: (definition) => api.registerTool(definition),
|
|
695
|
+
registerCommand: (definition) => api.registerCommand(definition),
|
|
696
|
+
publish: api.publish,
|
|
697
|
+
capabilities: api.capabilities,
|
|
698
|
+
owner: BUILTIN_MEMORY_OWNER,
|
|
699
|
+
sessionId,
|
|
700
|
+
manifest: capabilityManifest,
|
|
701
|
+
agentDir: context.agentDir,
|
|
702
|
+
memoryMode,
|
|
703
|
+
now,
|
|
704
|
+
store,
|
|
705
|
+
purgeGate,
|
|
706
|
+
purgeJournal,
|
|
707
|
+
vectorProjection,
|
|
708
|
+
vectorChannelCtl,
|
|
709
|
+
resolveDomain,
|
|
710
|
+
assertNonEmptyString,
|
|
711
|
+
memoryToolInput,
|
|
712
|
+
statementOf,
|
|
713
|
+
unwrapEgressStatement,
|
|
714
|
+
tierUpgradePending: writeFace.tierUpgradePending,
|
|
715
|
+
clearTierUpgrade: writeFace.clearTierUpgrade,
|
|
2457
716
|
});
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
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
|
-
},
|
|
717
|
+
// ---- 注入钩子 (拆分战役 slice 7) ----
|
|
718
|
+
// auto-recall、assistant 偏好卡与 bash 失败教训沉淀已迁至 memory/hooks.ts:
|
|
719
|
+
// 钩子经 api 注册面订阅 (loop hook + lifecycle observer), 召回执行闭包、写入
|
|
720
|
+
// atom 构造与升档旁路经 deps 注入, 教训写入与工具面同序 (先账本后可见)。
|
|
721
|
+
const hooks = createMemoryHooks({
|
|
722
|
+
logger: api.logger,
|
|
723
|
+
registerLoopHook: api.registerLoopHook,
|
|
724
|
+
session: api.session,
|
|
725
|
+
lifecycle: api.lifecycle,
|
|
726
|
+
owner: BUILTIN_MEMORY_OWNER,
|
|
727
|
+
agentDir: context.agentDir,
|
|
728
|
+
now,
|
|
729
|
+
manifest: capabilityManifest,
|
|
730
|
+
isPlainObject,
|
|
731
|
+
resolveSessionDomain: () => resolveSessionDomain(api),
|
|
732
|
+
lessonsExplicit: context.memory?.lessons,
|
|
733
|
+
recallExecute: recallPipeline.recallExecute,
|
|
734
|
+
vectorChannelCtl,
|
|
735
|
+
resolveDomain,
|
|
736
|
+
store,
|
|
737
|
+
buildWriteAtom: writeFace.buildWriteAtom,
|
|
738
|
+
vectorProjection,
|
|
739
|
+
maybeTierUpgradeAfterWrite: writeFace.maybeTierUpgradeAfterWrite,
|
|
2524
740
|
});
|
|
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
741
|
const registrations = [
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
742
|
+
// 首位 = 宿主反向 dispose 时的末位: 先拆工具/命令/钩子等消费者, 最后关
|
|
743
|
+
// 底层通道, 避免消费者在通道已关后仍触发向量操作报错。释放本体
|
|
744
|
+
// (capabilityTeardown, slice 3b) 在 memory/vector-channel.ts。
|
|
745
|
+
{
|
|
746
|
+
id: "memory:capability-teardown",
|
|
747
|
+
kind: "capability-teardown",
|
|
748
|
+
dispose: () => vectorChannelCtl.dispose(),
|
|
749
|
+
},
|
|
750
|
+
writeFace.writeTool,
|
|
751
|
+
recallPipeline.recallTool,
|
|
752
|
+
commandFace.listTool,
|
|
753
|
+
commandFace.forgetTool,
|
|
754
|
+
commandFace.enableTool,
|
|
755
|
+
commandFace.memoryCommand,
|
|
756
|
+
commandFace.promoteCommand,
|
|
757
|
+
commandFace.schedulerCapability,
|
|
758
|
+
hooks.autoRecallHook,
|
|
759
|
+
hooks.preferenceCardHook,
|
|
760
|
+
...hooks.lessonRegistrations,
|
|
2549
761
|
];
|
|
2550
762
|
return {
|
|
2551
763
|
registrations,
|
|
2552
764
|
settleVectorWork: async () => {
|
|
2553
|
-
await
|
|
765
|
+
await vectorProjection.settle();
|
|
2554
766
|
},
|
|
2555
|
-
vectorState: () =>
|
|
767
|
+
vectorState: () => vectorChannelCtl.state(),
|
|
2556
768
|
// 触发惰性 enabling (已注入组件时已就绪) 并等待其落定: ready 或结构化失败。
|
|
2557
|
-
enablingOutcome:
|
|
2558
|
-
|
|
2559
|
-
void vectorEnsure();
|
|
2560
|
-
await enablingSettledPromise;
|
|
2561
|
-
return enablingFailure ?? "ready";
|
|
2562
|
-
},
|
|
769
|
+
enablingOutcome: () => vectorChannelCtl.enablingOutcome(),
|
|
770
|
+
teardown: () => vectorChannelCtl.dispose(),
|
|
2563
771
|
};
|
|
2564
772
|
}
|
|
2565
773
|
//# sourceMappingURL=capability.js.map
|