@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
package/dist/question.js CHANGED
@@ -1,56 +1,14 @@
1
- /**
2
- * §4④ tool-panorama — AskUserQuestion LIVE-stream HITL, the SERVICE side of core's `RunnerDeps.onQuestion`
3
- * seam (core `ask-question.ts`, design/64 §5.2). The SIBLING of {@link import("./elicitation.js").ElicitationCoordinator}:
4
- * elicitation is an inbound-MCP-server ask; this is the AGENT's own `AskUserQuestion` tool (a structured multiple-choice
5
- * question to the end user). core EMITS the call and hands it to `onQuestion`; the deployment routes it to a live human
6
- * over the run's stream and returns their {@link QuestionAnswer}. The durable-disconnected-human case is ALREADY wired
7
- * separately (`createDurableQuestionPolicy` + `QUESTION_AWAITS_RESUME` on the durable leg — the question SUSPENDS and a
8
- * resume carries the answer); this is the LIVE-only, same-replica, `steer()`-class half.
9
- *
10
- * The service is pure WIRE glue (the abstraction is core's): forward the questions to whoever tails the run's stream (a
11
- * `question` SSE frame), park a promise, resolve it from `POST /v1/questions/:id/respond`. 🔒 SECURITY: the ANSWER fence
12
- * is CORE's — core's AskUserQuestion tool echoes a `selected` value ONLY if it is one of the offered option labels
13
- * (`selected ⊆ options`) and wraps the free-text `note` in an untrusted-DATA fence (design/80 D-F), so a wire answer can
14
- * never be read as instructions. The service therefore does NOT re-fence the answer; it validates only the outer SHAPE
15
- * (the HTTP boundary) and REDACTS secrets from the model-authored QUESTION text before it is persisted/streamed (the
16
- * same redact-at-write contract as elicitation's `message`/tool outputs — the question is shown to a human, never
17
- * re-fed to a model, so no `delimitUntrusted` is needed on it). Deployment responsibilities core delegates: THROTTLE
18
- * (bound concurrency/total/ttl per run — an over-eager agent could flood the human).
19
- *
20
- * 🔴 VERDICT vs LANDING are separate jobs (#166 裁1). This coordinator is the deployment's HONEST reporter of one fact:
21
- * "no human answered this". Every arm with no real human answer — no run context, the run aborted, the throttle window
22
- * is spent, the question cannot be presented, the TTL expired (incl. the run-exit sweep) — returns core's
23
- * `{kind:"unavailable"}`. It NEVER synthesizes an empty answer set: an empty `{answers:[]}` reads to core as "a human
24
- * was here and picked nothing", which on a durable deployment consumes the pending approval and hands the model a
25
- * fabricated human verdict instead of parking the question where an operator can still answer it. Choosing the landing
26
- * is CORE's job: a durable leg parks a checkpoint (operator answers later via decide), a non-durable leg continues with
27
- * the `declined_unavailable` synthetic-continuation card (the model is told nobody is reachable and proceeds on its own
28
- * judgment — a run with no human still never hangs). Only a REAL human answer returns a {@link QuestionAnswer}.
29
- *
30
- * ROUTING mirrors elicitation: `onQuestion` lives on the SINGLETON runner's `RunnerDeps` (the request carries no taskId),
31
- * so the run leg establishes a per-run AsyncLocalStorage context (taskId throttle key, owner respond-gate, `emit`
32
- * closure). `node:async_hooks` propagates it through every await; no context ⇒ unavailable (never a hang).
33
- */
34
1
  import { AsyncLocalStorage } from "node:async_hooks";
35
2
  import { uuidv7 } from "@sema-agent/core";
36
3
  import { redactDeep } from "./trace/redact.js";
37
4
  import { recordFailOpen } from "./observability/fail-open.js";
38
- /** Size bound on the model-authored questions payload before it is persisted to the durable log + streamed (parity
39
- * with elicitation's MAX_ELICIT_* — an over-large questions blob amplifies into the socket/log). Over the cap ⇒ the
40
- * questions are dropped and the ask reports unavailable (a question the human can't see can't be answered). */
41
5
  const MAX_QUESTIONS_BYTES = 16384;
42
6
  export const DEFAULT_QUESTION_THROTTLE = {
43
7
  maxConcurrentPerRun: 2,
44
8
  maxTotalPerRun: 20,
45
9
  ttlMs: 5 * 60_000,
46
10
  };
47
- /** The honest "nobody answered this" outcome. Frozen + shared: it is pure data core reads through
48
- * `classifyQuestionOutcome`, and it must never be confused with an empty ANSWER (which claims a human chose nothing
49
- * and, on a durable leg, spends the approval that park would have preserved). */
50
11
  const UNAVAILABLE = Object.freeze({ kind: "unavailable" });
51
- /** Validate the respond body = core's {@link QuestionAnswer}. `selected`/`note` are OPAQUE passthrough (core fences
52
- * them: `selected ⊆ options`, `note` untrusted-fenced) — we validate only the outer SHAPE (answers[] of {header,
53
- * selected:string[], note?}), never the option semantics. */
54
12
  export function parseQuestionResponse(body) {
55
13
  if (body === null || typeof body !== "object" || Array.isArray(body))
56
14
  return { ok: false, error: "body must be an object" };
@@ -73,9 +31,6 @@ export function parseQuestionResponse(body) {
73
31
  }
74
32
  return { ok: true, value: { answers: out } };
75
33
  }
76
- /** redactDeep + size-bound the UNTRUSTED model-authored questions before they are persisted/streamed (a question's
77
- * free-text — question/header/option label+description — can echo a secret from the model's context). Over the byte
78
- * cap OR unserializable ⇒ undefined (drop; the ask reports unavailable). */
79
34
  function boundQuestions(questions) {
80
35
  const redacted = redactDeep(questions);
81
36
  try {
@@ -87,12 +42,6 @@ function boundQuestions(questions) {
87
42
  }
88
43
  return redacted;
89
44
  }
90
- /**
91
- * Coordinates AskUserQuestion HITL for the singleton runner. Process-local + same-replica (the pending map is in memory,
92
- * like ElicitationCoordinator / steerableRuns): a respond that lands on another replica finds nothing → 404. Present
93
- * (passed into `RunnerDeps.onQuestion` + the respond route) ONLY when `ASK_QUESTION_ENABLED` — absent ⇒ core mounts the
94
- * AskUserQuestion tool with no seam at all (`seam_absent` continuation; the model just can't get a live answer).
95
- */
96
45
  export class QuestionCoordinator {
97
46
  als = new AsyncLocalStorage();
98
47
  pending = new Map();
@@ -101,8 +50,6 @@ export class QuestionCoordinator {
101
50
  constructor(throttle = DEFAULT_QUESTION_THROTTLE) {
102
51
  this.throttle = throttle;
103
52
  }
104
- /** Run `fn` with the per-run question context ambient. On exit, release any still-pending question for this run (a
105
- * live-only question cannot outlive its leg → unavailable) and drop the run's counters (no leak). */
106
53
  runWithContext(ctx, fn) {
107
54
  return this.als.run(ctx, async () => {
108
55
  try {
@@ -117,30 +64,20 @@ export class QuestionCoordinator {
117
64
  }
118
65
  });
119
66
  }
120
- /** `RunnerDeps.onQuestion`. core calls this when the agent's AskUserQuestion tool fires; the resolved outcome is what
121
- * core acts on — a {@link QuestionAnswer} ONLY when a human really answered, otherwise `{kind:"unavailable"}` (core
122
- * then parks on a durable leg / continues with the `declined_unavailable` card on a non-durable one; either way the
123
- * run never hangs). Arrow property so it can be passed as `onQuestion: coordinator.question` with `this` bound. */
124
67
  question = async (req, signal) => {
125
68
  const ctx = this.als.getStore();
126
- // No run context (a leg not wrapped by runWithContext — synchronous /v1/tasks, verify/cascade, or a leader sub-task
127
- // with no live stream) ⇒ nobody to ask. Sub-tasks run in the parent's ALS frame → a child's question inherits the
128
- // top-level run's ctx (one human, one stream, one throttle budget), same as elicitation.
129
69
  if (!ctx)
130
70
  return UNAVAILABLE;
131
- // Aborted (call signal or the run's own): whatever comes back has no consumer, so claiming a human answered is a lie.
132
71
  if (signal?.aborted || ctx.abortSignal?.aborted)
133
72
  return UNAVAILABLE;
134
- // THROTTLE (service's job — core imposes none): bound concurrency + total. Breach ⇒ unavailable, never a denial and
135
- // never a fabricated answer — a spent window is a "not now", which is exactly what park/continue is for.
136
73
  const rc = this.countersFor(ctx.taskId);
137
74
  if (rc.inflight >= this.throttle.maxConcurrentPerRun || rc.total >= this.throttle.maxTotalPerRun)
138
75
  return UNAVAILABLE;
139
76
  const questions = boundQuestions(req.questions);
140
77
  if (!questions)
141
- return UNAVAILABLE; // over-cap / unserializable → can't show it → nobody can answer it
78
+ return UNAVAILABLE;
142
79
  const id = uuidv7();
143
- rc.inflight += 1; // reserve the concurrency slot; total charged only on a successful emit
80
+ rc.inflight += 1;
144
81
  let done = false;
145
82
  let timer;
146
83
  let resolveAnswer;
@@ -155,33 +92,17 @@ export class QuestionCoordinator {
155
92
  ctx.abortSignal?.removeEventListener("abort", onAbort);
156
93
  this.pending.delete(id);
157
94
  rc.inflight = Math.max(0, rc.inflight - 1);
158
- // stash the outcome for the completion frame emitted below (after the await returns)
159
95
  lastOutcome = outcome;
160
96
  resolveAnswer(a);
161
97
  };
162
98
  let lastOutcome = "unanswered";
163
99
  const onAbort = () => settle(UNAVAILABLE, "unanswered");
164
- // TTL: the human walked away. Release as unavailable — core decides whether that parks (durable, operator can still
165
- // answer) or continues (non-durable); holding core past the window is not one of the choices.
166
100
  timer = setTimeout(() => settle(UNAVAILABLE, "unanswered"), this.throttle.ttlMs);
167
101
  timer.unref?.();
168
102
  signal?.addEventListener("abort", onAbort, { once: true });
169
103
  ctx.abortSignal?.addEventListener("abort", onAbort, { once: true });
170
- // Register BEFORE emitting so a (fast) respond can never miss the entry.
171
104
  this.pending.set(id, { settle, owner: ctx.owner, taskId: ctx.taskId });
172
- // Emit the OPEN frame AWAITED so its durable append is ORDERED ahead of the answer/completion/terminal appends (a
173
- // fire-and-forget append can commit out of seq order and be skipped by the monotonic events-tail cursor). A FAILED
174
- // emit settles unavailable (nobody can answer a frame that never arrived) and must NOT burn the per-run total.
175
- // 🔴 RACED against the settlement (codex 复审 R2-high): a STALLED delivery (a durable append that never settles, a
176
- // half-open socket) is a third shape next to ok/threw, and a bare `await ctx.emit` on it outlives TTL *and* abort —
177
- // settle would resolve `answerP` while this frame is still parked on the emit, so `question()` never returns, core's
178
- // callTool never unblocks and the leg cannot even reach its own cleanup. "The window always releases" has to hold
179
- // for the stalled shape too, so the await ends at whichever comes first. Ordering is unchanged on the normal path
180
- // (the settlement is still pending there, so the emit is still fully awaited).
181
- // The loser of the race keeps its own handlers — an emit that rejects AFTER the race must not surface as an
182
- // unhandled rejection.
183
105
  const emitted = (async () => {
184
- // [4851]③ 窗三键与上面 setTimeout 用同一个 ttlMs 铸(单源;壳倒计时永不与真释放口径分歧)。
185
106
  const now = Date.now();
186
107
  return ctx.emit({ type: "question", questionId: id, questions, serverNowMs: now, expiresInMs: this.throttle.ttlMs, expiresAtMs: now + this.throttle.ttlMs });
187
108
  })()
@@ -191,23 +112,10 @@ export class QuestionCoordinator {
191
112
  rc.total += 1;
192
113
  }
193
114
  else if (delivery === "failed") {
194
- // #157(codex 复审 MEDIUM,2026-08-07):投递失败 ⇒ 人在环这道门被**一次故障**跳过,模型自己往下判。
195
- // 「绝不把 run 挂死」是刻意的产品姿态,所以方向不改;但它是 P-DEBT 不是合法兜底——债要在遥测里显形。
196
- // #166 后本臂结算成 unavailable(而非空答):留痕语义不变(一次投递故障仍可能让人在环被跳过——非
197
- // durable 形 core 合成续跑),durable 形则改由 core park,债的方向与本记账口径一致。
198
115
  recordFailOpen("server.question.open-frame-undelivered", `question=${id}`);
199
116
  settle(UNAVAILABLE, "unanswered");
200
117
  }
201
- // delivery==="stalled":窗已由 TTL/abort/清扫结算,这一问就此让位;账在下面那个迟到臂里补(投递还没
202
- // 判出成败,现在既不能计费也不能记债)。
203
118
  const answer = await answerP;
204
- // Completion breadcrumb (dialog dismiss) — FIRE-AND-FORGET so a slow durable append can never delay returning the
205
- // answer to core. Best-effort (the shell already has the answer via respond), but CHAINED BEHIND the open frame and
206
- // BRANCHED on how that frame ended (codex 复审 R3):
207
- // · 送达(含**迟到**送达)⇒ 人真看见了这张卡:补烧总量额度(否则「反复悬挂再落地」是一条绕过刷屏帽的
208
- // 路子),再发消卡帧把它消解掉;
209
- // · 投递失败(含**迟到**失败)⇒ 没有卡可消解,发消卡帧只会在流里留一条无主帧;此时补记那条 P-DEBT
210
- // 留痕(同步臂已经记过的不会重复:那条腿的 delivery 不是 stalled)。
211
119
  void emitted
212
120
  .then((result) => {
213
121
  if (result === "failed") {
@@ -219,17 +127,10 @@ export class QuestionCoordinator {
219
127
  rc.total += 1;
220
128
  return ctx.emit({ type: "question_complete", questionId: id, outcome: lastOutcome, serverNowMs: Date.now() });
221
129
  })
222
- // #157:F 类兜底(丢的是收尾渲染,不是答案)——合法,但必须留痕,否则"面包屑成片消失"只能靠
223
- // 用户报"对话框不消解"发现。
224
130
  .catch(() => recordFailOpen("server.question.complete-breadcrumb-dropped", `question=${id}`));
225
131
  return answer;
226
132
  };
227
- /** `POST /v1/questions/:id/respond` — resolve a parked question with the shell's answer. Owner-gated with a 404 (no
228
- * existence oracle): a non-owner AND an unknown id (answered / expired / wrong replica) both get 404. Returns the
229
- * HTTP {status, body}; the HTTP layer owns auth (gatedPrincipal + REQUIRE_PRINCIPAL) before calling. */
230
133
  respond(id, principal, body) {
231
- // Validate the body BEFORE the lookup (steer/elicit parity): a bad body is the caller's error regardless of run
232
- // state, so 400 is existence-independent (no 400-vs-404 existence oracle).
233
134
  const parsed = parseQuestionResponse(body);
234
135
  if (!parsed.ok)
235
136
  return { status: 400, body: { error: parsed.error, errorCode: "request.body_shape" } };
@@ -240,19 +141,9 @@ export class QuestionCoordinator {
240
141
  entry.settle(parsed.value, "answered");
241
142
  return { status: 200, body: { questionId: id, delivery: "applied" } };
242
143
  }
243
- /** Test/observability hook: number of currently-parked questions. */
244
144
  pendingCount() {
245
145
  return this.pending.size;
246
146
  }
247
- /** #152 ([2703] 案二):durable 部署上的 AskUserQuestion 判决探针——本调用点是否处在某条**投递得到人的**
248
- * 活流腿上(runWithContext 包裹的 bg/SSE 腿=true;sync /v1/tasks、verify/cascade、durable resume 驱动=false)。
249
- * resolve-spec 的 durable question policy 用它在**判决时**分腿:有活流 ⇒ allow(问活人),无 ⇒
250
- * ask(durable park)。ALS 让这个判断天然 per-leg,policy 组装期不必预知腿别。
251
- * 🔴 判据是「投递面此刻可达」而不只是「ALS 在场」(复审 A1):detach 腿断连后 run 仍在本作用域里跑,
252
- * 只判 ALS 会把一条谁也收不到的问题判成 allow——挂满 ttl 后以 unavailable 结算,非 durable 形 core
253
- * 就此合成续跑,活人再没机会答;而 park 才是它该走的腿。
254
- * 谓词缺席 ⇒ 恒可达(后台腿的 durable events tail 语义)。谓词本身抛错按不可达处理:判决面
255
- * fail-closed 到 park(park 可由运维补答,续跑掉的问题不可回收)。 */
256
147
  hasLiveContext() {
257
148
  const ctx = this.als.getStore();
258
149
  if (ctx === undefined)
@@ -1,21 +1,3 @@
1
- /**
2
- * design/80 seam #2 — the resource/preempt durable-suspend opt-in decision (design/74 "失败≠没做完" third state).
3
- *
4
- * Pure so it is unit-testable in isolation (the spec is otherwise assembled inside main.ts's bootstrap). When a
5
- * task opts in, a budget/turns/walltime limit OR a scheduler preempt durably SUSPENDS the task (resumable via
6
- * `POST /v1/assistant/tasks/:id/resume`) instead of FAILING; absent ⇒ a limit still FAILS.
7
- *
8
- * 🔴 EXCLUDE verify/cascade tasks. Core runs their implementation via `runWithVerification`/`runCascade`, which
9
- * map an inner durable suspend to a FAILED result (`errorCode: "unexpected.suspended"`,内层 status
10
- * "needs_review" 时铸 `"unexpected.needs_review"` —— 真源是 core `agents/suspend-guard.js` 的
11
- * `mapNestedSuspend`;`suspended.*` 三码是 bg 子代/委派报告面的词,不落在这条 TaskResult 腿上). The service would then
12
- * `setTerminal(failed)` while core had already minted a pending `resource_limit` checkpoint + suspended the
13
- * remote VM — ORPHANING both (the run row is terminal, so nothing resumes them). So a resource-limit hit on a
14
- * verify/cascade task must FAIL CLEANLY (no suspend), exactly as before the flag. This exclusion is the SAME
15
- * boundary the `preemptSignal` wiring enforces in runs.ts (stream-path only), so eligibility and the signal stay
16
- * aligned. (Adversarial review wtyc7ist3 HIGH finding: without this, enabling RESOURCE_SUSPEND leaks a suspended
17
- * VM + checkpoint on every verify/cascade task that hits a resource limit.)
18
- */
19
1
  export function resourceSuspendOptIn(opts) {
20
2
  if (!opts.enabled || opts.isVerify || opts.isCascade)
21
3
  return undefined;
@@ -1,51 +1,13 @@
1
- /**
2
- * The A value router — cash out the S1 supervisor value verdict (sema-internal server/docs/S1-VALUE-VERDICT.md §6.3) into a per-task
3
- * orchestration decision: run a task as `single` / `supervisor` / `team`.
4
- *
5
- * The verdict, in one line: **competent tasks have SUP/TEAM dominated (pure overhead) ⇒ default single; the real
6
- * supervisor value is (a) DANGER backstop (SUP prevention gate-deny > TEAM fan-out which AMPLIFIES danger ×K) and
7
- * (b) STRUCTURAL view-gap (goal-keeping / ask-aggregation, universally useful).** So this router is CONSERVATIVE
8
- * by construction:
9
- * - default **single** (escalation is never free — the verdict measured it as pure overhead on competent work);
10
- * - escalate to **supervisor** ONLY on a positive (a)-danger or (b)-structural signal;
11
- * - NEVER auto-select **team** (fan-out is dominated everywhere tested AND amplifies danger; team stays
12
- * caller-explicit — a deliberate `council`/`debate`/`team` request, or the `/v1/leader` endpoint).
13
- *
14
- * 🔴 ALL signals are DETERMINISTIC (config / mounted tools / exec-env isolation / autonomy / delegation). There is
15
- * NO LLM "difficulty" judge in the routing decision — the verdict's measurement-integrity rule (design/89 §2.2.1):
16
- * an LLM judge is a GATE inside an arm, NEVER the routing metric (it would make the router un-auditable + gameable).
17
- *
18
- * Pure + unit-testable. The decision is VALIDATED empirically against the S1 harness (a ROUTER arm that runs the
19
- * router's chosen arm per trap; the objective hidden-test oracle confirms danger→SUP prevents + competent→single
20
- * is not-worse) BEFORE it is wired into the live task path — never ship an unvalidated router.
21
- */
22
1
  import {} from "@sema-agent/core";
23
- /**
24
- * The routing decision (pure). Order matters: explicit caller choice first, then (a) danger, then (b) structural,
25
- * else the single default. `team` is NEVER produced here — it only ever appears via {@link RoutingSignals.explicitMode}.
26
- */
27
2
  export function routeOrchestration(s) {
28
- // 1. Caller-explicit wins — a deliberate council/debate/team request (or the /v1/leader endpoint) is intent.
29
3
  if (s.explicitMode)
30
4
  return { mode: s.explicitMode, reason: `explicit:${s.explicitMode}` };
31
- // 2. (a) DANGER → supervisor prevention. The verdict's strongest value: gate-deny PREVENTS (SUP) beats
32
- // fan-out which AMPLIFIES the danger ×K (TEAM) and even beats single's after-the-fact exposure.
33
5
  if (s.dangerousCapability)
34
6
  return { mode: "supervisor", reason: "danger:prevention-gate" };
35
- // 3. (b) STRUCTURAL → supervisor. Goal-keeping (worker local view vs supervisor global blueprint) +
36
- // cross-subtask ask-aggregation — universally useful when the task delegates / runs long.
37
7
  if (s.needsGoalKeeping)
38
8
  return { mode: "supervisor", reason: "structural:goal-keeping" };
39
- // 4. Default SINGLE — competent tasks: SUP/TEAM measured as pure overhead (dominated). Escalation is not free.
40
9
  return { mode: "single", reason: "default:single" };
41
10
  }
42
- /**
43
- * Derive the routing signals from a task's resolved context — DETERMINISTIC. The danger rule is CONSERVATIVE and
44
- * fail-safe: a `read-only` task cannot harm (no escalation); EGRESS / irreversible-external capability is danger
45
- * regardless of sandboxing (it escapes the sandbox); plain write/exec is danger ONLY on a non-isolated env (an
46
- * isolated sandbox contains it + is torn down). The exact threshold is the one tunable knob — validated
47
- * empirically against the S1 harness, not guessed.
48
- */
49
11
  export function deriveRoutingSignals(ctx) {
50
12
  const readOnly = ctx.autonomy === "read-only";
51
13
  const dangerousCapability = !readOnly && (ctx.canEgress || (ctx.canWriteOrExec && !ctx.isolatedExecEnv));
@@ -55,41 +17,18 @@ export function deriveRoutingSignals(ctx) {
55
17
  needsGoalKeeping: ctx.delegatedOrLongHorizon,
56
18
  };
57
19
  }
58
- /**
59
- * Service-facing convenience: map a /v1/tasks task's DETERMINISTIC facts → a routing decision (the service signal
60
- * derivation + {@link deriveRoutingSignals} + {@link routeOrchestration} in one tested call; main.ts's resolveSpec
61
- * calls this). Signal mapping:
62
- * - `isolatedExecEnv` ⟺ a sandboxed remote-exec backend (`e2b`/`k8s` Kata) — `ssh`/`host`/`adb` are NOT isolated.
63
- * - hand tools (write_file/bash) are mounted only when an executionEnv exists AND hands aren't read-only/plan.
64
- * - `canEgress` is left false in v1 (per-tool egress detection deferred; danger derives from non-isolation).
65
- * - `delegatedOrLongHorizon` is false on the single-task path (the (b) value lives on the /v1/leader path).
66
- */
67
20
  export function routeServiceTask(input) {
68
21
  const readOnlyHands = input.autonomy === "read-only" || input.autonomy === "plan";
69
22
  return routeOrchestration(deriveRoutingSignals({
70
23
  ...(input.explicitTeam ? { explicitMode: "team" } : {}),
71
24
  ...(input.autonomy ? { autonomy: input.autonomy } : {}),
72
25
  canWriteOrExec: input.hasExecutionEnv && !readOnlyHands,
73
- // 🔴 v1 EXPLICITLY-ACCEPTED LIMITATION (review MEDIUM): `canEgress` is false here, so an isolated sandbox
74
- // with outbound network (e2b defaults internet ON) is NOT auto-routed to SUP for egress. Deriving danger
75
- // from network-PRESENCE would over-escalate (a routine `npm install` task needs network) — defeating the
76
- // verdict's default-single. The real egress danger is IRREVERSIBLE EXTERNAL actions (git push to a real
77
- // repo, a deploy), which are TOOL-specific, not network-presence, and not cleanly detectable here. When a
78
- // per-tool egress/irreversibility signal is wired (k8s P3 egress + tool marks), set this from it.
79
26
  canEgress: false,
80
27
  isolatedExecEnv: input.isolatedExecEnv,
81
28
  delegatedOrLongHorizon: false,
82
29
  }));
83
30
  }
84
- /** VM-isolation runtime classes — a REAL sandbox boundary (separate kernel). Plain `runc` / `""` / unknown share
85
- * the host kernel ⇒ NOT isolated. */
86
31
  const VM_ISOLATION_CLASSES = new Set(["kata", "kata-qemu", "kata-clh", "kata-fc", "gvisor", "runsc"]);
87
- /**
88
- * Resolve whether a task's execution env STRONGLY isolates side effects (a VM/kernel boundary the sandbox tears
89
- * down). FAIL-CLOSED (review HIGH): a `k8s` backend is isolated ONLY with a VM-isolation `runtimeClass`
90
- * (kata-x or gvisor) — a runc downgrade (`runtimeClass: ""` / `runc` / unknown) is NOT isolated (shared kernel,
91
- * container escape). `e2b` is a Firecracker microVM (isolated). `host`/`ssh`/`adb` and anything unknown ⇒ NOT isolated.
92
- */
93
32
  export function isIsolatedExecEnv(provider, runtimeClass) {
94
33
  if (provider === "e2b")
95
34
  return true;
@@ -99,23 +38,7 @@ export function isIsolatedExecEnv(provider, runtimeClass) {
99
38
  }
100
39
  return false;
101
40
  }
102
- /** The write-capable hand tools the SUP posture gates DIRECTLY (Bash is gated via `shellGate`). Q6 (core 1.161):
103
- * CC-aligned tool names (was edit_file/write_file). NotebookEdit (core 1.163) is the same fs-mutation class — it
104
- * edits a `.ipynb` on the real fs, so it MUST be gated too, else a SUP-routed (capability-danger) task could mutate
105
- * a notebook ungated (the exact "write tool bypasses the SUP gate" hole the review-HIGH closed for Edit/Write).
106
- * Curated, NOT derived from HAND_TOOL_EFFECTS: that taxonomy is replay-safety (Write="idempotent"; Bash="write" but
107
- * shell-gated), not "is an fs mutation". CANONICAL names only (core 1.202 retired MultiEdit into an Edit alias —
108
- * (5.0.0:折叠面退役——重放的旧名调用由 core roster 层响亮 miss,不再折到 "Edit";raw 直比)
109
- * gated without a stale alias entry here). */
110
41
  const WRITE_HAND_TOOLS = new Set(["Edit", "Write", "NotebookEdit"]);
111
- /**
112
- * The TaskSpec overrides for the SUP prevention posture (mode=supervisor). Applied via `tightenTaskSpec`
113
- * (tighten-only — composes with the deployment baseline, never loosens):
114
- * - `shellGate: "always"` — gate every `Bash` as an irreversible_ask (the verdict's gate-deny on the shell).
115
- * - `toolPolicy` — ALSO ask on the direct write hand tools `Edit`/`Write` (review HIGH: shellGate alone
116
- * leaves a non-isolated SUP task able to mutate the real fs via write tools ungated). The worker's existing
117
- * approval machinery (durable suspend / poll) enforces the `ask` — same precondition as `shellGate:"always"`.
118
- */
119
42
  export function supPostureOverrides() {
120
43
  const ask = (toolName) => ({ action: "ask", message: `${toolName} gated by supervisor routing (capability-danger task)`, decisionReason: "rule" });
121
44
  const writeGate = {
@@ -107,7 +107,26 @@ export type RuleImportRedeemed = {
107
107
  detail: string;
108
108
  retryable?: true;
109
109
  };
110
- /** 卡道兑付的结果。 */
110
+ /**
111
+ * #340([4763])—— 一次卡道兑付**要兑的是什么**:两个显式臂,永不折叠。
112
+ *
113
+ * 🔴 判别式联合而不是「一个文本 + 一个布尔」:两个臂的文本**含义不同**——候选臂的串是「卡上第几条」的
114
+ * 定位键(对不上 ⇒ `rule_not_offered`,语义一个字不变),编辑臂的串是**人自己写的规则**(由 core 的
115
+ * 卡编辑面校验 + 覆盖门判)。同一个字段两种含义会让下一个读者(和下一次改动)分不清哪条路上该做哪道门。
116
+ */
117
+ export type CardRuleRedemption =
118
+ /** 人**点了卡上的某条候选**:文本用来在引擎铸的候选表里定位下标。 */
119
+ {
120
+ kind: "offered";
121
+ ruleText: string;
122
+ }
123
+ /** 人在卡上**手改了规则文本**(core 的 card-edit 面:同一条已鉴权确认通道,`editedCandidate`)。 */
124
+ | {
125
+ kind: "edited";
126
+ text: string;
127
+ };
128
+ /** 卡道兑付的结果。`canonical` = **真正落盘**的那条规则文本(编辑臂上它可能与人敲的原字节不同 ——
129
+ * core 会把 `Bash(adb *)` 规范成 `Bash(adb:*)`;界面要回显的是这一份,不是输入框里的那一份)。 */
111
130
  export type CardRulePersisted = {
112
131
  ok: true;
113
132
  rule: string;
@@ -115,7 +134,25 @@ export type CardRulePersisted = {
115
134
  alreadyRedeemed: boolean;
116
135
  } | {
117
136
  ok: false;
118
- reason: "no-candidates" | "unknown-candidate" | "confirm-refused" | "redeem-refused";
137
+ reason: "no-candidates" | "unknown-candidate" | "confirm-refused" | "redeem-refused"
138
+ /** #340:编辑面**在这只卡上用不了**——卡没有绑定锚(`boundInputHash` 缺席 ⇒ 无从证明「我编辑的
139
+ * 是显示了这条命令的那张卡」),或这台部署没有打开编辑面。与下面那个词刻意分家:这个说的是
140
+ * 「别再渲那个输入框」,那个说的是「你写的这条不行,改一下再来」——壳要做的事完全相反。 */
141
+ | "edit-unsupported"
142
+ /**
143
+ * #340:人写的文本**被引擎的门拒了**——共享校验器不认这个拼写,或它不覆盖本次被裁决的命令
144
+ * (「编辑可以更宽,但不许换成另一条授权」)。
145
+ *
146
+ * 🔴 **这一格不是「重试就好」**(codex 对抗复审 [medium],验真后按实况改词):兑付发生在裁决
147
+ * **落定之后**(那条顺序是硬的 —— 一次没有生效的裁决不该留下一条永久规则),所以拿到这个词时
148
+ * 这张卡已经消费掉了,同一个 `approvalId` 再回决是 404。人的「允许这次」照常生效,只是这一次
149
+ * 没能顺手存下规则;改好的文本要落,得等**下一次同命令的 ask**(与 `rule_input_edited` 逐字
150
+ * 同一条处置)。壳的措辞据此写,别渲成「点这里重试」。
151
+ * 想做到「同一张卡上改了再来」需要一次**裁决之前**的预检,而那要求 core 把编辑面的校验(含它自己
152
+ * 的拼写规范化)导出成一只纯函数 —— 在本层照抄一个更严的校验器会把 `Bash(adb *)` 这条**成因形**
153
+ * 当场误拒。已登记为上游请托,不在本层用第二个解析器凑。
154
+ */
155
+ | "edit-rejected";
119
156
  detail: string;
120
157
  };
121
158
  /** 一层用户交上来的 settings(HTTP 载荷已 zod 校验过的形)。 */
@@ -197,11 +234,13 @@ export interface RuleConsentLane {
197
234
  persistCardRule(input: {
198
235
  principal: string;
199
236
  toolName: string;
200
- /** 被裁决的命令**原字节**(与 ask 上的那份同源;规则文本由引擎从它铸,调用方不供文本)。 */
237
+ /** 被裁决的命令**原字节**(与 ask 上的那份同源;候选臂的规则文本由引擎从它铸,编辑臂的覆盖门也拿它判)。 */
201
238
  command: string;
202
- /** 客户端选中的候选**文本**——用来在引擎铸出的候选表里定位下标。文本对不上 拒(不猜)。 */
203
- ruleText: string;
239
+ /** 这次要兑的是候选表里的哪一条,还是人手改的自由文本(两个显式臂,见 {@link CardRuleRedemption})。 */
240
+ redemption: CardRuleRedemption;
204
241
  toolCallId?: string;
242
+ /** 卡上的实参摘要。编辑臂的**必要条件**:core 要求编辑回显它(「我编辑的是显示了这条命令的那张卡」),
243
+ * 缺席 ⇒ 编辑臂 fail-closed 拒(候选臂不受影响,那条路上它只是记账用)。 */
205
244
  boundInputHash?: string;
206
245
  scope?: RuleScope;
207
246
  }): Promise<CardRulePersisted>;