@sema-agent/server 7.43.0 → 7.44.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 (345) hide show
  1. package/USAGE.md +18 -0
  2. package/dist/adoption/plan.js +31 -192
  3. package/dist/adoption/quiesce.js +3 -86
  4. package/dist/adoption/runner.js +3 -140
  5. package/dist/adoption/sql.js +0 -74
  6. package/dist/adoption/wire.js +0 -80
  7. package/dist/approval-ask-machine.js +0 -75
  8. package/dist/approval-card.js +0 -323
  9. package/dist/approval-deny-reasons.js +0 -42
  10. package/dist/approval-hmac.js +0 -35
  11. package/dist/approval-reconciler.js +8 -166
  12. package/dist/approval.js +4 -67
  13. package/dist/audit.js +1 -44
  14. package/dist/auth-bridge.js +4 -47
  15. package/dist/auth-keys.js +0 -23
  16. package/dist/bake-runner/main.js +4 -65
  17. package/dist/bake-runner/protocol.js +3 -58
  18. package/dist/bake-runner/runner.js +5 -91
  19. package/dist/bench/l8/artifact.js +2 -61
  20. package/dist/bench/l8/escape.js +0 -25
  21. package/dist/bench/l8/index.js +0 -14
  22. package/dist/bench/l8/probes.js +0 -60
  23. package/dist/bench/l8/run-probes.js +2 -45
  24. package/dist/bench/s1/arms.js +14 -189
  25. package/dist/bench/s1/live-deps.js +11 -253
  26. package/dist/bench/s1/oracle.js +1 -36
  27. package/dist/bench/s1/repair-oracle-adapter.js +0 -30
  28. package/dist/bench/s1/reviewer.js +0 -34
  29. package/dist/bench/s1/row.js +0 -62
  30. package/dist/bench/s1/run-firm.js +4 -70
  31. package/dist/bench/s1/runner-ctx.js +0 -40
  32. package/dist/bench/s1/tasks.js +0 -115
  33. package/dist/boot/adoption.js +0 -20
  34. package/dist/boot/budget-tracing.js +3 -55
  35. package/dist/boot/config-center.js +43 -551
  36. package/dist/boot/coordinators.js +1 -69
  37. package/dist/boot/crash-last.js +0 -18
  38. package/dist/boot/deferred-sandbox-path-env.js +3 -119
  39. package/dist/boot/execution-env.js +5 -171
  40. package/dist/boot/governance-seams.js +0 -92
  41. package/dist/boot/leader.js +0 -69
  42. package/dist/boot/limit-sync.js +0 -2
  43. package/dist/boot/memory-boundary.js +3 -91
  44. package/dist/boot/org-memory.js +1 -25
  45. package/dist/boot/parked-revive-gate.js +0 -150
  46. package/dist/boot/permission-rules-audit.js +1 -56
  47. package/dist/boot/reapers.js +15 -273
  48. package/dist/boot/resolve-spec.js +9 -768
  49. package/dist/boot/retention-lane.js +4 -87
  50. package/dist/boot/runner-deps.js +3 -297
  51. package/dist/boot/runtime-caps.js +0 -40
  52. package/dist/boot/session-faces.js +2 -133
  53. package/dist/boot/shutdown.js +9 -83
  54. package/dist/boot/side-query-lane.js +2 -137
  55. package/dist/boot/stores.js +15 -215
  56. package/dist/boot/task-list-lane.js +0 -18
  57. package/dist/boot/webfetch-summarize-lane.js +0 -47
  58. package/dist/boot/workflow-orchestration.js +3 -99
  59. package/dist/boot-reclaim.js +0 -20
  60. package/dist/bounded-session-map.js +0 -19
  61. package/dist/brain.js +2 -139
  62. package/dist/budget.js +9 -244
  63. package/dist/capabilities/builtin-tools.js +0 -3
  64. package/dist/capabilities/center-plugins.js +6 -51
  65. package/dist/capabilities/center-prompts.js +6 -71
  66. package/dist/capabilities/code-review-council.js +7 -40
  67. package/dist/capabilities/collab-workflows.js +1 -44
  68. package/dist/capabilities/hands-lane.js +0 -65
  69. package/dist/capabilities/memory-notice.js +0 -70
  70. package/dist/capabilities/prompt.js +0 -14
  71. package/dist/capabilities/prompts/code-review.js +0 -14
  72. package/dist/capabilities/prompts/identity.js +0 -6
  73. package/dist/capabilities/prompts/team.js +0 -4
  74. package/dist/capabilities/repo-tools.js +0 -34
  75. package/dist/capabilities/sandbox-file-send.js +3 -92
  76. package/dist/capabilities/scenarios.d.ts +0 -1
  77. package/dist/capabilities/scenarios.js +18 -279
  78. package/dist/capabilities/select-environment-tool.js +0 -32
  79. package/dist/capabilities/send-user-file-tool.js +2 -66
  80. package/dist/capabilities/skills.d.ts +1 -2
  81. package/dist/capabilities/skills.js +3 -21
  82. package/dist/capabilities/team.d.ts +3 -11
  83. package/dist/capabilities/team.js +3 -51
  84. package/dist/capabilities/tool-defer.js +0 -3
  85. package/dist/config-center/apply-effective.js +15 -330
  86. package/dist/config-center/apply-ledger.js +5 -38
  87. package/dist/config-center/facade.js +0 -41
  88. package/dist/config-center/hot-keys-registry.js +0 -20
  89. package/dist/config-center/http-client.js +2 -125
  90. package/dist/config-center/mcp-revocation.js +2 -34
  91. package/dist/config-center/read-face.js +0 -59
  92. package/dist/config-center/restart-signal.js +1 -85
  93. package/dist/config-center/skills-mcp.d.ts +1 -1
  94. package/dist/config-center/skills-mcp.js +9 -76
  95. package/dist/config-center/stage-limits.js +8 -35
  96. package/dist/config-invariants.js +0 -16
  97. package/dist/config-lkg.js +0 -42
  98. package/dist/config-provider.js +3 -186
  99. package/dist/config-types.js +0 -5
  100. package/dist/config.js +107 -1145
  101. package/dist/degenerate-instrument.js +3 -67
  102. package/dist/deployment-governance.js +0 -124
  103. package/dist/digest-form.js +0 -12
  104. package/dist/elicitation.js +3 -86
  105. package/dist/env-facts.js +7 -75
  106. package/dist/fleet/fleet-bus.js +34 -507
  107. package/dist/fleet/fleet-reconciler.js +9 -149
  108. package/dist/fleet/fleet-terminal-window.js +10 -178
  109. package/dist/fleet/subagent-tail-bus.js +3 -72
  110. package/dist/fleet-client.js +10 -70
  111. package/dist/fleet-lease.js +5 -79
  112. package/dist/git-api-kind.js +0 -3
  113. package/dist/governance-ask-marks.js +2 -78
  114. package/dist/hooks/branch-transcript.js +0 -74
  115. package/dist/hooks/cc-agent-hook-prompt.js +0 -29
  116. package/dist/hooks/cc-stop-prompt.js +1 -46
  117. package/dist/hooks/hook-llm.js +1 -53
  118. package/dist/hooks/hook-runner.js +20 -414
  119. package/dist/http/active-run-conflict.js +4 -129
  120. package/dist/http/cursor-fingerprint.d.ts +5 -0
  121. package/dist/http/cursor-fingerprint.js +5 -0
  122. package/dist/http/idempotency.js +0 -37
  123. package/dist/http/principal-gate.js +3 -40
  124. package/dist/http/route-ctx.js +0 -9
  125. package/dist/http/routes/a2a-serve.js +7 -319
  126. package/dist/http/routes/admin-config-refresh.js +0 -4
  127. package/dist/http/routes/admin-drain.js +0 -10
  128. package/dist/http/routes/adoption.js +1 -29
  129. package/dist/http/routes/agents-roster.js +1 -47
  130. package/dist/http/routes/approvals-assistant.js +32 -395
  131. package/dist/http/routes/attachments.js +4 -22
  132. package/dist/http/routes/capabilities.js +3 -420
  133. package/dist/http/routes/diagnostics.js +1 -81
  134. package/dist/http/routes/fleet.js +9 -185
  135. package/dist/http/routes/images.js +14 -239
  136. package/dist/http/routes/leader.js +0 -13
  137. package/dist/http/routes/memory-bundle.js +1 -61
  138. package/dist/http/routes/memory-policy.js +9 -99
  139. package/dist/http/routes/notify-wake.js +3 -37
  140. package/dist/http/routes/observability.js +2 -19
  141. package/dist/http/routes/retention-ops.js +2 -34
  142. package/dist/http/routes/rules.js +1 -93
  143. package/dist/http/routes/runs.js +64 -873
  144. package/dist/http/routes/session-sync.js +19 -258
  145. package/dist/http/routes/sessions-list.js +17 -43
  146. package/dist/http/routes/sessions.js +34 -226
  147. package/dist/http/routes/shared-memory.js +5 -36
  148. package/dist/http/routes/side-query.js +1 -87
  149. package/dist/http/routes/tasks.js +52 -722
  150. package/dist/http/routes/trace-usage.js +32 -195
  151. package/dist/http/routes/workflows.js +19 -195
  152. package/dist/http/run-meta.js +0 -6
  153. package/dist/http/send.js +0 -32
  154. package/dist/http/server.js +92 -1597
  155. package/dist/http/sse-lifecycle.js +2 -13
  156. package/dist/http/sse-log.js +3 -48
  157. package/dist/http/tar.js +5 -21
  158. package/dist/http/verify-rounds.js +0 -5
  159. package/dist/http/wire-gate.js +0 -9
  160. package/dist/http/workspace-content.js +0 -10
  161. package/dist/images/bake-validate.js +1 -70
  162. package/dist/images/manifest.js +1 -6
  163. package/dist/index.js +0 -21
  164. package/dist/key-resolver.js +2 -17
  165. package/dist/leader/diffout.js +1 -20
  166. package/dist/leader/diffup.js +0 -47
  167. package/dist/leader/endpoint.js +2 -57
  168. package/dist/leader/fanout.js +3 -45
  169. package/dist/leader/grader-env-factory.js +3 -72
  170. package/dist/leader/leader.js +5 -156
  171. package/dist/leader/merge.js +8 -115
  172. package/dist/leader/planner.js +3 -54
  173. package/dist/leader/repair-oracle.js +1 -60
  174. package/dist/leader/repair-wire.js +2 -79
  175. package/dist/leader/wire.js +8 -307
  176. package/dist/lsp/e2b-bridge.js +4 -64
  177. package/dist/lsp/e2b-manager.js +6 -94
  178. package/dist/lsp/lsp-frames.js +0 -12
  179. package/dist/lsp/manager.js +4 -96
  180. package/dist/lsp/ws-transport.js +5 -55
  181. package/dist/lsp-evict.js +1 -15
  182. package/dist/main.js +57 -765
  183. package/dist/memory-bundle-engine.js +0 -55
  184. package/dist/memory-export.js +0 -4
  185. package/dist/memory-posture.js +1 -15
  186. package/dist/memory-scope.js +11 -148
  187. package/dist/memory-sync-client.js +2 -44
  188. package/dist/memory-sync.js +1 -80
  189. package/dist/model-select.js +3 -80
  190. package/dist/observability/cost-quota.js +1 -17
  191. package/dist/observability/cost-taxonomy.js +0 -34
  192. package/dist/observability/fail-open.js +7 -86
  193. package/dist/observability/logger.js +0 -6
  194. package/dist/observability/metrics.js +0 -94
  195. package/dist/observability/otel-exporter.js +3 -13
  196. package/dist/observability/principal-context.js +0 -9
  197. package/dist/observability/prompt-manifest.js +1 -37
  198. package/dist/observability/rate-limit.js +0 -4
  199. package/dist/observability/secret-env-scrub.js +2 -56
  200. package/dist/observability/tool-trace.js +1 -70
  201. package/dist/orchestration/hardened-vm-runner.js +4 -118
  202. package/dist/orchestration/hardened-vm-worker-runner.js +1 -26
  203. package/dist/orchestration/hardened-vm-worker.js +0 -27
  204. package/dist/orchestration/subagent-steer.js +1 -45
  205. package/dist/orchestration/workflow-agent-steer.js +1 -80
  206. package/dist/orchestration/workflow-completion-inbox.js +32 -285
  207. package/dist/orchestration/workflow-notify-journal.js +16 -259
  208. package/dist/org-memory-admission.js +3 -47
  209. package/dist/parent-watch.js +2 -48
  210. package/dist/parked-decide.js +1 -109
  211. package/dist/per-task-image.js +0 -57
  212. package/dist/plan-cache-probe.js +3 -27
  213. package/dist/plugins/adoption-log-sql.js +2 -119
  214. package/dist/plugins/approval-ask-store-memory.js +3 -38
  215. package/dist/plugins/approval-ask-store-sql.js +6 -188
  216. package/dist/plugins/approval-exemption-store.js +2 -28
  217. package/dist/plugins/background-agent-store-sql.js +4 -105
  218. package/dist/plugins/background-shell-support.js +14 -122
  219. package/dist/plugins/blob-backend.js +6 -169
  220. package/dist/plugins/breaker-state-sql.js +8 -46
  221. package/dist/plugins/caching-session-store.js +4 -106
  222. package/dist/plugins/checkpoint-store-sql.js +18 -505
  223. package/dist/plugins/e2b-orphan-reclaim.js +0 -45
  224. package/dist/plugins/file-outcome-sink.js +0 -9
  225. package/dist/plugins/file-resume-anchor-store.js +4 -43
  226. package/dist/plugins/file-run-store.js +26 -364
  227. package/dist/plugins/file-snapshot-store-sql.js +10 -181
  228. package/dist/plugins/fork-routing-session-store.js +8 -111
  229. package/dist/plugins/host-platform.js +2 -91
  230. package/dist/plugins/image-bake-store-sql.js +7 -250
  231. package/dist/plugins/image-index-sql.js +4 -123
  232. package/dist/plugins/k8s-bg-scripts.js +4 -88
  233. package/dist/plugins/k8s-exec-protocol.js +0 -41
  234. package/dist/plugins/leader-run-store-sql.js +0 -101
  235. package/dist/plugins/local-checkpoint-store.js +8 -128
  236. package/dist/plugins/local-session-store.js +34 -296
  237. package/dist/plugins/local-task-attachment-store.js +2 -16
  238. package/dist/plugins/mailbox-store-sql.js +8 -65
  239. package/dist/plugins/memory-embedder-fingerprint.js +5 -166
  240. package/dist/plugins/memory-embedder.js +4 -70
  241. package/dist/plugins/memory-engine-pg.js +6 -170
  242. package/dist/plugins/memory-engine-tidb.js +7 -155
  243. package/dist/plugins/memory-engine-vector-util.js +0 -10
  244. package/dist/plugins/memory-key-guards.js +0 -34
  245. package/dist/plugins/memory-origin-law.js +0 -187
  246. package/dist/plugins/memory-resume-anchor-store.js +0 -17
  247. package/dist/plugins/memory-run-store.js +12 -89
  248. package/dist/plugins/memory-session-policy-store.js +0 -17
  249. package/dist/plugins/memory-sync-store-pg.js +4 -49
  250. package/dist/plugins/memory-sync-store-tidb.js +3 -35
  251. package/dist/plugins/outcome-ledger-sql.js +3 -97
  252. package/dist/plugins/permission-rule-store-file.js +5 -133
  253. package/dist/plugins/permission-rule-store-sql.d.ts +3 -0
  254. package/dist/plugins/permission-rule-store-sql.js +50 -307
  255. package/dist/plugins/pg-cost-quota.js +0 -7
  256. package/dist/plugins/pg-pool.js +0 -92
  257. package/dist/plugins/pg-rate-limiter.js +2 -13
  258. package/dist/plugins/pg-safe-json.js +4 -40
  259. package/dist/plugins/pg-session-storage.js +25 -189
  260. package/dist/plugins/posix-shell-fs.js +1 -31
  261. package/dist/plugins/remote-env-adb.js +12 -101
  262. package/dist/plugins/remote-env-e2b.js +40 -371
  263. package/dist/plugins/remote-env-file-error.js +0 -37
  264. package/dist/plugins/remote-env-host.js +63 -483
  265. package/dist/plugins/remote-env-k8s.js +32 -326
  266. package/dist/plugins/remote-env-local-docker.js +24 -157
  267. package/dist/plugins/remote-env-ssh.js +20 -128
  268. package/dist/plugins/remote-scratchpad.js +2 -32
  269. package/dist/plugins/remote-shell.js +1 -32
  270. package/dist/plugins/resume-anchor-store-sql.js +0 -11
  271. package/dist/plugins/retention-lane-store-sql.js +0 -108
  272. package/dist/plugins/retention-store-sql.js +4 -383
  273. package/dist/plugins/roster-store-sql.js +0 -55
  274. package/dist/plugins/run-store-sql.js +14 -251
  275. package/dist/plugins/s3-presign.js +2 -49
  276. package/dist/plugins/scheduler-support.js +3 -80
  277. package/dist/plugins/send-file-ledger.js +4 -53
  278. package/dist/plugins/send-user-file.js +4 -94
  279. package/dist/plugins/session-placement.js +1 -89
  280. package/dist/plugins/session-policy-store-sql.js +3 -81
  281. package/dist/plugins/session-store.js +0 -57
  282. package/dist/plugins/shared-memory-store-sql.js +4 -167
  283. package/dist/plugins/sql-driver.js +0 -17
  284. package/dist/plugins/sql-errors.js +0 -7
  285. package/dist/plugins/sql-escape.js +0 -8
  286. package/dist/plugins/sql-row-helpers.js +0 -25
  287. package/dist/plugins/store-backend.js +42 -222
  288. package/dist/plugins/store-contracts.js +2 -46
  289. package/dist/plugins/task-attachment-store.js +3 -49
  290. package/dist/plugins/task-list-store-sql.js +0 -79
  291. package/dist/plugins/tidb-cost-quota.js +1 -4
  292. package/dist/plugins/tidb-pool.js +1 -207
  293. package/dist/plugins/tidb-rate-limiter.js +3 -9
  294. package/dist/plugins/tidb-session-storage.js +4 -70
  295. package/dist/plugins/tidb-session-store.js +19 -341
  296. package/dist/plugins/tool-result-store-sql.js +4 -179
  297. package/dist/plugins/usage-window-store-sql.js +0 -8
  298. package/dist/plugins/web-search.js +10 -132
  299. package/dist/plugins/workflow-journal-store-sql.js +2 -58
  300. package/dist/plugins/workflow-run-store-sql.js +6 -91
  301. package/dist/plugins/worktree-isolation.js +6 -126
  302. package/dist/plugins/write-behind-counter.js +16 -75
  303. package/dist/principal-jwt.js +5 -60
  304. package/dist/project-memory.js +15 -146
  305. package/dist/prompts-domain-validate.js +1 -51
  306. package/dist/question.js +2 -111
  307. package/dist/resource-suspend.js +0 -18
  308. package/dist/router/route-orchestration.js +0 -77
  309. package/dist/rules-consent.d.ts +44 -5
  310. package/dist/rules-consent.js +75 -215
  311. package/dist/run-local.js +14 -381
  312. package/dist/runs.js +29 -617
  313. package/dist/runtime-caps-resolver.js +6 -132
  314. package/dist/runtime-governance.js +1 -232
  315. package/dist/sandbox-pkg-source.js +0 -37
  316. package/dist/sealed-key.js +3 -68
  317. package/dist/security.js +6 -291
  318. package/dist/session-leaf-bus.js +0 -32
  319. package/dist/session-sync-content.js +1 -69
  320. package/dist/session-sync-kernel.js +3 -60
  321. package/dist/session-sync.js +3 -66
  322. package/dist/session-titler.js +7 -41
  323. package/dist/session-watch.js +9 -73
  324. package/dist/shared-memory-scope-authorizer.js +0 -18
  325. package/dist/sighup-idle.js +1 -12
  326. package/dist/spec-fields.js +4 -143
  327. package/dist/store-live-probe.js +3 -49
  328. package/dist/task-a2a.js +1 -126
  329. package/dist/task-cwd.js +1 -103
  330. package/dist/task-mcp.js +1 -89
  331. package/dist/task-settings.js +10 -300
  332. package/dist/task-workflow.js +8 -75
  333. package/dist/tool-approval.d.ts +28 -1
  334. package/dist/tool-approval.js +70 -1430
  335. package/dist/trace/artifacts.js +5 -20
  336. package/dist/trace/engine-notice-wire.js +6 -138
  337. package/dist/trace/ledger-sink.js +7 -96
  338. package/dist/trace/project.js +4 -448
  339. package/dist/trace/redact.js +9 -77
  340. package/dist/turn-activity.js +1 -27
  341. package/dist/usage-analytics.js +4 -34
  342. package/dist/wall-clock-jump-guard.js +1 -68
  343. package/package.json +3 -3
  344. package/dist/capabilities/scenario-alias.d.ts +0 -27
  345. package/dist/capabilities/scenario-alias.js +0 -61
@@ -1,75 +1,3 @@
1
- /**
2
- * DESIGN-269 §3 车2 —— **server-as-peer** 半场:这台 sema-server 被别的 orchestrator 当成一个 A2A agent 调用。
3
- *
4
- * 两条路由,**刻意分挂在服务凭据门的两侧**(clay 终裁 R-11,2026-08-16):
5
- * · `GET /.well-known/agent-card.json` —— 门**外**。公网匿名可发现:一张卡就是「这里有一个 agent、
6
- * 它叫什么、能干什么、往哪儿调」,没有它对端连协商都开始不了。卡上**零存量信息**(没有任何 task /
7
- * run / session 面),所以匿名可读不构成披露。
8
- * · `POST /v1/a2a` —— 门**内**。JSON-RPC 调用面,吃与本仓其余 API 同一套服务凭据(`headers` 形),
9
- * 与我们 client 腿发出去的鉴权形对称。
10
- *
11
- * 总旋钮 `A2A_SERVE_ENABLED` 默认 **OFF**,OFF ⇒ 两条路由**都** 404(`config.a2aServe === undefined`
12
- * 即 OFF —— 缺席即关,见 config-types 的 {@link import("../../config-types.js").A2aServeConfig})。
13
- * 「不是发卡但拒调」是设计稿 §3.3 的原话:对外不存在就该整个不存在。
14
- *
15
- * ## 演化槽(设计稿 §4,本模块吸收其中三个)
16
- * · **版本槽**:读请求 `a2a-version` 头 —— 认识与否都照常答,响应恒带自己的版本头。版本头是**诊断材料**
17
- * 不是拒绝闸(开集容忍);真不兼容会在方法/形状层自然显形为具名错误。
18
- * · **方法槽**:分派器是一张 {@link A2A_METHODS} 方法表,新方法=加一行;未知方法恒 `-32601`。
19
- * 这是**协议内置的能力协商机制** —— 调用方按码降级,我们不自铸能力表。
20
- * · **词表槽**:任务态只发 core `A2A_TASK_STATES` 九词闭集(我们是铸点)。
21
- *
22
- * ## v1 明确不做(成文定界,防镀金 —— 设计稿 §3 尾)
23
- * · `message/stream`(SSE 腿)、push notification —— 未实现方法走 -32601,调用方按协议降级到轮询。
24
- * · `tasks/cancel` —— v2 映射到既有 cancel 链(`POST /v1/runs/:id/cancel`);v1 不做半吊子取消。
25
- * · card `securitySchemes` —— 我们的鉴权是**头形**,卡上如实不声明 OAuth 类 scheme,不假装。
26
- * · file / data part 的**消费** —— v1 只收 text part,其余 part **具名拒绝**(-32005),不静默丢:
27
- * 静默丢一个附件等于把「我没看见你给的文件」渲染成「我看过了」。
28
- * · 批量(数组形)JSON-RPC —— 一个批里半数成功半数失败的**记账**语义要单独想清楚,先具名拒。
29
- *
30
- * ## 🔴 id 域的定谳(施工期裁决,与设计稿 §3.2 的措辞有偏离,理由成文于此)
31
- * 设计稿 §3.2 写的是「`canAccessAgentRecord` 门后 `toA2ATaskState(record)` 投影」,那三只 core 原语
32
- * 的入参是 **`BackgroundAgentRecord`**(`a*` 句柄域 = 一次 run **内部**派生的后台子 agent 行)。而
33
- * 本车的硬约束是「`message/send` 必须复用既有提交链、不另铸提交路径」——既有提交链(`prepareSpec`
34
- * → `createDurableRun`)产出的是一条 **run 行**(`RunRecord`,uuidv7 taskId),它**永远不会**是一条
35
- * `BackgroundAgentRecord`(那是 core 在 run 内部为子 agent 铸的行,server 没有直接铸它的口)。两者的
36
- * status 词表也不同(run:`running|suspended|needs_review|completed|failed|blocked`;bg agent:
37
- * `running|parked|completed|failed|killed`),`toA2ATaskState` 的穷举 switch 对 `RunRecord` 根本不可调用。
38
- * ⇒ 采「run 句柄域」(施工任务书明列的两案之一):`Task.id` = run 的 `taskId`,`contextId` = `sessionId`,
39
- * 反枚举门 = 与 `GET /v1/runs/:id` **同一条** owner 判据,态投影 = {@link a2aStateForRun}
40
- * (与 core `toA2ATaskState` 同纪律:词表增删两向都是编译期红——载体是
41
- * {@link A2A_STATE_BY_RUN_STATUS} 的 `satisfies`;park 与否另问属主谓词 `isParkedRunStatus`,
42
- * 本文件零 park 词表。运行期只发九词闭集)。
43
- * 这不是「绕过 core 原语」:那几只原语的定义域里没有我们要投影的这条行,硬套才是撒谎。
44
- *
45
- * ## 🔎 方言对账(codex 轮4 R4-1/R4-2 的驳,证据在码上)
46
- * 有复审主张我们该改用 `SendMessage`/`GetTask` + `supportedInterfaces` 的 proto 派生绑定。**驳**:本
47
- * 半场的第一顺位互操作对象是 core 自己那条 A2A **客户端**腿(设计稿 §0 的立项理由就是「client 腿全链
48
- * 真实现、server 全树零接线」)。装树 `@sema-agent/core/dist/core/a2a.js` 亲读:它发 `message/send`
49
- * (:513)/`tasks/get`(:483)、置头 `a2a-version: 1.0`(:149,`A2A_PROTOCOL_VERSION = "1.0"` :28)、
50
- * 先拉 `/.well-known/agent-card.json`(:27)、按**顶层 `url` + `preferredTransport`** 协商(:244)。
51
- * 照那条建议改,恰好会与我们唯一能亲验的消费方互不相通。这条对账已钉成机器判据(见
52
- * `a2a-serve-rpc.test.ts` 的「轮4」格):装树换方言时当场红,那才是重新裁定的时刻。
53
- * 🟠 其中**一条**留作 v2 候选(不在 v1 射程,设计稿 §3 尾已把它列进「明确不做」):卡上不声明任何
54
- * security scheme ⇒ 发现面不告诉调用方「调用需要凭据」。R-11 裁的是「不假装 OAuth」,而一个如实的
55
- * `http: bearer` 声明并不构成假装 —— 值得在 v2 连同 card 字段面一起重议。
56
- *
57
- * ## 🟡 已知残余(登记,不假装没有)
58
- * ① `/v1/a2a` 是 `isBillableSubmitPath` 的**静态**成员(纯 url 谓词)。于是在「没有任何 service token
59
- * 且未开 `ALLOW_UNAUTHED_WRITES`」这一形的部署上,`POST /v1/a2a` 拿到的是 503
60
- * `auth.service_token_required` 而不是本模块的 404 —— 即便旋钮是 OFF。那是一条很弱的指纹(「这个
61
- * build 认得 /v1/a2a」),射程仅限一台**拒收一切写**的机器,且 server 的版本/能力面本就可发现。
62
- * 不修的理由:那道门必须在**解析请求体之前**判(未鉴权的写不该先被解析),而它只有 url 可看。
63
- * (drain / model-roster 两道**可用性**门已按方法下沉,见 `submitUnavailable` —— 它们不在这条残余里。)
64
- * ② `prepareSpec` 的**显式字段校验**分支(objective 空、sessionId 超长…)仍以裸 HTTP 4xx 应答,不进
65
- * JSON-RPC 信封。射程有界且不泄漏:那些文案只谈**本腿自己铸**的字段、不含任何内部词表(会泄漏的
66
- * typed 分支已经走 `onTypedFailure` 钩子改口,见 `rpcErrorFromTypedFailure`)。彻底收口要把那一串
67
- * `sendError` 全改成 typed 返回值 —— 那是共享腿的形变更,留给后续批。
68
- *
69
- * 分层:本模块只值 import `http/` 的叶子(send / principal-gate / idempotency / route-ctx)、
70
- * `routes/runs.js` 的**提交腿导出**(单向边,runs.ts 不反向引用本模块 ⇒ 不成环)与 src 下的普通模块
71
- * —— 绝不值 import server.ts(见 route-ctx.ts 头注的分层硬约束)。
72
- */
73
1
  import { createHash } from "node:crypto";
74
2
  import { A2A_TASK_STATES } from "@sema-agent/core";
75
3
  import { isParkedRunStatus } from "../../plugins/store-contracts.js";
@@ -78,12 +6,8 @@ import { scopedIdempotencyKey } from "../idempotency.js";
78
6
  import { sendJson, sendError } from "../send.js";
79
7
  import { gatedPrincipal } from "../principal-gate.js";
80
8
  import { createDurableRun } from "./runs.js";
81
- /** 本实现说的 A2A 协议版本 —— 卡的 `protocolVersion` 与每条 RPC 响应的 `a2a-version` 头同源(单点)。 */
82
9
  export const A2A_PROTOCOL_VERSION = "1.0";
83
- /** 版本头名。**读**请求侧的它只是诊断材料(开集容忍:认识与否都照常答),**写**响应侧的它是我们自报
84
- * 家门 —— 设计稿 §4-1 版本槽。 */
85
10
  export const A2A_VERSION_HEADER = "a2a-version";
86
- /** 配置 → 卡(纯投影,零 I/O、零存量读)。`build*`:返回纯数据。 */
87
11
  export function buildAgentCard(serve) {
88
12
  return {
89
13
  name: serve.name,
@@ -91,20 +15,9 @@ export function buildAgentCard(serve) {
91
15
  protocolVersion: A2A_PROTOCOL_VERSION,
92
16
  url: serve.url,
93
17
  preferredTransport: "JSONRPC",
94
- // 🔒 逐键**白名单**构造(fleet 的 stripT 同纪律):配置条目新增字段时默认**不**上卡,要上必须在这里
95
- // 显式写一行 —— 反过来(spread 配置对象再删几个键)会让下一个加到 A2aServeSkill 上的内部字段
96
- // 静默出现在公网文档上,那正是本行要防的事故形。
97
18
  skills: serve.skills.map((s) => ({ id: s.id, name: s.name ?? s.id, ...(s.description !== undefined ? { description: s.description } : {}) })),
98
19
  };
99
20
  }
100
- /**
101
- * 域入口①:公开发现面。**位置即契约** —— 必须挂在 `server.ts` 的全局 service-credential 门**之前**
102
- * (R-11「公网匿名可发现」;挂在门后等于要凭据才能发现,协商第一步就断)。
103
- *
104
- * 命名:本模块有**两个**域入口(卡在凭据门外、RPC 在门内),而 `route-shape-roster` 层E 的判据是
105
- * 「域名词干 ⊆ 某个 capability 位名」。两个名字都取成 `capabilities.a2aServe` 的**子串**
106
- * (`a2aServe` / `a2a`),于是两条都被那道门如实认出来 —— 而不是去放宽那道门的判据。
107
- */
108
21
  export async function handleA2aServe(req, res, url, ctx) {
109
22
  const miss = { fell: false };
110
23
  await handleA2aServeBody(req, res, url, ctx, miss);
@@ -114,8 +27,6 @@ async function handleA2aServeBody(req, res, url, ctx, miss) {
114
27
  const { deps } = ctx;
115
28
  if (req.method === "GET" && url === "/.well-known/agent-card.json") {
116
29
  const serve = deps.config.a2aServe;
117
- // OFF ⇒ 404,与「本部署没有这条路由」逐字节同形:对外不存在就是不存在,不发一个「功能已禁用」的
118
- // 具名错误(那本身就是一条「这里跑的是 sema-server 且它认得 A2A」的指纹)。
119
30
  if (!serve) {
120
31
  sendError(res, 404, "not_found.route", "not found");
121
32
  return;
@@ -126,52 +37,16 @@ async function handleA2aServeBody(req, res, url, ctx, miss) {
126
37
  }
127
38
  miss.fell = true;
128
39
  }
129
- // ─────────────────────────────────────────────────────────────────────────────
130
- // 件2:JSON-RPC 端点(服务凭据门**内**)
131
- // ─────────────────────────────────────────────────────────────────────────────
132
- /** JSON-RPC 2.0 传输层错误码(协议保留段)。 */
133
40
  const JSONRPC_PARSE_ERROR = -32700;
134
41
  const JSONRPC_INVALID_REQUEST = -32600;
135
42
  const JSONRPC_METHOD_NOT_FOUND = -32601;
136
43
  const JSONRPC_INVALID_PARAMS = -32602;
137
44
  const JSONRPC_INTERNAL_ERROR = -32603;
138
- /** A2A 自有错误码(spec 的 -32001..-32006 段)。只用我们真会发的三只。 */
139
45
  const A2A_TASK_NOT_FOUND = -32001;
140
46
  const A2A_UNSUPPORTED_OPERATION = -32004;
141
47
  const A2A_CONTENT_TYPE_NOT_SUPPORTED = -32005;
142
- /**
143
- * 🔴 **反枚举不变式(A-041)在本模块的执行点**:`tasks/get` 的「这条 task 不存在」与「这条 task 不是
144
- * 你的」必须**逐字节**同形 —— 同一个 code、同一条 message、同一个(缺席的)data。两臂共用**这一个**
145
- * 常量就是那条不变式的机制支撑:任何一臂想说得更具体,都得先改这里,而改这里会被
146
- * `a2a-serve-rpc.test.ts` 的逐字节比对当场钉住。
147
- *
148
- * (同族先例:`route-ctx.ts` 的 `RUN_NOT_FOUND_MESSAGE` —— #262 抓到的正是两臂**文案长度**不同就足以
149
- * 枚举他人 run id 的存在性。JSON-RPC 侧一模一样,只是载体从 HTTP body 换成了 error 对象。)
150
- */
151
48
  export const A2A_TASK_NOT_FOUND_MESSAGE = "task not found";
152
- /** 无 durable run store 时两个方法的**同一条**具名回答(不是静默降级成一次同步跑)。 */
153
49
  const A2A_NO_RUN_STORE_MESSAGE = "this deployment has no durable run store, so A2A tasks cannot be created or polled (set DB_BACKEND=mysql|pg|local)";
154
- /**
155
- * `RunRecord["status"]` → A2A 任务态的**逐词处置表**。
156
- *
157
- * `satisfies Record<RunRecord["status"], …>` 是这张表的执法点(fleet 的 `SCOPED_TASK_KEY_DISPOSITION`
158
- * 同纪律):core 给 `TaskStatus` **增**一个成员 ⇒ 表不完整、tsc 红,加成员的人被迫当场回答「这个新态
159
- * 投成哪个协议词」;**删**一个成员 ⇒ 多余键同样红。写成表而不是 `switch` 的第二个理由见下面 `null` 那两行。
160
- *
161
- * 逐词理由:
162
- * · `running` → `working`。
163
- * · `completed` → `completed`。
164
- * · `failed` → `failed`(`errorCode === "cancelled"` 的分叉在读点:那是**有人明确要求**的停止,写
165
- * `POST /v1/runs/:id/cancel` 时落的字面量。与 core 对 `killed` 行按 `stoppedBy` 分叉的判词同源 ——
166
- * `canceled` 断言的是一个意图,没人表达过就不许替他断言)。
167
- * · `blocked` → `failed`。它是终局的「跑不下去了」;协议的 `rejected` 说的是**准入时**拒收
168
- * (我们准入拒收会在 RPC 层直接报错、根本不建行),投它会撒谎。
169
- * · **park 族两词 → `null`**,读作「这个词的归属不归本表裁,归 `store-contracts.ts` 的
170
- * `isParkedRunStatus`(词表属主:穷举 switch + `never` 臂)」。本文件**不留第二张 park 词表** ——
171
- * 那正是 `source-hygiene-gate` 点名的病:手抄形绕开属主的编译期执法,core 新增一个 park 类成员时
172
- * 手抄处会静默判「非 park」。运行期这两个值到不了(读点先问属主);到得了就说明两边不一致,那时
173
- * 诚实答 `unknown` 好过在这里再猜一次。
174
- */
175
50
  const A2A_STATE_BY_RUN_STATUS = {
176
51
  running: "working",
177
52
  completed: "completed",
@@ -180,47 +55,26 @@ const A2A_STATE_BY_RUN_STATUS = {
180
55
  suspended: null,
181
56
  needs_review: null,
182
57
  };
183
- /**
184
- * run 行 → A2A 任务态。词表槽(设计稿 §4-2):我们是铸点,只发 core `A2A_TASK_STATES` 九词闭集。
185
- *
186
- * 判据分工:park 与否问**属主谓词**;其余按 {@link A2A_STATE_BY_RUN_STATUS} 投影(那张表的
187
- * `satisfies` 是词表增删两向的编译期执行点)。
188
- *
189
- * park → `input-required` 的语义:协议的「等调用方给点什么」在本引擎被**收窄**成「等一个审批决议」,
190
- * 而那个决议走的是持久审批口、不是一条回信 —— 与 core 对自己 `parked` 行的裁词同源。A2A 调用方因此
191
- * **不能**靠再发一条 message 解开它(附录 C.3 的表里成文写着这句)。
192
- */
193
58
  export function a2aStateForRun(run, stale) {
194
- // stale 折叠与 `GET /v1/runs/:id` 同源:那条读面把「running 但心跳超期」直接答成 failed;两个读面
195
- // 对同一行给出不同的终局判断会是最难查的一类不一致。
196
59
  if (stale)
197
60
  return "failed";
198
61
  if (isParkedRunStatus(run.status))
199
- return "input-required"; // 判据属主单源,本文件零 park 词表
200
- // 表外的词 = 滚动升级里更新副本写入的新状态(运行期真会发生:SQL 文本列被裸 cast 成联合)。
201
- // `unknown` 是九词闭集里**为此而设**的那一个,也是唯一诚实的答案:本进程读不懂这行就说读不懂,
202
- // 而不是猜一个终局态让对端停止轮询。
62
+ return "input-required";
203
63
  const mapped = A2A_STATE_BY_RUN_STATUS[run.status];
204
64
  if (mapped === null || mapped === undefined)
205
65
  return "unknown";
206
66
  return mapped === "failed" && run.errorCode === "cancelled" ? "canceled" : mapped;
207
67
  }
208
- /** 一条我们铸的 agent 消息(`status.message`)。 */
209
68
  function agentMessage(taskId, contextId, text) {
210
69
  return {
211
70
  kind: "message",
212
71
  role: "agent",
213
- // messageId 由**我们**铸(协议要求每条消息有 id);由 taskId 派生 ⇒ 同一条 task 的重复读稳定同值。
214
72
  messageId: `${taskId}:status`,
215
73
  parts: [{ kind: "text", text }],
216
74
  taskId,
217
75
  contextId,
218
76
  };
219
77
  }
220
- /** 状态文本。终局 completed 发最终文本(与 `GET /v1/runs/:id` 发的 `result` 是**同一份数据**,同样
221
- * 不裁剪 —— 裁剪会造出一个「其余部分在哪里」的问题,而这条协议面没有第二个读口可指);失败发错误
222
- * 文本;park 态发一句**不带任何工具/门细节**的通用说明(对端是外部 agent,不该从状态行里读出我们
223
- * 内部拦了什么工具)。 */
224
78
  function statusTextForRun(run, state) {
225
79
  if (state === "input-required")
226
80
  return "this task is suspended awaiting an approval decision on the serving deployment; it resumes once that decision is made";
@@ -232,8 +86,6 @@ function statusTextForRun(run, state) {
232
86
  return run.result.errorMessage;
233
87
  return undefined;
234
88
  }
235
- /** run 行 → A2A Task。**最小披露**:只发 id / contextId / 态 / 一段状态文本 —— stats、cost、model、
236
- * source、jobId、owner 一律不上(对端要的是「做完没有、结果是什么」,其余都是内部账)。 */
237
89
  function taskFromRun(run, stale) {
238
90
  const state = a2aStateForRun(run, stale);
239
91
  const text = statusTextForRun(run, state);
@@ -248,7 +100,6 @@ function taskFromRun(run, stale) {
248
100
  },
249
101
  };
250
102
  }
251
- /** 具名 RPC 错误 —— 处理器抛它,分派器翻成 JSON-RPC error 体。 */
252
103
  class RpcError extends Error {
253
104
  code;
254
105
  constructor(code, message) {
@@ -256,7 +107,6 @@ class RpcError extends Error {
256
107
  this.code = code;
257
108
  }
258
109
  }
259
- // ── 请求形校验(小纯函数,便于逐条钉) ─────────────────────────────────────────
260
110
  function asRecord(v, what) {
261
111
  if (typeof v !== "object" || v === null || Array.isArray(v))
262
112
  throw new RpcError(JSONRPC_INVALID_PARAMS, `${what} must be an object`);
@@ -269,17 +119,10 @@ function asNonEmptyString(v, what, max) {
269
119
  throw new RpcError(JSONRPC_INVALID_PARAMS, `${what} must be at most ${max} characters`);
270
120
  return v;
271
121
  }
272
- /** 上限:与 `prepareSpec` 的 `sessionId` 列宽同源(64)—— 越界在这里具名拒,不留给 INSERT 去截断。 */
273
122
  const MAX_CONTEXT_ID_CHARS = 64;
274
- /** `messageId` 只当幂等键用,长度按既有 Idempotency-Key 面同量级收窄。 */
275
123
  const MAX_MESSAGE_ID_CHARS = 200;
276
- /** task id 读面上限(uuidv7 远短于此;这一格防的是无界字符串进 store 查询)。 */
277
124
  const MAX_TASK_ID_CHARS = 128;
278
- /** skill id 上限与 config 侧声明面同源。 */
279
125
  const MAX_SKILL_ID_CHARS = 128;
280
- /** `message.parts` → objective。**只收 text part,其余具名拒绝**(v1 定界):静默丢一个 file part
281
- * 等于把「我没看你给的文件」渲染成「我看过了」。A2A 的 `kind` 与旧稿的 `type` 两种拼法都认(读侧
282
- * 开集容忍;写侧我们只铸 `kind`)。 */
283
126
  function objectiveFromParts(parts) {
284
127
  if (!Array.isArray(parts) || parts.length === 0)
285
128
  throw new RpcError(JSONRPC_INVALID_PARAMS, "params.message.parts must be a non-empty array");
@@ -300,13 +143,7 @@ function objectiveFromParts(parts) {
300
143
  throw new RpcError(JSONRPC_INVALID_PARAMS, "params.message carries no non-empty text part — there is nothing to work on");
301
144
  return objective;
302
145
  }
303
- /** 我们在 `message.metadata` 上认的**唯一**扩展键:调用方用它点名一条**卡上公告过**的 skill。
304
- * A2A spec 本身没有 skill 选择字段(卡只做广告),`metadata` 是它的开放扩展点 —— 命名空间前缀防撞。
305
- * 缺席 ⇒ 走部署缺省场景(既有行为);在场但不认识 ⇒ **具名拒绝**:路由输入静默回落到缺省场景 =
306
- * 请求以为自己选了 A、实际跑了 B,是最坏的一种「成功」。 */
307
146
  const SKILL_METADATA_KEY = "sema.skillId";
308
- /** `message.metadata["sema.skillId"]` → 那条**已声明**的 skill(缺席 ⇒ undefined = 走部署缺省场景)。
309
- * ⚠️ 错误文案里**不回显** scenario —— 卡公开的是 skill id,内部场景键不是公开面。 */
310
147
  function skillFromMetadata(metadata, serve) {
311
148
  if (metadata === undefined || metadata === null)
312
149
  return undefined;
@@ -320,16 +157,6 @@ function skillFromMetadata(metadata, serve) {
320
157
  throw new RpcError(JSONRPC_INVALID_PARAMS, `unknown skill id ${JSON.stringify(id)} — the skills this agent serves are the ones listed on its agent card`);
321
158
  return skill;
322
159
  }
323
- /**
324
- * typed 提交拒绝(`HttpError`)→ JSON-RPC 错误。**只带我们自己的话**:原 `message` 与 `extra` 一律不上线。
325
- *
326
- * 🔴 codex 轮2 R2-4:那些 `extra` 里装的是给壳看的指路材料 —— `scenario_unknown` 的 extra 是**本部署的
327
- * 全部场景名**,`scenario_not_allowed` 的是这位 principal 的场景白名单。发给一个**外部 peer** 就是内部
328
- * 词表泄漏,而 skill 声明制存在的全部理由正是不把场景名给公网(设计稿 §3.1)。这里改口成:只说**公开的**
329
- * skill id 出了什么问题,真因(哪个场景不存在)只进服务端日志。
330
- *
331
- * 4xx = 调用方的参数问题 ⇒ -32602;其余(5xx/意外)⇒ -32603,同样不带内部细节。
332
- */
333
160
  function rpcErrorFromTypedFailure(err, skillId) {
334
161
  const where = skillId !== undefined ? `skill ${JSON.stringify(skillId)}` : "this request";
335
162
  if (err.status >= 400 && err.status < 500) {
@@ -337,31 +164,13 @@ function rpcErrorFromTypedFailure(err, skillId) {
337
164
  }
338
165
  return new RpcError(JSONRPC_INTERNAL_ERROR, "internal error");
339
166
  }
340
- /**
341
- * **可用性**两门,按**方法**判(不是按 url)。
342
- *
343
- * `handle()` 里那两道 503(drain / model-roster-pending)挂在 `isBillableSubmitPath` 上,而那是一张**纯
344
- * url** 谓词表 —— 对 `/v1/a2a` 一刀切的代价是 `tasks/get` 这条纯读也一起 503。单副本部署(或卡上公告的
345
- * 就是这一台)于是恰好在重启/依赖降级的窗口里取不回已完成的结果,而那正是外部 peer 最需要读的时刻。
346
- * ⇒ `handle()` 对本 url **跳过**这两道(见 `isMethodDispatchedSubmitPath`),改由本函数在 `message/send`
347
- * 里施加。**服务凭据门那一道不动**:它判的是「这台机没有任何 service token 时能不能收写」,与方法无关,
348
- * 而把它挪进方法层会让一次未鉴权的提交先被解析、再被拒 —— 门必须尽早。
349
- * 文案与 `handle()` 的两道逐字同源(消费端按 `error:"draining"` 字面判型,那是冻结的 wire 契约)。
350
- * 🔴 **同源不止文案,还包括体上的 additive 键**(A-054.9,2026-08-19 合并重扫 confirmed):#291 给
351
- * draining 503 体加 `reason` 时只加在 `handle()` 那道门上,本函数一个字没带 —— 而 wire 契约附录 A 的
352
- * 序言明写「**码是判别语义**,同一语义的多个站点共用同一个码」(按码编排、跨站点、无路径辖域),
353
- * `draining` 行又逐字承诺「7.35.0+:体上可带 additive 键 `reason`」,C.5 还让 a2a 消费端就地读 503 体。
354
- * agent card 只公告 `/v1/a2a` 一个 url ⇒ 外部 peer 在这条腿上读不到因由就是真的读不到。此前那句
355
- * 「文案逐字同源」不算说谎(漂的是键不是文案),但它正是让人以为两道同源、从而漏补的诱因 —— 所以本注
356
- * 从此把「同源」的范围写死到体级。新增站点照抄这一条:同码 ⇒ 同键集。
357
- */
358
167
  function submitUnavailable(res, ctx) {
359
168
  const { deps } = ctx;
360
169
  if (deps.drainState?.draining) {
361
170
  res.setHeader("retry-after", "15");
362
171
  sendError(res, 503, "draining", "draining", {
363
172
  message: "this instance is draining for shutdown/upgrade — retry against the replacement instance",
364
- ...(deps.drainState.reason ? { reason: deps.drainState.reason } : {}), // 未声明 ⇒ 键缺席(additive 纪律)
173
+ ...(deps.drainState.reason ? { reason: deps.drainState.reason } : {}),
365
174
  });
366
175
  return true;
367
176
  }
@@ -372,8 +181,6 @@ function submitUnavailable(res, ctx) {
372
181
  }
373
182
  return false;
374
183
  }
375
- /** `configuration.blocking` 的读取。spec 缺省 false;非布尔值**具名拒**(一个 `"true"` 字符串被当成
376
- * 真,会让调用方以为自己在同步等)。 */
377
184
  function readBlocking(params) {
378
185
  if (params.configuration === undefined)
379
186
  return false;
@@ -384,21 +191,9 @@ function readBlocking(params) {
384
191
  throw new RpcError(JSONRPC_INVALID_PARAMS, "params.configuration.blocking must be a boolean");
385
192
  return c.blocking;
386
193
  }
387
- /** 与 `GET /v1/runs/:id` 逐字同源的 stale 判据(running 但心跳超期 ⇒ 折成 failed)。 */
388
194
  function isStaleRun(run, runStaleSec) {
389
195
  return run.status === "running" && Date.now() - new Date(run.updatedAt).getTime() > runStaleSec * 1000;
390
196
  }
391
- /**
392
- * 有界等待一条 run 走出 `working`。`budgetMs<=0` ⇒ 只读一次(非 blocking 形)。
393
- * 轮询 durable 行而不是挂进程内事件:这条 run 可能由**另一个副本**驱动,durable 行才是跨副本的唯一真相。
394
- *
395
- * 🔴 codex 轮4 R4-3 随修的两条**止损**(同族病:一个不看外界的循环在依赖降级时最能放大伤害):
396
- * ① **对端断了就停**:每拍检查 `res.writableEnded / socket.destroyed`。没有这一条,一个已经走掉的
397
- * 调用方仍能让我们对库读满整个窗(fleet 那条流的 ackIfAlive 是同一句判词)。
398
- * ② **库读失败不再静默当 miss**:原先 `.catch(() => undefined)` 会把一次真故障读成「还没好」,于是
399
- * 在库正挂着的时候反而把读打满(把故障隐藏 + 放大)。改成**当拍停**并把已知的最后一手交回去 ——
400
- * 调用方拿到的是 `submitted`/上一拍的真状态,自己去轮询,而不是我们替他空转。
401
- */
402
197
  async function waitForRun(ctx, res, taskId, budgetMs) {
403
198
  const { deps } = ctx;
404
199
  const store = deps.runStore;
@@ -413,7 +208,7 @@ async function waitForRun(ctx, res, taskId, budgetMs) {
413
208
  }
414
209
  catch (e) {
415
210
  deps.logger?.warn?.("a2a_serve_wait_read_failed", { error: e?.message });
416
- return last; // ② 读挂了就别再敲它
211
+ return last;
417
212
  }
418
213
  last = run ?? last;
419
214
  if (run && a2aStateForRun(run, isStaleRun(run, deps.config.runStaleSec)) !== "working")
@@ -422,7 +217,7 @@ async function waitForRun(ctx, res, taskId, budgetMs) {
422
217
  if (left <= 0)
423
218
  return run;
424
219
  if (res.writableEnded || res.socket?.destroyed)
425
- return run; // ① 对端已走,别替他继续读库
220
+ return run;
426
221
  await new Promise((r) => {
427
222
  const t = setTimeout(r, Math.min(250, Math.max(1, left)));
428
223
  if (typeof t.unref === "function")
@@ -430,18 +225,6 @@ async function waitForRun(ctx, res, taskId, budgetMs) {
430
225
  });
431
226
  }
432
227
  }
433
- /**
434
- * `message/send` —— 翻译进**既有**任务提交链。
435
- *
436
- * 复用点(不另铸提交路径):`ctx.legs.prepareSpec(req, res, body)` 的读体/校验/建 spec 段 +
437
- * {@link createDurableRun}(`POST /v1/runs` 的建行+驱动段,本车从 runs.ts 提取成具名腿,两个调用方
438
- * 共用同一份实现)。本模块**没有**自己的 `createRun` / `runInBackground` 调用。
439
- *
440
- * blocking 语义:A2A `configuration.blocking` 缺省 false(spec)。true 时按 `A2A_SERVE_BLOCKING_WAIT_MS`
441
- * 有界等待 —— blocking 的意思是「尽力等」,不是「必须等到终局」:窗到期回 `working` 态 + id,调用方按
442
- * 协议转 `tasks/get` 轮询,这是**正常路径**不是降级。窗必须远小于 core 客户端腿的 10min 总预算:那是
443
- * **调用方**的钱包,而按住一条 inbound 请求 10 分钟花的是**我们**的连接池。
444
- */
445
228
  async function handleMessageSend({ req, res, ctx, params, serve }) {
446
229
  const { deps } = ctx;
447
230
  const { rateLimited, quotaExceeded } = ctx.helpers;
@@ -449,41 +232,15 @@ async function handleMessageSend({ req, res, ctx, params, serve }) {
449
232
  if (message.role !== "user")
450
233
  throw new RpcError(JSONRPC_INVALID_PARAMS, `params.message.role must be "user" (this is the inbound leg — an agent-role message has no meaning here)`);
451
234
  const objective = objectiveFromParts(message.parts);
452
- // contextId = 我们的 sessionId(协议的「同一段对话」与本引擎的 session 是同一个概念)。缺席 ⇒ 提交链铸新的。
453
235
  const contextId = message.contextId === undefined ? undefined : asNonEmptyString(message.contextId, "params.message.contextId", MAX_CONTEXT_ID_CHARS);
454
- // 🔴 codex 轮 H2a:`messageId` **必填**。协议本就把它列为 `Message` 的必填字段,而在这里它还兼着
455
- // **幂等键** —— 收一条没有它的消息 = 悄悄把去重整个关掉(重发即第二条 run、第二笔账),那正是
456
- // 「无声 fail-open」。缺席具名拒,不给「宽容」留口子。
457
236
  const messageId = asNonEmptyString(message.messageId, "params.message.messageId", MAX_MESSAGE_ID_CHARS);
458
237
  const skill = skillFromMetadata(message.metadata, serve);
459
238
  const skillId = skill?.id;
460
239
  const scenario = skill?.scenario;
461
- // 🔴 codex 轮3 R3-1(验真后修):**请求形校验必须全部走完再动状态**。这一行原先在下面等待窗那里才求值 ——
462
- // 于是一个 `blocking:"true"`(字符串)会拿到 -32602,而一条计费 run 已经建好并在跑:调用方既没有收据、
463
- // 重试还会再开一条。「拒了却已经烧上钱」是最坏的组合,校验不该有先后之分。
464
240
  const blocking = readBlocking(params);
465
241
  if (!deps.runStore)
466
242
  throw new RpcError(A2A_UNSUPPORTED_OPERATION, A2A_NO_RUN_STORE_MESSAGE);
467
- // 幂等两道(与 `POST /v1/runs` **同一对**机制,不新铸第三种):
468
- // ① in-memory:按身份加盐的 `Idempotency-Key` 缓存 —— 同副本内的并发/快速重试共享同一次产出。
469
- // ② durable:caller-minted `body.taskId` 重放,读的是 run **store** ⇒ 跨副本、跨重启都成立。
470
- // A2A 调用方不知道我们的 taskId 体例,所以由**我们**从 (身份, messageId) 派生一个 uuidv7 形的 id。
471
- // 🔒 **按身份加盐**是承重的,不是装饰:不加盐时两位租户用同一个 messageId 会派生出同一个 taskId,
472
- // 后到的那位撞上 `409 taskId already exists` —— 那是一个现成的跨租户存在性 oracle(猜 messageId
473
- // 就能验证别人发过没有),与 A-041 直接冲突。加盐后各租户各得各的键,那条臂对 A2A 面不可达。
474
- //
475
- // 🔴 codex 轮2 R2-2(验真后修):键里**必须**含请求内容。只按 (身份, messageId) 取键时,一个复用了
476
- // 旧 messageId 的**新**请求会被判成重试 —— 它**从未执行**,而调用方拿回一条与自己所问无关的旧结果
477
- // (无凭据部署下两位调用方还共用同一个键空间,后果更糟)。判据改成:幂等键 = (身份, messageId,
478
- // **这条消息的规范化内容**)。于是 ①真重试(逐字节相同)照旧只跑一次;②同 id 换内容 = 另一条任务,
479
- // 真的会被执行。v1 **不**做 Stripe 式「同键异体 ⇒ 409 拒」——那需要一列持久指纹(= SQL 面,不在本车),
480
- // 而它要防的伤害在此已经没有了:代价从「静默回放无关结果」降到「多一条本来就该跑的任务」。
481
243
  const idemScope = `${ctx.req.source ?? ""}\x1f${gatedPrincipal(req, deps.config) ?? ""}`;
482
- //
483
- // 🔴 codex 轮3 R3-3(验真后修):指纹只由**线上输入**构成 —— 取调用方给的 `skillId`,**不取**服务端
484
- // 解析出的内部 `scenario`。后者是**可变的服务端路由**:滚动配置变更期两台副本可以把同一个已公告的
485
- // skill 映射到不同场景 ⇒ 同一条线请求在两台上派生出不同 taskId ⇒ 一次 failover 重试开出第二条 run,
486
- // 恰恰推翻我们对外承诺的「跨副本/跨重启只有一条」。幂等的自变量必须是调用方看得见的那些字节。
487
244
  const fingerprint = createHash("sha256")
488
245
  .update(messageId).update("\x1f")
489
246
  .update(objective).update("\x1f")
@@ -497,23 +254,8 @@ async function handleMessageSend({ req, res, ctx, params, serve }) {
497
254
  ...(contextId !== undefined ? { sessionId: contextId } : {}),
498
255
  ...(scenario !== undefined ? { scenario } : {}),
499
256
  };
500
- // 🔴 codex 轮 H1:准入门。顺序与 `POST /v1/runs` 逐字同源 —— **重放先于 rate/quota**(一次重试不该
501
- // 因为窗满改判 429、把一条已经建好的 run 的收据吞掉;那条口的「复审 C1」注写的就是这件事),
502
- // 而一次**真新**的提交必须与原生提交面吃同一组限流与累计成本门。修前这条链上只有 lease/治理窗,
503
- // 于是一个已鉴权的外部 peer 可以无视本部署配置的 `RATE_LIMIT_RPM` / `MAX_PRINCIPAL_COST_USD`。
504
- // 🔴 codex 轮3 R3-2(验真后修):**durable 重放是 message/send 的第一个有状态动作**,排在 rate/quota
505
- // 之前。只查进程内缓存不够 —— 丢响应后的重试、重启后的重试、LB failover 全都落到一台**缓存是空的**
506
- // 副本上;那时先撞 429 的后果不是「稍后再试」,而是调用方**永远拿不到**一条已经在跑、已经在计费的
507
- // task 的句柄。与「重放先于 rate/quota」是同一条判据,只是把它从进程内延伸到跨副本。
508
- // owner 门与 `tasks/get` 同源(验证过的 principal;`owner === null` = 无主部署的行)。
509
257
  const replay = await deps.runStore.getRun(body.taskId).catch(() => undefined);
510
258
  if (replay && (replay.owner === null || replay.owner === gatedPrincipal(req, deps.config))) {
511
- // 🔴 codex 轮4 R4-3(验真后修):重放**立即**作答,**不**进等待循环 —— 哪怕 `blocking:true`。
512
- // 理由是位置决定的:这一段排在 rate/quota **之前**(R3-2 要求如此,否则重试拿不到已计费 task 的
513
- // 句柄),于是任何在这里发生的等待都**不受任何准入门约束** —— 一场重试风暴能对同一条 task 叠出
514
- // 任意多个各自每 250ms 读一次库的独立循环,而且恰好发生在依赖降级、库最脆弱的时候。
515
- // 语义上也正确:重放的对象是一条**已经受理**的 task,协议给调用方的出路就是 `tasks/get` 轮询;
516
- // `blocking` 是「这次**提交**尽力等」,不是「每次重试都再等一轮」。
517
259
  return taskFromRun(replay, isStaleRun(replay, deps.config.runStaleSec));
518
260
  }
519
261
  const cached = idemKey ? ctx.registry.idemCache.peek(idemKey) : undefined;
@@ -522,14 +264,10 @@ async function handleMessageSend({ req, res, ctx, params, serve }) {
522
264
  receipt = await cached;
523
265
  }
524
266
  else {
525
- // 🔴 codex 轮2 R2-3(验真后修):**可用性**两门(drain / model-roster-pending)在这里、按**方法**判,
526
- // 而不是在 `handle()` 里按 url 判 —— 见 `isMethodDispatchedSubmitPath` 的顶注:整条 `/v1/a2a` 按 url
527
- // 一刀切会让 `tasks/get` 这条**纯读**在排空/roster 未落时一并 503,而单副本部署恰恰在重启窗里最需要
528
- // 取回已完成的结果。写面照旧 503(一台正在下线的副本不许开新模型工作)。
529
267
  if (submitUnavailable(res, ctx))
530
268
  return undefined;
531
269
  if (rateLimited(req, res) || quotaExceeded(req, res))
532
- return undefined; // 已应答 429
270
+ return undefined;
533
271
  let typed;
534
272
  const prepared = await ctx.legs.prepareSpec(req, res, body, (e) => {
535
273
  typed = e;
@@ -537,14 +275,13 @@ async function handleMessageSend({ req, res, ctx, params, serve }) {
537
275
  if (typed)
538
276
  throw rpcErrorFromTypedFailure(typed, skillId);
539
277
  if (!prepared)
540
- return undefined; // prepareSpec 的显式字段校验分支已自行应答(见下方 🟡 残余注)
278
+ return undefined;
541
279
  receipt = await createDurableRun(req, res, ctx, prepared, idemKey);
542
280
  }
543
281
  if (!receipt)
544
- return undefined; // 提交腿已应答
282
+ return undefined;
545
283
  const receiptBody = receipt.body;
546
284
  if (receipt.status !== 202 || typeof receiptBody.taskId !== "string" || typeof receiptBody.sessionId !== "string") {
547
- // 既有提交链的非 202 收据(会话占用 409 等)。翻成具名 RPC 错误,**不**假装建成了一条 task。
548
285
  throw new RpcError(JSONRPC_INVALID_PARAMS, `the serving deployment did not accept this submission (it answered ${receipt.status}); a session already running a turn must finish or be cancelled before the next message on the same contextId`);
549
286
  }
550
287
  const taskId = receiptBody.taskId;
@@ -552,7 +289,6 @@ async function handleMessageSend({ req, res, ctx, params, serve }) {
552
289
  const run = await waitForRun(ctx, res, taskId, blocking ? serve.blockingWaitMs : 0);
553
290
  if (run)
554
291
  return taskFromRun(run, isStaleRun(run, deps.config.runStaleSec));
555
- // 行刚建完还没读回来(或读失败):照 202 收据答 `submitted` —— 协议里正是「已受理、尚未开始」那个词。
556
292
  return {
557
293
  kind: "task",
558
294
  id: taskId,
@@ -560,19 +296,6 @@ async function handleMessageSend({ req, res, ctx, params, serve }) {
560
296
  status: { state: "submitted", timestamp: new Date().toISOString() },
561
297
  };
562
298
  }
563
- /**
564
- * (身份, messageId) → 一个 **uuidv7 形**的确定性 taskId,喂给既有的 caller-minted `body.taskId`
565
- * durable 幂等通道(`createDurableRun` 里那段读 run store 的重放)。
566
- *
567
- * 为什么派生而不是直接用 messageId:那条通道的形门是 `isUuidV7`(server 自己铸的字母表,防调用方注入
568
- * 任意 PK 形),而 A2A 的 `messageId` 是任意字符串。派生同时解决三件事 —— 形合法、**跨副本/跨重启**
569
- * 生效(读的是 store,不是进程内缓存)、且**按身份加盐**(见调用点的 🔒 注:不加盐就是一个跨租户
570
- * 存在性 oracle)。
571
- *
572
- * 摘要 → uuid 的位改写:第 7 字节高半字节写 `7`(version),第 9 字节高两位写 `10`(RFC4122 variant),
573
- * 与 `UUIDV7_RE` 的两处约束一一对应。**不含时间戳**(v7 通常前 48 位是毫秒时钟)——本 id 的用途是
574
- * 「同一条消息恒定映射到同一行」,时间序会破坏这一点;形门只校形,不校时钟。
575
- */
576
299
  function durableIdempotencyTaskId(scope, messageId) {
577
300
  const digest = createHash("sha256").update(scope).update("\x1f").update(messageId).digest();
578
301
  const b = Buffer.from(digest.subarray(0, 16));
@@ -581,14 +304,6 @@ function durableIdempotencyTaskId(scope, messageId) {
581
304
  const hex = b.toString("hex");
582
305
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
583
306
  }
584
- /**
585
- * `tasks/get` —— 读回一条 task。
586
- *
587
- * 🔴 反枚举:**不存在**与**不属于你**走同一条出路({@link A2A_TASK_NOT_FOUND_MESSAGE},同 code 同文,
588
- * 逐字节相同)。这是 A-041 不变式在 JSON-RPC 侧的执行点 —— 一个外部 peer 不该能靠错误文案的差别枚举
589
- * 本部署里别人的 task id。principal 门在**查库之前**求值(与 `GET /v1/runs/:id` 同序),免得 401 与
590
- * not-found 的先后本身成为存在性 oracle;那道门在分派器入口(见本文件的域入口②)。
591
- */
592
307
  async function handleTasksGet({ req, ctx, params }) {
593
308
  const { deps } = ctx;
594
309
  const id = asNonEmptyString(params.id, "params.id", MAX_TASK_ID_CHARS);
@@ -597,30 +312,14 @@ async function handleTasksGet({ req, ctx, params }) {
597
312
  const run = await deps.runStore.getRun(id);
598
313
  if (!run)
599
314
  throw new RpcError(A2A_TASK_NOT_FOUND, A2A_TASK_NOT_FOUND_MESSAGE);
600
- // owner 判据与 `runOwnerOk` 同源:比对**验证过的** principal(gatedPrincipal),绝不比 principal 头
601
- // (直连门上那是可伪造的 —— 伪一个头就能读别人的 task)。`owner === null` = 无主部署的行,任何已过
602
- // 服务凭据门的调用方可读(与那条 HTTP 读面逐字同判)。
603
315
  if (run.owner !== null && gatedPrincipal(req, deps.config) !== run.owner)
604
316
  throw new RpcError(A2A_TASK_NOT_FOUND, A2A_TASK_NOT_FOUND_MESSAGE);
605
317
  return taskFromRun(run, isStaleRun(run, deps.config.runStaleSec));
606
318
  }
607
- /** 演化槽 §4-4 —— **方法表**。新方法 = 加一行;表外的一切恒 -32601(协议内置的能力协商)。 */
608
319
  const A2A_METHODS = {
609
320
  "message/send": handleMessageSend,
610
321
  "tasks/get": handleTasksGet,
611
322
  };
612
- /**
613
- * 域入口②:JSON-RPC 端点。**位置即契约** —— 挂在全局 service-credential 门**之后**(R-11「持服务凭据
614
- * 才可调」),且在 drain / model-roster-pending 两道 503 门之后(`/v1/a2a` 进 `isBillableSubmitPath`:
615
- * `message/send` 会烧模型)。
616
- *
617
- * ⚠️ 代价成文:端点是**单 URL** 的(卡上只能公告一个 `url`),所以排空期 `tasks/get` 也一并 503。
618
- * 方向取「宁紧」—— 让一台正在下线的副本开新模型工作,正是那几道门存在的全部理由;而读面的补偿是
619
- * 现成的:行是 durable 的,调用方按 A2A 的传输层重试语义换一台副本读同一条行即可。
620
- *
621
- * 命名(`handleA2a`,与 `handleA2aServe` 同族):见域入口①的命名注 —— 两个名字都取成
622
- * `capabilities.a2aServe` 的子串,好让 `route-shape-roster` 层E 如实认出它们。
623
- */
624
323
  export async function handleA2a(req, res, url, ctx) {
625
324
  const miss = { fell: false };
626
325
  await handleA2aRpcBody(req, res, url, ctx, miss);
@@ -634,14 +333,11 @@ async function handleA2aRpcBody(req, res, url, ctx, miss) {
634
333
  }
635
334
  const serve = deps.config.a2aServe;
636
335
  if (!serve) {
637
- // OFF ⇒ 与卡同形的 404(整条路由对外不存在),不是「功能已禁用」的具名错误。
638
336
  sendError(res, 404, "not_found.route", "not found");
639
337
  return;
640
338
  }
641
- // 版本槽:**读**请求头只为诊断,认识与否都照常答;响应恒带自己的版本(开集容忍)。
642
339
  const requestedVersion = req.headers[A2A_VERSION_HEADER];
643
340
  res.setHeader(A2A_VERSION_HEADER, A2A_PROTOCOL_VERSION);
644
- // principal 门在**任何查库之前**(与 runs 面同序:401 与 not-found 的先后不得成为存在性 oracle)。
645
341
  if (deps.config.requirePrincipal && !gatedPrincipal(req, deps.config)) {
646
342
  deps.metrics?.inc("a2a_serve_rejected_total", { reason: "principal_required" });
647
343
  sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
@@ -669,8 +365,6 @@ async function handleA2aRpcBody(req, res, url, ctx, miss) {
669
365
  return;
670
366
  }
671
367
  const method = rpc.method;
672
- // 方法槽:表里没有 ⇒ 恒 -32601。这是协议**自带**的能力协商 —— 调用方据此降级(例如 message/stream
673
- // 拿到 -32601 就转 message/send + tasks/get 轮询),所以我们不自铸一张能力表。
674
368
  const handler = A2A_METHODS[method];
675
369
  if (!handler) {
676
370
  deps.metrics?.inc("a2a_serve_rpc_total", { method: "(unknown)", outcome: "method_not_found" });
@@ -685,12 +379,10 @@ async function handleA2aRpcBody(req, res, url, ctx, miss) {
685
379
  sendRpc(res, { jsonrpc: "2.0", id, error: { code: JSONRPC_INVALID_PARAMS, message: "params must be an object (positional params are not supported)" } });
686
380
  return;
687
381
  }
688
- // 版本头进日志(诊断维),**不**进任何判据;限长防超长头把日志行撑爆(车1 自审第四镜同族)。
689
382
  deps.logger?.debug?.("a2a_serve_rpc", { method, ...(typeof requestedVersion === "string" ? { requestedVersion: requestedVersion.slice(0, 32) } : {}) });
690
383
  try {
691
384
  const result = await handler({ req, res, ctx, params: params, serve });
692
385
  if (result === undefined) {
693
- // 处理器已自行应答(提交链的 400/409 走 HTTP 形 —— 那是**传输层**的拒绝,不是一次成功的 RPC)。
694
386
  deps.metrics?.inc("a2a_serve_rpc_total", { method, outcome: "refused" });
695
387
  return;
696
388
  }
@@ -704,17 +396,13 @@ async function handleA2aRpcBody(req, res, url, ctx, miss) {
704
396
  sendRpc(res, { jsonrpc: "2.0", id, error: { code: e.code, message: e.message } });
705
397
  return;
706
398
  }
707
- // 非具名异常:回一条**不含内部细节**的 -32603(栈/消息只进日志,不上线)。
708
399
  deps.logger?.error?.("a2a_serve_rpc_failed", { method, error: e?.message });
709
400
  deps.metrics?.inc("a2a_serve_rpc_total", { method, outcome: "internal_error" });
710
401
  sendRpc(res, { jsonrpc: "2.0", id, error: { code: JSONRPC_INTERNAL_ERROR, message: "internal error" } });
711
402
  }
712
403
  }
713
- /** JSON-RPC 响应体。**HTTP 恒 200**(错误在 body 的 `error` 成员里)—— 这是 JSON-RPC over HTTP 的常规:
714
- * 把 RPC 级错误映射成 4xx 会让传输层的重试逻辑把「参数写错了」当成「网络抖了」反复重发。 */
715
404
  function sendRpc(res, body) {
716
405
  sendJson(res, 200, body);
717
406
  }
718
- /** 九词闭集的对外可见锚(测试用它证明我们只发闭集词;也让「加一个自造词」在这里就被看见)。 */
719
407
  export const A2A_SERVE_TASK_STATES = A2A_TASK_STATES;
720
408
  //# sourceMappingURL=a2a-serve.js.map
@@ -26,8 +26,6 @@ async function handleAdminConfigRefreshBody(req, res, url, ctx, miss) {
26
26
  const trigger = deps.configRefreshNow;
27
27
  const outcome = trigger ? await trigger() : "not-wired";
28
28
  if (outcome === "failed") {
29
- // 🔴 codex R2-F3:定时拍吞掉异常是为了保住循环,但**按下刷新的那个人**必须知道这一拍真的失败了
30
- // ——「中心超时」与「已刷新」在回执上不可分辨,正是这个口存在的反面。502:失败在依赖侧(配置源)。
31
29
  sendError(res, 502, "internal.config_refresh_failed", "the config refresh tick failed (source unreachable / unreadable / apply error) — the live config is unchanged; see the worker's sema_registry_refresh_failed log line for the cause");
32
30
  return;
33
31
  }
@@ -35,8 +33,6 @@ async function handleAdminConfigRefreshBody(req, res, url, ctx, miss) {
35
33
  sendError(res, 409, "config.refresh_unavailable", "this deployment has no config pipeline to refresh — set CONFIG_CENTER (remote control plane) or CONFIG_PROVIDER=local (config.d on disk); an env-only worker has nothing to pull");
36
34
  return;
37
35
  }
38
- // 世代号取自世代账(与 /health 的 configTargetVersion/configAppliedVersion 同一份读数)。账缺席只发生在
39
- // 「触发口在场但世代账没接线」的夹具形 —— 诚实报 0(不编一个看起来像真答案的数)。
40
36
  const gen = deps.configApplyState?.();
41
37
  sendJson(res, 200, { triggered: true, targetVersion: gen?.configTargetVersion ?? 0, appliedVersion: gen?.configAppliedVersion ?? 0 });
42
38
  }