@ai-agent-forge/plugin-memory 0.88.11 → 0.88.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"embedding-reranker.d.ts","sourceRoot":"","sources":["../../src/memory/embedding-reranker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAUH,MAAM,WAAW,gBAAgB;IAChC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,+FAA+F;IAC/F,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CAAC;CAC3E;AAED;;;GAGG;AACH,MAAM,WAAW,6BAA6B;IAC7C,sEAAsE;IACtE,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;CAC5D;AAED,MAAM,MAAM,4BAA4B,GAAG,CAC1C,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,KACd,OAAO,CAAC,6BAA6B,CAAC,CAAC;AAsC5C;;;;;;;;;;;;GAYG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE;IACnD,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,QAAQ,CAAC,QAAQ,CAAC,EAAE,4BAA4B,CAAC;IACjD,iGAAiG;IACjG,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;CAClC,GAAG,gBAAgB,CA0DnB","sourcesContent":["/**\n * Memory cross-encoder reranker for hybrid retrieval (混合检索 · 精排侧).\n *\n * POC-verified (2026-09, 100k corpus): reranking drift-query retrieval with\n * Xenova/bge-reranker-base lifts high-tier recall to re@10 100% (低档 83%),\n * inference deterministic across runs. Reranking is an independently\n * disable-able component (宪法: 独立可禁用); tiers are caller configuration.\n *\n * Model note: default \"Xenova/bge-reranker-base\" is the only officially\n * converted (transformers.js) reranker verified in POC; q8 weights ~280MB,\n * 512-token context — the caller owns the write-side length contract\n * (statement ≤700 chars); overlong inputs are gracefully truncated by the\n * tokenizer (`truncation: true`). Upgrade path bge-reranker-v2-m3 is a\n * modelId swap away, but it currently has no official transformers.js\n * conversion.\n *\n * Runtime dep @huggingface/transformers is lazy-loaded via `await import` at\n * first rerank (惯例参考 packages/agent-forge/src/utils/photon.ts); type\n * positions use type-only imports. Any failure propagates — 不静默降级.\n */\n\n/**\n * Cross-encoder reranker. Higher score = more relevant; raw logits are only\n * order-preserving, not calibrated probabilities.\n */\nimport { join } from \"node:path\";\nimport { importHostOrBareModule } from \"./host-module-import.ts\";\nimport { annotateUpstreamListingPoison, purgeZeroByteCacheArtifacts } from \"./model-cache-hygiene.ts\";\n\nexport interface MemoryRerankerV1 {\n\treadonly modelId: string;\n\t/** Score each doc against the query; higher = more relevant (raw logits, order-preserving). */\n\trerank(query: string, docs: readonly string[]): Promise<readonly number[]>;\n}\n\n/**\n * Minimal structural seam around a transformers.js sequence-classification\n * pair, so tests can inject a fake without touching the real loader.\n */\nexport interface TransformersRerankerBackendV1 {\n\t/** Score docs against the query; result[i] corresponds to docs[i]. */\n\tscore: (query: string, docs: string[]) => Promise<number[]>;\n}\n\nexport type TransformersRerankerLoadImpl = (\n\tmodelId: string,\n\tcacheDir: string,\n\tremoteHost: string,\n) => Promise<TransformersRerankerBackendV1>;\n\nconst DEFAULT_RERANKER_MODEL_ID = \"Xenova/bge-reranker-base\";\nconst DEFAULT_REMOTE_HOST = \"https://huggingface.co\";\n\n/** Structural view of the tokenizer call; returns the model-input object. */\ntype RerankTokenizerCall = (\n\ttexts: string[],\n\toptions: { text_pair: string[]; padding: boolean; truncation: boolean },\n) => unknown;\n\n/** Structural view of the sequence-classification forward pass. */\ntype RerankModelCall = (inputs: unknown) => Promise<{ logits: { data: ArrayLike<number> } }>;\n\nconst createDefaultTransformersLoadImpl = (moduleEntryPath?: string): TransformersRerankerLoadImpl => {\n\treturn async (modelId, cacheDir, remoteHost) => {\n\t\tconst transformers = await importHostOrBareModule<typeof import(\"@huggingface/transformers\")>(\n\t\t\tmoduleEntryPath ?? \"@huggingface/transformers\",\n\t\t);\n\t\ttransformers.env.cacheDir = cacheDir;\n\t\ttransformers.env.remoteHost = remoteHost;\n\t\tconst tokenizer = (await transformers.AutoTokenizer.from_pretrained(modelId)) as unknown as RerankTokenizerCall;\n\t\tconst model = (await transformers.AutoModelForSequenceClassification.from_pretrained(modelId, {\n\t\t\tdtype: \"q8\",\n\t\t})) as unknown as RerankModelCall;\n\t\treturn {\n\t\t\tscore: async (query, docs) => {\n\t\t\t\tconst inputs = tokenizer(\n\t\t\t\t\tdocs.map(() => query),\n\t\t\t\t\t{ text_pair: docs, padding: true, truncation: true },\n\t\t\t\t);\n\t\t\t\tconst output = await model(inputs);\n\t\t\t\treturn Array.from(output.logits.data);\n\t\t\t},\n\t\t};\n\t};\n};\n\n/**\n * Transformers.js-backed cross-encoder reranker (两档共用默认\n * Xenova/bge-reranker-base, 见文件头模型说明)。\n *\n * 镜像序列: 首次 rerank 时按 `remoteHosts` 顺序逐个尝试加载 (调用方通常传\n * [\"https://huggingface.co\", \"https://hf-mirror.com\"]); 某 host 加载抛错则\n * 尝试下一个, 全部失败时抛最后一个错误。host 确定后模型句柄只加载一次\n * (并发 rerank 共享同一 in-flight promise); 加载失败不缓存, 下次 rerank\n * 重新走完整序列, 失败原因原样保留, 不静默降级。\n *\n * `loadImpl` 是测试 seam: 注入后绕过真实加载, 签名\n * (modelId, cacheDir, remoteHost) => backend。\n */\nexport function createTransformersReranker(options: {\n\treadonly modelId?: string;\n\treadonly cacheDir: string;\n\treadonly remoteHosts?: readonly string[];\n\treadonly loadImpl?: TransformersRerankerLoadImpl;\n\t/** Host-resolved transformers entry path (install-store visibility); undefined = bare import. */\n\treadonly moduleEntryPath?: string;\n}): MemoryRerankerV1 {\n\tconst modelId = options.modelId ?? DEFAULT_RERANKER_MODEL_ID;\n\tconst cacheDir = options.cacheDir;\n\tconst remoteHosts = options.remoteHosts ?? [DEFAULT_REMOTE_HOST];\n\tconst load = options.loadImpl ?? createDefaultTransformersLoadImpl(options.moduleEntryPath);\n\n\tlet backendPromise: Promise<TransformersRerankerBackendV1> | null = null;\n\tconst loadBackend = (): Promise<TransformersRerankerBackendV1> => {\n\t\tconst existing = backendPromise;\n\t\tif (existing) {\n\t\t\treturn existing;\n\t\t}\n\t\tconst attempt = (async () => {\n\t\t\tlet lastError: unknown;\n\t\t\tfor (const remoteHost of remoteHosts) {\n\t\t\t\ttry {\n\t\t\t\t\treturn await load(modelId, cacheDir, remoteHost);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tlastError = error;\n\t\t\t\t\t// 镜像轮换前清 0 字节残骸 (embedding-provider 同理, model-cache-hygiene)。\n\t\t\t\t\tpurgeZeroByteCacheArtifacts(join(cacheDir, modelId));\n\t\t\t\t}\n\t\t\t\t// 同 host 一次短退避重试 (2026-10-06 live E2E): 覆盖 rejection 型瞬断\n\t\t\t\t// (fetch 网络错/5xx)。对\"仓库 API 清单缺文件\"型不生效——该结果被\n\t\t\t\t// transformers 模块级 memoize (见 annotateUpstreamListingPoison),\n\t\t\t\t// 进程内换 host 也拿同一空清单。\n\t\t\t\ttry {\n\t\t\t\t\tawait new Promise((resolve) => setTimeout(resolve, 2_000));\n\t\t\t\t\treturn await load(modelId, cacheDir, remoteHost);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tlastError = error;\n\t\t\t\t\tpurgeZeroByteCacheArtifacts(join(cacheDir, modelId));\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (lastError === undefined) throw new Error(`no remote hosts configured for model ${modelId}`);\n\t\t\tthrow annotateUpstreamListingPoison(lastError);\n\t\t})();\n\t\tbackendPromise = attempt;\n\t\tattempt.catch(() => {\n\t\t\t// 失败不缓存: 下次 rerank 重新走镜像序列 (显式失败, 不静默降级)。\n\t\t\tif (backendPromise === attempt) {\n\t\t\t\tbackendPromise = null;\n\t\t\t}\n\t\t});\n\t\treturn attempt;\n\t};\n\n\treturn {\n\t\tmodelId,\n\t\trerank: async (query, docs) => {\n\t\t\tif (docs.length === 0) {\n\t\t\t\t// 空 docs 直接返回, 不触发模型加载 (lazy-load 只发生在真正需要打分时)。\n\t\t\t\treturn [];\n\t\t\t}\n\t\t\tconst backend = await loadBackend();\n\t\t\treturn backend.score(query, [...docs]);\n\t\t},\n\t};\n}\n"]}
1
+ {"version":3,"file":"embedding-reranker.d.ts","sourceRoot":"","sources":["../../src/memory/embedding-reranker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAUH,MAAM,WAAW,gBAAgB;IAChC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,+FAA+F;IAC/F,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CAAC;IAC3E,qFAAiC;IACjC,OAAO,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED;;;GAGG;AACH,MAAM,WAAW,6BAA6B;IAC7C,sEAAsE;IACtE,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC5D,OAAO,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED,MAAM,MAAM,4BAA4B,GAAG,CAC1C,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,KACd,OAAO,CAAC,6BAA6B,CAAC,CAAC;AA2C5C;;;;;;;;;;;;GAYG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE;IACnD,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,QAAQ,CAAC,QAAQ,CAAC,EAAE,4BAA4B,CAAC;IACjD,iGAAiG;IACjG,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;CAClC,GAAG,gBAAgB,CAkFnB","sourcesContent":["/**\n * Memory cross-encoder reranker for hybrid retrieval (混合检索 · 精排侧).\n *\n * POC-verified (2026-09, 100k corpus): reranking drift-query retrieval with\n * Xenova/bge-reranker-base lifts high-tier recall to re@10 100% (低档 83%),\n * inference deterministic across runs. Reranking is an independently\n * disable-able component (宪法: 独立可禁用); tiers are caller configuration.\n *\n * Model note: default \"Xenova/bge-reranker-base\" is the only officially\n * converted (transformers.js) reranker verified in POC; q8 weights ~280MB,\n * 512-token context — the caller owns the write-side length contract\n * (statement ≤700 chars); overlong inputs are gracefully truncated by the\n * tokenizer (`truncation: true`). Upgrade path bge-reranker-v2-m3 is a\n * modelId swap away, but it currently has no official transformers.js\n * conversion.\n *\n * Runtime dep @huggingface/transformers is lazy-loaded via `await import` at\n * first rerank (惯例参考 packages/agent-forge/src/utils/photon.ts); type\n * positions use type-only imports. Any failure propagates — 不静默降级.\n */\n\n/**\n * Cross-encoder reranker. Higher score = more relevant; raw logits are only\n * order-preserving, not calibrated probabilities.\n */\nimport { join } from \"node:path\";\nimport { importHostOrBareModule } from \"./host-module-import.ts\";\nimport { annotateUpstreamListingPoison, purgeZeroByteCacheArtifacts } from \"./model-cache-hygiene.ts\";\n\nexport interface MemoryRerankerV1 {\n\treadonly modelId: string;\n\t/** Score each doc against the query; higher = more relevant (raw logits, order-preserving). */\n\trerank(query: string, docs: readonly string[]): Promise<readonly number[]>;\n\t/** 释放本实例模型;在途原生推理完成后关闭,不删除缓存。 */\n\tdispose?(): Promise<void>;\n}\n\n/**\n * Minimal structural seam around a transformers.js sequence-classification\n * pair, so tests can inject a fake without touching the real loader.\n */\nexport interface TransformersRerankerBackendV1 {\n\t/** Score docs against the query; result[i] corresponds to docs[i]. */\n\tscore: (query: string, docs: string[]) => Promise<number[]>;\n\tdispose?(): Promise<void>;\n}\n\nexport type TransformersRerankerLoadImpl = (\n\tmodelId: string,\n\tcacheDir: string,\n\tremoteHost: string,\n) => Promise<TransformersRerankerBackendV1>;\n\nconst DEFAULT_RERANKER_MODEL_ID = \"Xenova/bge-reranker-base\";\nconst DEFAULT_REMOTE_HOST = \"https://huggingface.co\";\n\n/** Structural view of the tokenizer call; returns the model-input object. */\ntype RerankTokenizerCall = (\n\ttexts: string[],\n\toptions: { text_pair: string[]; padding: boolean; truncation: boolean },\n) => unknown;\n\n/** Structural view of the sequence-classification forward pass. */\ntype RerankModelCall = ((inputs: unknown) => Promise<{ logits: { data: ArrayLike<number> } }>) & {\n\tdispose(): Promise<unknown>;\n};\n\nconst createDefaultTransformersLoadImpl = (moduleEntryPath?: string): TransformersRerankerLoadImpl => {\n\treturn async (modelId, cacheDir, remoteHost) => {\n\t\tconst transformers = await importHostOrBareModule<typeof import(\"@huggingface/transformers\")>(\n\t\t\tmoduleEntryPath ?? \"@huggingface/transformers\",\n\t\t);\n\t\ttransformers.env.cacheDir = cacheDir;\n\t\ttransformers.env.remoteHost = remoteHost;\n\t\tconst tokenizer = (await transformers.AutoTokenizer.from_pretrained(modelId)) as unknown as RerankTokenizerCall;\n\t\tconst model = (await transformers.AutoModelForSequenceClassification.from_pretrained(modelId, {\n\t\t\tdtype: \"q8\",\n\t\t})) as unknown as RerankModelCall;\n\t\treturn {\n\t\t\tdispose: async () => {\n\t\t\t\tawait model.dispose();\n\t\t\t},\n\t\t\tscore: async (query, docs) => {\n\t\t\t\tconst inputs = tokenizer(\n\t\t\t\t\tdocs.map(() => query),\n\t\t\t\t\t{ text_pair: docs, padding: true, truncation: true },\n\t\t\t\t);\n\t\t\t\tconst output = await model(inputs);\n\t\t\t\treturn Array.from(output.logits.data);\n\t\t\t},\n\t\t};\n\t};\n};\n\n/**\n * Transformers.js-backed cross-encoder reranker (两档共用默认\n * Xenova/bge-reranker-base, 见文件头模型说明)。\n *\n * 镜像序列: 首次 rerank 时按 `remoteHosts` 顺序逐个尝试加载 (调用方通常传\n * [\"https://huggingface.co\", \"https://hf-mirror.com\"]); 某 host 加载抛错则\n * 尝试下一个, 全部失败时抛最后一个错误。host 确定后模型句柄只加载一次\n * (并发 rerank 共享同一 in-flight promise); 加载失败不缓存, 下次 rerank\n * 重新走完整序列, 失败原因原样保留, 不静默降级。\n *\n * `loadImpl` 是测试 seam: 注入后绕过真实加载, 签名\n * (modelId, cacheDir, remoteHost) => backend。\n */\nexport function createTransformersReranker(options: {\n\treadonly modelId?: string;\n\treadonly cacheDir: string;\n\treadonly remoteHosts?: readonly string[];\n\treadonly loadImpl?: TransformersRerankerLoadImpl;\n\t/** Host-resolved transformers entry path (install-store visibility); undefined = bare import. */\n\treadonly moduleEntryPath?: string;\n}): MemoryRerankerV1 {\n\tconst modelId = options.modelId ?? DEFAULT_RERANKER_MODEL_ID;\n\tconst cacheDir = options.cacheDir;\n\tconst remoteHosts = options.remoteHosts ?? [DEFAULT_REMOTE_HOST];\n\tconst load = options.loadImpl ?? createDefaultTransformersLoadImpl(options.moduleEntryPath);\n\n\tlet backendPromise: Promise<TransformersRerankerBackendV1> | null = null;\n\tlet disposed = false;\n\tlet disposePromise: Promise<void> | undefined;\n\tconst activeCalls = new Set<Promise<unknown>>();\n\tconst loadBackend = (): Promise<TransformersRerankerBackendV1> => {\n\t\tconst existing = backendPromise;\n\t\tif (existing) {\n\t\t\treturn existing;\n\t\t}\n\t\tconst attempt = (async () => {\n\t\t\tlet lastError: unknown;\n\t\t\tfor (const remoteHost of remoteHosts) {\n\t\t\t\ttry {\n\t\t\t\t\treturn await load(modelId, cacheDir, remoteHost);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tlastError = error;\n\t\t\t\t\tif (disposed) throw error;\n\t\t\t\t\t// 镜像轮换前清 0 字节残骸 (embedding-provider 同理, model-cache-hygiene)。\n\t\t\t\t\tpurgeZeroByteCacheArtifacts(join(cacheDir, modelId));\n\t\t\t\t}\n\t\t\t\t// 同 host 一次短退避重试 (2026-10-06 live E2E): 覆盖 rejection 型瞬断\n\t\t\t\t// (fetch 网络错/5xx)。对\"仓库 API 清单缺文件\"型不生效——该结果被\n\t\t\t\t// transformers 模块级 memoize (见 annotateUpstreamListingPoison),\n\t\t\t\t// 进程内换 host 也拿同一空清单。\n\t\t\t\ttry {\n\t\t\t\t\tawait new Promise((resolve) => setTimeout(resolve, 2_000));\n\t\t\t\t\tif (disposed) throw lastError;\n\t\t\t\t\treturn await load(modelId, cacheDir, remoteHost);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tlastError = error;\n\t\t\t\t\tif (disposed) throw error;\n\t\t\t\t\tpurgeZeroByteCacheArtifacts(join(cacheDir, modelId));\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (lastError === undefined) throw new Error(`no remote hosts configured for model ${modelId}`);\n\t\t\tthrow annotateUpstreamListingPoison(lastError);\n\t\t})();\n\t\tbackendPromise = attempt;\n\t\tattempt.catch(() => {\n\t\t\t// 失败不缓存: 下次 rerank 重新走镜像序列 (显式失败, 不静默降级)。\n\t\t\tif (backendPromise === attempt) {\n\t\t\t\tbackendPromise = null;\n\t\t\t}\n\t\t});\n\t\treturn attempt;\n\t};\n\n\tconst rerank = async (query: string, docs: readonly string[]): Promise<readonly number[]> => {\n\t\tif (disposed) throw new Error(\"memory reranker is disposed\");\n\t\tif (docs.length === 0) return [];\n\t\tconst backend = await loadBackend();\n\t\tif (disposed) throw new Error(\"memory reranker is disposed\");\n\t\treturn backend.score(query, [...docs]);\n\t};\n\treturn {\n\t\tmodelId,\n\t\trerank: (query, docs) => {\n\t\t\tconst call = rerank(query, docs);\n\t\t\tactiveCalls.add(call);\n\t\t\tvoid call.then(\n\t\t\t\t() => activeCalls.delete(call),\n\t\t\t\t() => activeCalls.delete(call),\n\t\t\t);\n\t\t\treturn call;\n\t\t},\n\t\tdispose: () => {\n\t\t\tdisposed = true;\n\t\t\tdisposePromise ??= (async () => {\n\t\t\t\tconst pendingBackend = backendPromise;\n\t\t\t\tawait Promise.allSettled([...activeCalls]);\n\t\t\t\tconst backend = await pendingBackend?.catch(() => undefined);\n\t\t\t\tawait backend?.dispose?.();\n\t\t\t})();\n\t\t\treturn disposePromise;\n\t\t},\n\t};\n}\n"]}
@@ -37,6 +37,9 @@ const createDefaultTransformersLoadImpl = (moduleEntryPath) => {
37
37
  dtype: "q8",
38
38
  }));
39
39
  return {
40
+ dispose: async () => {
41
+ await model.dispose();
42
+ },
40
43
  score: async (query, docs) => {
41
44
  const inputs = tokenizer(docs.map(() => query), { text_pair: docs, padding: true, truncation: true });
42
45
  const output = await model(inputs);
@@ -64,6 +67,9 @@ export function createTransformersReranker(options) {
64
67
  const remoteHosts = options.remoteHosts ?? [DEFAULT_REMOTE_HOST];
65
68
  const load = options.loadImpl ?? createDefaultTransformersLoadImpl(options.moduleEntryPath);
66
69
  let backendPromise = null;
70
+ let disposed = false;
71
+ let disposePromise;
72
+ const activeCalls = new Set();
67
73
  const loadBackend = () => {
68
74
  const existing = backendPromise;
69
75
  if (existing) {
@@ -77,6 +83,8 @@ export function createTransformersReranker(options) {
77
83
  }
78
84
  catch (error) {
79
85
  lastError = error;
86
+ if (disposed)
87
+ throw error;
80
88
  // 镜像轮换前清 0 字节残骸 (embedding-provider 同理, model-cache-hygiene)。
81
89
  purgeZeroByteCacheArtifacts(join(cacheDir, modelId));
82
90
  }
@@ -86,10 +94,14 @@ export function createTransformersReranker(options) {
86
94
  // 进程内换 host 也拿同一空清单。
87
95
  try {
88
96
  await new Promise((resolve) => setTimeout(resolve, 2_000));
97
+ if (disposed)
98
+ throw lastError;
89
99
  return await load(modelId, cacheDir, remoteHost);
90
100
  }
91
101
  catch (error) {
92
102
  lastError = error;
103
+ if (disposed)
104
+ throw error;
93
105
  purgeZeroByteCacheArtifacts(join(cacheDir, modelId));
94
106
  }
95
107
  }
@@ -106,15 +118,33 @@ export function createTransformersReranker(options) {
106
118
  });
107
119
  return attempt;
108
120
  };
121
+ const rerank = async (query, docs) => {
122
+ if (disposed)
123
+ throw new Error("memory reranker is disposed");
124
+ if (docs.length === 0)
125
+ return [];
126
+ const backend = await loadBackend();
127
+ if (disposed)
128
+ throw new Error("memory reranker is disposed");
129
+ return backend.score(query, [...docs]);
130
+ };
109
131
  return {
110
132
  modelId,
111
- rerank: async (query, docs) => {
112
- if (docs.length === 0) {
113
- // 空 docs 直接返回, 不触发模型加载 (lazy-load 只发生在真正需要打分时)。
114
- return [];
115
- }
116
- const backend = await loadBackend();
117
- return backend.score(query, [...docs]);
133
+ rerank: (query, docs) => {
134
+ const call = rerank(query, docs);
135
+ activeCalls.add(call);
136
+ void call.then(() => activeCalls.delete(call), () => activeCalls.delete(call));
137
+ return call;
138
+ },
139
+ dispose: () => {
140
+ disposed = true;
141
+ disposePromise ??= (async () => {
142
+ const pendingBackend = backendPromise;
143
+ await Promise.allSettled([...activeCalls]);
144
+ const backend = await pendingBackend?.catch(() => undefined);
145
+ await backend?.dispose?.();
146
+ })();
147
+ return disposePromise;
118
148
  },
119
149
  };
120
150
  }
@@ -1 +1 @@
1
- {"version":3,"file":"embedding-reranker.js","sourceRoot":"","sources":["../../src/memory/embedding-reranker.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Memory cross-encoder reranker for hybrid retrieval (混合检索 · 精排侧).\n *\n * POC-verified (2026-09, 100k corpus): reranking drift-query retrieval with\n * Xenova/bge-reranker-base lifts high-tier recall to re@10 100% (低档 83%),\n * inference deterministic across runs. Reranking is an independently\n * disable-able component (宪法: 独立可禁用); tiers are caller configuration.\n *\n * Model note: default \"Xenova/bge-reranker-base\" is the only officially\n * converted (transformers.js) reranker verified in POC; q8 weights ~280MB,\n * 512-token context — the caller owns the write-side length contract\n * (statement ≤700 chars); overlong inputs are gracefully truncated by the\n * tokenizer (`truncation: true`). Upgrade path bge-reranker-v2-m3 is a\n * modelId swap away, but it currently has no official transformers.js\n * conversion.\n *\n * Runtime dep @huggingface/transformers is lazy-loaded via `await import` at\n * first rerank (惯例参考 packages/agent-forge/src/utils/photon.ts); type\n * positions use type-only imports. Any failure propagates — 不静默降级.\n */\n\n/**\n * Cross-encoder reranker. Higher score = more relevant; raw logits are only\n * order-preserving, not calibrated probabilities.\n */\nimport { join } from \"node:path\";\nimport { importHostOrBareModule } from \"./host-module-import.ts\";\nimport { annotateUpstreamListingPoison, purgeZeroByteCacheArtifacts } from \"./model-cache-hygiene.ts\";\n\nexport interface MemoryRerankerV1 {\n\treadonly modelId: string;\n\t/** Score each doc against the query; higher = more relevant (raw logits, order-preserving). */\n\trerank(query: string, docs: readonly string[]): Promise<readonly number[]>;\n}\n\n/**\n * Minimal structural seam around a transformers.js sequence-classification\n * pair, so tests can inject a fake without touching the real loader.\n */\nexport interface TransformersRerankerBackendV1 {\n\t/** Score docs against the query; result[i] corresponds to docs[i]. */\n\tscore: (query: string, docs: string[]) => Promise<number[]>;\n}\n\nexport type TransformersRerankerLoadImpl = (\n\tmodelId: string,\n\tcacheDir: string,\n\tremoteHost: string,\n) => Promise<TransformersRerankerBackendV1>;\n\nconst DEFAULT_RERANKER_MODEL_ID = \"Xenova/bge-reranker-base\";\nconst DEFAULT_REMOTE_HOST = \"https://huggingface.co\";\n\n/** Structural view of the tokenizer call; returns the model-input object. */\ntype RerankTokenizerCall = (\n\ttexts: string[],\n\toptions: { text_pair: string[]; padding: boolean; truncation: boolean },\n) => unknown;\n\n/** Structural view of the sequence-classification forward pass. */\ntype RerankModelCall = (inputs: unknown) => Promise<{ logits: { data: ArrayLike<number> } }>;\n\nconst createDefaultTransformersLoadImpl = (moduleEntryPath?: string): TransformersRerankerLoadImpl => {\n\treturn async (modelId, cacheDir, remoteHost) => {\n\t\tconst transformers = await importHostOrBareModule<typeof import(\"@huggingface/transformers\")>(\n\t\t\tmoduleEntryPath ?? \"@huggingface/transformers\",\n\t\t);\n\t\ttransformers.env.cacheDir = cacheDir;\n\t\ttransformers.env.remoteHost = remoteHost;\n\t\tconst tokenizer = (await transformers.AutoTokenizer.from_pretrained(modelId)) as unknown as RerankTokenizerCall;\n\t\tconst model = (await transformers.AutoModelForSequenceClassification.from_pretrained(modelId, {\n\t\t\tdtype: \"q8\",\n\t\t})) as unknown as RerankModelCall;\n\t\treturn {\n\t\t\tscore: async (query, docs) => {\n\t\t\t\tconst inputs = tokenizer(\n\t\t\t\t\tdocs.map(() => query),\n\t\t\t\t\t{ text_pair: docs, padding: true, truncation: true },\n\t\t\t\t);\n\t\t\t\tconst output = await model(inputs);\n\t\t\t\treturn Array.from(output.logits.data);\n\t\t\t},\n\t\t};\n\t};\n};\n\n/**\n * Transformers.js-backed cross-encoder reranker (两档共用默认\n * Xenova/bge-reranker-base, 见文件头模型说明)。\n *\n * 镜像序列: 首次 rerank 时按 `remoteHosts` 顺序逐个尝试加载 (调用方通常传\n * [\"https://huggingface.co\", \"https://hf-mirror.com\"]); 某 host 加载抛错则\n * 尝试下一个, 全部失败时抛最后一个错误。host 确定后模型句柄只加载一次\n * (并发 rerank 共享同一 in-flight promise); 加载失败不缓存, 下次 rerank\n * 重新走完整序列, 失败原因原样保留, 不静默降级。\n *\n * `loadImpl` 是测试 seam: 注入后绕过真实加载, 签名\n * (modelId, cacheDir, remoteHost) => backend。\n */\nexport function createTransformersReranker(options: {\n\treadonly modelId?: string;\n\treadonly cacheDir: string;\n\treadonly remoteHosts?: readonly string[];\n\treadonly loadImpl?: TransformersRerankerLoadImpl;\n\t/** Host-resolved transformers entry path (install-store visibility); undefined = bare import. */\n\treadonly moduleEntryPath?: string;\n}): MemoryRerankerV1 {\n\tconst modelId = options.modelId ?? DEFAULT_RERANKER_MODEL_ID;\n\tconst cacheDir = options.cacheDir;\n\tconst remoteHosts = options.remoteHosts ?? [DEFAULT_REMOTE_HOST];\n\tconst load = options.loadImpl ?? createDefaultTransformersLoadImpl(options.moduleEntryPath);\n\n\tlet backendPromise: Promise<TransformersRerankerBackendV1> | null = null;\n\tconst loadBackend = (): Promise<TransformersRerankerBackendV1> => {\n\t\tconst existing = backendPromise;\n\t\tif (existing) {\n\t\t\treturn existing;\n\t\t}\n\t\tconst attempt = (async () => {\n\t\t\tlet lastError: unknown;\n\t\t\tfor (const remoteHost of remoteHosts) {\n\t\t\t\ttry {\n\t\t\t\t\treturn await load(modelId, cacheDir, remoteHost);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tlastError = error;\n\t\t\t\t\t// 镜像轮换前清 0 字节残骸 (embedding-provider 同理, model-cache-hygiene)。\n\t\t\t\t\tpurgeZeroByteCacheArtifacts(join(cacheDir, modelId));\n\t\t\t\t}\n\t\t\t\t// 同 host 一次短退避重试 (2026-10-06 live E2E): 覆盖 rejection 型瞬断\n\t\t\t\t// (fetch 网络错/5xx)。对\"仓库 API 清单缺文件\"型不生效——该结果被\n\t\t\t\t// transformers 模块级 memoize (见 annotateUpstreamListingPoison),\n\t\t\t\t// 进程内换 host 也拿同一空清单。\n\t\t\t\ttry {\n\t\t\t\t\tawait new Promise((resolve) => setTimeout(resolve, 2_000));\n\t\t\t\t\treturn await load(modelId, cacheDir, remoteHost);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tlastError = error;\n\t\t\t\t\tpurgeZeroByteCacheArtifacts(join(cacheDir, modelId));\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (lastError === undefined) throw new Error(`no remote hosts configured for model ${modelId}`);\n\t\t\tthrow annotateUpstreamListingPoison(lastError);\n\t\t})();\n\t\tbackendPromise = attempt;\n\t\tattempt.catch(() => {\n\t\t\t// 失败不缓存: 下次 rerank 重新走镜像序列 (显式失败, 不静默降级)。\n\t\t\tif (backendPromise === attempt) {\n\t\t\t\tbackendPromise = null;\n\t\t\t}\n\t\t});\n\t\treturn attempt;\n\t};\n\n\treturn {\n\t\tmodelId,\n\t\trerank: async (query, docs) => {\n\t\t\tif (docs.length === 0) {\n\t\t\t\t// 空 docs 直接返回, 不触发模型加载 (lazy-load 只发生在真正需要打分时)。\n\t\t\t\treturn [];\n\t\t\t}\n\t\t\tconst backend = await loadBackend();\n\t\t\treturn backend.score(query, [...docs]);\n\t\t},\n\t};\n}\n"]}
1
+ {"version":3,"file":"embedding-reranker.js","sourceRoot":"","sources":["../../src/memory/embedding-reranker.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Memory cross-encoder reranker for hybrid retrieval (混合检索 · 精排侧).\n *\n * POC-verified (2026-09, 100k corpus): reranking drift-query retrieval with\n * Xenova/bge-reranker-base lifts high-tier recall to re@10 100% (低档 83%),\n * inference deterministic across runs. Reranking is an independently\n * disable-able component (宪法: 独立可禁用); tiers are caller configuration.\n *\n * Model note: default \"Xenova/bge-reranker-base\" is the only officially\n * converted (transformers.js) reranker verified in POC; q8 weights ~280MB,\n * 512-token context — the caller owns the write-side length contract\n * (statement ≤700 chars); overlong inputs are gracefully truncated by the\n * tokenizer (`truncation: true`). Upgrade path bge-reranker-v2-m3 is a\n * modelId swap away, but it currently has no official transformers.js\n * conversion.\n *\n * Runtime dep @huggingface/transformers is lazy-loaded via `await import` at\n * first rerank (惯例参考 packages/agent-forge/src/utils/photon.ts); type\n * positions use type-only imports. Any failure propagates — 不静默降级.\n */\n\n/**\n * Cross-encoder reranker. Higher score = more relevant; raw logits are only\n * order-preserving, not calibrated probabilities.\n */\nimport { join } from \"node:path\";\nimport { importHostOrBareModule } from \"./host-module-import.ts\";\nimport { annotateUpstreamListingPoison, purgeZeroByteCacheArtifacts } from \"./model-cache-hygiene.ts\";\n\nexport interface MemoryRerankerV1 {\n\treadonly modelId: string;\n\t/** Score each doc against the query; higher = more relevant (raw logits, order-preserving). */\n\trerank(query: string, docs: readonly string[]): Promise<readonly number[]>;\n\t/** 释放本实例模型;在途原生推理完成后关闭,不删除缓存。 */\n\tdispose?(): Promise<void>;\n}\n\n/**\n * Minimal structural seam around a transformers.js sequence-classification\n * pair, so tests can inject a fake without touching the real loader.\n */\nexport interface TransformersRerankerBackendV1 {\n\t/** Score docs against the query; result[i] corresponds to docs[i]. */\n\tscore: (query: string, docs: string[]) => Promise<number[]>;\n\tdispose?(): Promise<void>;\n}\n\nexport type TransformersRerankerLoadImpl = (\n\tmodelId: string,\n\tcacheDir: string,\n\tremoteHost: string,\n) => Promise<TransformersRerankerBackendV1>;\n\nconst DEFAULT_RERANKER_MODEL_ID = \"Xenova/bge-reranker-base\";\nconst DEFAULT_REMOTE_HOST = \"https://huggingface.co\";\n\n/** Structural view of the tokenizer call; returns the model-input object. */\ntype RerankTokenizerCall = (\n\ttexts: string[],\n\toptions: { text_pair: string[]; padding: boolean; truncation: boolean },\n) => unknown;\n\n/** Structural view of the sequence-classification forward pass. */\ntype RerankModelCall = ((inputs: unknown) => Promise<{ logits: { data: ArrayLike<number> } }>) & {\n\tdispose(): Promise<unknown>;\n};\n\nconst createDefaultTransformersLoadImpl = (moduleEntryPath?: string): TransformersRerankerLoadImpl => {\n\treturn async (modelId, cacheDir, remoteHost) => {\n\t\tconst transformers = await importHostOrBareModule<typeof import(\"@huggingface/transformers\")>(\n\t\t\tmoduleEntryPath ?? \"@huggingface/transformers\",\n\t\t);\n\t\ttransformers.env.cacheDir = cacheDir;\n\t\ttransformers.env.remoteHost = remoteHost;\n\t\tconst tokenizer = (await transformers.AutoTokenizer.from_pretrained(modelId)) as unknown as RerankTokenizerCall;\n\t\tconst model = (await transformers.AutoModelForSequenceClassification.from_pretrained(modelId, {\n\t\t\tdtype: \"q8\",\n\t\t})) as unknown as RerankModelCall;\n\t\treturn {\n\t\t\tdispose: async () => {\n\t\t\t\tawait model.dispose();\n\t\t\t},\n\t\t\tscore: async (query, docs) => {\n\t\t\t\tconst inputs = tokenizer(\n\t\t\t\t\tdocs.map(() => query),\n\t\t\t\t\t{ text_pair: docs, padding: true, truncation: true },\n\t\t\t\t);\n\t\t\t\tconst output = await model(inputs);\n\t\t\t\treturn Array.from(output.logits.data);\n\t\t\t},\n\t\t};\n\t};\n};\n\n/**\n * Transformers.js-backed cross-encoder reranker (两档共用默认\n * Xenova/bge-reranker-base, 见文件头模型说明)。\n *\n * 镜像序列: 首次 rerank 时按 `remoteHosts` 顺序逐个尝试加载 (调用方通常传\n * [\"https://huggingface.co\", \"https://hf-mirror.com\"]); 某 host 加载抛错则\n * 尝试下一个, 全部失败时抛最后一个错误。host 确定后模型句柄只加载一次\n * (并发 rerank 共享同一 in-flight promise); 加载失败不缓存, 下次 rerank\n * 重新走完整序列, 失败原因原样保留, 不静默降级。\n *\n * `loadImpl` 是测试 seam: 注入后绕过真实加载, 签名\n * (modelId, cacheDir, remoteHost) => backend。\n */\nexport function createTransformersReranker(options: {\n\treadonly modelId?: string;\n\treadonly cacheDir: string;\n\treadonly remoteHosts?: readonly string[];\n\treadonly loadImpl?: TransformersRerankerLoadImpl;\n\t/** Host-resolved transformers entry path (install-store visibility); undefined = bare import. */\n\treadonly moduleEntryPath?: string;\n}): MemoryRerankerV1 {\n\tconst modelId = options.modelId ?? DEFAULT_RERANKER_MODEL_ID;\n\tconst cacheDir = options.cacheDir;\n\tconst remoteHosts = options.remoteHosts ?? [DEFAULT_REMOTE_HOST];\n\tconst load = options.loadImpl ?? createDefaultTransformersLoadImpl(options.moduleEntryPath);\n\n\tlet backendPromise: Promise<TransformersRerankerBackendV1> | null = null;\n\tlet disposed = false;\n\tlet disposePromise: Promise<void> | undefined;\n\tconst activeCalls = new Set<Promise<unknown>>();\n\tconst loadBackend = (): Promise<TransformersRerankerBackendV1> => {\n\t\tconst existing = backendPromise;\n\t\tif (existing) {\n\t\t\treturn existing;\n\t\t}\n\t\tconst attempt = (async () => {\n\t\t\tlet lastError: unknown;\n\t\t\tfor (const remoteHost of remoteHosts) {\n\t\t\t\ttry {\n\t\t\t\t\treturn await load(modelId, cacheDir, remoteHost);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tlastError = error;\n\t\t\t\t\tif (disposed) throw error;\n\t\t\t\t\t// 镜像轮换前清 0 字节残骸 (embedding-provider 同理, model-cache-hygiene)。\n\t\t\t\t\tpurgeZeroByteCacheArtifacts(join(cacheDir, modelId));\n\t\t\t\t}\n\t\t\t\t// 同 host 一次短退避重试 (2026-10-06 live E2E): 覆盖 rejection 型瞬断\n\t\t\t\t// (fetch 网络错/5xx)。对\"仓库 API 清单缺文件\"型不生效——该结果被\n\t\t\t\t// transformers 模块级 memoize (见 annotateUpstreamListingPoison),\n\t\t\t\t// 进程内换 host 也拿同一空清单。\n\t\t\t\ttry {\n\t\t\t\t\tawait new Promise((resolve) => setTimeout(resolve, 2_000));\n\t\t\t\t\tif (disposed) throw lastError;\n\t\t\t\t\treturn await load(modelId, cacheDir, remoteHost);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tlastError = error;\n\t\t\t\t\tif (disposed) throw error;\n\t\t\t\t\tpurgeZeroByteCacheArtifacts(join(cacheDir, modelId));\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (lastError === undefined) throw new Error(`no remote hosts configured for model ${modelId}`);\n\t\t\tthrow annotateUpstreamListingPoison(lastError);\n\t\t})();\n\t\tbackendPromise = attempt;\n\t\tattempt.catch(() => {\n\t\t\t// 失败不缓存: 下次 rerank 重新走镜像序列 (显式失败, 不静默降级)。\n\t\t\tif (backendPromise === attempt) {\n\t\t\t\tbackendPromise = null;\n\t\t\t}\n\t\t});\n\t\treturn attempt;\n\t};\n\n\tconst rerank = async (query: string, docs: readonly string[]): Promise<readonly number[]> => {\n\t\tif (disposed) throw new Error(\"memory reranker is disposed\");\n\t\tif (docs.length === 0) return [];\n\t\tconst backend = await loadBackend();\n\t\tif (disposed) throw new Error(\"memory reranker is disposed\");\n\t\treturn backend.score(query, [...docs]);\n\t};\n\treturn {\n\t\tmodelId,\n\t\trerank: (query, docs) => {\n\t\t\tconst call = rerank(query, docs);\n\t\t\tactiveCalls.add(call);\n\t\t\tvoid call.then(\n\t\t\t\t() => activeCalls.delete(call),\n\t\t\t\t() => activeCalls.delete(call),\n\t\t\t);\n\t\t\treturn call;\n\t\t},\n\t\tdispose: () => {\n\t\t\tdisposed = true;\n\t\t\tdisposePromise ??= (async () => {\n\t\t\t\tconst pendingBackend = backendPromise;\n\t\t\t\tawait Promise.allSettled([...activeCalls]);\n\t\t\t\tconst backend = await pendingBackend?.catch(() => undefined);\n\t\t\t\tawait backend?.dispose?.();\n\t\t\t})();\n\t\t\treturn disposePromise;\n\t\t},\n\t};\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"foundation.d.ts","sourceRoot":"","sources":["../../src/memory/foundation.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Memory Foundation — canonical atom, identity, and immutable snapshots (1C.1a).\n *\n * A canonical atom is the immutable committed fact. Derived state (attention,\n * effective status, purgeAt) is NOT part of the atom; it is rebuilt from\n * lifecycle events (1C.1b/1C.2). Everything returned to hosts and plugins is a\n * detached, deep-frozen snapshot: mutating the input after `buildMemoryAtomV1`\n * never changes the stored atom.\n *\n * Field semantics follow `docs/design/记忆系统设计.md` §4 (记录模型), which is the\n * authoritative source for memory fields.\n */\nimport type { JsonValue } from \"@agent-forge/plugin-sdk\";\n\n/** Source of one memory: where the fact came from, optionally pinned by revision/digest. */\nexport interface MemorySourceRefV1 {\n\treadonly kind: \"session\" | \"entry\" | \"artifact\" | \"tool\" | \"state\";\n\treadonly id: string;\n\treadonly revision?: string;\n\treadonly digest?: string;\n}\n\n/** Namespace-scoped search facet. Facets are atom facts, never derived. */\nexport interface MemoryFacetV1 {\n\treadonly namespace: string;\n\treadonly schemaVersion: number;\n\treadonly key: string;\n\treadonly value: string;\n}\n\n/** Typed relation between atoms. Weights/validity belong to projections, not here. */\nexport interface MemoryRelationV1 {\n\treadonly relationId: string;\n\treadonly namespace: string;\n\treadonly schemaVersion: number;\n\treadonly kind: string;\n\treadonly targetMemoryId?: string;\n\treadonly targetRef?: MemorySourceRefV1;\n\treadonly confidence?: number;\n\treadonly relationRevision: string;\n}\n\nexport interface MemoryTimeRangeV1 {\n\treadonly from?: string;\n\treadonly to?: string;\n}\n\n/** Applicability narrowing; empty means \"applies without narrowing\". */\nexport interface MemoryApplicabilityV1 {\n\treadonly scenes?: readonly string[];\n\treadonly projects?: readonly string[];\n\treadonly tasks?: readonly string[];\n\treadonly timeRange?: MemoryTimeRangeV1;\n\treadonly exceptions?: readonly string[];\n}\n\n/** Preference envelope: the generic schema frozen for 1C; policy semantics come later. */\nexport interface MemoryPreferenceEnvelopeV1 {\n\treadonly subject: \"user\" | \"project\" | \"task\" | \"environment\";\n\treadonly key: string;\n\treadonly preferredValue: JsonValue;\n\treadonly alternatives?: readonly JsonValue[];\n\treadonly scope: {\n\t\treadonly level: \"task\" | \"project\" | \"scene\" | \"timeRange\" | \"profile-private\" | \"user-default\";\n\t\treadonly scenes?: readonly string[];\n\t\treadonly projects?: readonly string[];\n\t\treadonly tasks?: readonly string[];\n\t\treadonly timeRange?: MemoryTimeRangeV1;\n\t\treadonly exceptions?: readonly string[];\n\t};\n\treadonly evidence: {\n\t\treadonly class: \"explicit\" | \"repeated_behavior\" | \"inferred\";\n\t\treadonly sourceRefs: readonly MemorySourceRefV1[];\n\t\treadonly confidence: number;\n\t};\n\treadonly applicabilityConfidence: number;\n\treadonly confirmedAt?: string;\n}\n\n/** Provenance for synthesis/compaction memories derived from other atoms. */\nexport interface MemoryProvenanceV1 {\n\treadonly sourceMemoryIds: readonly string[];\n\treadonly purgeGroupId: string;\n\treadonly containsSourceContent: boolean;\n}\n\nexport type MemoryScopeV1 = \"session\" | \"cycle\" | \"long-term\";\nexport type MemoryKindV1 =\n\t| \"observation\"\n\t| \"preference\"\n\t| \"fact\"\n\t| \"decision\"\n\t| \"constraint\"\n\t| \"inference\"\n\t| \"synthesis\";\nexport type MemoryEvidenceClassV1 = \"explicit\" | \"repeated_behavior\" | \"tool_or_test\" | \"derived\";\n\n/** The immutable canonical atom. Once committed, no field may change in place. */\nexport interface MemoryAtomV1<TPayload = JsonValue> {\n\treadonly memoryId: string;\n\treadonly contractVersion: string;\n\treadonly schemaVersion: number;\n\treadonly scope: MemoryScopeV1;\n\treadonly retentionMode: string;\n\treadonly owner: string;\n\treadonly profileId: string;\n\treadonly shareGroupId?: string;\n\t/**\n\t * Suite (方案) this atom belongs to (方案系统设计 §6.1/§11, M5). Absent\n\t * (or protocol-level `null` on the wire, normalized to absent here) marks a\n\t * legacy pre-M5 atom: such atoms are fail-safe INVISIBLE to every\n\t * suite-scoped read and only readable through suite-unscoped queries. The\n\t * one cross-suite exception is an explicitly promoted user-default\n\t * preference (see {@link memorySuiteVisibility}).\n\t */\n\treadonly suiteId?: string;\n\treadonly retentionPolicyVersion: string;\n\treadonly memoryKind: MemoryKindV1;\n\treadonly payload: TPayload;\n\treadonly preference?: MemoryPreferenceEnvelopeV1;\n\treadonly occurredAt: string;\n\treadonly recordedAt: string;\n\treadonly sourceRefs: readonly MemorySourceRefV1[];\n\treadonly observationId: string;\n\treadonly sessionRefs: readonly string[];\n\treadonly agentInstanceRefs: readonly string[];\n\treadonly projectRefs: readonly string[];\n\treadonly subjectRefs: readonly string[];\n\treadonly facets: readonly MemoryFacetV1[];\n\treadonly relations: readonly MemoryRelationV1[];\n\treadonly provenance?: MemoryProvenanceV1;\n\treadonly confidence: number;\n\treadonly importance: number;\n\treadonly applicability?: MemoryApplicabilityV1;\n\treadonly evidenceClass: MemoryEvidenceClassV1;\n\treadonly contentRevision: string;\n\treadonly writeReason: string;\n}\n\ntype PlainRecord = Record<string, unknown>;\n\nfunction isPlainRecord(value: unknown): value is PlainRecord {\n\tif (value === null || typeof value !== \"object\" || Array.isArray(value)) return false;\n\tconst prototype = Object.getPrototypeOf(value);\n\treturn prototype === Object.prototype || prototype === null;\n}\n\nfunction assertPlainRecord(value: unknown, label: string): asserts value is PlainRecord {\n\tif (!isPlainRecord(value)) throw new Error(`${label} must be a plain object`);\n}\n\nfunction assertExactKeys(\n\tvalue: PlainRecord,\n\trequired: readonly string[],\n\toptional: readonly string[],\n\tlabel: string,\n): void {\n\tconst allowed = new Set([...required, ...optional]);\n\tconst unsupported = Object.keys(value)\n\t\t.filter((key) => !allowed.has(key))\n\t\t.sort();\n\tif (unsupported.length > 0) throw new Error(`${label} has unsupported fields: ${unsupported.join(\", \")}`);\n\tfor (const key of required) {\n\t\tif (!(key in value)) throw new Error(`${label} is missing required field: ${key}`);\n\t}\n}\n\nfunction assertNonEmptyString(value: unknown, label: string): asserts value is string {\n\tif (typeof value !== \"string\" || value.trim().length === 0) throw new Error(`${label} must be a non-empty string`);\n}\n\nfunction assertPositiveSafeInteger(value: unknown, label: string): asserts value is number {\n\tif (typeof value !== \"number\" || !Number.isSafeInteger(value) || value < 1) {\n\t\tthrow new Error(`${label} must be a positive safe integer`);\n\t}\n}\n\nfunction assertUnitInterval(value: unknown, label: string): asserts value is number {\n\tif (typeof value !== \"number\" || !Number.isFinite(value) || value < 0 || value > 1) {\n\t\tthrow new Error(`${label} must be a number between 0 and 1`);\n\t}\n}\n\nfunction assertTimestamp(value: unknown, label: string): asserts value is string {\n\tassertNonEmptyString(value, label);\n\tif (Number.isNaN(Date.parse(value))) throw new Error(`${label} must be an ISO-8601 timestamp`);\n}\n\nfunction assertJsonValue(value: unknown, label: string, seen = new WeakSet<object>()): asserts value is JsonValue {\n\tif (value === null || typeof value === \"string\" || typeof value === \"boolean\") return;\n\tif (typeof value === \"number\") {\n\t\tif (Number.isFinite(value)) return;\n\t\tthrow new Error(`${label} must contain finite JSON numbers`);\n\t}\n\tif (typeof value !== \"object\") throw new Error(`${label} must be JSON-compatible`);\n\tif (seen.has(value)) throw new Error(`${label} must not contain cycles`);\n\tseen.add(value);\n\tif (Array.isArray(value)) {\n\t\tfor (const [index, child] of value.entries()) assertJsonValue(child, `${label}[${index}]`, seen);\n\t} else {\n\t\tif (!isPlainRecord(value)) throw new Error(`${label} must contain only plain JSON objects`);\n\t\tfor (const [key, child] of Object.entries(value)) assertJsonValue(child, `${label}.${key}`, seen);\n\t}\n\tseen.delete(value);\n}\n\nfunction assertStringArray(value: unknown, label: string): asserts value is readonly string[] {\n\tif (!Array.isArray(value)) throw new Error(`${label} must be an array`);\n\tfor (const [index, item] of value.entries()) assertNonEmptyString(item, `${label}[${index}]`);\n}\n\nfunction detachedFreeze<T>(value: T, seen = new WeakMap<object, unknown>()): T {\n\tif (value === null || typeof value !== \"object\") return value;\n\tconst existing = seen.get(value);\n\t// v8 ignore next -- 校验后的原子全部由新副本构成,不存在共享引用\n\tif (existing !== undefined) return existing as T;\n\tif (Array.isArray(value)) {\n\t\tconst copy: unknown[] = [];\n\t\tseen.set(value, copy);\n\t\tfor (const child of value) copy.push(detachedFreeze(child, seen));\n\t\treturn Object.freeze(copy) as T;\n\t}\n\tconst copy: Record<string, unknown> = Object.create(null) as Record<string, unknown>;\n\tseen.set(value, copy);\n\tfor (const [key, child] of Object.entries(value as Record<string, unknown>)) {\n\t\tcopy[key] = detachedFreeze(child, seen);\n\t}\n\treturn Object.freeze(copy) as T;\n}\n\n/** Stable JSON serialization with recursively sorted object keys (for identity digests). */\nfunction stableStringify(value: JsonValue): string {\n\tif (value === null || typeof value === \"boolean\" || typeof value === \"number\" || typeof value === \"string\") {\n\t\treturn JSON.stringify(value);\n\t}\n\tif (Array.isArray(value)) return `[${value.map((item) => stableStringify(item)).join(\",\")}]`;\n\tconst entries = Object.keys(value)\n\t\t.sort()\n\t\t.map((key) => `${JSON.stringify(key)}:${stableStringify((value as Record<string, JsonValue>)[key])}`);\n\treturn `{${entries.join(\",\")}}`;\n}\n\nconst MEMORY_SCOPES_V1: readonly MemoryScopeV1[] = [\"session\", \"cycle\", \"long-term\"];\nconst MEMORY_KINDS_V1: readonly MemoryKindV1[] = [\n\t\"observation\",\n\t\"preference\",\n\t\"fact\",\n\t\"decision\",\n\t\"constraint\",\n\t\"inference\",\n\t\"synthesis\",\n];\nconst MEMORY_EVIDENCE_CLASSES_V1: readonly MemoryEvidenceClassV1[] = [\n\t\"explicit\",\n\t\"repeated_behavior\",\n\t\"tool_or_test\",\n\t\"derived\",\n];\nconst SOURCE_REF_KINDS: readonly MemorySourceRefV1[\"kind\"][] = [\"session\", \"entry\", \"artifact\", \"tool\", \"state\"];\n\nfunction validateSourceRef(value: unknown, label: string): MemorySourceRefV1 {\n\tassertPlainRecord(value, label);\n\tassertExactKeys(value, [\"kind\", \"id\"], [\"revision\", \"digest\"], label);\n\tif (!SOURCE_REF_KINDS.includes(value.kind as MemorySourceRefV1[\"kind\"])) {\n\t\tthrow new Error(`${label}.kind is invalid`);\n\t}\n\tassertNonEmptyString(value.id, `${label}.id`);\n\tif (value.revision !== undefined) assertNonEmptyString(value.revision, `${label}.revision`);\n\tif (value.digest !== undefined) assertNonEmptyString(value.digest, `${label}.digest`);\n\treturn detachedFreeze({\n\t\tkind: value.kind as MemorySourceRefV1[\"kind\"],\n\t\tid: value.id,\n\t\t...(value.revision === undefined ? {} : { revision: value.revision }),\n\t\t...(value.digest === undefined ? {} : { digest: value.digest }),\n\t});\n}\n\nfunction validateFacet(value: unknown, label: string): MemoryFacetV1 {\n\tassertPlainRecord(value, label);\n\tassertExactKeys(value, [\"namespace\", \"schemaVersion\", \"key\", \"value\"], [], label);\n\tassertNonEmptyString(value.namespace, `${label}.namespace`);\n\tassertPositiveSafeInteger(value.schemaVersion, `${label}.schemaVersion`);\n\tassertNonEmptyString(value.key, `${label}.key`);\n\tassertNonEmptyString(value.value, `${label}.value`);\n\treturn detachedFreeze({\n\t\tnamespace: value.namespace,\n\t\tschemaVersion: value.schemaVersion,\n\t\tkey: value.key,\n\t\tvalue: value.value,\n\t});\n}\n\nfunction validateRelation(value: unknown, label: string): MemoryRelationV1 {\n\tassertPlainRecord(value, label);\n\tassertExactKeys(\n\t\tvalue,\n\t\t[\"relationId\", \"namespace\", \"schemaVersion\", \"kind\", \"relationRevision\"],\n\t\t[\"targetMemoryId\", \"targetRef\", \"confidence\"],\n\t\tlabel,\n\t);\n\tassertNonEmptyString(value.relationId, `${label}.relationId`);\n\tassertNonEmptyString(value.namespace, `${label}.namespace`);\n\tassertPositiveSafeInteger(value.schemaVersion, `${label}.schemaVersion`);\n\tassertNonEmptyString(value.kind, `${label}.kind`);\n\tassertNonEmptyString(value.relationRevision, `${label}.relationRevision`);\n\tif (value.targetMemoryId !== undefined && value.targetRef !== undefined) {\n\t\tthrow new Error(`${label} must not declare both targetMemoryId and targetRef`);\n\t}\n\tconst targetMemoryId = value.targetMemoryId === undefined ? undefined : (value.targetMemoryId as string);\n\tif (targetMemoryId !== undefined) assertNonEmptyString(targetMemoryId, `${label}.targetMemoryId`);\n\tconst targetRef =\n\t\tvalue.targetRef === undefined ? undefined : validateSourceRef(value.targetRef, `${label}.targetRef`);\n\tif (value.confidence !== undefined) assertUnitInterval(value.confidence, `${label}.confidence`);\n\treturn detachedFreeze({\n\t\trelationId: value.relationId,\n\t\tnamespace: value.namespace,\n\t\tschemaVersion: value.schemaVersion,\n\t\tkind: value.kind,\n\t\t...(targetMemoryId === undefined ? {} : { targetMemoryId }),\n\t\t...(targetRef === undefined ? {} : { targetRef }),\n\t\t...(value.confidence === undefined ? {} : { confidence: value.confidence }),\n\t\trelationRevision: value.relationRevision,\n\t});\n}\n\nfunction validateTimeRange(value: unknown, label: string): MemoryTimeRangeV1 {\n\tassertPlainRecord(value, label);\n\t// Key order reversed on purpose: in the natural order the boundary-gate\n\t// lexical scanner reads this key pair as an import specifier (first key\n\t// string, comma, second key string). `assertExactKeys` builds a Set, so the\n\t// order is irrelevant to validation.\n\tassertExactKeys(value, [], [\"to\", \"from\"], label);\n\tconst from = value.from === undefined ? undefined : (value.from as string);\n\tconst to = value.to === undefined ? undefined : (value.to as string);\n\tif (from !== undefined) assertTimestamp(from, `${label}.from`);\n\tif (to !== undefined) assertTimestamp(to, `${label}.to`);\n\treturn detachedFreeze({\n\t\t...(from === undefined ? {} : { from }),\n\t\t...(to === undefined ? {} : { to }),\n\t});\n}\n\nfunction validateApplicability(value: unknown, label: string): MemoryApplicabilityV1 {\n\tassertPlainRecord(value, label);\n\tassertExactKeys(value, [], [\"scenes\", \"projects\", \"tasks\", \"timeRange\", \"exceptions\"], label);\n\tconst timeRange =\n\t\tvalue.timeRange === undefined ? undefined : validateTimeRange(value.timeRange, `${label}.timeRange`);\n\treturn detachedFreeze({\n\t\t...(value.scenes === undefined ? {} : { scenes: Object.freeze([...(value.scenes as readonly string[])]) }),\n\t\t...(value.projects === undefined ? {} : { projects: Object.freeze([...(value.projects as readonly string[])]) }),\n\t\t...(value.tasks === undefined ? {} : { tasks: Object.freeze([...(value.tasks as readonly string[])]) }),\n\t\t...(timeRange === undefined ? {} : { timeRange }),\n\t\t...(value.exceptions === undefined\n\t\t\t? {}\n\t\t\t: { exceptions: Object.freeze([...(value.exceptions as readonly string[])]) }),\n\t});\n}\n\nfunction validatePreferenceEnvelope(value: unknown, label: string): MemoryPreferenceEnvelopeV1 {\n\tassertPlainRecord(value, label);\n\tassertExactKeys(\n\t\tvalue,\n\t\t[\"subject\", \"key\", \"preferredValue\", \"scope\", \"evidence\", \"applicabilityConfidence\"],\n\t\t[\"alternatives\", \"confirmedAt\"],\n\t\tlabel,\n\t);\n\tconst subjects: readonly MemoryPreferenceEnvelopeV1[\"subject\"][] = [\"user\", \"project\", \"task\", \"environment\"];\n\tif (!subjects.includes(value.subject as MemoryPreferenceEnvelopeV1[\"subject\"])) {\n\t\tthrow new Error(`${label}.subject is invalid`);\n\t}\n\tassertNonEmptyString(value.key, `${label}.key`);\n\tassertJsonValue(value.preferredValue, `${label}.preferredValue`);\n\tif (value.alternatives !== undefined) {\n\t\tif (!Array.isArray(value.alternatives)) throw new Error(`${label}.alternatives must be an array`);\n\t\tfor (const [index, item] of value.alternatives.entries()) {\n\t\t\tassertJsonValue(item, `${label}.alternatives[${index}]`);\n\t\t}\n\t}\n\tconst scope = value.scope;\n\tassertPlainRecord(scope, `${label}.scope`);\n\tassertExactKeys(scope, [\"level\"], [\"scenes\", \"projects\", \"tasks\", \"timeRange\", \"exceptions\"], `${label}.scope`);\n\tconst levels: readonly MemoryPreferenceEnvelopeV1[\"scope\"][\"level\"][] = [\n\t\t\"task\",\n\t\t\"project\",\n\t\t\"scene\",\n\t\t\"timeRange\",\n\t\t\"profile-private\",\n\t\t\"user-default\",\n\t];\n\tif (!levels.includes(scope.level as MemoryPreferenceEnvelopeV1[\"scope\"][\"level\"])) {\n\t\tthrow new Error(`${label}.scope.level is invalid`);\n\t}\n\t// Anchor levels require their anchor to be present (记录模型约束).\n\tconst anchors: ReadonlyArray<[string, keyof PlainRecord]> = [\n\t\t[\"task\", \"tasks\"],\n\t\t[\"project\", \"projects\"],\n\t\t[\"scene\", \"scenes\"],\n\t\t[\"timeRange\", \"timeRange\"],\n\t];\n\tfor (const [level, field] of anchors) {\n\t\tif (\n\t\t\tscope.level === level &&\n\t\t\t(scope[field] === undefined || (Array.isArray(scope[field]) && scope[field].length === 0))\n\t\t) {\n\t\t\tthrow new Error(`${label}.scope.level \"${level}\" requires non-empty ${field}`);\n\t\t}\n\t}\n\tconst evidence = value.evidence;\n\tassertPlainRecord(evidence, `${label}.evidence`);\n\tassertExactKeys(evidence, [\"class\", \"sourceRefs\", \"confidence\"], [], `${label}.evidence`);\n\tconst evidenceClasses: readonly MemoryPreferenceEnvelopeV1[\"evidence\"][\"class\"][] = [\n\t\t\"explicit\",\n\t\t\"repeated_behavior\",\n\t\t\"inferred\",\n\t];\n\tif (!evidenceClasses.includes(evidence.class as MemoryPreferenceEnvelopeV1[\"evidence\"][\"class\"])) {\n\t\tthrow new Error(`${label}.evidence.class is invalid`);\n\t}\n\tif (!Array.isArray(evidence.sourceRefs) || evidence.sourceRefs.length === 0) {\n\t\tthrow new Error(`${label}.evidence.sourceRefs must not be empty`);\n\t}\n\tconst evidenceSourceRefs = evidence.sourceRefs.map((item, index) =>\n\t\tvalidateSourceRef(item, `${label}.evidence.sourceRefs[${index}]`),\n\t);\n\tassertUnitInterval(evidence.confidence, `${label}.evidence.confidence`);\n\tassertUnitInterval(value.applicabilityConfidence, `${label}.applicabilityConfidence`);\n\tif (value.confirmedAt !== undefined) assertTimestamp(value.confirmedAt, `${label}.confirmedAt`);\n\treturn detachedFreeze({\n\t\tsubject: value.subject as MemoryPreferenceEnvelopeV1[\"subject\"],\n\t\tkey: value.key,\n\t\tpreferredValue: value.preferredValue,\n\t\t...(value.alternatives === undefined ? {} : { alternatives: Object.freeze([...value.alternatives]) }),\n\t\tscope: detachedFreeze({\n\t\t\tlevel: scope.level as MemoryPreferenceEnvelopeV1[\"scope\"][\"level\"],\n\t\t\t...(scope.scenes === undefined ? {} : { scenes: Object.freeze([...(scope.scenes as readonly string[])]) }),\n\t\t\t...(scope.projects === undefined\n\t\t\t\t? {}\n\t\t\t\t: { projects: Object.freeze([...(scope.projects as readonly string[])]) }),\n\t\t\t...(scope.tasks === undefined ? {} : { tasks: Object.freeze([...(scope.tasks as readonly string[])]) }),\n\t\t\t...(scope.timeRange === undefined\n\t\t\t\t? {}\n\t\t\t\t: { timeRange: validateTimeRange(scope.timeRange, `${label}.scope.timeRange`) }),\n\t\t\t...(scope.exceptions === undefined\n\t\t\t\t? {}\n\t\t\t\t: { exceptions: Object.freeze([...(scope.exceptions as readonly string[])]) }),\n\t\t}),\n\t\tevidence: detachedFreeze({\n\t\t\tclass: evidence.class as MemoryPreferenceEnvelopeV1[\"evidence\"][\"class\"],\n\t\t\tsourceRefs: Object.freeze(evidenceSourceRefs),\n\t\t\tconfidence: evidence.confidence,\n\t\t}),\n\t\tapplicabilityConfidence: value.applicabilityConfidence,\n\t\t...(value.confirmedAt === undefined ? {} : { confirmedAt: value.confirmedAt }),\n\t}) as MemoryPreferenceEnvelopeV1;\n}\n\nfunction validateProvenance(value: unknown, label: string): MemoryProvenanceV1 {\n\tassertPlainRecord(value, label);\n\tassertExactKeys(value, [\"sourceMemoryIds\", \"purgeGroupId\", \"containsSourceContent\"], [], label);\n\tassertStringArray(value.sourceMemoryIds, `${label}.sourceMemoryIds`);\n\tif (value.sourceMemoryIds.length === 0) throw new Error(`${label}.sourceMemoryIds must not be empty`);\n\tassertNonEmptyString(value.purgeGroupId, `${label}.purgeGroupId`);\n\tif (typeof value.containsSourceContent !== \"boolean\") {\n\t\tthrow new Error(`${label}.containsSourceContent must be boolean`);\n\t}\n\treturn detachedFreeze({\n\t\tsourceMemoryIds: Object.freeze([...(value.sourceMemoryIds as readonly string[])]),\n\t\tpurgeGroupId: value.purgeGroupId,\n\t\tcontainsSourceContent: value.containsSourceContent,\n\t});\n}\n\n/**\n * Validates a canonical memory atom and returns a detached, deep-frozen\n * snapshot. Unknown fields, wrong enums, non-JSON payloads, preference-kind\n * atoms without a preference envelope, and identity fields (memoryId,\n * observationId, contentRevision, owner) that are not non-empty strings are\n * rejected.\n */\nexport function validateMemoryAtomV1<TPayload = JsonValue>(value: unknown): MemoryAtomV1<TPayload> {\n\tassertPlainRecord(value, \"Memory atom\");\n\tassertExactKeys(\n\t\tvalue,\n\t\t[\n\t\t\t\"memoryId\",\n\t\t\t\"contractVersion\",\n\t\t\t\"schemaVersion\",\n\t\t\t\"scope\",\n\t\t\t\"retentionMode\",\n\t\t\t\"owner\",\n\t\t\t\"profileId\",\n\t\t\t\"retentionPolicyVersion\",\n\t\t\t\"memoryKind\",\n\t\t\t\"payload\",\n\t\t\t\"occurredAt\",\n\t\t\t\"recordedAt\",\n\t\t\t\"sourceRefs\",\n\t\t\t\"observationId\",\n\t\t\t\"sessionRefs\",\n\t\t\t\"agentInstanceRefs\",\n\t\t\t\"projectRefs\",\n\t\t\t\"subjectRefs\",\n\t\t\t\"facets\",\n\t\t\t\"relations\",\n\t\t\t\"confidence\",\n\t\t\t\"importance\",\n\t\t\t\"evidenceClass\",\n\t\t\t\"contentRevision\",\n\t\t\t\"writeReason\",\n\t\t],\n\t\t[\"tenantId\", \"shareGroupId\", \"suiteId\", \"preference\", \"provenance\", \"applicability\"],\n\t\t\"Memory atom\",\n\t);\n\tassertNonEmptyString(value.memoryId, \"Memory atom.memoryId\");\n\tassertNonEmptyString(value.contractVersion, \"Memory atom.contractVersion\");\n\tassertPositiveSafeInteger(value.schemaVersion, \"Memory atom.schemaVersion\");\n\tif (!MEMORY_SCOPES_V1.includes(value.scope as MemoryScopeV1)) throw new Error(\"Memory atom.scope is invalid\");\n\tassertNonEmptyString(value.retentionMode, \"Memory atom.retentionMode\");\n\tassertNonEmptyString(value.owner, \"Memory atom.owner\");\n\tassertNonEmptyString(value.profileId, \"Memory atom.profileId\");\n\tif (value.shareGroupId !== undefined) assertNonEmptyString(value.shareGroupId, \"Memory atom.shareGroupId\");\n\t// Legacy (pre-M5) atoms may carry protocol-level `null` for \"no suite\"; it\n\t// is tolerated here and normalized to an absent field (存量语义), while a\n\t// present suiteId must be a non-empty string.\n\tif (value.suiteId !== undefined && value.suiteId !== null) {\n\t\tassertNonEmptyString(value.suiteId, \"Memory atom.suiteId\");\n\t}\n\tassertNonEmptyString(value.retentionPolicyVersion, \"Memory atom.retentionPolicyVersion\");\n\tif (!MEMORY_KINDS_V1.includes(value.memoryKind as MemoryKindV1))\n\t\tthrow new Error(\"Memory atom.memoryKind is invalid\");\n\tassertJsonValue(value.payload, \"Memory atom.payload\");\n\tif (value.memoryKind === \"preference\" && value.preference === undefined) {\n\t\tthrow new Error(\"Memory atom.memoryKind preference requires a preference envelope\");\n\t}\n\tconst preference =\n\t\tvalue.preference === undefined\n\t\t\t? undefined\n\t\t\t: validatePreferenceEnvelope(value.preference, \"Memory atom.preference\");\n\tassertTimestamp(value.occurredAt, \"Memory atom.occurredAt\");\n\tassertTimestamp(value.recordedAt, \"Memory atom.recordedAt\");\n\tif (!Array.isArray(value.sourceRefs)) throw new Error(\"Memory atom.sourceRefs must be an array\");\n\tconst sourceRefs = value.sourceRefs.map((item, index) =>\n\t\tvalidateSourceRef(item, `Memory atom.sourceRefs[${index}]`),\n\t);\n\tassertNonEmptyString(value.observationId, \"Memory atom.observationId\");\n\tassertStringArray(value.sessionRefs, \"Memory atom.sessionRefs\");\n\tassertStringArray(value.agentInstanceRefs, \"Memory atom.agentInstanceRefs\");\n\tassertStringArray(value.projectRefs, \"Memory atom.projectRefs\");\n\tassertStringArray(value.subjectRefs, \"Memory atom.subjectRefs\");\n\tif (!Array.isArray(value.facets)) throw new Error(\"Memory atom.facets must be an array\");\n\tconst facets = value.facets.map((item, index) => validateFacet(item, `Memory atom.facets[${index}]`));\n\tif (!Array.isArray(value.relations)) throw new Error(\"Memory atom.relations must be an array\");\n\tconst relations = value.relations.map((item, index) => validateRelation(item, `Memory atom.relations[${index}]`));\n\tconst provenance =\n\t\tvalue.provenance === undefined ? undefined : validateProvenance(value.provenance, \"Memory atom.provenance\");\n\tassertUnitInterval(value.confidence, \"Memory atom.confidence\");\n\tassertUnitInterval(value.importance, \"Memory atom.importance\");\n\tconst applicability =\n\t\tvalue.applicability === undefined\n\t\t\t? undefined\n\t\t\t: validateApplicability(value.applicability, \"Memory atom.applicability\");\n\tif (!MEMORY_EVIDENCE_CLASSES_V1.includes(value.evidenceClass as MemoryEvidenceClassV1)) {\n\t\tthrow new Error(\"Memory atom.evidenceClass is invalid\");\n\t}\n\tassertNonEmptyString(value.contentRevision, \"Memory atom.contentRevision\");\n\tassertNonEmptyString(value.writeReason, \"Memory atom.writeReason\");\n\n\treturn detachedFreeze({\n\t\tmemoryId: value.memoryId,\n\t\tcontractVersion: value.contractVersion,\n\t\tschemaVersion: value.schemaVersion,\n\t\tscope: value.scope as MemoryScopeV1,\n\t\tretentionMode: value.retentionMode,\n\t\towner: value.owner,\n\t\tprofileId: value.profileId,\n\t\t...(value.shareGroupId === undefined ? {} : { shareGroupId: value.shareGroupId }),\n\t\t...(value.suiteId === undefined || value.suiteId === null ? {} : { suiteId: value.suiteId }),\n\t\tretentionPolicyVersion: value.retentionPolicyVersion,\n\t\tmemoryKind: value.memoryKind as MemoryKindV1,\n\t\tpayload: value.payload as TPayload,\n\t\t...(preference === undefined ? {} : { preference }),\n\t\toccurredAt: value.occurredAt,\n\t\trecordedAt: value.recordedAt,\n\t\tsourceRefs: Object.freeze(sourceRefs),\n\t\tobservationId: value.observationId,\n\t\tsessionRefs: Object.freeze([...value.sessionRefs]),\n\t\tagentInstanceRefs: Object.freeze([...value.agentInstanceRefs]),\n\t\tprojectRefs: Object.freeze([...value.projectRefs]),\n\t\tsubjectRefs: Object.freeze([...value.subjectRefs]),\n\t\tfacets: Object.freeze(facets),\n\t\trelations: Object.freeze(relations),\n\t\t...(provenance === undefined ? {} : { provenance }),\n\t\tconfidence: value.confidence,\n\t\timportance: value.importance,\n\t\t...(applicability === undefined ? {} : { applicability }),\n\t\tevidenceClass: value.evidenceClass as MemoryEvidenceClassV1,\n\t\tcontentRevision: value.contentRevision,\n\t\twriteReason: value.writeReason,\n\t}) as MemoryAtomV1<TPayload>;\n}\n\n/** Builds a detached, frozen canonical atom (validate + freeze). */\nexport function buildMemoryAtomV1<TPayload = JsonValue>(atom: MemoryAtomV1<TPayload>): MemoryAtomV1<TPayload> {\n\treturn validateMemoryAtomV1<TPayload>(atom);\n}\n\n/** FNV-1a 64-bit digest over the stable JSON serialization of `value`. */\nexport function memoryContentDigest(value: JsonValue): string {\n\tconst serialized = stableStringify(value);\n\tlet digest = 14695981039346656037n;\n\tfor (const byte of new TextEncoder().encode(serialized)) {\n\t\tdigest ^= BigInt(byte);\n\t\tdigest = BigInt.asUintN(64, digest * 1099511628211n);\n\t}\n\treturn digest.toString(16).padStart(16, \"0\");\n}\n\n/** Idempotent submission identity: the same (owner, observationId) is one observation. */\nexport function memoryObservationKey(owner: string, observationId: string): string {\n\tassertNonEmptyString(owner, \"owner\");\n\tassertNonEmptyString(observationId, \"observationId\");\n\treturn `${owner}\\u0000${observationId}`;\n}\n\n// ---------------------------------------------------------------------------\n// Suite (方案) read boundary (方案系统设计 §6.1/§11, M5)\n// ---------------------------------------------------------------------------\n\n/** Why a record was excluded from a suite-scoped read (读取边界惰性可观测). */\nexport type MemorySuiteVisibilityV1 =\n\t/** The atom matches the queried suite, or is an explicitly promoted user-default preference. */\n\t| \"visible\"\n\t/** Legacy (pre-M5) atom without a suiteId — fail-safe invisible to every suite. */\n\t| \"legacy\"\n\t/** The atom belongs to a different suite. */\n\t| \"foreign-suite\";\n\n/**\n * Skipped-record counters attached to suite-scoped reads. The read boundary is\n * lazy and observable (设计 §11 三段式): excluded atoms are counted, never\n * silently dropped.\n */\nexport interface MemorySuiteFilterStatsV1 {\n\t/** Legacy (suiteId-less) atoms skipped as invisible to any suite. */\n\treadonly legacySkipped: number;\n\t/** Atoms bound to a different suiteId that were skipped. */\n\treadonly foreignSuiteSkipped: number;\n}\n\n/**\n * Suite-scoped visibility of one atom (方案系统设计 §6.1/§11):\n * - `atom.suiteId === suiteId` → visible;\n * - an explicitly promoted user-default preference (scope.level\n * \"user-default\" WITH `confirmedAt` — the canonical explicit-confirmation\n * marker set only by the explicit promotion channel) ignores the suiteId\n * filter and is visible in every suite (§6.1 跨方案偏好);\n * - absent suiteId (legacy/存量) → invisible to every suite (fail-safe\n * default), counted via {@link MemorySuiteFilterStatsV1};\n * - any other suiteId → invisible.\n */\nexport function memorySuiteVisibility(atom: MemoryAtomV1, suiteId: string): MemorySuiteVisibilityV1 {\n\tassertNonEmptyString(suiteId, \"suiteId\");\n\tif (\n\t\tatom.memoryKind === \"preference\" &&\n\t\tatom.preference?.scope.level === \"user-default\" &&\n\t\tatom.preference.confirmedAt !== undefined\n\t) {\n\t\treturn \"visible\";\n\t}\n\tif (atom.suiteId === undefined) return \"legacy\";\n\tif (atom.suiteId === suiteId) return \"visible\";\n\treturn \"foreign-suite\";\n}\n"]}
1
+ {"version":3,"file":"foundation.d.ts","sourceRoot":"","sources":["../../src/memory/foundation.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Memory Foundation — canonical atom, identity, and immutable snapshots (1C.1a).\n *\n * A canonical atom is the immutable committed fact. Derived state (attention,\n * effective status, purgeAt) is NOT part of the atom; it is rebuilt from\n * lifecycle events (1C.1b/1C.2). Everything returned to hosts and plugins is a\n * detached, deep-frozen snapshot: mutating the input after `buildMemoryAtomV1`\n * never changes the stored atom.\n *\n * Field semantics follow `docs/design/记忆系统设计.md` §4 (记录模型), which is the\n * authoritative source for memory fields.\n */\nimport type { JsonValue } from \"@agent-forge/plugin-sdk\";\n\n/** Source of one memory: where the fact came from, optionally pinned by revision/digest. */\nexport interface MemorySourceRefV1 {\n\treadonly kind: \"session\" | \"entry\" | \"artifact\" | \"tool\" | \"state\";\n\treadonly id: string;\n\treadonly revision?: string;\n\treadonly digest?: string;\n}\n\n/** Namespace-scoped search facet. Facets are atom facts, never derived. */\nexport interface MemoryFacetV1 {\n\treadonly namespace: string;\n\treadonly schemaVersion: number;\n\treadonly key: string;\n\treadonly value: string;\n}\n\n/** Typed relation between atoms. Weights/validity belong to projections, not here. */\nexport interface MemoryRelationV1 {\n\treadonly relationId: string;\n\treadonly namespace: string;\n\treadonly schemaVersion: number;\n\treadonly kind: string;\n\treadonly targetMemoryId?: string;\n\treadonly targetRef?: MemorySourceRefV1;\n\treadonly confidence?: number;\n\treadonly relationRevision: string;\n}\n\nexport interface MemoryTimeRangeV1 {\n\treadonly from?: string;\n\treadonly to?: string;\n}\n\n/** Applicability narrowing; empty means \"applies without narrowing\". */\nexport interface MemoryApplicabilityV1 {\n\treadonly scenes?: readonly string[];\n\treadonly projects?: readonly string[];\n\treadonly tasks?: readonly string[];\n\treadonly timeRange?: MemoryTimeRangeV1;\n\treadonly exceptions?: readonly string[];\n}\n\n/** Preference envelope: the generic schema frozen for 1C; policy semantics come later. */\nexport interface MemoryPreferenceEnvelopeV1 {\n\treadonly subject: \"user\" | \"project\" | \"task\" | \"environment\";\n\treadonly key: string;\n\treadonly preferredValue: JsonValue;\n\treadonly alternatives?: readonly JsonValue[];\n\treadonly scope: {\n\t\treadonly level: \"task\" | \"project\" | \"scene\" | \"timeRange\" | \"profile-private\" | \"user-default\";\n\t\treadonly scenes?: readonly string[];\n\t\treadonly projects?: readonly string[];\n\t\treadonly tasks?: readonly string[];\n\t\treadonly timeRange?: MemoryTimeRangeV1;\n\t\treadonly exceptions?: readonly string[];\n\t};\n\treadonly evidence: {\n\t\treadonly class: \"explicit\" | \"repeated_behavior\" | \"inferred\";\n\t\treadonly sourceRefs: readonly MemorySourceRefV1[];\n\t\treadonly confidence: number;\n\t};\n\treadonly applicabilityConfidence: number;\n\treadonly confirmedAt?: string;\n}\n\n/** Provenance for synthesis/compaction memories derived from other atoms. */\nexport interface MemoryProvenanceV1 {\n\treadonly sourceMemoryIds: readonly string[];\n\treadonly purgeGroupId: string;\n\treadonly containsSourceContent: boolean;\n}\n\nexport type MemoryScopeV1 = \"session\" | \"cycle\" | \"long-term\";\nexport type MemoryKindV1 =\n\t| \"observation\"\n\t| \"preference\"\n\t| \"fact\"\n\t| \"decision\"\n\t| \"constraint\"\n\t| \"inference\"\n\t| \"synthesis\";\nexport type MemoryEvidenceClassV1 = \"explicit\" | \"repeated_behavior\" | \"tool_or_test\" | \"derived\";\n\n/** The immutable canonical atom. Once committed, no field may change in place. */\nexport interface MemoryAtomV1<TPayload = JsonValue> {\n\treadonly memoryId: string;\n\treadonly contractVersion: string;\n\treadonly schemaVersion: number;\n\treadonly scope: MemoryScopeV1;\n\treadonly retentionMode: string;\n\treadonly owner: string;\n\treadonly profileId: string;\n\treadonly shareGroupId?: string;\n\t/**\n\t * Suite (方案) this atom belongs to (方案系统设计 §6.1/§11, M5). Absent\n\t * (or protocol-level `null` on the wire, normalized to absent here) marks a\n\t * legacy pre-M5 atom: such atoms are fail-safe INVISIBLE to every\n\t * suite-scoped read and only readable through suite-unscoped queries. The\n\t * one cross-suite exception is an explicitly promoted user-default\n\t * preference (see {@link memorySuiteVisibility}).\n\t */\n\treadonly suiteId?: string;\n\treadonly retentionPolicyVersion: string;\n\treadonly memoryKind: MemoryKindV1;\n\treadonly payload: TPayload;\n\treadonly preference?: MemoryPreferenceEnvelopeV1;\n\treadonly occurredAt: string;\n\treadonly recordedAt: string;\n\treadonly sourceRefs: readonly MemorySourceRefV1[];\n\treadonly observationId: string;\n\treadonly sessionRefs: readonly string[];\n\treadonly agentInstanceRefs: readonly string[];\n\treadonly projectRefs: readonly string[];\n\treadonly subjectRefs: readonly string[];\n\treadonly facets: readonly MemoryFacetV1[];\n\treadonly relations: readonly MemoryRelationV1[];\n\treadonly provenance?: MemoryProvenanceV1;\n\treadonly confidence: number;\n\treadonly importance: number;\n\treadonly applicability?: MemoryApplicabilityV1;\n\treadonly evidenceClass: MemoryEvidenceClassV1;\n\treadonly contentRevision: string;\n\treadonly writeReason: string;\n}\n\ntype PlainRecord = Record<string, unknown>;\n\nfunction isPlainRecord(value: unknown): value is PlainRecord {\n\tif (value === null || typeof value !== \"object\" || Array.isArray(value)) return false;\n\tconst prototype = Object.getPrototypeOf(value);\n\treturn prototype === Object.prototype || prototype === null;\n}\n\nfunction assertPlainRecord(value: unknown, label: string): asserts value is PlainRecord {\n\tif (!isPlainRecord(value)) throw new Error(`${label} must be a plain object`);\n}\n\nfunction assertExactKeys(\n\tvalue: PlainRecord,\n\trequired: readonly string[],\n\toptional: readonly string[],\n\tlabel: string,\n): void {\n\tconst allowed = new Set([...required, ...optional]);\n\tconst unsupported = Object.keys(value)\n\t\t.filter((key) => !allowed.has(key))\n\t\t.sort();\n\tif (unsupported.length > 0) throw new Error(`${label} has unsupported fields: ${unsupported.join(\", \")}`);\n\tfor (const key of required) {\n\t\tif (!(key in value)) throw new Error(`${label} is missing required field: ${key}`);\n\t}\n}\n\nfunction assertNonEmptyString(value: unknown, label: string): asserts value is string {\n\tif (typeof value !== \"string\" || value.trim().length === 0) throw new Error(`${label} must be a non-empty string`);\n}\n\nfunction assertPositiveSafeInteger(value: unknown, label: string): asserts value is number {\n\tif (typeof value !== \"number\" || !Number.isSafeInteger(value) || value < 1) {\n\t\tthrow new Error(`${label} must be a positive safe integer`);\n\t}\n}\n\nfunction assertUnitInterval(value: unknown, label: string): asserts value is number {\n\tif (typeof value !== \"number\" || !Number.isFinite(value) || value < 0 || value > 1) {\n\t\tthrow new Error(`${label} must be a number between 0 and 1`);\n\t}\n}\n\nfunction assertTimestamp(value: unknown, label: string): asserts value is string {\n\tassertNonEmptyString(value, label);\n\tif (Number.isNaN(Date.parse(value))) throw new Error(`${label} must be an ISO-8601 timestamp`);\n}\n\nfunction assertJsonValue(value: unknown, label: string, seen = new WeakSet<object>()): asserts value is JsonValue {\n\tif (value === null || typeof value === \"string\" || typeof value === \"boolean\") return;\n\tif (typeof value === \"number\") {\n\t\tif (Number.isFinite(value)) return;\n\t\tthrow new Error(`${label} must contain finite JSON numbers`);\n\t}\n\tif (typeof value !== \"object\") throw new Error(`${label} must be JSON-compatible`);\n\tif (seen.has(value)) throw new Error(`${label} must not contain cycles`);\n\tseen.add(value);\n\tif (Array.isArray(value)) {\n\t\tfor (const [index, child] of value.entries()) assertJsonValue(child, `${label}[${index}]`, seen);\n\t} else {\n\t\tif (!isPlainRecord(value)) throw new Error(`${label} must contain only plain JSON objects`);\n\t\tfor (const [key, child] of Object.entries(value)) assertJsonValue(child, `${label}.${key}`, seen);\n\t}\n\tseen.delete(value);\n}\n\nfunction assertStringArray(value: unknown, label: string): asserts value is readonly string[] {\n\tif (!Array.isArray(value)) throw new Error(`${label} must be an array`);\n\tfor (const [index, item] of value.entries()) assertNonEmptyString(item, `${label}[${index}]`);\n}\n\nfunction detachedFreeze<T>(value: T, seen = new WeakMap<object, unknown>()): T {\n\tif (value === null || typeof value !== \"object\") return value;\n\tconst existing = seen.get(value);\n\t// v8 ignore next -- 校验后的原子全部由新副本构成,不存在共享引用\n\tif (existing !== undefined) return existing as T;\n\tif (Array.isArray(value)) {\n\t\tconst copy: unknown[] = [];\n\t\tseen.set(value, copy);\n\t\tfor (const child of value) copy.push(detachedFreeze(child, seen));\n\t\treturn Object.freeze(copy) as T;\n\t}\n\tconst copy: Record<string, unknown> = Object.create(null) as Record<string, unknown>;\n\tseen.set(value, copy);\n\tfor (const [key, child] of Object.entries(value as Record<string, unknown>)) {\n\t\tcopy[key] = detachedFreeze(child, seen);\n\t}\n\treturn Object.freeze(copy) as T;\n}\n\n/** Stable JSON serialization with recursively sorted object keys (for identity digests). */\nfunction stableStringify(value: JsonValue): string {\n\tif (value === null || typeof value === \"boolean\" || typeof value === \"number\" || typeof value === \"string\") {\n\t\treturn JSON.stringify(value);\n\t}\n\tif (Array.isArray(value)) return `[${value.map((item) => stableStringify(item)).join(\",\")}]`;\n\tconst entries = Object.keys(value)\n\t\t.sort()\n\t\t.map((key) => `${JSON.stringify(key)}:${stableStringify((value as Record<string, JsonValue>)[key])}`);\n\treturn `{${entries.join(\",\")}}`;\n}\n\nconst MEMORY_SCOPES_V1: readonly MemoryScopeV1[] = [\"session\", \"cycle\", \"long-term\"];\nconst MEMORY_KINDS_V1: readonly MemoryKindV1[] = [\n\t\"observation\",\n\t\"preference\",\n\t\"fact\",\n\t\"decision\",\n\t\"constraint\",\n\t\"inference\",\n\t\"synthesis\",\n];\nconst MEMORY_EVIDENCE_CLASSES_V1: readonly MemoryEvidenceClassV1[] = [\n\t\"explicit\",\n\t\"repeated_behavior\",\n\t\"tool_or_test\",\n\t\"derived\",\n];\nconst SOURCE_REF_KINDS: readonly MemorySourceRefV1[\"kind\"][] = [\"session\", \"entry\", \"artifact\", \"tool\", \"state\"];\n\nfunction validateSourceRef(value: unknown, label: string): MemorySourceRefV1 {\n\tassertPlainRecord(value, label);\n\tassertExactKeys(value, [\"kind\", \"id\"], [\"revision\", \"digest\"], label);\n\tif (!SOURCE_REF_KINDS.includes(value.kind as MemorySourceRefV1[\"kind\"])) {\n\t\tthrow new Error(`${label}.kind is invalid`);\n\t}\n\tassertNonEmptyString(value.id, `${label}.id`);\n\tif (value.revision !== undefined) assertNonEmptyString(value.revision, `${label}.revision`);\n\tif (value.digest !== undefined) assertNonEmptyString(value.digest, `${label}.digest`);\n\treturn detachedFreeze({\n\t\tkind: value.kind as MemorySourceRefV1[\"kind\"],\n\t\tid: value.id,\n\t\t...(value.revision === undefined ? {} : { revision: value.revision }),\n\t\t...(value.digest === undefined ? {} : { digest: value.digest }),\n\t});\n}\n\nfunction validateFacet(value: unknown, label: string): MemoryFacetV1 {\n\tassertPlainRecord(value, label);\n\tassertExactKeys(value, [\"namespace\", \"schemaVersion\", \"key\", \"value\"], [], label);\n\tassertNonEmptyString(value.namespace, `${label}.namespace`);\n\tassertPositiveSafeInteger(value.schemaVersion, `${label}.schemaVersion`);\n\tassertNonEmptyString(value.key, `${label}.key`);\n\tassertNonEmptyString(value.value, `${label}.value`);\n\treturn detachedFreeze({\n\t\tnamespace: value.namespace,\n\t\tschemaVersion: value.schemaVersion,\n\t\tkey: value.key,\n\t\tvalue: value.value,\n\t});\n}\n\nfunction validateRelation(value: unknown, label: string): MemoryRelationV1 {\n\tassertPlainRecord(value, label);\n\tassertExactKeys(\n\t\tvalue,\n\t\t[\"relationId\", \"namespace\", \"schemaVersion\", \"kind\", \"relationRevision\"],\n\t\t[\"targetMemoryId\", \"targetRef\", \"confidence\"],\n\t\tlabel,\n\t);\n\tassertNonEmptyString(value.relationId, `${label}.relationId`);\n\tassertNonEmptyString(value.namespace, `${label}.namespace`);\n\tassertPositiveSafeInteger(value.schemaVersion, `${label}.schemaVersion`);\n\tassertNonEmptyString(value.kind, `${label}.kind`);\n\tassertNonEmptyString(value.relationRevision, `${label}.relationRevision`);\n\tif (value.targetMemoryId !== undefined && value.targetRef !== undefined) {\n\t\tthrow new Error(`${label} must not declare both targetMemoryId and targetRef`);\n\t}\n\tconst targetMemoryId = value.targetMemoryId === undefined ? undefined : (value.targetMemoryId as string);\n\tif (targetMemoryId !== undefined) assertNonEmptyString(targetMemoryId, `${label}.targetMemoryId`);\n\tconst targetRef =\n\t\tvalue.targetRef === undefined ? undefined : validateSourceRef(value.targetRef, `${label}.targetRef`);\n\tif (value.confidence !== undefined) assertUnitInterval(value.confidence, `${label}.confidence`);\n\treturn detachedFreeze({\n\t\trelationId: value.relationId,\n\t\tnamespace: value.namespace,\n\t\tschemaVersion: value.schemaVersion,\n\t\tkind: value.kind,\n\t\t...(targetMemoryId === undefined ? {} : { targetMemoryId }),\n\t\t...(targetRef === undefined ? {} : { targetRef }),\n\t\t...(value.confidence === undefined ? {} : { confidence: value.confidence }),\n\t\trelationRevision: value.relationRevision,\n\t});\n}\n\nfunction validateTimeRange(value: unknown, label: string): MemoryTimeRangeV1 {\n\tassertPlainRecord(value, label);\n\t// Key order reversed on purpose: in the natural order the boundary-gate\n\t// lexical scanner reads this key pair as an import specifier (first key\n\t// string, comma, second key string). `assertExactKeys` builds a Set, so the\n\t// order is irrelevant to validation.\n\tassertExactKeys(value, [], [\"to\", \"from\"], label);\n\tconst from = value.from === undefined ? undefined : (value.from as string);\n\tconst to = value.to === undefined ? undefined : (value.to as string);\n\tif (from !== undefined) assertTimestamp(from, `${label}.from`);\n\tif (to !== undefined) assertTimestamp(to, `${label}.to`);\n\treturn detachedFreeze({\n\t\t...(from === undefined ? {} : { from }),\n\t\t...(to === undefined ? {} : { to }),\n\t});\n}\n\nfunction validateApplicability(value: unknown, label: string): MemoryApplicabilityV1 {\n\tassertPlainRecord(value, label);\n\tassertExactKeys(value, [], [\"scenes\", \"projects\", \"tasks\", \"timeRange\", \"exceptions\"], label);\n\tif (value.scenes !== undefined) assertStringArray(value.scenes, `${label}.scenes`);\n\tif (value.projects !== undefined) assertStringArray(value.projects, `${label}.projects`);\n\tif (value.tasks !== undefined) assertStringArray(value.tasks, `${label}.tasks`);\n\tif (value.exceptions !== undefined) assertStringArray(value.exceptions, `${label}.exceptions`);\n\tconst timeRange =\n\t\tvalue.timeRange === undefined ? undefined : validateTimeRange(value.timeRange, `${label}.timeRange`);\n\treturn detachedFreeze({\n\t\t...(value.scenes === undefined ? {} : { scenes: Object.freeze([...(value.scenes as readonly string[])]) }),\n\t\t...(value.projects === undefined ? {} : { projects: Object.freeze([...(value.projects as readonly string[])]) }),\n\t\t...(value.tasks === undefined ? {} : { tasks: Object.freeze([...(value.tasks as readonly string[])]) }),\n\t\t...(timeRange === undefined ? {} : { timeRange }),\n\t\t...(value.exceptions === undefined\n\t\t\t? {}\n\t\t\t: { exceptions: Object.freeze([...(value.exceptions as readonly string[])]) }),\n\t});\n}\n\nfunction validatePreferenceEnvelope(value: unknown, label: string): MemoryPreferenceEnvelopeV1 {\n\tassertPlainRecord(value, label);\n\tassertExactKeys(\n\t\tvalue,\n\t\t[\"subject\", \"key\", \"preferredValue\", \"scope\", \"evidence\", \"applicabilityConfidence\"],\n\t\t[\"alternatives\", \"confirmedAt\"],\n\t\tlabel,\n\t);\n\tconst subjects: readonly MemoryPreferenceEnvelopeV1[\"subject\"][] = [\"user\", \"project\", \"task\", \"environment\"];\n\tif (!subjects.includes(value.subject as MemoryPreferenceEnvelopeV1[\"subject\"])) {\n\t\tthrow new Error(`${label}.subject is invalid`);\n\t}\n\tassertNonEmptyString(value.key, `${label}.key`);\n\tassertJsonValue(value.preferredValue, `${label}.preferredValue`);\n\tif (value.alternatives !== undefined) {\n\t\tif (!Array.isArray(value.alternatives)) throw new Error(`${label}.alternatives must be an array`);\n\t\tfor (const [index, item] of value.alternatives.entries()) {\n\t\t\tassertJsonValue(item, `${label}.alternatives[${index}]`);\n\t\t}\n\t}\n\tconst scope = value.scope;\n\tassertPlainRecord(scope, `${label}.scope`);\n\tassertExactKeys(scope, [\"level\"], [\"scenes\", \"projects\", \"tasks\", \"timeRange\", \"exceptions\"], `${label}.scope`);\n\tconst levels: readonly MemoryPreferenceEnvelopeV1[\"scope\"][\"level\"][] = [\n\t\t\"task\",\n\t\t\"project\",\n\t\t\"scene\",\n\t\t\"timeRange\",\n\t\t\"profile-private\",\n\t\t\"user-default\",\n\t];\n\tif (!levels.includes(scope.level as MemoryPreferenceEnvelopeV1[\"scope\"][\"level\"])) {\n\t\tthrow new Error(`${label}.scope.level is invalid`);\n\t}\n\t// String-array scope collections face the same rigor as adjacent collection\n\t// fields (sessionRefs, applicability): a bare string must be rejected, not\n\t// silently exploded into a per-character array by the freeze copy.\n\tif (scope.scenes !== undefined) assertStringArray(scope.scenes, `${label}.scope.scenes`);\n\tif (scope.projects !== undefined) assertStringArray(scope.projects, `${label}.scope.projects`);\n\tif (scope.tasks !== undefined) assertStringArray(scope.tasks, `${label}.scope.tasks`);\n\tif (scope.exceptions !== undefined) assertStringArray(scope.exceptions, `${label}.scope.exceptions`);\n\t// Anchor levels require their anchor to be present (记录模型约束).\n\tconst anchors: ReadonlyArray<[string, keyof PlainRecord]> = [\n\t\t[\"task\", \"tasks\"],\n\t\t[\"project\", \"projects\"],\n\t\t[\"scene\", \"scenes\"],\n\t\t[\"timeRange\", \"timeRange\"],\n\t];\n\tfor (const [level, field] of anchors) {\n\t\tif (\n\t\t\tscope.level === level &&\n\t\t\t(scope[field] === undefined || (Array.isArray(scope[field]) && scope[field].length === 0))\n\t\t) {\n\t\t\tthrow new Error(`${label}.scope.level \"${level}\" requires non-empty ${field}`);\n\t\t}\n\t}\n\tconst evidence = value.evidence;\n\tassertPlainRecord(evidence, `${label}.evidence`);\n\tassertExactKeys(evidence, [\"class\", \"sourceRefs\", \"confidence\"], [], `${label}.evidence`);\n\tconst evidenceClasses: readonly MemoryPreferenceEnvelopeV1[\"evidence\"][\"class\"][] = [\n\t\t\"explicit\",\n\t\t\"repeated_behavior\",\n\t\t\"inferred\",\n\t];\n\tif (!evidenceClasses.includes(evidence.class as MemoryPreferenceEnvelopeV1[\"evidence\"][\"class\"])) {\n\t\tthrow new Error(`${label}.evidence.class is invalid`);\n\t}\n\tif (!Array.isArray(evidence.sourceRefs) || evidence.sourceRefs.length === 0) {\n\t\tthrow new Error(`${label}.evidence.sourceRefs must not be empty`);\n\t}\n\tconst evidenceSourceRefs = evidence.sourceRefs.map((item, index) =>\n\t\tvalidateSourceRef(item, `${label}.evidence.sourceRefs[${index}]`),\n\t);\n\tassertUnitInterval(evidence.confidence, `${label}.evidence.confidence`);\n\tassertUnitInterval(value.applicabilityConfidence, `${label}.applicabilityConfidence`);\n\tif (value.confirmedAt !== undefined) assertTimestamp(value.confirmedAt, `${label}.confirmedAt`);\n\treturn detachedFreeze({\n\t\tsubject: value.subject as MemoryPreferenceEnvelopeV1[\"subject\"],\n\t\tkey: value.key,\n\t\tpreferredValue: value.preferredValue,\n\t\t...(value.alternatives === undefined ? {} : { alternatives: Object.freeze([...value.alternatives]) }),\n\t\tscope: detachedFreeze({\n\t\t\tlevel: scope.level as MemoryPreferenceEnvelopeV1[\"scope\"][\"level\"],\n\t\t\t...(scope.scenes === undefined ? {} : { scenes: Object.freeze([...(scope.scenes as readonly string[])]) }),\n\t\t\t...(scope.projects === undefined\n\t\t\t\t? {}\n\t\t\t\t: { projects: Object.freeze([...(scope.projects as readonly string[])]) }),\n\t\t\t...(scope.tasks === undefined ? {} : { tasks: Object.freeze([...(scope.tasks as readonly string[])]) }),\n\t\t\t...(scope.timeRange === undefined\n\t\t\t\t? {}\n\t\t\t\t: { timeRange: validateTimeRange(scope.timeRange, `${label}.scope.timeRange`) }),\n\t\t\t...(scope.exceptions === undefined\n\t\t\t\t? {}\n\t\t\t\t: { exceptions: Object.freeze([...(scope.exceptions as readonly string[])]) }),\n\t\t}),\n\t\tevidence: detachedFreeze({\n\t\t\tclass: evidence.class as MemoryPreferenceEnvelopeV1[\"evidence\"][\"class\"],\n\t\t\tsourceRefs: Object.freeze(evidenceSourceRefs),\n\t\t\tconfidence: evidence.confidence,\n\t\t}),\n\t\tapplicabilityConfidence: value.applicabilityConfidence,\n\t\t...(value.confirmedAt === undefined ? {} : { confirmedAt: value.confirmedAt }),\n\t}) as MemoryPreferenceEnvelopeV1;\n}\n\nfunction validateProvenance(value: unknown, label: string): MemoryProvenanceV1 {\n\tassertPlainRecord(value, label);\n\tassertExactKeys(value, [\"sourceMemoryIds\", \"purgeGroupId\", \"containsSourceContent\"], [], label);\n\tassertStringArray(value.sourceMemoryIds, `${label}.sourceMemoryIds`);\n\tif (value.sourceMemoryIds.length === 0) throw new Error(`${label}.sourceMemoryIds must not be empty`);\n\tassertNonEmptyString(value.purgeGroupId, `${label}.purgeGroupId`);\n\tif (typeof value.containsSourceContent !== \"boolean\") {\n\t\tthrow new Error(`${label}.containsSourceContent must be boolean`);\n\t}\n\treturn detachedFreeze({\n\t\tsourceMemoryIds: Object.freeze([...(value.sourceMemoryIds as readonly string[])]),\n\t\tpurgeGroupId: value.purgeGroupId,\n\t\tcontainsSourceContent: value.containsSourceContent,\n\t});\n}\n\n/**\n * Validates a canonical memory atom and returns a detached, deep-frozen\n * snapshot. Unknown fields, wrong enums, non-JSON payloads, preference-kind\n * atoms without a preference envelope, and identity fields (memoryId,\n * observationId, contentRevision, owner) that are not non-empty strings are\n * rejected.\n */\nexport function validateMemoryAtomV1<TPayload = JsonValue>(value: unknown): MemoryAtomV1<TPayload> {\n\tassertPlainRecord(value, \"Memory atom\");\n\tassertExactKeys(\n\t\tvalue,\n\t\t[\n\t\t\t\"memoryId\",\n\t\t\t\"contractVersion\",\n\t\t\t\"schemaVersion\",\n\t\t\t\"scope\",\n\t\t\t\"retentionMode\",\n\t\t\t\"owner\",\n\t\t\t\"profileId\",\n\t\t\t\"retentionPolicyVersion\",\n\t\t\t\"memoryKind\",\n\t\t\t\"payload\",\n\t\t\t\"occurredAt\",\n\t\t\t\"recordedAt\",\n\t\t\t\"sourceRefs\",\n\t\t\t\"observationId\",\n\t\t\t\"sessionRefs\",\n\t\t\t\"agentInstanceRefs\",\n\t\t\t\"projectRefs\",\n\t\t\t\"subjectRefs\",\n\t\t\t\"facets\",\n\t\t\t\"relations\",\n\t\t\t\"confidence\",\n\t\t\t\"importance\",\n\t\t\t\"evidenceClass\",\n\t\t\t\"contentRevision\",\n\t\t\t\"writeReason\",\n\t\t],\n\t\t[\"tenantId\", \"shareGroupId\", \"suiteId\", \"preference\", \"provenance\", \"applicability\"],\n\t\t\"Memory atom\",\n\t);\n\tassertNonEmptyString(value.memoryId, \"Memory atom.memoryId\");\n\tassertNonEmptyString(value.contractVersion, \"Memory atom.contractVersion\");\n\tassertPositiveSafeInteger(value.schemaVersion, \"Memory atom.schemaVersion\");\n\tif (!MEMORY_SCOPES_V1.includes(value.scope as MemoryScopeV1)) throw new Error(\"Memory atom.scope is invalid\");\n\tassertNonEmptyString(value.retentionMode, \"Memory atom.retentionMode\");\n\tassertNonEmptyString(value.owner, \"Memory atom.owner\");\n\tassertNonEmptyString(value.profileId, \"Memory atom.profileId\");\n\tif (value.shareGroupId !== undefined) assertNonEmptyString(value.shareGroupId, \"Memory atom.shareGroupId\");\n\t// Legacy (pre-M5) atoms may carry protocol-level `null` for \"no suite\"; it\n\t// is tolerated here and normalized to an absent field (存量语义), while a\n\t// present suiteId must be a non-empty string.\n\tif (value.suiteId !== undefined && value.suiteId !== null) {\n\t\tassertNonEmptyString(value.suiteId, \"Memory atom.suiteId\");\n\t}\n\tassertNonEmptyString(value.retentionPolicyVersion, \"Memory atom.retentionPolicyVersion\");\n\tif (!MEMORY_KINDS_V1.includes(value.memoryKind as MemoryKindV1))\n\t\tthrow new Error(\"Memory atom.memoryKind is invalid\");\n\tassertJsonValue(value.payload, \"Memory atom.payload\");\n\tif (value.memoryKind === \"preference\" && value.preference === undefined) {\n\t\tthrow new Error(\"Memory atom.memoryKind preference requires a preference envelope\");\n\t}\n\tconst preference =\n\t\tvalue.preference === undefined\n\t\t\t? undefined\n\t\t\t: validatePreferenceEnvelope(value.preference, \"Memory atom.preference\");\n\tassertTimestamp(value.occurredAt, \"Memory atom.occurredAt\");\n\tassertTimestamp(value.recordedAt, \"Memory atom.recordedAt\");\n\tif (!Array.isArray(value.sourceRefs)) throw new Error(\"Memory atom.sourceRefs must be an array\");\n\tconst sourceRefs = value.sourceRefs.map((item, index) =>\n\t\tvalidateSourceRef(item, `Memory atom.sourceRefs[${index}]`),\n\t);\n\tassertNonEmptyString(value.observationId, \"Memory atom.observationId\");\n\tassertStringArray(value.sessionRefs, \"Memory atom.sessionRefs\");\n\tassertStringArray(value.agentInstanceRefs, \"Memory atom.agentInstanceRefs\");\n\tassertStringArray(value.projectRefs, \"Memory atom.projectRefs\");\n\tassertStringArray(value.subjectRefs, \"Memory atom.subjectRefs\");\n\tif (!Array.isArray(value.facets)) throw new Error(\"Memory atom.facets must be an array\");\n\tconst facets = value.facets.map((item, index) => validateFacet(item, `Memory atom.facets[${index}]`));\n\tif (!Array.isArray(value.relations)) throw new Error(\"Memory atom.relations must be an array\");\n\tconst relations = value.relations.map((item, index) => validateRelation(item, `Memory atom.relations[${index}]`));\n\tconst provenance =\n\t\tvalue.provenance === undefined ? undefined : validateProvenance(value.provenance, \"Memory atom.provenance\");\n\tassertUnitInterval(value.confidence, \"Memory atom.confidence\");\n\tassertUnitInterval(value.importance, \"Memory atom.importance\");\n\tconst applicability =\n\t\tvalue.applicability === undefined\n\t\t\t? undefined\n\t\t\t: validateApplicability(value.applicability, \"Memory atom.applicability\");\n\tif (!MEMORY_EVIDENCE_CLASSES_V1.includes(value.evidenceClass as MemoryEvidenceClassV1)) {\n\t\tthrow new Error(\"Memory atom.evidenceClass is invalid\");\n\t}\n\tassertNonEmptyString(value.contentRevision, \"Memory atom.contentRevision\");\n\tassertNonEmptyString(value.writeReason, \"Memory atom.writeReason\");\n\n\treturn detachedFreeze({\n\t\tmemoryId: value.memoryId,\n\t\tcontractVersion: value.contractVersion,\n\t\tschemaVersion: value.schemaVersion,\n\t\tscope: value.scope as MemoryScopeV1,\n\t\tretentionMode: value.retentionMode,\n\t\towner: value.owner,\n\t\tprofileId: value.profileId,\n\t\t...(value.shareGroupId === undefined ? {} : { shareGroupId: value.shareGroupId }),\n\t\t...(value.suiteId === undefined || value.suiteId === null ? {} : { suiteId: value.suiteId }),\n\t\tretentionPolicyVersion: value.retentionPolicyVersion,\n\t\tmemoryKind: value.memoryKind as MemoryKindV1,\n\t\tpayload: value.payload as TPayload,\n\t\t...(preference === undefined ? {} : { preference }),\n\t\toccurredAt: value.occurredAt,\n\t\trecordedAt: value.recordedAt,\n\t\tsourceRefs: Object.freeze(sourceRefs),\n\t\tobservationId: value.observationId,\n\t\tsessionRefs: Object.freeze([...value.sessionRefs]),\n\t\tagentInstanceRefs: Object.freeze([...value.agentInstanceRefs]),\n\t\tprojectRefs: Object.freeze([...value.projectRefs]),\n\t\tsubjectRefs: Object.freeze([...value.subjectRefs]),\n\t\tfacets: Object.freeze(facets),\n\t\trelations: Object.freeze(relations),\n\t\t...(provenance === undefined ? {} : { provenance }),\n\t\tconfidence: value.confidence,\n\t\timportance: value.importance,\n\t\t...(applicability === undefined ? {} : { applicability }),\n\t\tevidenceClass: value.evidenceClass as MemoryEvidenceClassV1,\n\t\tcontentRevision: value.contentRevision,\n\t\twriteReason: value.writeReason,\n\t}) as MemoryAtomV1<TPayload>;\n}\n\n/** Builds a detached, frozen canonical atom (validate + freeze). */\nexport function buildMemoryAtomV1<TPayload = JsonValue>(atom: MemoryAtomV1<TPayload>): MemoryAtomV1<TPayload> {\n\treturn validateMemoryAtomV1<TPayload>(atom);\n}\n\n/** FNV-1a 64-bit digest over the stable JSON serialization of `value`. */\nexport function memoryContentDigest(value: JsonValue): string {\n\tconst serialized = stableStringify(value);\n\tlet digest = 14695981039346656037n;\n\tfor (const byte of new TextEncoder().encode(serialized)) {\n\t\tdigest ^= BigInt(byte);\n\t\tdigest = BigInt.asUintN(64, digest * 1099511628211n);\n\t}\n\treturn digest.toString(16).padStart(16, \"0\");\n}\n\n/** Idempotent submission identity: the same (owner, observationId) is one observation. */\nexport function memoryObservationKey(owner: string, observationId: string): string {\n\tassertNonEmptyString(owner, \"owner\");\n\tassertNonEmptyString(observationId, \"observationId\");\n\treturn `${owner}\\u0000${observationId}`;\n}\n\n// ---------------------------------------------------------------------------\n// Suite (方案) read boundary (方案系统设计 §6.1/§11, M5)\n// ---------------------------------------------------------------------------\n\n/** Why a record was excluded from a suite-scoped read (读取边界惰性可观测). */\nexport type MemorySuiteVisibilityV1 =\n\t/** The atom matches the queried suite, or is an explicitly promoted user-default preference. */\n\t| \"visible\"\n\t/** Legacy (pre-M5) atom without a suiteId — fail-safe invisible to every suite. */\n\t| \"legacy\"\n\t/** The atom belongs to a different suite. */\n\t| \"foreign-suite\";\n\n/**\n * Skipped-record counters attached to suite-scoped reads. The read boundary is\n * lazy and observable (设计 §11 三段式): excluded atoms are counted, never\n * silently dropped.\n */\nexport interface MemorySuiteFilterStatsV1 {\n\t/** Legacy (suiteId-less) atoms skipped as invisible to any suite. */\n\treadonly legacySkipped: number;\n\t/** Atoms bound to a different suiteId that were skipped. */\n\treadonly foreignSuiteSkipped: number;\n}\n\n/**\n * Suite-scoped visibility of one atom (方案系统设计 §6.1/§11):\n * - `atom.suiteId === suiteId` → visible;\n * - an explicitly promoted user-default preference (scope.level\n * \"user-default\" WITH `confirmedAt` — the canonical explicit-confirmation\n * marker set only by the explicit promotion channel) ignores the suiteId\n * filter and is visible in every suite (§6.1 跨方案偏好);\n * - absent suiteId (legacy/存量) → invisible to every suite (fail-safe\n * default), counted via {@link MemorySuiteFilterStatsV1};\n * - any other suiteId → invisible.\n */\nexport function memorySuiteVisibility(atom: MemoryAtomV1, suiteId: string): MemorySuiteVisibilityV1 {\n\tassertNonEmptyString(suiteId, \"suiteId\");\n\tif (\n\t\tatom.memoryKind === \"preference\" &&\n\t\tatom.preference?.scope.level === \"user-default\" &&\n\t\tatom.preference.confirmedAt !== undefined\n\t) {\n\t\treturn \"visible\";\n\t}\n\tif (atom.suiteId === undefined) return \"legacy\";\n\tif (atom.suiteId === suiteId) return \"visible\";\n\treturn \"foreign-suite\";\n}\n"]}
@@ -201,6 +201,14 @@ function validateTimeRange(value, label) {
201
201
  function validateApplicability(value, label) {
202
202
  assertPlainRecord(value, label);
203
203
  assertExactKeys(value, [], ["scenes", "projects", "tasks", "timeRange", "exceptions"], label);
204
+ if (value.scenes !== undefined)
205
+ assertStringArray(value.scenes, `${label}.scenes`);
206
+ if (value.projects !== undefined)
207
+ assertStringArray(value.projects, `${label}.projects`);
208
+ if (value.tasks !== undefined)
209
+ assertStringArray(value.tasks, `${label}.tasks`);
210
+ if (value.exceptions !== undefined)
211
+ assertStringArray(value.exceptions, `${label}.exceptions`);
204
212
  const timeRange = value.timeRange === undefined ? undefined : validateTimeRange(value.timeRange, `${label}.timeRange`);
205
213
  return detachedFreeze({
206
214
  ...(value.scenes === undefined ? {} : { scenes: Object.freeze([...value.scenes]) }),
@@ -242,6 +250,17 @@ function validatePreferenceEnvelope(value, label) {
242
250
  if (!levels.includes(scope.level)) {
243
251
  throw new Error(`${label}.scope.level is invalid`);
244
252
  }
253
+ // String-array scope collections face the same rigor as adjacent collection
254
+ // fields (sessionRefs, applicability): a bare string must be rejected, not
255
+ // silently exploded into a per-character array by the freeze copy.
256
+ if (scope.scenes !== undefined)
257
+ assertStringArray(scope.scenes, `${label}.scope.scenes`);
258
+ if (scope.projects !== undefined)
259
+ assertStringArray(scope.projects, `${label}.scope.projects`);
260
+ if (scope.tasks !== undefined)
261
+ assertStringArray(scope.tasks, `${label}.scope.tasks`);
262
+ if (scope.exceptions !== undefined)
263
+ assertStringArray(scope.exceptions, `${label}.scope.exceptions`);
245
264
  // Anchor levels require their anchor to be present (记录模型约束).
246
265
  const anchors = [
247
266
  ["task", "tasks"],