@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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inference-service-main.d.ts","sourceRoot":"","sources":["../../src/memory/inference-service-main.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,yBAAyB,CAAC;AAE7E,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,yBAAyB,CAAC;AAG5E,OAAO,EAEN,KAAK,wBAAwB,EAC7B,KAAK,uBAAuB,EAC5B,MAAM,uBAAuB,CAAC;AAG/B,0FAAyC;AACzC,eAAO,MAAM,wCAAwC,QAAgB,CAAC;AAEtE,MAAM,WAAW,6BAA6B;IAC7C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAAC;IAChC,+GAAmE;IACnE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,yDAAmD;IACnD,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAC9B,sFAAsC;IACtC,QAAQ,CAAC,OAAO,CAAC,EAAE,wBAAwB,CAAC;IAC5C,QAAQ,CAAC,gBAAgB,CAAC,EAAE;QAAE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1F,QAAQ,CAAC,SAAS,CAAC,EAAE;QACpB,QAAQ,CAAC,SAAS,CAAC,EAAE,6BAA6B,CAAC;QACnD,QAAQ,CAAC,QAAQ,CAAC,EAAE,4BAA4B,CAAC;KACjD,CAAC;IACF,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACtC;AAED;;;GAGG;AACH,wBAAsB,yBAAyB,CAC9C,OAAO,EAAE,6BAA6B,GACpC,OAAO,CAAC,uBAAuB,CAAC,CAoElC","sourcesContent":["/**\n * 记忆共享推理服务 — 服务进程主函数 (记忆共享推理服务设计 §3.1/§3.6, Slice B1)。\n *\n * `agent-forge --memory-inference` 进程入口的执行体: 装配真实推理后端\n * (transformers embedding + reranker + LanceDB 向量索引 — 与会话进程本地装配\n * 完全同一组构件与模型缓存路径, 设计 §3.6「下载与装配只发生一次」的装配侧),\n * 然后以固定默认端口常驻服务 (设计 §3.5: 空闲自杀、信号优雅停机)。\n *\n * consent 链完整性 (设计 §3.6): 服务进程**不做 consent 判定、永不自主下载** —\n * 拉起方 (B2 接线: 会话侧在用户同意、模型经预热下载在位后) 才 spawn 本进程;\n * 启动时模型文件缺失即结构化退出 (拉起方把它当服务不可用, 按裁决 A 呈现)。\n * 下载只发生在会话进程的既有预热流程 (memory_enable download), 行为不变。\n *\n * 测试缝: `backend` 直接注入完整后端 (跳过真实装配); `loadImpls`/`moduleEntryPaths`\n * 透传给真实装配 (与 capability 的 enabling 缝同构)。\n */\nimport { join } from \"node:path\";\nimport { VECTOR_DB_DIRNAME, VECTOR_MODEL_CACHE_DIRNAME, VECTOR_RERANKER_MODEL_ID } from \"../capability.ts\";\nimport type { TransformersEmbeddingLoadImpl } from \"./embedding-provider.ts\";\nimport { createTransformersEmbeddingProvider } from \"./embedding-provider.ts\";\nimport type { TransformersRerankerLoadImpl } from \"./embedding-reranker.ts\";\nimport { createTransformersReranker } from \"./embedding-reranker.ts\";\nimport { MODEL_MIN_WEIGHT_BYTES, modelFilesCheck, RERANKER_MIN_WEIGHT_BYTES, vectorPresetForMode } from \"./enabling.ts\";\nimport {\n\tcreateMemoryInferenceServer,\n\ttype MemoryInferenceBackendV1,\n\ttype MemoryInferenceServerV1,\n} from \"./inference-server.ts\";\nimport { createMemoryVectorIndex } from \"./vector-index.ts\";\n\n/** 服务空闲自杀阈值出厂默认 (设计 §3.5; 登记产品默认值清单)。 */\nexport const MEMORY_INFERENCE_SERVICE_IDLE_TIMEOUT_MS = 5 * 60 * 1000;\n\nexport interface MemoryInferenceServiceOptions {\n\treadonly agentDir: string;\n\treadonly mode: \"light\" | \"full\";\n\t/** 缺省固定默认端口 (单例互斥); env 覆盖由 createMemoryInferenceServer 端口参数承担。 */\n\treadonly port?: number;\n\t/** 缺省 MEMORY_INFERENCE_SERVICE_IDLE_TIMEOUT_MS。 */\n\treadonly idleTimeoutMs?: number;\n\treadonly signal?: AbortSignal;\n\t/** 测试缝: 直接给定后端, 跳过真实装配 (模型检查一并跳过)。 */\n\treadonly backend?: MemoryInferenceBackendV1;\n\treadonly moduleEntryPaths?: { readonly lancedb?: string; readonly transformers?: string };\n\treadonly loadImpls?: {\n\t\treadonly embedding?: TransformersEmbeddingLoadImpl;\n\t\treadonly reranker?: TransformersRerankerLoadImpl;\n\t};\n\treadonly log?: (line: string) => void;\n}\n\n/**\n * 装配并启动共享推理服务。真实装配路径的失败 (模型缺失) 抛结构化错误 —\n * 进程入口将其转为一行 stderr + 非零退出码; 已监听后返回 server 句柄。\n */\nexport async function runMemoryInferenceService(\n\toptions: MemoryInferenceServiceOptions,\n): Promise<MemoryInferenceServerV1> {\n\tconst log = options.log ?? (() => {});\n\tconst preset = vectorPresetForMode(options.mode);\n\tconst modelCacheDir = join(options.agentDir, \"memory\", VECTOR_MODEL_CACHE_DIRNAME);\n\n\tlet backend = options.backend;\n\tif (backend === undefined) {\n\t\t// consent 链守门: 模型缺失即退出, 服务进程绝不触发下载 (拉起方保证\n\t\t// \"已同意且已下载\"; 缺失说明拉起时机错误或缓存被清)。\n\t\tconst missingEmbedding = modelFilesCheck(\n\t\t\tmodelCacheDir,\n\t\t\tpreset.embeddingModelId,\n\t\t\tMODEL_MIN_WEIGHT_BYTES[options.mode],\n\t\t);\n\t\tconst missingReranker = modelFilesCheck(modelCacheDir, VECTOR_RERANKER_MODEL_ID, RERANKER_MIN_WEIGHT_BYTES);\n\t\tconst missing = missingEmbedding ?? missingReranker;\n\t\tif (missing !== undefined) {\n\t\t\tthrow new Error(\n\t\t\t\t`memory inference service refuses to start: ${missing} — the service never downloads models itself; ` +\n\t\t\t\t\t\"the spawner must start it after the session-side preheat has the models in place\",\n\t\t\t);\n\t\t}\n\t\t// remoteHosts 不传 (provider 缺省序列): 服务的前置条件是模型已在位,\n\t\t// 加载命中本地缓存、远端不可达也不影响; 显式空数组会被 provider 当\n\t\t// \"零镜像可用\", 首次加载直接报 no remote hosts (2026-10-06 live E2E)。\n\t\tconst embedProvider = createTransformersEmbeddingProvider({\n\t\t\tmodelId: preset.embeddingModelId,\n\t\t\tdimensions: preset.dimensions,\n\t\t\tcacheDir: modelCacheDir,\n\t\t\t...(options.moduleEntryPaths?.transformers === undefined\n\t\t\t\t? {}\n\t\t\t\t: { moduleEntryPath: options.moduleEntryPaths.transformers }),\n\t\t\t...(options.loadImpls?.embedding === undefined ? {} : { loadImpl: options.loadImpls.embedding }),\n\t\t});\n\t\tconst reranker = createTransformersReranker({\n\t\t\tmodelId: VECTOR_RERANKER_MODEL_ID,\n\t\t\tcacheDir: modelCacheDir,\n\t\t\t...(options.moduleEntryPaths?.transformers === undefined\n\t\t\t\t? {}\n\t\t\t\t: { moduleEntryPath: options.moduleEntryPaths.transformers }),\n\t\t\t...(options.loadImpls?.reranker === undefined ? {} : { loadImpl: options.loadImpls.reranker }),\n\t\t});\n\t\tconst vectorIndex = await createMemoryVectorIndex({\n\t\t\tdbPath: join(options.agentDir, \"memory\", VECTOR_DB_DIRNAME),\n\t\t\tdimensions: preset.dimensions,\n\t\t\t...(options.moduleEntryPaths?.lancedb === undefined\n\t\t\t\t? {}\n\t\t\t\t: { moduleEntryPath: options.moduleEntryPaths.lancedb }),\n\t\t});\n\t\tbackend = { embedProvider, reranker, vectorIndex };\n\t}\n\n\tconst server = await createMemoryInferenceServer({\n\t\tbackend,\n\t\t// agentDir 身份上报 (2026-10-06 串线修复): 服务进程的 agent-dir 由 spawn\n\t\t// env 解析而来, 经 hello 回报 — 会话装配据此拒绝跨 agent-dir 串线。\n\t\tagentDir: options.agentDir,\n\t\t...(options.port === undefined ? {} : { port: options.port }),\n\t\tidleTimeoutMs: options.idleTimeoutMs ?? MEMORY_INFERENCE_SERVICE_IDLE_TIMEOUT_MS,\n\t\t...(options.signal === undefined ? {} : { signal: options.signal }),\n\t\tonError: (error, context) => {\n\t\t\tlog(`memory-inference: ${context}: ${error.message}`);\n\t\t},\n\t});\n\tlog(\n\t\t`memory-inference: serving on ${server.host}:${server.port} (mode=${options.mode}, idle-exit=${options.idleTimeoutMs ?? MEMORY_INFERENCE_SERVICE_IDLE_TIMEOUT_MS}ms)`,\n\t);\n\treturn server;\n}\n"]}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 记忆共享推理服务 — 服务进程主函数 (记忆共享推理服务设计 §3.1/§3.6, Slice B1)。
|
|
3
|
+
*
|
|
4
|
+
* `agent-forge --memory-inference` 进程入口的执行体: 装配真实推理后端
|
|
5
|
+
* (transformers embedding + reranker + LanceDB 向量索引 — 与会话进程本地装配
|
|
6
|
+
* 完全同一组构件与模型缓存路径, 设计 §3.6「下载与装配只发生一次」的装配侧),
|
|
7
|
+
* 然后以固定默认端口常驻服务 (设计 §3.5: 空闲自杀、信号优雅停机)。
|
|
8
|
+
*
|
|
9
|
+
* consent 链完整性 (设计 §3.6): 服务进程**不做 consent 判定、永不自主下载** —
|
|
10
|
+
* 拉起方 (B2 接线: 会话侧在用户同意、模型经预热下载在位后) 才 spawn 本进程;
|
|
11
|
+
* 启动时模型文件缺失即结构化退出 (拉起方把它当服务不可用, 按裁决 A 呈现)。
|
|
12
|
+
* 下载只发生在会话进程的既有预热流程 (memory_enable download), 行为不变。
|
|
13
|
+
*
|
|
14
|
+
* 测试缝: `backend` 直接注入完整后端 (跳过真实装配); `loadImpls`/`moduleEntryPaths`
|
|
15
|
+
* 透传给真实装配 (与 capability 的 enabling 缝同构)。
|
|
16
|
+
*/
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { VECTOR_DB_DIRNAME, VECTOR_MODEL_CACHE_DIRNAME, VECTOR_RERANKER_MODEL_ID } from "../capability.js";
|
|
19
|
+
import { createTransformersEmbeddingProvider } from "./embedding-provider.js";
|
|
20
|
+
import { createTransformersReranker } from "./embedding-reranker.js";
|
|
21
|
+
import { MODEL_MIN_WEIGHT_BYTES, modelFilesCheck, RERANKER_MIN_WEIGHT_BYTES, vectorPresetForMode } from "./enabling.js";
|
|
22
|
+
import { createMemoryInferenceServer, } from "./inference-server.js";
|
|
23
|
+
import { createMemoryVectorIndex } from "./vector-index.js";
|
|
24
|
+
/** 服务空闲自杀阈值出厂默认 (设计 §3.5; 登记产品默认值清单)。 */
|
|
25
|
+
export const MEMORY_INFERENCE_SERVICE_IDLE_TIMEOUT_MS = 5 * 60 * 1000;
|
|
26
|
+
/**
|
|
27
|
+
* 装配并启动共享推理服务。真实装配路径的失败 (模型缺失) 抛结构化错误 —
|
|
28
|
+
* 进程入口将其转为一行 stderr + 非零退出码; 已监听后返回 server 句柄。
|
|
29
|
+
*/
|
|
30
|
+
export async function runMemoryInferenceService(options) {
|
|
31
|
+
const log = options.log ?? (() => { });
|
|
32
|
+
const preset = vectorPresetForMode(options.mode);
|
|
33
|
+
const modelCacheDir = join(options.agentDir, "memory", VECTOR_MODEL_CACHE_DIRNAME);
|
|
34
|
+
let backend = options.backend;
|
|
35
|
+
if (backend === undefined) {
|
|
36
|
+
// consent 链守门: 模型缺失即退出, 服务进程绝不触发下载 (拉起方保证
|
|
37
|
+
// "已同意且已下载"; 缺失说明拉起时机错误或缓存被清)。
|
|
38
|
+
const missingEmbedding = modelFilesCheck(modelCacheDir, preset.embeddingModelId, MODEL_MIN_WEIGHT_BYTES[options.mode]);
|
|
39
|
+
const missingReranker = modelFilesCheck(modelCacheDir, VECTOR_RERANKER_MODEL_ID, RERANKER_MIN_WEIGHT_BYTES);
|
|
40
|
+
const missing = missingEmbedding ?? missingReranker;
|
|
41
|
+
if (missing !== undefined) {
|
|
42
|
+
throw new Error(`memory inference service refuses to start: ${missing} — the service never downloads models itself; ` +
|
|
43
|
+
"the spawner must start it after the session-side preheat has the models in place");
|
|
44
|
+
}
|
|
45
|
+
// remoteHosts 不传 (provider 缺省序列): 服务的前置条件是模型已在位,
|
|
46
|
+
// 加载命中本地缓存、远端不可达也不影响; 显式空数组会被 provider 当
|
|
47
|
+
// "零镜像可用", 首次加载直接报 no remote hosts (2026-10-06 live E2E)。
|
|
48
|
+
const embedProvider = createTransformersEmbeddingProvider({
|
|
49
|
+
modelId: preset.embeddingModelId,
|
|
50
|
+
dimensions: preset.dimensions,
|
|
51
|
+
cacheDir: modelCacheDir,
|
|
52
|
+
...(options.moduleEntryPaths?.transformers === undefined
|
|
53
|
+
? {}
|
|
54
|
+
: { moduleEntryPath: options.moduleEntryPaths.transformers }),
|
|
55
|
+
...(options.loadImpls?.embedding === undefined ? {} : { loadImpl: options.loadImpls.embedding }),
|
|
56
|
+
});
|
|
57
|
+
const reranker = createTransformersReranker({
|
|
58
|
+
modelId: VECTOR_RERANKER_MODEL_ID,
|
|
59
|
+
cacheDir: modelCacheDir,
|
|
60
|
+
...(options.moduleEntryPaths?.transformers === undefined
|
|
61
|
+
? {}
|
|
62
|
+
: { moduleEntryPath: options.moduleEntryPaths.transformers }),
|
|
63
|
+
...(options.loadImpls?.reranker === undefined ? {} : { loadImpl: options.loadImpls.reranker }),
|
|
64
|
+
});
|
|
65
|
+
const vectorIndex = await createMemoryVectorIndex({
|
|
66
|
+
dbPath: join(options.agentDir, "memory", VECTOR_DB_DIRNAME),
|
|
67
|
+
dimensions: preset.dimensions,
|
|
68
|
+
...(options.moduleEntryPaths?.lancedb === undefined
|
|
69
|
+
? {}
|
|
70
|
+
: { moduleEntryPath: options.moduleEntryPaths.lancedb }),
|
|
71
|
+
});
|
|
72
|
+
backend = { embedProvider, reranker, vectorIndex };
|
|
73
|
+
}
|
|
74
|
+
const server = await createMemoryInferenceServer({
|
|
75
|
+
backend,
|
|
76
|
+
// agentDir 身份上报 (2026-10-06 串线修复): 服务进程的 agent-dir 由 spawn
|
|
77
|
+
// env 解析而来, 经 hello 回报 — 会话装配据此拒绝跨 agent-dir 串线。
|
|
78
|
+
agentDir: options.agentDir,
|
|
79
|
+
...(options.port === undefined ? {} : { port: options.port }),
|
|
80
|
+
idleTimeoutMs: options.idleTimeoutMs ?? MEMORY_INFERENCE_SERVICE_IDLE_TIMEOUT_MS,
|
|
81
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
82
|
+
onError: (error, context) => {
|
|
83
|
+
log(`memory-inference: ${context}: ${error.message}`);
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
log(`memory-inference: serving on ${server.host}:${server.port} (mode=${options.mode}, idle-exit=${options.idleTimeoutMs ?? MEMORY_INFERENCE_SERVICE_IDLE_TIMEOUT_MS}ms)`);
|
|
87
|
+
return server;
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=inference-service-main.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inference-service-main.js","sourceRoot":"","sources":["../../src/memory/inference-service-main.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * 记忆共享推理服务 — 服务进程主函数 (记忆共享推理服务设计 §3.1/§3.6, Slice B1)。\n *\n * `agent-forge --memory-inference` 进程入口的执行体: 装配真实推理后端\n * (transformers embedding + reranker + LanceDB 向量索引 — 与会话进程本地装配\n * 完全同一组构件与模型缓存路径, 设计 §3.6「下载与装配只发生一次」的装配侧),\n * 然后以固定默认端口常驻服务 (设计 §3.5: 空闲自杀、信号优雅停机)。\n *\n * consent 链完整性 (设计 §3.6): 服务进程**不做 consent 判定、永不自主下载** —\n * 拉起方 (B2 接线: 会话侧在用户同意、模型经预热下载在位后) 才 spawn 本进程;\n * 启动时模型文件缺失即结构化退出 (拉起方把它当服务不可用, 按裁决 A 呈现)。\n * 下载只发生在会话进程的既有预热流程 (memory_enable download), 行为不变。\n *\n * 测试缝: `backend` 直接注入完整后端 (跳过真实装配); `loadImpls`/`moduleEntryPaths`\n * 透传给真实装配 (与 capability 的 enabling 缝同构)。\n */\nimport { join } from \"node:path\";\nimport { VECTOR_DB_DIRNAME, VECTOR_MODEL_CACHE_DIRNAME, VECTOR_RERANKER_MODEL_ID } from \"../capability.ts\";\nimport type { TransformersEmbeddingLoadImpl } from \"./embedding-provider.ts\";\nimport { createTransformersEmbeddingProvider } from \"./embedding-provider.ts\";\nimport type { TransformersRerankerLoadImpl } from \"./embedding-reranker.ts\";\nimport { createTransformersReranker } from \"./embedding-reranker.ts\";\nimport { MODEL_MIN_WEIGHT_BYTES, modelFilesCheck, RERANKER_MIN_WEIGHT_BYTES, vectorPresetForMode } from \"./enabling.ts\";\nimport {\n\tcreateMemoryInferenceServer,\n\ttype MemoryInferenceBackendV1,\n\ttype MemoryInferenceServerV1,\n} from \"./inference-server.ts\";\nimport { createMemoryVectorIndex } from \"./vector-index.ts\";\n\n/** 服务空闲自杀阈值出厂默认 (设计 §3.5; 登记产品默认值清单)。 */\nexport const MEMORY_INFERENCE_SERVICE_IDLE_TIMEOUT_MS = 5 * 60 * 1000;\n\nexport interface MemoryInferenceServiceOptions {\n\treadonly agentDir: string;\n\treadonly mode: \"light\" | \"full\";\n\t/** 缺省固定默认端口 (单例互斥); env 覆盖由 createMemoryInferenceServer 端口参数承担。 */\n\treadonly port?: number;\n\t/** 缺省 MEMORY_INFERENCE_SERVICE_IDLE_TIMEOUT_MS。 */\n\treadonly idleTimeoutMs?: number;\n\treadonly signal?: AbortSignal;\n\t/** 测试缝: 直接给定后端, 跳过真实装配 (模型检查一并跳过)。 */\n\treadonly backend?: MemoryInferenceBackendV1;\n\treadonly moduleEntryPaths?: { readonly lancedb?: string; readonly transformers?: string };\n\treadonly loadImpls?: {\n\t\treadonly embedding?: TransformersEmbeddingLoadImpl;\n\t\treadonly reranker?: TransformersRerankerLoadImpl;\n\t};\n\treadonly log?: (line: string) => void;\n}\n\n/**\n * 装配并启动共享推理服务。真实装配路径的失败 (模型缺失) 抛结构化错误 —\n * 进程入口将其转为一行 stderr + 非零退出码; 已监听后返回 server 句柄。\n */\nexport async function runMemoryInferenceService(\n\toptions: MemoryInferenceServiceOptions,\n): Promise<MemoryInferenceServerV1> {\n\tconst log = options.log ?? (() => {});\n\tconst preset = vectorPresetForMode(options.mode);\n\tconst modelCacheDir = join(options.agentDir, \"memory\", VECTOR_MODEL_CACHE_DIRNAME);\n\n\tlet backend = options.backend;\n\tif (backend === undefined) {\n\t\t// consent 链守门: 模型缺失即退出, 服务进程绝不触发下载 (拉起方保证\n\t\t// \"已同意且已下载\"; 缺失说明拉起时机错误或缓存被清)。\n\t\tconst missingEmbedding = modelFilesCheck(\n\t\t\tmodelCacheDir,\n\t\t\tpreset.embeddingModelId,\n\t\t\tMODEL_MIN_WEIGHT_BYTES[options.mode],\n\t\t);\n\t\tconst missingReranker = modelFilesCheck(modelCacheDir, VECTOR_RERANKER_MODEL_ID, RERANKER_MIN_WEIGHT_BYTES);\n\t\tconst missing = missingEmbedding ?? missingReranker;\n\t\tif (missing !== undefined) {\n\t\t\tthrow new Error(\n\t\t\t\t`memory inference service refuses to start: ${missing} — the service never downloads models itself; ` +\n\t\t\t\t\t\"the spawner must start it after the session-side preheat has the models in place\",\n\t\t\t);\n\t\t}\n\t\t// remoteHosts 不传 (provider 缺省序列): 服务的前置条件是模型已在位,\n\t\t// 加载命中本地缓存、远端不可达也不影响; 显式空数组会被 provider 当\n\t\t// \"零镜像可用\", 首次加载直接报 no remote hosts (2026-10-06 live E2E)。\n\t\tconst embedProvider = createTransformersEmbeddingProvider({\n\t\t\tmodelId: preset.embeddingModelId,\n\t\t\tdimensions: preset.dimensions,\n\t\t\tcacheDir: modelCacheDir,\n\t\t\t...(options.moduleEntryPaths?.transformers === undefined\n\t\t\t\t? {}\n\t\t\t\t: { moduleEntryPath: options.moduleEntryPaths.transformers }),\n\t\t\t...(options.loadImpls?.embedding === undefined ? {} : { loadImpl: options.loadImpls.embedding }),\n\t\t});\n\t\tconst reranker = createTransformersReranker({\n\t\t\tmodelId: VECTOR_RERANKER_MODEL_ID,\n\t\t\tcacheDir: modelCacheDir,\n\t\t\t...(options.moduleEntryPaths?.transformers === undefined\n\t\t\t\t? {}\n\t\t\t\t: { moduleEntryPath: options.moduleEntryPaths.transformers }),\n\t\t\t...(options.loadImpls?.reranker === undefined ? {} : { loadImpl: options.loadImpls.reranker }),\n\t\t});\n\t\tconst vectorIndex = await createMemoryVectorIndex({\n\t\t\tdbPath: join(options.agentDir, \"memory\", VECTOR_DB_DIRNAME),\n\t\t\tdimensions: preset.dimensions,\n\t\t\t...(options.moduleEntryPaths?.lancedb === undefined\n\t\t\t\t? {}\n\t\t\t\t: { moduleEntryPath: options.moduleEntryPaths.lancedb }),\n\t\t});\n\t\tbackend = { embedProvider, reranker, vectorIndex };\n\t}\n\n\tconst server = await createMemoryInferenceServer({\n\t\tbackend,\n\t\t// agentDir 身份上报 (2026-10-06 串线修复): 服务进程的 agent-dir 由 spawn\n\t\t// env 解析而来, 经 hello 回报 — 会话装配据此拒绝跨 agent-dir 串线。\n\t\tagentDir: options.agentDir,\n\t\t...(options.port === undefined ? {} : { port: options.port }),\n\t\tidleTimeoutMs: options.idleTimeoutMs ?? MEMORY_INFERENCE_SERVICE_IDLE_TIMEOUT_MS,\n\t\t...(options.signal === undefined ? {} : { signal: options.signal }),\n\t\tonError: (error, context) => {\n\t\t\tlog(`memory-inference: ${context}: ${error.message}`);\n\t\t},\n\t});\n\tlog(\n\t\t`memory-inference: serving on ${server.host}:${server.port} (mode=${options.mode}, idle-exit=${options.idleTimeoutMs ?? MEMORY_INFERENCE_SERVICE_IDLE_TIMEOUT_MS}ms)`,\n\t);\n\treturn server;\n}\n"]}
|
package/dist/memory/ledger.d.ts
CHANGED
|
@@ -49,6 +49,21 @@ export interface DurableMemoryLedgerV1 {
|
|
|
49
49
|
* which a lock counts as abandoned and is stolen. Both exist so tests can
|
|
50
50
|
* exercise the timeout and steal paths quickly.
|
|
51
51
|
*/
|
|
52
|
+
/**
|
|
53
|
+
* 跨进程 canonical 身份扫描 (2026-10-07 对账反向清理守卫): 只读扫描 owner 名下
|
|
54
|
+
* 全部域账本文件的 memoryId 并集。逐文件用临时 ledger 实例 load (assistant-card
|
|
55
|
+
* 扫描先例), 不触碰调用进程任何 ledger 实例的内存快照。complete=false 表示
|
|
56
|
+
* 有账本文件读取失败 (非 ENOENT 的 IO 错误) —— 调用方必须禁用本轮基于缺席的
|
|
57
|
+
* 删除, 防瞬时 IO 错误把其他会话的活跃行放大成误删; 行级损坏容错 (跳过坏行)
|
|
58
|
+
* 是 loader 的既 declared 语义, 不影响 complete。
|
|
59
|
+
*/
|
|
60
|
+
export declare function scanOwnerLedgerMemoryIds(options: {
|
|
61
|
+
readonly agentDir: string;
|
|
62
|
+
readonly owner: string;
|
|
63
|
+
}): {
|
|
64
|
+
readonly memoryIds: ReadonlySet<string>;
|
|
65
|
+
readonly complete: boolean;
|
|
66
|
+
};
|
|
52
67
|
export declare function createDurableMemoryLedger(options: {
|
|
53
68
|
readonly path: string;
|
|
54
69
|
readonly now?: () => number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ledger.d.ts","sourceRoot":"","sources":["../../src/memory/ledger.ts"],"names":[],"mappings":"AA6DA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAGpD,iFAA4E;AAC5E,eAAO,MAAM,oBAAoB,WAAW,CAAC;AAkC7C,4FAA4F;AAC5F,MAAM,WAAW,+BAA+B;IAC/C,qEAAmE;IACnE,QAAQ,CAAC,KAAK,EAAE,SAAS,YAAY,EAAE,CAAC;IACxC,2EAA2E;IAC3E,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,qFAAqF;IACrF,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,gFAAgF;IAChF,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,qBAAqB;IACrC;;;;OAIG;IACH,IAAI,IAAI,+BAA+B,CAAC;IACxC;;;;;;OAMG;IACH,MAAM,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAAC;IACjC;;;;;;;;OAQG;IACH,OAAO,CAAC,KAAK,EAAE,SAAS,YAAY,EAAE,GAAG,IAAI,CAAC;IAC9C,gFAAgF;IAChF,aAAa,IAAI,SAAS,YAAY,EAAE,CAAC;CACzC;AAMD;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE;IAClD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IAC5B,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC9B,GAAG,qBAAqB,CA+MxB","sourcesContent":["/**\n * Durable memory ledger (统一修复轮 A, 方案系统设计 §6.1 + 方案实施计划 §11 A) —\n * the append-only JSONL persistent replica of a Memory Foundation store.\n *\n * Layout: `<agentDir>/memory/<owner>/ledger-<suiteId|\"legacy\">.jsonl`. The\n * owner segment is the store/facade isolation principal (one local user per\n * agentDir in this product); the suite segment is the 方案 read-boundary\n * domain — a real suite id, or the sentinel \"legacy\" for sessions without a\n * suite binding. One line holds exactly one committed canonical atom\n * (contract @2, including `suiteId`).\n *\n * Replica semantics: the write path appends every committed atom (canonical\n * copy on disk); the read path `load()`s the atoms at startup and replays them\n * into a fresh store. After a forget, `rewrite(atom[])` physically rewrites\n * the file so purged atoms cannot survive a restart. Corruption tolerance:\n * malformed lines are skipped and counted, duplicates are skipped and counted\n * — loading never fails. Only the WRITE path validates strictly: a ledger must\n * never persist an atom the Foundation would reject.\n *\n * Concurrency and durability envelope (统一修复轮 H1, #7 深度修复; rewrite 共\n * 锁 + fsync 关洞修复): competing writers — append AND rewrite — serialize\n * through a `.lock` sibling file taken via exclusive creation (`wx` — atomic\n * on every supported platform). A second writer polls (25 ms) until the\n * holder removes the lock, steals a lock whose mtime is older than\n * `staleLockMs` (default 60 s — a crashed writer left it behind; a live\n * holder keeps the lock for milliseconds), and throws after `lockTimeoutMs`\n * (default 5 s, configurable) — a writer never silently blocks and never\n * silently drops its row; lock or IO failures propagate to the caller. The\n * lock is wall-clock based (real waiting), not the injected deterministic\n * `now`. Durability: both write paths run open → write → fsync → close, so the\n * file contents of an acknowledged append or rewrite survive a hard crash,\n * subject to the directory-entry boundary declared below (a torn trailing\n * line, possible only in files written by older builds or damaged externally,\n * is still skipped by the loader's corruption tolerance).\n * Remaining boundaries, declared honestly: the exclusive create is a mutex\n * only where exclusive creation is atomic — local filesystems; multi-host or\n * network-share access is out of scope. `rewrite` still publishes the calling\n * process's atom set as the whole file: rows another process appended since\n * this process last loaded are not merged into a rewrite (the forget caller\n * owns that reconciliation) — the lock closes the interleaving window, not\n * stale in-memory views. And fsync persists file contents, not directory\n * entries (Windows cannot fsync a directory): a crash between rewrite's fsync\n * and its rename leaves the previous file intact, never a half-written one.\n * An fsync failure on append propagates after the row has already reached the\n * OS: the unacknowledged row may be readable, and a caller retry appends it\n * again — the loader's duplicate tolerance (`duplicateSkipped`) absorbs that.\n */\nimport {\n\tcloseSync,\n\tfsyncSync,\n\tmkdirSync,\n\topenSync,\n\treadFileSync,\n\trenameSync,\n\tstatSync,\n\tunlinkSync,\n\twriteFileSync,\n\twriteSync,\n} from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport { sleepSync } from \"../utils/sync-sleep.ts\";\nimport type { MemoryAtomV1 } from \"./foundation.ts\";\nimport { validateMemoryAtomV1 } from \"./foundation.ts\";\n\n/** Sentinel suite domain for sessions without a suite binding (设计 §6.1). */\nexport const LEGACY_MEMORY_DOMAIN = \"legacy\";\n\n/** Suffix of the append lock sibling file (`<ledgerPath>.lock`). */\nconst LOCK_FILE_SUFFIX = \".lock\";\n/**\n * Wall-clock wait between lock acquisition attempts. Kept as a per-file\n * constant even though the durable store backend currently uses the same\n * 25 ms value: the two lock protocols stay independent.\n */\nconst LOCK_POLL_INTERVAL_MS = 25;\n/** Default ceiling on waiting for a competing writer's lock. */\nconst DEFAULT_LOCK_TIMEOUT_MS = 5000;\n/**\n * Default age at which a lock counts as abandoned (its writer crashed while\n * holding it). Deliberately far above any live hold time (milliseconds of\n * appendFileSync) and above the wait timeout, so a waiter fails loudly at the\n * timeout instead of stealing a lock that might still be held.\n */\nconst DEFAULT_STALE_LOCK_MS = 60_000;\n\n/**\n * Writes every byte of `data` to the fd, looping over partial `writeSync`\n * results (a short write would silently break the fsync durability promise:\n * the caller fsyncs and closes only after the full payload is handed to the\n * OS).\n */\nconst writeAllSync = (fd: number, data: string): void => {\n\tconst buffer = Buffer.from(data, \"utf8\");\n\tlet written = 0;\n\twhile (written < buffer.length) {\n\t\twritten += writeSync(fd, buffer, written, buffer.length - written);\n\t}\n};\n\n/** Result of {@link DurableMemoryLedgerV1.load}: replayable atoms plus skip diagnostics. */\nexport interface DurableMemoryLedgerLoadResultV1 {\n\t/** Valid atoms in file order — replay these into a fresh store. */\n\treadonly atoms: readonly MemoryAtomV1[];\n\t/** Malformed lines (invalid JSON or invalid atoms) skipped during load. */\n\treadonly corruptedLines: number;\n\t/** Atoms whose memoryId was already loaded (crash windows can duplicate appends). */\n\treadonly duplicateSkipped: number;\n\t/** True when the file existed but could not be read (permissions, IO error). */\n\treadonly unreadable: boolean;\n}\n\nexport interface DurableMemoryLedgerV1 {\n\t/**\n\t * Reads the ledger file and returns the replayable atom set plus skip\n\t * counters. A missing file is an empty ledger, not an error; malformed or\n\t * duplicate lines are skipped and counted — load NEVER throws.\n\t */\n\tload(): DurableMemoryLedgerLoadResultV1;\n\t/**\n\t * Appends one committed atom as a single JSONL line (strictly validated).\n\t * The write serializes against other processes through the `.lock` sibling\n\t * file and is fsynced before close, so an acknowledged append survives a\n\t * hard crash; a lock timeout or IO failure throws (an append never fails\n\t * silently).\n\t */\n\tappend(atom: MemoryAtomV1): void;\n\t/**\n\t * Physically rewrites the whole file with the given atoms (atomic tmp-file\n\t * + rename). The forget/purge flow calls this after removing atoms so the\n\t * durable replica matches the surviving canonical set. The rewrite holds\n\t * the same cross-process lock as append for its whole duration (same\n\t * timeout and stale-lock-stealing semantics), so a competing writer's\n\t * append cannot be lost inside the rewrite window, and the new content is\n\t * fsynced before the rename.\n\t */\n\trewrite(atoms: readonly MemoryAtomV1[]): void;\n\t/** The ledger's current in-memory atom set (append/rewrite keep it in sync). */\n\tatomsSnapshot(): readonly MemoryAtomV1[];\n}\n\nfunction assertNonEmptyString(value: unknown, label: string): asserts value is string {\n\tif (typeof value !== \"string\" || value.trim().length === 0) throw new Error(`${label} must be a non-empty string`);\n}\n\n/**\n * Creates a durable JSONL memory ledger at `path`. `now` suffixes the rewrite\n * tmp file so deterministic tests can pin it; it never participates in atom\n * identity (timestamps live inside the canonical atoms) and never drives the\n * append lock, which is a real-time mechanism. `lockTimeoutMs` bounds the\n * wall-clock wait for a competing writer's lock; `staleLockMs` is the age at\n * which a lock counts as abandoned and is stolen. Both exist so tests can\n * exercise the timeout and steal paths quickly.\n */\nexport function createDurableMemoryLedger(options: {\n\treadonly path: string;\n\treadonly now?: () => number;\n\treadonly lockTimeoutMs?: number;\n\treadonly staleLockMs?: number;\n}): DurableMemoryLedgerV1 {\n\tassertNonEmptyString(options.path, \"Durable ledger path\");\n\tif (options.lockTimeoutMs !== undefined && (!Number.isFinite(options.lockTimeoutMs) || options.lockTimeoutMs <= 0)) {\n\t\tthrow new Error(\"Durable ledger lockTimeoutMs must be a positive number\");\n\t}\n\tif (options.staleLockMs !== undefined && (!Number.isFinite(options.staleLockMs) || options.staleLockMs <= 0)) {\n\t\tthrow new Error(\"Durable ledger staleLockMs must be a positive number\");\n\t}\n\tconst now = options.now ?? (() => Date.now());\n\tconst path = options.path;\n\tconst lockTimeoutMs = options.lockTimeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS;\n\tconst staleLockMs = options.staleLockMs ?? DEFAULT_STALE_LOCK_MS;\n\tconst lockPath = `${path}${LOCK_FILE_SUFFIX}`;\n\t/** In-memory replica of the durable atom set; kept in sync by append/rewrite/load. */\n\tlet atoms: MemoryAtomV1[] = [];\n\n\tconst ensureDir = (): void => {\n\t\tmkdirSync(dirname(path), { recursive: true });\n\t};\n\n\t/**\n\t * Takes the ledger writer lock, shared by append and rewrite: exclusive\n\t * create (`wx`) is the mutex; an existing lock is polled until released,\n\t * stolen once its mtime exceeds `staleLockMs` (crashed writer), or turned\n\t * into a thrown error at the wait deadline. The lock file carries a\n\t * per-acquisition token so a stale-steal victim releasing late can never\n\t * delete the thief's fresh lock.\n\t */\n\tconst acquireAppendLock = (): string => {\n\t\tconst deadline = Date.now() + lockTimeoutMs;\n\t\tfor (;;) {\n\t\t\tensureDir();\n\t\t\tconst token = `${process.pid}:${Date.now()}`;\n\t\t\ttry {\n\t\t\t\tconst fd = openSync(lockPath, \"wx\");\n\t\t\t\ttry {\n\t\t\t\t\twriteFileSync(fd, token);\n\t\t\t\t} finally {\n\t\t\t\t\tcloseSync(fd);\n\t\t\t\t}\n\t\t\t\treturn token;\n\t\t\t} catch (error) {\n\t\t\t\t// POSIX maps a lost create race to EEXIST; Windows/libuv maps the\n\t\t\t\t// same concurrent-create sharing violation to EPERM. Both mean the\n\t\t\t\t// lock file existed when we tried to create it — contend (poll /\n\t\t\t\t// stale-steal below). For EPERM the existence check re-stats: a\n\t\t\t\t// lock still there is contention, while ENOENT means the holder\n\t\t\t\t// released and deleted it between the failed open and this stat\n\t\t\t\t// (or Windows briefly left a delete-pending name that has now\n\t\t\t\t// gone) — the same vanished-lock window the EEXIST path tolerates,\n\t\t\t\t// so retry too. A stat error other than ENOENT means the EPERM was\n\t\t\t\t// a genuine permission failure and propagates, and the vanish path\n\t\t\t\t// still rethrows the original EPERM at the wait deadline: a\n\t\t\t\t// persistent EPERM with no lock file ever appearing is a genuine\n\t\t\t\t// permission failure, not a held lock.\n\t\t\t\tconst code = (error as NodeJS.ErrnoException).code;\n\t\t\t\tif (code === \"EPERM\") {\n\t\t\t\t\tlet vanished = false;\n\t\t\t\t\ttry {\n\t\t\t\t\t\tstatSync(lockPath);\n\t\t\t\t\t} catch (statError) {\n\t\t\t\t\t\tif ((statError as NodeJS.ErrnoException).code !== \"ENOENT\") throw error;\n\t\t\t\t\t\tvanished = true;\n\t\t\t\t\t}\n\t\t\t\t\tif (vanished) {\n\t\t\t\t\t\tif (Date.now() >= deadline) throw error;\n\t\t\t\t\t\tsleepSync(LOCK_POLL_INTERVAL_MS);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t} else if (code !== \"EEXIST\") {\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t\tlet stale = false;\n\t\t\t\ttry {\n\t\t\t\t\tstale = Date.now() - statSync(lockPath).mtimeMs > staleLockMs;\n\t\t\t\t} catch {\n\t\t\t\t\t// The holder released the lock between the failed open and the\n\t\t\t\t\t// stat — retry the exclusive create immediately.\n\t\t\t\t}\n\t\t\t\tif (stale) {\n\t\t\t\t\t// A crashed writer left the lock behind: steal it. A competing\n\t\t\t\t\t// waiter may steal first — the next exclusive create resolves that.\n\t\t\t\t\ttry {\n\t\t\t\t\t\tunlinkSync(lockPath);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// Already stolen — retry.\n\t\t\t\t\t}\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (Date.now() >= deadline) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`memory ledger append lock timeout after ${lockTimeoutMs} ms: ${lockPath} is held by another writer`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tsleepSync(LOCK_POLL_INTERVAL_MS);\n\t\t\t}\n\t\t}\n\t};\n\n\t/** Releases the append lock, but only if our own token still owns it. */\n\tconst releaseAppendLock = (token: string): void => {\n\t\ttry {\n\t\t\tif (readFileSync(lockPath, \"utf8\") === token) unlinkSync(lockPath);\n\t\t} catch {\n\t\t\t// The lock is already gone (or unreadable) — nothing to release.\n\t\t}\n\t};\n\n\tconst parseAtomLine = (line: string): MemoryAtomV1 | undefined => {\n\t\ttry {\n\t\t\treturn validateMemoryAtomV1(JSON.parse(line));\n\t\t} catch {\n\t\t\t// Corruption tolerance: a malformed line is skipped and counted by the\n\t\t\t// caller — a damaged ledger must never fail the startup replay.\n\t\t\treturn undefined;\n\t\t}\n\t};\n\n\treturn {\n\t\tload() {\n\t\t\tlet raw: string;\n\t\t\ttry {\n\t\t\t\traw = readFileSync(path, \"utf8\");\n\t\t\t} catch (error) {\n\t\t\t\tif ((error as NodeJS.ErrnoException).code === \"ENOENT\") {\n\t\t\t\t\tatoms = [];\n\t\t\t\t\treturn { atoms: Object.freeze([]), corruptedLines: 0, duplicateSkipped: 0, unreadable: false };\n\t\t\t\t}\n\t\t\t\tatoms = [];\n\t\t\t\treturn { atoms: Object.freeze([]), corruptedLines: 0, duplicateSkipped: 0, unreadable: true };\n\t\t\t}\n\t\t\tconst loaded: MemoryAtomV1[] = [];\n\t\t\tconst seen = new Set<string>();\n\t\t\tlet corruptedLines = 0;\n\t\t\tlet duplicateSkipped = 0;\n\t\t\tfor (const line of raw.split(\"\\n\")) {\n\t\t\t\tif (line.trim() === \"\") continue;\n\t\t\t\tconst atom = parseAtomLine(line);\n\t\t\t\tif (!atom) {\n\t\t\t\t\tcorruptedLines += 1;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (seen.has(atom.memoryId)) {\n\t\t\t\t\tduplicateSkipped += 1;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tseen.add(atom.memoryId);\n\t\t\t\tloaded.push(atom);\n\t\t\t}\n\t\t\tatoms = loaded;\n\t\t\treturn {\n\t\t\t\tatoms: Object.freeze([...loaded]),\n\t\t\t\tcorruptedLines,\n\t\t\t\tduplicateSkipped,\n\t\t\t\tunreadable: false,\n\t\t\t};\n\t\t},\n\t\tappend(atom) {\n\t\t\tconst validated = validateMemoryAtomV1(atom);\n\t\t\tconst token = acquireAppendLock();\n\t\t\ttry {\n\t\t\t\t// One committed atom per line; the trailing newline keeps the file\n\t\t\t\t// a well-formed JSONL stream for crash-truncated-tail recovery.\n\t\t\t\t// The line is fsynced before close: once append returns, a hard\n\t\t\t\t// crash cannot lose it.\n\t\t\t\tconst fd = openSync(path, \"a\");\n\t\t\t\ttry {\n\t\t\t\t\twriteAllSync(fd, `${JSON.stringify(validated)}\\n`);\n\t\t\t\t\tfsyncSync(fd);\n\t\t\t\t} finally {\n\t\t\t\t\tcloseSync(fd);\n\t\t\t\t}\n\t\t\t} finally {\n\t\t\t\treleaseAppendLock(token);\n\t\t\t}\n\t\t\tatoms.push(validated);\n\t\t},\n\t\trewrite(nextAtoms) {\n\t\t\tconst validated = nextAtoms.map((atom) => validateMemoryAtomV1(atom));\n\t\t\t// The rewrite shares the append lock: a competing writer either\n\t\t\t// finished (its row is on disk and observable to the caller's loaded\n\t\t\t// view) or waits out the whole rebuild — no append can land inside\n\t\t\t// the rewrite window anymore.\n\t\t\tconst token = acquireAppendLock();\n\t\t\ttry {\n\t\t\t\tensureDir();\n\t\t\t\tconst tmpPath = `${path}.${now()}.rewrite.tmp`;\n\t\t\t\t// fsync the rebuilt file before the atomic rename: once rewrite\n\t\t\t\t// returns, a hard crash cannot lose the new content (and the\n\t\t\t\t// rename itself means the old file survives intact until then).\n\t\t\t\tconst fd = openSync(tmpPath, \"w\");\n\t\t\t\ttry {\n\t\t\t\t\twriteAllSync(fd, validated.map((atom) => `${JSON.stringify(atom)}\\n`).join(\"\"));\n\t\t\t\t\tfsyncSync(fd);\n\t\t\t\t} finally {\n\t\t\t\t\tcloseSync(fd);\n\t\t\t\t}\n\t\t\t\trenameSync(tmpPath, path);\n\t\t\t} finally {\n\t\t\t\treleaseAppendLock(token);\n\t\t\t}\n\t\t\tatoms = [...validated];\n\t\t},\n\t\tatomsSnapshot() {\n\t\t\treturn Object.freeze([...atoms]);\n\t\t},\n\t};\n}\n"]}
|
|
1
|
+
{"version":3,"file":"ledger.d.ts","sourceRoot":"","sources":["../../src/memory/ledger.ts"],"names":[],"mappings":"AA8DA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAGpD,iFAA4E;AAC5E,eAAO,MAAM,oBAAoB,WAAW,CAAC;AAkC7C,4FAA4F;AAC5F,MAAM,WAAW,+BAA+B;IAC/C,qEAAmE;IACnE,QAAQ,CAAC,KAAK,EAAE,SAAS,YAAY,EAAE,CAAC;IACxC,2EAA2E;IAC3E,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,qFAAqF;IACrF,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,gFAAgF;IAChF,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,qBAAqB;IACrC;;;;OAIG;IACH,IAAI,IAAI,+BAA+B,CAAC;IACxC;;;;;;OAMG;IACH,MAAM,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAAC;IACjC;;;;;;;;OAQG;IACH,OAAO,CAAC,KAAK,EAAE,SAAS,YAAY,EAAE,GAAG,IAAI,CAAC;IAC9C,gFAAgF;IAChF,aAAa,IAAI,SAAS,YAAY,EAAE,CAAC;CACzC;AAMD;;;;;;;;GAQG;AACH;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE;IAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG;IACzG,QAAQ,CAAC,SAAS,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACxC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;CAC3B,CAoBA;AAED,wBAAgB,yBAAyB,CAAC,OAAO,EAAE;IAClD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IAC5B,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC9B,GAAG,qBAAqB,CA+MxB","sourcesContent":["/**\n * Durable memory ledger (统一修复轮 A, 方案系统设计 §6.1 + 方案实施计划 §11 A) —\n * the append-only JSONL persistent replica of a Memory Foundation store.\n *\n * Layout: `<agentDir>/memory/<owner>/ledger-<suiteId|\"legacy\">.jsonl`. The\n * owner segment is the store/facade isolation principal (one local user per\n * agentDir in this product); the suite segment is the 方案 read-boundary\n * domain — a real suite id, or the sentinel \"legacy\" for sessions without a\n * suite binding. One line holds exactly one committed canonical atom\n * (contract @2, including `suiteId`).\n *\n * Replica semantics: the write path appends every committed atom (canonical\n * copy on disk); the read path `load()`s the atoms at startup and replays them\n * into a fresh store. After a forget, `rewrite(atom[])` physically rewrites\n * the file so purged atoms cannot survive a restart. Corruption tolerance:\n * malformed lines are skipped and counted, duplicates are skipped and counted\n * — loading never fails. Only the WRITE path validates strictly: a ledger must\n * never persist an atom the Foundation would reject.\n *\n * Concurrency and durability envelope (统一修复轮 H1, #7 深度修复; rewrite 共\n * 锁 + fsync 关洞修复): competing writers — append AND rewrite — serialize\n * through a `.lock` sibling file taken via exclusive creation (`wx` — atomic\n * on every supported platform). A second writer polls (25 ms) until the\n * holder removes the lock, steals a lock whose mtime is older than\n * `staleLockMs` (default 60 s — a crashed writer left it behind; a live\n * holder keeps the lock for milliseconds), and throws after `lockTimeoutMs`\n * (default 5 s, configurable) — a writer never silently blocks and never\n * silently drops its row; lock or IO failures propagate to the caller. The\n * lock is wall-clock based (real waiting), not the injected deterministic\n * `now`. Durability: both write paths run open → write → fsync → close, so the\n * file contents of an acknowledged append or rewrite survive a hard crash,\n * subject to the directory-entry boundary declared below (a torn trailing\n * line, possible only in files written by older builds or damaged externally,\n * is still skipped by the loader's corruption tolerance).\n * Remaining boundaries, declared honestly: the exclusive create is a mutex\n * only where exclusive creation is atomic — local filesystems; multi-host or\n * network-share access is out of scope. `rewrite` still publishes the calling\n * process's atom set as the whole file: rows another process appended since\n * this process last loaded are not merged into a rewrite (the forget caller\n * owns that reconciliation) — the lock closes the interleaving window, not\n * stale in-memory views. And fsync persists file contents, not directory\n * entries (Windows cannot fsync a directory): a crash between rewrite's fsync\n * and its rename leaves the previous file intact, never a half-written one.\n * An fsync failure on append propagates after the row has already reached the\n * OS: the unacknowledged row may be readable, and a caller retry appends it\n * again — the loader's duplicate tolerance (`duplicateSkipped`) absorbs that.\n */\nimport {\n\tcloseSync,\n\tfsyncSync,\n\tmkdirSync,\n\topenSync,\n\treaddirSync,\n\treadFileSync,\n\trenameSync,\n\tstatSync,\n\tunlinkSync,\n\twriteFileSync,\n\twriteSync,\n} from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { sleepSync } from \"../utils/sync-sleep.ts\";\nimport type { MemoryAtomV1 } from \"./foundation.ts\";\nimport { validateMemoryAtomV1 } from \"./foundation.ts\";\n\n/** Sentinel suite domain for sessions without a suite binding (设计 §6.1). */\nexport const LEGACY_MEMORY_DOMAIN = \"legacy\";\n\n/** Suffix of the append lock sibling file (`<ledgerPath>.lock`). */\nconst LOCK_FILE_SUFFIX = \".lock\";\n/**\n * Wall-clock wait between lock acquisition attempts. Kept as a per-file\n * constant even though the durable store backend currently uses the same\n * 25 ms value: the two lock protocols stay independent.\n */\nconst LOCK_POLL_INTERVAL_MS = 25;\n/** Default ceiling on waiting for a competing writer's lock. */\nconst DEFAULT_LOCK_TIMEOUT_MS = 5000;\n/**\n * Default age at which a lock counts as abandoned (its writer crashed while\n * holding it). Deliberately far above any live hold time (milliseconds of\n * appendFileSync) and above the wait timeout, so a waiter fails loudly at the\n * timeout instead of stealing a lock that might still be held.\n */\nconst DEFAULT_STALE_LOCK_MS = 60_000;\n\n/**\n * Writes every byte of `data` to the fd, looping over partial `writeSync`\n * results (a short write would silently break the fsync durability promise:\n * the caller fsyncs and closes only after the full payload is handed to the\n * OS).\n */\nconst writeAllSync = (fd: number, data: string): void => {\n\tconst buffer = Buffer.from(data, \"utf8\");\n\tlet written = 0;\n\twhile (written < buffer.length) {\n\t\twritten += writeSync(fd, buffer, written, buffer.length - written);\n\t}\n};\n\n/** Result of {@link DurableMemoryLedgerV1.load}: replayable atoms plus skip diagnostics. */\nexport interface DurableMemoryLedgerLoadResultV1 {\n\t/** Valid atoms in file order — replay these into a fresh store. */\n\treadonly atoms: readonly MemoryAtomV1[];\n\t/** Malformed lines (invalid JSON or invalid atoms) skipped during load. */\n\treadonly corruptedLines: number;\n\t/** Atoms whose memoryId was already loaded (crash windows can duplicate appends). */\n\treadonly duplicateSkipped: number;\n\t/** True when the file existed but could not be read (permissions, IO error). */\n\treadonly unreadable: boolean;\n}\n\nexport interface DurableMemoryLedgerV1 {\n\t/**\n\t * Reads the ledger file and returns the replayable atom set plus skip\n\t * counters. A missing file is an empty ledger, not an error; malformed or\n\t * duplicate lines are skipped and counted — load NEVER throws.\n\t */\n\tload(): DurableMemoryLedgerLoadResultV1;\n\t/**\n\t * Appends one committed atom as a single JSONL line (strictly validated).\n\t * The write serializes against other processes through the `.lock` sibling\n\t * file and is fsynced before close, so an acknowledged append survives a\n\t * hard crash; a lock timeout or IO failure throws (an append never fails\n\t * silently).\n\t */\n\tappend(atom: MemoryAtomV1): void;\n\t/**\n\t * Physically rewrites the whole file with the given atoms (atomic tmp-file\n\t * + rename). The forget/purge flow calls this after removing atoms so the\n\t * durable replica matches the surviving canonical set. The rewrite holds\n\t * the same cross-process lock as append for its whole duration (same\n\t * timeout and stale-lock-stealing semantics), so a competing writer's\n\t * append cannot be lost inside the rewrite window, and the new content is\n\t * fsynced before the rename.\n\t */\n\trewrite(atoms: readonly MemoryAtomV1[]): void;\n\t/** The ledger's current in-memory atom set (append/rewrite keep it in sync). */\n\tatomsSnapshot(): readonly MemoryAtomV1[];\n}\n\nfunction assertNonEmptyString(value: unknown, label: string): asserts value is string {\n\tif (typeof value !== \"string\" || value.trim().length === 0) throw new Error(`${label} must be a non-empty string`);\n}\n\n/**\n * Creates a durable JSONL memory ledger at `path`. `now` suffixes the rewrite\n * tmp file so deterministic tests can pin it; it never participates in atom\n * identity (timestamps live inside the canonical atoms) and never drives the\n * append lock, which is a real-time mechanism. `lockTimeoutMs` bounds the\n * wall-clock wait for a competing writer's lock; `staleLockMs` is the age at\n * which a lock counts as abandoned and is stolen. Both exist so tests can\n * exercise the timeout and steal paths quickly.\n */\n/**\n * 跨进程 canonical 身份扫描 (2026-10-07 对账反向清理守卫): 只读扫描 owner 名下\n * 全部域账本文件的 memoryId 并集。逐文件用临时 ledger 实例 load (assistant-card\n * 扫描先例), 不触碰调用进程任何 ledger 实例的内存快照。complete=false 表示\n * 有账本文件读取失败 (非 ENOENT 的 IO 错误) —— 调用方必须禁用本轮基于缺席的\n * 删除, 防瞬时 IO 错误把其他会话的活跃行放大成误删; 行级损坏容错 (跳过坏行)\n * 是 loader 的既 declared 语义, 不影响 complete。\n */\nexport function scanOwnerLedgerMemoryIds(options: { readonly agentDir: string; readonly owner: string }): {\n\treadonly memoryIds: ReadonlySet<string>;\n\treadonly complete: boolean;\n} {\n\tif (options.owner.trim() === \"\") throw new Error(\"scanOwnerLedgerMemoryIds owner must be a non-empty string\");\n\tconst ledgerDir = join(options.agentDir, \"memory\", options.owner);\n\tlet files: string[];\n\ttry {\n\t\tfiles = readdirSync(ledgerDir).filter((name) => name.startsWith(\"ledger-\") && name.endsWith(\".jsonl\"));\n\t} catch (error) {\n\t\tif ((error as NodeJS.ErrnoException).code === \"ENOENT\") {\n\t\t\treturn { memoryIds: new Set(), complete: true };\n\t\t}\n\t\treturn { memoryIds: new Set(), complete: false };\n\t}\n\tconst memoryIds = new Set<string>();\n\tlet complete = true;\n\tfor (const file of files) {\n\t\tconst result = createDurableMemoryLedger({ path: join(ledgerDir, file) }).load();\n\t\tif (result.unreadable) complete = false;\n\t\tfor (const atom of result.atoms) memoryIds.add(atom.memoryId);\n\t}\n\treturn { memoryIds, complete };\n}\n\nexport function createDurableMemoryLedger(options: {\n\treadonly path: string;\n\treadonly now?: () => number;\n\treadonly lockTimeoutMs?: number;\n\treadonly staleLockMs?: number;\n}): DurableMemoryLedgerV1 {\n\tassertNonEmptyString(options.path, \"Durable ledger path\");\n\tif (options.lockTimeoutMs !== undefined && (!Number.isFinite(options.lockTimeoutMs) || options.lockTimeoutMs <= 0)) {\n\t\tthrow new Error(\"Durable ledger lockTimeoutMs must be a positive number\");\n\t}\n\tif (options.staleLockMs !== undefined && (!Number.isFinite(options.staleLockMs) || options.staleLockMs <= 0)) {\n\t\tthrow new Error(\"Durable ledger staleLockMs must be a positive number\");\n\t}\n\tconst now = options.now ?? (() => Date.now());\n\tconst path = options.path;\n\tconst lockTimeoutMs = options.lockTimeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS;\n\tconst staleLockMs = options.staleLockMs ?? DEFAULT_STALE_LOCK_MS;\n\tconst lockPath = `${path}${LOCK_FILE_SUFFIX}`;\n\t/** In-memory replica of the durable atom set; kept in sync by append/rewrite/load. */\n\tlet atoms: MemoryAtomV1[] = [];\n\n\tconst ensureDir = (): void => {\n\t\tmkdirSync(dirname(path), { recursive: true });\n\t};\n\n\t/**\n\t * Takes the ledger writer lock, shared by append and rewrite: exclusive\n\t * create (`wx`) is the mutex; an existing lock is polled until released,\n\t * stolen once its mtime exceeds `staleLockMs` (crashed writer), or turned\n\t * into a thrown error at the wait deadline. The lock file carries a\n\t * per-acquisition token so a stale-steal victim releasing late can never\n\t * delete the thief's fresh lock.\n\t */\n\tconst acquireAppendLock = (): string => {\n\t\tconst deadline = Date.now() + lockTimeoutMs;\n\t\tfor (;;) {\n\t\t\tensureDir();\n\t\t\tconst token = `${process.pid}:${Date.now()}`;\n\t\t\ttry {\n\t\t\t\tconst fd = openSync(lockPath, \"wx\");\n\t\t\t\ttry {\n\t\t\t\t\twriteFileSync(fd, token);\n\t\t\t\t} finally {\n\t\t\t\t\tcloseSync(fd);\n\t\t\t\t}\n\t\t\t\treturn token;\n\t\t\t} catch (error) {\n\t\t\t\t// POSIX maps a lost create race to EEXIST; Windows/libuv maps the\n\t\t\t\t// same concurrent-create sharing violation to EPERM. Both mean the\n\t\t\t\t// lock file existed when we tried to create it — contend (poll /\n\t\t\t\t// stale-steal below). For EPERM the existence check re-stats: a\n\t\t\t\t// lock still there is contention, while ENOENT means the holder\n\t\t\t\t// released and deleted it between the failed open and this stat\n\t\t\t\t// (or Windows briefly left a delete-pending name that has now\n\t\t\t\t// gone) — the same vanished-lock window the EEXIST path tolerates,\n\t\t\t\t// so retry too. A stat error other than ENOENT means the EPERM was\n\t\t\t\t// a genuine permission failure and propagates, and the vanish path\n\t\t\t\t// still rethrows the original EPERM at the wait deadline: a\n\t\t\t\t// persistent EPERM with no lock file ever appearing is a genuine\n\t\t\t\t// permission failure, not a held lock.\n\t\t\t\tconst code = (error as NodeJS.ErrnoException).code;\n\t\t\t\tif (code === \"EPERM\") {\n\t\t\t\t\tlet vanished = false;\n\t\t\t\t\ttry {\n\t\t\t\t\t\tstatSync(lockPath);\n\t\t\t\t\t} catch (statError) {\n\t\t\t\t\t\tif ((statError as NodeJS.ErrnoException).code !== \"ENOENT\") throw error;\n\t\t\t\t\t\tvanished = true;\n\t\t\t\t\t}\n\t\t\t\t\tif (vanished) {\n\t\t\t\t\t\tif (Date.now() >= deadline) throw error;\n\t\t\t\t\t\tsleepSync(LOCK_POLL_INTERVAL_MS);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t} else if (code !== \"EEXIST\") {\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t\tlet stale = false;\n\t\t\t\ttry {\n\t\t\t\t\tstale = Date.now() - statSync(lockPath).mtimeMs > staleLockMs;\n\t\t\t\t} catch {\n\t\t\t\t\t// The holder released the lock between the failed open and the\n\t\t\t\t\t// stat — retry the exclusive create immediately.\n\t\t\t\t}\n\t\t\t\tif (stale) {\n\t\t\t\t\t// A crashed writer left the lock behind: steal it. A competing\n\t\t\t\t\t// waiter may steal first — the next exclusive create resolves that.\n\t\t\t\t\ttry {\n\t\t\t\t\t\tunlinkSync(lockPath);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// Already stolen — retry.\n\t\t\t\t\t}\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (Date.now() >= deadline) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`memory ledger append lock timeout after ${lockTimeoutMs} ms: ${lockPath} is held by another writer`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tsleepSync(LOCK_POLL_INTERVAL_MS);\n\t\t\t}\n\t\t}\n\t};\n\n\t/** Releases the append lock, but only if our own token still owns it. */\n\tconst releaseAppendLock = (token: string): void => {\n\t\ttry {\n\t\t\tif (readFileSync(lockPath, \"utf8\") === token) unlinkSync(lockPath);\n\t\t} catch {\n\t\t\t// The lock is already gone (or unreadable) — nothing to release.\n\t\t}\n\t};\n\n\tconst parseAtomLine = (line: string): MemoryAtomV1 | undefined => {\n\t\ttry {\n\t\t\treturn validateMemoryAtomV1(JSON.parse(line));\n\t\t} catch {\n\t\t\t// Corruption tolerance: a malformed line is skipped and counted by the\n\t\t\t// caller — a damaged ledger must never fail the startup replay.\n\t\t\treturn undefined;\n\t\t}\n\t};\n\n\treturn {\n\t\tload() {\n\t\t\tlet raw: string;\n\t\t\ttry {\n\t\t\t\traw = readFileSync(path, \"utf8\");\n\t\t\t} catch (error) {\n\t\t\t\tif ((error as NodeJS.ErrnoException).code === \"ENOENT\") {\n\t\t\t\t\tatoms = [];\n\t\t\t\t\treturn { atoms: Object.freeze([]), corruptedLines: 0, duplicateSkipped: 0, unreadable: false };\n\t\t\t\t}\n\t\t\t\tatoms = [];\n\t\t\t\treturn { atoms: Object.freeze([]), corruptedLines: 0, duplicateSkipped: 0, unreadable: true };\n\t\t\t}\n\t\t\tconst loaded: MemoryAtomV1[] = [];\n\t\t\tconst seen = new Set<string>();\n\t\t\tlet corruptedLines = 0;\n\t\t\tlet duplicateSkipped = 0;\n\t\t\tfor (const line of raw.split(\"\\n\")) {\n\t\t\t\tif (line.trim() === \"\") continue;\n\t\t\t\tconst atom = parseAtomLine(line);\n\t\t\t\tif (!atom) {\n\t\t\t\t\tcorruptedLines += 1;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (seen.has(atom.memoryId)) {\n\t\t\t\t\tduplicateSkipped += 1;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tseen.add(atom.memoryId);\n\t\t\t\tloaded.push(atom);\n\t\t\t}\n\t\t\tatoms = loaded;\n\t\t\treturn {\n\t\t\t\tatoms: Object.freeze([...loaded]),\n\t\t\t\tcorruptedLines,\n\t\t\t\tduplicateSkipped,\n\t\t\t\tunreadable: false,\n\t\t\t};\n\t\t},\n\t\tappend(atom) {\n\t\t\tconst validated = validateMemoryAtomV1(atom);\n\t\t\tconst token = acquireAppendLock();\n\t\t\ttry {\n\t\t\t\t// One committed atom per line; the trailing newline keeps the file\n\t\t\t\t// a well-formed JSONL stream for crash-truncated-tail recovery.\n\t\t\t\t// The line is fsynced before close: once append returns, a hard\n\t\t\t\t// crash cannot lose it.\n\t\t\t\tconst fd = openSync(path, \"a\");\n\t\t\t\ttry {\n\t\t\t\t\twriteAllSync(fd, `${JSON.stringify(validated)}\\n`);\n\t\t\t\t\tfsyncSync(fd);\n\t\t\t\t} finally {\n\t\t\t\t\tcloseSync(fd);\n\t\t\t\t}\n\t\t\t} finally {\n\t\t\t\treleaseAppendLock(token);\n\t\t\t}\n\t\t\tatoms.push(validated);\n\t\t},\n\t\trewrite(nextAtoms) {\n\t\t\tconst validated = nextAtoms.map((atom) => validateMemoryAtomV1(atom));\n\t\t\t// The rewrite shares the append lock: a competing writer either\n\t\t\t// finished (its row is on disk and observable to the caller's loaded\n\t\t\t// view) or waits out the whole rebuild — no append can land inside\n\t\t\t// the rewrite window anymore.\n\t\t\tconst token = acquireAppendLock();\n\t\t\ttry {\n\t\t\t\tensureDir();\n\t\t\t\tconst tmpPath = `${path}.${now()}.rewrite.tmp`;\n\t\t\t\t// fsync the rebuilt file before the atomic rename: once rewrite\n\t\t\t\t// returns, a hard crash cannot lose the new content (and the\n\t\t\t\t// rename itself means the old file survives intact until then).\n\t\t\t\tconst fd = openSync(tmpPath, \"w\");\n\t\t\t\ttry {\n\t\t\t\t\twriteAllSync(fd, validated.map((atom) => `${JSON.stringify(atom)}\\n`).join(\"\"));\n\t\t\t\t\tfsyncSync(fd);\n\t\t\t\t} finally {\n\t\t\t\t\tcloseSync(fd);\n\t\t\t\t}\n\t\t\t\trenameSync(tmpPath, path);\n\t\t\t} finally {\n\t\t\t\treleaseAppendLock(token);\n\t\t\t}\n\t\t\tatoms = [...validated];\n\t\t},\n\t\tatomsSnapshot() {\n\t\t\treturn Object.freeze([...atoms]);\n\t\t},\n\t};\n}\n"]}
|
package/dist/memory/ledger.js
CHANGED
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
* OS: the unacknowledged row may be readable, and a caller retry appends it
|
|
46
46
|
* again — the loader's duplicate tolerance (`duplicateSkipped`) absorbs that.
|
|
47
47
|
*/
|
|
48
|
-
import { closeSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync, writeSync, } from "node:fs";
|
|
49
|
-
import { dirname } from "node:path";
|
|
48
|
+
import { closeSync, fsyncSync, mkdirSync, openSync, readdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync, writeSync, } from "node:fs";
|
|
49
|
+
import { dirname, join } from "node:path";
|
|
50
50
|
import { sleepSync } from "../utils/sync-sleep.js";
|
|
51
51
|
import { validateMemoryAtomV1 } from "./foundation.js";
|
|
52
52
|
/** Sentinel suite domain for sessions without a suite binding (设计 §6.1). */
|
|
@@ -94,6 +94,39 @@ function assertNonEmptyString(value, label) {
|
|
|
94
94
|
* which a lock counts as abandoned and is stolen. Both exist so tests can
|
|
95
95
|
* exercise the timeout and steal paths quickly.
|
|
96
96
|
*/
|
|
97
|
+
/**
|
|
98
|
+
* 跨进程 canonical 身份扫描 (2026-10-07 对账反向清理守卫): 只读扫描 owner 名下
|
|
99
|
+
* 全部域账本文件的 memoryId 并集。逐文件用临时 ledger 实例 load (assistant-card
|
|
100
|
+
* 扫描先例), 不触碰调用进程任何 ledger 实例的内存快照。complete=false 表示
|
|
101
|
+
* 有账本文件读取失败 (非 ENOENT 的 IO 错误) —— 调用方必须禁用本轮基于缺席的
|
|
102
|
+
* 删除, 防瞬时 IO 错误把其他会话的活跃行放大成误删; 行级损坏容错 (跳过坏行)
|
|
103
|
+
* 是 loader 的既 declared 语义, 不影响 complete。
|
|
104
|
+
*/
|
|
105
|
+
export function scanOwnerLedgerMemoryIds(options) {
|
|
106
|
+
if (options.owner.trim() === "")
|
|
107
|
+
throw new Error("scanOwnerLedgerMemoryIds owner must be a non-empty string");
|
|
108
|
+
const ledgerDir = join(options.agentDir, "memory", options.owner);
|
|
109
|
+
let files;
|
|
110
|
+
try {
|
|
111
|
+
files = readdirSync(ledgerDir).filter((name) => name.startsWith("ledger-") && name.endsWith(".jsonl"));
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
if (error.code === "ENOENT") {
|
|
115
|
+
return { memoryIds: new Set(), complete: true };
|
|
116
|
+
}
|
|
117
|
+
return { memoryIds: new Set(), complete: false };
|
|
118
|
+
}
|
|
119
|
+
const memoryIds = new Set();
|
|
120
|
+
let complete = true;
|
|
121
|
+
for (const file of files) {
|
|
122
|
+
const result = createDurableMemoryLedger({ path: join(ledgerDir, file) }).load();
|
|
123
|
+
if (result.unreadable)
|
|
124
|
+
complete = false;
|
|
125
|
+
for (const atom of result.atoms)
|
|
126
|
+
memoryIds.add(atom.memoryId);
|
|
127
|
+
}
|
|
128
|
+
return { memoryIds, complete };
|
|
129
|
+
}
|
|
97
130
|
export function createDurableMemoryLedger(options) {
|
|
98
131
|
assertNonEmptyString(options.path, "Durable ledger path");
|
|
99
132
|
if (options.lockTimeoutMs !== undefined && (!Number.isFinite(options.lockTimeoutMs) || options.lockTimeoutMs <= 0)) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ledger.js","sourceRoot":"","sources":["../../src/memory/ledger.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Durable memory ledger (统一修复轮 A, 方案系统设计 §6.1 + 方案实施计划 §11 A) —\n * the append-only JSONL persistent replica of a Memory Foundation store.\n *\n * Layout: `<agentDir>/memory/<owner>/ledger-<suiteId|\"legacy\">.jsonl`. The\n * owner segment is the store/facade isolation principal (one local user per\n * agentDir in this product); the suite segment is the 方案 read-boundary\n * domain — a real suite id, or the sentinel \"legacy\" for sessions without a\n * suite binding. One line holds exactly one committed canonical atom\n * (contract @2, including `suiteId`).\n *\n * Replica semantics: the write path appends every committed atom (canonical\n * copy on disk); the read path `load()`s the atoms at startup and replays them\n * into a fresh store. After a forget, `rewrite(atom[])` physically rewrites\n * the file so purged atoms cannot survive a restart. Corruption tolerance:\n * malformed lines are skipped and counted, duplicates are skipped and counted\n * — loading never fails. Only the WRITE path validates strictly: a ledger must\n * never persist an atom the Foundation would reject.\n *\n * Concurrency and durability envelope (统一修复轮 H1, #7 深度修复; rewrite 共\n * 锁 + fsync 关洞修复): competing writers — append AND rewrite — serialize\n * through a `.lock` sibling file taken via exclusive creation (`wx` — atomic\n * on every supported platform). A second writer polls (25 ms) until the\n * holder removes the lock, steals a lock whose mtime is older than\n * `staleLockMs` (default 60 s — a crashed writer left it behind; a live\n * holder keeps the lock for milliseconds), and throws after `lockTimeoutMs`\n * (default 5 s, configurable) — a writer never silently blocks and never\n * silently drops its row; lock or IO failures propagate to the caller. The\n * lock is wall-clock based (real waiting), not the injected deterministic\n * `now`. Durability: both write paths run open → write → fsync → close, so the\n * file contents of an acknowledged append or rewrite survive a hard crash,\n * subject to the directory-entry boundary declared below (a torn trailing\n * line, possible only in files written by older builds or damaged externally,\n * is still skipped by the loader's corruption tolerance).\n * Remaining boundaries, declared honestly: the exclusive create is a mutex\n * only where exclusive creation is atomic — local filesystems; multi-host or\n * network-share access is out of scope. `rewrite` still publishes the calling\n * process's atom set as the whole file: rows another process appended since\n * this process last loaded are not merged into a rewrite (the forget caller\n * owns that reconciliation) — the lock closes the interleaving window, not\n * stale in-memory views. And fsync persists file contents, not directory\n * entries (Windows cannot fsync a directory): a crash between rewrite's fsync\n * and its rename leaves the previous file intact, never a half-written one.\n * An fsync failure on append propagates after the row has already reached the\n * OS: the unacknowledged row may be readable, and a caller retry appends it\n * again — the loader's duplicate tolerance (`duplicateSkipped`) absorbs that.\n */\nimport {\n\tcloseSync,\n\tfsyncSync,\n\tmkdirSync,\n\topenSync,\n\treadFileSync,\n\trenameSync,\n\tstatSync,\n\tunlinkSync,\n\twriteFileSync,\n\twriteSync,\n} from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport { sleepSync } from \"../utils/sync-sleep.ts\";\nimport type { MemoryAtomV1 } from \"./foundation.ts\";\nimport { validateMemoryAtomV1 } from \"./foundation.ts\";\n\n/** Sentinel suite domain for sessions without a suite binding (设计 §6.1). */\nexport const LEGACY_MEMORY_DOMAIN = \"legacy\";\n\n/** Suffix of the append lock sibling file (`<ledgerPath>.lock`). */\nconst LOCK_FILE_SUFFIX = \".lock\";\n/**\n * Wall-clock wait between lock acquisition attempts. Kept as a per-file\n * constant even though the durable store backend currently uses the same\n * 25 ms value: the two lock protocols stay independent.\n */\nconst LOCK_POLL_INTERVAL_MS = 25;\n/** Default ceiling on waiting for a competing writer's lock. */\nconst DEFAULT_LOCK_TIMEOUT_MS = 5000;\n/**\n * Default age at which a lock counts as abandoned (its writer crashed while\n * holding it). Deliberately far above any live hold time (milliseconds of\n * appendFileSync) and above the wait timeout, so a waiter fails loudly at the\n * timeout instead of stealing a lock that might still be held.\n */\nconst DEFAULT_STALE_LOCK_MS = 60_000;\n\n/**\n * Writes every byte of `data` to the fd, looping over partial `writeSync`\n * results (a short write would silently break the fsync durability promise:\n * the caller fsyncs and closes only after the full payload is handed to the\n * OS).\n */\nconst writeAllSync = (fd: number, data: string): void => {\n\tconst buffer = Buffer.from(data, \"utf8\");\n\tlet written = 0;\n\twhile (written < buffer.length) {\n\t\twritten += writeSync(fd, buffer, written, buffer.length - written);\n\t}\n};\n\n/** Result of {@link DurableMemoryLedgerV1.load}: replayable atoms plus skip diagnostics. */\nexport interface DurableMemoryLedgerLoadResultV1 {\n\t/** Valid atoms in file order — replay these into a fresh store. */\n\treadonly atoms: readonly MemoryAtomV1[];\n\t/** Malformed lines (invalid JSON or invalid atoms) skipped during load. */\n\treadonly corruptedLines: number;\n\t/** Atoms whose memoryId was already loaded (crash windows can duplicate appends). */\n\treadonly duplicateSkipped: number;\n\t/** True when the file existed but could not be read (permissions, IO error). */\n\treadonly unreadable: boolean;\n}\n\nexport interface DurableMemoryLedgerV1 {\n\t/**\n\t * Reads the ledger file and returns the replayable atom set plus skip\n\t * counters. A missing file is an empty ledger, not an error; malformed or\n\t * duplicate lines are skipped and counted — load NEVER throws.\n\t */\n\tload(): DurableMemoryLedgerLoadResultV1;\n\t/**\n\t * Appends one committed atom as a single JSONL line (strictly validated).\n\t * The write serializes against other processes through the `.lock` sibling\n\t * file and is fsynced before close, so an acknowledged append survives a\n\t * hard crash; a lock timeout or IO failure throws (an append never fails\n\t * silently).\n\t */\n\tappend(atom: MemoryAtomV1): void;\n\t/**\n\t * Physically rewrites the whole file with the given atoms (atomic tmp-file\n\t * + rename). The forget/purge flow calls this after removing atoms so the\n\t * durable replica matches the surviving canonical set. The rewrite holds\n\t * the same cross-process lock as append for its whole duration (same\n\t * timeout and stale-lock-stealing semantics), so a competing writer's\n\t * append cannot be lost inside the rewrite window, and the new content is\n\t * fsynced before the rename.\n\t */\n\trewrite(atoms: readonly MemoryAtomV1[]): void;\n\t/** The ledger's current in-memory atom set (append/rewrite keep it in sync). */\n\tatomsSnapshot(): readonly MemoryAtomV1[];\n}\n\nfunction assertNonEmptyString(value: unknown, label: string): asserts value is string {\n\tif (typeof value !== \"string\" || value.trim().length === 0) throw new Error(`${label} must be a non-empty string`);\n}\n\n/**\n * Creates a durable JSONL memory ledger at `path`. `now` suffixes the rewrite\n * tmp file so deterministic tests can pin it; it never participates in atom\n * identity (timestamps live inside the canonical atoms) and never drives the\n * append lock, which is a real-time mechanism. `lockTimeoutMs` bounds the\n * wall-clock wait for a competing writer's lock; `staleLockMs` is the age at\n * which a lock counts as abandoned and is stolen. Both exist so tests can\n * exercise the timeout and steal paths quickly.\n */\nexport function createDurableMemoryLedger(options: {\n\treadonly path: string;\n\treadonly now?: () => number;\n\treadonly lockTimeoutMs?: number;\n\treadonly staleLockMs?: number;\n}): DurableMemoryLedgerV1 {\n\tassertNonEmptyString(options.path, \"Durable ledger path\");\n\tif (options.lockTimeoutMs !== undefined && (!Number.isFinite(options.lockTimeoutMs) || options.lockTimeoutMs <= 0)) {\n\t\tthrow new Error(\"Durable ledger lockTimeoutMs must be a positive number\");\n\t}\n\tif (options.staleLockMs !== undefined && (!Number.isFinite(options.staleLockMs) || options.staleLockMs <= 0)) {\n\t\tthrow new Error(\"Durable ledger staleLockMs must be a positive number\");\n\t}\n\tconst now = options.now ?? (() => Date.now());\n\tconst path = options.path;\n\tconst lockTimeoutMs = options.lockTimeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS;\n\tconst staleLockMs = options.staleLockMs ?? DEFAULT_STALE_LOCK_MS;\n\tconst lockPath = `${path}${LOCK_FILE_SUFFIX}`;\n\t/** In-memory replica of the durable atom set; kept in sync by append/rewrite/load. */\n\tlet atoms: MemoryAtomV1[] = [];\n\n\tconst ensureDir = (): void => {\n\t\tmkdirSync(dirname(path), { recursive: true });\n\t};\n\n\t/**\n\t * Takes the ledger writer lock, shared by append and rewrite: exclusive\n\t * create (`wx`) is the mutex; an existing lock is polled until released,\n\t * stolen once its mtime exceeds `staleLockMs` (crashed writer), or turned\n\t * into a thrown error at the wait deadline. The lock file carries a\n\t * per-acquisition token so a stale-steal victim releasing late can never\n\t * delete the thief's fresh lock.\n\t */\n\tconst acquireAppendLock = (): string => {\n\t\tconst deadline = Date.now() + lockTimeoutMs;\n\t\tfor (;;) {\n\t\t\tensureDir();\n\t\t\tconst token = `${process.pid}:${Date.now()}`;\n\t\t\ttry {\n\t\t\t\tconst fd = openSync(lockPath, \"wx\");\n\t\t\t\ttry {\n\t\t\t\t\twriteFileSync(fd, token);\n\t\t\t\t} finally {\n\t\t\t\t\tcloseSync(fd);\n\t\t\t\t}\n\t\t\t\treturn token;\n\t\t\t} catch (error) {\n\t\t\t\t// POSIX maps a lost create race to EEXIST; Windows/libuv maps the\n\t\t\t\t// same concurrent-create sharing violation to EPERM. Both mean the\n\t\t\t\t// lock file existed when we tried to create it — contend (poll /\n\t\t\t\t// stale-steal below). For EPERM the existence check re-stats: a\n\t\t\t\t// lock still there is contention, while ENOENT means the holder\n\t\t\t\t// released and deleted it between the failed open and this stat\n\t\t\t\t// (or Windows briefly left a delete-pending name that has now\n\t\t\t\t// gone) — the same vanished-lock window the EEXIST path tolerates,\n\t\t\t\t// so retry too. A stat error other than ENOENT means the EPERM was\n\t\t\t\t// a genuine permission failure and propagates, and the vanish path\n\t\t\t\t// still rethrows the original EPERM at the wait deadline: a\n\t\t\t\t// persistent EPERM with no lock file ever appearing is a genuine\n\t\t\t\t// permission failure, not a held lock.\n\t\t\t\tconst code = (error as NodeJS.ErrnoException).code;\n\t\t\t\tif (code === \"EPERM\") {\n\t\t\t\t\tlet vanished = false;\n\t\t\t\t\ttry {\n\t\t\t\t\t\tstatSync(lockPath);\n\t\t\t\t\t} catch (statError) {\n\t\t\t\t\t\tif ((statError as NodeJS.ErrnoException).code !== \"ENOENT\") throw error;\n\t\t\t\t\t\tvanished = true;\n\t\t\t\t\t}\n\t\t\t\t\tif (vanished) {\n\t\t\t\t\t\tif (Date.now() >= deadline) throw error;\n\t\t\t\t\t\tsleepSync(LOCK_POLL_INTERVAL_MS);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t} else if (code !== \"EEXIST\") {\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t\tlet stale = false;\n\t\t\t\ttry {\n\t\t\t\t\tstale = Date.now() - statSync(lockPath).mtimeMs > staleLockMs;\n\t\t\t\t} catch {\n\t\t\t\t\t// The holder released the lock between the failed open and the\n\t\t\t\t\t// stat — retry the exclusive create immediately.\n\t\t\t\t}\n\t\t\t\tif (stale) {\n\t\t\t\t\t// A crashed writer left the lock behind: steal it. A competing\n\t\t\t\t\t// waiter may steal first — the next exclusive create resolves that.\n\t\t\t\t\ttry {\n\t\t\t\t\t\tunlinkSync(lockPath);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// Already stolen — retry.\n\t\t\t\t\t}\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (Date.now() >= deadline) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`memory ledger append lock timeout after ${lockTimeoutMs} ms: ${lockPath} is held by another writer`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tsleepSync(LOCK_POLL_INTERVAL_MS);\n\t\t\t}\n\t\t}\n\t};\n\n\t/** Releases the append lock, but only if our own token still owns it. */\n\tconst releaseAppendLock = (token: string): void => {\n\t\ttry {\n\t\t\tif (readFileSync(lockPath, \"utf8\") === token) unlinkSync(lockPath);\n\t\t} catch {\n\t\t\t// The lock is already gone (or unreadable) — nothing to release.\n\t\t}\n\t};\n\n\tconst parseAtomLine = (line: string): MemoryAtomV1 | undefined => {\n\t\ttry {\n\t\t\treturn validateMemoryAtomV1(JSON.parse(line));\n\t\t} catch {\n\t\t\t// Corruption tolerance: a malformed line is skipped and counted by the\n\t\t\t// caller — a damaged ledger must never fail the startup replay.\n\t\t\treturn undefined;\n\t\t}\n\t};\n\n\treturn {\n\t\tload() {\n\t\t\tlet raw: string;\n\t\t\ttry {\n\t\t\t\traw = readFileSync(path, \"utf8\");\n\t\t\t} catch (error) {\n\t\t\t\tif ((error as NodeJS.ErrnoException).code === \"ENOENT\") {\n\t\t\t\t\tatoms = [];\n\t\t\t\t\treturn { atoms: Object.freeze([]), corruptedLines: 0, duplicateSkipped: 0, unreadable: false };\n\t\t\t\t}\n\t\t\t\tatoms = [];\n\t\t\t\treturn { atoms: Object.freeze([]), corruptedLines: 0, duplicateSkipped: 0, unreadable: true };\n\t\t\t}\n\t\t\tconst loaded: MemoryAtomV1[] = [];\n\t\t\tconst seen = new Set<string>();\n\t\t\tlet corruptedLines = 0;\n\t\t\tlet duplicateSkipped = 0;\n\t\t\tfor (const line of raw.split(\"\\n\")) {\n\t\t\t\tif (line.trim() === \"\") continue;\n\t\t\t\tconst atom = parseAtomLine(line);\n\t\t\t\tif (!atom) {\n\t\t\t\t\tcorruptedLines += 1;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (seen.has(atom.memoryId)) {\n\t\t\t\t\tduplicateSkipped += 1;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tseen.add(atom.memoryId);\n\t\t\t\tloaded.push(atom);\n\t\t\t}\n\t\t\tatoms = loaded;\n\t\t\treturn {\n\t\t\t\tatoms: Object.freeze([...loaded]),\n\t\t\t\tcorruptedLines,\n\t\t\t\tduplicateSkipped,\n\t\t\t\tunreadable: false,\n\t\t\t};\n\t\t},\n\t\tappend(atom) {\n\t\t\tconst validated = validateMemoryAtomV1(atom);\n\t\t\tconst token = acquireAppendLock();\n\t\t\ttry {\n\t\t\t\t// One committed atom per line; the trailing newline keeps the file\n\t\t\t\t// a well-formed JSONL stream for crash-truncated-tail recovery.\n\t\t\t\t// The line is fsynced before close: once append returns, a hard\n\t\t\t\t// crash cannot lose it.\n\t\t\t\tconst fd = openSync(path, \"a\");\n\t\t\t\ttry {\n\t\t\t\t\twriteAllSync(fd, `${JSON.stringify(validated)}\\n`);\n\t\t\t\t\tfsyncSync(fd);\n\t\t\t\t} finally {\n\t\t\t\t\tcloseSync(fd);\n\t\t\t\t}\n\t\t\t} finally {\n\t\t\t\treleaseAppendLock(token);\n\t\t\t}\n\t\t\tatoms.push(validated);\n\t\t},\n\t\trewrite(nextAtoms) {\n\t\t\tconst validated = nextAtoms.map((atom) => validateMemoryAtomV1(atom));\n\t\t\t// The rewrite shares the append lock: a competing writer either\n\t\t\t// finished (its row is on disk and observable to the caller's loaded\n\t\t\t// view) or waits out the whole rebuild — no append can land inside\n\t\t\t// the rewrite window anymore.\n\t\t\tconst token = acquireAppendLock();\n\t\t\ttry {\n\t\t\t\tensureDir();\n\t\t\t\tconst tmpPath = `${path}.${now()}.rewrite.tmp`;\n\t\t\t\t// fsync the rebuilt file before the atomic rename: once rewrite\n\t\t\t\t// returns, a hard crash cannot lose the new content (and the\n\t\t\t\t// rename itself means the old file survives intact until then).\n\t\t\t\tconst fd = openSync(tmpPath, \"w\");\n\t\t\t\ttry {\n\t\t\t\t\twriteAllSync(fd, validated.map((atom) => `${JSON.stringify(atom)}\\n`).join(\"\"));\n\t\t\t\t\tfsyncSync(fd);\n\t\t\t\t} finally {\n\t\t\t\t\tcloseSync(fd);\n\t\t\t\t}\n\t\t\t\trenameSync(tmpPath, path);\n\t\t\t} finally {\n\t\t\t\treleaseAppendLock(token);\n\t\t\t}\n\t\t\tatoms = [...validated];\n\t\t},\n\t\tatomsSnapshot() {\n\t\t\treturn Object.freeze([...atoms]);\n\t\t},\n\t};\n}\n"]}
|
|
1
|
+
{"version":3,"file":"ledger.js","sourceRoot":"","sources":["../../src/memory/ledger.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Durable memory ledger (统一修复轮 A, 方案系统设计 §6.1 + 方案实施计划 §11 A) —\n * the append-only JSONL persistent replica of a Memory Foundation store.\n *\n * Layout: `<agentDir>/memory/<owner>/ledger-<suiteId|\"legacy\">.jsonl`. The\n * owner segment is the store/facade isolation principal (one local user per\n * agentDir in this product); the suite segment is the 方案 read-boundary\n * domain — a real suite id, or the sentinel \"legacy\" for sessions without a\n * suite binding. One line holds exactly one committed canonical atom\n * (contract @2, including `suiteId`).\n *\n * Replica semantics: the write path appends every committed atom (canonical\n * copy on disk); the read path `load()`s the atoms at startup and replays them\n * into a fresh store. After a forget, `rewrite(atom[])` physically rewrites\n * the file so purged atoms cannot survive a restart. Corruption tolerance:\n * malformed lines are skipped and counted, duplicates are skipped and counted\n * — loading never fails. Only the WRITE path validates strictly: a ledger must\n * never persist an atom the Foundation would reject.\n *\n * Concurrency and durability envelope (统一修复轮 H1, #7 深度修复; rewrite 共\n * 锁 + fsync 关洞修复): competing writers — append AND rewrite — serialize\n * through a `.lock` sibling file taken via exclusive creation (`wx` — atomic\n * on every supported platform). A second writer polls (25 ms) until the\n * holder removes the lock, steals a lock whose mtime is older than\n * `staleLockMs` (default 60 s — a crashed writer left it behind; a live\n * holder keeps the lock for milliseconds), and throws after `lockTimeoutMs`\n * (default 5 s, configurable) — a writer never silently blocks and never\n * silently drops its row; lock or IO failures propagate to the caller. The\n * lock is wall-clock based (real waiting), not the injected deterministic\n * `now`. Durability: both write paths run open → write → fsync → close, so the\n * file contents of an acknowledged append or rewrite survive a hard crash,\n * subject to the directory-entry boundary declared below (a torn trailing\n * line, possible only in files written by older builds or damaged externally,\n * is still skipped by the loader's corruption tolerance).\n * Remaining boundaries, declared honestly: the exclusive create is a mutex\n * only where exclusive creation is atomic — local filesystems; multi-host or\n * network-share access is out of scope. `rewrite` still publishes the calling\n * process's atom set as the whole file: rows another process appended since\n * this process last loaded are not merged into a rewrite (the forget caller\n * owns that reconciliation) — the lock closes the interleaving window, not\n * stale in-memory views. And fsync persists file contents, not directory\n * entries (Windows cannot fsync a directory): a crash between rewrite's fsync\n * and its rename leaves the previous file intact, never a half-written one.\n * An fsync failure on append propagates after the row has already reached the\n * OS: the unacknowledged row may be readable, and a caller retry appends it\n * again — the loader's duplicate tolerance (`duplicateSkipped`) absorbs that.\n */\nimport {\n\tcloseSync,\n\tfsyncSync,\n\tmkdirSync,\n\topenSync,\n\treaddirSync,\n\treadFileSync,\n\trenameSync,\n\tstatSync,\n\tunlinkSync,\n\twriteFileSync,\n\twriteSync,\n} from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { sleepSync } from \"../utils/sync-sleep.ts\";\nimport type { MemoryAtomV1 } from \"./foundation.ts\";\nimport { validateMemoryAtomV1 } from \"./foundation.ts\";\n\n/** Sentinel suite domain for sessions without a suite binding (设计 §6.1). */\nexport const LEGACY_MEMORY_DOMAIN = \"legacy\";\n\n/** Suffix of the append lock sibling file (`<ledgerPath>.lock`). */\nconst LOCK_FILE_SUFFIX = \".lock\";\n/**\n * Wall-clock wait between lock acquisition attempts. Kept as a per-file\n * constant even though the durable store backend currently uses the same\n * 25 ms value: the two lock protocols stay independent.\n */\nconst LOCK_POLL_INTERVAL_MS = 25;\n/** Default ceiling on waiting for a competing writer's lock. */\nconst DEFAULT_LOCK_TIMEOUT_MS = 5000;\n/**\n * Default age at which a lock counts as abandoned (its writer crashed while\n * holding it). Deliberately far above any live hold time (milliseconds of\n * appendFileSync) and above the wait timeout, so a waiter fails loudly at the\n * timeout instead of stealing a lock that might still be held.\n */\nconst DEFAULT_STALE_LOCK_MS = 60_000;\n\n/**\n * Writes every byte of `data` to the fd, looping over partial `writeSync`\n * results (a short write would silently break the fsync durability promise:\n * the caller fsyncs and closes only after the full payload is handed to the\n * OS).\n */\nconst writeAllSync = (fd: number, data: string): void => {\n\tconst buffer = Buffer.from(data, \"utf8\");\n\tlet written = 0;\n\twhile (written < buffer.length) {\n\t\twritten += writeSync(fd, buffer, written, buffer.length - written);\n\t}\n};\n\n/** Result of {@link DurableMemoryLedgerV1.load}: replayable atoms plus skip diagnostics. */\nexport interface DurableMemoryLedgerLoadResultV1 {\n\t/** Valid atoms in file order — replay these into a fresh store. */\n\treadonly atoms: readonly MemoryAtomV1[];\n\t/** Malformed lines (invalid JSON or invalid atoms) skipped during load. */\n\treadonly corruptedLines: number;\n\t/** Atoms whose memoryId was already loaded (crash windows can duplicate appends). */\n\treadonly duplicateSkipped: number;\n\t/** True when the file existed but could not be read (permissions, IO error). */\n\treadonly unreadable: boolean;\n}\n\nexport interface DurableMemoryLedgerV1 {\n\t/**\n\t * Reads the ledger file and returns the replayable atom set plus skip\n\t * counters. A missing file is an empty ledger, not an error; malformed or\n\t * duplicate lines are skipped and counted — load NEVER throws.\n\t */\n\tload(): DurableMemoryLedgerLoadResultV1;\n\t/**\n\t * Appends one committed atom as a single JSONL line (strictly validated).\n\t * The write serializes against other processes through the `.lock` sibling\n\t * file and is fsynced before close, so an acknowledged append survives a\n\t * hard crash; a lock timeout or IO failure throws (an append never fails\n\t * silently).\n\t */\n\tappend(atom: MemoryAtomV1): void;\n\t/**\n\t * Physically rewrites the whole file with the given atoms (atomic tmp-file\n\t * + rename). The forget/purge flow calls this after removing atoms so the\n\t * durable replica matches the surviving canonical set. The rewrite holds\n\t * the same cross-process lock as append for its whole duration (same\n\t * timeout and stale-lock-stealing semantics), so a competing writer's\n\t * append cannot be lost inside the rewrite window, and the new content is\n\t * fsynced before the rename.\n\t */\n\trewrite(atoms: readonly MemoryAtomV1[]): void;\n\t/** The ledger's current in-memory atom set (append/rewrite keep it in sync). */\n\tatomsSnapshot(): readonly MemoryAtomV1[];\n}\n\nfunction assertNonEmptyString(value: unknown, label: string): asserts value is string {\n\tif (typeof value !== \"string\" || value.trim().length === 0) throw new Error(`${label} must be a non-empty string`);\n}\n\n/**\n * Creates a durable JSONL memory ledger at `path`. `now` suffixes the rewrite\n * tmp file so deterministic tests can pin it; it never participates in atom\n * identity (timestamps live inside the canonical atoms) and never drives the\n * append lock, which is a real-time mechanism. `lockTimeoutMs` bounds the\n * wall-clock wait for a competing writer's lock; `staleLockMs` is the age at\n * which a lock counts as abandoned and is stolen. Both exist so tests can\n * exercise the timeout and steal paths quickly.\n */\n/**\n * 跨进程 canonical 身份扫描 (2026-10-07 对账反向清理守卫): 只读扫描 owner 名下\n * 全部域账本文件的 memoryId 并集。逐文件用临时 ledger 实例 load (assistant-card\n * 扫描先例), 不触碰调用进程任何 ledger 实例的内存快照。complete=false 表示\n * 有账本文件读取失败 (非 ENOENT 的 IO 错误) —— 调用方必须禁用本轮基于缺席的\n * 删除, 防瞬时 IO 错误把其他会话的活跃行放大成误删; 行级损坏容错 (跳过坏行)\n * 是 loader 的既 declared 语义, 不影响 complete。\n */\nexport function scanOwnerLedgerMemoryIds(options: { readonly agentDir: string; readonly owner: string }): {\n\treadonly memoryIds: ReadonlySet<string>;\n\treadonly complete: boolean;\n} {\n\tif (options.owner.trim() === \"\") throw new Error(\"scanOwnerLedgerMemoryIds owner must be a non-empty string\");\n\tconst ledgerDir = join(options.agentDir, \"memory\", options.owner);\n\tlet files: string[];\n\ttry {\n\t\tfiles = readdirSync(ledgerDir).filter((name) => name.startsWith(\"ledger-\") && name.endsWith(\".jsonl\"));\n\t} catch (error) {\n\t\tif ((error as NodeJS.ErrnoException).code === \"ENOENT\") {\n\t\t\treturn { memoryIds: new Set(), complete: true };\n\t\t}\n\t\treturn { memoryIds: new Set(), complete: false };\n\t}\n\tconst memoryIds = new Set<string>();\n\tlet complete = true;\n\tfor (const file of files) {\n\t\tconst result = createDurableMemoryLedger({ path: join(ledgerDir, file) }).load();\n\t\tif (result.unreadable) complete = false;\n\t\tfor (const atom of result.atoms) memoryIds.add(atom.memoryId);\n\t}\n\treturn { memoryIds, complete };\n}\n\nexport function createDurableMemoryLedger(options: {\n\treadonly path: string;\n\treadonly now?: () => number;\n\treadonly lockTimeoutMs?: number;\n\treadonly staleLockMs?: number;\n}): DurableMemoryLedgerV1 {\n\tassertNonEmptyString(options.path, \"Durable ledger path\");\n\tif (options.lockTimeoutMs !== undefined && (!Number.isFinite(options.lockTimeoutMs) || options.lockTimeoutMs <= 0)) {\n\t\tthrow new Error(\"Durable ledger lockTimeoutMs must be a positive number\");\n\t}\n\tif (options.staleLockMs !== undefined && (!Number.isFinite(options.staleLockMs) || options.staleLockMs <= 0)) {\n\t\tthrow new Error(\"Durable ledger staleLockMs must be a positive number\");\n\t}\n\tconst now = options.now ?? (() => Date.now());\n\tconst path = options.path;\n\tconst lockTimeoutMs = options.lockTimeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS;\n\tconst staleLockMs = options.staleLockMs ?? DEFAULT_STALE_LOCK_MS;\n\tconst lockPath = `${path}${LOCK_FILE_SUFFIX}`;\n\t/** In-memory replica of the durable atom set; kept in sync by append/rewrite/load. */\n\tlet atoms: MemoryAtomV1[] = [];\n\n\tconst ensureDir = (): void => {\n\t\tmkdirSync(dirname(path), { recursive: true });\n\t};\n\n\t/**\n\t * Takes the ledger writer lock, shared by append and rewrite: exclusive\n\t * create (`wx`) is the mutex; an existing lock is polled until released,\n\t * stolen once its mtime exceeds `staleLockMs` (crashed writer), or turned\n\t * into a thrown error at the wait deadline. The lock file carries a\n\t * per-acquisition token so a stale-steal victim releasing late can never\n\t * delete the thief's fresh lock.\n\t */\n\tconst acquireAppendLock = (): string => {\n\t\tconst deadline = Date.now() + lockTimeoutMs;\n\t\tfor (;;) {\n\t\t\tensureDir();\n\t\t\tconst token = `${process.pid}:${Date.now()}`;\n\t\t\ttry {\n\t\t\t\tconst fd = openSync(lockPath, \"wx\");\n\t\t\t\ttry {\n\t\t\t\t\twriteFileSync(fd, token);\n\t\t\t\t} finally {\n\t\t\t\t\tcloseSync(fd);\n\t\t\t\t}\n\t\t\t\treturn token;\n\t\t\t} catch (error) {\n\t\t\t\t// POSIX maps a lost create race to EEXIST; Windows/libuv maps the\n\t\t\t\t// same concurrent-create sharing violation to EPERM. Both mean the\n\t\t\t\t// lock file existed when we tried to create it — contend (poll /\n\t\t\t\t// stale-steal below). For EPERM the existence check re-stats: a\n\t\t\t\t// lock still there is contention, while ENOENT means the holder\n\t\t\t\t// released and deleted it between the failed open and this stat\n\t\t\t\t// (or Windows briefly left a delete-pending name that has now\n\t\t\t\t// gone) — the same vanished-lock window the EEXIST path tolerates,\n\t\t\t\t// so retry too. A stat error other than ENOENT means the EPERM was\n\t\t\t\t// a genuine permission failure and propagates, and the vanish path\n\t\t\t\t// still rethrows the original EPERM at the wait deadline: a\n\t\t\t\t// persistent EPERM with no lock file ever appearing is a genuine\n\t\t\t\t// permission failure, not a held lock.\n\t\t\t\tconst code = (error as NodeJS.ErrnoException).code;\n\t\t\t\tif (code === \"EPERM\") {\n\t\t\t\t\tlet vanished = false;\n\t\t\t\t\ttry {\n\t\t\t\t\t\tstatSync(lockPath);\n\t\t\t\t\t} catch (statError) {\n\t\t\t\t\t\tif ((statError as NodeJS.ErrnoException).code !== \"ENOENT\") throw error;\n\t\t\t\t\t\tvanished = true;\n\t\t\t\t\t}\n\t\t\t\t\tif (vanished) {\n\t\t\t\t\t\tif (Date.now() >= deadline) throw error;\n\t\t\t\t\t\tsleepSync(LOCK_POLL_INTERVAL_MS);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t} else if (code !== \"EEXIST\") {\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t\tlet stale = false;\n\t\t\t\ttry {\n\t\t\t\t\tstale = Date.now() - statSync(lockPath).mtimeMs > staleLockMs;\n\t\t\t\t} catch {\n\t\t\t\t\t// The holder released the lock between the failed open and the\n\t\t\t\t\t// stat — retry the exclusive create immediately.\n\t\t\t\t}\n\t\t\t\tif (stale) {\n\t\t\t\t\t// A crashed writer left the lock behind: steal it. A competing\n\t\t\t\t\t// waiter may steal first — the next exclusive create resolves that.\n\t\t\t\t\ttry {\n\t\t\t\t\t\tunlinkSync(lockPath);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// Already stolen — retry.\n\t\t\t\t\t}\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (Date.now() >= deadline) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`memory ledger append lock timeout after ${lockTimeoutMs} ms: ${lockPath} is held by another writer`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tsleepSync(LOCK_POLL_INTERVAL_MS);\n\t\t\t}\n\t\t}\n\t};\n\n\t/** Releases the append lock, but only if our own token still owns it. */\n\tconst releaseAppendLock = (token: string): void => {\n\t\ttry {\n\t\t\tif (readFileSync(lockPath, \"utf8\") === token) unlinkSync(lockPath);\n\t\t} catch {\n\t\t\t// The lock is already gone (or unreadable) — nothing to release.\n\t\t}\n\t};\n\n\tconst parseAtomLine = (line: string): MemoryAtomV1 | undefined => {\n\t\ttry {\n\t\t\treturn validateMemoryAtomV1(JSON.parse(line));\n\t\t} catch {\n\t\t\t// Corruption tolerance: a malformed line is skipped and counted by the\n\t\t\t// caller — a damaged ledger must never fail the startup replay.\n\t\t\treturn undefined;\n\t\t}\n\t};\n\n\treturn {\n\t\tload() {\n\t\t\tlet raw: string;\n\t\t\ttry {\n\t\t\t\traw = readFileSync(path, \"utf8\");\n\t\t\t} catch (error) {\n\t\t\t\tif ((error as NodeJS.ErrnoException).code === \"ENOENT\") {\n\t\t\t\t\tatoms = [];\n\t\t\t\t\treturn { atoms: Object.freeze([]), corruptedLines: 0, duplicateSkipped: 0, unreadable: false };\n\t\t\t\t}\n\t\t\t\tatoms = [];\n\t\t\t\treturn { atoms: Object.freeze([]), corruptedLines: 0, duplicateSkipped: 0, unreadable: true };\n\t\t\t}\n\t\t\tconst loaded: MemoryAtomV1[] = [];\n\t\t\tconst seen = new Set<string>();\n\t\t\tlet corruptedLines = 0;\n\t\t\tlet duplicateSkipped = 0;\n\t\t\tfor (const line of raw.split(\"\\n\")) {\n\t\t\t\tif (line.trim() === \"\") continue;\n\t\t\t\tconst atom = parseAtomLine(line);\n\t\t\t\tif (!atom) {\n\t\t\t\t\tcorruptedLines += 1;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (seen.has(atom.memoryId)) {\n\t\t\t\t\tduplicateSkipped += 1;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tseen.add(atom.memoryId);\n\t\t\t\tloaded.push(atom);\n\t\t\t}\n\t\t\tatoms = loaded;\n\t\t\treturn {\n\t\t\t\tatoms: Object.freeze([...loaded]),\n\t\t\t\tcorruptedLines,\n\t\t\t\tduplicateSkipped,\n\t\t\t\tunreadable: false,\n\t\t\t};\n\t\t},\n\t\tappend(atom) {\n\t\t\tconst validated = validateMemoryAtomV1(atom);\n\t\t\tconst token = acquireAppendLock();\n\t\t\ttry {\n\t\t\t\t// One committed atom per line; the trailing newline keeps the file\n\t\t\t\t// a well-formed JSONL stream for crash-truncated-tail recovery.\n\t\t\t\t// The line is fsynced before close: once append returns, a hard\n\t\t\t\t// crash cannot lose it.\n\t\t\t\tconst fd = openSync(path, \"a\");\n\t\t\t\ttry {\n\t\t\t\t\twriteAllSync(fd, `${JSON.stringify(validated)}\\n`);\n\t\t\t\t\tfsyncSync(fd);\n\t\t\t\t} finally {\n\t\t\t\t\tcloseSync(fd);\n\t\t\t\t}\n\t\t\t} finally {\n\t\t\t\treleaseAppendLock(token);\n\t\t\t}\n\t\t\tatoms.push(validated);\n\t\t},\n\t\trewrite(nextAtoms) {\n\t\t\tconst validated = nextAtoms.map((atom) => validateMemoryAtomV1(atom));\n\t\t\t// The rewrite shares the append lock: a competing writer either\n\t\t\t// finished (its row is on disk and observable to the caller's loaded\n\t\t\t// view) or waits out the whole rebuild — no append can land inside\n\t\t\t// the rewrite window anymore.\n\t\t\tconst token = acquireAppendLock();\n\t\t\ttry {\n\t\t\t\tensureDir();\n\t\t\t\tconst tmpPath = `${path}.${now()}.rewrite.tmp`;\n\t\t\t\t// fsync the rebuilt file before the atomic rename: once rewrite\n\t\t\t\t// returns, a hard crash cannot lose the new content (and the\n\t\t\t\t// rename itself means the old file survives intact until then).\n\t\t\t\tconst fd = openSync(tmpPath, \"w\");\n\t\t\t\ttry {\n\t\t\t\t\twriteAllSync(fd, validated.map((atom) => `${JSON.stringify(atom)}\\n`).join(\"\"));\n\t\t\t\t\tfsyncSync(fd);\n\t\t\t\t} finally {\n\t\t\t\t\tcloseSync(fd);\n\t\t\t\t}\n\t\t\t\trenameSync(tmpPath, path);\n\t\t\t} finally {\n\t\t\t\treleaseAppendLock(token);\n\t\t\t}\n\t\t\tatoms = [...validated];\n\t\t},\n\t\tatomsSnapshot() {\n\t\t\treturn Object.freeze([...atoms]);\n\t\t},\n\t};\n}\n"]}
|
|
@@ -15,4 +15,14 @@
|
|
|
15
15
|
* file, the subsequent download attempt should fail with that truth.
|
|
16
16
|
*/
|
|
17
17
|
export declare function purgeZeroByteCacheArtifacts(modelDir: string): void;
|
|
18
|
+
/**
|
|
19
|
+
* transformers 上游缺陷签名标注 (2026-10-06 live E2E 终审 P1-1): 镜像对
|
|
20
|
+
* tokenizer_config.json 的元数据请求瞬时 404 时, transformers 把
|
|
21
|
+
* `{exists:false}` 作为**成功结果**做模块级 memoize (key 不含 host), 之后
|
|
22
|
+
* 同进程内任何重试/换 host 都拿到同一空清单, `AutoTokenizer` 解构 undefined
|
|
23
|
+
* 抛 "reading 'tokenizer_class'"。此类失败在当前进程内不可自愈 (同 host
|
|
24
|
+
* 退避重试只覆盖 rejection 型瞬断) — 如实标注, 让"重启会话"成为明确的
|
|
25
|
+
* 有效动作而不是被失败行动清单的 retry 误导。
|
|
26
|
+
*/
|
|
27
|
+
export declare function annotateUpstreamListingPoison(error: unknown): unknown;
|
|
18
28
|
//# sourceMappingURL=model-cache-hygiene.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model-cache-hygiene.d.ts","sourceRoot":"","sources":["../../src/memory/model-cache-hygiene.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,2BAA2B,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAiBlE","sourcesContent":["import type { Dirent } from \"node:fs\";\nimport { readdirSync, rmSync, statSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\n/**\n * Zero-byte cache-artifact hygiene for transformers.js model downloads.\n *\n * A failed download attempt (unreachable mirror, aborted transfer) can leave a\n * 0-byte file in the cache; transformers.js then treats the file as a cache\n * hit on the NEXT attempt — including attempts against a different mirror —\n * and never heals. Empty tokenizer/weight files are unambiguously failed\n * downloads (real tokenizer.json is never empty), so the mirror-rotation loop\n * purges zero-byte files between hosts. Non-empty-but-truncated files are\n * deliberately NOT touched: those stay terminal per 设计 §3.2 (manual action\n * list), because deleting user-provided manual imports on a guess would be\n * worse than failing loudly.\n *\n * Best effort: purge failures propagate — if we cannot even delete an empty\n * file, the subsequent download attempt should fail with that truth.\n */\nexport function purgeZeroByteCacheArtifacts(modelDir: string): void {\n\tlet entries: Dirent[];\n\ttry {\n\t\tentries = readdirSync(modelDir, { withFileTypes: true }) as Dirent[];\n\t} catch {\n\t\treturn; // No dir yet = nothing to purge.\n\t}\n\tfor (const entry of entries) {\n\t\tconst entryPath = join(modelDir, entry.name.toString());\n\t\tif (entry.isDirectory()) {\n\t\t\tpurgeZeroByteCacheArtifacts(entryPath);\n\t\t\tcontinue;\n\t\t}\n\t\tif (statSync(entryPath).size === 0) {\n\t\t\trmSync(entryPath, { force: true });\n\t\t}\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"model-cache-hygiene.d.ts","sourceRoot":"","sources":["../../src/memory/model-cache-hygiene.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,2BAA2B,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAiBlE;AAED;;;;;;;;GAQG;AACH,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAMrE","sourcesContent":["import type { Dirent } from \"node:fs\";\nimport { readdirSync, rmSync, statSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\n/**\n * Zero-byte cache-artifact hygiene for transformers.js model downloads.\n *\n * A failed download attempt (unreachable mirror, aborted transfer) can leave a\n * 0-byte file in the cache; transformers.js then treats the file as a cache\n * hit on the NEXT attempt — including attempts against a different mirror —\n * and never heals. Empty tokenizer/weight files are unambiguously failed\n * downloads (real tokenizer.json is never empty), so the mirror-rotation loop\n * purges zero-byte files between hosts. Non-empty-but-truncated files are\n * deliberately NOT touched: those stay terminal per 设计 §3.2 (manual action\n * list), because deleting user-provided manual imports on a guess would be\n * worse than failing loudly.\n *\n * Best effort: purge failures propagate — if we cannot even delete an empty\n * file, the subsequent download attempt should fail with that truth.\n */\nexport function purgeZeroByteCacheArtifacts(modelDir: string): void {\n\tlet entries: Dirent[];\n\ttry {\n\t\tentries = readdirSync(modelDir, { withFileTypes: true }) as Dirent[];\n\t} catch {\n\t\treturn; // No dir yet = nothing to purge.\n\t}\n\tfor (const entry of entries) {\n\t\tconst entryPath = join(modelDir, entry.name.toString());\n\t\tif (entry.isDirectory()) {\n\t\t\tpurgeZeroByteCacheArtifacts(entryPath);\n\t\t\tcontinue;\n\t\t}\n\t\tif (statSync(entryPath).size === 0) {\n\t\t\trmSync(entryPath, { force: true });\n\t\t}\n\t}\n}\n\n/**\n * transformers 上游缺陷签名标注 (2026-10-06 live E2E 终审 P1-1): 镜像对\n * tokenizer_config.json 的元数据请求瞬时 404 时, transformers 把\n * `{exists:false}` 作为**成功结果**做模块级 memoize (key 不含 host), 之后\n * 同进程内任何重试/换 host 都拿到同一空清单, `AutoTokenizer` 解构 undefined\n * 抛 \"reading 'tokenizer_class'\"。此类失败在当前进程内不可自愈 (同 host\n * 退避重试只覆盖 rejection 型瞬断) — 如实标注, 让\"重启会话\"成为明确的\n * 有效动作而不是被失败行动清单的 retry 误导。\n */\nexport function annotateUpstreamListingPoison(error: unknown): unknown {\n\tconst message = error instanceof Error ? error.message : String(error);\n\tif (!message.includes(\"reading 'tokenizer_class'\")) return error;\n\treturn new Error(\n\t\t`${message} — the mirror served an incomplete file listing which transformers cached for this process (upstream bug); retries inside the current session cannot clear it, restart the session and retry`,\n\t);\n}\n"]}
|
|
@@ -35,4 +35,19 @@ export function purgeZeroByteCacheArtifacts(modelDir) {
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* transformers 上游缺陷签名标注 (2026-10-06 live E2E 终审 P1-1): 镜像对
|
|
40
|
+
* tokenizer_config.json 的元数据请求瞬时 404 时, transformers 把
|
|
41
|
+
* `{exists:false}` 作为**成功结果**做模块级 memoize (key 不含 host), 之后
|
|
42
|
+
* 同进程内任何重试/换 host 都拿到同一空清单, `AutoTokenizer` 解构 undefined
|
|
43
|
+
* 抛 "reading 'tokenizer_class'"。此类失败在当前进程内不可自愈 (同 host
|
|
44
|
+
* 退避重试只覆盖 rejection 型瞬断) — 如实标注, 让"重启会话"成为明确的
|
|
45
|
+
* 有效动作而不是被失败行动清单的 retry 误导。
|
|
46
|
+
*/
|
|
47
|
+
export function annotateUpstreamListingPoison(error) {
|
|
48
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
49
|
+
if (!message.includes("reading 'tokenizer_class'"))
|
|
50
|
+
return error;
|
|
51
|
+
return new Error(`${message} — the mirror served an incomplete file listing which transformers cached for this process (upstream bug); retries inside the current session cannot clear it, restart the session and retry`);
|
|
52
|
+
}
|
|
38
53
|
//# sourceMappingURL=model-cache-hygiene.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model-cache-hygiene.js","sourceRoot":"","sources":["../../src/memory/model-cache-hygiene.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACxD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,2BAA2B,CAAC,QAAgB,EAAQ;IACnE,IAAI,OAAiB,CAAC;IACtB,IAAI,CAAC;QACJ,OAAO,GAAG,WAAW,CAAC,QAAQ,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAa,CAAC;IACtE,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,CAAC,iCAAiC;IAC1C,CAAC;IACD,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,SAAS,GAAG,IAAI,CAAC,QAAQ,EAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;QACxD,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;YACzB,2BAA2B,CAAC,SAAS,CAAC,CAAC;YACvC,SAAS;QACV,CAAC;QACD,IAAI,QAAQ,CAAC,SAAS,CAAC,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;YACpC,MAAM,CAAC,SAAS,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACpC,CAAC;IACF,CAAC;AAAA,CACD","sourcesContent":["import type { Dirent } from \"node:fs\";\nimport { readdirSync, rmSync, statSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\n/**\n * Zero-byte cache-artifact hygiene for transformers.js model downloads.\n *\n * A failed download attempt (unreachable mirror, aborted transfer) can leave a\n * 0-byte file in the cache; transformers.js then treats the file as a cache\n * hit on the NEXT attempt — including attempts against a different mirror —\n * and never heals. Empty tokenizer/weight files are unambiguously failed\n * downloads (real tokenizer.json is never empty), so the mirror-rotation loop\n * purges zero-byte files between hosts. Non-empty-but-truncated files are\n * deliberately NOT touched: those stay terminal per 设计 §3.2 (manual action\n * list), because deleting user-provided manual imports on a guess would be\n * worse than failing loudly.\n *\n * Best effort: purge failures propagate — if we cannot even delete an empty\n * file, the subsequent download attempt should fail with that truth.\n */\nexport function purgeZeroByteCacheArtifacts(modelDir: string): void {\n\tlet entries: Dirent[];\n\ttry {\n\t\tentries = readdirSync(modelDir, { withFileTypes: true }) as Dirent[];\n\t} catch {\n\t\treturn; // No dir yet = nothing to purge.\n\t}\n\tfor (const entry of entries) {\n\t\tconst entryPath = join(modelDir, entry.name.toString());\n\t\tif (entry.isDirectory()) {\n\t\t\tpurgeZeroByteCacheArtifacts(entryPath);\n\t\t\tcontinue;\n\t\t}\n\t\tif (statSync(entryPath).size === 0) {\n\t\t\trmSync(entryPath, { force: true });\n\t\t}\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"model-cache-hygiene.js","sourceRoot":"","sources":["../../src/memory/model-cache-hygiene.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACxD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,2BAA2B,CAAC,QAAgB,EAAQ;IACnE,IAAI,OAAiB,CAAC;IACtB,IAAI,CAAC;QACJ,OAAO,GAAG,WAAW,CAAC,QAAQ,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAa,CAAC;IACtE,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,CAAC,iCAAiC;IAC1C,CAAC;IACD,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,SAAS,GAAG,IAAI,CAAC,QAAQ,EAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;QACxD,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;YACzB,2BAA2B,CAAC,SAAS,CAAC,CAAC;YACvC,SAAS;QACV,CAAC;QACD,IAAI,QAAQ,CAAC,SAAS,CAAC,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;YACpC,MAAM,CAAC,SAAS,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACpC,CAAC;IACF,CAAC;AAAA,CACD;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,6BAA6B,CAAC,KAAc,EAAW;IACtE,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACvE,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,2BAA2B,CAAC;QAAE,OAAO,KAAK,CAAC;IACjE,OAAO,IAAI,KAAK,CACf,GAAG,OAAO,gMAA8L,CACxM,CAAC;AAAA,CACF","sourcesContent":["import type { Dirent } from \"node:fs\";\nimport { readdirSync, rmSync, statSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\n/**\n * Zero-byte cache-artifact hygiene for transformers.js model downloads.\n *\n * A failed download attempt (unreachable mirror, aborted transfer) can leave a\n * 0-byte file in the cache; transformers.js then treats the file as a cache\n * hit on the NEXT attempt — including attempts against a different mirror —\n * and never heals. Empty tokenizer/weight files are unambiguously failed\n * downloads (real tokenizer.json is never empty), so the mirror-rotation loop\n * purges zero-byte files between hosts. Non-empty-but-truncated files are\n * deliberately NOT touched: those stay terminal per 设计 §3.2 (manual action\n * list), because deleting user-provided manual imports on a guess would be\n * worse than failing loudly.\n *\n * Best effort: purge failures propagate — if we cannot even delete an empty\n * file, the subsequent download attempt should fail with that truth.\n */\nexport function purgeZeroByteCacheArtifacts(modelDir: string): void {\n\tlet entries: Dirent[];\n\ttry {\n\t\tentries = readdirSync(modelDir, { withFileTypes: true }) as Dirent[];\n\t} catch {\n\t\treturn; // No dir yet = nothing to purge.\n\t}\n\tfor (const entry of entries) {\n\t\tconst entryPath = join(modelDir, entry.name.toString());\n\t\tif (entry.isDirectory()) {\n\t\t\tpurgeZeroByteCacheArtifacts(entryPath);\n\t\t\tcontinue;\n\t\t}\n\t\tif (statSync(entryPath).size === 0) {\n\t\t\trmSync(entryPath, { force: true });\n\t\t}\n\t}\n}\n\n/**\n * transformers 上游缺陷签名标注 (2026-10-06 live E2E 终审 P1-1): 镜像对\n * tokenizer_config.json 的元数据请求瞬时 404 时, transformers 把\n * `{exists:false}` 作为**成功结果**做模块级 memoize (key 不含 host), 之后\n * 同进程内任何重试/换 host 都拿到同一空清单, `AutoTokenizer` 解构 undefined\n * 抛 \"reading 'tokenizer_class'\"。此类失败在当前进程内不可自愈 (同 host\n * 退避重试只覆盖 rejection 型瞬断) — 如实标注, 让\"重启会话\"成为明确的\n * 有效动作而不是被失败行动清单的 retry 误导。\n */\nexport function annotateUpstreamListingPoison(error: unknown): unknown {\n\tconst message = error instanceof Error ? error.message : String(error);\n\tif (!message.includes(\"reading 'tokenizer_class'\")) return error;\n\treturn new Error(\n\t\t`${message} — the mirror served an incomplete file listing which transformers cached for this process (upstream bug); retries inside the current session cannot clear it, restart the session and retry`,\n\t);\n}\n"]}
|
|
@@ -69,19 +69,5 @@ export interface PromotePreferenceToUserDefaultInputV1 {
|
|
|
69
69
|
/** ISO-8601 confirmation timestamp stamped into the promoted envelope's `confirmedAt`. */
|
|
70
70
|
readonly confirmedAt: string;
|
|
71
71
|
}
|
|
72
|
-
/**
|
|
73
|
-
* Explicitly promotes a preference atom to the user-default scope (方案系统
|
|
74
|
-
* 设计 §6.1 跨方案规则). Produces a NEW canonical atom — the original is never
|
|
75
|
-
* rewritten — whose envelope carries `scope.level "user-default"` and
|
|
76
|
-
* `confirmedAt`, the canonical explicit-confirmation marker the suite read
|
|
77
|
-
* boundary ({@link memorySuiteVisibility} in foundation.ts) requires for
|
|
78
|
-
* cross-suite visibility. Scope anchors are intentionally dropped: a
|
|
79
|
-
* user-default preference applies without narrowing. The automatic
|
|
80
|
-
* applicabilityConfidence policy above stays orthogonal — it never touches
|
|
81
|
-
* user-default and never sets `confirmedAt`.
|
|
82
|
-
*
|
|
83
|
-
* Rejects non-preference atoms, atoms already at user-default, and missing
|
|
84
|
-
* `authorizedBy`/`confirmedAt`.
|
|
85
|
-
*/
|
|
86
72
|
export declare function promotePreferenceToUserDefault(input: PromotePreferenceToUserDefaultInputV1): MemoryAtomV1;
|
|
87
73
|
//# sourceMappingURL=preference-promotion.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"preference-promotion.d.ts","sourceRoot":"","sources":["../../src/memory/preference-promotion.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACzD,OAAO,KAAK,EAAE,YAAY,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAGhF,MAAM,WAAW,2BAA2B;IAC3C,qDAAqD;IACrD,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,yEAAyE;IACzE,QAAQ,CAAC,yBAAyB,EAAE,MAAM,CAAC;IAC3C,gEAAgE;IAChE,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,8DAA8D;IAC9D,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;CACpC;AAED,MAAM,MAAM,yBAAyB,GAAG,0BAA0B,CAAC,UAAU,CAAC,CAAC,OAAO,CAAC,CAAC;AAExF,MAAM,WAAW,uBAAuB;IACvC,QAAQ,CAAC,aAAa,EAAE,yBAAyB,CAAC;IAClD,+EAA+E;IAC/E,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,MAAM,6BAA6B,GACtC;IACA,QAAQ,CAAC,QAAQ,EAAE,UAAU,CAAC;IAC9B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,sBAAsB,EAAE,MAAM,CAAC;CACvC,GACD;IACA,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,sBAAsB,EAAE,MAAM,CAAC;IACxC,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAC;CACrC,GACD;IAAE,QAAQ,CAAC,QAAQ,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;CAAE,CAAC;AAErE,MAAM,WAAW,2BAA2B;IAC3C,qFAAqF;IACrF,QAAQ,CAAC,KAAK,EAAE;QACf,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;QACnC,QAAQ,CAAC,YAAY,EAAE,SAAS,uBAAuB,EAAE,CAAC;KAC1D,GAAG,6BAA6B,CAAC;IAClC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAC/B;AAED,wBAAgB,+BAA+B,CAAC,OAAO,EAAE;IACxD,QAAQ,CAAC,MAAM,EAAE,2BAA2B,CAAC;CAC7C,GAAG,2BAA2B,CAiD9B;AAED,wDAAwD;AACxD,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,2BAA2B,GAAG,SAAS,CAQvF;AAMD,MAAM,WAAW,qCAAqC;IACrD,6DAA6D;IAC7D,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,iFAAiF;IACjF,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,0FAA0F;IAC1F,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC7B;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,8BAA8B,CAAC,KAAK,EAAE,qCAAqC,GAAG,YAAY,CA6BzG","sourcesContent":["/**\n * Automatic preference promotion (2.4d1) — the explicit-confirmation-free\n * promotion policy over preference observations.\n *\n * Promotion NEVER fakes a user confirmation (no `confirmedAt`, no\n * `explicit` evidence class): it deterministically raises the winning\n * preference's `applicabilityConfidence` once independent repeated-behavior\n * or inferred observations reach the policy thresholds. Policies carry a\n * version and an explicit `enabled` switch — a disabled policy returns\n * `disabled` for every input and never promotes silently. The same\n * observation sequence under the same policy version resolves identically\n * on any engine instance, so decisions are migratable/replayable.\n * Complex disambiguation (2.4d2), full lifecycle promotion/decay (2.4d3),\n * and networks (2.4d4) are out of scope.\n */\nimport type { JsonValue } from \"@agent-forge/plugin-sdk\";\nimport type { MemoryAtomV1, MemoryPreferenceEnvelopeV1 } from \"./foundation.ts\";\nimport { validateMemoryAtomV1 } from \"./foundation.ts\";\n\nexport interface PreferencePromotionPolicyV1 {\n\t/** Master switch; disabled engines never promote. */\n\treadonly enabled: boolean;\n\treadonly policyVersion: string;\n\t/** Independent repeated-behavior observations required for promotion. */\n\treadonly repeatedBehaviorThreshold: number;\n\t/** Independent inferred observations required for promotion. */\n\treadonly inferredThreshold: number;\n\t/** The confidence assigned by a promotion decision (0..1]. */\n\treadonly promotedConfidence: number;\n}\n\nexport type PreferenceEvidenceClassV1 = MemoryPreferenceEnvelopeV1[\"evidence\"][\"class\"];\n\nexport interface PreferenceObservationV1 {\n\treadonly evidenceClass: PreferenceEvidenceClassV1;\n\t/** Distinct observation identity; duplicates within a sequence are ignored. */\n\treadonly observationId: string;\n\treadonly confidence?: number;\n}\n\nexport type PreferencePromotionDecisionV1 =\n\t| {\n\t\t\treadonly decision: \"promoted\";\n\t\t\treadonly policyVersion: string;\n\t\t\treadonly fromConfidence: number;\n\t\t\treadonly toConfidence: number;\n\t\t\treadonly supportingObservations: number;\n\t }\n\t| {\n\t\t\treadonly decision: \"not_promoted\";\n\t\t\treadonly policyVersion: string;\n\t\t\treadonly supportingObservations: number;\n\t\t\treadonly requiredObservations: number;\n\t }\n\t| { readonly decision: \"disabled\"; readonly policyVersion: string };\n\nexport interface PreferencePromotionEngineV1 {\n\t/** Evaluates one preference candidate (same subject+key+value) deterministically. */\n\tevaluate(input: {\n\t\treadonly currentConfidence: number;\n\t\treadonly observations: readonly PreferenceObservationV1[];\n\t}): PreferencePromotionDecisionV1;\n\treadonly policyVersion: string;\n}\n\nexport function createPreferencePromotionEngine(options: {\n\treadonly policy: PreferencePromotionPolicyV1;\n}): PreferencePromotionEngineV1 {\n\tconst policy = options.policy;\n\tif (policy.promotedConfidence <= 0 || policy.promotedConfidence > 1) {\n\t\tthrow new Error(\"Promotion promotedConfidence must be within (0..1]\");\n\t}\n\tif (policy.repeatedBehaviorThreshold < 1 || policy.inferredThreshold < 1) {\n\t\tthrow new Error(\"Promotion thresholds must be at least 1 observation\");\n\t}\n\n\treturn {\n\t\tpolicyVersion: policy.policyVersion,\n\t\tevaluate(input) {\n\t\t\tif (!policy.enabled) {\n\t\t\t\treturn { decision: \"disabled\", policyVersion: policy.policyVersion };\n\t\t\t}\n\t\t\t// Independent observations only: dedupe by observationId deterministically.\n\t\t\tconst seen = new Map<string, PreferenceObservationV1>();\n\t\t\tfor (const observation of input.observations) {\n\t\t\t\tif (observation.evidenceClass !== \"repeated_behavior\" && observation.evidenceClass !== \"inferred\") {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (!seen.has(observation.observationId)) seen.set(observation.observationId, observation);\n\t\t\t}\n\t\t\tconst supporting = [...seen.values()].filter((observation) => (observation.confidence ?? 1) >= 0.5);\n\n\t\t\tconst repeatedCount = supporting.filter(\n\t\t\t\t(observation) => observation.evidenceClass === \"repeated_behavior\",\n\t\t\t).length;\n\t\t\tconst inferredCount = supporting.filter((observation) => observation.evidenceClass === \"inferred\").length;\n\t\t\tconst required = repeatedCount >= inferredCount ? policy.repeatedBehaviorThreshold : policy.inferredThreshold;\n\t\t\tconst eligibleCount = Math.max(repeatedCount, inferredCount);\n\n\t\t\tif (repeatedCount >= policy.repeatedBehaviorThreshold || inferredCount >= policy.inferredThreshold) {\n\t\t\t\treturn {\n\t\t\t\t\tdecision: \"promoted\",\n\t\t\t\t\tpolicyVersion: policy.policyVersion,\n\t\t\t\t\tfromConfidence: input.currentConfidence,\n\t\t\t\t\ttoConfidence: Math.max(policy.promotedConfidence, input.currentConfidence),\n\t\t\t\t\tsupportingObservations: eligibleCount,\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tdecision: \"not_promoted\",\n\t\t\t\tpolicyVersion: policy.policyVersion,\n\t\t\t\tsupportingObservations: eligibleCount,\n\t\t\t\trequiredObservations: required,\n\t\t\t};\n\t\t},\n\t};\n}\n\n/** Serializes a policy for migration/replay ledgers. */\nexport function serializePromotionPolicy(policy: PreferencePromotionPolicyV1): JsonValue {\n\treturn {\n\t\tenabled: policy.enabled,\n\t\tpolicyVersion: policy.policyVersion,\n\t\trepeatedBehaviorThreshold: policy.repeatedBehaviorThreshold,\n\t\tinferredThreshold: policy.inferredThreshold,\n\t\tpromotedConfidence: policy.promotedConfidence,\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// Explicit user-confirmed promotion to user-default (方案系统设计 §6.1, M5)\n// ---------------------------------------------------------------------------\n\nexport interface PromotePreferenceToUserDefaultInputV1 {\n\t/** The source preference atom; it is preserved untouched. */\n\treadonly atom: MemoryAtomV1;\n\t/** Who authorized the promotion (explicit confirmation provenance); required. */\n\treadonly authorizedBy: string;\n\t/** ISO-8601 confirmation timestamp stamped into the promoted envelope's `confirmedAt`. */\n\treadonly confirmedAt: string;\n}\n\n/**\n * Explicitly promotes a preference atom to the user-default scope (方案系统\n * 设计 §6.1 跨方案规则). Produces a NEW canonical atom — the original is never\n * rewritten — whose envelope carries `scope.level \"user-default\"` and\n * `confirmedAt`, the canonical explicit-confirmation marker the suite read\n * boundary ({@link memorySuiteVisibility} in foundation.ts) requires for\n * cross-suite visibility. Scope anchors are intentionally dropped: a\n * user-default preference applies without narrowing. The automatic\n * applicabilityConfidence policy above stays orthogonal — it never touches\n * user-default and never sets `confirmedAt`.\n *\n * Rejects non-preference atoms, atoms already at user-default, and missing\n * `authorizedBy`/`confirmedAt`.\n */\nexport function promotePreferenceToUserDefault(input: PromotePreferenceToUserDefaultInputV1): MemoryAtomV1 {\n\tif (typeof input.authorizedBy !== \"string\" || input.authorizedBy.trim().length === 0) {\n\t\tthrow new Error(\"User-default preference promotion requires a non-empty authorizedBy\");\n\t}\n\tif (typeof input.confirmedAt !== \"string\" || input.confirmedAt.trim().length === 0) {\n\t\tthrow new Error(\"User-default preference promotion requires a non-empty confirmedAt\");\n\t}\n\tif (Number.isNaN(Date.parse(input.confirmedAt))) {\n\t\tthrow new Error(\"User-default preference promotion confirmedAt must be an ISO-8601 timestamp\");\n\t}\n\tconst atom = input.atom;\n\tif (atom.memoryKind !== \"preference\" || atom.preference === undefined) {\n\t\tthrow new Error(\"User-default preference promotion requires a preference atom with a preference envelope\");\n\t}\n\tif (atom.preference.scope.level === \"user-default\") {\n\t\tthrow new Error(`Preference ${atom.memoryId} is already in the user-default scope`);\n\t}\n\tconst envelope: MemoryPreferenceEnvelopeV1 = {\n\t\t...atom.preference,\n\t\tscope: { level: \"user-default\" },\n\t\tconfirmedAt: input.confirmedAt,\n\t};\n\treturn validateMemoryAtomV1({\n\t\t...atom,\n\t\tmemoryId: `${atom.memoryId}-user-default`,\n\t\tobservationId: `${atom.observationId}-user-default`,\n\t\tpreference: envelope,\n\t\twriteReason: `promote-preference-to-user-default by ${input.authorizedBy}`,\n\t});\n}\n"]}
|
|
1
|
+
{"version":3,"file":"preference-promotion.d.ts","sourceRoot":"","sources":["../../src/memory/preference-promotion.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACzD,OAAO,KAAK,EAAE,YAAY,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAGhF,MAAM,WAAW,2BAA2B;IAC3C,qDAAqD;IACrD,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,yEAAyE;IACzE,QAAQ,CAAC,yBAAyB,EAAE,MAAM,CAAC;IAC3C,gEAAgE;IAChE,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,8DAA8D;IAC9D,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;CACpC;AAED,MAAM,MAAM,yBAAyB,GAAG,0BAA0B,CAAC,UAAU,CAAC,CAAC,OAAO,CAAC,CAAC;AAExF,MAAM,WAAW,uBAAuB;IACvC,QAAQ,CAAC,aAAa,EAAE,yBAAyB,CAAC;IAClD,+EAA+E;IAC/E,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,MAAM,6BAA6B,GACtC;IACA,QAAQ,CAAC,QAAQ,EAAE,UAAU,CAAC;IAC9B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,sBAAsB,EAAE,MAAM,CAAC;CACvC,GACD;IACA,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,sBAAsB,EAAE,MAAM,CAAC;IACxC,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAC;CACrC,GACD;IAAE,QAAQ,CAAC,QAAQ,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;CAAE,CAAC;AAErE,MAAM,WAAW,2BAA2B;IAC3C,qFAAqF;IACrF,QAAQ,CAAC,KAAK,EAAE;QACf,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;QACnC,QAAQ,CAAC,YAAY,EAAE,SAAS,uBAAuB,EAAE,CAAC;KAC1D,GAAG,6BAA6B,CAAC;IAClC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAC/B;AAED,wBAAgB,+BAA+B,CAAC,OAAO,EAAE;IACxD,QAAQ,CAAC,MAAM,EAAE,2BAA2B,CAAC;CAC7C,GAAG,2BAA2B,CAiD9B;AAED,wDAAwD;AACxD,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,2BAA2B,GAAG,SAAS,CAQvF;AAMD,MAAM,WAAW,qCAAqC;IACrD,6DAA6D;IAC7D,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,iFAAiF;IACjF,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,0FAA0F;IAC1F,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC7B;AAwBD,wBAAgB,8BAA8B,CAAC,KAAK,EAAE,qCAAqC,GAAG,YAAY,CAuCzG","sourcesContent":["/**\n * Automatic preference promotion (2.4d1) — the explicit-confirmation-free\n * promotion policy over preference observations.\n *\n * Promotion NEVER fakes a user confirmation (no `confirmedAt`, no\n * `explicit` evidence class): it deterministically raises the winning\n * preference's `applicabilityConfidence` once independent repeated-behavior\n * or inferred observations reach the policy thresholds. Policies carry a\n * version and an explicit `enabled` switch — a disabled policy returns\n * `disabled` for every input and never promotes silently. The same\n * observation sequence under the same policy version resolves identically\n * on any engine instance, so decisions are migratable/replayable.\n * Complex disambiguation (2.4d2), full lifecycle promotion/decay (2.4d3),\n * and networks (2.4d4) are out of scope.\n */\nimport type { JsonValue } from \"@agent-forge/plugin-sdk\";\nimport type { MemoryAtomV1, MemoryPreferenceEnvelopeV1 } from \"./foundation.ts\";\nimport { validateMemoryAtomV1 } from \"./foundation.ts\";\n\nexport interface PreferencePromotionPolicyV1 {\n\t/** Master switch; disabled engines never promote. */\n\treadonly enabled: boolean;\n\treadonly policyVersion: string;\n\t/** Independent repeated-behavior observations required for promotion. */\n\treadonly repeatedBehaviorThreshold: number;\n\t/** Independent inferred observations required for promotion. */\n\treadonly inferredThreshold: number;\n\t/** The confidence assigned by a promotion decision (0..1]. */\n\treadonly promotedConfidence: number;\n}\n\nexport type PreferenceEvidenceClassV1 = MemoryPreferenceEnvelopeV1[\"evidence\"][\"class\"];\n\nexport interface PreferenceObservationV1 {\n\treadonly evidenceClass: PreferenceEvidenceClassV1;\n\t/** Distinct observation identity; duplicates within a sequence are ignored. */\n\treadonly observationId: string;\n\treadonly confidence?: number;\n}\n\nexport type PreferencePromotionDecisionV1 =\n\t| {\n\t\t\treadonly decision: \"promoted\";\n\t\t\treadonly policyVersion: string;\n\t\t\treadonly fromConfidence: number;\n\t\t\treadonly toConfidence: number;\n\t\t\treadonly supportingObservations: number;\n\t }\n\t| {\n\t\t\treadonly decision: \"not_promoted\";\n\t\t\treadonly policyVersion: string;\n\t\t\treadonly supportingObservations: number;\n\t\t\treadonly requiredObservations: number;\n\t }\n\t| { readonly decision: \"disabled\"; readonly policyVersion: string };\n\nexport interface PreferencePromotionEngineV1 {\n\t/** Evaluates one preference candidate (same subject+key+value) deterministically. */\n\tevaluate(input: {\n\t\treadonly currentConfidence: number;\n\t\treadonly observations: readonly PreferenceObservationV1[];\n\t}): PreferencePromotionDecisionV1;\n\treadonly policyVersion: string;\n}\n\nexport function createPreferencePromotionEngine(options: {\n\treadonly policy: PreferencePromotionPolicyV1;\n}): PreferencePromotionEngineV1 {\n\tconst policy = options.policy;\n\tif (policy.promotedConfidence <= 0 || policy.promotedConfidence > 1) {\n\t\tthrow new Error(\"Promotion promotedConfidence must be within (0..1]\");\n\t}\n\tif (policy.repeatedBehaviorThreshold < 1 || policy.inferredThreshold < 1) {\n\t\tthrow new Error(\"Promotion thresholds must be at least 1 observation\");\n\t}\n\n\treturn {\n\t\tpolicyVersion: policy.policyVersion,\n\t\tevaluate(input) {\n\t\t\tif (!policy.enabled) {\n\t\t\t\treturn { decision: \"disabled\", policyVersion: policy.policyVersion };\n\t\t\t}\n\t\t\t// Independent observations only: dedupe by observationId deterministically.\n\t\t\tconst seen = new Map<string, PreferenceObservationV1>();\n\t\t\tfor (const observation of input.observations) {\n\t\t\t\tif (observation.evidenceClass !== \"repeated_behavior\" && observation.evidenceClass !== \"inferred\") {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (!seen.has(observation.observationId)) seen.set(observation.observationId, observation);\n\t\t\t}\n\t\t\tconst supporting = [...seen.values()].filter((observation) => (observation.confidence ?? 1) >= 0.5);\n\n\t\t\tconst repeatedCount = supporting.filter(\n\t\t\t\t(observation) => observation.evidenceClass === \"repeated_behavior\",\n\t\t\t).length;\n\t\t\tconst inferredCount = supporting.filter((observation) => observation.evidenceClass === \"inferred\").length;\n\t\t\tconst required = repeatedCount >= inferredCount ? policy.repeatedBehaviorThreshold : policy.inferredThreshold;\n\t\t\tconst eligibleCount = Math.max(repeatedCount, inferredCount);\n\n\t\t\tif (repeatedCount >= policy.repeatedBehaviorThreshold || inferredCount >= policy.inferredThreshold) {\n\t\t\t\treturn {\n\t\t\t\t\tdecision: \"promoted\",\n\t\t\t\t\tpolicyVersion: policy.policyVersion,\n\t\t\t\t\tfromConfidence: input.currentConfidence,\n\t\t\t\t\ttoConfidence: Math.max(policy.promotedConfidence, input.currentConfidence),\n\t\t\t\t\tsupportingObservations: eligibleCount,\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tdecision: \"not_promoted\",\n\t\t\t\tpolicyVersion: policy.policyVersion,\n\t\t\t\tsupportingObservations: eligibleCount,\n\t\t\t\trequiredObservations: required,\n\t\t\t};\n\t\t},\n\t};\n}\n\n/** Serializes a policy for migration/replay ledgers. */\nexport function serializePromotionPolicy(policy: PreferencePromotionPolicyV1): JsonValue {\n\treturn {\n\t\tenabled: policy.enabled,\n\t\tpolicyVersion: policy.policyVersion,\n\t\trepeatedBehaviorThreshold: policy.repeatedBehaviorThreshold,\n\t\tinferredThreshold: policy.inferredThreshold,\n\t\tpromotedConfidence: policy.promotedConfidence,\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// Explicit user-confirmed promotion to user-default (方案系统设计 §6.1, M5)\n// ---------------------------------------------------------------------------\n\nexport interface PromotePreferenceToUserDefaultInputV1 {\n\t/** The source preference atom; it is preserved untouched. */\n\treadonly atom: MemoryAtomV1;\n\t/** Who authorized the promotion (explicit confirmation provenance); required. */\n\treadonly authorizedBy: string;\n\t/** ISO-8601 confirmation timestamp stamped into the promoted envelope's `confirmedAt`. */\n\treadonly confirmedAt: string;\n}\n\n/**\n * Explicitly promotes a preference atom to the user-default scope (方案系统\n * 设计 §6.1 跨方案规则). Produces a NEW canonical atom — the original is never\n * rewritten — whose envelope carries `scope.level \"user-default\"` and\n * `confirmedAt`, the canonical explicit-confirmation marker the suite read\n * boundary ({@link memorySuiteVisibility} in foundation.ts) requires for\n * cross-suite visibility. Scope anchors are intentionally dropped: a\n * user-default preference applies without narrowing. The automatic\n * applicabilityConfidence policy above stays orthogonal — it never touches\n * user-default and never sets `confirmedAt`.\n *\n * Rejects non-preference atoms, atoms already at user-default, and missing\n * `authorizedBy`/`confirmedAt`.\n */\n/**\n * 晋升副本的证据档 (记忆写入与删除授权诚实化设计 §1.2 表第 5 行): 用户经\n * /promote 显式确认 = user-stated 级证据 — envelope/atom 统一升格 explicit,\n * 置信取与 capability.ts MEMORY_EVIDENCE_CONFIDENCE[\"user-stated\"] 同源的保守\n * 常量 (已登记产品默认值清单)。源 atom 保持原样不动。\n */\nconst PROMOTED_EVIDENCE_CONFIDENCE = 0.9;\n\nexport function promotePreferenceToUserDefault(input: PromotePreferenceToUserDefaultInputV1): MemoryAtomV1 {\n\tif (typeof input.authorizedBy !== \"string\" || input.authorizedBy.trim().length === 0) {\n\t\tthrow new Error(\"User-default preference promotion requires a non-empty authorizedBy\");\n\t}\n\tif (typeof input.confirmedAt !== \"string\" || input.confirmedAt.trim().length === 0) {\n\t\tthrow new Error(\"User-default preference promotion requires a non-empty confirmedAt\");\n\t}\n\tif (Number.isNaN(Date.parse(input.confirmedAt))) {\n\t\tthrow new Error(\"User-default preference promotion confirmedAt must be an ISO-8601 timestamp\");\n\t}\n\tconst atom = input.atom;\n\tif (atom.memoryKind !== \"preference\" || atom.preference === undefined) {\n\t\tthrow new Error(\"User-default preference promotion requires a preference atom with a preference envelope\");\n\t}\n\tif (atom.preference.scope.level === \"user-default\") {\n\t\tthrow new Error(`Preference ${atom.memoryId} is already in the user-default scope`);\n\t}\n\tconst envelope: MemoryPreferenceEnvelopeV1 = {\n\t\t...atom.preference,\n\t\tscope: { level: \"user-default\" },\n\t\tconfirmedAt: input.confirmedAt,\n\t\t// 证据升格 (§1.2): 晋升即用户显式确认 — 源可能是 inferred/0.6 转述,\n\t\t// 副本必须如实反映\"用户已确认\"这一更高证据事实。\n\t\tevidence: {\n\t\t\t...atom.preference.evidence,\n\t\t\tclass: \"explicit\",\n\t\t\tconfidence: PROMOTED_EVIDENCE_CONFIDENCE,\n\t\t},\n\t\tapplicabilityConfidence: PROMOTED_EVIDENCE_CONFIDENCE,\n\t};\n\treturn validateMemoryAtomV1({\n\t\t...atom,\n\t\tmemoryId: `${atom.memoryId}-user-default`,\n\t\tobservationId: `${atom.observationId}-user-default`,\n\t\tpreference: envelope,\n\t\tevidenceClass: \"explicit\",\n\t\tconfidence: PROMOTED_EVIDENCE_CONFIDENCE,\n\t\twriteReason: `promote-preference-to-user-default by ${input.authorizedBy}`,\n\t});\n}\n"]}
|
|
@@ -69,6 +69,13 @@ export function serializePromotionPolicy(policy) {
|
|
|
69
69
|
* Rejects non-preference atoms, atoms already at user-default, and missing
|
|
70
70
|
* `authorizedBy`/`confirmedAt`.
|
|
71
71
|
*/
|
|
72
|
+
/**
|
|
73
|
+
* 晋升副本的证据档 (记忆写入与删除授权诚实化设计 §1.2 表第 5 行): 用户经
|
|
74
|
+
* /promote 显式确认 = user-stated 级证据 — envelope/atom 统一升格 explicit,
|
|
75
|
+
* 置信取与 capability.ts MEMORY_EVIDENCE_CONFIDENCE["user-stated"] 同源的保守
|
|
76
|
+
* 常量 (已登记产品默认值清单)。源 atom 保持原样不动。
|
|
77
|
+
*/
|
|
78
|
+
const PROMOTED_EVIDENCE_CONFIDENCE = 0.9;
|
|
72
79
|
export function promotePreferenceToUserDefault(input) {
|
|
73
80
|
if (typeof input.authorizedBy !== "string" || input.authorizedBy.trim().length === 0) {
|
|
74
81
|
throw new Error("User-default preference promotion requires a non-empty authorizedBy");
|
|
@@ -90,12 +97,22 @@ export function promotePreferenceToUserDefault(input) {
|
|
|
90
97
|
...atom.preference,
|
|
91
98
|
scope: { level: "user-default" },
|
|
92
99
|
confirmedAt: input.confirmedAt,
|
|
100
|
+
// 证据升格 (§1.2): 晋升即用户显式确认 — 源可能是 inferred/0.6 转述,
|
|
101
|
+
// 副本必须如实反映"用户已确认"这一更高证据事实。
|
|
102
|
+
evidence: {
|
|
103
|
+
...atom.preference.evidence,
|
|
104
|
+
class: "explicit",
|
|
105
|
+
confidence: PROMOTED_EVIDENCE_CONFIDENCE,
|
|
106
|
+
},
|
|
107
|
+
applicabilityConfidence: PROMOTED_EVIDENCE_CONFIDENCE,
|
|
93
108
|
};
|
|
94
109
|
return validateMemoryAtomV1({
|
|
95
110
|
...atom,
|
|
96
111
|
memoryId: `${atom.memoryId}-user-default`,
|
|
97
112
|
observationId: `${atom.observationId}-user-default`,
|
|
98
113
|
preference: envelope,
|
|
114
|
+
evidenceClass: "explicit",
|
|
115
|
+
confidence: PROMOTED_EVIDENCE_CONFIDENCE,
|
|
99
116
|
writeReason: `promote-preference-to-user-default by ${input.authorizedBy}`,
|
|
100
117
|
});
|
|
101
118
|
}
|