@sema-agent/server 7.12.0 → 7.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/USAGE.md +51 -2
  2. package/dist/adoption/plan.d.ts +38 -4
  3. package/dist/adoption/plan.js +72 -0
  4. package/dist/adoption/quiesce.d.ts +70 -0
  5. package/dist/adoption/quiesce.js +148 -0
  6. package/dist/adoption/runner.js +63 -5
  7. package/dist/adoption/sql.d.ts +15 -0
  8. package/dist/adoption/sql.js +18 -0
  9. package/dist/adoption/wire.d.ts +7 -1
  10. package/dist/adoption/wire.js +6 -0
  11. package/dist/approval-card.d.ts +5 -0
  12. package/dist/approval-card.js +22 -0
  13. package/dist/boot/coordinators.js +2 -1
  14. package/dist/boot/memory-boundary.d.ts +84 -0
  15. package/dist/boot/memory-boundary.js +110 -0
  16. package/dist/boot/permission-rules-audit.js +29 -1
  17. package/dist/boot/reapers.d.ts +15 -0
  18. package/dist/boot/reapers.js +101 -44
  19. package/dist/boot/resolve-spec.js +31 -0
  20. package/dist/boot/runner-deps.d.ts +16 -2
  21. package/dist/boot/runner-deps.js +24 -4
  22. package/dist/boot/stores.js +93 -3
  23. package/dist/capabilities/memory-notice.d.ts +83 -0
  24. package/dist/capabilities/memory-notice.js +90 -0
  25. package/dist/config-types.d.ts +100 -11
  26. package/dist/config.d.ts +1 -1
  27. package/dist/config.js +111 -1
  28. package/dist/governance-ask-marks.js +2 -1
  29. package/dist/http/active-run-conflict.d.ts +33 -8
  30. package/dist/http/active-run-conflict.js +37 -2
  31. package/dist/http/routes/adoption.js +25 -2
  32. package/dist/http/routes/approvals-assistant.js +33 -3
  33. package/dist/http/routes/capabilities.js +35 -5
  34. package/dist/http/routes/images.js +18 -0
  35. package/dist/http/routes/runs.js +21 -5
  36. package/dist/http/routes/tasks.js +18 -6
  37. package/dist/http/routes/trace-usage.js +43 -14
  38. package/dist/http/server.d.ts +35 -9
  39. package/dist/http/server.js +111 -17
  40. package/dist/http/wire-types.d.ts +6 -1
  41. package/dist/main.js +46 -6
  42. package/dist/observability/fail-open.d.ts +4 -0
  43. package/dist/observability/fail-open.js +4 -0
  44. package/dist/plugins/adoption-log-sql.d.ts +40 -0
  45. package/dist/plugins/adoption-log-sql.js +69 -2
  46. package/dist/plugins/approval-ask-store-sql.d.ts +2 -1
  47. package/dist/plugins/approval-ask-store-sql.js +2 -1
  48. package/dist/plugins/file-run-store.d.ts +85 -1
  49. package/dist/plugins/file-run-store.js +450 -17
  50. package/dist/plugins/memory-embedder.d.ts +44 -0
  51. package/dist/plugins/memory-embedder.js +173 -0
  52. package/dist/plugins/permission-rule-store-sql.d.ts +45 -0
  53. package/dist/plugins/permission-rule-store-sql.js +60 -2
  54. package/dist/plugins/shared-memory-store-sql.d.ts +23 -9
  55. package/dist/plugins/shared-memory-store-sql.js +55 -18
  56. package/dist/plugins/sql-driver.d.ts +19 -0
  57. package/dist/plugins/sql-driver.js +12 -0
  58. package/dist/plugins/store-backend.d.ts +3 -1
  59. package/dist/plugins/store-backend.js +24 -1
  60. package/dist/plugins/tidb-pool.js +11 -4
  61. package/dist/plugins/tool-result-store-sql.d.ts +35 -2
  62. package/dist/plugins/tool-result-store-sql.js +127 -11
  63. package/dist/plugins/web-search.d.ts +3 -1
  64. package/dist/plugins/web-search.js +3 -1
  65. package/dist/rules-consent.d.ts +33 -4
  66. package/dist/rules-consent.js +43 -2
  67. package/dist/run-local.js +6 -2
  68. package/dist/runtime-governance.d.ts +33 -0
  69. package/dist/runtime-governance.js +32 -0
  70. package/dist/security.js +3 -1
  71. package/dist/tool-approval.d.ts +32 -0
  72. package/dist/tool-approval.js +39 -0
  73. package/dist/trace/core-keyset-guard.d.ts +1 -1
  74. package/package.json +3 -3
@@ -0,0 +1,83 @@
1
+ /**
2
+ * #217(板 [3499]②/[3521]④ 两次点名的 server 接线半场)—— 零记忆面部署形的**持久记忆披露**。
3
+ *
4
+ * 病(core [3479] 实测,#148 立案):一台没有任何持久记忆面的部署,用户说「请记住 X」,模型回一句
5
+ * 「已经记住了」——落盘处并不存在。用户带着「这条事实已入库」的信念离开,下一次会话才发现是空的。
6
+ *
7
+ * core 5.26.0 的分工:引擎**能自证**只读的两态(engine 侧 `writeScope:null` / runner 侧 `handsReadOnly`)
8
+ * 由引擎自动挂 `MEMORY_READONLY_NOTICE`;而「整台部署根本没有记忆面」这一形 core 只导出文本
9
+ * (`NO_PERSISTENT_MEMORY_NOTICE`)、**不注入** —— 逐字原文:「A deployment that composes its own prompt
10
+ * should include this block exactly when no memory face is mounted」。组装归宿主,与 `MEMORY_SAFETY`
11
+ * 同姿势。本模块就是那半场。
12
+ *
13
+ * ⚠️ 两条边界,越界即双重注入/假话:
14
+ * · **engine 在场就不是我们的座位**。哪怕 `memoryWrite:false`(writeScope null)——那恰恰是引擎自己
15
+ * 挂只读披露的状态,server 再叠一句口径不同的「你没有持久记忆」= 同一件事两种说法同框。
16
+ * · **推断到「有落盘路」就闭嘴**。判别式与 core 的 `rosterCanPersist` 同口径(已知落盘路 = 挂载且未
17
+ * 排除的文件写工具 / 可写 shell)。`excludeTools:["Write"]` **不算**无写通道(Bash/Edit 仍可写)—— 本
18
+ * 判别式压根不读工具名单,读的是**手带表态**(`hands`/`handsReadOnly`),所以这条负控是构造性成立的:
19
+ * 请求面摘一个工具名改不动部署级的手带事实。
20
+ */
21
+ import { type PromptProvider, type TaskSpec } from "@sema-agent/core";
22
+ import type { ScenarioHands } from "./hands-lane.js";
23
+ /**
24
+ * 「已知落盘路」的工具名闭集 —— 与 core 的 `rosterCanPersist` 同口径:**文件写工具**(Write/Edit/
25
+ * NotebookEdit)或**可写 shell**(Bash)。刻意**不是** `HAND_TOOL_EFFECTS` 里 effect=write 的全集:
26
+ * core 逐字裁过「a generic write-effect tool does NOT count — a mail sender's side effect is not a
27
+ * memory store」,`TaskStop` 就是那种(有写副作用、不是落盘路)。
28
+ *
29
+ * 名字会漂,所以配一道防漂移钉(test/no-persistent-memory-notice.test.ts):每个名字必须在 core 的
30
+ * `HAND_TOOL_EFFECTS` 里且 effect 为 `write` —— core 改名/改档位当场红,而不是让这里静默失配成
31
+ * 「没有一个写工具被排除」(fail-open 方向)。
32
+ */
33
+ export declare const KNOWN_PERSISTENCE_TOOL_NAMES: readonly ["Write", "Edit", "NotebookEdit", "Bash"];
34
+ /** 披露段在 `prompt.assembled` 清单里的段 id(`core/` 命名空间是 core 保留的,部署段一律自家前缀)。 */
35
+ export declare const NO_PERSISTENT_MEMORY_SECTION_ID = "sema/memory.no-persistent";
36
+ /** 判别式的全部输入 —— 每一位都是**部署/装配期事实**,没有一位来自请求体的自我表态。 */
37
+ export interface MemoryFaceFacts {
38
+ /** 本次任务最终的 `TaskSpec.memory`。`undefined` = 这一腿根本没有引擎记忆面(引擎未接、多租户 dark、
39
+ * remote lane 平面分裂 fail-closed……三条路殊途同归:core 不会 prepare 记忆,也就不会挂任何披露)。 */
40
+ memory: TaskSpec["memory"];
41
+ /** 部署声明 `MEMORY_PERSISTENCE_CAPABLE`(三态)。`true` = operator 自证有推断看不见的持久通道
42
+ * (自定义 writer / MCP 记忆服务)⇒ 披露会变成假话,撤回;`false` = 强制披露;缺席 ⇒ 走推断。 */
43
+ declaredCapable: boolean | undefined;
44
+ /** 本请求命中场景的手带表态(#196 判别位)。`none` = 该场景跑在不挂 `executionEnvFactory` 的 Runner 上,
45
+ * core 的手带 band(Bash/Edit/Write/…)整批不 mount。 */
46
+ hands: ScenarioHands;
47
+ /** 折完 governance+settings 后的最终 `handsReadOnly`(plan 模式 / autonomy 收窄)。手带只剩读腿。 */
48
+ handsReadOnly: boolean;
49
+ /** 折完之后的最终 `TaskSpec.excludeTools`(roster 真卸载,不是延迟披露)。**全部**已知落盘路都被排掉
50
+ * ⇒ 手带在场也等于没有写通道 —— core 的 `rosterCanPersist` 也是按排除后的 roster 算的,这里跟上。 */
51
+ excludeTools: readonly string[] | undefined;
52
+ /** 本部署的执行环境在 core 眼里算不算 **remote**(`isRemoteExecutionEnv`)。判别位 = `REMOTE_EXEC` 有没有
53
+ * 设 —— 注意 `host` 也算:core 的判别是**鸭子类型**(`workspaceHandle`/`execStream`/`suspendVM`/…),
54
+ * 而我方的 `remote-env-host.ts` 这几件全实现,亲验坐实。
55
+ * 为什么这一位必须进判别式(codex 复审 round2 [high],已核真):core 的 `rosterCanPersist` 在
56
+ * `handsEnabled && isRemoteExecutionEnv` 时把 Write/Edit/NotebookEdit/Bash **全部不算**落盘路(沙箱盘
57
+ * 每任务即焚,够不着 host 记忆根)。而 server 在任何 remote 车道上默认让记忆引擎 dark —— 两件事合起来
58
+ * 正是**最常见的云部署形**:引擎不在、手带在,却一份持久面都没有。漏了这一位,那台机器就继续给
59
+ * 「已经记住了」的幻觉式回执 —— 恰恰是 #148 立案要消灭的形态。 */
60
+ remoteExecutionEnv: boolean;
61
+ }
62
+ /**
63
+ * 该不该把 `NO_PERSISTENT_MEMORY_NOTICE` 组进本次任务的 system prompt。
64
+ *
65
+ * 优先序(每一条都有独立理由,不是同一条判据的三种写法):
66
+ * ① engine 在场 ⇒ **否**(引擎的座位,见文件头注);
67
+ * ② 部署显式声明 ⇒ 声明说了算(`true` 撤回 / `false` 强制)—— operator 的部署事实压过我们的推断,
68
+ * 与 core 对 `memoryPersistenceCapable` 的处置同向;
69
+ * ③ 否则推断:有已知落盘路(手带在场且不是只读)⇒ 否;没有 ⇒ 是。
70
+ */
71
+ export declare function shouldDiscloseNoPersistentMemory(facts: MemoryFaceFacts): boolean;
72
+ /**
73
+ * 把披露段组进一个 prompt provider,**按 provider 自己的钩子形分腿**(换形会改段身份与 append 座位):
74
+ * · typed(`stableBlocks`)⇒ 追加一条 `behavior` 声明。**不能**用 `identity`:那个 slot 会替换 role base。
75
+ * · string(`stableSystem`)⇒ 接在 role 层文本之后(落进 `core/role.base` 段)。
76
+ * · 两个钩子都没有(= core 会用 `defaultPromptProvider`)⇒ **自铸 role 层**。这里绝不能去包
77
+ * `defaultPromptProvider.stableSystem` —— 它返回的是 `composeConstitution(...)` 的**整装** prompt,
78
+ * 装配器的迁移卫探到三锚(CYBER_RISK/URL_SAFETY/`# Harness`)就走 pass-through 臂,那条臂的段子集
79
+ * 没有 `core/role.append`,调用方的 append rider 会被静默丢掉。自铸的是 core 自己那一句
80
+ * `userSystemPrompt ?? DEFAULT_SYSTEM_PROMPT`(引用常量,不复制字节)。
81
+ */
82
+ export declare function withNoPersistentMemoryNotice(provider: PromptProvider | undefined): PromptProvider;
83
+ //# sourceMappingURL=memory-notice.d.ts.map
@@ -0,0 +1,90 @@
1
+ /**
2
+ * #217(板 [3499]②/[3521]④ 两次点名的 server 接线半场)—— 零记忆面部署形的**持久记忆披露**。
3
+ *
4
+ * 病(core [3479] 实测,#148 立案):一台没有任何持久记忆面的部署,用户说「请记住 X」,模型回一句
5
+ * 「已经记住了」——落盘处并不存在。用户带着「这条事实已入库」的信念离开,下一次会话才发现是空的。
6
+ *
7
+ * core 5.26.0 的分工:引擎**能自证**只读的两态(engine 侧 `writeScope:null` / runner 侧 `handsReadOnly`)
8
+ * 由引擎自动挂 `MEMORY_READONLY_NOTICE`;而「整台部署根本没有记忆面」这一形 core 只导出文本
9
+ * (`NO_PERSISTENT_MEMORY_NOTICE`)、**不注入** —— 逐字原文:「A deployment that composes its own prompt
10
+ * should include this block exactly when no memory face is mounted」。组装归宿主,与 `MEMORY_SAFETY`
11
+ * 同姿势。本模块就是那半场。
12
+ *
13
+ * ⚠️ 两条边界,越界即双重注入/假话:
14
+ * · **engine 在场就不是我们的座位**。哪怕 `memoryWrite:false`(writeScope null)——那恰恰是引擎自己
15
+ * 挂只读披露的状态,server 再叠一句口径不同的「你没有持久记忆」= 同一件事两种说法同框。
16
+ * · **推断到「有落盘路」就闭嘴**。判别式与 core 的 `rosterCanPersist` 同口径(已知落盘路 = 挂载且未
17
+ * 排除的文件写工具 / 可写 shell)。`excludeTools:["Write"]` **不算**无写通道(Bash/Edit 仍可写)—— 本
18
+ * 判别式压根不读工具名单,读的是**手带表态**(`hands`/`handsReadOnly`),所以这条负控是构造性成立的:
19
+ * 请求面摘一个工具名改不动部署级的手带事实。
20
+ */
21
+ import { DEFAULT_SYSTEM_PROMPT, NO_PERSISTENT_MEMORY_NOTICE } from "@sema-agent/core";
22
+ /**
23
+ * 「已知落盘路」的工具名闭集 —— 与 core 的 `rosterCanPersist` 同口径:**文件写工具**(Write/Edit/
24
+ * NotebookEdit)或**可写 shell**(Bash)。刻意**不是** `HAND_TOOL_EFFECTS` 里 effect=write 的全集:
25
+ * core 逐字裁过「a generic write-effect tool does NOT count — a mail sender's side effect is not a
26
+ * memory store」,`TaskStop` 就是那种(有写副作用、不是落盘路)。
27
+ *
28
+ * 名字会漂,所以配一道防漂移钉(test/no-persistent-memory-notice.test.ts):每个名字必须在 core 的
29
+ * `HAND_TOOL_EFFECTS` 里且 effect 为 `write` —— core 改名/改档位当场红,而不是让这里静默失配成
30
+ * 「没有一个写工具被排除」(fail-open 方向)。
31
+ */
32
+ export const KNOWN_PERSISTENCE_TOOL_NAMES = ["Write", "Edit", "NotebookEdit", "Bash"];
33
+ /** 披露段在 `prompt.assembled` 清单里的段 id(`core/` 命名空间是 core 保留的,部署段一律自家前缀)。 */
34
+ export const NO_PERSISTENT_MEMORY_SECTION_ID = "sema/memory.no-persistent";
35
+ /** 排除后还剩不剩已知落盘路。空/缺席的排除名单 ⇒ 一定还剩(最常见的路径,提前短路)。 */
36
+ function anyPersistenceToolSurvives(excludeTools) {
37
+ if (excludeTools === undefined || excludeTools.length === 0)
38
+ return true;
39
+ const excluded = new Set(excludeTools);
40
+ return KNOWN_PERSISTENCE_TOOL_NAMES.some((name) => !excluded.has(name));
41
+ }
42
+ /**
43
+ * 该不该把 `NO_PERSISTENT_MEMORY_NOTICE` 组进本次任务的 system prompt。
44
+ *
45
+ * 优先序(每一条都有独立理由,不是同一条判据的三种写法):
46
+ * ① engine 在场 ⇒ **否**(引擎的座位,见文件头注);
47
+ * ② 部署显式声明 ⇒ 声明说了算(`true` 撤回 / `false` 强制)—— operator 的部署事实压过我们的推断,
48
+ * 与 core 对 `memoryPersistenceCapable` 的处置同向;
49
+ * ③ 否则推断:有已知落盘路(手带在场且不是只读)⇒ 否;没有 ⇒ 是。
50
+ */
51
+ export function shouldDiscloseNoPersistentMemory(facts) {
52
+ if (facts.memory !== undefined)
53
+ return false;
54
+ if (facts.declaredCapable !== undefined)
55
+ return !facts.declaredCapable;
56
+ if (facts.hands === "none" || facts.handsReadOnly)
57
+ return true;
58
+ // remote 执行环境:手带写的是沙箱盘(每任务即焚),够不着任何 host 侧持久面 —— core 的
59
+ // `rosterCanPersist` 在这一形下把四只写工具**全部**不算,这里逐字同口径。
60
+ if (facts.remoteExecutionEnv)
61
+ return true;
62
+ // 手带在场,但 roster 可能把落盘路整批卸掉了(`excludeTools` 是真卸载)。⚠️ 负控仍成立:排掉**一个**
63
+ // `Write` 不算无写通道(Bash/Edit 还在)—— 必须**全部**已知落盘路都不在,才算这条链断了。
64
+ return !anyPersistenceToolSurvives(facts.excludeTools);
65
+ }
66
+ /**
67
+ * 把披露段组进一个 prompt provider,**按 provider 自己的钩子形分腿**(换形会改段身份与 append 座位):
68
+ * · typed(`stableBlocks`)⇒ 追加一条 `behavior` 声明。**不能**用 `identity`:那个 slot 会替换 role base。
69
+ * · string(`stableSystem`)⇒ 接在 role 层文本之后(落进 `core/role.base` 段)。
70
+ * · 两个钩子都没有(= core 会用 `defaultPromptProvider`)⇒ **自铸 role 层**。这里绝不能去包
71
+ * `defaultPromptProvider.stableSystem` —— 它返回的是 `composeConstitution(...)` 的**整装** prompt,
72
+ * 装配器的迁移卫探到三锚(CYBER_RISK/URL_SAFETY/`# Harness`)就走 pass-through 臂,那条臂的段子集
73
+ * 没有 `core/role.append`,调用方的 append rider 会被静默丢掉。自铸的是 core 自己那一句
74
+ * `userSystemPrompt ?? DEFAULT_SYSTEM_PROMPT`(引用常量,不复制字节)。
75
+ */
76
+ export function withNoPersistentMemoryNotice(provider) {
77
+ if (provider?.stableBlocks) {
78
+ const blocks = provider.stableBlocks.bind(provider);
79
+ return {
80
+ ...provider,
81
+ stableBlocks: (ctx) => [...blocks(ctx), { id: NO_PERSISTENT_MEMORY_SECTION_ID, slot: "behavior", text: NO_PERSISTENT_MEMORY_NOTICE }],
82
+ };
83
+ }
84
+ if (provider?.stableSystem) {
85
+ const stable = provider.stableSystem.bind(provider);
86
+ return { ...provider, stableSystem: (ctx) => `${stable(ctx)}\n\n${NO_PERSISTENT_MEMORY_NOTICE}` };
87
+ }
88
+ return { stableSystem: (ctx) => `${ctx.userSystemPrompt ?? DEFAULT_SYSTEM_PROMPT}\n\n${NO_PERSISTENT_MEMORY_NOTICE}` };
89
+ }
90
+ //# sourceMappingURL=memory-notice.js.map
@@ -13,6 +13,22 @@ import type { QuestionThrottle } from "./question.js";
13
13
  import type { InfraCostRates } from "./observability/cost-taxonomy.js";
14
14
  import type { Autonomy, CommandRule } from "./runtime-governance.js";
15
15
  import type { GitApiKind } from "./git-api-kind.js";
16
+ /** #228:记忆向量面的 embedder 坐标(env `MEMORY_EMBEDDER_*` 解析结果;单一属主 = 本类型,
17
+ * `plugins/memory-embedder.ts` 的工厂直接消费它,不复制形状)。语义与三问见
18
+ * {@link ServiceConfigFlat.memoryEmbedder}。纯数据 —— 端点原文照存(拼 `/embeddings` 是 transport
19
+ * 的活,与 `memorySync.url` 的尾斜杠口径一致)。 */
20
+ export interface MemoryEmbedderConfig {
21
+ /** OpenAI 兼容服务的基址或完整 `/embeddings` 端点(两种写法都收,拼接在工厂里归一)。 */
22
+ endpoint: string;
23
+ /** 模型名,原样进请求体 `{ model, input }`。 */
24
+ model: string;
25
+ /** 声明维度(正整数)。响应向量长度与它不等 = 抛,绝不截断/补零。 */
26
+ dimensions: number;
27
+ /** 单次 embed 的活性上限(ms)。`MEMORY_EMBEDDER_TIMEOUT_MS`,默认 30s。 */
28
+ timeoutMs: number;
29
+ /** 可选 Bearer(自托管 TEI/vllm/ollama 通常不需要;公有云端点需要)。 */
30
+ apiKey?: string;
31
+ }
16
32
  /** A sema-registry MCP server resolved to a core spec (env-NAME refs already → real values) plus the
17
33
  * scenarios it applies to (empty = all). `resolveSpec` filters by scenario and passes `spec` to core. */
18
34
  export interface ScopedMcpServer {
@@ -23,7 +39,8 @@ export interface ScopedMcpServer {
23
39
  * off (the control plane never mounts). The runner-facing facts (the runner principal, the build-host CACHE_BASE,
24
40
  * the default base ref) live here so image-api injects only VETTED, server-fixed values into the build.sh argv —
25
41
  * operator free-text never reaches the runner. */
26
- /** #151 车3(design/172 流内审批协议)的配置面。总开关默认 **false**;四个从属旋钮只在开关为真时生效
42
+ /** #151 车3(design/172 流内审批协议)的配置面。总开关默认 **true**(clay 裁 2026-08-08,#164 翻真验证后;
43
+ * 树上已生效,发布线落在 7.4.0 之后的下一个发布);四个从属旋钮只在开关为真时生效
27
44
  * (语义与「谁需要 / 谁被伤 / 什么补偿」三问见 `ServiceConfigFlat.streamApproval` 的注)。 */
28
45
  export interface StreamApprovalConfig {
29
46
  /** 协议总开关。`STREAM_APPROVAL_ENABLED`,默认 **true**(clay 裁 2026-08-08,#164 翻真验证后;7.5.0 起)。显式 false ⇒ 全链逐字 7.3.0 前行为(唯一干净还原键)。
@@ -233,6 +250,37 @@ export interface ServiceConfigFlat {
233
250
  /** The single-user deployment's one memory scope name (env MEMORY_SCOPE; default "local" under
234
251
  * single-user). Feeds `spec.memory.scope` — the new engine consumes the same normalized scopes. */
235
252
  memoryScope?: string;
253
+ /** #217(core 5.26.0 `TaskSpec.memoryPersistenceCapable`)—— 本部署对「这台机器上的任务**能不能**把
254
+ * 用户说的『记住 X』落到持久处」的**自我声明**,env `MEMORY_PERSISTENCE_CAPABLE`,三态:
255
+ * · 缺席(默认)⇒ 键不上 TaskSpec,core 按自己的 roster 推断(已知落盘路 = 挂载且未排除的文件写工具
256
+ * 或可写 shell;remote 执行环境的手带写的是沙箱盘,不算够得着 host 记忆根);
257
+ * · `true` ⇒ 部署自证有推断**看不见**的持久通道(自定义 writer / MCP 记忆服务 / remote lane 与记忆根
258
+ * 共享挂载)。它撤回引擎的只读披露,并在 remote lane 上**恢复** `# Memory` 写指令(core 5.26.0 r13/r14
259
+ * 收窄的官方恢复路径);
260
+ * · `false` ⇒ 强制披露只读/无持久面,并关掉文件工具往记忆根的写通道(披露不会被存储事实打脸)。
261
+ *
262
+ * 🔴 **`false` 自 core 5.27.0 起还多一层**([3612] F2,#231 提货):declared-false 的会话是**受限会话**
263
+ * —— materialize/search/harvest 一律按**已提交账**(ledger + shadow)供给,磁盘上无事务背书的分歧
264
+ * 既不收编也不供给,而是留盘 + 响亮点名(`restricted_divergence`,`HarvestRejectionCode` 新成员),
265
+ * 等下一次**非受限**会话走正常门收编。这补上了 5.26.0 自己登记的那条残余(读侧入站同步收编磁盘分歧
266
+ * **与会话意图无关**)——在那之前,这里写的「披露不会被存储事实打脸」只是半真话:披露说「本机存不下」,
267
+ * 引擎却仍可能把别处落进记忆根的字节收进这次会话的账。
268
+ * ⚠️ **这一层的射程 = file 记忆引擎**(`MEMORY_ENGINE_BACKEND` 未设的默认单机形):受限视图是
269
+ * `FileMemoryEngineBackend.restrictedAdoptionView` 的实装,而 `engine.materialize` 取它走的是
270
+ * `?? this.backend` 的**可选** seam。我方 pg/tidb 两只记忆后端没有实装、也不需要——它们的读侧走库,
271
+ * 盘上目录只是每任务的投影工作区,没有「读磁盘即收编」这条通道。`false` 在 DB 后端上仍照常买到
272
+ * 披露 / 写门 / 零收编 harvest 三件(引擎级,与后端无关)。
273
+ *
274
+ * 与请求键 `memoryWrite`(MF-30)**两轴正交**,不是同一件事的两种写法:`memoryWrite` 是**本次运行**的
275
+ * 客户端表态(per-run 暂停写,writeScope:null),`memoryPersistenceCapable` 是**部署**声明(operator 面,
276
+ * 与请求无关)。两轴任一取「否」方向即只读方向 —— 客户端能收紧本次运行,永远不能靠表态把部署声明的
277
+ * `false` 撑开(operator 旋钮不挂客户端表态派生腿)。
278
+ * ⚠️ **别把两轴的「只读」当成同一件事**([3612] F2 修后口径,逐字对齐 core):**受限**是**会话级的
279
+ * verdict 单键**(= 这里的 declared-false),而 `writeScope:null` 的只读**平面**(`memoryWrite:false` 的
280
+ * per-run 暂停 / org 层默认只读 / 双根非写面)**保持 adopt-on-read 原样** —— 受限是会话的**声明**,
281
+ * 永远不是平面的**结构**。所以:一次 `memoryWrite:false` 的运行照常收编磁盘上的新内容(用户手改、
282
+ * git pull 落下的文件都还认),它只是不 harvest;而 declared-false 的部署连收编都不做。 */
283
+ memoryPersistenceCapable?: boolean;
236
284
  /** 142-S2.5-W1: TOC 同步 client 腿 —— file memory 形态把本地记忆盘与中心(DB 权威,
237
285
  * POST /v1/memory/sync/:scope)做一轮一 RTT 双向同步。env 三键:`MEMORY_SYNC_URL`(设了=开;缺省
238
286
  * undefined=纯本地现状零变化)/`MEMORY_SYNC_TOKEN`(Bearer)/`MEMORY_SYNC_SCOPE`(可选,缺省=
@@ -246,6 +294,25 @@ export interface ServiceConfigFlat {
246
294
  maxPushEntries?: number;
247
295
  maxPullEntries?: number;
248
296
  };
297
+ /** #228(黑板 [3590]/[3606] 两裁):记忆检索的**向量面**接线 —— OpenAI 兼容 `/embeddings` 端点。
298
+ * env 四键(+ 可选 key):`MEMORY_EMBEDDER_ENDPOINT` / `MEMORY_EMBEDDER_MODEL` / `MEMORY_EMBEDDER_DIM`
299
+ * (+ `MEMORY_EMBEDDER_TIMEOUT_MS` / `MEMORY_EMBEDDER_API_KEY`)。三问:
300
+ *
301
+ * ① **谁需要**:pg 记忆后端的部署。core 的记忆引擎三档(`vectorMode = lexical|portable|native`)不是
302
+ * 档位旋钮,而是「后端上限 × embedder 在场」的**推断结果**([3590] 裁①:不开档位 env —— 显式选档
303
+ * 只会造出「选了 native 却没 embedder」这类矛盾态)。本仓在 #228 之前从未注入过 embedder,所以
304
+ * pg 部署的检索**恒 lexical**(词面 jaccard),向量列空转。配上这几键 = 该部署真正拿到语义检索。
305
+ * ② **半配为何拒启**:三键里少任何一件,「向量面」都无从组装。静默忽略的后果不是功能缺席,而是
306
+ * **operator 以为开了**——检索照常返回结果(词面档),质量差异只在最终答案里,任何日志都不报。
307
+ * 所以照 `MEMORY_SYNC_*` / `REQUIRED_ENVS_BY_LANE` 的同款姿势整拒并点名缺哪几键。
308
+ * ③ **维度守卫为何是拒启不是 warn**:向量列写错维度是**静默毒库**形([3606] 裁③)——错长度的向量
309
+ * 既不会让 DB 报错(本仓 pg 记忆表的 embedding 是 `jsonb`,无维度约束),也不会让检索报错(维度
310
+ * 不匹配的行只是悄悄退回词面档),只会让「向量面开着」这句话变成谎。所以坏 DIM(非正整数)在
311
+ * boot 期拒;运行期真维度不符(模型换了 / 端点指错)由 embedder 自己抛,绝不截断补零。
312
+ *
313
+ * ⚠️ 后端门:只有 `MEMORY_ENGINE_BACKEND=pg` 收这几键 —— tidb 记忆后端 v1 无向量档(传 embedder
314
+ * 它自己就 fail-loud 拒),file 引擎压根没有 embedder 接缝。配错后端 = 拒启点名。 */
315
+ memoryEmbedder?: MemoryEmbedderConfig;
249
316
  /** design/170 件A(clay 三裁 [2687]):org 记忆准入模式。`enforce`(默认)=判决即结果;`audit`=判决
250
317
  * 照算但纯观察零行为变化(不整拒、不窄化 writeScope,只记 would-deny/would-narrow)——运维诊断位,
251
318
  * 非发布步骤(audit-first 灰度步按裁2 免除)。env `MEMORY_ORG_ADMISSION_MODE`(enumEnv 二值)。 */
@@ -552,8 +619,20 @@ export interface ServiceConfigFlat {
552
619
  approvalAutoBudget: number;
553
620
  /** design/80 D-E (inv#2 — safety asks are never budgetable): gated tools that ALWAYS require a human even when
554
621
  * the auto-budget is on — the "irreversible / never auto-approve" set. empty = the budget may auto-approve any
555
- * required tool. (In-service every gated ask is gate.kind="human", so this set is the ONLY way to keep an
556
- * irreversible tool human-gated under an auto-budget.) `APPROVAL_NEVER_AUTO`.
622
+ * required tool THIS layer sees. `APPROVAL_NEVER_AUTO`.
623
+ *
624
+ * 🔴 **NOT the only human-gate under an auto-budget**(2026-08-10 修口,#205 件2)。这里原来写的是
625
+ * "(In-service every gated ask is gate.kind=\"human\", so this set is the ONLY way to keep an irreversible
626
+ * tool human-gated under an auto-budget.)" —— 两句都已过期,且过期方向是**高估本旋钮的责任**:
627
+ * · design/80 D-2 起,带 egress / irreversibility 静态标记的工具铸的是 `irreversible_ask`,不是 `human`;
628
+ * · core 5.24.0(#130/#131/#120)起,带 `requiresRealApproval` 的 ask 一律铸
629
+ * `irreversible_ask` + `gate.realApproval:{origin}` + checkpoint v8 —— **不再铸可预算的 `human`**。
630
+ * 而 `requiresRealApproval` 的生产者(`createTranscriptIntegrityPolicy` / `createUnverifiableDeletePolicy`)
631
+ * 被 core 在 `prepare-task.js` 里**无条件**焊进每一条策略链,所以这条路径在**每一个**部署上都活着,
632
+ * 不是 org 治理部署的专属面。
633
+ * ⇒ 本旋钮承担的是**本层**(`createDurableAskPolicy` 的 per-leg 计数预算)的 inv#2;安全类 ask 另有
634
+ * core 侧那两层守卫兜底,两层各自独立、不互为前提。消费侧钉:`test/durable-approval-policy.test.ts`
635
+ * 的「#205 件2」describe(含反向锚:普通调用仍走预算自动批)。
557
636
  *
558
637
  * 🔴 **NOT required to be a subset of `approvalRequire`**(2026-07-31 修口)。这里原来写的是
559
638
  * "Subset of approvalRequire",而 `approval.ts` 的 inv#2 执行面逐字相反:
@@ -896,16 +975,26 @@ export interface ServiceConfigFlat {
896
975
  * 旋钮本身缺省对现行为零影响(D1)。`STREAM_ASK_WINDOW_MARGIN_MS`,默认 10000(10s)。ttl 概念沿用既有
897
976
  * `approvalTimeoutSec` 族,本旋钮不新增第二个 TTL 概念。 */
898
977
  streamAskWindowMarginMs: number;
899
- /** #151 车3(design/172 流内审批协议)总开关 + 四个从属旋钮。**协议整体默认 OFF**——开关关闭时全链
900
- * 逐字现行为:不发 `approval_request` 帧、不落 `approval_ask` 行、窗 = 既有 `DEFAULT_APPROVAL_TTL_MS`
901
- * (5min)、`askStore` 不注入协调器。理由(三问):
902
- * - 谁需要 60s 窗:开了协议的部署——approver 就盯着流、秒级应答,窗短 少占一条活腿、更快落到可恢复
903
- * park 面。这是协议的设计意图,不是随手取的数。
978
+ /** #151 车3(design/172 流内审批协议)总开关 + 四个从属旋钮。
979
+ *
980
+ * **现行默认(clay 裁 2026-08-08 走 (a),#164 翻真验证后)**:`STREAM_APPROVAL_ENABLED` 默认 **true**
981
+ * = 协议整体默认 **ON**;`STREAM_ASK_WINDOW_MS` 默认 **300000**(5min,与 sync 腿既有活卡窗、与
982
+ * `DEFAULT_APPROVAL_TTL_MS` 对齐——「开协议」不再顺带把窗砍短)。⚠️ 版本坐标:翻转**已在树上生效**,
983
+ * 发布线上落在 7.4.0 之后的下一个发布,所以一台自报 7.4.0 的**已发布** worker 仍是 OFF,别只按
984
+ * `/health` 的版本号推默认。显式 `STREAM_APPROVAL_ENABLED=false` 是**唯一干净还原键** ⇒ 全链逐字回到
985
+ * 翻转前行为:不发 `approval_request` 帧、不落 `approval_ask` 行、不起收敛器腿、窗 = 既有
986
+ * `DEFAULT_APPROVAL_TTL_MS`(5min)、`askStore` 不注入协调器。
987
+ *
988
+ * **旋钮为什么存在(2026-08-08 翻转前的成文前提,已被上面的裁定取代,留作沿革)**:那一版默认 false +
989
+ * 60s 窗,三问记的是那个世界——
990
+ * - 谁需要 60s 窗:opt-in 开了协议的部署——approver 就盯着流、秒级应答,窗短 ⇒ 少占一条活腿、更快落到
991
+ * 可恢复的 park 面。
904
992
  * - 谁被伤:①未 opt-in 的存量部署(把 5min 砍成 60s = 人离开工位 90 秒回来卡已经没了);②**park 设施
905
993
  * 不在场**的部署——窗到期后 `"unavailable"` 没有降级目的地,core fail-closed deny,60s 会把
906
994
  * 「人 90 秒后批准 = 放行」变成「拒绝」,是真行为回归。
907
- * - 补偿:把 60s 绑在**新旋钮 + 新开关**上,一个字不动 `DEFAULT_APPROVAL_TTL_MS`;两边都不伤,也不给
908
- * 既有腿新增第二个 TTL 概念。
995
+ * - 补偿:把 60s 绑在**新旋钮 + 新开关**上,一个字不动 `DEFAULT_APPROVAL_TTL_MS`。
996
+ * 翻真时「窗变短」这一项被从翻转里摘出(窗默认抬回 300000),被伤面①②因此消失;60s 只在运维显式设
997
+ * `STREAM_ASK_WINDOW_MS=60000` 时才回来。
909
998
  *
910
999
  * `STREAM_APPROVAL_ENABLED` / `STREAM_ASK_WINDOW_MS` / `STREAM_APPROVAL_REPLAY_MAX` /
911
1000
  * `STREAM_APPROVAL_ADMIT_MAX_PER_TASK` / `STREAM_APPROVAL_ADMIT_MAX_PER_OWNER` /
@@ -1034,7 +1123,7 @@ export type ServiceModelPlaneConfig = Pick<ServiceConfigFlat, "gatewayBaseUrl" |
1034
1123
  * 就是本组的门状态,故进组;`parseApprovalDomain` 的返回类型相应是 `Omit<…, "directDoorActive">`。 */
1035
1124
  export type ServiceApprovalConfig = Pick<ServiceConfigFlat, "approvalRequire" | "approvalDeny" | "approvalTimeoutSec" | "approvalAutoBudget" | "approvalNeverAuto" | "approvalHmacKeys" | "durableApproval" | "directApprovalDoor" | "directDoorActive" | "resourceSuspend" | "resourceSuspendTtlSec" | "askQuestionEnabled" | "questionThrottle" | "toolApprovalEnabled" | "permissionRulesEnabled" | "permissionRulesEnabledExplicit" | "streamAskWindowMarginMs" | "streamApproval" | "mcpElicitation" | "sensitiveWritePatterns" | "manualModeShellGate">;
1036
1125
  /** 组:memory(记忆面 + TOC 同步腿)。 */
1037
- export type ServiceMemoryConfig = Pick<ServiceConfigFlat, "memoryEngineEnabled" | "memoryEngineDir" | "memoryEngineRemoteLaneAllowed" | "memoryEngineBackend" | "memoryScope" | "memorySync" | "memoryOrgAdmissionMode" | "memoryOrgDirectoryJson" | "memoryOrgGrantTtlMs" | "memoryOrgUnavailableBackoffMs" | "projectMemoryEnabled" | "syncImportLeaseStaleSec">;
1126
+ export type ServiceMemoryConfig = Pick<ServiceConfigFlat, "memoryEngineEnabled" | "memoryEngineDir" | "memoryEngineRemoteLaneAllowed" | "memoryEngineBackend" | "memoryScope" | "memoryPersistenceCapable" | "memorySync" | "memoryEmbedder" | "memoryOrgAdmissionMode" | "memoryOrgDirectoryJson" | "memoryOrgGrantTtlMs" | "memoryOrgUnavailableBackoffMs" | "projectMemoryEnabled" | "syncImportLeaseStaleSec">;
1038
1127
  /** 组:auth(鉴权 / 身份 / 治理棒)。`commandPolicy` 只有 sema-registry 腿(无 env 标量形),故 env 解析
1039
1128
  * 函数不产出它,但它与 `autonomy` 是同一根治理棒的两半,归本组。 */
1040
1129
  export type ServiceAuthConfig = Pick<ServiceConfigFlat, "authToken" | "authTokens" | "allowUnauthedWrites" | "corsOrigins" | "principalHeader" | "requirePrincipal" | "autonomy" | "commandPolicy" | "operatorPrincipals" | "principalJwtPubkeys" | "principalJwtIss" | "principalJwtAud" | "principalJwtMaxTtlSec" | "bindHost" | "bindHostSource">;
package/dist/config.d.ts CHANGED
@@ -4,7 +4,7 @@ import type { ServiceConfig, ServiceConfigFlat, ServiceConfigGroups } from "./co
4
4
  /** design/158 A1: the config TYPE face lives in the leaf module config-types.ts; re-exported here so
5
5
  * every existing `from "./config.js"` importer compiles unchanged (pure-type consumers should prefer
6
6
  * importing config-types.js directly — a type-only leaf, no loader baggage). */
7
- export type { ServiceConfig, ScopedMcpServer, ImageBakeConfig, ServiceConfigFlat, ServiceConfigGroups, ServiceStoreConfig, ServiceModelPlaneConfig, ServiceApprovalConfig, ServiceMemoryConfig, ServiceAuthConfig, ServiceOrchestrationConfig, ServiceLimitsHttpConfig, ServiceObservabilityConfig, ServiceIntegrationsConfig, } from "./config-types.js";
7
+ export type { ServiceConfig, ScopedMcpServer, ImageBakeConfig, ServiceConfigFlat, ServiceConfigGroups, ServiceStoreConfig, ServiceModelPlaneConfig, ServiceApprovalConfig, ServiceMemoryConfig, ServiceAuthConfig, ServiceOrchestrationConfig, ServiceLimitsHttpConfig, ServiceObservabilityConfig, ServiceIntegrationsConfig, MemoryEmbedderConfig, } from "./config-types.js";
8
8
  /** Parse the AUTONOMY env into a validated autonomy mode. Unset/empty → undefined (unmanaged → no extra
9
9
  * tightening). An UNKNOWN value FAILS at startup rather than silently becoming a no-op (a typo'd `AUTONOMY=readonly`
10
10
  * must not silently leave a deployment ungoverned — fail-loud, same discipline as numEnv). Exported so the HOT
package/dist/config.js CHANGED
@@ -1257,6 +1257,32 @@ function parseMemoryEngineWord(word) {
1257
1257
  `(engine ON, the default) or ${MEMORY_ENGINE_OFF_WORDS.join(" | ")} (engine OFF). ` +
1258
1258
  `An unrecognized value used to read as "on", so a misspelled kill-switch silently kept memory injection running.`);
1259
1259
  }
1260
+ /**
1261
+ * #217:`MEMORY_PERSISTENCE_CAPABLE` 的**三态**读取(core 5.26.0 `TaskSpec.memoryPersistenceCapable`)。
1262
+ *
1263
+ * 与 {@link parseMemoryEngineWord} 共用同一份双族词表(operator 面写法一致,别让同一台机器上的两个记忆
1264
+ * 旋钮各认一套词),但**多一个态**:缺席/空串 ⇒ `undefined` ⇒ TaskSpec 上整键缺席 ⇒ core 走自己的推断
1265
+ * (三态语义的 absent 腿,零迁移)。空串等同未设,与 MEMORY_ENGINE 同理由(compose 的 `${X:-}` 模板天天产它)。
1266
+ *
1267
+ * 不认得的词 **拒启**而不是回落:这个键是「本部署能不能替用户记住事情」的真值来源,静默倒向任何一侧都
1268
+ * 是猜——倒向 `true` 会让一台其实存不下的部署继续给幻觉式回执(#148 原病),倒向 `false` 会让一台真有
1269
+ * 自定义持久通道的部署对用户说假话。
1270
+ */
1271
+ function parseMemoryPersistenceCapable(raw) {
1272
+ if (raw === undefined)
1273
+ return undefined;
1274
+ const w = raw.trim().toLowerCase();
1275
+ if (w === "")
1276
+ return undefined;
1277
+ if (MEMORY_ENGINE_ON_WORDS.includes(w))
1278
+ return true;
1279
+ if (MEMORY_ENGINE_OFF_WORDS.includes(w))
1280
+ return false;
1281
+ throw new Error(`env MEMORY_PERSISTENCE_CAPABLE="${raw}" is not a known on/off word — use one of: ${MEMORY_ENGINE_ON_WORDS.join(" | ")} ` +
1282
+ `(this deployment CAN persist "remember X" — declares a channel the roster inference cannot see, and restores the ` +
1283
+ `\`# Memory\` write instruction on a remote execution lane) or ${MEMORY_ENGINE_OFF_WORDS.join(" | ")} (it CANNOT — ` +
1284
+ `forces the read-only disclosure). Leave it UNSET to let the engine infer from the mounted roster.`);
1285
+ }
1260
1286
  /** #210:SANDBOX_PKG_SOURCE 的闭集校验(词表 = README/MIGRATION 成文的四词)。`none` 由调用点的三元
1261
1287
  * 链先摘走(字段整个缺席),留在这里的只可能是前三词或一个手滑值。 */
1262
1288
  const SANDBOX_PKG_SOURCES = ["global", "cn", "custom", "none"];
@@ -1278,6 +1304,85 @@ function syncEntryCap(name, field) {
1278
1304
  throw new Error(`env ${name}="${raw}" must be an integer >= 1 (it is an entries-per-batch cap; an ignored cap means NO cap)`);
1279
1305
  return { [field]: n };
1280
1306
  }
1307
+ /**
1308
+ * #228(黑板 [3590]/[3606] 两裁):`MEMORY_EMBEDDER_*` 族 —— 记忆检索向量面的**唯一**开关。
1309
+ *
1310
+ * 三态(与 `MEMORY_SYNC_*` 半配拒启同族):
1311
+ * · 三键全缺席 ⇒ `undefined`,不注入 embedder,现状零变化(检索恒 lexical 词面档);
1312
+ * · 半配(1~2 键)⇒ **拒启并点名缺哪几键**(REQUIRED_ENVS_BY_LANE 的报文形)。静默忽略的后果不是
1313
+ * 功能缺席,而是 operator 以为向量面开着——检索照常出结果,差异只在答案质量里,零日志;
1314
+ * · 坏值(URL 不合法 / 非 http(s) / DIM 非正整数 / TIMEOUT 非法或越界)⇒ 拒启带指路(#210 A 档)。
1315
+ *
1316
+ * 另两条门(都是「配了但永远不生效」的死旋钮形,一律拒启而非 warn):
1317
+ * · `MEMORY_ENGINE_BACKEND` 必须是 `pg` —— tidb 记忆后端 v1 无向量档(传 embedder 它自己 fail-loud
1318
+ * 拒),file 引擎没有 embedder 接缝;
1319
+ * · 记忆引擎本身必须开着(`MEMORY_ENGINE != off`)。
1320
+ *
1321
+ * 维度守卫的**位置**(诚实记档):本仓建 pg 记忆表时不传 `memoryVector`(见 boot/stores.ts 的
1322
+ * `ensurePgMemoryEngineSchema(q)`),即 embedding 列是 `jsonb`、schema 侧**没有可对表的维度常量**;
1323
+ * 所以 boot 期能做的只有「DIM 是正整数」,真正的维度守卫在写入路径两处:① 本仓 embedder 拿到长度
1324
+ * 不符的响应直接抛(绝不截断/补零);② `PgMemoryEngineBackend.embeddingParam` 对长度不符的向量返回
1325
+ * null(该行退回词面档,不写坏向量)。
1326
+ */
1327
+ function parseMemoryEmbedder(ctx) {
1328
+ // 空串 = 未设(compose `${X:-}` 模板天天产它,本仓一律同判)
1329
+ const endpoint = process.env.MEMORY_EMBEDDER_ENDPOINT || undefined;
1330
+ const model = process.env.MEMORY_EMBEDDER_MODEL || undefined;
1331
+ const dimRaw = process.env.MEMORY_EMBEDDER_DIM || undefined;
1332
+ const apiKey = process.env.MEMORY_EMBEDDER_API_KEY || undefined;
1333
+ const timeoutRaw = process.env.MEMORY_EMBEDDER_TIMEOUT_MS || undefined;
1334
+ const trio = [
1335
+ ["MEMORY_EMBEDDER_ENDPOINT", endpoint],
1336
+ ["MEMORY_EMBEDDER_MODEL", model],
1337
+ ["MEMORY_EMBEDDER_DIM", dimRaw],
1338
+ ];
1339
+ const missing = trio.filter(([, v]) => v === undefined).map(([name]) => name);
1340
+ if (missing.length === 3) {
1341
+ // 三键全缺席 = 文档化的默认姿态(不注入)。但**从属**两键单独在场 = 同款半配陷阱:operator 调了
1342
+ // 超时/配了 key,却一个向量都不会算(MEMORY_SYNC_TOKEN/SCOPE 无 URL 对称拒启的同款先例)。
1343
+ if (timeoutRaw !== undefined || apiKey !== undefined) {
1344
+ throw new Error(`MEMORY_EMBEDDER_TIMEOUT_MS/MEMORY_EMBEDDER_API_KEY are set but the embedder itself is not configured — refusing to start half-configured (set MEMORY_EMBEDDER_ENDPOINT, MEMORY_EMBEDDER_MODEL and MEMORY_EMBEDDER_DIM to enable the memory vector plane, or unset the other MEMORY_EMBEDDER_* keys)`);
1345
+ }
1346
+ return undefined;
1347
+ }
1348
+ // 三键缺任一(TS 也靠这一支收窄,不需要非空断言)
1349
+ if (endpoint === undefined || model === undefined || dimRaw === undefined) {
1350
+ throw new Error(`MEMORY_EMBEDDER_* is half-configured — missing ${missing.join(", ")}. The memory vector plane needs all three of MEMORY_EMBEDDER_ENDPOINT, MEMORY_EMBEDDER_MODEL, MEMORY_EMBEDDER_DIM; refusing to start rather than silently running lexical-only retrieval while the deployment believes vectors are on (#228).`);
1351
+ }
1352
+ if (!URL.canParse(endpoint)) {
1353
+ throw new Error(`MEMORY_EMBEDDER_ENDPOINT="${endpoint}" is not a valid URL (expected e.g. https://embeddings.example.com/v1 — the OpenAI-compatible base, or the full .../embeddings endpoint)`);
1354
+ }
1355
+ const parsedEndpoint = new URL(endpoint);
1356
+ const proto = parsedEndpoint.protocol;
1357
+ if (proto !== "http:" && proto !== "https:") {
1358
+ throw new Error(`MEMORY_EMBEDDER_ENDPOINT="${endpoint}" must be http(s) (got "${proto}") — the embedder leg is an HTTP POST to an OpenAI-compatible /embeddings face`);
1359
+ }
1360
+ // URL 里的 userinfo 一律拒(codex 二轮 F1):`https://user:pass@host/v1` 形把凭据种进一个会被
1361
+ // 传输层错误、日志、PatchReport 冲突文案反复复制的字符串里 —— 脱敏只能事后擦,拒收才是源头。
1362
+ // 需要 auth 就走 MEMORY_EMBEDDER_API_KEY(Bearer 头,不进 URL)。
1363
+ if (parsedEndpoint.username !== "" || parsedEndpoint.password !== "") {
1364
+ throw new Error("MEMORY_EMBEDDER_ENDPOINT must not carry credentials in the URL (user:password@host) — URL userinfo leaks into transport errors, logs and memory conflict reports. Use MEMORY_EMBEDDER_API_KEY (sent as a Bearer header) instead.");
1365
+ }
1366
+ const dimensions = Number(dimRaw);
1367
+ if (!Number.isSafeInteger(dimensions) || dimensions < 1) {
1368
+ throw new Error(`MEMORY_EMBEDDER_DIM="${dimRaw}" must be a positive integer (the embedding dimension, e.g. 1024). A wrong dimension writes wrong-length vectors into the memory embedding column, which neither the DB nor retrieval reports — the vector plane just silently degrades to the lexical floor (#228).`);
1369
+ }
1370
+ if (ctx.memoryEngineBackend !== "pg") {
1371
+ throw new Error(`MEMORY_EMBEDDER_* is set but MEMORY_ENGINE_BACKEND=${ctx.memoryEngineBackend} — the vector plane exists only on the pg memory backend (the tidb backend is lexical-only in v1 and refuses an embedder; the file engine has no embedder seam). Set MEMORY_ENGINE_BACKEND=pg, or unset MEMORY_EMBEDDER_*.`);
1372
+ }
1373
+ if (!ctx.memoryEngineEnabled) {
1374
+ throw new Error("MEMORY_EMBEDDER_* is set but MEMORY_ENGINE=off — the memory engine is not wired at all, so the embedder would never be called; unset one of the two (#228)");
1375
+ }
1376
+ return {
1377
+ endpoint,
1378
+ model,
1379
+ dimensions,
1380
+ // 活性保护:挂死的 embedder 会把整条记忆写入路径吊住(undici 默认 headers 超时 300s 太钝)。
1381
+ // 下界 1s(比这更短的向量调用只会全轮超时);上界 5min(再长就不是超时保护了)。
1382
+ timeoutMs: numEnvBounded("MEMORY_EMBEDDER_TIMEOUT_MS", "30000", 1000, 300_000),
1383
+ ...(apiKey !== undefined ? { apiKey } : {}),
1384
+ };
1385
+ }
1281
1386
  /** 域:memory(记忆面 + TOC 同步腿)—— 记忆引擎开关/方言/根目录、scope 缺省、同步 client 三键、项目记忆。 */
1282
1387
  function parseMemoryDomain(ctx) {
1283
1388
  const { requirePrincipal } = ctx; // 跨域入参①:多租户下记忆面 DARK(scope 缺省不铸)
@@ -1376,7 +1481,12 @@ function parseMemoryDomain(ctx) {
1376
1481
  // memoryScopeFor returns undefined there). An explicit MEMORY_SCOPE always wins.
1377
1482
  memoryEngineBackend,
1378
1483
  memoryScope, // hoisted above (the 142-S2.5-W1 sync-scope default consumes it)
1484
+ // #217:三态 ⇒ 只有真表态才上键(缺席腿必须是**整键缺席**,不是 present-as-undefined —— 后者会被
1485
+ // resolve-spec 原样搬上 TaskSpec,把 core 的「absent = 各自推断」读成一个显式表态)。
1486
+ ...((v) => (v !== undefined ? { memoryPersistenceCapable: v } : {}))(parseMemoryPersistenceCapable(process.env.MEMORY_PERSISTENCE_CAPABLE)),
1379
1487
  ...(memorySync ? { memorySync } : {}),
1488
+ // #228:向量面接线(三态解析 + 后端/引擎两门,全在 parseMemoryEmbedder 里 fail-loud)
1489
+ ...((v) => (v !== undefined ? { memoryEmbedder: v } : {}))(parseMemoryEmbedder({ memoryEngineBackend, memoryEngineEnabled })),
1380
1490
  memoryOrgAdmissionMode,
1381
1491
  ...(memoryOrgDirectoryJson !== undefined ? { memoryOrgDirectoryJson } : {}),
1382
1492
  memoryOrgGrantTtlMs,
@@ -1856,7 +1966,7 @@ const APPROVAL_GROUP_KEYS = [
1856
1966
  ];
1857
1967
  const MEMORY_GROUP_KEYS = [
1858
1968
  "memoryEngineEnabled", "memoryEngineDir", "memoryEngineRemoteLaneAllowed", "memoryEngineBackend", "memoryScope",
1859
- "memorySync", "memoryOrgAdmissionMode", "memoryOrgDirectoryJson", "memoryOrgGrantTtlMs", "memoryOrgUnavailableBackoffMs",
1969
+ "memoryPersistenceCapable", "memorySync", "memoryEmbedder", "memoryOrgAdmissionMode", "memoryOrgDirectoryJson", "memoryOrgGrantTtlMs", "memoryOrgUnavailableBackoffMs",
1860
1970
  "projectMemoryEnabled", "syncImportLeaseStaleSec",
1861
1971
  ];
1862
1972
  const AUTH_GROUP_KEYS = [
@@ -40,7 +40,8 @@
40
40
  *
41
41
  * 🔴 codex 交叉复审 round3 [medium](验真):`toolCallId` **不是**全局唯一的 —— 它多数情况下是**提供方**
42
42
  * 逐字给的 id(core 的 brain 层不另铸),本仓自己的代码就是这么假设的:core 把 tool-result 引用命名成
43
- * `tr_<sessionId>_<toolCallId>`( `plugins/tool-result-store-sql.ts` 的 `deleteBySession` 顶注),
43
+ * `tr_<sessionId>~<toolCallId>~<contentSeg>`(core 5.26.0 #119 起的四段单射形;见 `plugins/tool-result-store-sql.ts`
44
+ * 的 `deleteBySession` 顶注),
44
45
  * `deriveAskId` 也把 `sourceTaskId`/`runId`/`legKey` 一并入哈希。一张不分格的表会**跨任务/跨租户串味**:
45
46
  * 低熵 id(本地/自建模型服务常见的 `call_1` 之类)下,A 任务的治理标会被 B 任务的普通 ask 读走(假出处),
46
47
  * 或被 B 的一次治理放行反手清掉(丢真出处)。
@@ -13,17 +13,37 @@
13
13
  * (`gate_not_tool_approval`/`gate_not_resumable`/`wake.gate_pending` 的指路句)。三处文案与本表若漂移,
14
14
  * test/session-active-conflict-materials.test.ts 的分门用例会红。认不出的 kind ⇒ null(诚实缺席,不铸假门)。
15
15
  */
16
+ import { type Autonomy } from "../runtime-governance.js";
17
+ /** #220 归因的部署侧入参(两个治理旋钮;判据属主见 `governanceMandatesShellGateAlways`)。 */
18
+ export interface GovernancePosture {
19
+ autonomy?: Autonomy;
20
+ manualModeShellGate?: "always" | "classify";
21
+ }
16
22
  /** 与 runs.ts/tasks.ts 三个 409 位共享的旧文案(byte-frozen:api-error-text-freeze 门认这句)。 */
17
23
  export declare const ACTIVE_RUN_CONFLICT_BASE_TEXT = "session already has an active run \u2014 POST /v1/runs/{activeTaskId}/cancel stops it (same-instance interactive runs abort immediately)";
24
+ /**
25
+ * 409 体(与 SSE done 帧 result 共用)里的**待批门材料**。
26
+ *
27
+ * `kind`/`decidePath` = 出路(去哪决议);#220 追加的 `governanceForced` = **出身**(这道门是谁下的)——
28
+ * [3513] T3「park 后待批通道事件」的读者此前只知道「有一道 X 型门」,不知道它是运维治理层强制的还是
29
+ * 别的来源,于是「我都开了 bypassPermissions 为什么还在问」这个 UX 缺口在 park 腿上原样复现
30
+ * ([3031]#1/[3038] 在活卡腿上的实证)。
31
+ *
32
+ * 🔴 **additive + 只在为真时在场**:与活卡帧 `ToolApprovalFrame.governanceForced` **同一条纪律**——
33
+ * 缺席 ≠「这不是治理门」,而是「没有治理来源的证据」(判据够不着的形也落在缺席里,见装配处的注)。
34
+ */
35
+ export interface PendingGateMaterial {
36
+ kind: string;
37
+ decidePath: string;
38
+ /** 见 {@link PendingGateMaterial} 顶注:`true` 才在场,恒不写 `false`。 */
39
+ governanceForced?: true;
40
+ }
18
41
  export interface ActiveRunConflictBody {
19
42
  error: string;
20
43
  errorCode: "conflict.session_active_run";
21
44
  activeTaskId: string | null;
22
45
  activeTaskStatus?: string;
23
- pendingGate?: {
24
- kind: string;
25
- decidePath: string;
26
- };
46
+ pendingGate?: PendingGateMaterial;
27
47
  }
28
48
  /** gate.kind → 它的那一个 resume 入口(sessionId/taskId 寻址,无秘密)。 */
29
49
  export declare function resumeEntryForGate(kind: string, ids: {
@@ -43,10 +63,7 @@ export interface ActiveRunConflictDoneResult {
43
63
  errorMessage: string;
44
64
  activeTaskId: string | null;
45
65
  activeTaskStatus?: string;
46
- pendingGate?: {
47
- kind: string;
48
- decidePath: string;
49
- };
66
+ pendingGate?: PendingGateMaterial;
50
67
  }
51
68
  export declare function toDoneFrameResult(body: ActiveRunConflictBody): ActiveRunConflictDoneResult;
52
69
  interface ConflictProbeDeps<TToken> {
@@ -58,12 +75,20 @@ interface ConflictProbeDeps<TToken> {
58
75
  checkpointStore?: {
59
76
  peekPendingScope?: (sessionId: string) => Promise<string | null | undefined>;
60
77
  findPendingTokenBySession?: (sessionId: string, scope?: string) => Promise<TToken | null | undefined>;
78
+ /** 结构形(不 import core 的 `Checkpoint`):只声明本模块**真读**的两格。`riskDescriptor` 是
79
+ * core 在 mint 时挂到升级门上的判读元数据(display/triage only),`shellGateDoctrine` 是 2026-08-05
80
+ * 裁定加的取证格 —— 见下方 `governanceOriginOf` 的推导论证。 */
61
81
  get?: (token: TToken) => Promise<{
62
82
  gate?: {
63
83
  kind?: string;
84
+ riskDescriptor?: {
85
+ shellGateDoctrine?: "classify" | "always";
86
+ };
64
87
  };
65
88
  } | null | undefined>;
66
89
  } | undefined;
90
+ /** #220:出身归因的部署侧一半(缺席 ⇒ 归不出治理出身 ⇒ 键缺席 —— 与 best-effort 同方向)。 */
91
+ governance?: GovernancePosture | undefined;
67
92
  }
68
93
  export declare function buildActiveRunConflict<TToken>(deps: ConflictProbeDeps<TToken>, sessionId: string, activeTaskId: string | null | undefined): Promise<ActiveRunConflictBody>;
69
94
  export {};