@sema-agent/server 7.4.0 → 7.6.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 (201) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +18 -3
  3. package/README.zh-CN.md +14 -3
  4. package/USAGE.md +80 -1
  5. package/dist/approval-card.d.ts +15 -3
  6. package/dist/approval-card.js +41 -7
  7. package/dist/approval-reconciler.d.ts +109 -12
  8. package/dist/approval-reconciler.js +152 -24
  9. package/dist/boot/config-center.js +15 -2
  10. package/dist/boot/coordinators.js +10 -2
  11. package/dist/boot/execution-env.js +1 -1
  12. package/dist/boot/org-memory.d.ts +6 -0
  13. package/dist/boot/org-memory.js +1 -1
  14. package/dist/boot/parked-revive-gate.d.ts +78 -0
  15. package/dist/boot/parked-revive-gate.js +114 -0
  16. package/dist/boot/reapers.d.ts +2 -0
  17. package/dist/boot/reapers.js +11 -4
  18. package/dist/boot/resolve-spec.d.ts +3 -19
  19. package/dist/boot/resolve-spec.js +73 -67
  20. package/dist/boot/runner-deps.d.ts +23 -1
  21. package/dist/boot/runner-deps.js +8 -11
  22. package/dist/boot/workflow-orchestration.d.ts +8 -3
  23. package/dist/boot/workflow-orchestration.js +23 -1
  24. package/dist/budget.d.ts +1 -1
  25. package/dist/budget.js +1 -1
  26. package/dist/capabilities/repo-tools.d.ts +1 -1
  27. package/dist/capabilities/repo-tools.js +8 -2
  28. package/dist/config-center/apply-effective.js +33 -10
  29. package/dist/config-provider.d.ts +1 -0
  30. package/dist/config-provider.js +23 -3
  31. package/dist/config-types.d.ts +27 -9
  32. package/dist/config.d.ts +6 -1
  33. package/dist/config.js +61 -15
  34. package/dist/deployment-governance.d.ts +168 -0
  35. package/dist/deployment-governance.js +206 -0
  36. package/dist/env-facts.d.ts +3 -1
  37. package/dist/env-facts.js +3 -1
  38. package/dist/fleet/fleet-bus.d.ts +17 -2
  39. package/dist/fleet/fleet-bus.js +68 -3
  40. package/dist/fleet/fleet-terminal-window.d.ts +98 -0
  41. package/dist/fleet/fleet-terminal-window.js +316 -0
  42. package/dist/governance-ask-marks.d.ts +31 -0
  43. package/dist/governance-ask-marks.js +122 -0
  44. package/dist/hooks/hook-runner.d.ts +28 -0
  45. package/dist/hooks/hook-runner.js +149 -25
  46. package/dist/http/routes/approvals-assistant.js +6 -7
  47. package/dist/http/routes/diagnostics.js +10 -5
  48. package/dist/http/routes/fleet.js +160 -14
  49. package/dist/http/routes/memory-policy.d.ts +2 -1
  50. package/dist/http/routes/memory-policy.js +77 -13
  51. package/dist/http/routes/runs.js +6 -2
  52. package/dist/http/routes/tasks.js +59 -22
  53. package/dist/http/routes/trace-usage.js +3 -4
  54. package/dist/http/send.d.ts +23 -0
  55. package/dist/http/send.js +23 -0
  56. package/dist/http/server.d.ts +9 -0
  57. package/dist/http/server.js +28 -14
  58. package/dist/http/sse-log.js +3 -4
  59. package/dist/http/wire-types.d.ts +7 -2
  60. package/dist/leader/diffout.d.ts +10 -0
  61. package/dist/leader/diffout.js +14 -2
  62. package/dist/leader/diffup.js +3 -2
  63. package/dist/leader/planner.js +7 -0
  64. package/dist/main.js +39 -31
  65. package/dist/observability/fail-open.d.ts +17 -2
  66. package/dist/observability/fail-open.js +19 -4
  67. package/dist/observability/prompt-manifest.d.ts +5 -1
  68. package/dist/orchestration/workflow-notify-journal.d.ts +58 -2
  69. package/dist/orchestration/workflow-notify-journal.js +130 -45
  70. package/dist/parked-decide.js +9 -4
  71. package/dist/plugins/approval-ask-store-memory.d.ts +2 -2
  72. package/dist/plugins/approval-ask-store-memory.js +3 -2
  73. package/dist/plugins/approval-ask-store-sql.d.ts +60 -5
  74. package/dist/plugins/approval-ask-store-sql.js +75 -35
  75. package/dist/plugins/background-agent-store-sql.js +16 -16
  76. package/dist/plugins/background-shell-support.d.ts +1 -1
  77. package/dist/plugins/background-shell-support.js +2 -2
  78. package/dist/plugins/breaker-state-sql.js +2 -2
  79. package/dist/plugins/checkpoint-store-sql.d.ts +67 -8
  80. package/dist/plugins/checkpoint-store-sql.js +76 -13
  81. package/dist/plugins/image-bake-store-sql.d.ts +1 -1
  82. package/dist/plugins/image-bake-store-sql.js +27 -27
  83. package/dist/plugins/image-index-sql.js +15 -15
  84. package/dist/plugins/local-checkpoint-store.d.ts +20 -1
  85. package/dist/plugins/local-checkpoint-store.js +19 -0
  86. package/dist/plugins/mailbox-store-sql.d.ts +4 -10
  87. package/dist/plugins/mailbox-store-sql.js +59 -6
  88. package/dist/plugins/memory-engine-pg.js +9 -9
  89. package/dist/plugins/memory-engine-tidb.js +7 -7
  90. package/dist/plugins/memory-sync-store-pg.js +13 -13
  91. package/dist/plugins/memory-sync-store-tidb.js +5 -5
  92. package/dist/plugins/outcome-ledger-sql.js +7 -7
  93. package/dist/plugins/pg-cost-quota.js +3 -3
  94. package/dist/plugins/pg-pool.js +84 -75
  95. package/dist/plugins/pg-rate-limiter.js +3 -3
  96. package/dist/plugins/pg-session-storage.d.ts +1 -1
  97. package/dist/plugins/pg-session-storage.js +12 -13
  98. package/dist/plugins/remote-env-host.js +3 -1
  99. package/dist/plugins/remote-env-local-docker.js +6 -3
  100. package/dist/plugins/remote-env-ssh.d.ts +13 -1
  101. package/dist/plugins/roster-store-sql.js +8 -8
  102. package/dist/plugins/store-contracts.d.ts +19 -0
  103. package/dist/plugins/store-contracts.js +42 -0
  104. package/dist/plugins/task-attachment-store.js +5 -5
  105. package/dist/plugins/task-list-store-sql.js +1 -1
  106. package/dist/plugins/tidb-cost-quota.js +1 -1
  107. package/dist/plugins/tidb-pool.js +83 -60
  108. package/dist/plugins/tidb-rate-limiter.js +1 -1
  109. package/dist/plugins/tidb-session-store.js +2 -5
  110. package/dist/plugins/tool-result-store-sql.js +2 -2
  111. package/dist/plugins/usage-window-store-sql.js +13 -13
  112. package/dist/plugins/write-behind-counter.d.ts +10 -2
  113. package/dist/plugins/write-behind-counter.js +13 -3
  114. package/dist/resource-suspend.d.ts +3 -1
  115. package/dist/resource-suspend.js +3 -1
  116. package/dist/run-local.d.ts +73 -1
  117. package/dist/run-local.js +146 -5
  118. package/dist/runs.d.ts +11 -1
  119. package/dist/runs.js +18 -3
  120. package/dist/runtime-governance.d.ts +18 -0
  121. package/dist/runtime-governance.js +90 -3
  122. package/dist/security.d.ts +12 -0
  123. package/dist/security.js +12 -0
  124. package/dist/session-sync-kernel.d.ts +13 -0
  125. package/dist/session-sync-kernel.js +13 -0
  126. package/dist/task-settings.d.ts +3 -9
  127. package/dist/task-settings.js +16 -13
  128. package/dist/tool-approval.d.ts +33 -6
  129. package/dist/tool-approval.js +80 -23
  130. package/dist/trace/core-keyset-guard.d.ts +18 -4
  131. package/dist/trace/project.d.ts +10 -1
  132. package/dist/trace/project.js +31 -0
  133. package/package.json +3 -3
  134. package/dist/boot/lexical-path-env.d.ts +0 -10
  135. package/dist/boot/lexical-path-env.js +0 -88
  136. package/dist/capabilities/oa-tools.d.ts +0 -15
  137. package/dist/capabilities/oa-tools.js +0 -54
  138. package/dist/finance/cost-taxonomy.d.ts +0 -34
  139. package/dist/finance/cost-taxonomy.js +0 -26
  140. package/dist/plugins/approval-store-sql.d.ts +0 -116
  141. package/dist/plugins/approval-store-sql.js +0 -151
  142. package/dist/plugins/file-workflow-journal-store.d.ts +0 -12
  143. package/dist/plugins/file-workflow-journal-store.js +0 -12
  144. package/dist/plugins/pg-approval-store.d.ts +0 -9
  145. package/dist/plugins/pg-approval-store.js +0 -9
  146. package/dist/plugins/pg-breaker-state.d.ts +0 -8
  147. package/dist/plugins/pg-breaker-state.js +0 -8
  148. package/dist/plugins/pg-checkpoint-store.d.ts +0 -10
  149. package/dist/plugins/pg-checkpoint-store.js +0 -10
  150. package/dist/plugins/pg-file-snapshot-store.d.ts +0 -8
  151. package/dist/plugins/pg-file-snapshot-store.js +0 -8
  152. package/dist/plugins/pg-image-bake.d.ts +0 -12
  153. package/dist/plugins/pg-image-bake.js +0 -11
  154. package/dist/plugins/pg-image-index.d.ts +0 -12
  155. package/dist/plugins/pg-image-index.js +0 -11
  156. package/dist/plugins/pg-outcome-ledger.d.ts +0 -12
  157. package/dist/plugins/pg-outcome-ledger.js +0 -11
  158. package/dist/plugins/pg-resume-anchor-store.d.ts +0 -7
  159. package/dist/plugins/pg-resume-anchor-store.js +0 -7
  160. package/dist/plugins/pg-run-store.d.ts +0 -9
  161. package/dist/plugins/pg-run-store.js +0 -9
  162. package/dist/plugins/pg-session-policy-store.d.ts +0 -7
  163. package/dist/plugins/pg-session-policy-store.js +0 -7
  164. package/dist/plugins/pg-session-store.d.ts +0 -12
  165. package/dist/plugins/pg-session-store.js +0 -12
  166. package/dist/plugins/pg-tool-result-store.d.ts +0 -9
  167. package/dist/plugins/pg-tool-result-store.js +0 -9
  168. package/dist/plugins/pg-workflow-journal-store.d.ts +0 -9
  169. package/dist/plugins/pg-workflow-journal-store.js +0 -9
  170. package/dist/plugins/pg-workflow-run-store.d.ts +0 -9
  171. package/dist/plugins/pg-workflow-run-store.js +0 -9
  172. package/dist/plugins/tidb-approval-store.d.ts +0 -8
  173. package/dist/plugins/tidb-approval-store.js +0 -8
  174. package/dist/plugins/tidb-breaker-state.d.ts +0 -7
  175. package/dist/plugins/tidb-breaker-state.js +0 -7
  176. package/dist/plugins/tidb-checkpoint-store.d.ts +0 -9
  177. package/dist/plugins/tidb-checkpoint-store.js +0 -9
  178. package/dist/plugins/tidb-file-snapshot-store.d.ts +0 -8
  179. package/dist/plugins/tidb-file-snapshot-store.js +0 -8
  180. package/dist/plugins/tidb-image-bake.d.ts +0 -12
  181. package/dist/plugins/tidb-image-bake.js +0 -11
  182. package/dist/plugins/tidb-image-index.d.ts +0 -12
  183. package/dist/plugins/tidb-image-index.js +0 -11
  184. package/dist/plugins/tidb-outcome-ledger.d.ts +0 -12
  185. package/dist/plugins/tidb-outcome-ledger.js +0 -12
  186. package/dist/plugins/tidb-resume-anchor-store.d.ts +0 -7
  187. package/dist/plugins/tidb-resume-anchor-store.js +0 -7
  188. package/dist/plugins/tidb-run-store.d.ts +0 -10
  189. package/dist/plugins/tidb-run-store.js +0 -9
  190. package/dist/plugins/tidb-session-policy-store.d.ts +0 -7
  191. package/dist/plugins/tidb-session-policy-store.js +0 -7
  192. package/dist/plugins/tidb-tool-result-store.d.ts +0 -8
  193. package/dist/plugins/tidb-tool-result-store.js +0 -10
  194. package/dist/plugins/tidb-workflow-journal-store.d.ts +0 -9
  195. package/dist/plugins/tidb-workflow-journal-store.js +0 -9
  196. package/dist/plugins/tidb-workflow-run-store.d.ts +0 -10
  197. package/dist/plugins/tidb-workflow-run-store.js +0 -10
  198. package/dist/plugins/workflow-journal-limits.d.ts +0 -12
  199. package/dist/plugins/workflow-journal-limits.js +0 -12
  200. package/dist/sema-registry.d.ts +0 -41
  201. package/dist/sema-registry.js +0 -40
@@ -9,6 +9,29 @@
9
9
  */
10
10
  import type { ServerResponse } from "node:http";
11
11
  export declare function sseHeaders(res: ServerResponse, extra?: Record<string, string>): void;
12
+ /**
13
+ * 一条 SSE 连接的墙钟上限。到点发一帧 `error`/`STREAM_MAX_DURATION`(**不是**静默关闭 —— 客户端分不出
14
+ * 「到帽了请重连」和「跑完了」)然后收流。
15
+ *
16
+ * 🔴 为什么共享(2026-08-08,#176 二轮扫描 finding):这个数与下面的心跳间隔此前在**三处**各手抄一份
17
+ * —— `http/sse-log.ts`(task_run + image_bake 共用的那台泵)、`http/routes/trace-usage.ts`、
18
+ * `http/routes/approvals-assistant.ts`(它的顶注还自称「parity with streamTaskTrace」)。三处各自有测试
19
+ * 钉着**自己那一份**,没有任何跨副本的一致性钉 ⇒ 单边调帽后「parity」只剩注释里那句话。
20
+ *
21
+ * ⚠️ 改这个数请连**文案**一起改:三处的 `STREAM_MAX_DURATION` 消息里把「15-minute」写死在散文里
22
+ * (文案面另有 freeze 门,故此处不做插值——散文与常量的同步靠这行告诫,不靠机器)。
23
+ */
24
+ export declare const SSE_MAX_STREAM_MS: number;
25
+ /**
26
+ * 长时间无事件时的心跳间隔(防中继的 idle timeout 掐流)。与 {@link SSE_MAX_STREAM_MS} 同批收口。
27
+ *
28
+ * ⚠️ 收口的只有**间隔**,**不含帧形**:`sse-log` / `trace-usage` 发 `data: {}`,`approvals-assistant`
29
+ * 发 `data: {"type":"heartbeat"}`。这是真分歧不是笔误 —— trace-usage 自己的 meta 帧还引着 [2373]C-12
30
+ * 「每个 data payload 都带 type」的约定,而它的心跳帧没有 type(probe-sse-1 的注释把这条记成「按
31
+ * data.type 分派的客户端会拿到 undefined」)。统一帧形是**行为面**改动(要么给两处加 type=多发一个键,
32
+ * 要么给 approvals 减 type=消费端破坏),属候裁,不在本批的零行为码范围内。
33
+ */
34
+ export declare const SSE_HEARTBEAT_IDLE_MS = 15000;
12
35
  export declare function sendJson(res: ServerResponse, status: number, body: unknown): void;
13
36
  /** A6(design/158 lens1 §C3/lens3 §C3a): canonical error-response sender for sites that already carry a
14
37
  * machine `errorCode` — `errorCode` is a required positional arg (not optional-bag) so a call site
package/dist/http/send.js CHANGED
@@ -6,6 +6,29 @@ export function sseHeaders(res, extra) {
6
6
  ...extra,
7
7
  });
8
8
  }
9
+ /**
10
+ * 一条 SSE 连接的墙钟上限。到点发一帧 `error`/`STREAM_MAX_DURATION`(**不是**静默关闭 —— 客户端分不出
11
+ * 「到帽了请重连」和「跑完了」)然后收流。
12
+ *
13
+ * 🔴 为什么共享(2026-08-08,#176 二轮扫描 finding):这个数与下面的心跳间隔此前在**三处**各手抄一份
14
+ * —— `http/sse-log.ts`(task_run + image_bake 共用的那台泵)、`http/routes/trace-usage.ts`、
15
+ * `http/routes/approvals-assistant.ts`(它的顶注还自称「parity with streamTaskTrace」)。三处各自有测试
16
+ * 钉着**自己那一份**,没有任何跨副本的一致性钉 ⇒ 单边调帽后「parity」只剩注释里那句话。
17
+ *
18
+ * ⚠️ 改这个数请连**文案**一起改:三处的 `STREAM_MAX_DURATION` 消息里把「15-minute」写死在散文里
19
+ * (文案面另有 freeze 门,故此处不做插值——散文与常量的同步靠这行告诫,不靠机器)。
20
+ */
21
+ export const SSE_MAX_STREAM_MS = 15 * 60 * 1000;
22
+ /**
23
+ * 长时间无事件时的心跳间隔(防中继的 idle timeout 掐流)。与 {@link SSE_MAX_STREAM_MS} 同批收口。
24
+ *
25
+ * ⚠️ 收口的只有**间隔**,**不含帧形**:`sse-log` / `trace-usage` 发 `data: {}`,`approvals-assistant`
26
+ * 发 `data: {"type":"heartbeat"}`。这是真分歧不是笔误 —— trace-usage 自己的 meta 帧还引着 [2373]C-12
27
+ * 「每个 data payload 都带 type」的约定,而它的心跳帧没有 type(probe-sse-1 的注释把这条记成「按
28
+ * data.type 分派的客户端会拿到 undefined」)。统一帧形是**行为面**改动(要么给两处加 type=多发一个键,
29
+ * 要么给 approvals 减 type=消费端破坏),属候裁,不在本批的零行为码范围内。
30
+ */
31
+ export const SSE_HEARTBEAT_IDLE_MS = 15_000;
9
32
  export function sendJson(res, status, body) {
10
33
  res.writeHead(status, { "content-type": "application/json" });
11
34
  res.end(JSON.stringify(body));
@@ -187,6 +187,10 @@ export interface ServiceCoordinatorDeps {
187
187
  * (the live multi-row task/workflow fleet view: status/elapsed/tokens/doneCount transitions pushed over time).
188
188
  * Present when wired in main.ts; the run + workflow lifecycle publish their per-row deltas to it. */
189
189
  fleetBus?: FleetEventBus;
190
+ /** [3193] 病理1:boot recover(判死前世 running 行并回灌 fleet)完成屏障——`/v1/fleet/stream` 快照组装
191
+ * 对它做**有界**等待(3s cap,routes/fleet.ts),SIGKILL 后壳重连不再与 recover 赛跑。缺席或已
192
+ * resolve=立即组装(memory 后端、无 notify gate 的部署);main.ts 铸(recover 收官即落,失败也落)。 */
193
+ fleetRecoveryDone?: Promise<void>;
190
194
  /** [1196] session-watch registry(SSE 订阅共享探针面)。present ⇔ sessionStorage 有 getLeafId/ownerOf
191
195
  * (main.ts 建);absent ⇒ /v1/sessions/:id/events 501(capabilities.sessionEvents 同谓词,永不漂)。 */
192
196
  sessionWatch?: import("../session-watch.js").SessionWatchRegistry;
@@ -266,6 +270,11 @@ export interface ServiceSeamDeps {
266
270
  * (MEMORY_ENGINE_BACKEND=pg|tidb); absent / file posture ⇒ 501 honestly (a single-user file plane
267
271
  * IS the TOC side of a sync — it has no central-authority half to serve). */
268
272
  memorySync?: (scope: string, syncReq: MemorySyncRequest) => Promise<MemorySyncResponse>;
273
+ /** design/170 件A §7 —— org 记忆授权目录,`boot/org-memory.ts` 装配的**同一个实例**(准入 seam 与
274
+ * 本面共享它的 TTL 缓存/退避窗/gen 高水位)。memory-policy 面的 `org:` 属主门用它回答成员性:
275
+ * 读面要该 scope 在授权表里、写面另要条目 `write === true`。缺席=本部署没有目录源 ⇒ `org:` 键
276
+ * 逐字保持 operator-only(收编前行为),绝不因目录不接线而放宽。 */
277
+ orgMemoryDirectory?: import("../org-memory-admission.js").OrgMemoryDirectory;
269
278
  /** WEB-SETUP-RECON 附B — org sessionMirror 治理态的 SERVICE 半场 = **观测面(audit),非执法面**。
270
279
  * 裁定 (c):「该镜像而未镜像」发生在数据产生端(壳)——本 server 是镜像**目标**,只看得见到达的 push;
271
280
  * 没到达的会话对 server 不存在(无客户端会话清单、无心跳契约),required 锁的真执法只能在壳(CC
@@ -5,7 +5,7 @@ import { uuidv7, isThinkingLevel, expandTiers, resumeWithVerification, Checkpoin
5
5
  import { decideParkedAgent, findParkedAgentForCheckpoint } from "../parked-decide.js";
6
6
  import { matchCatalogModel } from "../model-select.js";
7
7
  import {} from "../config-center/facade.js";
8
- import { HttpError, principalFrom, verifiedPrincipal, setSsoPrincipal, ssoVerifiedPrincipal, setSsoScope, isUuidV7, isDestructiveSessionWrite, decodeCheckpointScope, CHECKPOINT_PUBLIC_SCOPE } from "../security.js";
8
+ import { HttpError, principalFrom, verifiedPrincipal, setSsoPrincipal, ssoVerifiedPrincipal, setSsoScope, isUuidV7, isDestructiveSessionWrite, decodeCheckpointScope, CHECKPOINT_PUBLIC_SCOPE, PRINCIPAL_TOKEN_HEADER, APPROVAL_MAC_HEADER, APPROVAL_MAC_KID_HEADER } from "../security.js";
9
9
  import { verifyDirectDoorProof } from "../principal-jwt.js";
10
10
  import {} from "../memory-sync.js";
11
11
  import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_PERMISSION_RULES, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS } from "../task-settings.js";
@@ -1456,9 +1456,14 @@ export function createHttpServer(rawDeps) {
1456
1456
  // "approve a content-ask with no answer" (runtask.js: `taskConfig.onQuestion ?? deps.onQuestion` is
1457
1457
  // `undefined` or QUESTION_AWAITS_RESUME), but on an ASK_QUESTION_ENABLED deployment the spec no longer
1458
1458
  // stamps the sentinel and `deps.onQuestion` is the coordinator, so core's condition is never met and it
1459
- // does NOT refuse. Without the guard below the resume would execute the question on a leg with no live
1460
- // context, take the coordinator's empty "no human" default, and consume the checkpoint. Do not delete it
1461
- // on the belief that core still catches this (parked twin: src/parked-decide.ts).
1459
+ // does NOT refuse.
1460
+ // ⚠️ The ORIGINAL rationale — "the redeemed leg would take the coordinator's empty 'no human' default"
1461
+ // is DISPROVED and must not be repeated: since #166 QuestionCoordinator never synthesizes an empty answer
1462
+ // set (every no-human arm returns `{kind:"unavailable"}`), and core 5.16 answers a redeemed AskUserQuestion
1463
+ // with an isError ("will not silently self-answer") rather than a fabricated verdict. The guard STAYS on a
1464
+ // cheaper rationale: the checkpoint is consumed by the CAS BEFORE that revive can report unavailable, so
1465
+ // the operator loses the parked question for a revive that could only fail — refusing pre-CAS costs nothing
1466
+ // and gives a precise error. Do not delete it (parked twin: src/parked-decide.ts, same rationale).
1462
1467
  // - an answer is only meaningful on an approve of a question gate; on a deny or a non-question gate it
1463
1468
  // signals the operator is deciding a different pending action than the one parked — reject, don't guess.
1464
1469
  if (decision === "approve" && pendingTool === "AskUserQuestion" && !answer) {
@@ -1627,14 +1632,7 @@ export function createHttpServer(rawDeps) {
1627
1632
  // un-cancellable (cancel 202'd + flagged the row, but nothing honored it). markResuming reset the flag,
1628
1633
  // so only a cancel issued DURING this leg aborts it.
1629
1634
  const cancelCtrl = new AbortController();
1630
- // stoppedBy — resume leg NOW marked (core 1.256 delivered the mark-by-owner seam): on this
1631
- // leg core's canonical taskId == sessionId (the rebuilt taskConfig carries no spec.taskId), so a
1632
- // task-scoped child's owner KEY VALUE coincides with a session-scoped child's — the old `list()` path
1633
- // couldn't tell them apart (no sessionScoped flag exposed) and took an honest degrade. The seam filters
1634
- // by the EXPLICIT flag (`skipSessionScoped:true` inside the helper), so only task-scoped children take
1635
- // the "user" marker; session-scoped ones a cancelled parent deliberately does NOT stop stay unmarked
1636
- // (CC Backgrounded semantics). Owner = sessionId (this leg's canonical key), NOT the run-row taskId.
1637
- markChildrenStoppedByUserOnAbort(cancelCtrl.signal, sessionId, principal);
1635
+ // 🔴 stoppedBy 归因的注册点在**下面**(markResuming 认领成功之后),不在这里 —— #168 件3,见那里的注。
1638
1636
  // design/80 seam #2: a resumed leg is itself preempt-able. Register a FRESH preempt controller (markResuming
1639
1637
  // also reset preempt_requested), thread a LIVE preemptSignal into the resume taskConfig below, and poll the
1640
1638
  // durable preempt flag in the heartbeat — so the scheduler can preempt the RESUMED work once it makes
@@ -1648,6 +1646,19 @@ export function createHttpServer(rawDeps) {
1648
1646
  claimedRow = await deps.runStore.markResuming(taskId);
1649
1647
  if (!claimedRow)
1650
1648
  return { status: 409, body: { error: "run is not in a resumable (suspended) state (already resumed, decided, or expired)", errorCode: "conflict.not_resumable" } };
1649
+ // stoppedBy — resume leg NOW marked (core 1.256 delivered the mark-by-owner seam): on this
1650
+ // leg core's canonical taskId == sessionId (the rebuilt taskConfig carries no spec.taskId), so a
1651
+ // task-scoped child's owner KEY VALUE coincides with a session-scoped child's — the old `list()` path
1652
+ // couldn't tell them apart (no sessionScoped flag exposed) and took an honest degrade. The seam filters
1653
+ // by the EXPLICIT flag (`skipSessionScoped:true` inside the helper), so only task-scoped children take
1654
+ // the "user" marker; session-scoped ones a cancelled parent deliberately does NOT stop stay unmarked
1655
+ // (CC Backgrounded semantics). Owner = sessionId (this leg's canonical key), NOT the run-row taskId.
1656
+ //
1657
+ // 🔴 位置(#168 件3,与 /v1/tasks/stream 同族):归因规则只有**抢到这条 run 的腿**才有资格挂 ——
1658
+ // `markResuming` 输掉的那一支(别的副本已经在 resume,或行已被 reap)在上面就 409 走人,从此一次
1659
+ // 都不注册。本腿此前把它挂在 CAS 之前,只是因为 `cancelCtrl` 的唯一 abort 源(inflightRuns / 心跳
1660
+ // 轮询)也都在 CAS 之后注册才没出事 —— 那是巧合不是保证。
1661
+ markChildrenStoppedByUserOnAbort(cancelCtrl.signal, sessionId, principal);
1651
1662
  // MF-Fleet (#7): the row is now `running` again → re-publish it LIVE to the fleet (re-appears as the same row
1652
1663
  // that was sitting "waiting" while parked). After onStart only — never if markResuming lost the CAS (a sibling
1653
1664
  // owns the row). onTerminal at every settle below keeps the row's lifecycle in lock-step with the run-store row.
@@ -1952,7 +1963,7 @@ export function createHttpServer(rawDeps) {
1952
1963
  case "wiring_manifest":
1953
1964
  await flush();
1954
1965
  await append("wiring_manifest", wiringManifestEventData(ev));
1955
- break; // 🔴 core 5.14.0 design/173:resume 腿自证清单(每条腿发自己那份)。**同一个**构造器剥 `governance` 段(audience:"operator",no projection ⇒ do not disclose)——三腿同源,分头挑键即下一次泄露
1966
+ break; // 🔴 core 5.14.0 design/173:resume 腿自证清单(每条腿发自己那份)。本腿与 durable 账本腿共用**同一个**租户面构造器,剥 `governance` 段(audience:"operator",no projection ⇒ do not disclose)——账本写时不知道未来读者是谁,存全量 = 把 operator-only 面写进租户可读介质。⚠️ 「三腿同源」自 #154 件一起不再成立:live sync 腿(routes/tasks.ts)按连接身份分叉,operator 连接走 `wiringManifestOperatorEventData`。所以判据是「**durable 两腿恒剥、live 腿按身份分叉**」,不是「分头挑键即泄露」——别照旧注把已落地的 operator 面当泄露删掉,也别给第四条腿抄错构造器:先问这条腿知不知道读者是谁
1956
1967
  case "human_input":
1957
1968
  await flush();
1958
1969
  await append("human_input", humanInputEventData(ev));
@@ -2802,7 +2813,10 @@ function cors(res, req, origins, principalHeader) {
2802
2813
  // 后五项都是**真被 handler 读**的自定义头:`idempotency-key`(tasks/runs/images)、
2803
2814
  // `x-detach-on-disconnect`(tasks 断连不中止)、direct-door 决策证明三件套(approvals /decide 的
2804
2815
  // crypto 绑定腿 —— 缺了它,direct-door worker 上的 web 审批面在浏览器里根本用不了)。
2805
- res.setHeader("access-control-allow-headers", `content-type, authorization, last-event-id, ${principalHeader}, idempotency-key, x-detach-on-disconnect, x-approval-principal-token, x-approval-mac, x-approval-mac-kid, if-none-match`);
2816
+ res.setHeader("access-control-allow-headers",
2817
+ // direct-door 三件套用 security.ts 的具名导出插值(与同句的 `${principalHeader}` 同姿势)——手抄的
2818
+ // 头名漏改在这一行是**静默**的:预检挡下 ⇒ 请求到不了 handler,任何一处都不会报错。
2819
+ `content-type, authorization, last-event-id, ${principalHeader}, idempotency-key, x-detach-on-disconnect, ${PRINCIPAL_TOKEN_HEADER}, ${APPROVAL_MAC_HEADER}, ${APPROVAL_MAC_KID_HEADER}, if-none-match`);
2806
2820
  // 🔴 expose-headers 此前**整个缺席** ⇒ fetch 类客户端读不到 `X-Task-Id`(routes/tasks.ts 经 sseHeaders
2807
2821
  // 下发的 durable rewind handle)。tasks.ts 的 G15 meta 帧注释把这归因为「EventSource 与部分代理读不到
2808
2822
  // 响应头」—— 对 EventSource 成立,但对 fetch 客户端真因是这里缺 expose,meta 帧只是绕过。
@@ -1,4 +1,4 @@
1
- import { sendJson, sendError, sseHeaders } from "./send.js";
1
+ import { sendJson, sendError, sseHeaders, SSE_MAX_STREAM_MS, SSE_HEARTBEAT_IDLE_MS } from "./send.js";
2
2
  /** #151 车3 §5.3:preamble 的**有界**等待上限。挂起的 store 不许拖住开流——开流是壳的主路径,
3
3
  * 而 preamble 只是一个锦上添花的对账基准。超时即按「无卡集基准」继续(与今天等同)。 */
4
4
  export const SSE_PREAMBLE_TIMEOUT_MS = 2_000;
@@ -83,7 +83,6 @@ export async function streamSseLog(req, res, provider, id, staleMs) {
83
83
  }
84
84
  }
85
85
  const start = Date.now();
86
- const MAX_MS = 15 * 60 * 1000;
87
86
  let lastBeat = Date.now();
88
87
  const write = (events) => {
89
88
  for (const ev of events) {
@@ -116,7 +115,7 @@ export async function streamSseLog(req, res, provider, id, staleMs) {
116
115
  res.write(`event: ${sf.event}\ndata: ${JSON.stringify(sf.data)}\n\n`);
117
116
  break;
118
117
  }
119
- if (Date.now() - start > MAX_MS) {
118
+ if (Date.now() - start > SSE_MAX_STREAM_MS) {
120
119
  // BL-16: cap reached but the run continues — emit a terminal reconnect signal (not a silent close, which
121
120
  // a client couldn't distinguish from a completed run). Distinct from the `failed`/`done` run events.
122
121
  res.write(`event: error\ndata: ${JSON.stringify({ type: "error", errorCode: "STREAM_MAX_DURATION", message: "stream reached its 15-minute cap — reconnect with Last-Event-ID to continue (the run is still active)" })}\n\n`);
@@ -124,7 +123,7 @@ export async function streamSseLog(req, res, provider, id, staleMs) {
124
123
  }
125
124
  if (events.length === 0) {
126
125
  // Heartbeat during a long idle tail (e.g. a debate phase) so the relay's idle timeout doesn't fire.
127
- if (Date.now() - lastBeat > 15_000) {
126
+ if (Date.now() - lastBeat > SSE_HEARTBEAT_IDLE_MS) {
128
127
  res.write(`event: heartbeat\ndata: {}\n\n`);
129
128
  lastBeat = Date.now();
130
129
  }
@@ -235,8 +235,13 @@ export interface TaskRequestBody {
235
235
  * 后下一轮把占位符换成真 schema。
236
236
  * 为什么要上 wire:cli 的 #181 取证表明,忠实跟随广告 schema 解码的 provider(openai-completions 车道)
237
237
  * 在 `static` 下会**无界循环** —— 每轮发 `{}` → invalidArgumentsRejection → 下一轮广告仍空,纠错回路
238
- * 结构上无效。core 的缺省是 `spec.toolMaterializeStrategy ?? env ?? "static"`,进程级 env 说不了
239
- * 「每个模型」的话(模型目录车道 per-model api 可混),所以必须有一个**按任务**的位子。
238
+ * 结构上无效。core 的缺省链是 `spec.toolMaterializeStrategy ?? env ?? <core 缺省>`,而**进程级 env 说不了
239
+ * 「每个模型」的话**(模型目录车道 per-model api 可混),所以必须有一个**按任务**的位子——这条理由与
240
+ * core 缺省是哪个词无关,故不随其变。
241
+ * ⚠️ core 缺省本身已翻:5.15.0 BREAKING 起 `<core 缺省>` = **`"swap"`**(5.14 世代是 `"static"`;
242
+ * core 把 `"static"` 重新定性为「a strict explicit opt-in」)。本注上一版还写着 `?? "static"`,照它推
243
+ * 「不挂键 ⇒ 落 static」会反。缺省词的属主是 core,本仓不复述第二份——要确认取值去读 core 的
244
+ * `prepare-task` 与其 CHANGELOG。
240
245
  * 缺省不挂键(交给 core 的 env/缺省链);未知词 400 fail-loud(与 promptProfile/permissionMode 同姿势 ——
241
246
  * 静默折缺省会让调用方以为切了策略却没切)。 */
242
247
  toolMaterializeStrategy?: "static" | "swap";
@@ -38,6 +38,16 @@ export type PullDiffResult = {
38
38
  truncated?: boolean;
39
39
  exitCode?: number;
40
40
  };
41
+ /**
42
+ * 一次 diff 的字节帽(pull 腿与 push/上行腿**共用同一个数**)。
43
+ *
44
+ * 🔴 exported 是判据的一部分(2026-08-08,#176 二轮扫描 finding):`diffup.ts` 原先另抄了一份同值常量,
45
+ * 而它**本就已经** `import { safeRev } from "./diffout.js"` —— 依赖边在,共享零成本。两份手抄的后果不是
46
+ * 「多写一行」,是**单边改值后上行/pull 两腿对同一个 diff 的过/拒判定分家**:同一份补丁在一条腿上过、
47
+ * 另一条腿上被截断,而两处都「按自己的常量」正确工作,没有任何一处会报错。
48
+ * (同族先例:`checkpoint-store-sql.ts` 的 `export const TERMINAL_GRACE_MS`。)
49
+ */
50
+ export declare const DEFAULT_MAX_DIFF_BYTES: number;
41
51
  export declare function safeRev(rev: string): boolean;
42
52
  /**
43
53
  * Pull `<baseSha>..<ref>` from a worker container as a `git format-patch` series. Never throws — failures
@@ -1,6 +1,18 @@
1
- const DEFAULT_MAX_DIFF_BYTES = 5 * 1024 * 1024;
1
+ /**
2
+ * 一次 diff 的字节帽(pull 腿与 push/上行腿**共用同一个数**)。
3
+ *
4
+ * 🔴 exported 是判据的一部分(2026-08-08,#176 二轮扫描 finding):`diffup.ts` 原先另抄了一份同值常量,
5
+ * 而它**本就已经** `import { safeRev } from "./diffout.js"` —— 依赖边在,共享零成本。两份手抄的后果不是
6
+ * 「多写一行」,是**单边改值后上行/pull 两腿对同一个 diff 的过/拒判定分家**:同一份补丁在一条腿上过、
7
+ * 另一条腿上被截断,而两处都「按自己的常量」正确工作,没有任何一处会报错。
8
+ * (同族先例:`checkpoint-store-sql.ts` 的 `export const TERMINAL_GRACE_MS`。)
9
+ */
10
+ export const DEFAULT_MAX_DIFF_BYTES = 5 * 1024 * 1024;
2
11
  const DEFAULT_READ_TIMEOUT_MS = 30_000;
3
- /** A safe git rev token: sha, branch/tag name, or `A..B`. No shell metacharacters, no leading dash. */
12
+ /** A safe git rev token: sha, branch/tag name, or `A..B`. No shell metacharacters, no leading dash.
13
+ * 🔗 近亲判据:`leader/planner.ts` 的 branch 校验用同一套字符类 + 同一个 200 上限 + 同样拒前导 `-`,
14
+ * 只在 `..` 上有意分家(这里要放行 `A..B` 区间形,那边全拒)。差异与候裁说明写在 planner 那处
15
+ * (#176 二轮扫描 finding)——单边加字符类成员时,请两处一起看。 */
4
16
  const SAFE_REV = /^[A-Za-z0-9._/-]{1,200}$/;
5
17
  export function safeRev(rev) {
6
18
  return SAFE_REV.test(rev) && !rev.startsWith("-") && !rev.includes("..--");
@@ -14,9 +14,10 @@
14
14
  * A worker that skips the upload surfaces as `{ok:false}` → merge quarantines it (honest failure, no hang).
15
15
  * pullDiff stays as the compat path for static (leader-owned) envs — M2 降兼容.
16
16
  */
17
- import { safeRev } from "./diffout.js";
17
+ // 字节帽与 `safeRev` 同源取自 pull 腿(理由见 diffout.ts 的 DEFAULT_MAX_DIFF_BYTES 顶注:同值双抄一旦
18
+ // 单边改值,上行/pull 两腿对同一个 diff 的过/拒判定就分家,且两边都「按自己的常量」静默正确)。
19
+ import { safeRev, DEFAULT_MAX_DIFF_BYTES } from "./diffout.js";
18
20
  import { OBJECT_STORE_IO_TIMEOUT_MS } from "../plugins/blob-backend.js";
19
- const DEFAULT_MAX_DIFF_BYTES = 5 * 1024 * 1024;
20
21
  /** The staged upload script. `repoDir`/`workDir` are deployment config; `baseSha` is interpolated into a
21
22
  * shell line, so it is re-validated HERE (fail-closed) — callers should already have applied diffout's
22
23
  * safeRev rules, but this function is the last line before the shell. The patch is written OUTSIDE the repo
@@ -91,6 +91,13 @@ export function validateSubtasks(arr, opts = {}) {
91
91
  const branch = typeof e.branch === "string" && e.branch.trim() ? e.branch.trim() : `worker/${workerId}`;
92
92
  // Reject a leading "-" (would be parsed as an option by `git checkout -b <branch>`), ".." (ref escape), and
93
93
  // an over-long ref (git rejects it anyway; cap keeps the value tidy). The char set is shell-safe.
94
+ //
95
+ // 🔗 近亲判据(2026-08-08,#176 二轮扫描 finding):`leader/diffout.ts` 的 `safeRev` 用**同一套**字符
96
+ // 类 `[A-Za-z0-9._/-]`、**同一个** 200 长度上限、**同样**拒前导 `-`。唯一的真差异在 `..`:safeRev 只拒
97
+ // `..--`(rev 要支持 `A..B` 区间形),这里全拒 `..`(branch **名**里 `..` 无合法用途,git 自己也拒)
98
+ // —— 是**有意收紧**,不是抄漏。两处此前零互指、无共享基元、无跨处钉;单边加一个字符类成员时另一边
99
+ // 不会有任何提示。是否抽一个共享基元(比如 `refToken(s, { allowRange })`)属结构面决定,候裁;本批
100
+ // 只把「近亲关系 + 差异所在」两侧成文。
94
101
  if (branch.length > 200 || !/^[A-Za-z0-9._/-]+$/.test(branch) || branch.startsWith("-") || branch.includes("..")) {
95
102
  throw new Error(`sub-task ${workerId}: invalid branch`);
96
103
  }
package/dist/main.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
- import { Runner, TtlSessionStore, uuidv7, defaultTaskRegistry, combinePolicies, createAllowDenyPolicy, workflowsCapability, createWebFetchSummarizer, resolveTaskModel as coreResolveTaskModel, probeSearchBackend, describeStaticWiring } from "@sema-agent/core";
4
+ import { Runner, TtlSessionStore, uuidv7, defaultTaskRegistry, createAllowDenyPolicy, workflowsCapability, createWebFetchSummarizer, resolveTaskModel as coreResolveTaskModel, probeSearchBackend, describeStaticWiring } from "@sema-agent/core";
5
5
  import { createSessionTitler } from "./session-titler.js";
6
6
  import { posIntEnv } from "./session-watch.js";
7
7
  import { selectEnvironmentTool } from "./capabilities/select-environment-tool.js";
@@ -14,7 +14,7 @@ import { ensureChildSessionDurableWithPromotion } from "./plugins/session-store.
14
14
  import { ForkRoutingSessionStore } from "./plugins/fork-routing-session-store.js";
15
15
  import { webSearchConfigFromEnv, createWebSearchBackend, setWebSearchBadPayloadObserver, shouldProbeWebSearchOnBoot } from "./plugins/web-search.js";
16
16
  import { createAuthorizer, encodeCheckpointScope } from "./security.js";
17
- import { assertGateIntentServiceable, createDurableAskPolicy, hasOperatorGateIntent } from "./approval.js";
17
+ import { assertGateIntentServiceable, hasOperatorGateIntent } from "./approval.js";
18
18
  import { loadSkills } from "./capabilities/skills.js";
19
19
  import { GiteaClient } from "./capabilities/repo-tools.js";
20
20
  import { buildScenarios, builtinScenarioDetails } from "./capabilities/scenarios.js";
@@ -38,7 +38,8 @@ import { createHookLlm } from "./hooks/hook-llm.js";
38
38
  import { startFleetClientFromEnv } from "./fleet-client.js";
39
39
  import { reclaimOrphansAtBoot } from "./boot-reclaim.js";
40
40
  import { createConfigCenterRuntime } from "./boot/config-center.js";
41
- import { createDurableQuestionGate, createResolveSpec } from "./boot/resolve-spec.js";
41
+ import { createResolveSpec } from "./boot/resolve-spec.js";
42
+ import { createParkedReviveInheritedGate } from "./boot/parked-revive-gate.js";
42
43
  import { startReapers } from "./boot/reapers.js";
43
44
  import { openStores } from "./boot/stores.js";
44
45
  import { createBudgetAndTracing } from "./boot/budget-tracing.js";
@@ -184,7 +185,7 @@ async function main() {
184
185
  // baseline, so a task-hooks fold (resolveSpec) shadows neither.
185
186
  const deploymentHooks = toolTracer ? composeHooks(createPermissionDeniedMeter(metrics), toolTracer) : createPermissionDeniedMeter(metrics);
186
187
  // design/158 A10:workflow 编排面搬到 src/boot/workflow-orchestration.ts(逐字;构造次序契约见该文件头注)。
187
- const { sqlWorkflowRunStore, workflowNotifyJournal, workflowCompletionInbox, deliverWorkflowCompletion, workflowNotifyGate, fleetBus, workflowRunStore, workflowJournalStore, outcomeSink, workflowAgentRegistry, subagentSteerRegistry, } = createWorkflowOrchestration({ config, logger, metrics, localRoot, backend, getRunStore: () => runStore });
188
+ const { sqlWorkflowRunStore, workflowNotifyJournal, workflowCompletionInbox, deliverWorkflowCompletion, workflowNotifyGate, fleetBus, workflowRunStore, workflowJournalStore, outcomeSink, workflowRecoverOpts, workflowAgentRegistry, subagentSteerRegistry, } = createWorkflowOrchestration({ config, logger, metrics, localRoot, backend, getRunStore: () => runStore });
188
189
  // design/158 A10:活体协调器 + SendUserFile 工具面搬到 src/boot/coordinators.ts(逐字;durableEnabled 的
189
190
  // 「必须早于 runnerDeps 求值」次序契约见该文件头注)。
190
191
  const { elicitation, question, toolApproval, durableEnabled, streamApprovalGate, sendUserFileEmitter, sendFileLedger, sendUserFileToolSpec } = createLiveCoordinators({ config, logger, backend, sendUserFileTaskEnvs });
@@ -406,8 +407,15 @@ async function main() {
406
407
  // Finding #2 (edge case): the single-user auto-accept tool-policy BASELINE applies ONLY when the operator
407
408
  // expressed NO gate intent. If they SET approval flags (APPROVAL_REQUIRE/APPROVAL_DENY/DURABLE_APPROVAL) but no
408
409
  // backing store is wired (e.g. DB_BACKEND=local → no checkpointStore ⇒ durableEnabled
409
- // false), that is a MISCONFIG — do NOT mask it with allow-all; leave toolPolicy undefined so core's UNGATED warning
410
- // still surfaces it. So the baseline needs single-user turnkey ∧ zero expressed gate intent.
410
+ // false), that is a MISCONFIG — do NOT mask it with allow-all; leave toolPolicy undefined. So the baseline needs
411
+ // single-user turnkey ∧ zero expressed gate intent.
412
+ // 🔴 #177 后本段的**信号面**已换人(原文「so core's UNGATED warning still surfaces it」已过期):守卫集
413
+ // (SENSITIVE_WRITE_PATTERNS,缺省非空)现在从 governance 拍无条件铸一条 DENY 基线 ⇒ 缺省部署的
414
+ // `spec.toolPolicy` 恒在场 ⇒ core 的 `hasEffectAwareGate`(prepare-task,判据是 policyLayers.length>0)
415
+ // 恒为真,那条 UNGATED onError 不再触发。补偿件在 boot/resolve-spec.ts:同一形(durable 门关 ∧ 单用户
416
+ // 基线不适用 ∧ 守卫集开)在 boot 期发一次 `tool_policy_only_sensitive_baseline` warn,把「守卫段之外
417
+ // 无裁决」这件事按我们自己的话说出来。本行留 `undefined` 仍是对的(不拿 allow-all 掩盖 misconfig),
418
+ // 只是它不再是那条信号的产地。
411
419
  // 单源在 approval.ts(与 inv#2 同文件)—— 这里原是内联的三格枚举,漏了 approvalNeverAuto,详见该函数顶注。
412
420
  const operatorGateIntent = hasOperatorGateIntent(config);
413
421
  // D3-F1(全窗复审,HIGH):门意图已表达但唯一门形(durable)不可服务 ⇒ boot 拒启带双出路指引。
@@ -622,29 +630,12 @@ async function main() {
622
630
  // checkpointStore 同实例天然成立;per-request 项如 webSearch 对 revive 无关——objective 在 core
623
631
  // driveParkedResume 里被丢弃)。checkpointStore/agentStore 任一缺席 = park 本就不可达,分支不接线。
624
632
  const parkedReviveTool = checkpointStore && backgroundAgentStore ? scenarios.default?.({})?.tools.find((t) => t.name === "Agent") : undefined;
625
- // [1596]/[1597] 跨进程父约束重供席工厂:host 任务的 toolPolicy 解析槽按部署配置**同参重建**
626
- // (resolveSpec durableEnabled 分支的同款折叠:durableQuestion + durableAsk 四旋钮),单层链
627
- // count=1,durableMandate=true(host durableApproval durable 车道无 live onAsk 的恒形)。
628
- // exempt 探针锚 = row.rootSessionId——与 parked decide 的 remember grant 锚同键([1591] 候裁②),
629
- // 重建链上的豁免语义与原链一致。重建不出(无 root 锚)= 不供席 = core pre-CAS 门诚实拒(现状)。
630
- // #152 复审 A2:「同参重建」是本工厂自述的不变式,所以 question 门必须调 resolve-spec 的**同一个**
631
- // 工厂并传同一个活体面,不能自折 core 原形——分腿改动只落一侧就是两份拷贝开始漂移的那一刻。
632
- // 赎回腿今天无 question ALS ctx(/decide 路由触发,core driveParkedResume 内驱动)⇒ 判 ask,与原形同判。
633
+ // [1596]/[1597] 跨副本父约束重供席工厂:host 任务的 toolPolicy 解析槽按**当前**部署配置重建
634
+ // 「部署 操作员」两层完整链(design/181 件二收编;实现与全部理由在 boot/parked-revive-gate.ts,
635
+ // 提出去的唯一理由是 main.ts 顶层 `void main()` 让那条腿的运行期语义在原地一格都钉不住)。
636
+ // 构造条件逐字保持:裸 Agent 工具在场 部署真开了 durable 审批。
633
637
  const parkedReviveInheritedGate = parkedReviveTool && config.durableApproval
634
- ? (row) => ({
635
- parentConstraints: [{
636
- policy: combinePolicies(createDurableQuestionGate(question), createDurableAskPolicy({
637
- requireApproval: config.approvalRequire, deny: config.approvalDeny, autoBudget: config.approvalAutoBudget, neverAuto: config.approvalNeverAuto,
638
- ...(approvalExemptionStore && row.rootSessionId
639
- ? {
640
- exempt: (toolName) => approvalExemptionStore.has(row.rootSessionId, toolName),
641
- onExempted: (toolName, rawToolName) => logger.info("approval_exempted", { sessionId: row.rootSessionId, toolName, rawToolName, leg: "parked_revive" }),
642
- }
643
- : {}),
644
- })),
645
- durableMandate: true,
646
- }],
647
- })
638
+ ? createParkedReviveInheritedGate({ config, question, approvalExemptionStore, logger, localRoot })
648
639
  : undefined;
649
640
  // 场景详情:内建 details 必须在 overlay 合并【前】构建(探针要打纯内建工厂,不是被 center 顶掉的);
650
641
  // center details 随 overlay 同判定源盖同名——source 语义与 selectScenario 实际取用永一致(约定①)。
@@ -663,7 +654,7 @@ async function main() {
663
654
  const reaper = startReapers({
664
655
  config, logger, metrics, localRoot, backend, subRunner, runStore, checkpointStore,
665
656
  rateLimiter, costQuota, toolResultStore, fileSnapshotStore, taskAttachmentStore, imageBakes, worktreeReap,
666
- workflowNotifyGate, workflowJournalStore, sqlWorkflowRunStore, workflowNotifyJournal, rosterStore,
657
+ workflowNotifyGate, workflowRecoverOpts, workflowJournalStore, sqlWorkflowRunStore, workflowNotifyJournal, rosterStore,
667
658
  backgroundAgentStore, mailboxStore, toolApproval,
668
659
  getRunDenySweep: () => runDenySweep,
669
660
  });
@@ -772,6 +763,10 @@ async function main() {
772
763
  // Undefined-safe: the env-only/no-backend worker 501s the routes.
773
764
  backend: backend ? backend : undefined,
774
765
  };
766
+ // [3193] 病理1:fleet 快照的 recovery 屏障(deferred 形,声明先于 deps 装配、落定在下方 recover 块——
767
+ // 对装配顺序鲁棒;无 notify gate 的部署在 recover 块 else 臂立即落)。
768
+ let resolveFleetRecovery;
769
+ const fleetRecoveryDone = new Promise((r) => { resolveFleetRecovery = r; });
775
770
  /** 进程内活体协调面(本副本 only,重启即空:HITL 协调器/可 steer 句柄注册表/总线/收件箱) */
776
771
  const coordinators = {
777
772
  elicitation: elicitation ? elicitation : undefined, // E23: live-only inbound-elicitation coordinator (respond route + ALS wraps)
@@ -781,6 +776,7 @@ async function main() {
781
776
  subagentSteerRegistry, // C2 (core 1.219): steerable Task-subagent handle registry (POST /v1/runs/:id/subagents/:target/steer)
782
777
  workflowCompletionInbox: workflowCompletionInbox ? workflowCompletionInbox : undefined, // P1 ①②: drain → workflow_complete frame on stream-open
783
778
  fleetBus,
779
+ fleetRecoveryDone, // [3193] 病理1:快照组装的有界等待信号(recover 块落定)
784
780
  // [1196] session 级 SSE 订阅:watch registry(N 订阅共享一条探针,自适应拍距,无订阅即停)。谓词与
785
781
  // /head 探针同门(getLeafId+ownerOf);缺 store 面不建=路由 501+capabilities.sessionEvents=false。
786
782
  sessionWatch: sessionWatchRegistry ? sessionWatchRegistry : undefined, // [1196] 建于 purgeSession 之前(delete 即 dropSession 的同副本围栏)
@@ -810,6 +806,10 @@ async function main() {
810
806
  memorySync: memoryExportBackend && memorySyncCursors
811
807
  ? (scope, syncReq) => performMemorySync(memoryExportBackend, memorySyncCursors, scope, syncReq)
812
808
  : undefined,
809
+ // design/170 件A §7 收编:memory-policy 面的 `org:` 属主门与 core 准入 seam 读**同一个**目录实例
810
+ // (`createOrgMemoryAdmissionWiring` 的产物),两面因此共享 TTL 缓存/退避窗/gen 高水位 —— 一个进程
811
+ // 对「谁属于 org:acme」只有一个答案。缺席(无 center 且无 env 表)⇒ 策略面 `org:` 键仍 operator-only。
812
+ orgMemoryDirectory: orgMemoryAdmission.directory,
813
813
  // sessionMirror 观测面(server 非执法端,论证在 ServiceDeps.sessionMirrorRuling):
814
814
  // 与 executionRuling 同车同缓存(零额外 center RTT);无 center/dry-run ⇒ 不接线,观测面暗、零行为差。
815
815
  sessionMirrorRuling: principalCaps
@@ -1028,10 +1028,18 @@ async function main() {
1028
1028
  // blocked on it (recovery only re-fires completion notifications — independent of serving new requests). A
1029
1029
  // sweep throw is logged, never fatal.
1030
1030
  if (workflowNotifyGate) {
1031
+ // [2995]/[2999] opts 单点合成在 workflow-orchestration(可见性回灌 + replica-local 形 boot 孤儿判死)。
1032
+ // [3193] 病理1:recover 收官(成败皆算)即落 fleet 快照屏障——「serving 不等 recovery」对**提交请求**
1033
+ // 仍成立,但 fleet 快照组装现在对它做有界等待(routes/fleet.ts,3s cap):SIGKILL 后壳重连不再
1034
+ // 与 boot 判死赛跑(cli [3193] 插桩实证的竞态)。
1031
1035
  void workflowNotifyGate
1032
- .recover({ orphanGraceMs: config.workflowOrphanGraceMs })
1036
+ .recover(workflowRecoverOpts)
1033
1037
  .then((tally) => logger.info("workflow_notify_recovered", tally))
1034
- .catch((err) => logger.warn("workflow_notify_recover_failed", { err: String(err) }));
1038
+ .catch((err) => logger.warn("workflow_notify_recover_failed", { err: String(err) }))
1039
+ .finally(() => resolveFleetRecovery());
1040
+ }
1041
+ else {
1042
+ resolveFleetRecovery();
1035
1043
  }
1036
1044
  // design/158 A10:收尾段搬到 src/boot/shutdown.ts(逐字)。⚠️ 调用点必须留在 listen 之后 —— 位置即契约,
1037
1045
  // 理由(信号注册时点/三信号相对次序/clearInterval 先于 server.close)见该文件头注。
@@ -11,7 +11,7 @@ export interface FailOpenTagEntry {
11
11
  export declare const FAIL_OPEN_TAGS: {
12
12
  readonly "server.hitl.frame-undelivered-stream-closed": {
13
13
  readonly cls: "P-DEBT";
14
- readonly note: "HITL 的 **open** 帧(question / elicitation)写向一条已断/已关的 SSE 连接 ⇒ 静默丢弃,上游据此把投递记成成功,问到 TTL 才按无人应答结算。方向(no-op 而非 throw)是既有的刻意决定,本条只保证它不再无声。";
14
+ readonly note: "HITL 的 **open** 帧写向一条已断/已关的 SSE 连接 ⇒ 静默丢弃,上游据此把投递记成成功,该 ask 挂到 TTL 才按无人应答结算。⚠️ #173 后 question 开帧改走断流 THROW(立即结算 unavailable),因此本 tag 在产的只剩 **elicitation** 一族——读计数时勿把它当作 question 的人在环缺口。no-op 而非 throw 对 elicitation 仍是既有的刻意决定,本条只保证它不再无声。";
15
15
  };
16
16
  readonly "server.stream.frame-dropped-stream-closed": {
17
17
  readonly cls: "F";
@@ -19,7 +19,7 @@ export declare const FAIL_OPEN_TAGS: {
19
19
  };
20
20
  readonly "server.question.open-frame-undelivered": {
21
21
  readonly cls: "P-DEBT";
22
- readonly note: "AskUserQuestion 的 open 帧投递失败 ⇒ headless 默认结算,模型不问人自己往下判。方向是刻意的产品姿态(绝不把 run 挂死),但一次投递故障确实让人在环这道门被跳过——记债,不当合法兜底。";
22
+ readonly note: "AskUserQuestion 的 open 帧投递失败 ⇒ 该问按 unavailable 结算(#166 前是合成空答)。⚠️ 本臂**恒在门判 allow 之后**(工具已在执行),core 已过挂起点 ⇒ 落点**不是 park**:普通腿发 declined_unavailable 自答续跑卡,赎回既有审批的腿返 isError(engine 拒绝自答)。也就是说这一形**不铸 checkpoint、不可恢复**,人在环这道门被一次投递故障真的跳过了(门判 allow 之前就不可达的那些腿走的是 durable park,不经本臂)。「绝不把 run 挂死」是刻意的产品姿态,但债要在遥测里显形——记债,不当合法兜底。";
23
23
  };
24
24
  readonly "server.elicitation.open-frame-undelivered": {
25
25
  readonly cls: "F";
@@ -37,6 +37,14 @@ export declare const FAIL_OPEN_TAGS: {
37
37
  readonly cls: "F";
38
38
  readonly note: "已验签的提示词工件回填本地缓存失败。工件本身已返回给调用方;代价是下次同 digest 还要再取一次网络。";
39
39
  };
40
+ readonly "server.fleet.snapshot-terminal-window-unavailable": {
41
+ readonly cls: "F";
42
+ readonly note: "`/v1/fleet/stream` 连接时快照的**近期终态行窗**(#189:引擎重启后 boot 判死的 workflow 行,pull 自 durable store)读失败或超预算(2s)⇒ 本次快照少这几行历史。放行的最坏后果=面板首屏看不到刚结束的 workflow(与本修之前的行为等同,不是新损失);真源不受影响(`GET /v1/workflows` 照常)。方向刻意 fail-open:一次慢查询不该把整条 SSE 的握手拖住——少几行是难看,连不上流是坏掉。";
43
+ };
44
+ readonly "server.fleet.subscriber-callback-threw": {
45
+ readonly cls: "F";
46
+ readonly note: "fleet bus 某订阅回调抛错 ⇒ 该回调本帧作废,其余订阅方与发布方不受影响。隔离是承重的:扇出同步,修前异常会传回发布方 put/update 投影点,core 持久化 catch{} 且不推进 storeRev ⇒ durable 行冻在 running 而 notify 已 ack(#183 复审 R3 HIGH)。丢的只是一个消费方的一帧渲染,故 F 类;但必须留痕——静默吞掉等于订阅方病灶永不显形。";
47
+ };
40
48
  };
41
49
  /** 词表键推导的闭集类型——未登记的 tag 传不进 {@link recordFailOpen}(编译期拒)。 */
42
50
  export type FailOpenTag = keyof typeof FAIL_OPEN_TAGS;
@@ -46,6 +54,13 @@ export type FailOpenTag = keyof typeof FAIL_OPEN_TAGS;
46
54
  * 一刀切记同一个 tag 是错的:承载 HITL 帧的那条闭包同时驮着完成面包屑与其它运行流帧,于是**一次**
47
55
  * 断连会让保护型计数涨两次(open 一次、complete 又一次),真正的 open 失败率被面包屑的假阳性盖住。
48
56
  * 只有 open 帧是"人在环这道门被跳过"的证据,其余都是体验损失。
57
+ *
58
+ * ⚠️ **两条 question 臂在生产上已不可达**(#173 之后,2026-08-07):本函数唯一的生产调用点是
59
+ * `routes/tasks.ts` 的 `emitAsk` 断流臂,而 question 与 question_complete 两型帧都先过同文件
60
+ * `emitQuestion` 的断流守卫(两道守卫同步相邻、其间无 await)并在断流时 throw,永不抵达这里;另一条
61
+ * question 路(`runs.ts` 的 durable append)根本不经 emitAsk。在产的只剩 elicitation 族与 default。
62
+ * 两臂**保留不删**:它们是本映射的语义定义(单测直打的纯函数),elicit 侧若哪天照 question 收紧、或
63
+ * 新增第四条 emit 腿忘了守卫,留着的臂是正确落点而不是死码。读遥测时按上面这段判在产分布。
49
64
  */
50
65
  export declare function failOpenTagForDroppedFrame(frameType: string): FailOpenTag;
51
66
  /** {@link createFailOpenRecorder} 返回的活对象(有行为、有状态 ⇒ `create*` 而非 `build*`)。 */
@@ -34,7 +34,7 @@ import { appendFileSync } from "node:fs";
34
34
  export const FAIL_OPEN_TAGS = {
35
35
  "server.hitl.frame-undelivered-stream-closed": {
36
36
  cls: "P-DEBT",
37
- note: "HITL 的 **open** 帧(question / elicitation)写向一条已断/已关的 SSE 连接 ⇒ 静默丢弃,上游据此把投递记成成功,问到 TTL 才按无人应答结算。方向(no-op 而非 throw)是既有的刻意决定,本条只保证它不再无声。",
37
+ note: "HITL 的 **open** 帧写向一条已断/已关的 SSE 连接 ⇒ 静默丢弃,上游据此把投递记成成功,该 ask 挂到 TTL 才按无人应答结算。⚠️ #173 后 question 开帧改走断流 THROW(立即结算 unavailable),因此本 tag 在产的只剩 **elicitation** 一族——读计数时勿把它当作 question 的人在环缺口。no-op 而非 throw 对 elicitation 仍是既有的刻意决定,本条只保证它不再无声。",
38
38
  },
39
39
  "server.stream.frame-dropped-stream-closed": {
40
40
  cls: "F",
@@ -42,7 +42,7 @@ export const FAIL_OPEN_TAGS = {
42
42
  },
43
43
  "server.question.open-frame-undelivered": {
44
44
  cls: "P-DEBT",
45
- note: "AskUserQuestion 的 open 帧投递失败 ⇒ headless 默认结算,模型不问人自己往下判。方向是刻意的产品姿态(绝不把 run 挂死),但一次投递故障确实让人在环这道门被跳过——记债,不当合法兜底。",
45
+ note: "AskUserQuestion 的 open 帧投递失败 ⇒ 该问按 unavailable 结算(#166 前是合成空答)。⚠️ 本臂**恒在门判 allow 之后**(工具已在执行),core 已过挂起点 ⇒ 落点**不是 park**:普通腿发 declined_unavailable 自答续跑卡,赎回既有审批的腿返 isError(engine 拒绝自答)。也就是说这一形**不铸 checkpoint、不可恢复**,人在环这道门被一次投递故障真的跳过了(门判 allow 之前就不可达的那些腿走的是 durable park,不经本臂)。「绝不把 run 挂死」是刻意的产品姿态,但债要在遥测里显形——记债,不当合法兜底。",
46
46
  },
47
47
  "server.elicitation.open-frame-undelivered": {
48
48
  cls: "F",
@@ -60,6 +60,14 @@ export const FAIL_OPEN_TAGS = {
60
60
  cls: "F",
61
61
  note: "已验签的提示词工件回填本地缓存失败。工件本身已返回给调用方;代价是下次同 digest 还要再取一次网络。",
62
62
  },
63
+ "server.fleet.snapshot-terminal-window-unavailable": {
64
+ cls: "F",
65
+ note: "`/v1/fleet/stream` 连接时快照的**近期终态行窗**(#189:引擎重启后 boot 判死的 workflow 行,pull 自 durable store)读失败或超预算(2s)⇒ 本次快照少这几行历史。放行的最坏后果=面板首屏看不到刚结束的 workflow(与本修之前的行为等同,不是新损失);真源不受影响(`GET /v1/workflows` 照常)。方向刻意 fail-open:一次慢查询不该把整条 SSE 的握手拖住——少几行是难看,连不上流是坏掉。",
66
+ },
67
+ "server.fleet.subscriber-callback-threw": {
68
+ cls: "F",
69
+ note: "fleet bus 某订阅回调抛错 ⇒ 该回调本帧作废,其余订阅方与发布方不受影响。隔离是承重的:扇出同步,修前异常会传回发布方 put/update 投影点,core 持久化 catch{} 且不推进 storeRev ⇒ durable 行冻在 running 而 notify 已 ack(#183 复审 R3 HIGH)。丢的只是一个消费方的一帧渲染,故 F 类;但必须留痕——静默吞掉等于订阅方病灶永不显形。",
70
+ },
63
71
  };
64
72
  /**
65
73
  * 断流丢帧该记哪个 tag —— **按帧型分类**,纯函数(与写流的那条闭包解耦,才单测得动)。
@@ -67,13 +75,20 @@ export const FAIL_OPEN_TAGS = {
67
75
  * 一刀切记同一个 tag 是错的:承载 HITL 帧的那条闭包同时驮着完成面包屑与其它运行流帧,于是**一次**
68
76
  * 断连会让保护型计数涨两次(open 一次、complete 又一次),真正的 open 失败率被面包屑的假阳性盖住。
69
77
  * 只有 open 帧是"人在环这道门被跳过"的证据,其余都是体验损失。
78
+ *
79
+ * ⚠️ **两条 question 臂在生产上已不可达**(#173 之后,2026-08-07):本函数唯一的生产调用点是
80
+ * `routes/tasks.ts` 的 `emitAsk` 断流臂,而 question 与 question_complete 两型帧都先过同文件
81
+ * `emitQuestion` 的断流守卫(两道守卫同步相邻、其间无 await)并在断流时 throw,永不抵达这里;另一条
82
+ * question 路(`runs.ts` 的 durable append)根本不经 emitAsk。在产的只剩 elicitation 族与 default。
83
+ * 两臂**保留不删**:它们是本映射的语义定义(单测直打的纯函数),elicit 侧若哪天照 question 收紧、或
84
+ * 新增第四条 emit 腿忘了守卫,留着的臂是正确落点而不是死码。读遥测时按上面这段判在产分布。
70
85
  */
71
86
  export function failOpenTagForDroppedFrame(frameType) {
72
87
  switch (frameType) {
73
- case "question":
88
+ case "question": // 见头注:#173 后生产不可达(emitQuestion 先 throw),保留为映射定义
74
89
  case "elicitation":
75
90
  return "server.hitl.frame-undelivered-stream-closed";
76
- case "question_complete":
91
+ case "question_complete": // 同上,生产不可达
77
92
  return "server.question.complete-breadcrumb-dropped";
78
93
  case "elicitation_complete":
79
94
  return "server.elicitation.complete-breadcrumb-dropped";
@@ -68,7 +68,11 @@ export declare function promptManifestRecordOf(e: {
68
68
  wireSchemaDigest: string;
69
69
  }>;
70
70
  /** core 5.15.0(#181,[2872] 提货②):deferred 工具的披露策略自证——{deferredTools(数), strategy
71
- * ("static"|"swap"), source(四值:spec/env/api-default/default)}。诊断三键(数+两闭集词,零内容) */
71
+ * ("static"|"swap"), source(四值:`spec` / `env` / `default` / `degraded_no_direct_lane`)}。
72
+ * 诊断三键(数 + 两闭集词,零内容)。词表属主是 core(`core/trace.d.ts` 的 `source` 联合),本仓宽收
73
+ * ⇒ 词错不会红,所以照抄时务必对表:`degraded_no_direct_lane` = 直呼车道联锁把 `static` 请求收窄成
74
+ * `swap` 的那一格,正是运维最需要认出来的一格(本注上一版把它写成了不存在的 `api-default`,下游看板
75
+ * 照抄就会把真实的 degraded 读成未知词)。 */
72
76
  toolDisclosure?: {
73
77
  deferredTools: number;
74
78
  strategy: string;