@sema-agent/server 7.103.2 → 7.104.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 (175) hide show
  1. package/MIGRATION.md +346 -2
  2. package/README.md +6 -1
  3. package/README.zh-CN.md +5 -1
  4. package/USAGE.md +271 -16
  5. package/deploy/sema-up/README.md +57 -7
  6. package/deploy/sema-up/cluster-up.sh +18 -4
  7. package/deploy/sema-up/kube-up.sh +16 -0
  8. package/deploy/sema-up/lib/dataroot.sh +142 -0
  9. package/deploy/sema-up/lib/flagshape.sh +69 -0
  10. package/deploy/sema-up/lib/lanes.py +14 -4
  11. package/deploy/sema-up/lib/shell-access.sh +41 -0
  12. package/deploy/sema-up/preflight.sh +14 -1
  13. package/deploy/sema-up/sema-up.sh +134 -37
  14. package/deploy/sema-up/smoke.sh +5 -0
  15. package/dist/auto-mode-face.js +3 -4
  16. package/dist/boot/budget-tracing.js +1 -1
  17. package/dist/boot/config-center.d.ts +6 -2
  18. package/dist/boot/config-center.js +5 -59
  19. package/dist/boot/execution-env.d.ts +11 -0
  20. package/dist/boot/execution-env.js +14 -0
  21. package/dist/boot/execution-lane.d.ts +25 -0
  22. package/dist/boot/execution-lane.js +95 -0
  23. package/dist/boot/memory-consolidation.d.ts +185 -9
  24. package/dist/boot/memory-consolidation.js +192 -32
  25. package/dist/boot/resolve-spec.d.ts +2 -0
  26. package/dist/boot/resolve-spec.js +28 -24
  27. package/dist/boot/runner-deps.d.ts +6 -2
  28. package/dist/boot/runner-deps.js +4 -0
  29. package/dist/boot/session-faces.d.ts +5 -0
  30. package/dist/boot/session-faces.js +18 -2
  31. package/dist/boot/session-incarnation-seat.d.ts +41 -0
  32. package/dist/boot/session-incarnation-seat.js +69 -0
  33. package/dist/boot/stage-01-config.js +2 -2
  34. package/dist/boot/stage-02-registry-pull.js +3 -1
  35. package/dist/boot/stage-04-stores.d.ts +2 -0
  36. package/dist/boot/stage-04-stores.js +3 -1
  37. package/dist/boot/stage-05-execution-env.d.ts +4 -0
  38. package/dist/boot/stage-05-execution-env.js +2 -2
  39. package/dist/boot/stage-06-runners.d.ts +4 -1
  40. package/dist/boot/stage-06-runners.js +7 -14
  41. package/dist/boot/stage-07-capability-layer.d.ts +4 -1
  42. package/dist/boot/stage-08-reapers.d.ts +4 -1
  43. package/dist/boot/stage-09-leader.d.ts +6 -1
  44. package/dist/boot/stage-09-leader.js +2 -1
  45. package/dist/boot/stage-10-http-server.d.ts +6 -1
  46. package/dist/boot/stage-10-http-server.js +4 -1
  47. package/dist/brain.d.ts +1 -1
  48. package/dist/config-catalog.d.ts +16 -1
  49. package/dist/config-catalog.js +9 -9
  50. package/dist/config-center/restart-signal.d.ts +4 -4
  51. package/dist/config-invariants.d.ts +7 -1
  52. package/dist/config-invariants.js +37 -6
  53. package/dist/config-provider.d.ts +42 -10
  54. package/dist/config-provider.js +18 -16
  55. package/dist/config-types.d.ts +37 -16
  56. package/dist/config-types.js +1 -0
  57. package/dist/config.d.ts +13 -28
  58. package/dist/config.js +53 -70
  59. package/dist/env-facts.d.ts +13 -3
  60. package/dist/env-facts.js +15 -3
  61. package/dist/execution-lane-caps.d.ts +15 -0
  62. package/dist/execution-lane-caps.js +8 -0
  63. package/dist/file-history-capture.d.ts +42 -35
  64. package/dist/file-history-capture.js +15 -10
  65. package/dist/http/admission.d.ts +11 -4
  66. package/dist/http/admission.js +5 -1
  67. package/dist/http/dispatch.js +3 -3
  68. package/dist/http/principal-gate.d.ts +22 -0
  69. package/dist/http/principal-gate.js +20 -0
  70. package/dist/http/resume-legs.js +9 -5
  71. package/dist/http/routes/agents-roster.d.ts +7 -0
  72. package/dist/http/routes/agents-roster.js +6 -2
  73. package/dist/http/routes/approvals-assistant.d.ts +48 -19
  74. package/dist/http/routes/approvals-assistant.js +117 -50
  75. package/dist/http/routes/capabilities.js +3 -2
  76. package/dist/http/routes/config-catalog.js +1 -0
  77. package/dist/http/routes/memory-consolidation.d.ts +3 -2
  78. package/dist/http/routes/memory-policy.js +3 -3
  79. package/dist/http/routes/rules.js +2 -2
  80. package/dist/http/routes/runs.js +27 -2
  81. package/dist/http/routes/session-sync.js +8 -14
  82. package/dist/http/routes/sessions-list.js +3 -2
  83. package/dist/http/routes/sessions.js +44 -23
  84. package/dist/http/routes/shared-memory.js +2 -2
  85. package/dist/http/routes/tasks.js +22 -19
  86. package/dist/http/routes/trace-usage.js +4 -4
  87. package/dist/http/send.d.ts +12 -0
  88. package/dist/http/send.js +12 -1
  89. package/dist/http/server.d.ts +21 -4
  90. package/dist/http/server.js +7 -4
  91. package/dist/http/sync-submit-body.d.ts +43 -0
  92. package/dist/http/sync-submit-body.js +4 -0
  93. package/dist/http/wire-types.d.ts +23 -5
  94. package/dist/index.d.ts +1 -0
  95. package/dist/index.js +1 -0
  96. package/dist/leader/merge.d.ts +8 -3
  97. package/dist/memory-incident-sink.js +2 -1
  98. package/dist/model-select.d.ts +9 -0
  99. package/dist/model-select.js +5 -1
  100. package/dist/observability/corrupt-read-seat.d.ts +151 -16
  101. package/dist/observability/corrupt-read-seat.js +158 -15
  102. package/dist/observability/fail-open.d.ts +12 -0
  103. package/dist/observability/fail-open.js +12 -0
  104. package/dist/observability/run-terminal.d.ts +15 -1
  105. package/dist/observability/run-terminal.js +4 -0
  106. package/dist/parked-decide.d.ts +8 -3
  107. package/dist/parked-decide.js +56 -20
  108. package/dist/parked-revive-surface.d.ts +14 -7
  109. package/dist/parked-revive-surface.js +5 -1
  110. package/dist/plugins/approval-ask-store-file.d.ts +4 -1
  111. package/dist/plugins/approval-ask-store-file.js +9 -12
  112. package/dist/plugins/approval-ask-store-sql.d.ts +4 -0
  113. package/dist/plugins/approval-ask-store-sql.js +4 -1
  114. package/dist/plugins/approval-exemption-store.d.ts +21 -1
  115. package/dist/plugins/approval-exemption-store.js +36 -7
  116. package/dist/plugins/approval-nonce-store.d.ts +16 -2
  117. package/dist/plugins/approval-nonce-store.js +17 -7
  118. package/dist/plugins/caching-session-store.d.ts +5 -0
  119. package/dist/plugins/caching-session-store.js +3 -0
  120. package/dist/plugins/file-resume-anchor-store.d.ts +7 -2
  121. package/dist/plugins/file-resume-anchor-store.js +19 -5
  122. package/dist/plugins/file-run-store.d.ts +21 -8
  123. package/dist/plugins/file-run-store.js +60 -38
  124. package/dist/plugins/local-session-store.d.ts +22 -0
  125. package/dist/plugins/local-session-store.js +74 -11
  126. package/dist/plugins/memory-run-store.d.ts +7 -5
  127. package/dist/plugins/memory-run-store.js +11 -4
  128. package/dist/plugins/one-time-migrations.d.ts +38 -1
  129. package/dist/plugins/one-time-migrations.js +143 -15
  130. package/dist/plugins/permission-rule-store-file.d.ts +9 -2
  131. package/dist/plugins/permission-rule-store-file.js +37 -51
  132. package/dist/plugins/pg-session-storage.d.ts +3 -0
  133. package/dist/plugins/pg-session-storage.js +19 -10
  134. package/dist/plugins/run-store-sql.d.ts +12 -4
  135. package/dist/plugins/run-store-sql.js +15 -13
  136. package/dist/plugins/send-file-ledger.d.ts +5 -1
  137. package/dist/plugins/send-file-ledger.js +11 -6
  138. package/dist/plugins/session-event-payload.d.ts +14 -4
  139. package/dist/plugins/session-event-payload.js +5 -2
  140. package/dist/plugins/store-backend.d.ts +7 -3
  141. package/dist/plugins/store-backend.js +10 -6
  142. package/dist/plugins/store-contracts.d.ts +7 -2
  143. package/dist/plugins/tidb-pool.js +8 -2
  144. package/dist/plugins/tidb-session-store.d.ts +6 -1
  145. package/dist/plugins/tidb-session-store.js +17 -10
  146. package/dist/project-identity.d.ts +4 -6
  147. package/dist/request-key-closure.js +1 -1
  148. package/dist/request-value-closure.d.ts +72 -3
  149. package/dist/request-value-closure.js +12 -4
  150. package/dist/roster-bound-http-status.d.ts +6 -0
  151. package/dist/roster-bound-http-status.js +6 -0
  152. package/dist/run-local.js +22 -27
  153. package/dist/runs.d.ts +22 -2
  154. package/dist/runs.js +29 -7
  155. package/dist/sealed-key.d.ts +31 -4
  156. package/dist/sealed-key.js +9 -0
  157. package/dist/security.d.ts +5 -1
  158. package/dist/session-children-retire.d.ts +92 -0
  159. package/dist/session-children-retire.js +85 -0
  160. package/dist/session-error-wire.d.ts +58 -0
  161. package/dist/session-error-wire.js +38 -0
  162. package/dist/session-incarnation.d.ts +61 -0
  163. package/dist/session-incarnation.js +43 -0
  164. package/dist/session-records-skipped-notice.d.ts +20 -0
  165. package/dist/session-records-skipped-notice.js +27 -0
  166. package/dist/tool-approval.d.ts +65 -0
  167. package/dist/tool-approval.js +36 -9
  168. package/dist/trace/core-keyset-guard.d.ts +8 -7
  169. package/dist/trace/engine-notice-wire.d.ts +3 -1
  170. package/dist/trace/engine-notice-wire.js +2 -0
  171. package/dist/trace/project.d.ts +1 -1
  172. package/dist/trace/project.js +9 -0
  173. package/dist/trace/wire-projection-faces.d.ts +10 -3
  174. package/dist/trace/wire-projection-faces.js +8 -2
  175. package/package.json +2 -2
@@ -1,35 +1,41 @@
1
1
  /**
2
- * S-589 —— 文件历史的**装配单点**与能力词 `fileHistoryCapture` 的闭集(叶模块:只 type-import store-backend)。
2
+ * S-589 → S-724 —— 文件历史的**装配单点**与能力词 `fileHistoryCapture` 的闭集(叶模块:只 type-import store-backend 与 core 型)。
3
3
  *
4
- * ## 病(修前)
5
- * `boot/budget-tracing.ts` 只要后端有文件历史店就无条件交给 core(`RunnerDeps.fileHistoryStore`)。core 的首次触碰
6
- * 捕获是**进程级**的:接了店就恒捕获,没有任何单会话 / 单 run 的杠杆;全仓相关旋钮只有保留条数
7
- * `FILE_HISTORY_RETENTION_KEEP`。于是用户在客户端关掉「文件检查点」关掉的只是提示,文件字节照旧进
8
- * `file_history_*` —— 隐私 / 留存轴上的用户面语义谎言。
4
+ * ## 病史
5
+ * S-589(7.97.0):`boot/budget-tracing.ts` 只要后端有文件历史店就无条件交给 core(`RunnerDeps.fileHistoryStore`)。core 的首次
6
+ * 触碰捕获当时是**进程级**的:接了店就恒捕获,没有任何单会话 / 单 run 的杠杆。于是用户在客户端关掉「文件检查点」关掉的只是
7
+ * 提示,文件字节照旧进 `file_history_*`。S-589 给了部署旋钮 `FILE_HISTORY_CAPTURE`,而 core 那时只有一种写法表达「不捕获」——
8
+ * **不接店** ⇒ 旋钮关 = 摘店,代价是已存历史也一起读不了(restore / rewind / 2c sync 一并关)。
9
+ * S-724(7.104.0):core 7.31.0 收了两件 —— 部署的**捕获位** `RunnerDeps.fileHistoryCapture: "off"`(店照接,只停新捕获,
10
+ * 已存历史照常服务 `restoreFiles` / `rewindFilesTo`)与请求级关断词 `TaskSpec.fileHistory: "disabled"`。旋钮改落 core 的同名座:
11
+ * **名字说的是「捕获」,就只关捕获** —— 一条旋钮一个意思。
9
12
  *
10
- * ## 一处判定,两张脸(按动词类别切分,不是特例)
11
- * 「这台部署有没有文件历史店」只在 {@link assembleFileHistory} 判一次,部署旋钮 `FILE_HISTORY_CAPTURE` 在这一处合取:
12
- * · **捕获脸** `store` = 后端 ∧ 旋钮。喂 core 的首次触碰捕获(`RunnerDeps.fileHistoryStore`)、rewind 族能力位
13
- * (`rewindFiles` / `restoreFiles` / `rewindFilesTo` / `sessionSync`)与本模块的能力词、2c sync 路由族。
14
- * · **擦除脸** `erasure` = 后端在即在,类型上**只有删除类动词**。喂 E21 在线删除(`boot/session-faces.ts`)与
15
- * 孤儿 blob GC(`boot/reapers.ts`)。
16
- * 规则一句话:**旋钮只管写入,从不管擦除**。关掉捕获的部署照样要能删掉已经捕获的文件字节 —— 否则翻旋钮之后
17
- * `DELETE /v1/sessions/:id` 会回成功却把存量文件内容留在库里,而留存车道(`plugins/retention-store-sql.ts` 的
18
- * 原生 SQL 删六张 `file_history_*` 表,本来就不看店在不在)与在线删除从此不再等价(E21 两个执行体分家)。
19
- * ⚠️ 关掉 ≠ 删掉:翻旋钮**不**做任何批量删除。已捕获的历史照既有的会话删除 / 留存规则清理;在旋钮关着的
20
- * 部署上它也不会再被读用(rewind / restore / sync 都随捕获脸一起缺席)。
13
+ * ## 两件事,两个单点
14
+ * · 「这台部署有没有文件历史店」只在 {@link assembleFileHistory} 判一次(后端在即在,**与旋钮无关**),产出两张脸:
15
+ * **店** `store` 喂 core 的 `RunnerDeps.fileHistoryStore`(还原族)、rewind 族能力位(`rewindFiles` / `restoreFiles` /
16
+ * `rewindFilesTo` / `sessionSync`)与 2c sync 路由族;**擦除脸** `erasure` = 店的**只有删除类动词**的收窄视图,喂 E21 在线删除
17
+ * (`boot/session-faces.ts`)与孤儿 blob GC(`boot/reapers.ts`)。旋钮从不管擦除:关掉捕获的部署照样要能删掉已捕获的文件字节。
18
+ * · 「捕不捕」只在 {@link fileHistoryCaptureOf} 判一次(纯函数,输入 = 店在不在 + 旋钮)。两个消费点喂它同一组输入:
19
+ * core 的部署捕获位({@link coreFileHistoryCaptureSeat},`boot/runner-deps.ts`)与能力词(`http/routes/capabilities.ts`)——
20
+ * core 收到的与能力面说的不可能各说各话。捕获位 `"off"` ⟺ 店在 ∧ 旋钮关;**没有店的部署形不下发**该位:core 对
21
+ * 「off 但没店」拒 prepare(`config.invalid_file_history_capture`,「nothing to switch off」),而旋钮的意思「不捕获」在没店的
22
+ * 部署上本来就成立 —— 旋钮照样无条件生效,只是那一形无事可关(选这一臂而不是拒启的理由见 CHANGELOG 7.104.0 S-724 条:
23
+ * 拒启会把「形合法、意思成立」的配置判成错,且会与 auto 后端的降级臂耦合)。
24
+ * ⚠️ 关掉 ≠ 删掉:翻旋钮**不**做任何批量删除;已捕获的历史照旧被 restore / rewind / sync 读用,按既有的会话删除 /
25
+ * 留存规则清理。要让已存历史也不可用,部署今天的出路只有逐会话 `DELETE /v1/sessions/:id` 与留存清扫(见 USAGE)。
21
26
  *
22
- * ## 能力词为什么是闭集两词、没有 `on-optional`
23
- * cli 要的是「`/config` 那个开关该不该出现」。present-iff 布尔在今天的**每一台**部署上都只能回答「关不掉」
24
- * (core 的捕获是进程级的)⇒ 出一格三词闭集的前两词:`off`(这台部署根本不捕获,也没有 rewind)与
25
- * `on-always`(捕获中,且无法按会话关闭)。第三词 `on-optional`(「本会话可以不捕获」)要等 core 收一个
26
- * 单会话 / 单 run 的不捕获位,**在那之前刻意留空**:预先声明一个结构上不可达的词,等于给客户端一个永远等不到的分支。
27
+ * ## 能力词为什么是 `off` / `on-optional` 两词
28
+ * cli 要的是「`/config` 那个开关该不该出现」。三词闭集的设计(S-589)是 `off` / `on-always` / `on-optional`;S-589 时第三词
29
+ * 结构上不可达(没有单 run 不捕获位)。S-724 起请求词 `fileHistory: "disabled"` 受理 ⇒ 捕获中的部署恒可按 run 关掉 ⇒
30
+ * `on-optional` 成真,而 `on-always`(「捕获中且关不掉」)**结构上不可达** —— 预先声明一个到不了的词等于给客户端一个
31
+ * 永远等不到的分支,所以删词不留(7.97.0–7.103.x 的 worker 仍会发它,消费端读法在契约附录 B)。
27
32
  */
33
+ import type { RunnerDeps } from "@sema-agent/core";
28
34
  import type { ServiceFileHistoryStore, StoreBackend } from "./plugins/store-backend.js";
29
35
  /** 能力词闭集的单点属主(`Record` 锁步:联合加词而这里不补 ⇒ 编译红;词表数组由它派生,不手抄)。 */
30
36
  declare const FILE_HISTORY_CAPTURE_WORD_SET: {
31
37
  readonly off: true;
32
- readonly "on-always": true;
38
+ readonly "on-optional": true;
33
39
  };
34
40
  /** `GET /v1/capabilities` 的 `fileHistoryCapture` 词。 */
35
41
  export type FileHistoryCapture = keyof typeof FILE_HISTORY_CAPTURE_WORD_SET;
@@ -41,25 +47,26 @@ export declare const FILE_HISTORY_CAPTURE_WORDS: readonly FileHistoryCapture[];
41
47
  export type FileHistoryErasureFace = Pick<ServiceFileHistoryStore, "reap" | "deleteBySession" | "sweepOrphanBlobs">;
42
48
  /** 装配结果:两张脸。 */
43
49
  export interface FileHistoryAssembly {
44
- /** 捕获脸:在场 ⟺ 后端在 ∧ `FILE_HISTORY_CAPTURE`。 */
50
+ /** 店:在场 ⟺ 后端在(与旋钮无关)。 */
45
51
  readonly store: ServiceFileHistoryStore | undefined;
46
- /** 擦除脸:在场 ⟺ 后端在(与旋钮无关)。 */
52
+ /** 擦除脸:在场 ⟺ 后端在(与旋钮无关;是 {@link store} 的删除类动词视图)。 */
47
53
  readonly erasure: FileHistoryErasureFace | undefined;
48
54
  }
55
+ /** 装配:「这台部署有没有文件历史店」的唯一判定。旋钮不在这里 —— 它只关捕获,不摘店(S-724)。 */
56
+ export declare function assembleFileHistory(backend: StoreBackend | undefined): FileHistoryAssembly;
49
57
  /**
50
- * **唯一**的装配判定。`capture` = 部署旋钮 `FILE_HISTORY_CAPTURE`(`config.fileHistoryCapture`)。
58
+ * 「捕不捕」的**唯一**判定(纯函数):能力词。`store` = 装配出来的店(在不在),`knob` = 部署旋钮 `FILE_HISTORY_CAPTURE`
59
+ * (`config.fileHistoryCapture`)。从**装配结果**派生,不照旋钮字面回声:后端本来就没有这只店的部署同样是 `off`。
51
60
  *
52
- * 形照 `execution-lane-caps.ts` 的穷举 switch(无 default 臂):意图词加一个(将来的 `on-optional`)而这里不加臂
53
- * ⇒ 编译红 —— 加臂的那一刻就是决定「这一词下捕获脸接不接、core 那一位单会话不捕获位怎么递」的地方。
61
+ * 与 `rewindFiles` 分叉(S-589 当时写下的分叉条件,自本版成真):本词答「这台部署捕不捕、能不能按 run 关掉」,
62
+ * `rewindFiles` 答「这台部署能不能还原」—— 旋钮关的部署本词 `off` 而 `rewindFiles` 仍为真。
54
63
  */
55
- export declare function assembleFileHistory(backend: StoreBackend | undefined, capture: boolean): FileHistoryAssembly;
64
+ export declare function fileHistoryCaptureOf(store: object | undefined, knob: boolean): FileHistoryCapture;
56
65
  /**
57
- * 能力词 = 捕获脸在不在(读**装配结果**,不照旋钮字面回声:后端本来就没有这只店的部署同样是 `off`)。
58
- *
59
- * 与 `rewindFiles` 读的是**同一个判定**(`deps.fileHistoryStore` 在场),今天两位同构。分叉条件(写在这里免得
60
- * 将来有人把两位当成一回事):core 收了单会话不捕获位之后,本词在捕获脸在场时会变成 `on-optional`,而
61
- * `rewindFiles` 仍是 true —— 它回答的是「这台部署能不能还原」,本词回答的是「捕获关不关得掉」。
66
+ * core 的部署捕获位 `RunnerDeps.fileHistoryCapture`,由 {@link fileHistoryCaptureOf} 派生(同一组输入、同一只判定)。
67
+ * 形照 `execution-lane-caps.ts` 的穷举 switch(无 default 臂):能力词加一个而这里不加臂 ⇒ 编译红 —— 加臂的那一刻就是
68
+ * 决定「这一词下 core 的捕获位怎么递」的地方。没有店 ⇒ 恒 `undefined`(core 对「off 但没店」拒 prepare)。
62
69
  */
63
- export declare function fileHistoryCaptureOf(captureFace: object | undefined): FileHistoryCapture;
70
+ export declare function coreFileHistoryCaptureSeat(store: object | undefined, knob: boolean): RunnerDeps["fileHistoryCapture"];
64
71
  export {};
65
72
  //# sourceMappingURL=file-history-capture.d.ts.map
@@ -1,21 +1,26 @@
1
1
  const FILE_HISTORY_CAPTURE_WORD_SET = {
2
2
  off: true,
3
- "on-always": true,
3
+ "on-optional": true,
4
4
  };
5
5
  export const FILE_HISTORY_CAPTURE_WORDS = Object.keys(FILE_HISTORY_CAPTURE_WORD_SET);
6
- export function assembleFileHistory(backend, capture) {
6
+ export function assembleFileHistory(backend) {
7
7
  if (backend === undefined)
8
8
  return { store: undefined, erasure: undefined };
9
9
  const physical = backend.fileHistory();
10
- const intent = capture ? "on-always" : "off";
11
- switch (intent) {
10
+ return { store: physical, erasure: physical };
11
+ }
12
+ export function fileHistoryCaptureOf(store, knob) {
13
+ return store !== undefined && knob ? "on-optional" : "off";
14
+ }
15
+ export function coreFileHistoryCaptureSeat(store, knob) {
16
+ if (store === undefined)
17
+ return undefined;
18
+ const word = fileHistoryCaptureOf(store, knob);
19
+ switch (word) {
12
20
  case "off":
13
- return { store: undefined, erasure: physical };
14
- case "on-always":
15
- return { store: physical, erasure: physical };
21
+ return "off";
22
+ case "on-optional":
23
+ return undefined;
16
24
  }
17
25
  }
18
- export function fileHistoryCaptureOf(captureFace) {
19
- return captureFace === undefined ? "off" : "on-always";
20
- }
21
26
  //# sourceMappingURL=file-history-capture.js.map
@@ -29,11 +29,18 @@ export declare const MAX_OUTPUT_SCHEMA_CHARS = 32768;
29
29
  * · `skill.description must be a string of at most 1024 characters`(描述长度帽);
30
30
  * · `skill.name must be a non-empty string of at most 64 characters` 里的 **长度**那一半。
31
31
  *
32
- * 退役的判据是**上游两侧都没有同形门**:CC 2.1.259 的技能清单渲染(`cli259.js:23195` 的预算算法)
33
- * 在超预算时把**描述裁短**、再不够就退成 name-only,**从不**因为条数多或描述长而拒掉一条技能;
34
- * 它的 `Ir()` 名字校验也只判非空 / 无配对代理 / 无首尾空白 / 无括号逗号控制字符,没有长度帽。
32
+ * 退役的判据是**上游两侧都没有同形门**:CC 的技能清单渲染**从不**因为条数多或描述长而拒掉一条技能 —— 超预算时每一条
33
+ * 只有两种结局:全文,或退成 name-only(`- name`);bundled 技能恒为全文,其余按优先级在剩余预算内贪心升回全文
34
+ * (cli280.js:217940-217942 `JBt`;预算 = `max(1, floor((窗口 ?? 200000) × bytesPerToken(缺省 4)× fraction))`,
35
+ * env `SLASH_COMMAND_TOOL_CHAR_BUDGET` 优先,cli280.js:215920 `nUe`)。单条描述超过 `skillListingMaxDescChars`(默认 1536)
36
+ * 截成前 1535 字符加「…」,这一截与预算无关、恒生效(cli280.js:217940 `NFr`)。旧引的 `cli259.js:23195` 是 `skillListingBudgetFraction` 的 settings describe 文案,
37
+ * 不是算法本体。CC 的技能名校验(2.1.259 `Ir()` / 2.1.280 `Rs()`,cli280.js:236381;校验的是 Agent SDK `skills` 选项里的
38
+ * 技能名)判非空、无未配对代理、无首尾空白、无括号逗号控制字符、不是 `'*'`、无通配后缀、不以 `/` 开头、无连续反斜杠、
39
+ * 不以反斜杠结尾 —— **没有长度这一条**;CC 的名字长度界在别的面上且都不是 64:forked skill 作用域持久化 256
40
+ * (cli280.js:214671 `BV`)、transcript 载入(resume)时 `skill_listing.names` 每条 ≤ 512 / 至多 4096 条,不合格整条附件丢弃
41
+ * (cli280.js:217865)。
35
42
  * 引擎侧同理:唯一整拒的是 1 MiB 的单技能加载门。于是那三条帽是「装了 11 个技能就 400」这种
36
- * 纯本仓面的伤——收益是零(预算由渲染侧按字节兜),代价是用户装不进自己的技能库。
43
+ * 纯本仓面的伤——收益是零(预算由渲染侧兜),代价是用户装不进自己的技能库。
37
44
  *
38
45
  * **补偿**(三问的第三问):`settings.skillListingBudgetFraction` / `settings.skillListingMaxDescChars`
39
46
  * 两个旋钮直达引擎的渲染预算(per-turn 成本的真属主)。
@@ -4,7 +4,7 @@ import { modelUnavailableMessage } from "../model-entry-refusal.js";
4
4
  import { HttpError } from "../security.js";
5
5
  import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_PERMISSION_RULES, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS, SETTINGS_PERMISSION_MODE_WORDS, isSettingsPermissionMode, parseTaskSettings, skillListingBudgetFractionOk, skillListingMaxDescOk, taskSettingsKeyIssue } from "../task-settings.js";
6
6
  import { taskBodyKeyIssue } from "../request-key-closure.js";
7
- import { singleMemberOffFieldIssue } from "../request-value-closure.js";
7
+ import { FILE_HISTORY_DISABLED_NO_RESTORE, fileHistoryRestoreConflict, singleMemberOffFieldIssue } from "../request-value-closure.js";
8
8
  import { selfOrchestrationDenial } from "../task-workflow.js";
9
9
  import { centerEntitlementSourceWired } from "../runtime-caps-resolver.js";
10
10
  import { parseHooksConfig } from "../hooks/hook-runner.js";
@@ -333,6 +333,10 @@ export function createAdmission(ctx) {
333
333
  return null;
334
334
  }
335
335
  }
336
+ if (fileHistoryRestoreConflict(body)) {
337
+ sendError(res, 400, "request.field_conflict", FILE_HISTORY_DISABLED_NO_RESTORE);
338
+ return null;
339
+ }
336
340
  if (body.requireExistingSession !== undefined && typeof body.requireExistingSession !== "boolean") {
337
341
  sendError(res, 400, "request.field_invalid", "requireExistingSession must be a boolean");
338
342
  return null;
@@ -6,7 +6,7 @@ import { ROUTE_METHOD_ORDER } from "./route-ctx.js";
6
6
  import { PRE_CREDENTIAL_ROUTES, POST_CREDENTIAL_ROUTES, routeLabel, isBillableSubmitPath, isCredentialGatedRewrite, isMethodDispatchedSubmitPath, lookupRoute, isProbePath } from "./route-table.js";
7
7
  import { sendJson, sendError, isSseResponse } from "./send.js";
8
8
  import { readinessOf, healthReadinessKeys, sendReadyz, billableRefusalOf, sendBillableRefusal } from "./readiness.js";
9
- import { authorized, systemFor, gatedPrincipal, sendPrincipalRefusal } from "./principal-gate.js";
9
+ import { authorized, systemFor, gatedPrincipal, sendPrincipalRefusal, sendUnauthorized } from "./principal-gate.js";
10
10
  const PROCESS_STARTED_AT_MS = Date.now() - Math.round(process.uptime() * 1000);
11
11
  export function hasServiceAuth(config) {
12
12
  return Boolean(config.authToken) || Object.keys(config.authTokens ?? {}).length > 0;
@@ -175,7 +175,7 @@ export function createDispatch(ctx) {
175
175
  const devOpen = !anyT && !mt && !deps.config.requirePrincipal;
176
176
  const ok = devOpen || (anyT && systemFor(req, deps.config) !== undefined) || (!!mt && authorized(req, mt));
177
177
  if (!ok) {
178
- sendError(res, 401, "auth.unauthorized", "unauthorized");
178
+ sendUnauthorized(res, "operator");
179
179
  return;
180
180
  }
181
181
  if (url === "/metrics/summary") {
@@ -215,7 +215,7 @@ export function createDispatch(ctx) {
215
215
  source = "sso";
216
216
  }
217
217
  else {
218
- sendError(res, 401, "auth.unauthorized", "unauthorized");
218
+ sendUnauthorized(res, "credential");
219
219
  return;
220
220
  }
221
221
  }
@@ -44,6 +44,28 @@ export declare function principalTokenPresentedButUnverified(req: IncomingMessag
44
44
  * 调用链上的**证明三件套**,与本门(身份面)是两道门。
45
45
  */
46
46
  export declare function sendPrincipalRefusal(res: ServerResponse, req: IncomingMessage, config: ServiceConfig): void;
47
+ /**
48
+ * 🔴 **`401 auth.unauthorized` 的唯一铸点**(S-850,7.104.0;test [ref] QA 7a C15)。
49
+ *
50
+ * 病:十六处路由(9 个文件)各自手抄同一个 401 —— 码 `auth.unauthorized`、message 就一个词 `unauthorized`。照 sema-up 部署、壳接上
51
+ * 引擎后,第一句就撞上它,不说要哪份凭据、放哪、去哪拿。
52
+ *
53
+ * 规则:码不变;message 按**这道门收什么**分三面(闭集 {@link UnauthorizedFace},穷举 switch —— 加一面不接句子 = 编译红),
54
+ * 每面一句模块级常量(冻结门按常量解引用逐字钉)。三句都**不含任何值**(令牌 / 头名 / 路径实参一概不回显),且**不区分**
55
+ * 「没带」与「带错」—— 同一道门上两者逐字同体,区分就是探测面。
56
+ * · `credential` —— 全局凭据门(`dispatch.ts`):部署配了 service token,请求没带或带的不认(registry 登录令牌验不过同属此面);
57
+ * · `operator` —— 只给运维凭据开的口(`/metrics*` 与 `GET /v1/tasks/source-summary`):service token 开得了,指标口另认只读的
58
+ * `METRICS_TOKEN`(它在全局门之前判);
59
+ * · `identity` —— 过了凭据门(或这台部署没配 service token)、但这一口要一个**验过的调用方身份**:属主寻址的读 / 写面、规则面、
60
+ * 记忆面。在配了 service token 的部署上,「带 service token 却没身份」只会落在只认身份的口(规则 / 记忆 / 会话策略)上 ——
61
+ * 这时再叫调用方「去带 service token」是假出路,所以这一面的句子只讲身份。
62
+ * `auth.principal_required` / `principal_unverified`(上面 {@link sendPrincipalRefusal})是另一只码、另一只铸点,不在此列。
63
+ */
64
+ export type UnauthorizedFace = "credential" | "operator" | "identity";
65
+ export declare const UNAUTHORIZED_CREDENTIAL = "unauthorized \u2014 the request carries no credential this deployment accepts: send the deployment's service token as a Bearer token in the Authorization header (its operator set it as SERVICE_AUTH_TOKEN or SERVICE_AUTH_TOKENS; a sema-up deployment keeps SERVICE_AUTH_TOKEN in ~/.sema-up/<project>/.env on the machine that ran sema-up)";
66
+ export declare const UNAUTHORIZED_OPERATOR = "unauthorized \u2014 this endpoint is for the deployment's operators and the request carries no operator credential: send the deployment's service token as a Bearer token in the Authorization header (the metrics endpoints also accept the read-only METRICS_TOKEN)";
67
+ export declare const UNAUTHORIZED_IDENTITY = "unauthorized \u2014 this endpoint acts for a named caller and the request carries no caller identity this deployment verifies: identify the caller the way this deployment does (a registry sign-in token as the Bearer token, a signed principal token on a direct-door deployment, or the principal header set by a trusted front end)";
68
+ export declare function sendUnauthorized(res: ServerResponse, face: UnauthorizedFace): void;
47
69
  /**
48
70
  * 🔴 **「这个调用方读得到这一行 run 吗」的唯一谓词**(S-297 抽取;此前只活在 `server.ts runOwnerOk` 的
49
71
  * 函数体里,而 `runOwnerOk` 把**判断**与**404 怎么发**焊在一起 —— 于是任何需要同一条判断但不想 404 的
@@ -65,6 +65,26 @@ export function sendPrincipalRefusal(res, req, config) {
65
65
  }
66
66
  sendError(res, 401, "auth.principal_required", `missing principal header '${config.principalHeader}'`);
67
67
  }
68
+ export const UNAUTHORIZED_CREDENTIAL = "unauthorized — the request carries no credential this deployment accepts: send the deployment's service token as a Bearer token in the Authorization header (its operator set it as SERVICE_AUTH_TOKEN or SERVICE_AUTH_TOKENS; a sema-up deployment keeps SERVICE_AUTH_TOKEN in ~/.sema-up/<project>/.env on the machine that ran sema-up)";
69
+ export const UNAUTHORIZED_OPERATOR = "unauthorized — this endpoint is for the deployment's operators and the request carries no operator credential: send the deployment's service token as a Bearer token in the Authorization header (the metrics endpoints also accept the read-only METRICS_TOKEN)";
70
+ export const UNAUTHORIZED_IDENTITY = "unauthorized — this endpoint acts for a named caller and the request carries no caller identity this deployment verifies: identify the caller the way this deployment does (a registry sign-in token as the Bearer token, a signed principal token on a direct-door deployment, or the principal header set by a trusted front end)";
71
+ export function sendUnauthorized(res, face) {
72
+ switch (face) {
73
+ case "credential":
74
+ sendError(res, 401, "auth.unauthorized", UNAUTHORIZED_CREDENTIAL);
75
+ return;
76
+ case "operator":
77
+ sendError(res, 401, "auth.unauthorized", UNAUTHORIZED_OPERATOR);
78
+ return;
79
+ case "identity":
80
+ sendError(res, 401, "auth.unauthorized", UNAUTHORIZED_IDENTITY);
81
+ return;
82
+ default: {
83
+ const unknownFace = face;
84
+ throw new Error(`sendUnauthorized: unknown face ${String(unknownFace)}`);
85
+ }
86
+ }
87
+ }
68
88
  export function runOwnerReadable(principal, owner, config) {
69
89
  if (owner === null)
70
90
  return true;
@@ -2,6 +2,9 @@ import { redactErrorMessage, redactHead } from "../observability/run-terminal.js
2
2
  import { createHash } from "node:crypto";
3
3
  import { uuidv7, resumeWithVerification, CheckpointError, defaultTaskRegistry, taskNotificationDedupKey, validatePendingSteer, GLOBAL_USAGE_KEY, PAUSE_REGISTRY } from "@sema-agent/core";
4
4
  import { decideParkedAgent, denyWorkflowParksUnreadable, findParkOrigin, planWorkflowParkedDecide } from "../parked-decide.js";
5
+ const sessionFaceUnavailable = async (sessionId) => {
6
+ throw new Error(`parked decide cannot verify which occupant session ${sessionId} belongs to: this deployment's session store has no sessionFaceOf read (every built-in store has one) — refusing rather than resuming unverified`);
7
+ };
5
8
  import { HttpError, decodeCheckpointScope, CHECKPOINT_PUBLIC_SCOPE } from "../security.js";
6
9
  import { evictIfConflict, parkToolCallId, TurnAnchorCapture, HEARTBEAT_MS, markChildrenStoppedByUserOnAbort } from "../runs.js";
7
10
  import { failureCodeOf, nestedPauseOf, sealResultForExit, outwardPlane } from "../terminal.js";
@@ -36,6 +39,7 @@ import { buildActiveRunConflict, resumeEntryForGate } from "./active-run-conflic
36
39
  import { commitPlacement, devicePlacementErrorBody } from "./device-placement.js";
37
40
  import { ASK_USER_QUESTION_TOOL_NAME } from "../approval-content-kind.js";
38
41
  import { readResumeCtx, resumeCtxCorruptAnswer } from "./resume-ctx-read.js";
42
+ import { httpErrorCode } from "./send.js";
39
43
  function isCheckpointReopenedFailure(r) {
40
44
  const code = failureCodeOf(r.terminal);
41
45
  return code === "resume.env_failed" || code === "resume.tool_unavailable" || code === "resume.tool_contract_mismatch";
@@ -258,7 +262,7 @@ export function createResumeLegs(ctx) {
258
262
  checkpointStore: cs,
259
263
  registry: defaultTaskRegistry,
260
264
  reviveSurface: parkedSurface,
261
- ...(deps.sessionStorage?.ownerOf ? { sessionOwnerOf: deps.sessionStorage.ownerOf.bind(deps.sessionStorage) } : {}),
265
+ sessionFaceOf: deps.sessionStorage?.sessionFaceOf ? deps.sessionStorage.sessionFaceOf.bind(deps.sessionStorage) : sessionFaceUnavailable,
262
266
  ...(deps.parkedKnownAgentTypes ? { knownAgentTypes: deps.parkedKnownAgentTypes } : {}),
263
267
  ...(deps.parkedReviveInheritedGate ? { rebuildInheritedGate: deps.parkedReviveInheritedGate } : {}),
264
268
  ...(deps.logger ? { warn: parkWarn } : {}),
@@ -341,7 +345,7 @@ export function createResumeLegs(ctx) {
341
345
  ok: false,
342
346
  answer: {
343
347
  status: 409,
344
- body: { error: `resume blocked by an admission policy change: ${e.message}`, errorCode: "resume_blocked_by_policy", ...(e.code ? { blockedBy: e.code } : {}), ...(e.extra ?? {}) },
348
+ body: { error: `resume blocked by an admission policy change: ${e.message}`, errorCode: "resume_blocked_by_policy", blockedBy: httpErrorCode(e.status, e.code), ...(e.extra ?? {}) },
345
349
  },
346
350
  };
347
351
  }
@@ -908,7 +912,7 @@ export function createResumeLegs(ctx) {
908
912
  settleFleet("failed");
909
913
  }
910
914
  else if (retriable) {
911
- await (outcome.gate === "plan_review" ? deps.runStore.setNeedsReview(taskId) : deps.runStore.setSuspended(taskId)).catch(() => undefined);
915
+ await (outcome.gate === "plan_review" ? deps.runStore.setNeedsReview(taskId, null) : deps.runStore.setSuspended(taskId, null)).catch(() => undefined);
912
916
  settleFleet(outcome.gate === "plan_review" ? "needs_review" : "suspended");
913
917
  }
914
918
  else {
@@ -973,9 +977,9 @@ export function createResumeLegs(ctx) {
973
977
  const resumePlane = outwardPlane(sealed.terminal);
974
978
  if (taskId && deps.runStore) {
975
979
  if (resumePlane.status === "suspended" || checkpointReopened)
976
- await deps.runStore.setSuspended(taskId);
980
+ await deps.runStore.setSuspended(taskId, sealed.terminal.kind === "paused" ? sealed : null);
977
981
  else if (resumePlane.status === "needs_review")
978
- await deps.runStore.setNeedsReview(taskId);
982
+ await deps.runStore.setNeedsReview(taskId, sealed);
979
983
  else
980
984
  await deps.runStore.setTerminal(taskId, resumePlane.status, sealed, resumePlane.errorMessage ?? null);
981
985
  }
@@ -25,10 +25,17 @@
25
25
  * · **能力位同源**:`capabilities.agentRoster` 与这里的 501 谓词是同一个 `deps.backgroundAgentStore`。
26
26
  */
27
27
  import type { IncomingMessage, ServerResponse } from "node:http";
28
+ import type { BackgroundAgentRowSummary } from "@sema-agent/core";
28
29
  import type { RouteCtx, RouteMatch, RouteIdsOf } from "../route-ctx.js";
29
30
  /** 名册窗的扫描上限(与 fleet 的终态窗同姿势:读面要有界)。窗满时回体带 `truncated:true`,
30
31
  * `total` 于是诚实地是「窗内行数」而不是租户全量 —— 一个悄悄封顶的 total 比没有 total 更坏。 */
31
32
  export declare const ROSTER_SCAN_LIMIT = 500;
33
+ /**
34
+ * 后台子代在 wire 上的 **id 投影**(唯一一只):名册行的 `handle` 与 `DELETE /v1/sessions/:id` 的 409
35
+ * `conflict.session_children_alive` 体里 `remaining[]` 的每一项都经它出门(S-790,裁定 2026-09-28)—— 两个面上
36
+ * 「同一只子代」必须是同一个串,消费方拿删除答复里的 id 才对得上名册里的行、才能去它运行的地方停它。
37
+ */
38
+ export declare function rosterAgentId(row: Pick<BackgroundAgentRowSummary, "handle">): string;
32
39
  export declare function handleAgentRoster(req: IncomingMessage, res: ServerResponse, match: RouteMatch<AgentRosterRouteId>, ctx: RouteCtx): Promise<void>;
33
40
  export declare const AGENT_ROSTER_ROUTES: readonly [{
34
41
  readonly id: "agents-roster";
@@ -1,4 +1,5 @@
1
1
  import { sendJson, sendError } from "../send.js";
2
+ import { sendUnauthorized } from "../principal-gate.js";
2
3
  import { assertNever } from "../route-ctx.js";
3
4
  export const ROSTER_SCAN_LIMIT = 500;
4
5
  const ROSTER_STATUSES = ["running", "parked", "completed", "failed", "killed"];
@@ -18,9 +19,12 @@ const ROSTER_KEY_DISPOSITION = {
18
19
  usage: "strip",
19
20
  };
20
21
  void ROSTER_KEY_DISPOSITION;
22
+ export function rosterAgentId(row) {
23
+ return row.handle;
24
+ }
21
25
  function projectRow(r) {
22
26
  return {
23
- handle: r.handle,
27
+ handle: rosterAgentId(r),
24
28
  ...(r.name !== undefined ? { name: r.name } : {}),
25
29
  ...(r.agentType !== undefined ? { agentType: r.agentType } : {}),
26
30
  status: r.status,
@@ -56,7 +60,7 @@ export async function handleAgentRoster(req, res, match, ctx) {
56
60
  case "agents-roster": {
57
61
  const { fleetWide, gateOwner } = sessionOwnerScope(req);
58
62
  if (!fleetWide && gateOwner === null) {
59
- sendError(res, 401, "auth.unauthorized", "unauthorized");
63
+ sendUnauthorized(res, "identity");
60
64
  return;
61
65
  }
62
66
  const store = deps.backgroundAgentStore;
@@ -3,31 +3,59 @@ import type { QuestionAnswer } from "@sema-agent/core";
3
3
  import type { CheckpointStoreFull } from "../../plugins/store-backend.js";
4
4
  import { type ToolApprovalCoordinator } from "../../tool-approval.js";
5
5
  import { type GovernancePosture } from "../../runtime-governance.js";
6
- import { type RouteCtx, type RouteMatch, type RouteIdsOf } from "../route-ctx.js";
6
+ import { type RouteCtx, type RouteDecl, type RouteMatch, type RouteIdsOf } from "../route-ctx.js";
7
7
  import { type KeyClosureIssue } from "../../request-key-closure.js";
8
+ /** 族的成员 = 路由表行 id(改名 / 删行 ⇒ 下表的键当场编译红)。 */
9
+ export type DecisionDoorId = Extract<ApprovalsAssistantRouteId, "approval-decide" | "assistant-task-plan-review">;
10
+ /** 一道门的静态描述 —— 只写**这道门自己**的东西;路径、路径段、别门的键一概不在这里写(从来源读)。 */
11
+ export interface DecisionDoor {
12
+ /** 体的受理集 = 本门的 `*_BODY_DECLARED_KEYS`(那张表的双向编译期门照旧咬着它的类型)。 */
13
+ readonly accepted: readonly string[];
14
+ /** `unknown` 桶标签里的门名(「not a <这里> key — check the spelling」)。 */
15
+ readonly requestNoun: string;
16
+ /** 拒体引子里「这道门上拼错一个键最贵的后果」那半句(人读;每门一句)。 */
17
+ readonly costliestTypo: string;
18
+ }
19
+ export type DecisionDoorFamily = {
20
+ readonly [K in DecisionDoorId]: DecisionDoor;
21
+ };
22
+ /** 「名字是真的、只是不在这个体上」的三族(闭集;加第四族 = 下面 `switch` 编译红)。 */
23
+ type NeighborFamily = "sibling_field" | "path_segment" | "submit_body";
24
+ /** 一个邻键:它属哪一族 + 它在哪儿是真的(wire 路径,可多条)。 */
25
+ interface NeighborKey {
26
+ readonly family: NeighborFamily;
27
+ readonly where: readonly string[];
28
+ }
29
+ /** 派生结果里一道门的全部判据(受理集 + 邻键表 + 两句人读)。 */
30
+ export interface DerivedDecisionDoor {
31
+ readonly accepted: ReadonlySet<string>;
32
+ readonly neighbors: ReadonlyMap<string, NeighborKey>;
33
+ readonly requestNoun: string;
34
+ readonly costliestTypo: string;
35
+ }
36
+ export type DecisionDoorKeyTable = {
37
+ readonly [K in DecisionDoorId]: DerivedDecisionDoor;
38
+ };
8
39
  /**
9
- * FRESH 决裁面的**顶层**键闭集裁决:受理集之外的键 ⇒ 返回拒体(调用方 400 `request.body_shape`);
10
- * 全部受理 ⇒ `null`。判据边界与孪生门(`taskBodyKeyIssue` / `taskSettingsKeyIssue`)逐字同一条:
11
- * **只判键名**(值形校验归各自既有的门,本门零抢话);体本身非对象 ⇒ 本门不判(留给「body must be
12
- * { decision … }」那道根形门,免得同一个体被两句话抢答)。
13
- *
14
- * 🔴 **重放不额外过门**,也不需要:本腿的幂等回放(`decideIdempotentReplay`)重放的是**同一次请求的同一份
15
- * 体**,它与首决走同一条路由、同一道门;而 checkpoint 的 resume 重建腿根本不读这个体(它读盘上的 body,
16
- * 走 `resolveSpec` 的宽容层)⇒ 4xx 砖不到任何存量 parked 任务。这与 tasks 面「RESUME 重放不过门」是同一条律
17
- * 的两种兑现方式:**门只站在人**(或客户端)**现写的那份字节**上。
40
+ * 回决门族的键闭集判据表 —— **唯一**的派生口(纯函数:同一份族描述 + 同一张路由表 ⇒ 同一张表)。
41
+ * 生产只在本模块尾调一次(路由表声明在文件尾,见 {@link DECISION_DOOR_KEY_TABLE});测试可以喂一份构造的族
42
+ * (「给兄弟门受理集加一个假键 ⇒ 本门的 unsupported 集自动多这一键」就是这么钉的)。
43
+ * 🔴 族两两不交是**前提**不是愿望:同一个键若既是某门的体字段又是某条 URL 的路径段,它的去处就有两个答案
44
+ * ⇒ 抛(模块加载期 = 启动即炸,不是请求期 500)。
18
45
  */
19
- export declare function decideBodyKeyIssue(raw: unknown): KeyClosureIssue | null;
46
+ export declare function deriveDecisionDoorKeyTable(family: DecisionDoorFamily, routes: readonly RouteDecl[]): DecisionDoorKeyTable;
20
47
  /**
21
- * FRESH 决裁面的**顶层**键闭集裁决:受理集之外的键 ⇒ 返回拒体(调用方 400 `request.body_shape`);
22
- * 全部受理 ⇒ `null`。判据边界与三个孪生门(`taskBodyKeyIssue` / `taskSettingsKeyIssue` /
23
- * {@link decideBodyKeyIssue})逐字同一条:**只判键名**(值形校验归各自既有的门,本门零抢话);体本身
24
- * 非对象 ⇒ 本门不判(留给「body must be { decision … }」那道根形门,免得同一个体被两句话抢答)。
48
+ * 回决门族**一道门**的顶层键闭集裁决:受理集之外的键 ⇒ 拒体(调用方 400 `request.body_shape`);全部受理 ⇒ `null`。
49
+ * 判据边界与两个孪生门(`taskBodyKeyIssue` / `taskSettingsKeyIssue`)逐字同一条:**只判键名**(值形校验归各自
50
+ * 既有的门,本门零抢话);体本身非对象 ⇒ 本门不判(留给「body must be { decision … }」那道根形门,免得同一个体
51
+ * 被两句话抢答)。
25
52
  *
26
- * 🔴 **重放不额外过门**,也不需要 —— 逐字同 {@link decideBodyKeyIssue} 的同名段:本腿的反重放是
27
- * 一次性 nonce(S-280),它守的是**同一份现写的字节**;而续跑重建读的是盘上的体、走 `resolveSpec` 的
28
- * 宽容层 ⇒ 4xx 砖不到任何存量挂起的任务。**门只站在人(或客户端)现写的那份字节上**。
53
+ * 🔴 **重放不额外过门**,也不需要:`/decide` 的幂等回放(`decideIdempotentReplay`)重放的是**同一次请求的同一份
54
+ * 体**、走同一条路由同一道门;plan_review 的反重放是一次性 nonce(S-280),守的也是同一份现写的字节;而续跑重建
55
+ * 读的是盘上的体、走 `resolveSpec` 的宽容层 ⇒ 4xx 砖不到任何存量挂起的任务。**门只站在人(或客户端)现写的
56
+ * 那份字节上**(与 tasks 面「RESUME 重放不过门」同一条律)。
29
57
  */
30
- export declare function planReviewBodyKeyIssue(raw: unknown): KeyClosureIssue | null;
58
+ export declare function decisionBodyKeyIssue(door: DecisionDoorId, raw: unknown, table?: DecisionDoorKeyTable): KeyClosureIssue | null;
31
59
  /**
32
60
  * S-342 —— 「空串的理由 = 没有理由 = 这是一次**裸拒**」。这条语义只适用于**本仓自铸**的那一个域。
33
61
  *
@@ -147,4 +175,5 @@ export declare const APPROVALS_ASSISTANT_ROUTES: readonly [{
147
175
  }];
148
176
  /** 本域可分派行的 `id` 闭集 —— handler 的 `switch` 按它判穷尽(漏一口 = 编译红)。 */
149
177
  export type ApprovalsAssistantRouteId = RouteIdsOf<typeof APPROVALS_ASSISTANT_ROUTES>;
178
+ export {};
150
179
  //# sourceMappingURL=approvals-assistant.d.ts.map