@sema-agent/server 7.116.1 → 7.118.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 (131) hide show
  1. package/MIGRATION.md +195 -0
  2. package/USAGE.md +65 -41
  3. package/dist/approval-card.d.ts +82 -18
  4. package/dist/approval-card.js +46 -4
  5. package/dist/approval.d.ts +4 -12
  6. package/dist/approval.js +4 -26
  7. package/dist/boot/budget-tracing.js +2 -3
  8. package/dist/boot/config-center.d.ts +2 -2
  9. package/dist/boot/config-center.js +5 -7
  10. package/dist/boot/parked-revive-gate.d.ts +1 -7
  11. package/dist/boot/parked-revive-gate.js +2 -12
  12. package/dist/boot/resolve-spec.js +18 -16
  13. package/dist/boot/runner-deps.d.ts +5 -3
  14. package/dist/boot/runner-deps.js +6 -4
  15. package/dist/boot/stage-07-capability-layer.js +2 -2
  16. package/dist/brain.d.ts +17 -8
  17. package/dist/brain.js +10 -7
  18. package/dist/capabilities/hands-lane.d.ts +25 -0
  19. package/dist/capabilities/hands-lane.js +1 -1
  20. package/dist/capabilities/mcp-probe.d.ts +2 -2
  21. package/dist/config-catalog.js +3 -2
  22. package/dist/config-center/apply-effective.d.ts +6 -16
  23. package/dist/config-center/apply-effective.js +0 -6
  24. package/dist/config-center/facade.d.ts +1 -1
  25. package/dist/config-center/facade.js +1 -1
  26. package/dist/config-center/restart-signal.d.ts +1 -1
  27. package/dist/config-types.d.ts +12 -16
  28. package/dist/config.d.ts +32 -29
  29. package/dist/config.js +20 -27
  30. package/dist/deployment-governance.d.ts +13 -19
  31. package/dist/deployment-governance.js +7 -14
  32. package/dist/governance-overrides.d.ts +20 -0
  33. package/dist/governance-overrides.js +8 -0
  34. package/dist/host-decision.d.ts +83 -25
  35. package/dist/host-decision.js +31 -1
  36. package/dist/http/admission.js +13 -7
  37. package/dist/http/approval-sweeps.js +16 -13
  38. package/dist/http/checkpoint-ctx.d.ts +83 -24
  39. package/dist/http/checkpoint-ctx.js +68 -8
  40. package/dist/http/mcp-dial-deadline.d.ts +35 -0
  41. package/dist/http/mcp-dial-deadline.js +21 -0
  42. package/dist/http/memory-compliance-failure-reply.d.ts +11 -5
  43. package/dist/http/memory-compliance-failure-reply.js +17 -8
  44. package/dist/http/memory-store-refusal-reply.d.ts +14 -11
  45. package/dist/http/memory-store-refusal-reply.js +16 -5
  46. package/dist/http/resume-legs.js +18 -18
  47. package/dist/http/retired-skill-manifest.d.ts +1 -1
  48. package/dist/http/route-ctx.d.ts +3 -3
  49. package/dist/http/routes/approvals-assistant.js +21 -24
  50. package/dist/http/routes/capabilities.js +5 -3
  51. package/dist/http/routes/mcp-reconnect.d.ts +17 -0
  52. package/dist/http/routes/mcp-reconnect.js +6 -1
  53. package/dist/http/routes/memory-bundle.js +1 -1
  54. package/dist/http/routes/memory-compliance.js +1 -1
  55. package/dist/http/routes/memory-origin.js +1 -1
  56. package/dist/http/routes/runs.js +53 -33
  57. package/dist/http/routes/sessions.d.ts +1 -1
  58. package/dist/http/routes/sessions.js +3 -6
  59. package/dist/http/routes/workflows.js +2 -0
  60. package/dist/http/wire-types.d.ts +6 -1
  61. package/dist/memory-store-refusal.d.ts +42 -17
  62. package/dist/memory-store-refusal.js +25 -6
  63. package/dist/model-select.d.ts +16 -5
  64. package/dist/model-select.js +7 -3
  65. package/dist/model-unreachable-notice.d.ts +1 -1
  66. package/dist/namespaced-rule-name.d.ts +45 -39
  67. package/dist/namespaced-rule-name.js +36 -22
  68. package/dist/observability/corrupt-read-seat.d.ts +4 -0
  69. package/dist/observability/corrupt-read-seat.js +1 -0
  70. package/dist/observability/fail-open.d.ts +15 -3
  71. package/dist/observability/fail-open.js +15 -3
  72. package/dist/observability/permission-rule-events.d.ts +3 -0
  73. package/dist/observability/permission-rule-events.js +4 -0
  74. package/dist/observability/run-terminal.js +21 -1
  75. package/dist/operator-ask.d.ts +4 -1
  76. package/dist/orchestration/workflow-agent-session-index.d.ts +82 -59
  77. package/dist/orchestration/workflow-agent-session-index.js +80 -14
  78. package/dist/orchestration/workflow-completion-inbox.d.ts +5 -0
  79. package/dist/orchestration/workflow-completion-inbox.js +39 -17
  80. package/dist/orchestration/workflow-notify-journal.d.ts +1 -1
  81. package/dist/orchestration/workflow-notify-journal.js +7 -2
  82. package/dist/outbound-dispatcher.d.ts +47 -20
  83. package/dist/outbound-dispatcher.js +198 -23
  84. package/dist/parked-decide.d.ts +43 -48
  85. package/dist/parked-decide.js +54 -16
  86. package/dist/permission-rule-list.d.ts +20 -31
  87. package/dist/permission-rule-vocab.d.ts +4 -3
  88. package/dist/permission-rule-vocab.js +1 -1
  89. package/dist/plugins/approval-ask-store-sql.d.ts +17 -20
  90. package/dist/plugins/approval-ask-store-sql.js +0 -3
  91. package/dist/plugins/one-time-migrations.d.ts +25 -0
  92. package/dist/plugins/one-time-migrations.js +192 -6
  93. package/dist/plugins/pg-pool.js +1 -1
  94. package/dist/plugins/tidb-pool.js +1 -1
  95. package/dist/plugins/web-search.d.ts +20 -1
  96. package/dist/plugins/web-search.js +39 -10
  97. package/dist/plugins/workflow-run-store-sql.d.ts +7 -6
  98. package/dist/plugins/workflow-run-store-sql.js +5 -7
  99. package/dist/request-key-closure.js +1 -1
  100. package/dist/request-value-closure.d.ts +1 -1
  101. package/dist/router/route-orchestration.d.ts +2 -23
  102. package/dist/rule-entry-list.d.ts +88 -0
  103. package/dist/rule-entry-list.js +102 -0
  104. package/dist/rules-consent.js +3 -0
  105. package/dist/run-local.d.ts +4 -2
  106. package/dist/run-local.js +4 -3
  107. package/dist/runs.d.ts +1 -1
  108. package/dist/runs.js +1 -0
  109. package/dist/runtime-governance.d.ts +5 -21
  110. package/dist/runtime-governance.js +4 -8
  111. package/dist/session-policy-wire.d.ts +13 -12
  112. package/dist/session-policy-wire.js +11 -6
  113. package/dist/spec-fields.d.ts +13 -24
  114. package/dist/spec-fields.js +13 -17
  115. package/dist/standing-answer-grant.d.ts +14 -2
  116. package/dist/standing-answer-grant.js +8 -1
  117. package/dist/task-settings.d.ts +18 -18
  118. package/dist/task-settings.js +63 -32
  119. package/dist/tool-approval.d.ts +77 -145
  120. package/dist/tool-approval.js +80 -62
  121. package/dist/trace/core-keyset-guard.d.ts +6 -6
  122. package/dist/trace/engine-notice-wire.d.ts +4 -6
  123. package/dist/trace/engine-notice-wire.js +14 -1
  124. package/dist/trace/self-minted-wire.d.ts +16 -2
  125. package/dist/trace/self-minted-wire.js +7 -0
  126. package/dist/user-input-ledger.d.ts +24 -12
  127. package/dist/user-input-ledger.js +33 -14
  128. package/package.json +2 -2
  129. package/scripts/drain-check-local.mjs +103 -7
  130. package/scripts/drain-check.mysql.sql +59 -8
  131. package/scripts/drain-check.pg.sql +66 -10
@@ -45,21 +45,44 @@
45
45
  * `CONNECT` 交换)都经它的 `connect`,不多不少。{@link ProxyDialPool} 在这里把两类结局收成一只错误 {@link OutboundProxyError}:
46
46
  * · 拨号失败(拒连 / 解析不了 / 超时 / 代理 TLS / 收下就关 …)⇒ 包成 `OutboundProxyError`(`cause` = 原错误);
47
47
  * · `CONNECT` 回非 200 ⇒ 关掉那条 socket(undici 自己也是这么做的),同样包成 `OutboundProxyError`(`cause` 说出状态码)。
48
- * 🔴 **包装层不铸 `code`,这是承重的两件事**:① undici 客户端在连接阶段把 `UND_ERR_SOCKET` 当可恢复错误重连(`client.js` 的 `onError`),
48
+ * 🔴 **包装层的 `code` 只有一种来源:代理答了拒**(CONNECT 回非 200 / 绝对 URL 形回 407,唯一铸点 {@link proxyRefusal})⇒ 包装节点带引擎包根的
49
+ * `PROXY_TUNNEL_REFUSED` 与数字 `statusCode`(代理自己说的状态),节点之下只有本文件自己写的无码句子;**拨号失败不铸码**(下面那枚 errno / undici 码当家)。承重的两件事:
50
+ * ① undici 客户端在连接阶段只把 `UND_ERR_INFO` / `UND_ERR_SOCKET` 当可恢复错误重连(7.30.0 `client.js` `onError` 逐字判这两枚码),
49
51
  * 而 `CONNECT` 恰在连接阶段 —— 代理收下 TCP 就关(Linux 上是 FIN ⇒ `UND_ERR_SOCKET`)时,裸 `ProxyAgent` / Node 24 `NODE_USE_ENV_PROXY`
50
- * 都会重连死循环(修前实测 10 s 四五万条连接、请求挂到调用方超时、abort 之后仍在涨);无 code 的错误让排队的请求当场失败。
51
- * ② core 的连接失败分类(`classifyConnectFailure`)只看链上的 `code`,无 code 的一节对它透明 —— 重试 / 切备 / 断路器的判据不变。
52
+ * 都会重连死循环(修前实测 10 s 四五万条连接、请求挂到调用方超时、abort 之后仍在涨);包装节点的码不是那两枚(无码,或 `PROXY_TUNNEL_REFUSED`)⇒ 排队的请求当场失败。
53
+ * ② core 的连接失败判官(`readConnectFailure` / `classifyConnectFailure`)只看图上的 `code`:无码的包装节点对它透明(拨号失败的判据不变);带 `PROXY_TUNNEL_REFUSED`
54
+ * 的节点是确定的答案(短阶梯,终局句按 `statusCode` 选)—— 前提是节点之下没有别的带码节点(一枚外来码否决全图),所以拒绝的 `cause` 只能是本文件的无码句子,
55
+ * 不能盖在 undici 自己的拒(`UND_ERR_ABORTED`)之上。
52
56
  * 修前读数(装了 dispatcher、未加归因;引擎终局句 = core `describeNetworkError` 原样):代理拒连 `[network] fetch failed (connect ECONNREFUSED
53
57
  * 127.0.0.1:1)`;代理主机解析不了 `… (getaddrinfo ENOTFOUND proxy.s776.invalid)`;CONNECT 回 403 `… (Request was cancelled. ← UND_ERR_ABORTED:
54
58
  * Proxy response (403) !== 200 when HTTP Tunneling)` —— 都不说「经代理」。修后:`… (via outbound proxy (from HTTPS_PROXY) ←
55
- * connect ECONNREFUSED 127.0.0.1:1)`(message 不带代理地址 —— 主会话 F3 裁定,见 {@link OutboundProxyError})。core 的句子一字不改(它逐层展开 cause 链,包装层就是链上多一节)。隧道建好之后的失败(目标的 TLS、
59
+ * ECONNREFUSED)`(整条链不带代理地址与凭据:包装句只说「经代理 + 哪只变量」,`cause` 链上只有码 —— 见 {@link projectHopFailure}、{@link OutboundProxyError})。core 的句子一字不改(它逐层展开 cause 链,
60
+ * 包装层就是链上多一节、`cause` 那一节换成同形副本)。隧道建好之后的失败(目标的 TLS、
56
61
  * 目标的响应)不经这个接缝,不冠代理 —— 那是目标的事。
57
62
  *
63
+ * ── http 目标:绝对 URL 形,不建隧道(S-804;test 黑盒验收 A1)──────────────────────────────────────────────────
64
+ * 病:undici `ProxyAgent` 的 `proxyTunnel` **代码缺省是 true**(7.30.0 `lib/dispatcher/proxy-agent.js` 构造器 `const { proxyTunnel = true } = opts`;它的文档写缺省 false,
65
+ * 两处相反,以代码为准)⇒ 修前连 **http 网关**也被对代理发 `CONNECT host:80`;只放行 `:443` CONNECT 的代理(squid 缺省)拒掉它,run 约 175 s 后失败。
66
+ * 现在构造 `EnvHttpProxyAgent` 时显式 `proxyTunnel:false`:**http 目标 ⇒ 绝对 URL 形请求**(`GET http://host/path`,undici 的 `Http1ProxyWrapper`),**https 目标 ⇒ 照旧 CONNECT**。
67
+ * 范围是 undici 划的:目标 `http:` **且**代理 URL 也是 `http:`;`https://` 代理 + http 目标 undici 不走绝对 URL 形,仍 CONNECT。
68
+ * 三件事照旧成立(逐项读实现得出):
69
+ * · 代理认证头:`Proxy-Authorization` 由 `ProxyAgent` 构造器从 userinfo 译一次、两条腿共用(隧道腿放进 CONNECT 请求头,绝对 URL 形腿放进每条请求头);
70
+ * · `NO_PROXY` 与回环恒直连:路由在 `EnvHttpProxyAgent` / 本文件外壳里判,先于选哪条腿,不受影响;
71
+ * · 代理**拨号**失败(拒连 / 解析不了 / 超时 / 代理 TLS)⇒ 仍是 {@link OutboundProxyError}(`factory` 接缝,见 {@link createProxyAgentFactory}),
72
+ * 建连失败的观察照报 `proxyInUse`;`targetHost` 读对目标(绝对 URL 形请求的 `origin` 是代理、目标在 `path`,见 {@link targetHostOf})。
73
+ * 🔴 **与 CONNECT 腿不同、读者要知道的三件**(绝对 URL 形里代理的回话是普通 HTTP 响应,不是 CONNECT 的回包):
74
+ * ① 代理的 **407** ⇒ {@link OutboundProxyError},与 CONNECT 腿的 407 **同类、同一只铸点**({@link proxyRefusal})。407 按协议只可能来自代理;undici 的 `Http1ProxyWrapper`
75
+ * 自己把它转成 `InvalidArgumentError`(只留文案、没有状态码位,按结构认不出),所以改在代理客户端那一层({@link ProxyHopClient})**按响应状态码**先认、先于 undici 的转换,
76
+ * 不看任何文案;
77
+ * ② 代理的 **403** 等其余拒绝状态是**普通响应**原样交给调用方 —— 在 HTTP 层与网关自己回的 403 **原理上分不开**(无通用判别式),调用方读到的是「网关回了 403」(接受并披露);
78
+ * ③ 请求发出之后的失败(代理中途断流 / 收下连接就关)与目标自己的失败同形、不冠代理(与隧道建好之后的失败同理)。
79
+ * 保密面:http 目标的请求行与头(含发给网关的凭据头)以明文过代理,代理看得到完整 URL;要对代理保密就用 https 网关(隧道里是密文)。
80
+ *
58
81
  * ── 安全轴 ───────────────────────────────────────────────────────────────────────────────────────
59
82
  * 代理 URL 的 userinfo 是凭据(undici 把它译成 `Proxy-Authorization: Basic …`)。它只进 dispatcher,不进任何观测面:姿态
60
83
  * ({@link buildOutboundProxyPosture})、拒启句与归因句同一把尺 {@link displayProxyUrl}(形干净只出 `scheme//host[:port]`、过全仓那一只
61
84
  * display-safe 铸点;形不干净整只占位);姿态只上启动日志一行
62
- * `outbound_proxy`,不进 `/health`、不进公告、不进租户 wire(代理那一跳失败的终局句只说「经代理 + 哪只变量」,F3 裁定)。代理变量不可用(undici 构造即拒:无 scheme、`ftp:`、带路径 / 查询)⇒
85
+ * `outbound_proxy`,不进 `/health`、不进公告、不进租户 wire(代理那一跳失败的终局句只说「经代理 + 哪只变量 + 码」,整句无代理地址与凭据 —— {@link projectHopFailure};原句只进运维日志 `outbound_connect_failed.proxyHop`)。代理变量不可用(undici 构造即拒:无 scheme、`ftp:`、带路径 / 查询)⇒
63
86
  * **拒启**(与 Node 24 `NODE_USE_ENV_PROXY=1` 遇坏值的行为同向)—— 静默直连 = 运维以为出网走了代理而实际没有,那是出网策略面的
64
87
  * fail-open;拒因只回显 display-safe 值。
65
88
  *
@@ -68,8 +91,8 @@
68
91
  * 观察点 = undici 的**公开诊断通道**(`undici:request:create` / `:headers` / `:error`,进程级按名订阅,npm undici 与 Node 内建那一份
69
92
  * 同名同发):不包 handler、不动 dispatch 的热路径。{@link installOutboundDispatcher} 装一次:
70
93
  * ① 每条请求的建连期失败(码在闭集 {@link OUTBOUND_CONNECT_FAILURE_CODES} 里、且这条请求还没收到过响应头)⇒ 一条结构化 warn
71
- * `outbound_connect_failed { targetHost, code, proxyInUse, proxyReadAtMs, sinceBootMs, hint }`(`proxyInUse` = 这一跳走的代理**变量名**,
72
- * 不打值;直连 ⇒ `null`)—— 全进程的出网都算(模型、中心、工具 …),不只模型;
94
+ * `outbound_connect_failed { targetHost, code, proxyInUse, proxyHop?, proxyReadAtMs, sinceBootMs, hint }`(`proxyInUse` = 这一跳走的代理**变量名**,
95
+ * 不打值;直连 ⇒ `null`。`proxyHop` = 只在走了代理时在场的原句,含代理 host:port —— 运行时失败里代理地址**唯一**的去处,见 {@link projectHopFailure})—— 全进程的出网都算(模型、中心、工具 …),不只模型;
73
96
  * ② 最近一次建连失败 + 生效代理的**名字**与读取时刻 ⇒ `GET /v1/diagnostics/wiring` 的 `outbound` 段({@link readOutboundWiring});
74
97
  * ③ 请求若创建在一次**模型调用**的作用域里(`model-reach-scope.ts`),结局记到那次调用上 —— 模型调用终态时由
75
98
  * `model-unreachable-notice.ts` 决定要不要给这条腿的用户铸 `server.model_unreachable`(只落日志不算闭环:运营用户看不到日志)。
@@ -77,6 +100,7 @@
77
100
  * 目标请求上,那一条才知道「要去哪」。Bun 的原生 fetch 不经 undici ⇒ 通道不发、三件都没有(如实:与回环直连同一条 Bun 例外)。
78
101
  */
79
102
  import { Dispatcher } from "undici";
103
+ import { PROXY_TUNNEL_REFUSED } from "@sema-agent/core";
80
104
  import type { Logger } from "./observability/logger.js";
81
105
  /** 三只变量的读值(boot 冻结;空串 = 未设)。 */
82
106
  interface OutboundProxyEnv {
@@ -106,18 +130,25 @@ export declare function buildOutboundProxyPosture(env?: NodeJS.ProcessEnv): Outb
106
130
  */
107
131
  export declare function isLoopbackOrigin(origin: string | URL | undefined): boolean;
108
132
  /**
109
- * 代理那一跳失败的**唯一**错误形(`cause` = 原错误;**不铸 `code`**,理由见顶注)。`proxy` = display-safe 代理地址,`from` = 哪只变量给的。
110
- * 🔴 **message 不带地址**(主会话 F3 裁定,09-26):这句经 core 的终局句进租户可见的失败原因;地址是基础设施细节,只点明「经代理 + 哪只变量」,
111
- * 一条规则、不按姿态分叉。地址留在 `proxy` 属性上供进程内日志腿取;单用户运维在启动日志 `outbound_proxy` 行里有同一串可对照。根因那一节是
112
- * undici 自己的错误原文(拒连 / 解析不了两形会点名代理的 host:port,与直连失败点名目标同形),本仓不改写上游错误。
113
- * 字符串化(`String(err)`)带出根因:只打第一层 cause 的日志腿(如 hook 的 `String(cause)`)也读得到 `ECONNREFUSED` 这类根因;
114
- * core 的 `describeNetworkError` 读的是 `.message`,不受影响、不重复。
133
+ * 代理那一跳失败的**唯一**错误形(`cause` = 原错误经 {@link projectHopFailure} 的 wire 副本)。`proxy` = display-safe 代理地址,`from` = 哪只变量给的。
134
+ * `refusedWith` 在场 = 代理**答了**拒(只由 {@link proxyRefusal} 传)⇒ 本节点带 `code: PROXY_TUNNEL_REFUSED` + 数字 `statusCode`;缺席(拨号失败)⇒ 两键都不在(理由见顶注)。
135
+ * 🔴 **这句经 core 的终局句进租户可见的失败原因,所以整条链上没有代理地址、没有凭据**:message 只点明「经代理 + 哪只变量」,`cause` 链上只有码与本文件自己写的句子。
136
+ * 一条规则、不按姿态分叉;运维要地址 ⇒ 运维日志 `outbound_connect_failed` 的 `proxyHop`(原句)与启动日志 `outbound_proxy` 行(display-safe 代理值)。
137
+ * `proxy` 是**不可枚举**的自有属性(只能按名读;`JSON.stringify` / `util.inspect` 这类枚举自有属性的口读不到),原句只经 {@link OutboundProxyError.hopDetail} 取。
138
+ * 字符串化(`String(err)`)带出根因:只打第一层 cause 的日志腿(如 hook 的 `String(cause)`)也读得到 `ECONNREFUSED` 这类码;core 的 `describeNetworkError` 读的是 `.message`,不受影响、不重复。
115
139
  */
116
140
  export declare class OutboundProxyError extends Error {
117
- readonly proxy: string;
141
+ #private;
118
142
  readonly from: string;
119
143
  readonly name = "OutboundProxyError";
120
- constructor(proxy: string, from: string, cause: unknown);
144
+ readonly proxy: string;
145
+ /** 代理答了拒 ⇒ 引擎的确定性连接码;拨号失败 ⇒ 不在场(`declare`:不铸 `undefined` 自有键,`"code" in` 与引擎的读法都读成缺席)。 */
146
+ readonly code?: typeof PROXY_TUNNEL_REFUSED;
147
+ /** 代理自己说的 HTTP 状态(与 `code` 同生同灭)。 */
148
+ readonly statusCode?: number;
149
+ constructor(proxy: string, from: string, cause: unknown, refusedWith?: number);
150
+ /** 代理那一跳的原句(含代理 host:port;凭据位已过脱敏器)。**只给运维日志**:不进 message、不进 `cause`、不进任何自有属性。 */
151
+ get hopDetail(): string;
121
152
  toString(): string;
122
153
  }
123
154
  /** 代理变量不可用的拒启码(拼法随 `config.*` 家族,出现在拒句方括号里)。 */
@@ -152,11 +183,7 @@ export interface OutboundConnectFailure {
152
183
  readonly code: OutboundConnectFailureCode;
153
184
  readonly proxyInUse: string | null;
154
185
  }
155
- /**
156
- * 一只请求错误是不是**建连期失败**:在错误图里找第一枚闭集码;路上遇到 {@link OutboundProxyError} ⇒ 这一跳走了代理,
157
- * 记下它的变量名。找不到闭集码 ⇒ `undefined`(不是建连失败:TLS、中途断、取消、超时、代理 CONNECT 非 200 …)。
158
- * 纯函数、不抛(只读 `code` / `cause` / `errors` 三个自有数据位)。
159
- */
186
+ /** 一只请求错误的建连期失败码与它走的代理变量名(`undefined` = 不是建连失败)。原句不在这里 —— 它只给运维日志。 */
160
187
  export declare function classifyOutboundConnectFailure(error: unknown): {
161
188
  readonly code: OutboundConnectFailureCode;
162
189
  readonly proxyInUse: string | null;
@@ -1,7 +1,8 @@
1
- import { Agent, Dispatcher, EnvHttpProxyAgent, getGlobalDispatcher, Pool, setGlobalDispatcher } from "undici";
1
+ import { Agent, Client, Dispatcher, EnvHttpProxyAgent, getGlobalDispatcher, Pool, setGlobalDispatcher } from "undici";
2
+ import { PROXY_TUNNEL_REFUSED } from "@sema-agent/core";
2
3
  import { isIP } from "node:net";
3
4
  import diagnosticsChannel from "node:diagnostics_channel";
4
- import { buildDisplaySafeUrl, DISPLAY_SAFE_URL_TOKEN } from "./trace/redact.js";
5
+ import { buildDisplaySafeUrl, DISPLAY_SAFE_URL_TOKEN, redactSecrets } from "./trace/redact.js";
5
6
  import { recordFailOpen } from "./observability/fail-open.js";
6
7
  import { currentModelCallReach } from "./model-reach-scope.js";
7
8
  function readOutboundProxyEnv(env = process.env) {
@@ -80,14 +81,95 @@ class OutboundDispatcher extends Dispatcher {
80
81
  void done.then(cb, cb);
81
82
  }
82
83
  }
84
+ class ProxyHopNote extends Error {
85
+ }
86
+ const UNCLASSIFIED_HOP_FAILURE = "proxy hop failed (no error code)";
87
+ const CONNECT_ERROR_GRAPH_BUDGET = 16;
88
+ const HOP_CODE_SHAPE = /^[A-Za-z][A-Za-z0-9_]{0,63}$/;
89
+ const UNREADABLE_HOP_CODE = "HOP_CODE_UNREADABLE";
90
+ const HOP_DETAIL_CAP = 500;
91
+ const readMember = (value, key) => {
92
+ try {
93
+ return value === null || value === undefined ? undefined : value[key];
94
+ }
95
+ catch {
96
+ recordFailOpen("server.outbound.hop-member-unreadable");
97
+ return undefined;
98
+ }
99
+ };
100
+ const isErrorLike = (value) => {
101
+ try {
102
+ return value instanceof Error || Object.prototype.toString.call(value) === "[object Error]";
103
+ }
104
+ catch {
105
+ recordFailOpen("server.outbound.hop-member-unreadable");
106
+ return false;
107
+ }
108
+ };
109
+ function projectHopFailure(raw) {
110
+ const copies = new Map();
111
+ const frontier = [];
112
+ const messages = [];
113
+ let created = 0;
114
+ const make = (source) => {
115
+ created++;
116
+ const text = typeof source === "string" ? source : readMember(source, "message");
117
+ if (typeof text === "string" && text !== "" && !messages.includes(text))
118
+ messages.push(text);
119
+ const rawCode = readMember(source, "code");
120
+ const code = typeof rawCode !== "string" ? undefined : HOP_CODE_SHAPE.test(rawCode) ? rawCode : UNREADABLE_HOP_CODE;
121
+ const message = code ?? (source instanceof ProxyHopNote ? source.message : UNCLASSIFIED_HOP_FAILURE);
122
+ const wire = source instanceof AggregateError ? new AggregateError([], message) : new Error(message);
123
+ if (code !== undefined)
124
+ Object.assign(wire, { code });
125
+ if (typeof source === "object" && source !== null) {
126
+ copies.set(source, wire);
127
+ frontier.push([source, wire]);
128
+ }
129
+ return wire;
130
+ };
131
+ const copy = (source) => {
132
+ const known = typeof source === "object" && source !== null ? copies.get(source) : undefined;
133
+ return known ?? (created < CONNECT_ERROR_GRAPH_BUDGET ? make(source) : undefined);
134
+ };
135
+ const place = (target, key, value) => void Object.defineProperty(target, key, { value, writable: true, configurable: true, enumerable: false });
136
+ const wire = make(raw);
137
+ for (let next = frontier.shift(); next !== undefined; next = frontier.shift()) {
138
+ const [source, target] = next;
139
+ if (!isErrorLike(source))
140
+ continue;
141
+ const cause = readMember(source, "cause");
142
+ const below = cause === undefined || cause === null ? undefined : copy(cause);
143
+ if (below !== undefined)
144
+ place(target, "cause", below);
145
+ const members = readMember(source, "errors");
146
+ if (members === undefined)
147
+ continue;
148
+ const length = readMember(members, "length");
149
+ if (typeof length !== "number" || !Number.isInteger(length) || length <= 0) {
150
+ place(target, "errors", null);
151
+ continue;
152
+ }
153
+ const legs = Array.from({ length: Math.min(length, CONNECT_ERROR_GRAPH_BUDGET) }, (_, i) => copy(readMember(members, String(i)))).filter((leg) => leg !== undefined);
154
+ place(target, "errors", Array.isArray(members) ? legs : Object.assign({ length: legs.length }, Object.fromEntries(legs.map((leg, i) => [String(i), leg]))));
155
+ }
156
+ return { wire, detail: redactSecrets(messages.join(" ← ")).slice(0, HOP_DETAIL_CAP) };
157
+ }
83
158
  export class OutboundProxyError extends Error {
84
- proxy;
85
159
  from;
86
160
  name = "OutboundProxyError";
87
- constructor(proxy, from, cause) {
88
- super(`via outbound proxy (from ${from})`, { cause });
89
- this.proxy = proxy;
161
+ #hopDetail;
162
+ constructor(proxy, from, cause, refusedWith) {
163
+ const { wire, detail } = projectHopFailure(cause);
164
+ super(`via outbound proxy (from ${from})`, { cause: wire });
90
165
  this.from = from;
166
+ Object.defineProperty(this, "proxy", { value: proxy, enumerable: false });
167
+ if (refusedWith !== undefined)
168
+ Object.assign(this, { code: PROXY_TUNNEL_REFUSED, statusCode: refusedWith });
169
+ this.#hopDetail = detail;
170
+ }
171
+ get hopDetail() {
172
+ return this.#hopDetail;
91
173
  }
92
174
  toString() {
93
175
  let root = this.cause;
@@ -128,9 +210,12 @@ class ProxyDialPool extends Pool {
128
210
  if (data.statusCode === 200)
129
211
  return undefined;
130
212
  data.socket.on("error", () => undefined).destroy();
131
- return new OutboundProxyError(this.shown, this.from, new Error(`proxy answered CONNECT with HTTP ${String(data.statusCode)}`));
213
+ return proxyRefusal(this.shown, this.from, data.statusCode, "CONNECT");
132
214
  }
133
215
  }
216
+ function proxyRefusal(shown, from, statusCode, leg) {
217
+ return new OutboundProxyError(shown, from, new ProxyHopNote(`proxy answered ${leg} with HTTP ${String(statusCode)}`), statusCode);
218
+ }
134
219
  function withDialDeadline(options) {
135
220
  const inner = Reflect.get(options, "connect");
136
221
  if (typeof inner !== "function")
@@ -141,24 +226,105 @@ function withDialDeadline(options) {
141
226
  opts,
142
227
  (err, socket) => {
143
228
  if (err === null && socket !== null) {
144
- const expire = () => void socket.destroy(new Error(`proxy did not answer CONNECT within ${String(PROXY_CONNECT_DEADLINE_MS)} ms`));
229
+ const expire = () => void socket.destroy(new ProxyHopNote(`proxy did not answer CONNECT within ${String(PROXY_CONNECT_DEADLINE_MS)} ms`));
145
230
  dialDeadlines.set(socket, setTimeout(expire, Math.max(0, expiresAt - performance.now())));
146
231
  socket.once("close", () => clearDialDeadline(socket));
147
232
  callback(null, socket);
148
233
  }
149
234
  else
150
- callback(err ?? new Error("proxy connector returned neither a socket nor an error"), null);
235
+ callback(err ?? new ProxyHopNote("proxy connector returned neither a socket nor an error"), null);
151
236
  },
152
237
  ]);
153
238
  };
154
239
  return { ...options, connect };
155
240
  }
156
- function createProxyClientFactory(v) {
241
+ function createProxyIdentity(v) {
157
242
  const vars = [["HTTP_PROXY", v.httpProxy], ["HTTPS_PROXY", v.httpsProxy]].filter(([, raw]) => raw !== "" && URL.canParse(raw));
243
+ return (url) => {
244
+ const same = vars.filter(([, raw]) => new URL(raw).href === url.href);
245
+ const first = same[0];
246
+ return first === undefined ? undefined : { shown: displayProxyUrl(first[1]), from: same.map(([name]) => name).join("/") };
247
+ };
248
+ }
249
+ function createProxyClientFactory(v) {
250
+ const identify = createProxyIdentity(v);
251
+ return (origin, options) => {
252
+ const { shown, from } = identify(origin) ?? { shown: displayProxyUrl(origin.href), from: "proxy variable" };
253
+ return new ProxyDialPool(origin, withDialDeadline(options), shown, from);
254
+ };
255
+ }
256
+ const PROXY_AUTH_REQUIRED = 407;
257
+ function interceptProxyAuthRequired(handler, refuse) {
258
+ let abort;
259
+ return new Proxy(handler, {
260
+ get(target, prop) {
261
+ const member = Reflect.get(target, prop, target);
262
+ if (typeof member !== "function")
263
+ return member;
264
+ if (prop === "onConnect") {
265
+ return (...args) => {
266
+ const candidate = args[0];
267
+ if (typeof candidate === "function")
268
+ abort = (err) => void Reflect.apply(candidate, undefined, [err]);
269
+ return Reflect.apply(member, target, args);
270
+ };
271
+ }
272
+ if (prop === "onHeaders") {
273
+ return (...args) => {
274
+ if (args[0] !== PROXY_AUTH_REQUIRED)
275
+ return Reflect.apply(member, target, args);
276
+ const refusal = refuse();
277
+ if (abort !== undefined)
278
+ abort(refusal);
279
+ else {
280
+ const deliver = Reflect.get(target, "onError", target);
281
+ if (typeof deliver === "function")
282
+ Reflect.apply(deliver, target, [refusal]);
283
+ }
284
+ return undefined;
285
+ };
286
+ }
287
+ return member.bind(target);
288
+ },
289
+ });
290
+ }
291
+ class ProxyHopClient extends Client {
292
+ shown;
293
+ from;
294
+ constructor(origin, options, shown, from) {
295
+ super(origin, options);
296
+ this.shown = shown;
297
+ this.from = from;
298
+ }
299
+ dispatch(options, handler) {
300
+ return super.dispatch(options, interceptProxyAuthRequired(handler, () => proxyRefusal(this.shown, this.from, PROXY_AUTH_REQUIRED, "the request")));
301
+ }
302
+ }
303
+ function withProxyAttribution(options, shown, from) {
304
+ const inner = Reflect.get(options, "connect");
305
+ const connect = typeof inner !== "function"
306
+ ? undefined
307
+ : (opts, callback) => {
308
+ Reflect.apply(inner, undefined, [
309
+ opts,
310
+ (err, socket) => {
311
+ if (err !== null)
312
+ callback(new OutboundProxyError(shown, from, err), null);
313
+ else if (socket !== null)
314
+ callback(null, socket);
315
+ else
316
+ callback(new OutboundProxyError(shown, from, new ProxyHopNote("proxy connector returned neither a socket nor an error")), null);
317
+ },
318
+ ]);
319
+ };
320
+ return { ...options, ...(connect !== undefined ? { connect } : {}), factory: (origin, clientOptions) => new ProxyHopClient(origin, { ...clientOptions }, shown, from) };
321
+ }
322
+ function createProxyAgentFactory(v) {
323
+ const identify = createProxyIdentity(v);
158
324
  return (origin, options) => {
159
- const same = vars.filter(([, raw]) => new URL(raw).href === origin.href);
160
- const raw = same[0]?.[1] ?? origin.href;
161
- return new ProxyDialPool(origin, withDialDeadline(options), displayProxyUrl(raw), same.length > 0 ? same.map(([name]) => name).join("/") : "proxy variable");
325
+ const text = String(origin);
326
+ const proxy = URL.canParse(text) ? identify(new URL(text)) : undefined;
327
+ return new Pool(origin, proxy === undefined ? { ...options } : withProxyAttribution(options, proxy.shown, proxy.from));
162
328
  };
163
329
  }
164
330
  export const OUTBOUND_PROXY_INVALID_CODE = "config.outbound_proxy_invalid";
@@ -171,7 +337,7 @@ export function createOutboundDispatcher(env = process.env) {
171
337
  throw refuse("ambiguous-url");
172
338
  let proxied;
173
339
  try {
174
- proxied = new EnvHttpProxyAgent({ httpProxy: v.httpProxy, httpsProxy: v.httpsProxy, noProxy: v.noProxy, clientFactory: createProxyClientFactory(v) });
340
+ proxied = new EnvHttpProxyAgent({ httpProxy: v.httpProxy, httpsProxy: v.httpsProxy, noProxy: v.noProxy, proxyTunnel: false, clientFactory: createProxyClientFactory(v), factory: createProxyAgentFactory(v) });
175
341
  }
176
342
  catch (err) {
177
343
  throw refuse(err instanceof Error && "code" in err && typeof err.code === "string" ? err.code : "invalid");
@@ -187,19 +353,21 @@ export const OUTBOUND_CONNECT_FAILURE_CODES = {
187
353
  ENETUNREACH: "the network is unreachable",
188
354
  };
189
355
  const isConnectFailureCode = (c) => typeof c === "string" && Object.hasOwn(OUTBOUND_CONNECT_FAILURE_CODES, c);
190
- const CONNECT_ERROR_GRAPH_BUDGET = 16;
191
- export function classifyOutboundConnectFailure(error) {
356
+ function scanConnectFailure(error) {
192
357
  const queue = [error];
193
358
  let proxyInUse = null;
359
+ let proxyHop = null;
194
360
  for (let seen = 0; queue.length > 0 && seen < CONNECT_ERROR_GRAPH_BUDGET; seen++) {
195
361
  const node = queue.shift();
196
362
  if (typeof node !== "object" || node === null)
197
363
  continue;
198
- if (node instanceof OutboundProxyError)
364
+ if (node instanceof OutboundProxyError) {
199
365
  proxyInUse = node.from;
366
+ proxyHop = node.hopDetail;
367
+ }
200
368
  const code = Object.getOwnPropertyDescriptor(node, "code")?.value;
201
369
  if (isConnectFailureCode(code))
202
- return { code, proxyInUse };
370
+ return { code, proxyInUse, proxyHop };
203
371
  const cause = Object.getOwnPropertyDescriptor(node, "cause")?.value;
204
372
  if (cause !== undefined)
205
373
  queue.push(cause);
@@ -209,8 +377,14 @@ export function classifyOutboundConnectFailure(error) {
209
377
  }
210
378
  return undefined;
211
379
  }
212
- function targetHostOf(origin) {
213
- const text = typeof origin === "string" ? origin : origin instanceof URL ? origin.href : "";
380
+ export function classifyOutboundConnectFailure(error) {
381
+ const found = scanConnectFailure(error);
382
+ return found === undefined ? undefined : { code: found.code, proxyInUse: found.proxyInUse };
383
+ }
384
+ function targetHostOf(request) {
385
+ const path = Reflect.get(request, "path");
386
+ const origin = Reflect.get(request, "origin");
387
+ const text = typeof path === "string" && /^https?:\/\//i.test(path) ? path : typeof origin === "string" ? origin : origin instanceof URL ? origin.href : "";
214
388
  return URL.canParse(text) ? new URL(text).host : "(unreadable origin)";
215
389
  }
216
390
  let observer;
@@ -257,16 +431,17 @@ function startConnectObserver(logger, nowMs) {
257
431
  if (request === undefined || Reflect.get(request, "method") === "CONNECT")
258
432
  return;
259
433
  const call = requestCalls.get(request);
260
- const verdict = answeredRequests.has(request) ? undefined : classifyOutboundConnectFailure(Reflect.get(message, "error"));
261
- if (verdict === undefined) {
434
+ const found = answeredRequests.has(request) ? undefined : scanConnectFailure(Reflect.get(message, "error"));
435
+ if (found === undefined) {
262
436
  if (call !== undefined)
263
437
  call.last = { kind: "other" };
264
438
  return;
265
439
  }
266
- const failure = { targetHost: targetHostOf(Reflect.get(request, "origin")), ...verdict };
440
+ const failure = { targetHost: targetHostOf(request), code: found.code, proxyInUse: found.proxyInUse };
267
441
  current.last = { ...failure, atMs: Date.now() };
268
442
  logger.warn("outbound_connect_failed", {
269
443
  ...failure,
444
+ ...(found.proxyHop !== null ? { proxyHop: found.proxyHop } : {}),
270
445
  proxyReadAtMs: current.proxyReadAtMs,
271
446
  sinceBootMs: Math.round(process.uptime() * 1000),
272
447
  hint: `${OUTBOUND_CONNECT_FAILURE_CODES[failure.code]} — ${failure.proxyInUse !== null ? `this hop went through the proxy from ${failure.proxyInUse} (read once at boot)` : "this hop was a direct connection"}; if the VPN / proxy changed since boot, restart the server so it re-reads HTTP(S)_PROXY / NO_PROXY`,
@@ -43,8 +43,8 @@ export type ParkOrigin = {
43
43
  handle: string;
44
44
  row: BackgroundAgentRecord;
45
45
  }
46
- /** `park` 是**权威条目**(从 run 记录的真源 `parks[]` 里按赎回键复核出来的那一条);身份位一律从它读
47
- * —— 索引行是可被改盘的派生投影,不做第二个身份写者(7.69.0 合并重扫 c1 同族收口)。 */
46
+ /** `park` 是**权威条目**:被路由的那条记录(持有者 = `parkedBy === record.id`、最高代次恰一条)持有的条目,(子会话, token) 逐字相等;
47
+ * 身份位一律从它读 —— 索引行是可被改盘的派生投影,不做第二个身份写者(7.69.0 合并重扫 c1 同族收口)。 */
48
48
  | {
49
49
  kind: "workflow";
50
50
  runId: string;
@@ -52,31 +52,29 @@ export type ParkOrigin = {
52
52
  park: WorkflowRunParkKey;
53
53
  }
54
54
  /**
55
- * S-252 —— **这条记录的 park 真源在本车道读不出来**(第四种出身,不是第四条车道)。
56
- *
57
- * 索引行还在、run 行也还在,但记录上没有一份读得出来的 `parks[]`:旧引擎(core < 7.17.0)写下的存量行
58
- * 根本没有这一位,或者某一条条目的形坏了。而这一位是**引擎自己**的 park 责任表,也是本车道把调用方的
59
- * checkpoint join 到某一条 park 的**单一真源** ⇒ 这条决定**结构上**送不出去:答 200「已交给宿主的
60
- * 下一跑」就是一张兑付不了的回执。
61
- * 此前这一格与「索引落后一拍 / 这条 run 已不再负责它」两种**正常** miss 完全同形,于是落 legacy 腿拿
62
- * 一个错因 409。判别式据真源的**可读性**把这一格分出来,三个消费点各自响亮处置(decide 拒 + 不驱动
63
- * 宿主续跑;SLA sweep 照旧静默跳过——它同样复核不了)。
64
- *
65
- * 🔴 **7.78.0 / core 7.18.0([ref]):本臂的「为什么拒」没变,「拒了之后干什么」变了。**
66
- * 旧注(与旧拒文)断言 core 的 resume 准入对 `!Array.isArray(prior.parks)` 一律整条拒,并据此告诉运维
67
- * 「这条 run 恢复不了,开一条新的」。core 7.18.0 把那条准入拆成**三臂**([ref]「走 a″」),该断言
68
- * 自此为假:①记录带 `parks` ⇒ 那张表独自作数(不变);②**`parks` 缺席 ⇒ 候选从 journal 的 `parked` 臂
69
- * 与记录自己 `parked` 腿行的 `parkedCheckpointToken` 取并集派生**,非空即准入(每个派生候选仍逐个
70
- * `checkpointStore.get(token)` 验真);③缺席且并集为空 ⇒ 仍拒(`parks_unreadable`,条件收窄到这一格)。
71
- * ⇒ 存量记录**多数可以被 resume 救回来**,而旧拒文让人去开新 run = 把一条救得回来的 run 劝弃。
72
- * 本车道**仍然拒**(core 逐字:那种记录的赎回键是在准入期派生的、**从记录上读不出来**;本腿要么拒、
73
- * 要么就得在服务端重算 core 的三臂 —— 后者正是这次统一掉的双读),但恢复动作改成真话:**去 resume
74
- * 这条 run**。准入一旦走完,core 会把 `parks` 写上记录([ref]),下一次 `/decide` 就走正常的 workflow 臂。
55
+ * **这条决定证不了属于哪一条记录**(core 7.44.0 #1319 的「没有持有者」两臂;判决属主 = 索引模块的 `routeWorkflowPark`)。
56
+ * `cause`:`unreadable` = 某一条点名这个子会话的记录 `parks[]` 读不出(条目形坏 / 这一位在场却不是列表;缺席 = 没有条目,不在此列)—— 它可能正是持有者;
57
+ * `older_engine` = 点名它的条目没有持有者位(写于不带 `parkedBy` / `generation` 的引擎,升级那一刻还停驻着的存量记录)。
58
+ * 两种都**响亮拒**(409 `decide.workflow_parks_unreadable`,码一个、句子按成因两句),一步都不驱动;出路按成因:`older_engine` = 去续跑 `runId` 那条 run
59
+ * (新引擎准入后走到这张停驻会写下自己的持有者行,之后在这里决就正常);`unreadable` = 修好那条记录(它的指针不随任何后继记录消失,续跑解不开)。
60
+ * 不回落读 `agents[]`(那是被退役的第二种读法)。
61
+ * `readableHolderRunId`(只在 `unreadable` 上可能在场):其余可读记录里持有这张卡的最高代次恰一条 —— 读不出的那条引擎往往恰拒续跑,健康的持有者
62
+ * 就在旁边,拒体多给这一条线索(判决不变,仍是整卡拒;不是解除阻断的出路)。
75
63
  */
76
64
  | {
77
65
  kind: "workflow_parks_unreadable";
78
66
  runId: string;
79
67
  run: WorkflowRun;
68
+ cause: "unreadable" | "older_engine";
69
+ readableHolderRunId?: string;
70
+ }
71
+ /**
72
+ * **分叉**:同一条 run 先后被续跑两次,两支都走到了这个子会话、代次相同 —— 没有唯一一条记录可以交付(core 把拒交给消费方)。
73
+ * 响亮拒 409 `decide.workflow_park_forked`,体带并列的 `runIds`;出路 = 选其中一条续跑,它走到这张停驻时代次更高,之后在这里决就正常。
74
+ */
75
+ | {
76
+ kind: "workflow_park_forked";
77
+ runIds: readonly string[];
80
78
  };
81
79
  /** 判别式的两个源。各自可缺席(部署没接就是没接)—— 缺席 ⇒ 那条臂不判,不是失败。 */
82
80
  export interface ParkOriginSources {
@@ -265,38 +263,35 @@ export type WorkflowParkedDecidePlan = {
265
263
  deny: ParkedDecideResult;
266
264
  };
267
265
  /**
268
- * S-252 —— **park 真源读不出来的那条记录的响亮拒**(与上面那族拒臂同一种返回形)。
266
+ * **证不了这条决定属于哪一条记录**的响亮拒(与上面那族拒臂同一种返回形)—— 判决属主 = 索引模块的 `routeWorkflowPark`
267
+ * (「没有持有者」两臂)。码一个(409 `decide.workflow_parks_unreadable`),句子按成因两句,体键 `runId` = 去续跑的那一条;`unreadable` 臂另带顶层
268
+ * `readableHolderRunId`(与 `runId` / `runIds` 同为顶层键)—— **在场才带,缺席整键不在**(没有 / 并列的可读持有者不编一个):读不出的那条引擎往往恰拒续跑,
269
+ * 健康的持有者在旁时指给人看工作可以从哪条记录续上。**只是线索,不是解除阻断的出路**:判决仍是整卡拒(规则 ①),读不出的那条的指针不随任何后继记录消失 ——
270
+ * 续跑可读持有者之后这张卡照旧答本码,直到读不出的那条被修好(拒句如实说了这一点)。
269
271
  *
270
- * 为什么是拒而不是「尽力而为地投递一次」:记录上的 `parks[]` 是本车道把调用方的 checkpoint join 到某一条
271
- * park 的**单一真源**。读不出来 ⇒ 这条决定结构上送不出去,答 200「已交给宿主的下一跑」就是一张兑付不了的
272
- * 回执:卡还 pending、人以为决完了。⇒ 拒,并把真话说出来;卡仍 pending,什么都没有被伪造。
272
+ * 为什么是拒而不是「尽力而为地投递一次」:「这条记录的 run 站在这张停驻上」只有一种读法(`parkedBy === record.id`),读不出来 ⇒
273
+ * 这条决定结构上送不出去,答 200「已交给宿主的下一跑」就是一张兑付不了的回执:卡还 pending、人以为决完了。⇒ 拒,把真话说出来;
274
+ * 卡仍 pending,什么都没有被伪造。不回落读 `agents[]`(那正是被退役的第二种读法)。
275
+ * · `older_engine`:点名这张卡的条目写于不带持有者位的引擎(升级那一刻还停驻着的存量记录)。续跑这条 run ⇒ 新引擎准入、走到这张
276
+ * 停驻时写下自己的持有者行 ⇒ 之后在这里决就正常;
277
+ * · `unreadable`:某一条点名这张卡的记录读不出(条目形坏 / 这一份在场却不是列表;记录上**没有**这一份 = 没有条目,不到这里 —— 规则见
278
+ * `readRunParks`)。续跑**不是**出路:读不出的那条记录的指针不随任何后继记录消失,续跑之后规则 ① 照旧整卡拒(条目形坏的记录引擎本来也拒续跑);
279
+ * 出路 = 运维修好那条记录(`runId`),之后在这里决就正常。
273
280
  *
274
- * 🔴 **7.78.0 / core 7.18.0([ref]):真话变了一句,拒没变。** 亲读安装树 7.18.0 的三臂准入(判别式顶注逐字
275
- * 记着):`parks` 缺席时 core **从 journal 的 `parked` 臂 + 记录自己 `parked` 腿行的 `parkedCheckpointToken`
276
- * 派生候选**并逐个向 checkpoint 店验真,非空即准入 —— 所以「这条 run 恢复不了」自 7.18.0 起对**多数**存量
277
- * 记录是假的,照旧文答就是劝人弃掉一条救得回来的 run。本腿仍拒的理由是另一条(core 逐字):那种记录的赎回键
278
- * 是**在准入期派生的、从记录上读不出来**,服务端要么拒、要么重算 core 的三臂(被这次统一掉的双读)。
279
- * ⇒ 恢复动作 = **去 resume 这条 run**(`Workflow({resumeFromRunId})`):准入走完 core 会把 `parks` 写上
280
- * 记录([ref]),下一次 `/decide` 走正常 workflow 臂。若引擎那一步也拒(journal 与腿行都给不出候选,
281
- * core 的第三臂),那才是真的恢复不了。
282
- *
283
- * 这条臂的受害集 = **存量持久数据**(core < 7.17.0 的引擎写下、升级后还 parked 着的 run 记录)。硬 breaking
284
- * 三句里持久数据那一句要的就是这个形:响亮拒,不做静默双读(读 `agents[]` 补一份身份正是被退役的双算)。
285
- *
286
- * 体上只带 `runId`(那是操作员唯一用得上的坐标;真源都读不出来,任何计数都是编的)。
287
- *
288
- * 🔴 **S-376(词域门,7.80.1):拒文逐字改,status / errorCode / 体键零变。** 旧句把本注里的内部机件整段
289
- * 搬上了 wire(「the parks the run owns」「when it parks」「join your checkpoint to a park」「its own parked
290
- * legs」)—— 那些名字调用方一个都拿不到。新句把承重的四件事一件不少地留着(不投递 / 卡仍 pending 什么都没
291
- * 伪造 / 恢复动作 `Workflow({resumeFromRunId})` / 只有引擎也拒才是真恢复不了)。
292
- * ⚠️ **新句刻意不提 `checkpointToken`**(codex 对抗复审 r1 指出、亲核属实):契约 D-1 逐字写着它是
293
- * server-INTERNAL、不上 `/v1/approvals` 记录、合规 SDK 绝不发送 —— 让调用方去找一个它既没发也拿不到的
294
- * 字段是错指路。新句说的是「无法确认这项待决审批属于这条 run」。判据与冻结钉见
295
- * `test/api-error-text-lexicon.test.ts`。
281
+ * 🔴 **S-376(词域门)**:句子不提 `checkpointToken`(契约 D-1 逐字:server-INTERNAL、合规 SDK 绝不发送)与内部机件名;判据与冻结钉见
282
+ * `test/api-error-text-lexicon.test.ts` / `test/api-error-text-freeze.test.ts`。
296
283
  */
297
284
  export declare function denyWorkflowParksUnreadable(origin: Extract<ParkOrigin, {
298
285
  kind: "workflow_parks_unreadable";
299
286
  }>): ParkedDecideResult;
287
+ /**
288
+ * **分叉**的响亮拒(core 7.44.0 #1319:引擎准入同一条 run 的两次先后续跑,把拒交给消费方)。同一张卡上并列着两条以上同代次的
289
+ * 持有记录 ⇒ 没有唯一一条可以交付;不猜、不按写序取。体带并列的 `runIds`(升序;与同族 `runId` 同为顶层键)。出路 = 续跑其中一条:
290
+ * 它走到这张停驻时代次更高,之后在这里决就正常。卡仍 pending,什么都没投递。
291
+ */
292
+ export declare function denyWorkflowParkForked(origin: Extract<ParkOrigin, {
293
+ kind: "workflow_park_forked";
294
+ }>): ParkedDecideResult;
300
295
  export declare function planWorkflowParkedDecide(origin: Extract<ParkOrigin, {
301
296
  kind: "workflow";
302
297
  }>, req: ParkedDecideRequest): WorkflowParkedDecidePlan;