@sema-agent/server 7.132.0 → 7.134.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 (473) hide show
  1. package/MIGRATION.md +353 -0
  2. package/README.md +8 -9
  3. package/README.zh-CN.md +7 -6
  4. package/USAGE.md +301 -64
  5. package/deploy/sema-up/README.md +35 -1
  6. package/deploy/sema-up/chart/Chart.yaml +1 -1
  7. package/deploy/sema-up/chart/templates/NOTES.txt +15 -0
  8. package/deploy/sema-up/chart/templates/_helpers.tpl +129 -0
  9. package/deploy/sema-up/chart/templates/sealed-key.yaml +24 -0
  10. package/deploy/sema-up/chart/templates/server-v4pro.yaml +19 -2
  11. package/deploy/sema-up/chart/templates/server.yaml +19 -2
  12. package/deploy/sema-up/chart/values-full-single.yaml +5 -1
  13. package/deploy/sema-up/chart/values.yaml +34 -0
  14. package/deploy/sema-up/stack/compose.yaml +3 -0
  15. package/deploy/sema-up/stack/sema-stack-cluster.yaml +4 -2
  16. package/dist/adoption/plan.d.ts +3 -2
  17. package/dist/adoption/plan.js +7 -5
  18. package/dist/adoption/runner.js +3 -10
  19. package/dist/adoption/sql.d.ts +3 -2
  20. package/dist/adoption/sql.js +3 -5
  21. package/dist/adoption/wire.d.ts +4 -1
  22. package/dist/adoption/wire.js +1 -0
  23. package/dist/approval-ask-audit-store.d.ts +32 -14
  24. package/dist/approval-ask-audit-store.js +272 -74
  25. package/dist/approval-ask-machine.d.ts +15 -0
  26. package/dist/approval-ask-machine.js +6 -0
  27. package/dist/approval-ask-retention.d.ts +136 -0
  28. package/dist/approval-ask-retention.js +102 -0
  29. package/dist/approval-card.d.ts +83 -23
  30. package/dist/approval-card.js +45 -15
  31. package/dist/approval-content-kind.d.ts +5 -2
  32. package/dist/approval-content-kind.js +16 -3
  33. package/dist/approval-deny-reasons.d.ts +1 -1
  34. package/dist/approval-gate-currency.d.ts +72 -0
  35. package/dist/approval-gate-currency.js +94 -0
  36. package/dist/approval.d.ts +15 -0
  37. package/dist/approval.js +55 -17
  38. package/dist/audit.d.ts +27 -10
  39. package/dist/audit.js +12 -16
  40. package/dist/bake-runner/main.js +3 -2
  41. package/dist/boot/coordinators.d.ts +6 -1
  42. package/dist/boot/coordinators.js +6 -3
  43. package/dist/boot/deferred-sandbox-path-env.js +1 -1
  44. package/dist/boot/execution-env.js +9 -7
  45. package/dist/boot/execution-lane.js +11 -0
  46. package/dist/boot/leader.d.ts +2 -0
  47. package/dist/boot/leader.js +4 -1
  48. package/dist/boot/memory-boundary.js +2 -2
  49. package/dist/boot/memory-consolidation.d.ts +5 -4
  50. package/dist/boot/memory-consolidation.js +7 -6
  51. package/dist/boot/org-delivery.d.ts +28 -0
  52. package/dist/boot/org-delivery.js +20 -0
  53. package/dist/boot/read-deny-anchor-audit.js +2 -2
  54. package/dist/boot/reapers.d.ts +3 -0
  55. package/dist/boot/reapers.js +21 -1
  56. package/dist/boot/resolve-spec.d.ts +3 -0
  57. package/dist/boot/resolve-spec.js +60 -81
  58. package/dist/boot/runner-deps.d.ts +18 -5
  59. package/dist/boot/runner-deps.js +10 -0
  60. package/dist/boot/session-faces.d.ts +1 -0
  61. package/dist/boot/session-faces.js +4 -2
  62. package/dist/boot/stage-02-web-admin-pull.js +4 -1
  63. package/dist/boot/stage-06-runners.d.ts +6 -3
  64. package/dist/boot/stage-06-runners.js +20 -4
  65. package/dist/boot/stage-07-capability-layer.d.ts +4 -1
  66. package/dist/boot/stage-07-capability-layer.js +24 -3
  67. package/dist/boot/stage-08-reapers.d.ts +4 -1
  68. package/dist/boot/stage-08-reapers.js +2 -2
  69. package/dist/boot/stage-09-leader.d.ts +5 -1
  70. package/dist/boot/stage-09-leader.js +2 -2
  71. package/dist/boot/stage-10-http-server.d.ts +5 -1
  72. package/dist/boot/stage-10-http-server.js +6 -6
  73. package/dist/boot/stores.d.ts +8 -0
  74. package/dist/boot/stores.js +10 -14
  75. package/dist/boot/web-admin.d.ts +10 -0
  76. package/dist/boot/web-admin.js +162 -65
  77. package/dist/boot/workflow-orchestration.d.ts +2 -0
  78. package/dist/boot/workflow-orchestration.js +2 -1
  79. package/dist/brain.js +8 -3
  80. package/dist/breaker-key.d.ts +6 -0
  81. package/dist/breaker-key.js +29 -0
  82. package/dist/budget.js +5 -1
  83. package/dist/caller-text-echo.d.ts +11 -6
  84. package/dist/caller-text-echo.js +10 -12
  85. package/dist/canonical-json.d.ts +2 -41
  86. package/dist/canonical-json.js +2 -13
  87. package/dist/capabilities/center-plugins.d.ts +7 -0
  88. package/dist/capabilities/center-plugins.js +1 -1
  89. package/dist/capabilities/composite-members.d.ts +1 -1
  90. package/dist/capabilities/composite-members.js +1 -1
  91. package/dist/capabilities/execution-lane.js +2 -2
  92. package/dist/capabilities/hands-lane.d.ts +21 -0
  93. package/dist/capabilities/hands-lane.js +10 -0
  94. package/dist/capabilities/mcp-probe.d.ts +36 -4
  95. package/dist/capabilities/mcp-probe.js +29 -6
  96. package/dist/capabilities/scenarios.d.ts +63 -7
  97. package/dist/capabilities/scenarios.js +39 -7
  98. package/dist/capability-facts.d.ts +36 -4
  99. package/dist/capability-facts.js +30 -4
  100. package/dist/center-config-tier.d.ts +69 -0
  101. package/dist/center-config-tier.js +42 -0
  102. package/dist/checkpoint-ctx-seal-m28.d.ts +42 -0
  103. package/dist/checkpoint-ctx-seal-m28.js +19 -0
  104. package/dist/checkpoint-identity.d.ts +16 -0
  105. package/dist/checkpoint-identity.js +19 -0
  106. package/dist/config-catalog.d.ts +17 -10
  107. package/dist/config-catalog.js +43 -28
  108. package/dist/config-d-migration.d.ts +3 -1
  109. package/dist/config-d-migration.js +2 -2
  110. package/dist/config-dir-trust.d.ts +114 -0
  111. package/dist/config-dir-trust.js +388 -0
  112. package/dist/config-invariants.d.ts +18 -5
  113. package/dist/config-invariants.js +35 -3
  114. package/dist/config-provider.d.ts +6 -1
  115. package/dist/config-provider.js +6 -2
  116. package/dist/config-types.d.ts +71 -14
  117. package/dist/config-types.js +8 -0
  118. package/dist/config.d.ts +13 -0
  119. package/dist/config.js +121 -59
  120. package/dist/delegated-session-owner.d.ts +185 -0
  121. package/dist/delegated-session-owner.js +119 -0
  122. package/dist/device-protocol.d.ts +5 -3
  123. package/dist/device-protocol.js +1 -1
  124. package/dist/device-ws-hub.js +21 -2
  125. package/dist/device-ws-protocol.d.ts +70 -6
  126. package/dist/device-ws-protocol.js +50 -23
  127. package/dist/elicitation.js +3 -2
  128. package/dist/entry-problems.d.ts +34 -0
  129. package/dist/entry-problems.js +14 -0
  130. package/dist/execution-lane-caps.d.ts +49 -10
  131. package/dist/execution-lane-caps.js +43 -4
  132. package/dist/field-conflict.d.ts +44 -0
  133. package/dist/field-conflict.js +21 -0
  134. package/dist/file-history-disclosure.d.ts +34 -5
  135. package/dist/file-history-disclosure.js +2 -2
  136. package/dist/fleet/fleet-bus.d.ts +84 -6
  137. package/dist/fleet/fleet-bus.js +42 -9
  138. package/dist/hooks/classic-stations.d.ts +10 -5
  139. package/dist/hooks/classic-stations.js +4 -4
  140. package/dist/hooks/command-spawn.d.ts +57 -0
  141. package/dist/hooks/command-spawn.js +139 -0
  142. package/dist/hooks/hook-llm.js +2 -2
  143. package/dist/hooks/hook-runner.d.ts +127 -25
  144. package/dist/hooks/hook-runner.js +668 -287
  145. package/dist/hooks/hook-source.d.ts +44 -0
  146. package/dist/hooks/hook-source.js +40 -0
  147. package/dist/http/active-run-conflict.d.ts +5 -13
  148. package/dist/http/active-run-conflict.js +17 -27
  149. package/dist/http/admission.js +111 -60
  150. package/dist/http/approval-sweeps.js +58 -19
  151. package/dist/http/caller-trust.d.ts +77 -0
  152. package/dist/http/caller-trust.js +143 -0
  153. package/dist/http/checkpoint-ctx.d.ts +173 -18
  154. package/dist/http/checkpoint-ctx.js +171 -12
  155. package/dist/http/claimed-run.d.ts +4 -3
  156. package/dist/http/device-placement.d.ts +2 -4
  157. package/dist/http/device-placement.js +2 -1
  158. package/dist/http/dispatch.js +13 -7
  159. package/dist/http/durable-run-leg.d.ts +4 -32
  160. package/dist/http/durable-run-leg.js +37 -12
  161. package/dist/http/idempotency.d.ts +2 -2
  162. package/dist/http/idempotency.js +2 -2
  163. package/dist/http/owner-access.d.ts +56 -0
  164. package/dist/http/owner-access.js +42 -0
  165. package/dist/http/principal-gate.d.ts +78 -27
  166. package/dist/http/principal-gate.js +64 -14
  167. package/dist/http/readiness.d.ts +11 -5
  168. package/dist/http/readiness.js +10 -3
  169. package/dist/http/refusal-body.d.ts +12 -0
  170. package/dist/http/refusal-body.js +11 -0
  171. package/dist/http/request-guards.d.ts +16 -32
  172. package/dist/http/request-guards.js +31 -30
  173. package/dist/http/resume-ctx-read.d.ts +50 -6
  174. package/dist/http/resume-ctx-read.js +33 -7
  175. package/dist/http/resume-legs.d.ts +5 -5
  176. package/dist/http/resume-legs.js +163 -91
  177. package/dist/http/retired-service-deps.d.ts +43 -0
  178. package/dist/http/retired-service-deps.js +18 -0
  179. package/dist/http/route-ctx.d.ts +44 -37
  180. package/dist/http/route-ctx.js +4 -0
  181. package/dist/http/route-table.js +2 -0
  182. package/dist/http/routes/a2a-serve.js +3 -4
  183. package/dist/http/routes/admin-config-refresh.js +1 -1
  184. package/dist/http/routes/admin-drain.js +1 -1
  185. package/dist/http/routes/admin-memory-optout.js +5 -1
  186. package/dist/http/routes/adoption.js +2 -9
  187. package/dist/http/routes/agents-roster.js +2 -3
  188. package/dist/http/routes/approvals-assistant.js +91 -56
  189. package/dist/http/routes/background.js +3 -2
  190. package/dist/http/routes/capabilities.js +20 -4
  191. package/dist/http/routes/config-catalog.d.ts +3 -3
  192. package/dist/http/routes/config-catalog.js +1 -1
  193. package/dist/http/routes/devices.js +3 -17
  194. package/dist/http/routes/diagnostics.d.ts +3 -3
  195. package/dist/http/routes/diagnostics.js +6 -2
  196. package/dist/http/routes/fleet.js +4 -4
  197. package/dist/http/routes/images.js +6 -7
  198. package/dist/http/routes/leader.js +1 -8
  199. package/dist/http/routes/mcp-reconnect.d.ts +1 -1
  200. package/dist/http/routes/mcp-reconnect.js +3 -2
  201. package/dist/http/routes/memory-bundle.js +1 -1
  202. package/dist/http/routes/memory-compliance.js +1 -1
  203. package/dist/http/routes/memory-consolidation.js +1 -1
  204. package/dist/http/routes/memory-origin.d.ts +1 -1
  205. package/dist/http/routes/memory-origin.js +1 -1
  206. package/dist/http/routes/memory-policy.d.ts +1 -1
  207. package/dist/http/routes/memory-policy.js +3 -2
  208. package/dist/http/routes/notify-wake.js +8 -21
  209. package/dist/http/routes/observability.js +2 -2
  210. package/dist/http/routes/principal-inbox.d.ts +39 -0
  211. package/dist/http/routes/principal-inbox.js +76 -0
  212. package/dist/http/routes/retention-ops.js +2 -9
  213. package/dist/http/routes/rules.js +9 -22
  214. package/dist/http/routes/runs.d.ts +5 -0
  215. package/dist/http/routes/runs.js +63 -115
  216. package/dist/http/routes/session-sync.d.ts +3 -2
  217. package/dist/http/routes/session-sync.js +53 -48
  218. package/dist/http/routes/sessions-list.d.ts +1 -1
  219. package/dist/http/routes/sessions-list.js +3 -4
  220. package/dist/http/routes/sessions.js +33 -43
  221. package/dist/http/routes/side-query.js +6 -4
  222. package/dist/http/routes/tasks.js +71 -30
  223. package/dist/http/routes/trace-usage.js +7 -9
  224. package/dist/http/routes/workflows.d.ts +351 -0
  225. package/dist/http/routes/workflows.js +153 -119
  226. package/dist/http/run-declared-families.d.ts +16 -0
  227. package/dist/http/run-declared-families.js +12 -0
  228. package/dist/http/send.d.ts +24 -3
  229. package/dist/http/send.js +14 -2
  230. package/dist/http/server.d.ts +23 -18
  231. package/dist/http/server.js +20 -15
  232. package/dist/http/stored-body-replay.js +17 -4
  233. package/dist/http/submit-ctx.d.ts +4 -3
  234. package/dist/http/submit-ctx.js +23 -6
  235. package/dist/http/submit-replay.d.ts +4 -3
  236. package/dist/http/submit-replay.js +5 -4
  237. package/dist/http/wire-types.d.ts +17 -10
  238. package/dist/index.d.ts +3 -1
  239. package/dist/index.js +1 -1
  240. package/dist/lane-home-probe.d.ts +78 -9
  241. package/dist/lane-home-probe.js +78 -12
  242. package/dist/leader/endpoint.d.ts +4 -1
  243. package/dist/leader/endpoint.js +11 -10
  244. package/dist/leader/wire.d.ts +4 -0
  245. package/dist/leader/wire.js +8 -7
  246. package/dist/limit-refusal.d.ts +17 -5
  247. package/dist/limit-refusal.js +4 -1
  248. package/dist/main.js +1 -1
  249. package/dist/mcp-assertion-header.d.ts +31 -10
  250. package/dist/mcp-assertion-header.js +10 -3
  251. package/dist/memory-origin-fence.js +2 -2
  252. package/dist/memory-project-layer.d.ts +37 -0
  253. package/dist/memory-project-layer.js +46 -0
  254. package/dist/memory-scope.d.ts +23 -16
  255. package/dist/memory-scope.js +24 -8
  256. package/dist/memory-sync-client.d.ts +27 -13
  257. package/dist/memory-sync-client.js +107 -18
  258. package/dist/model-compat.d.ts +1 -1
  259. package/dist/model-entry-refusal.d.ts +9 -1
  260. package/dist/model-entry-refusal.js +6 -1
  261. package/dist/model-plane-face.d.ts +51 -0
  262. package/dist/model-plane-face.js +24 -0
  263. package/dist/model-provider.d.ts +5 -0
  264. package/dist/model-provider.js +4 -2
  265. package/dist/model-route-endpoint.d.ts +8 -11
  266. package/dist/model-route-endpoint.js +4 -7
  267. package/dist/model-route-env.d.ts +22 -0
  268. package/dist/model-route-env.js +2 -0
  269. package/dist/model-route-gap.d.ts +60 -30
  270. package/dist/model-route-gap.js +21 -15
  271. package/dist/observability/corrupt-read-seat.d.ts +7 -0
  272. package/dist/observability/corrupt-read-seat.js +9 -0
  273. package/dist/observability/err-text.d.ts +6 -0
  274. package/dist/observability/err-text.js +18 -4
  275. package/dist/observability/fail-open.d.ts +35 -9
  276. package/dist/observability/fail-open.js +35 -9
  277. package/dist/observability/metrics.js +1 -0
  278. package/dist/observability/prompt-manifest.d.ts +22 -25
  279. package/dist/observability/prompt-manifest.js +54 -50
  280. package/dist/observability/tool-trace.js +9 -10
  281. package/dist/operator-ask.d.ts +10 -0
  282. package/dist/operator-ask.js +3 -0
  283. package/dist/orchestration/workflow-agent-session-index.d.ts +12 -12
  284. package/dist/orchestration/workflow-agent-session-index.js +9 -13
  285. package/dist/orchestration/workflow-completion-inbox.d.ts +42 -15
  286. package/dist/orchestration/workflow-completion-inbox.js +13 -0
  287. package/dist/orchestration/workflow-notify-journal.d.ts +13 -32
  288. package/dist/orchestration/workflow-notify-journal.js +27 -34
  289. package/dist/orchestration/workflow-park-decision.d.ts +89 -0
  290. package/dist/orchestration/workflow-park-decision.js +119 -0
  291. package/dist/org-delivery-gate.d.ts +81 -0
  292. package/dist/org-delivery-gate.js +59 -0
  293. package/dist/panel-tool-names.d.ts +23 -0
  294. package/dist/panel-tool-names.js +8 -0
  295. package/dist/parked-decide.d.ts +43 -11
  296. package/dist/parked-decide.js +100 -51
  297. package/dist/parked-revive-surface.d.ts +9 -2
  298. package/dist/parked-revive-surface.js +11 -8
  299. package/dist/permission-rule-list.d.ts +1 -1
  300. package/dist/permission-rule-list.js +2 -2
  301. package/dist/permission-rule-vocab.d.ts +1 -23
  302. package/dist/permission-rule-vocab.js +0 -13
  303. package/dist/plugins/approval-ask-store-file.d.ts +16 -1
  304. package/dist/plugins/approval-ask-store-file.js +21 -0
  305. package/dist/plugins/approval-ask-store-memory.d.ts +18 -1
  306. package/dist/plugins/approval-ask-store-memory.js +34 -2
  307. package/dist/plugins/approval-ask-store-sql.d.ts +25 -0
  308. package/dist/plugins/approval-ask-store-sql.js +37 -1
  309. package/dist/plugins/approval-payload-fingerprint.d.ts +14 -6
  310. package/dist/plugins/approval-payload-fingerprint.js +1 -1
  311. package/dist/plugins/blob-backend.d.ts +38 -25
  312. package/dist/plugins/blob-backend.js +29 -19
  313. package/dist/plugins/breaker-state-sql.d.ts +2 -2
  314. package/dist/plugins/breaker-state-sql.js +4 -4
  315. package/dist/plugins/caching-session-store.d.ts +4 -1
  316. package/dist/plugins/caching-session-store.js +7 -3
  317. package/dist/plugins/checkpoint-store-sql.d.ts +36 -1
  318. package/dist/plugins/checkpoint-store-sql.js +65 -3
  319. package/dist/plugins/file-history-store-sql.d.ts +5 -2
  320. package/dist/plugins/file-history-store-sql.js +3 -1
  321. package/dist/plugins/file-run-store.d.ts +5 -13
  322. package/dist/plugins/file-run-store.js +9 -0
  323. package/dist/plugins/fork-routing-session-store.d.ts +9 -1
  324. package/dist/plugins/fork-routing-session-store.js +12 -1
  325. package/dist/plugins/host-platform.d.ts +2 -0
  326. package/dist/plugins/host-platform.js +2 -1
  327. package/dist/plugins/local-checkpoint-store.d.ts +15 -2
  328. package/dist/plugins/local-checkpoint-store.js +32 -1
  329. package/dist/plugins/local-session-store.d.ts +13 -10
  330. package/dist/plugins/local-session-store.js +23 -14
  331. package/dist/plugins/local-session-window.d.ts +45 -0
  332. package/dist/plugins/local-session-window.js +38 -0
  333. package/dist/plugins/mailbox-recipient-lifecycle.d.ts +3 -2
  334. package/dist/plugins/mailbox-store-sql.d.ts +30 -48
  335. package/dist/plugins/mailbox-store-sql.js +77 -52
  336. package/dist/plugins/memory-embedder-fingerprint.js +2 -1
  337. package/dist/plugins/memory-run-store.d.ts +5 -13
  338. package/dist/plugins/memory-run-store.js +8 -0
  339. package/dist/plugins/one-time-migrations.d.ts +121 -5
  340. package/dist/plugins/one-time-migrations.js +332 -8
  341. package/dist/plugins/org-delivery-quota-store.d.ts +123 -0
  342. package/dist/plugins/org-delivery-quota-store.js +228 -0
  343. package/dist/plugins/permission-rule-store-file.js +10 -6
  344. package/dist/plugins/permission-rule-store-sql.js +8 -8
  345. package/dist/plugins/pg-pool.js +39 -7
  346. package/dist/plugins/pg-session-storage.d.ts +18 -5
  347. package/dist/plugins/pg-session-storage.js +68 -23
  348. package/dist/plugins/remote-env-adb.d.ts +6 -12
  349. package/dist/plugins/remote-env-adb.js +12 -2
  350. package/dist/plugins/remote-env-device.d.ts +14 -7
  351. package/dist/plugins/remote-env-device.js +23 -15
  352. package/dist/plugins/remote-env-k8s.d.ts +8 -12
  353. package/dist/plugins/remote-env-k8s.js +23 -5
  354. package/dist/plugins/remote-env-local-docker.d.ts +10 -15
  355. package/dist/plugins/remote-env-local-docker.js +12 -9
  356. package/dist/plugins/remote-env-ssh.d.ts +10 -16
  357. package/dist/plugins/remote-env-ssh.js +17 -11
  358. package/dist/plugins/run-store-sql.d.ts +16 -14
  359. package/dist/plugins/run-store-sql.js +22 -2
  360. package/dist/plugins/schema-indexes.js +2 -0
  361. package/dist/plugins/send-user-file.js +2 -2
  362. package/dist/plugins/session-epoch-anchor.d.ts +32 -6
  363. package/dist/plugins/session-epoch-anchor.js +2 -3
  364. package/dist/plugins/session-ownership.d.ts +11 -0
  365. package/dist/plugins/session-ownership.js +11 -0
  366. package/dist/plugins/session-placement.d.ts +12 -14
  367. package/dist/plugins/session-placement.js +2 -15
  368. package/dist/plugins/session-store.d.ts +18 -8
  369. package/dist/plugins/session-store.js +15 -4
  370. package/dist/plugins/sql-driver.d.ts +74 -11
  371. package/dist/plugins/sql-driver.js +210 -12
  372. package/dist/plugins/sql-errors.d.ts +23 -0
  373. package/dist/plugins/sql-errors.js +21 -0
  374. package/dist/plugins/store-backend.d.ts +36 -1
  375. package/dist/plugins/store-backend.js +40 -14
  376. package/dist/plugins/store-contracts.d.ts +42 -0
  377. package/dist/plugins/store-contracts.js +1 -0
  378. package/dist/plugins/tidb-pool.d.ts +6 -1
  379. package/dist/plugins/tidb-pool.js +44 -10
  380. package/dist/plugins/tidb-session-storage.d.ts +3 -2
  381. package/dist/plugins/tidb-session-storage.js +2 -2
  382. package/dist/plugins/tidb-session-store.d.ts +25 -5
  383. package/dist/plugins/tidb-session-store.js +67 -21
  384. package/dist/plugins/workflow-journal-store-sql.d.ts +73 -19
  385. package/dist/plugins/workflow-journal-store-sql.js +19 -6
  386. package/dist/plugins/workflow-run-store-sql.d.ts +30 -8
  387. package/dist/plugins/workflow-run-store-sql.js +50 -9
  388. package/dist/posture-source.d.ts +18 -10
  389. package/dist/posture-source.js +4 -1
  390. package/dist/project-identity.d.ts +45 -6
  391. package/dist/project-identity.js +162 -8
  392. package/dist/project-layer-key.d.ts +28 -0
  393. package/dist/project-layer-key.js +29 -0
  394. package/dist/question.js +3 -2
  395. package/dist/read-deny-entry.d.ts +25 -0
  396. package/dist/read-deny-entry.js +12 -0
  397. package/dist/read-face-posture.d.ts +37 -11
  398. package/dist/read-face-posture.js +32 -3
  399. package/dist/request-key-closure.d.ts +7 -0
  400. package/dist/request-key-closure.js +9 -1
  401. package/dist/request-tool-axes.d.ts +12 -0
  402. package/dist/request-tool-axes.js +20 -0
  403. package/dist/request-value-closure.d.ts +5 -0
  404. package/dist/request-value-closure.js +11 -3
  405. package/dist/rule-entry-list.d.ts +26 -2
  406. package/dist/rule-entry-list.js +72 -12
  407. package/dist/rules-consent.js +3 -4
  408. package/dist/run-acceptance.d.ts +19 -3
  409. package/dist/run-acceptance.js +13 -1
  410. package/dist/run-local.js +37 -6
  411. package/dist/runs.d.ts +12 -10
  412. package/dist/runs.js +25 -6
  413. package/dist/runtime-caps-resolver.d.ts +16 -6
  414. package/dist/runtime-caps-resolver.js +18 -5
  415. package/dist/sandbox-pkg-source.d.ts +19 -1
  416. package/dist/sandbox-pkg-source.js +29 -7
  417. package/dist/sealed-key.d.ts +32 -0
  418. package/dist/sealed-key.js +55 -1
  419. package/dist/security.d.ts +95 -15
  420. package/dist/security.js +52 -9
  421. package/dist/session-error-wire.d.ts +31 -7
  422. package/dist/session-error-wire.js +9 -2
  423. package/dist/session-sync-kernel.d.ts +37 -10
  424. package/dist/session-sync-kernel.js +10 -0
  425. package/dist/session-titled-frame.d.ts +33 -0
  426. package/dist/session-titled-frame.js +4 -0
  427. package/dist/session-titler.d.ts +9 -1
  428. package/dist/session-titler.js +15 -4
  429. package/dist/spec-fields.js +21 -2
  430. package/dist/sql-engine-posture.d.ts +3 -1
  431. package/dist/task-a2a.js +4 -3
  432. package/dist/task-cwd.d.ts +1 -1
  433. package/dist/task-mcp.d.ts +5 -5
  434. package/dist/task-mcp.js +27 -74
  435. package/dist/task-settings.d.ts +93 -88
  436. package/dist/task-settings.js +71 -63
  437. package/dist/text-bidi.d.ts +0 -4
  438. package/dist/text-bidi.js +1 -3
  439. package/dist/tighten-only-seats.d.ts +150 -0
  440. package/dist/tighten-only-seats.js +196 -0
  441. package/dist/tool-approval.d.ts +451 -136
  442. package/dist/tool-approval.js +733 -256
  443. package/dist/tool-rule-names.d.ts +6 -0
  444. package/dist/trace/core-keyset-guard.d.ts +108 -25
  445. package/dist/trace/exit-seat.d.ts +40 -0
  446. package/dist/trace/exit-seat.js +130 -0
  447. package/dist/trace/ledger-events.d.ts +6 -3
  448. package/dist/trace/project.d.ts +98 -13
  449. package/dist/trace/project.js +57 -32
  450. package/dist/trace/projection-table.d.ts +100 -0
  451. package/dist/trace/projection-table.js +130 -0
  452. package/dist/trace/redact.d.ts +57 -0
  453. package/dist/trace/redact.js +962 -168
  454. package/dist/trace/self-minted-wire.d.ts +349 -1
  455. package/dist/trace/self-minted-wire.js +118 -1
  456. package/dist/trace/wire-projection-faces.d.ts +304 -2
  457. package/dist/trace/wire-projection-faces.js +160 -13
  458. package/dist/url-refusal.js +2 -0
  459. package/dist/web-admin-client/apply-effective.d.ts +12 -0
  460. package/dist/web-admin-client/apply-effective.js +56 -5
  461. package/dist/web-admin-client/facade.d.ts +1 -1
  462. package/dist/web-admin-client/facade.js +1 -1
  463. package/dist/web-admin-client/read-face.js +5 -9
  464. package/dist/web-admin-client/skills-mcp.d.ts +1 -1
  465. package/dist/web-admin-client/skills-mcp.js +6 -8
  466. package/dist/web-admin-client/types.d.ts +1 -1
  467. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +57 -9
  468. package/package.json +8 -4
  469. package/scripts/drain-check-local.mjs +34 -2
  470. package/scripts/drain-check.mysql.sql +20 -2
  471. package/scripts/drain-check.pg.sql +20 -2
  472. package/dist/mcp-tool-faces.d.ts +0 -69
  473. package/dist/mcp-tool-faces.js +0 -106
@@ -4,12 +4,15 @@ import { AsyncLocalStorage } from "node:async_hooks";
4
4
  import { uuidv7, classifyAsk, foldArbitrationRow, policyCannotAnswer, screenAskClassMembers, MAX_RULE_TEXT_CHARS, READ_ROOT_CANDIDATE_DIR_MAX, parseRuleText, pathConfinableWriteToolNames } from "@sema-agent/core";
5
5
  import { precheckCardRuleText } from "./rules-consent.js";
6
6
  import { redactDeep, redactSecrets, redactSecretsWithArms } from "./trace/redact.js";
7
+ import { seatText } from "./trace/exit-seat.js";
7
8
  import { hasBidiControls } from "./text-bidi.js";
8
9
  import { approvalPayloadFingerprint } from "./plugins/approval-payload-fingerprint.js";
9
10
  import { createLogger } from "./observability/logger.js";
11
+ import { errorKindOf } from "./observability/err-text.js";
10
12
  import { recordFailOpen } from "./observability/fail-open.js";
11
- import { deriveAskId, deriveAskScopedBatchId, deriveBatchId, MAX_DECISION_NOTE_CHARS } from "./approval-ask-machine.js";
12
- import { currentParkArm, MAX_RULE_OFFERS, buildApprovalCard, buildApprovalCardEnvelope, buildApprovalRequestFrame, buildRevokeFrame, readDenialLimitFallback, readProbeCause, readProbeReason, readReadRootCandidate, readStoredCardEnvelope, readRuleEvidence, readAllowRuleShadowed, readRuleOffersAbsence, readRuleOffersAbsenceSeat, readParkRowTrueSeat, readAskClassPair, readClosedWordSeat, RuleOfferRawSchema, backfillLegacyBatchMemberKind, screenUncoveredDetailSeat, } from "./approval-card.js";
13
+ import { ownerOfCheckpointScope } from "./security.js";
14
+ import { deriveAskId, deriveAskScopedBatchId, deriveBatchId, MAX_DECISION_NOTE_CHARS, noteLandedOnRow, noteRecordedReceipt } from "./approval-ask-machine.js";
15
+ import { currentParkArm, MAX_RULE_OFFERS, buildApprovalCard, buildApprovalCardEnvelope, buildApprovalRequestFrame, buildReplayFrame, buildRevokeFrame, readDenialLimitFallback, readProbeCause, readProbeReason, readReadRootCandidate, readStoredCardEnvelope, readRuleEvidence, readAllowRuleShadowed, readRuleOffersAbsence, readRuleOffersAbsenceSeat, readParkRowTrueSeat, readAskClassPair, readClosedWordSeat, buildApprovalCompleteFrame, RuleOfferRawSchema, backfillLegacyBatchMemberKind, screenUncoveredDetailSeat, } from "./approval-card.js";
13
16
  import { POLICY_REFUSAL_DECISION, projectAskTerminalClaimTruth } from "./plugins/approval-ask-store-sql.js";
14
17
  import { APPROVAL_ENDING_CAUSES, SETTLE_OUTCOME, causeOfAskRow } from "./approval-deny-reasons.js";
15
18
  import { settleByPolicy } from "./approval-policy-settlement.js";
@@ -18,6 +21,7 @@ import { governanceOriginOf } from "./runtime-governance.js";
18
21
  import { callGrantBindingOf, questionGrantBindingOf } from "./standing-answer-grant.js";
19
22
  import { operatorAsk } from "./operator-ask.js";
20
23
  import { APPROVAL_EXEMPTED_EVENT } from "./plugins/approval-exemption-store.js";
24
+ import { refusalBody } from "./http/refusal-body.js";
21
25
  const defaultLogger = createLogger();
22
26
  export const APPROVAL_GATE_KINDS = ["human", "irreversible_ask"];
23
27
  export function isApprovalGateKind(k) {
@@ -32,15 +36,17 @@ const DEFAULT_APPROVAL_TTL_MS = 5 * 60_000;
32
36
  const DEFAULT_UNREACHED_ASK_TTL_MS = 60 * 60_000;
33
37
  const SUSPENDED_POLL_MAX_PER_TICK = 64;
34
38
  const SUSPENDED_POLL_BUDGET_MS = 5_000;
39
+ const ADOPT_OPEN_ASKS_MAX = 1_000;
35
40
  const EDITED_DECISION_MARK_CAP_MS = 10_000;
36
41
  const DEFAULT_WINDOW_MARGIN_MS = 10_000;
37
42
  const SHUTDOWN_DRAIN_POLL_MS = 50;
38
43
  const MAX_ALLOW_SESSIONS = 4096;
39
44
  const DEFAULT_ADMIT_MAX_PER_TASK = 32;
40
45
  const DEFAULT_ADMIT_MAX_PER_OWNER = 256;
41
- const DEFAULT_PARK_TOMBSTONE_MAX = 2000;
42
- const DEFAULT_PARK_TOMBSTONE_TTL_MS = 60 * 60_000;
43
- const DEFAULT_PARK_TOMBSTONE_PER_PRINCIPAL_MAX = 128;
46
+ const DEFAULT_PARK_REDEMPTION_MAX = 2000;
47
+ const DEFAULT_PARK_REDEMPTION_TTL_MS = 60 * 60_000;
48
+ const DEFAULT_PARK_REDEMPTION_PER_PRINCIPAL_MAX = 128;
49
+ const PARK_REDEMPTION_ADMISSION_RETRY_AFTER_SEC = 5;
44
50
  const PARKING_RETRY_AFTER_MS = 250;
45
51
  export function approvalDecidePath(sessionId) {
46
52
  return `/v1/approvals/${encodeURIComponent(sessionId)}/decide`;
@@ -63,6 +69,22 @@ function durableRuleMaterial(material) {
63
69
  return { ruleCommand: material.command, ruleScopeRoot: material.scopeRoot ?? null };
64
70
  }
65
71
  export const NOT_PENDING_UNKNOWN_CAUSE = "unknown_or_other_replica";
72
+ export const TOOL_APPROVAL_READ_STATES = ["pending", "decided", "parked", "void"];
73
+ function decidedRowFacts(row) {
74
+ if (row.state !== "DECIDED" || (row.decision !== "approve" && row.decision !== "deny") || row.provisional || row.decidedAtMs === null)
75
+ return undefined;
76
+ return { decision: row.decision === "approve" ? "allow" : "deny", decidedAtMs: row.decidedAtMs, ...(row.decisionNote !== null ? { decisionNote: row.decisionNote } : {}) };
77
+ }
78
+ function buildAskRowReadProjection(row, nowMs) {
79
+ const frame = buildReplayFrame(row, nowMs);
80
+ return {
81
+ approvalId: row.askId,
82
+ ts: row.createdAtMs,
83
+ expiresAtMs: row.expiresAtMs,
84
+ ...(row.sessionId !== row.taskId ? { sessionId: row.sessionId } : {}),
85
+ ...(frame !== undefined ? { toolName: frame.card.toolName, frame } : {}),
86
+ };
87
+ }
66
88
  export const ASK_DECISION_CONSUMER_ABSENT = "absent";
67
89
  const MAX_INFLIGHT_TERMINAL_CLAIMS = 2;
68
90
  const DURABLE_CONVERGE_BACKOFF_MS = [50, 100, 200, 400, 800, 1600];
@@ -181,11 +203,45 @@ function mandatedFromSeat(seat) {
181
203
  return false;
182
204
  }
183
205
  }
206
+ function askRowVisibleTo(row, principal) {
207
+ return row !== null && (row.owner === null || row.owner === principal);
208
+ }
209
+ function waitingLegsReached(entry, othersReached) {
210
+ return (entry.adopted === true ? 0 : 1) + othersReached;
211
+ }
212
+ function withConsumerDisclosure(result, waitingLegs) {
213
+ return waitingLegs > 0 ? result : { status: result.status, body: { ...result.body, consumerState: ASK_DECISION_CONSUMER_ABSENT } };
214
+ }
215
+ function livePendingRowOf(e) {
216
+ const listFrame = e.adopted === true ? e.cardFrame : (e.cardFrame ?? e.frame);
217
+ const approvalCard = e.adopted === true ? e.cardFrame.approvalCard : e.frame.approvalCard;
218
+ return {
219
+ approvalId: e.approvalId,
220
+ toolName: e.toolName,
221
+ ts: e.registeredAtMs,
222
+ expiresAtMs: e.expiresAtMs,
223
+ ...(e.sessionId !== undefined ? { sessionId: e.sessionId } : {}),
224
+ ...(e.requiresRealApproval ? { requiresRealApproval: true } : {}),
225
+ ...(e.governanceForced ? { governanceForced: true } : {}),
226
+ ...(e.mandated ? { mandated: true } : {}),
227
+ ...(standingAnswerHome(pendingEntryStandingAnswerFacts(e)) === "none" ? { rememberRefused: true } : {}),
228
+ ...(e.mandate !== undefined ? { mandate: e.mandate } : {}),
229
+ ...(e.origin !== undefined ? { origin: e.origin } : {}),
230
+ ...(e.originTaskId !== undefined ? { fromSubagent: true, originTaskId: e.originTaskId } : {}),
231
+ ...(approvalCard !== undefined ? { approvalCard } : {}),
232
+ frame: listFrame,
233
+ ...(e.adopted === true ? { consumerState: ASK_DECISION_CONSUMER_ABSENT } : {}),
234
+ };
235
+ }
236
+ function canPresentWithoutDurableRow(c) {
237
+ return c.emitCard === undefined;
238
+ }
184
239
  export const PERSIST_RULE_TEXT_ERROR = `persistRule.rule must be a non-empty string of at most ${MAX_RULE_TEXT_CHARS} characters`;
185
240
  export const PERSIST_RULE_EDITED_FLAG_ERROR = "persistRule.edited must be a boolean when present";
186
241
  export const PERSIST_RULE_BATCH_INDEX_ERROR = "persistRule.batchOfferIndex must be a non-negative integer when present";
187
242
  export const PERSIST_RULE_BATCH_EXCLUSIVE_ERROR = "persistRule.batchOfferIndex is mutually exclusive with persistRule.rule / persistRule.edited — send exactly one arm";
188
243
  export const READ_ROOT_GRANT_ALLOW_ONLY = 'readRootGrant is only meaningful with decision "allow" — it is the card\'s "Yes, and add directory" answer (allow this call and add the card\'s readRootCandidate.dir to this run\'s read directories)';
244
+ export const READ_ROOT_GRANT_NOT_ACCEPTED = 'readRootGrant is not accepted on this deployment — a read-directory grant is honored only on a single-user host-lane deployment (GET /v1/capabilities approvalReadRootGrant is false here); answer with plain decision "allow" instead';
189
245
  export const READ_ROOT_GRANT_SHAPE = `readRootGrant must be the card's readRootCandidate.dir sent back byte for byte — a non-empty string of at most ${READ_ROOT_CANDIDATE_DIR_MAX} characters`;
190
246
  export const PERSIST_RULE_SCOPE_REFUSED = "this endpoint does not accept a rule scope — the server mints it from where the approval happened (send neither `scope` nor `persistRule.scope`)";
191
247
  export function parseToolApprovalResponse(body) {
@@ -272,9 +328,14 @@ function parseToolApprovalFields(body, retired) {
272
328
  export function buildUnattendedPolicyRefusal(reason) {
273
329
  return { allow: false, settledBy: POLICY_REFUSAL_DECISION.settledBy, reason };
274
330
  }
275
- function unattendedRefusalReason(arm) {
331
+ function unattendedRefusalReason(site) {
332
+ if (site.arm === "frame_family_undeclared") {
333
+ return (`UNATTENDED_APPROVAL_POLICY=deny: this run has no live approval channel — the client that started it did not declare the "${site.family}" ` +
334
+ "approval frame family (settings.approvalFrameFamilies), so no approval card is shown to it, and an approval request no one can answer " +
335
+ `is refused here instead of being parked for a person. Upgrade the client, or declare the family it renders (arm: ${site.arm}).`);
336
+ }
276
337
  return ("UNATTENDED_APPROVAL_POLICY=deny: this deployment declares that no one attends its approvals, so an approval " +
277
- `request no one can answer is refused here instead of being parked for a person (arm: ${arm}).`);
338
+ `request no one can answer is refused here instead of being parked for a person (arm: ${site.arm}).`);
278
339
  }
279
340
  function isChildAsk(req) {
280
341
  return req.fromSubagent === true;
@@ -306,45 +367,113 @@ export function resolveStreamApprovalGate(input) {
306
367
  return { active: false, reason: "protocol_disabled" };
307
368
  if (!input.backend)
308
369
  return { active: false, reason: "no_backend" };
309
- if (!input.parkFacility)
370
+ if (input.checkpointStore === undefined)
310
371
  return { active: false, reason: "no_park_facility" };
311
- return { active: true, askStore: input.backend.approvalAsk() };
372
+ return { active: true, askStore: input.backend.approvalAsk(), checkpointStore: input.checkpointStore };
373
+ }
374
+ export const APPROVAL_FRAME_FAMILIES = ["tool_approval", "approval_request"];
375
+ export function presentedApprovalFrameFamily(streamApprovalOn) {
376
+ return streamApprovalOn ? "approval_request" : "tool_approval";
377
+ }
378
+ const APPROVAL_FRAME_FAMILIES_FIELD = "settings.approvalFrameFamilies";
379
+ const APPROVAL_FRAME_FAMILY_WORDS = APPROVAL_FRAME_FAMILIES.map((w) => `"${w}"`).join(" | ");
380
+ export function approvalFrameFamiliesIssue(settings) {
381
+ if (settings === null || typeof settings !== "object" || Array.isArray(settings))
382
+ return null;
383
+ const raw = Reflect.get(settings, "approvalFrameFamilies");
384
+ if (raw === undefined)
385
+ return null;
386
+ if (!Array.isArray(raw)) {
387
+ return {
388
+ message: `${APPROVAL_FRAME_FAMILIES_FIELD} must be an array of approval frame family words (${APPROVAL_FRAME_FAMILY_WORDS}) — the families of approval cards this client renders. ` +
389
+ 'Refused rather than ignored, because an ignored declaration reads as "in effect" to the caller',
390
+ fields: { field: APPROVAL_FRAME_FAMILIES_FIELD, reason: "bad_entry" },
391
+ };
392
+ }
393
+ const seen = new Set();
394
+ for (const [index, entry] of raw.entries()) {
395
+ if (!APPROVAL_FRAME_FAMILIES.includes(entry)) {
396
+ return {
397
+ message: `${APPROVAL_FRAME_FAMILIES_FIELD}[${index}] is not one of ${APPROVAL_FRAME_FAMILY_WORDS} — every entry must be an approval frame family word. Refused rather than dropped, because a dropped entry reads as "in effect" to the caller`,
398
+ fields: { field: APPROVAL_FRAME_FAMILIES_FIELD, reason: "bad_entry", index },
399
+ };
400
+ }
401
+ if (seen.has(entry)) {
402
+ return {
403
+ message: `${APPROVAL_FRAME_FAMILIES_FIELD}[${index}] repeats a family declared earlier in the list — declare each family once`,
404
+ fields: { field: APPROVAL_FRAME_FAMILIES_FIELD, reason: "bad_entry", index },
405
+ };
406
+ }
407
+ seen.add(entry);
408
+ }
409
+ return null;
410
+ }
411
+ export function declaredApprovalFrameFamilies(settings) {
412
+ if (approvalFrameFamiliesIssue(settings) !== null || settings === null || typeof settings !== "object")
413
+ return new Set();
414
+ const raw = Reflect.get(settings, "approvalFrameFamilies");
415
+ return new Set(Array.isArray(raw) ? raw.filter((w) => APPROVAL_FRAME_FAMILIES.includes(w)) : []);
312
416
  }
313
417
  export function createApprovalCardEmitter(sinks) {
314
- return async (frame) => {
315
- let accepted = false;
316
- if (sinks.appendDurable) {
317
- try {
318
- await sinks.appendDurable(frame);
319
- accepted = true;
320
- }
321
- catch {
322
- }
418
+ const present = [sinks.appendDurable, sinks.writeLive].filter((w) => w !== undefined);
419
+ const attempt = (write, frame) => (async () => {
420
+ try {
421
+ await write(frame);
422
+ return true;
323
423
  }
324
- if (sinks.writeLive) {
325
- try {
326
- await sinks.writeLive(frame);
327
- accepted = true;
328
- }
329
- catch {
424
+ catch (err) {
425
+ defaultLogger.warn("approval_card_sink_write_failed", { frame: frame.type, approvalId: frame.approvalId, errKind: errorKindOf(err) });
426
+ return false;
427
+ }
428
+ })();
429
+ const opened = new Map();
430
+ return async (frame, registration) => {
431
+ if (frame.type === "approval_request") {
432
+ const results = present.map((write) => attempt(write, frame));
433
+ opened.set(registration, results);
434
+ const accepted = await new Promise((resolve) => {
435
+ let pending = results.length;
436
+ if (pending === 0)
437
+ resolve(false);
438
+ for (const r of results) {
439
+ void r.then((ok) => {
440
+ if (ok)
441
+ resolve(true);
442
+ else if (--pending === 0)
443
+ resolve(false);
444
+ });
445
+ }
446
+ });
447
+ if (!accepted) {
448
+ opened.delete(registration);
449
+ throw new Error("approval card undeliverable — no sink accepted the frame (live stream closed and/or the durable append failed)");
330
450
  }
451
+ return;
331
452
  }
332
- if (!accepted)
333
- throw new Error("approval card undeliverable — no sink accepted the frame (live stream closed and/or the durable append failed)");
453
+ const results = opened.get(registration);
454
+ opened.delete(registration);
455
+ if (results === undefined)
456
+ return;
457
+ await Promise.all(present.map((write, i) => results[i].then((ok) => (ok ? attempt(write, frame) : false))));
334
458
  };
335
459
  }
336
460
  export function resolveApprovalLeg(input) {
337
- if (!input.streamApprovalOn)
338
- return { active: false, windowZero: input.windowMs === 0 };
339
- const tightDeadline = input.legWalltimeMs !== undefined && input.legWalltimeMs - input.windowMarginMs <= 0;
461
+ const active = input.streamApprovalOn;
462
+ const tightDeadline = active && input.legWalltimeMs !== undefined && input.legWalltimeMs - input.windowMarginMs <= 0;
340
463
  if (input.windowMs === 0 || tightDeadline)
341
- return { active: true, windowZero: true };
464
+ return { active, unattended: { arm: "window_zero" } };
465
+ const family = presentedApprovalFrameFamily(active);
466
+ if (!input.declaredFamilies.has(family))
467
+ return { active, unattended: { arm: "frame_family_undeclared", family } };
342
468
  return {
343
- active: true,
344
- windowZero: false,
345
- ...(input.legWalltimeMs !== undefined ? { legDeadlineMonotonic: input.nowMonotonicMs + input.legWalltimeMs } : {}),
469
+ active,
470
+ ...(active && input.legWalltimeMs !== undefined ? { legDeadlineMonotonic: input.nowMonotonicMs + input.legWalltimeMs } : {}),
346
471
  };
347
472
  }
473
+ export function approvalPreambleReplays(input) {
474
+ const leg = resolveApprovalLeg({ ...input, nowMonotonicMs: performance.now() });
475
+ return leg.active && leg.unattended === undefined;
476
+ }
348
477
  function effectiveAskWindowMs(ttlMs, windowMarginMs, legDeadlineMonotonic, nowMonotonicMs) {
349
478
  if (legDeadlineMonotonic === undefined)
350
479
  return ttlMs;
@@ -412,6 +541,7 @@ export class ToolApprovalCoordinator {
412
541
  return this.terminalClaimsInFlightCount(askId);
413
542
  }
414
543
  pendingByAskId = new Map();
544
+ presentedCardWatchers = new Map();
415
545
  allowAllSessions = new Set();
416
546
  streams = new Map();
417
547
  ttlMs;
@@ -423,6 +553,7 @@ export class ToolApprovalCoordinator {
423
553
  admitByTask = new Map();
424
554
  admitByOwner = new Map();
425
555
  suspendedPollOffset = 0;
556
+ adoptedPollOffset = 0;
426
557
  editedDecisionsInFlight = new Map();
427
558
  storeErrorWarned = false;
428
559
  storeErrorTally = 0;
@@ -430,11 +561,12 @@ export class ToolApprovalCoordinator {
430
561
  ruleConsent;
431
562
  unattendedPolicy;
432
563
  ruleScopeRootFor;
433
- parkTombstones = new Map();
434
- parkTombstoneMax;
435
- parkTombstoneTtlMs;
436
- parkTombstonePerPrincipalMax;
564
+ parkRedemptions = new Map();
565
+ parkRedemptionMax;
566
+ parkRedemptionTtlMs;
567
+ parkRedemptionPerPrincipalMax;
437
568
  parkedRedeem;
569
+ parkedGate;
438
570
  askAudit;
439
571
  sessionGrants;
440
572
  deploymentListAsks;
@@ -454,9 +586,10 @@ export class ToolApprovalCoordinator {
454
586
  this.admitMaxPerOwner = opts?.admitMaxPerOwner ?? DEFAULT_ADMIT_MAX_PER_OWNER;
455
587
  this.unattendedPolicy = opts?.unattendedPolicy ?? "park";
456
588
  this.parkedRedeem = opts?.parkedRedeem;
457
- this.parkTombstoneMax = opts?.parkTombstoneMax ?? DEFAULT_PARK_TOMBSTONE_MAX;
458
- this.parkTombstoneTtlMs = opts?.parkTombstoneTtlMs ?? DEFAULT_PARK_TOMBSTONE_TTL_MS;
459
- this.parkTombstonePerPrincipalMax = opts?.parkTombstonePerPrincipalMax ?? DEFAULT_PARK_TOMBSTONE_PER_PRINCIPAL_MAX;
589
+ this.parkedGate = opts?.parkedGate;
590
+ this.parkRedemptionMax = opts?.parkRedemptionMax ?? DEFAULT_PARK_REDEMPTION_MAX;
591
+ this.parkRedemptionTtlMs = opts?.parkRedemptionTtlMs ?? DEFAULT_PARK_REDEMPTION_TTL_MS;
592
+ this.parkRedemptionPerPrincipalMax = opts?.parkRedemptionPerPrincipalMax ?? DEFAULT_PARK_REDEMPTION_PER_PRINCIPAL_MAX;
460
593
  this.askAudit = opts?.askAudit;
461
594
  this.sessionGrants = opts?.sessionGrants;
462
595
  if (opts?.deploymentListAsks !== undefined && opts.sessionGrants === undefined) {
@@ -475,7 +608,7 @@ export class ToolApprovalCoordinator {
475
608
  outcomeWord = "unavailable";
476
609
  break;
477
610
  case "deny":
478
- outcome = buildUnattendedPolicyRefusal(unattendedRefusalReason(site.arm));
611
+ outcome = buildUnattendedPolicyRefusal(unattendedRefusalReason(site));
479
612
  outcomeWord = "deny";
480
613
  break;
481
614
  default: {
@@ -501,12 +634,12 @@ export class ToolApprovalCoordinator {
501
634
  return this.unattendedRoutesToPark ? deriveBatchId(sourceTaskId, runId, legKey) : deriveAskScopedBatchId(askId);
502
635
  }
503
636
  unattendedAskOutcome(site) {
504
- return this.unattendedOutcome({
505
- arm: "window_zero",
637
+ const facts = {
506
638
  ...(site.taskId !== undefined ? { taskId: site.taskId } : {}),
507
639
  ...(site.owner !== undefined && site.owner !== null ? { owner: site.owner } : {}),
508
640
  ...(site.toolName !== undefined ? { toolName: site.toolName } : {}),
509
- });
641
+ };
642
+ return this.unattendedOutcome(site.arm === "frame_family_undeclared" ? { arm: site.arm, family: site.family, ...facts } : { arm: site.arm, ...facts });
510
643
  }
511
644
  async landUnattendedOutcome(outcome, row) {
512
645
  const store = this.askStore;
@@ -855,12 +988,12 @@ export class ToolApprovalCoordinator {
855
988
  return false;
856
989
  }
857
990
  }
858
- boundUnattendedAsk = (identity) => {
991
+ boundUnattendedAsk = (identity, why) => {
859
992
  return async (req) => {
860
993
  const origin = { owner: identity.owner, taskId: identity.taskId, ...(identity.sessionId !== undefined ? { sessionId: identity.sessionId } : {}), legKey: "" };
861
994
  if (await this.standingGrantSettles(origin, req, this.governanceForcedOf(origin, req)))
862
995
  return true;
863
- return this.unattendedAskOutcome({ taskId: identity.taskId, owner: identity.owner, toolName: req.toolName });
996
+ return this.unattendedAskOutcome({ ...why, taskId: identity.taskId, owner: identity.owner, toolName: req.toolName });
864
997
  };
865
998
  };
866
999
  async sessionGrantUncontestedByRow(origin, req) {
@@ -951,75 +1084,84 @@ export class ToolApprovalCoordinator {
951
1084
  }
952
1085
  }
953
1086
  }
954
- recordParkTombstone(approvalId, tomb) {
955
- const existing = this.parkTombstones.get(approvalId);
956
- if (existing !== undefined) {
957
- if (existing.redeemed !== undefined || existing.inflight !== undefined)
958
- return;
959
- if (Date.now() - existing.parkedAtMs <= this.parkTombstoneTtlMs)
960
- return;
961
- this.parkTombstones.delete(approvalId);
1087
+ parkRedemptionAdmission(owner) {
1088
+ const effectivePerPrincipalMax = Math.min(this.parkRedemptionPerPrincipalMax, this.parkRedemptionMax);
1089
+ let inflight = 0;
1090
+ let ownInflight = 0;
1091
+ for (const r of this.parkRedemptions.values()) {
1092
+ if (r.inflight === undefined)
1093
+ continue;
1094
+ inflight++;
1095
+ if (r.owner === owner)
1096
+ ownInflight++;
962
1097
  }
963
- const effectivePerPrincipalMax = Math.min(this.parkTombstonePerPrincipalMax, this.parkTombstoneMax);
1098
+ if (ownInflight < effectivePerPrincipalMax && inflight < this.parkRedemptionMax)
1099
+ return undefined;
1100
+ defaultLogger.warn("parked approval redemption refused at admission: too many redemptions in flight", { owner: owner ?? "<none>", ownInflight, inflight });
1101
+ return {
1102
+ status: 429,
1103
+ body: refusalBody({
1104
+ errorCode: "limit.rate_exceeded",
1105
+ message: "too many late approval answers are being applied right now — retry after the Retry-After interval",
1106
+ facts: { retryAfterSec: PARK_REDEMPTION_ADMISSION_RETRY_AFTER_SEC },
1107
+ }),
1108
+ };
1109
+ }
1110
+ openParkRedemption(askId, owner) {
1111
+ const opened = { owner, stampedAtMs: Date.now() };
1112
+ this.parkRedemptions.delete(askId);
1113
+ this.parkRedemptions.set(askId, opened);
1114
+ return opened;
1115
+ }
1116
+ retainParkRedemption(askId, memo) {
964
1117
  const now = Date.now();
965
- let ownCount = 0;
966
- let ownExpiredVictim;
967
- let ownBridgeVictim;
968
- let ownPlainVictim;
969
- let ownEvictable;
970
- let globalExpiredVictim;
971
- let globalBridgeVictim;
972
- for (const [k, t] of this.parkTombstones) {
973
- const expired = now - t.parkedAtMs > this.parkTombstoneTtlMs;
974
- if (globalExpiredVictim === undefined && expired && t.inflight === undefined)
975
- globalExpiredVictim = k;
976
- if (globalBridgeVictim === undefined && t.kind === "voidBridge" && t.inflight === undefined)
977
- globalBridgeVictim = k;
978
- if (t.owner !== tomb.owner)
1118
+ const effectivePerPrincipalMax = Math.min(this.parkRedemptionPerPrincipalMax, this.parkRedemptionMax);
1119
+ let receipts = 0;
1120
+ let ownReceipts = 0;
1121
+ let ownExpired;
1122
+ let ownOldest;
1123
+ let globalExpired;
1124
+ for (const [k, r] of this.parkRedemptions) {
1125
+ if (k === askId || r.redeemed === undefined || r.inflight !== undefined)
979
1126
  continue;
980
- ownCount++;
981
- if (t.inflight !== undefined)
1127
+ receipts++;
1128
+ const expired = now - r.stampedAtMs > this.parkRedemptionTtlMs;
1129
+ if (globalExpired === undefined && expired)
1130
+ globalExpired = k;
1131
+ if (r.owner !== memo.owner)
982
1132
  continue;
983
- if (ownEvictable === undefined)
984
- ownEvictable = k;
985
- if (ownExpiredVictim === undefined && expired)
986
- ownExpiredVictim = k;
987
- if (ownBridgeVictim === undefined && t.kind === "voidBridge")
988
- ownBridgeVictim = k;
989
- if (ownPlainVictim === undefined && t.kind === "park" && t.redeemed === undefined)
990
- ownPlainVictim = k;
991
- }
992
- const insertingBridge = tomb.kind === "voidBridge";
993
- if (ownCount >= effectivePerPrincipalMax) {
994
- const victim = insertingBridge ? (ownExpiredVictim ?? ownBridgeVictim) : (ownExpiredVictim ?? ownBridgeVictim ?? ownPlainVictim ?? ownEvictable);
995
- if (victim === undefined) {
996
- recordFailOpen("server.approval.park-tombstone-global-cap-refused", `owner=${tomb.owner ?? "<none>"} reason=${insertingBridge ? "bridge-would-evict-redeemable" : "all-inflight"}`);
997
- return;
1133
+ ownReceipts++;
1134
+ if (ownOldest === undefined)
1135
+ ownOldest = k;
1136
+ if (ownExpired === undefined && expired)
1137
+ ownExpired = k;
1138
+ }
1139
+ if (ownReceipts >= effectivePerPrincipalMax) {
1140
+ const victim = ownExpired ?? ownOldest;
1141
+ if (victim !== undefined)
1142
+ this.parkRedemptions.delete(victim);
1143
+ }
1144
+ else if (receipts >= this.parkRedemptionMax) {
1145
+ if (globalExpired === undefined) {
1146
+ recordFailOpen("server.approval.park-redemption-cap-refused", `owner=${memo.owner ?? "<none>"}`);
1147
+ if (this.parkRedemptions.get(askId) === memo)
1148
+ this.parkRedemptions.delete(askId);
1149
+ return false;
998
1150
  }
999
- this.parkTombstones.delete(victim);
1000
- }
1001
- else if (this.parkTombstones.size >= this.parkTombstoneMax && globalExpiredVictim !== undefined) {
1002
- this.parkTombstones.delete(globalExpiredVictim);
1003
- }
1004
- else if (this.parkTombstones.size >= this.parkTombstoneMax && !insertingBridge && globalBridgeVictim !== undefined) {
1005
- this.parkTombstones.delete(globalBridgeVictim);
1151
+ this.parkRedemptions.delete(globalExpired);
1006
1152
  }
1007
- else if (this.parkTombstones.size >= this.parkTombstoneMax) {
1008
- recordFailOpen("server.approval.park-tombstone-global-cap-refused", `owner=${tomb.owner ?? "<none>"}${insertingBridge ? " reason=bridge-refused-at-global-cap" : ""}`);
1009
- return;
1010
- }
1011
- this.parkTombstones.set(approvalId, tomb);
1153
+ memo.stampedAtMs = now;
1154
+ return true;
1012
1155
  }
1013
- lookupParkTombstone(approvalId) {
1014
- const tomb = this.parkTombstones.get(approvalId);
1015
- if (tomb === undefined)
1156
+ lookupParkRedemption(askId) {
1157
+ const r = this.parkRedemptions.get(askId);
1158
+ if (r === undefined)
1016
1159
  return undefined;
1017
- if (Date.now() - tomb.parkedAtMs > this.parkTombstoneTtlMs) {
1018
- if (tomb.inflight === undefined)
1019
- this.parkTombstones.delete(approvalId);
1160
+ if (r.inflight === undefined && Date.now() - r.stampedAtMs > this.parkRedemptionTtlMs) {
1161
+ this.parkRedemptions.delete(askId);
1020
1162
  return undefined;
1021
1163
  }
1022
- return tomb;
1164
+ return r;
1023
1165
  }
1024
1166
  notifyExternalDecision(askId, outcome) {
1025
1167
  const entries = this.pendingByAskId.get(askId);
@@ -1029,7 +1171,8 @@ export class ToolApprovalCoordinator {
1029
1171
  let settled = 0;
1030
1172
  for (const entry of [...entries]) {
1031
1173
  entry.settle(verdictOfDecided(args));
1032
- settled += 1;
1174
+ if (entry.adopted !== true)
1175
+ settled += 1;
1033
1176
  }
1034
1177
  return { settled };
1035
1178
  }
@@ -1211,7 +1354,6 @@ export class ToolApprovalCoordinator {
1211
1354
  let askId;
1212
1355
  let batchId;
1213
1356
  const card = this.askStore ? buildApprovalCard(frame, req, req.requiresRealApproval === true) : undefined;
1214
- let wireApprovalId = id;
1215
1357
  let cardForFrame = card;
1216
1358
  let persistedExpiresAtMs = expiresAtMs;
1217
1359
  let lateEnsuredRow;
@@ -1257,7 +1399,7 @@ export class ToolApprovalCoordinator {
1257
1399
  toolCallId: req.toolCallId,
1258
1400
  legKey,
1259
1401
  parentToolCallId: req.delegation?.parentToolCallId ?? null,
1260
- cardJson: buildApprovalCardEnvelope(id, card),
1402
+ cardJson: buildApprovalCardEnvelope(askId, card),
1261
1403
  boundInputHash: readBoundInputHash(req),
1262
1404
  ...durableRuleMaterial(ruleLaneMaterial),
1263
1405
  schemaVersion: 1,
@@ -1289,8 +1431,6 @@ export class ToolApprovalCoordinator {
1289
1431
  return existingOrCreated.inserted ? this.landUnattendedOutcome(unattended, existingOrCreated.row) : unattended;
1290
1432
  }
1291
1433
  persistedExpiresAtMs = existingOrCreated.row.expiresAtMs;
1292
- wireApprovalId = persistedEnvelope.approvalId;
1293
- frame.approvalId = wireApprovalId;
1294
1434
  cardForFrame = persistedEnvelope.card;
1295
1435
  if (cardForFrame.governanceForced === true)
1296
1436
  frame.governanceForced = true;
@@ -1359,8 +1499,8 @@ export class ToolApprovalCoordinator {
1359
1499
  if (!confirmedRowExists) {
1360
1500
  askId = undefined;
1361
1501
  batchId = undefined;
1362
- if (reachability === "unreached") {
1363
- const unattended = this.unattendedOutcome({ arm: "unreached_no_row", taskId: origin.taskId, ...(origin.owner !== null ? { owner: origin.owner } : {}), toolName: req.toolName, reachability });
1502
+ if (!aliveCtxs.some(canPresentWithoutDurableRow)) {
1503
+ const unattended = this.unattendedOutcome({ arm: "no_row_no_presenter", taskId: origin.taskId, ...(origin.owner !== null ? { owner: origin.owner } : {}), toolName: req.toolName, reachability });
1364
1504
  const landed = abandonRow(unattended);
1365
1505
  releaseAdmission();
1366
1506
  return landed ?? unattended;
@@ -1369,6 +1509,8 @@ export class ToolApprovalCoordinator {
1369
1509
  }
1370
1510
  }
1371
1511
  }
1512
+ const wireApprovalId = askId ?? id;
1513
+ frame.approvalId = wireApprovalId;
1372
1514
  let done = false;
1373
1515
  let emitFailed = false;
1374
1516
  let emitSettled = false;
@@ -1427,19 +1569,24 @@ export class ToolApprovalCoordinator {
1427
1569
  sig.removeEventListener("abort", fn);
1428
1570
  this.pending.delete(id);
1429
1571
  if (windowRouteUnavailable && askId !== undefined && batchId !== undefined && this.unattendedRoutesToPark) {
1430
- this.recordParkTombstone(wireApprovalId, { kind: "park", askId, batchId, owner: origin.owner, parkedAtMs: Date.now() });
1431
1572
  parkRouted = true;
1432
1573
  }
1574
+ let lastLiveRegistration = false;
1433
1575
  if (askId !== undefined) {
1434
1576
  const siblings = this.pendingByAskId.get(askId);
1435
1577
  if (siblings && selfEntry) {
1436
1578
  siblings.delete(selfEntry);
1437
- if (siblings.size === 0)
1579
+ if (siblings.size === 0) {
1438
1580
  this.pendingByAskId.delete(askId);
1581
+ lastLiveRegistration = true;
1582
+ }
1439
1583
  }
1440
1584
  }
1441
1585
  lastOutcome = outcome;
1442
1586
  lastCause = verdict.by === "system" ? verdict.cause : undefined;
1587
+ if (lastLiveRegistration && askId !== undefined) {
1588
+ this.notifyPresentedCardWatchers(askId, buildApprovalCompleteFrame({ askId, taskId: origin.taskId, approvalId: wireApprovalId, outcome, ...(lastCause !== undefined ? { cause: lastCause } : {}), parked: parkRouted, nowMs: Date.now() }));
1589
+ }
1443
1590
  releaseAdmission();
1444
1591
  if (selfEntry)
1445
1592
  selfEntry.settledOutcome = outcome;
@@ -1511,7 +1658,7 @@ export class ToolApprovalCoordinator {
1511
1658
  settleFromClaimLoss(read, armCause);
1512
1659
  };
1513
1660
  const boundedClaim = (mk, label, timeoutMs, late) => {
1514
- const release = this.admitTerminalClaim(askId ?? wireApprovalId);
1661
+ const release = this.admitTerminalClaim(wireApprovalId);
1515
1662
  if (release === undefined)
1516
1663
  return undefined;
1517
1664
  const raw = mk();
@@ -1583,7 +1730,6 @@ export class ToolApprovalCoordinator {
1583
1730
  arm.onWon(outcome);
1584
1731
  return;
1585
1732
  }
1586
- arm.onLostTruth?.(outcome.current);
1587
1733
  const read = this.classifyClaimLoss(pinnedAskId, outcome.current, `claim:${arm.intent}`);
1588
1734
  if (done) {
1589
1735
  broadcastClaimTruth(read, arm.cause);
@@ -1673,32 +1819,23 @@ export class ToolApprovalCoordinator {
1673
1819
  const store = this.askStore;
1674
1820
  const pinnedAskId = askId;
1675
1821
  const pinnedBatchId = batchId;
1676
- const cancelLostTruth = (current) => {
1677
- if (current.state !== "VOID")
1678
- return;
1679
- this.recordParkTombstone(wireApprovalId, { kind: "voidBridge", askId: pinnedAskId, batchId: pinnedBatchId, owner: origin.owner, parkedAtMs: Date.now() });
1680
- };
1681
1822
  await resolveLocalOutcome({
1682
1823
  intent: "cancel",
1683
1824
  cause,
1684
1825
  storeLabel: "claimTerminal(cancel)",
1685
1826
  claim: (timeoutMs) => boundedClaim(() => store.claimTerminal(pinnedAskId, pinnedBatchId, "cancel", { cause }), "claimTerminal(cancel)", timeoutMs, (late) => {
1686
1827
  if (!late.claimed) {
1687
- cancelLostTruth(late.current);
1688
1828
  lateClaimLossSettle(late.current, "cancel", cause);
1689
1829
  return;
1690
1830
  }
1691
1831
  this.settleVoidedSiblings([pinnedAskId], cause);
1692
- this.recordParkTombstone(wireApprovalId, { kind: "voidBridge", askId: pinnedAskId, batchId: pinnedBatchId, owner: origin.owner, parkedAtMs: Date.now() });
1693
1832
  this.emitRevokeTo(aliveCtxs, buildRevokeFrame(pinnedBatchId, [pinnedAskId], "aborted", Date.now(), origin.taskId));
1694
1833
  }),
1695
1834
  onWon: () => {
1696
1835
  settle({ by: "system", cause });
1697
1836
  this.settleVoidedSiblings([pinnedAskId], cause);
1698
- this.recordParkTombstone(wireApprovalId, { kind: "voidBridge", askId: pinnedAskId, batchId: pinnedBatchId, owner: origin.owner, parkedAtMs: Date.now() });
1699
1837
  this.emitRevokeTo(aliveCtxs, buildRevokeFrame(pinnedBatchId, [pinnedAskId], "aborted", Date.now(), origin.taskId));
1700
1838
  },
1701
- onLostTruth: cancelLostTruth,
1702
1839
  settleByIntent: () => settle({ by: "system", cause }),
1703
1840
  });
1704
1841
  };
@@ -1750,6 +1887,9 @@ export class ToolApprovalCoordinator {
1750
1887
  const { rememberRefused: _stale, ...cardRest } = cardForFrame;
1751
1888
  cardForFrame = rememberRefused ? { ...cardRest, rememberRefused: true } : cardRest;
1752
1889
  }
1890
+ const cardFrame = askId !== undefined && cardForFrame !== undefined
1891
+ ? buildApprovalRequestFrame({ askId, taskId: origin.taskId, approvalId: wireApprovalId, card: cardForFrame, expiresAtMs: persistedExpiresAtMs, nowMs: Date.now(), live: frame })
1892
+ : undefined;
1753
1893
  const pendingEntry = {
1754
1894
  settle,
1755
1895
  forceParkNow,
@@ -1760,6 +1900,9 @@ export class ToolApprovalCoordinator {
1760
1900
  ...(child ? { originTaskId: req.sourceTaskId } : primary ? { targetCtxs: new Set(aliveCtxs), onTargetGone } : {}),
1761
1901
  ...(origin.sessionId ? { sessionId: origin.sessionId } : {}),
1762
1902
  frame,
1903
+ ...(cardFrame !== undefined ? { cardFrame } : {}),
1904
+ approvalId: wireApprovalId,
1905
+ registrationId: id,
1763
1906
  toolName: req.toolName,
1764
1907
  grantBinding: entryGrantBinding,
1765
1908
  ...(entryClasses !== undefined ? { classes: entryClasses } : {}),
@@ -1777,6 +1920,7 @@ export class ToolApprovalCoordinator {
1777
1920
  selfEntry = pendingEntry;
1778
1921
  this.pending.set(id, pendingEntry);
1779
1922
  if (askId !== undefined) {
1923
+ this.releaseAdoptedRegistrations(askId);
1780
1924
  let siblings = this.pendingByAskId.get(askId);
1781
1925
  if (!siblings) {
1782
1926
  siblings = new Set();
@@ -1798,9 +1942,6 @@ export class ToolApprovalCoordinator {
1798
1942
  });
1799
1943
  if (this.shutdownDraining)
1800
1944
  forceParkNow(APPROVAL_ENDING_CAUSES.SHUTDOWN_BEFORE_PARK);
1801
- const cardFrame = askId !== undefined && cardForFrame !== undefined
1802
- ? buildApprovalRequestFrame({ askId, taskId: origin.taskId, approvalId: wireApprovalId, card: cardForFrame, expiresAtMs: persistedExpiresAtMs, nowMs: Date.now() })
1803
- : undefined;
1804
1945
  {
1805
1946
  const serverNowMs = Date.now();
1806
1947
  frame.expiresAtMs = persistedExpiresAtMs;
@@ -1808,28 +1949,37 @@ export class ToolApprovalCoordinator {
1808
1949
  frame.expiresInMs = Math.max(0, persistedExpiresAtMs - serverNowMs);
1809
1950
  }
1810
1951
  const emitOne = async (c) => {
1811
- let delivered = true;
1812
- try {
1813
- await c.emit(frame);
1952
+ let family;
1953
+ let send;
1954
+ if (c.emitCard !== undefined) {
1955
+ if (cardFrame === undefined) {
1956
+ defaultLogger.warn("approval card leg cannot present this ask: it has no confirmed durable identity (no approval_request frame is minted without one)", {
1957
+ taskId: origin.taskId,
1958
+ approvalId: wireApprovalId,
1959
+ toolName: req.toolName,
1960
+ });
1961
+ return { delivered: false };
1962
+ }
1963
+ family = "approval_request";
1964
+ send = () => c.emitCard(cardFrame, id);
1814
1965
  }
1815
- catch {
1816
- delivered = false;
1966
+ else {
1967
+ family = "tool_approval";
1968
+ send = () => c.emit(frame);
1817
1969
  }
1818
- let cardDelivered = false;
1819
- if (cardFrame && c.emitCard) {
1820
- try {
1821
- await c.emitCard(cardFrame);
1822
- cardDelivered = true;
1823
- }
1824
- catch {
1825
- }
1970
+ try {
1971
+ await send();
1972
+ return { delivered: true };
1973
+ }
1974
+ catch (err) {
1975
+ defaultLogger.warn("approval_card_delivery_failed", { taskId: origin.taskId, approvalId: wireApprovalId, family, errKind: errorKindOf(err) });
1976
+ return { delivered: false };
1826
1977
  }
1827
- return { legacy: delivered, card: cardDelivered };
1828
1978
  };
1829
1979
  const openByCtx = new Map(aliveCtxs.map((c) => [c, emitOne(c)]));
1830
1980
  const emitAllP = (async () => {
1831
1981
  const results = await Promise.all(openByCtx.values());
1832
- const anySucceeded = reachability === "unreached" || this.hasOtherSuspendedOwner(askId, selfEntry) || results.some((r) => r.legacy || r.card);
1982
+ const anySucceeded = reachability === "unreached" || this.hasOtherSuspendedOwner(askId, selfEntry) || results.some((r) => r.delivered);
1833
1983
  emitSettled = true;
1834
1984
  if (!anySucceeded && !done) {
1835
1985
  if (this.askStore && askId !== undefined && batchId !== undefined) {
@@ -1873,15 +2023,30 @@ export class ToolApprovalCoordinator {
1873
2023
  for (const c of aliveCtxs) {
1874
2024
  void openByCtx
1875
2025
  .get(c)
1876
- .then((delivered) => delivered.legacy
1877
- ? c.emit({
2026
+ .then(({ delivered }) => {
2027
+ if (!delivered)
2028
+ return undefined;
2029
+ if (c.emitCard !== undefined) {
2030
+ if (cardFrame === undefined)
2031
+ return undefined;
2032
+ return c.emitCard(buildApprovalCompleteFrame({
2033
+ askId: cardFrame.askId,
2034
+ taskId: cardFrame.taskId,
2035
+ approvalId: wireApprovalId,
2036
+ outcome: lastOutcome,
2037
+ ...(lastCause !== undefined ? { cause: lastCause } : {}),
2038
+ parked: parkRouted,
2039
+ nowMs: Date.now(),
2040
+ }), id);
2041
+ }
2042
+ return c.emit({
1878
2043
  type: "tool_approval_complete",
1879
2044
  approvalId: wireApprovalId,
1880
2045
  outcome: lastOutcome,
1881
2046
  ...(lastCause !== undefined ? { cause: lastCause } : {}),
1882
2047
  ...(parkRouted ? { parked: true } : {}),
1883
- })
1884
- : undefined)
2048
+ });
2049
+ })
1885
2050
  .catch(() => undefined);
1886
2051
  }
1887
2052
  if (emitFailed)
@@ -1892,24 +2057,47 @@ export class ToolApprovalCoordinator {
1892
2057
  return this.unattendedOutcome({ arm: "emit_unsettled", taskId: origin.taskId, ...(origin.owner !== null ? { owner: origin.owner } : {}), toolName: req.toolName, reachability });
1893
2058
  return shapeOutcome(settled);
1894
2059
  }
1895
- respond(id, principal, body, httpReq) {
2060
+ respond(id, principal, body, httpReq, deployment) {
1896
2061
  const parsed = parseToolApprovalResponse(body);
1897
2062
  if (!parsed.ok)
1898
- return { status: 400, body: { error: parsed.error, errorCode: "request.body_shape", ...(parsed.field !== undefined ? { field: parsed.field } : {}) } };
1899
- const entry = this.pending.get(id);
2063
+ return { status: 400, body: refusalBody({ errorCode: "request.body_shape", message: parsed.error, facts: { ...(parsed.field !== undefined ? { field: parsed.field } : {}) } }) };
2064
+ if (parsed.readRootGrant !== undefined && deployment?.readRootGrantAccepted !== true) {
2065
+ return { status: 400, body: refusalBody({ errorCode: "request.body_shape", message: READ_ROOT_GRANT_NOT_ACCEPTED }) };
2066
+ }
2067
+ const entry = this.liveEntryFor(id);
2068
+ if (entry?.adopted === true)
2069
+ return this.respondAdopted(entry, id, principal, parsed, httpReq);
1900
2070
  if (!entry || (entry.owner !== null && entry.owner !== principal)) {
1901
2071
  return this.respondLate(id, principal, parsed, httpReq);
1902
2072
  }
2073
+ return this.answerRegistration(entry, id, principal, parsed, httpReq);
2074
+ }
2075
+ async respondAdopted(entry, id, principal, parsed, httpReq) {
2076
+ const store = this.askStore;
2077
+ if (!store)
2078
+ return this.notPending();
2079
+ const row = await this.askRowFor(store, id, "getAsk(respond-adopted)");
2080
+ if (!askRowVisibleTo(row, principal))
2081
+ return this.notPending(NOT_PENDING_UNKNOWN_CAUSE);
2082
+ if (row.state !== "STREAM_PENDING")
2083
+ return this.dispatchRowTruth(row, id, parsed, httpReq, principal);
2084
+ return this.answerRegistration(entry, id, principal, parsed, httpReq);
2085
+ }
2086
+ answerRegistration(entry, id, principal, parsed, httpReq) {
1903
2087
  const preRule = this.precheckEditedRuleBeforeDecision(entry, parsed);
1904
2088
  if (preRule !== undefined)
1905
2089
  return preRule;
1906
2090
  if (parsed.value !== "deny" && policyCannotAnswer(heldAskOf(entry.requiresRealApproval === true), parsed.decidedBy)) {
1907
- return { status: 400, body: { error: POLICY_CANNOT_ALLOW_PERSON_ASK, errorCode: "request.body_shape" } };
2091
+ return { status: 400, body: refusalBody({ errorCode: "request.body_shape", message: POLICY_CANNOT_ALLOW_PERSON_ASK }) };
1908
2092
  }
1909
2093
  const ruleEligibility = { eligible: true };
1910
- const decided = this.askStore && entry.askId !== undefined && entry.batchId !== undefined
1911
- ? this.respondWithCas(this.askStore, entry.askId, entry.batchId, id, entry, parsed, ruleEligibility)
1912
- : this.finishRespond(id, entry, parsed);
2094
+ const store = this.askStore;
2095
+ if (store && entry.askId !== undefined && entry.batchId !== undefined) {
2096
+ return this.respondWithCas(store, entry.askId, entry.batchId, entry, parsed, ruleEligibility).then((cas) => cas.lost ? this.dispatchRowTruth(cas.row, id, parsed, httpReq, principal) : this.afterLiveDecision(cas.result, entry, parsed, principal, ruleEligibility));
2097
+ }
2098
+ return this.afterLiveDecision(this.finishRespond(entry, parsed), entry, parsed, principal, ruleEligibility);
2099
+ }
2100
+ afterLiveDecision(decided, entry, parsed, principal, ruleEligibility) {
1913
2101
  const settled = this.recordDeploymentListAnswer(decided, entry, parsed, principal, ruleEligibility);
1914
2102
  if (parsed.persistRule === undefined)
1915
2103
  return settled;
@@ -1956,11 +2144,13 @@ export class ToolApprovalCoordinator {
1956
2144
  return undefined;
1957
2145
  return {
1958
2146
  status: 400,
1959
- body: {
1960
- error: verdict.message,
2147
+ body: refusalBody({
1961
2148
  errorCode: "request.body_shape",
1962
- ...(verdict.code !== undefined ? { ruleRejectCode: verdict.code } : {}),
1963
- },
2149
+ message: verdict.message,
2150
+ facts: {
2151
+ ...(verdict.code !== undefined ? { ruleRejectCode: verdict.code } : {}),
2152
+ },
2153
+ }),
1964
2154
  };
1965
2155
  }
1966
2156
  editedArmEcho(persistRule, persisted) {
@@ -1971,45 +2161,194 @@ export class ToolApprovalCoordinator {
1971
2161
  notPending(cause) {
1972
2162
  return {
1973
2163
  status: 404,
1974
- body: {
1975
- error: "no pending tool approval for this id (settled, expired, or not on this replica)",
2164
+ body: refusalBody({
1976
2165
  errorCode: "tool_approval.not_pending",
1977
- ...(cause !== undefined ? { cause } : {}),
1978
- },
2166
+ message: "no pending tool approval for this id (settled, expired, or not on this replica)",
2167
+ facts: {
2168
+ ...(cause !== undefined ? { cause } : {}),
2169
+ },
2170
+ }),
1979
2171
  };
1980
2172
  }
1981
- respondLate(id, principal, parsed, httpReq) {
2173
+ readApproval(id, principal) {
2174
+ const entry = this.liveEntryFor(id);
2175
+ if (entry?.adopted === true)
2176
+ return this.readAdopted(entry, id, principal);
2177
+ if (entry !== undefined && (entry.owner === null || entry.owner === principal)) {
2178
+ const body = { state: "pending", ...livePendingRowOf(entry) };
2179
+ return { status: 200, body };
2180
+ }
1982
2181
  const store = this.askStore;
1983
2182
  if (!store)
1984
2183
  return this.notPending();
1985
- const tomb = this.lookupParkTombstone(id);
1986
- if (tomb === undefined || (tomb.owner !== null && tomb.owner !== principal)) {
1987
- return this.notPending(NOT_PENDING_UNKNOWN_CAUSE);
2184
+ return this.readAskRowState(store, id, principal);
2185
+ }
2186
+ async readAdopted(entry, id, principal) {
2187
+ const store = this.askStore;
2188
+ if (!store)
2189
+ return this.notPending();
2190
+ const row = await this.askRowFor(store, id, "getAsk(read-adopted)");
2191
+ if (askRowVisibleTo(row, principal) && row.state === "STREAM_PENDING") {
2192
+ const body = { state: "pending", ...livePendingRowOf(entry) };
2193
+ return { status: 200, body };
1988
2194
  }
1989
- return this.dispatchLateByRow(store, tomb, id, parsed, httpReq, principal);
2195
+ return this.readStateOfRow(row, principal);
1990
2196
  }
1991
- async dispatchLateByRow(store, tomb, id, parsed, httpReq, principal) {
1992
- let row;
2197
+ async readAskRowState(store, id, principal) {
2198
+ return this.readStateOfRow(await this.askRowFor(store, id, "getAsk(read)"), principal);
2199
+ }
2200
+ async readParkedGate(row) {
2201
+ const gate = this.parkedGate;
2202
+ if (gate === undefined || row.gateToken === null)
2203
+ return { kind: "open" };
1993
2204
  try {
1994
- row = await this.withStoreDeadline(store.getAsk(tomb.askId), "getAsk(respond-late)", DURABLE_CONVERGE_READ_TIMEOUT_MS);
2205
+ return await this.withStoreDeadline(gate({ gateToken: row.gateToken, gateBoundCallId: row.gateBoundCallId }), "readParkedGate", DURABLE_CONVERGE_READ_TIMEOUT_MS);
1995
2206
  }
1996
2207
  catch (err) {
1997
- this.noteStoreError(err, "getAsk(respond-late)");
1998
- return this.notPending(NOT_PENDING_UNKNOWN_CAUSE);
2208
+ this.noteStoreError(err, "readParkedGate");
2209
+ return { kind: "open" };
1999
2210
  }
2000
- if (row === null)
2001
- return this.notPending(NOT_PENDING_UNKNOWN_CAUSE);
2002
- if (row.owner !== null && row.owner !== principal) {
2003
- defaultLogger.warn("tool approval late decision rejected: the ask row changed owner since it parked", { askId: row.askId });
2211
+ }
2212
+ async readStateOfRow(row, principal) {
2213
+ if (!askRowVisibleTo(row, principal))
2004
2214
  return this.notPending(NOT_PENDING_UNKNOWN_CAUSE);
2215
+ const projection = buildAskRowReadProjection(row, Date.now());
2216
+ let body;
2217
+ switch (row.state) {
2218
+ case "DECIDED": {
2219
+ const decided = decidedRowFacts(row);
2220
+ if (decided === undefined)
2221
+ return this.notPending();
2222
+ body = { state: "decided", ...projection, ...decided };
2223
+ break;
2224
+ }
2225
+ case "PARKING":
2226
+ case "PARKED": {
2227
+ const gate = row.state === "PARKED" ? await this.readParkedGate(row) : { kind: "open" };
2228
+ switch (gate.kind) {
2229
+ case "open": {
2230
+ const parkSession = parkSessionOf(row);
2231
+ body = { state: "parked", ...projection, ...(parkSession !== undefined ? { decidePath: approvalDecidePath(parkSession) } : {}) };
2232
+ break;
2233
+ }
2234
+ case "decided":
2235
+ body = { state: "decided", ...projection, decision: gate.decision, decidedAtMs: gate.decidedAtMs, ...(gate.decisionNote !== undefined ? { decisionNote: gate.decisionNote } : {}) };
2236
+ break;
2237
+ case "void":
2238
+ body = { state: "void", ...projection };
2239
+ break;
2240
+ case "unknown":
2241
+ if (gate.why === "decision_unreadable") {
2242
+ defaultLogger.error("tool approval read saw a PARKED row whose gate is settled but carries no usable decision", {
2243
+ askId: row.askId,
2244
+ defect: seatText("operator-log-text", gate.defects[0]),
2245
+ defectCount: gate.defects.length,
2246
+ });
2247
+ }
2248
+ return this.notPending();
2249
+ default: {
2250
+ const unhandled = gate;
2251
+ defaultLogger.error("tool approval read hit an unknown gate reading", { askId: row.askId, gate: unhandled });
2252
+ return this.notPending();
2253
+ }
2254
+ }
2255
+ break;
2256
+ }
2257
+ case "DENIED":
2258
+ case "VOID": {
2259
+ const rowCause = causeOfAskRow(row);
2260
+ body = { state: "void", ...projection, ...(rowCause?.kind === "ending" ? { cause: rowCause.cause } : {}) };
2261
+ break;
2262
+ }
2263
+ case "STREAM_PENDING":
2264
+ return this.notPending(NOT_PENDING_UNKNOWN_CAUSE);
2265
+ default: {
2266
+ const unhandled = row.state;
2267
+ defaultLogger.error("tool approval read hit an unknown ask state", { askId: row.askId, state: unhandled });
2268
+ return this.notPending();
2269
+ }
2270
+ }
2271
+ return { status: 200, body };
2272
+ }
2273
+ respondLate(id, principal, parsed, httpReq) {
2274
+ const store = this.askStore;
2275
+ if (!store)
2276
+ return this.notPending();
2277
+ return this.dispatchLateByRow(store, id, parsed, httpReq, principal);
2278
+ }
2279
+ watchPresentedCards(askIds, deliver) {
2280
+ const watcher = { deliver, askIds: new Set() };
2281
+ for (const askId of askIds) {
2282
+ if (!this.pendingByAskId.has(askId))
2283
+ continue;
2284
+ watcher.askIds.add(askId);
2285
+ let set = this.presentedCardWatchers.get(askId);
2286
+ if (set === undefined) {
2287
+ set = new Set();
2288
+ this.presentedCardWatchers.set(askId, set);
2289
+ }
2290
+ set.add(watcher);
2291
+ }
2292
+ return () => {
2293
+ for (const askId of watcher.askIds)
2294
+ this.dropPresentedCardWatcher(askId, watcher);
2295
+ watcher.askIds.clear();
2296
+ };
2297
+ }
2298
+ dropPresentedCardWatcher(askId, watcher) {
2299
+ const set = this.presentedCardWatchers.get(askId);
2300
+ if (set === undefined)
2301
+ return;
2302
+ set.delete(watcher);
2303
+ if (set.size === 0)
2304
+ this.presentedCardWatchers.delete(askId);
2305
+ }
2306
+ notifyPresentedCardWatchers(askId, frame) {
2307
+ const set = this.presentedCardWatchers.get(askId);
2308
+ if (set === undefined)
2309
+ return;
2310
+ this.presentedCardWatchers.delete(askId);
2311
+ for (const watcher of set) {
2312
+ watcher.askIds.delete(askId);
2313
+ try {
2314
+ watcher.deliver(frame);
2315
+ }
2316
+ catch (err) {
2317
+ defaultLogger.warn("presented-card completion notice could not be written to its connection", { askId, error: err instanceof Error ? err.message : String(err) });
2318
+ }
2005
2319
  }
2006
- if (tomb.redeemed !== undefined)
2007
- return this.replayRedeemReceipt(id, tomb, row, tomb.redeemed, parsed);
2320
+ }
2321
+ liveEntryFor(id) {
2322
+ const local = this.pending.get(id);
2323
+ if (local !== undefined)
2324
+ return local.askId === undefined ? local : undefined;
2325
+ for (const e of this.pendingByAskId.get(id) ?? [])
2326
+ return e;
2327
+ return undefined;
2328
+ }
2329
+ async dispatchLateByRow(store, id, parsed, httpReq, principal) {
2330
+ return this.dispatchRowTruth(await this.askRowFor(store, id, "getAsk(respond-late)"), id, parsed, httpReq, principal);
2331
+ }
2332
+ async askRowFor(store, id, label) {
2333
+ try {
2334
+ return await this.withStoreDeadline(store.getAsk(id), label, DURABLE_CONVERGE_READ_TIMEOUT_MS);
2335
+ }
2336
+ catch (err) {
2337
+ this.noteStoreError(err, label);
2338
+ return null;
2339
+ }
2340
+ }
2341
+ async dispatchRowTruth(row, id, parsed, httpReq, principal) {
2342
+ if (!askRowVisibleTo(row, principal))
2343
+ return this.notPending(NOT_PENDING_UNKNOWN_CAUSE);
2344
+ const receipt = this.lookupParkRedemption(row.askId);
2345
+ if (receipt?.redeemed !== undefined)
2346
+ return this.replayRedeemReceipt(id, receipt, row, receipt.redeemed, parsed);
2008
2347
  switch (row.state) {
2009
2348
  case "DECIDED":
2010
2349
  return this.replayDecidedAsk(id, row, parsed);
2011
2350
  case "PARKED":
2012
- return this.redeemParkedAsk(id, tomb, row, parsed, httpReq, principal);
2351
+ return this.redeemParkedAsk(id, row, parsed, httpReq, principal);
2013
2352
  case "PARKING": {
2014
2353
  const parkSession = parkSessionOf(row);
2015
2354
  return {
@@ -2036,7 +2375,7 @@ export class ToolApprovalCoordinator {
2036
2375
  return this.notPending(NOT_PENDING_UNKNOWN_CAUSE);
2037
2376
  default: {
2038
2377
  const unhandled = row.state;
2039
- defaultLogger.error("tool approval respond hit an unknown ask state", { askId: tomb.askId, state: unhandled });
2378
+ defaultLogger.error("tool approval respond hit an unknown ask state", { askId: row.askId, state: unhandled });
2040
2379
  return this.notPending();
2041
2380
  }
2042
2381
  }
@@ -2047,7 +2386,8 @@ export class ToolApprovalCoordinator {
2047
2386
  return { rulePersisted: false, ruleRefusal: "rule_lane_unavailable" };
2048
2387
  }
2049
2388
  replayDecidedAsk(id, row, parsed) {
2050
- if ((row.decision !== "approve" && row.decision !== "deny") || row.provisional || row.decidedAtMs === null) {
2389
+ const decided = decidedRowFacts(row);
2390
+ if (decided === undefined) {
2051
2391
  defaultLogger.error("tool approval late replay saw a DECIDED row that is not a usable human decision", {
2052
2392
  askId: row.askId,
2053
2393
  decision: row.decision,
@@ -2063,15 +2403,14 @@ export class ToolApprovalCoordinator {
2063
2403
  approvalId: id,
2064
2404
  askId: row.askId,
2065
2405
  idempotent: true,
2066
- decision: row.decision === "approve" ? "allow" : "deny",
2067
- decidedAtMs: row.decidedAtMs,
2068
- ...(row.decisionNote !== null ? { decisionNote: row.decisionNote } : {}),
2406
+ ...decided,
2069
2407
  ...(parsed.value === "allow_session" ? { rememberApplied: false } : {}),
2408
+ ...noteRecordedReceipt(parsed.note, noteLandedOnRow(parsed.note, row.decisionNote)),
2070
2409
  ...this.lateRuleRefusal(parsed),
2071
2410
  },
2072
2411
  };
2073
2412
  }
2074
- async redeemParkedAsk(id, tomb, row, parsed, httpReq, principal) {
2413
+ async redeemParkedAsk(id, row, parsed, httpReq, principal) {
2075
2414
  const redeem = this.parkedRedeem?.();
2076
2415
  if (redeem === undefined)
2077
2416
  return this.notPending();
@@ -2079,14 +2418,19 @@ export class ToolApprovalCoordinator {
2079
2418
  defaultLogger.error("tool approval parked row carries no gate token — nothing to redeem", { askId: row.askId });
2080
2419
  return this.notPending();
2081
2420
  }
2082
- if (tomb.redeemed !== undefined)
2083
- return this.replayRedeemReceipt(id, tomb, row, tomb.redeemed, parsed);
2084
- if (tomb.inflight !== undefined) {
2085
- const shared = await tomb.inflight;
2421
+ const prior = this.lookupParkRedemption(row.askId);
2422
+ if (prior?.redeemed !== undefined)
2423
+ return this.replayRedeemReceipt(id, prior, row, prior.redeemed, parsed);
2424
+ if (prior?.inflight !== undefined) {
2425
+ const shared = await prior.inflight;
2086
2426
  if (shared.status < 200 || shared.status >= 300)
2087
2427
  return shared;
2088
- return this.replayRedeemReceipt(id, tomb, row, { decision: shared.decision, body: shared.body, ...(shared.rule?.terminal === true ? { rule: shared.rule.body } : {}) }, parsed);
2428
+ return this.replayRedeemReceipt(id, prior, row, { decision: shared.decision, body: shared.body, ...(shared.rule?.terminal === true ? { rule: shared.rule.body } : {}) }, parsed);
2089
2429
  }
2430
+ const admission = this.parkRedemptionAdmission(row.owner);
2431
+ if (admission !== undefined)
2432
+ return admission;
2433
+ const memo = this.openParkRedemption(row.askId, row.owner);
2090
2434
  const decision = parsed.value === "deny" ? "deny" : "approve";
2091
2435
  const gateToken = row.gateToken;
2092
2436
  const flight = (async () => {
@@ -2107,25 +2451,29 @@ export class ToolApprovalCoordinator {
2107
2451
  }
2108
2452
  catch (err) {
2109
2453
  defaultLogger.error("tool approval parked redemption threw", { askId: row.askId, error: err instanceof Error ? err.message : String(err) });
2110
- return { status: 500, body: { error: "the parked approval could not be redeemed right now — retry", errorCode: "internal.error" }, decision };
2454
+ return { status: 500, body: refusalBody({ errorCode: "internal.error", message: "the parked approval could not be redeemed right now — retry" }), decision };
2111
2455
  }
2112
2456
  if (res.status < 200 || res.status >= 300)
2113
2457
  return { ...res, decision };
2114
2458
  const rule = await this.persistRuleFromRow(row, parsed);
2115
2459
  return { ...res, decision, ...(rule !== undefined ? { rule } : {}) };
2116
2460
  })();
2117
- tomb.inflight = flight;
2461
+ memo.inflight = flight;
2118
2462
  let out;
2119
2463
  try {
2120
2464
  out = await flight;
2121
2465
  }
2122
2466
  finally {
2123
- tomb.inflight = undefined;
2467
+ memo.inflight = undefined;
2124
2468
  }
2125
- if (out.status < 200 || out.status >= 300)
2469
+ if (out.status < 200 || out.status >= 300) {
2470
+ if (this.parkRedemptions.get(row.askId) === memo)
2471
+ this.parkRedemptions.delete(row.askId);
2126
2472
  return { status: out.status, body: out.body };
2473
+ }
2127
2474
  const rule = out.rule;
2128
- tomb.redeemed = { decision, body: out.body, ...(rule?.terminal === true ? { rule: rule.body } : {}) };
2475
+ if (this.retainParkRedemption(row.askId, memo))
2476
+ memo.redeemed = { decision, body: out.body, ...(rule?.terminal === true ? { rule: rule.body } : {}) };
2129
2477
  return {
2130
2478
  status: out.status,
2131
2479
  body: {
@@ -2216,24 +2564,24 @@ export class ToolApprovalCoordinator {
2216
2564
  return { ...flag(false, "rule_store_error"), terminal: false };
2217
2565
  }
2218
2566
  }
2219
- async replayRedeemReceipt(id, tomb, row, receipt, parsed) {
2220
- let rule = receipt.rule;
2221
- if (rule === undefined && parsed.persistRule !== undefined) {
2222
- const flight = tomb.ruleInflight ?? this.persistRuleFromRow(row, parsed);
2223
- const isOwner = tomb.ruleInflight === undefined;
2567
+ async replayRedeemReceipt(id, memo, row, receipt, parsed) {
2568
+ let rule = receipt.decision === "approve" ? receipt.rule : this.lateRuleRefusal(parsed);
2569
+ if (receipt.decision === "approve" && rule === undefined && parsed.persistRule !== undefined) {
2570
+ const flight = memo.ruleInflight ?? this.persistRuleFromRow(row, parsed);
2571
+ const isOwner = memo.ruleInflight === undefined;
2224
2572
  if (isOwner)
2225
- tomb.ruleInflight = flight;
2573
+ memo.ruleInflight = flight;
2226
2574
  let fresh;
2227
2575
  try {
2228
2576
  fresh = await flight;
2229
2577
  }
2230
2578
  finally {
2231
2579
  if (isOwner)
2232
- tomb.ruleInflight = undefined;
2580
+ memo.ruleInflight = undefined;
2233
2581
  }
2234
2582
  rule = fresh?.body;
2235
- if (fresh?.terminal === true && tomb.redeemed !== undefined)
2236
- tomb.redeemed.rule = fresh.body;
2583
+ if (fresh?.terminal === true && memo.redeemed !== undefined)
2584
+ memo.redeemed.rule = fresh.body;
2237
2585
  }
2238
2586
  return {
2239
2587
  status: 200,
@@ -2328,7 +2676,7 @@ export class ToolApprovalCoordinator {
2328
2676
  return withFlag(false, "rule_store_error");
2329
2677
  }
2330
2678
  }
2331
- async respondWithCas(askStore, askId, batchId, id, entry, parsed, ruleEligibility) {
2679
+ async respondWithCas(askStore, askId, batchId, entry, parsed, ruleEligibility) {
2332
2680
  let winnerRow;
2333
2681
  let ambiguousConfirmedRow;
2334
2682
  let noteLanded = false;
@@ -2347,7 +2695,7 @@ export class ToolApprovalCoordinator {
2347
2695
  noteLanded = true;
2348
2696
  }
2349
2697
  if (!decided.ok) {
2350
- return this.notPending();
2698
+ return { lost: true, row: decided.row };
2351
2699
  }
2352
2700
  }
2353
2701
  catch (err) {
@@ -2358,7 +2706,7 @@ export class ToolApprovalCoordinator {
2358
2706
  if (row?.state === "DECIDED" && row.decision === (parsed.value === "deny" ? "deny" : "approve")) {
2359
2707
  winnerRow = row;
2360
2708
  ambiguousConfirmedRow = row;
2361
- noteLanded = parsed.note === undefined || row.decisionNote === parsed.note;
2709
+ noteLanded = noteLandedOnRow(parsed.note, row.decisionNote);
2362
2710
  }
2363
2711
  }
2364
2712
  catch (confirmErr) {
@@ -2367,27 +2715,27 @@ export class ToolApprovalCoordinator {
2367
2715
  }
2368
2716
  const wantAllowed = parsed.value !== "deny";
2369
2717
  const decidedWon = winnerRow !== undefined;
2370
- if (this.pending.get(id) !== entry) {
2371
- if (winnerRow !== undefined)
2372
- this.notifyExternalDecision(askId, outcomeOfDecidedRow(winnerRow));
2373
- const converged = decidedWon && entry.settledOutcome === (wantAllowed ? SETTLE_OUTCOME.ALLOWED : SETTLE_OUTCOME.DENIED);
2718
+ if (entry.adopted === true && !decidedWon)
2719
+ return { lost: false, result: this.notPending() };
2720
+ if (this.pending.get(entry.registrationId) !== entry) {
2721
+ const othersReached = winnerRow !== undefined ? this.notifyExternalDecision(askId, outcomeOfDecidedRow(winnerRow)).settled : 0;
2722
+ const converged = decidedWon && (entry.adopted === true || entry.settledOutcome === (wantAllowed ? SETTLE_OUTCOME.ALLOWED : SETTLE_OUTCOME.DENIED));
2374
2723
  if (!converged) {
2375
- return this.notPending();
2724
+ return { lost: false, result: this.notPending() };
2376
2725
  }
2377
- return this.finishRespond(id, entry, parsed, { updatedInputDelivered: false, noteRecorded: noteLanded });
2726
+ return { lost: false, result: withConsumerDisclosure(this.finishRespond(entry, parsed, { updatedInputDelivered: false, noteRecorded: noteLanded }), waitingLegsReached(entry, othersReached)) };
2378
2727
  }
2379
2728
  const payloadIsMine = ambiguousConfirmedRow === undefined ||
2380
2729
  approvalPayloadFingerprint({ updatedInput: ambiguousConfirmedRow.updatedInput }) === approvalPayloadFingerprint({ updatedInput: parsed.updatedInput });
2381
- const result = this.finishRespond(id, entry, parsed, {
2730
+ const result = this.finishRespond(entry, parsed, {
2382
2731
  noteRecorded: noteLanded,
2383
2732
  ...(winnerRow !== undefined ? { settleOutcome: outcomeOfDecidedRow(winnerRow), settleOutcomeIsThisRequest: ambiguousConfirmedRow === undefined } : {}),
2384
- ...(ambiguousConfirmedRow !== undefined ? { updatedInputDelivered: payloadIsMine } : {}),
2733
+ ...(entry.adopted === true ? { updatedInputDelivered: false } : ambiguousConfirmedRow !== undefined ? { updatedInputDelivered: payloadIsMine } : {}),
2385
2734
  });
2386
- if (winnerRow !== undefined)
2387
- this.notifyExternalDecision(askId, outcomeOfDecidedRow(winnerRow));
2388
- return result;
2735
+ const othersReached = winnerRow !== undefined ? this.notifyExternalDecision(askId, outcomeOfDecidedRow(winnerRow)).settled : 0;
2736
+ return { lost: false, result: withConsumerDisclosure(result, waitingLegsReached(entry, othersReached)) };
2389
2737
  }
2390
- finishRespond(id, entry, parsed, opts) {
2738
+ finishRespond(entry, parsed, opts) {
2391
2739
  let rememberApplied;
2392
2740
  const home = standingAnswerHome(pendingEntryStandingAnswerFacts(entry));
2393
2741
  let grantable;
@@ -2430,42 +2778,146 @@ export class ToolApprovalCoordinator {
2430
2778
  return {
2431
2779
  status: 200,
2432
2780
  body: {
2433
- approvalId: id,
2781
+ approvalId: entry.approvalId,
2434
2782
  delivery: "applied",
2435
2783
  decision: parsed.value,
2436
2784
  ...(rememberApplied !== undefined ? { rememberApplied } : {}),
2437
2785
  ...(parsed.updatedInput !== undefined && allowed && updatedInputDelivered ? { updatedInputForwarded: true } : {}),
2438
- ...(parsed.note !== undefined ? { noteRecorded } : {}),
2786
+ ...noteRecordedReceipt(parsed.note, noteRecorded),
2439
2787
  },
2440
2788
  };
2441
2789
  }
2790
+ async adoptOpenAsks() {
2791
+ const store = this.askStore;
2792
+ if (!store)
2793
+ return { adopted: 0, alreadyLive: 0, cardUnreadable: 0, capped: false };
2794
+ let rows;
2795
+ try {
2796
+ rows = await this.withStoreDeadline(store.listByState("STREAM_PENDING", ADOPT_OPEN_ASKS_MAX + 1), "listByState(adopt)");
2797
+ }
2798
+ catch (err) {
2799
+ throw new Error(`approval ledger adoption: could not read the approval asks that are still pending in the ask ledger (${err instanceof Error ? err.message : String(err)}) — ` +
2800
+ "refusing to start, because serving now would hide asks that are still waiting for an answer from GET /v1/approvals. Check the database and start again.");
2801
+ }
2802
+ const capped = rows.length > ADOPT_OPEN_ASKS_MAX;
2803
+ const nowMs = Date.now();
2804
+ let adopted = 0;
2805
+ let alreadyLive = 0;
2806
+ let cardUnreadable = 0;
2807
+ for (const row of capped ? rows.slice(0, ADOPT_OPEN_ASKS_MAX) : rows) {
2808
+ if (this.pendingByAskId.has(row.askId)) {
2809
+ alreadyLive += 1;
2810
+ continue;
2811
+ }
2812
+ const replayFrame = buildReplayFrame(row, nowMs);
2813
+ if (replayFrame === undefined) {
2814
+ cardUnreadable += 1;
2815
+ continue;
2816
+ }
2817
+ this.registerAdopted(row, replayFrame);
2818
+ adopted += 1;
2819
+ }
2820
+ return { adopted, alreadyLive, cardUnreadable, capped };
2821
+ }
2822
+ registerAdopted(row, replayFrame) {
2823
+ const card = replayFrame.card;
2824
+ const registrationId = uuidv7();
2825
+ const facts = {
2826
+ ...(card.governanceForced === true ? { governanceForced: true } : {}),
2827
+ ...(card.risk.requiresRealApproval ? { requiresRealApproval: true } : {}),
2828
+ grantBinding: { kind: "unreadable" },
2829
+ ...(card.classes !== undefined ? { classes: card.classes } : {}),
2830
+ };
2831
+ const rememberRefused = standingAnswerHome(pendingEntryStandingAnswerFacts(facts)) === "none";
2832
+ const cardFrame = (card.rememberRefused === true) === rememberRefused
2833
+ ? replayFrame
2834
+ : { ...replayFrame, card: rememberRefused ? { ...card, rememberRefused: true } : (({ rememberRefused: _stale, ...rest }) => rest)(card) };
2835
+ let done = false;
2836
+ const leave = (ended) => {
2837
+ if (done)
2838
+ return;
2839
+ done = true;
2840
+ this.pending.delete(registrationId);
2841
+ entry.settledOutcome = ended?.outcome ?? SETTLE_OUTCOME.EXPIRED;
2842
+ const siblings = this.pendingByAskId.get(row.askId);
2843
+ siblings?.delete(entry);
2844
+ const lastRegistration = siblings !== undefined && siblings.size === 0;
2845
+ if (lastRegistration)
2846
+ this.pendingByAskId.delete(row.askId);
2847
+ if (ended !== undefined && lastRegistration) {
2848
+ this.notifyPresentedCardWatchers(row.askId, buildApprovalCompleteFrame({ askId: row.askId, taskId: row.taskId, approvalId: row.askId, outcome: ended.outcome, ...(ended.cause !== undefined ? { cause: ended.cause } : {}), parked: ended.parked, nowMs: Date.now() }));
2849
+ }
2850
+ };
2851
+ const release = () => leave(undefined);
2852
+ const entry = {
2853
+ adopted: true,
2854
+ cardFrame,
2855
+ release,
2856
+ settle: (verdict) => leave(verdict.by === "system" ? { outcome: SETTLE_OUTCOME.EXPIRED, cause: verdict.cause, parked: false } : { outcome: verdict.allowed ? SETTLE_OUTCOME.ALLOWED : SETTLE_OUTCOME.DENIED, parked: false }),
2857
+ forceParkNow: release,
2858
+ settleParkRoute: (cause) => leave({ outcome: SETTLE_OUTCOME.EXPIRED, cause, parked: this.unattendedRoutesToPark }),
2859
+ owner: row.owner,
2860
+ ...(row.sessionId !== row.taskId ? { sessionId: row.sessionId } : {}),
2861
+ ...(card.fromSubagent === true ? { originTaskId: row.sourceTaskId } : {}),
2862
+ toolName: card.toolName,
2863
+ ...facts,
2864
+ approvalId: row.askId,
2865
+ registrationId,
2866
+ registeredAtMs: row.createdAtMs,
2867
+ expiresAtMs: row.expiresAtMs,
2868
+ askId: row.askId,
2869
+ batchId: row.batchId,
2870
+ };
2871
+ this.pending.set(registrationId, entry);
2872
+ const siblings = this.pendingByAskId.get(row.askId) ?? new Set();
2873
+ siblings.add(entry);
2874
+ this.pendingByAskId.set(row.askId, siblings);
2875
+ }
2876
+ releaseAdoptedRegistrations(askId) {
2877
+ for (const e of [...(this.pendingByAskId.get(askId) ?? [])])
2878
+ if (e.adopted === true)
2879
+ e.release();
2880
+ }
2881
+ forgetAdoptedAsk(askId) {
2882
+ this.releaseAdoptedRegistrations(askId);
2883
+ if (this.pendingByAskId.has(askId))
2884
+ return;
2885
+ const watchers = this.presentedCardWatchers.get(askId);
2886
+ if (watchers === undefined)
2887
+ return;
2888
+ this.presentedCardWatchers.delete(askId);
2889
+ for (const watcher of watchers)
2890
+ watcher.askIds.delete(askId);
2891
+ }
2892
+ followAdoptedOwner(askId, owner) {
2893
+ for (const e of this.pendingByAskId.get(askId) ?? [])
2894
+ if (e.adopted === true)
2895
+ e.owner = owner;
2896
+ }
2897
+ waitingRegistrationCount(askId) {
2898
+ let n = 0;
2899
+ for (const e of this.pendingByAskId.get(askId) ?? [])
2900
+ if (e.adopted !== true)
2901
+ n += 1;
2902
+ return n;
2903
+ }
2442
2904
  listLivePending(scope) {
2443
2905
  const rows = [];
2444
- for (const [id, e] of this.pending) {
2445
- if (scope !== undefined && e.owner !== scope)
2906
+ const owner = scope === undefined ? undefined : ownerOfCheckpointScope(scope);
2907
+ const seen = new Set();
2908
+ for (const e of this.pending.values()) {
2909
+ if (owner !== undefined && e.owner !== owner)
2446
2910
  continue;
2447
- rows.push({
2448
- approvalId: id,
2449
- toolName: e.toolName,
2450
- ts: e.registeredAtMs,
2451
- expiresAtMs: e.expiresAtMs,
2452
- ...(e.sessionId !== undefined ? { sessionId: e.sessionId } : {}),
2453
- ...(e.requiresRealApproval ? { requiresRealApproval: true } : {}),
2454
- ...(e.governanceForced ? { governanceForced: true } : {}),
2455
- ...(e.mandated ? { mandated: true } : {}),
2456
- ...(standingAnswerHome(pendingEntryStandingAnswerFacts(e)) === "none" ? { rememberRefused: true } : {}),
2457
- ...(e.mandate !== undefined ? { mandate: e.mandate } : {}),
2458
- ...(e.origin !== undefined ? { origin: e.origin } : {}),
2459
- ...(e.originTaskId !== undefined ? { fromSubagent: true, originTaskId: e.originTaskId } : {}),
2460
- ...(e.frame.approvalCard !== undefined ? { approvalCard: e.frame.approvalCard } : {}),
2461
- frame: e.frame.approvalId === id ? e.frame : { ...e.frame, approvalId: id },
2462
- });
2911
+ if (seen.has(e.approvalId))
2912
+ continue;
2913
+ seen.add(e.approvalId);
2914
+ rows.push(livePendingRowOf(e));
2463
2915
  }
2464
2916
  return rows;
2465
2917
  }
2466
2918
  hasLivePendingFrom(originTaskId) {
2467
2919
  for (const e of this.pending.values())
2468
- if (e.originTaskId === originTaskId)
2920
+ if (e.originTaskId === originTaskId && e.adopted !== true)
2469
2921
  return true;
2470
2922
  return false;
2471
2923
  }
@@ -2473,27 +2925,42 @@ export class ToolApprovalCoordinator {
2473
2925
  const store = this.askStore;
2474
2926
  if (!store)
2475
2927
  return { polled: 0, settled: 0, backlog: 0, truncated: false };
2476
- const candidates = [];
2928
+ const waiting = [];
2929
+ const adopted = [];
2477
2930
  const seen = new Set();
2478
2931
  for (const entry of this.pending.values()) {
2479
- if (entry.suspended === true && entry.askId !== undefined && entry.settledOutcome === undefined && !seen.has(entry.askId)) {
2480
- seen.add(entry.askId);
2481
- candidates.push(entry.askId);
2482
- }
2483
- }
2484
- const start = candidates.length > 0 ? this.suspendedPollOffset % candidates.length : 0;
2485
- const ordered = [...candidates.slice(start), ...candidates.slice(0, start)];
2932
+ if (entry.askId === undefined || entry.settledOutcome !== undefined || seen.has(entry.askId))
2933
+ continue;
2934
+ if (entry.suspended === true)
2935
+ waiting.push(entry.askId);
2936
+ else if (entry.adopted === true)
2937
+ adopted.push(entry.askId);
2938
+ else
2939
+ continue;
2940
+ seen.add(entry.askId);
2941
+ }
2942
+ const waitingStart = waiting.length > 0 ? this.suspendedPollOffset % waiting.length : 0;
2943
+ const adoptedStart = adopted.length > 0 ? this.adoptedPollOffset % adopted.length : 0;
2944
+ const ordered = [
2945
+ ...[...waiting.slice(waitingStart), ...waiting.slice(0, waitingStart)].map((askId) => ({ askId, tier: "waiting" })),
2946
+ ...[...adopted.slice(adoptedStart), ...adopted.slice(0, adoptedStart)].map((askId) => ({ askId, tier: "adopted" })),
2947
+ ];
2486
2948
  const deadlineMs = Date.now() + SUSPENDED_POLL_BUDGET_MS;
2487
2949
  let settled = 0;
2488
2950
  let polled = 0;
2951
+ let polledWaiting = 0;
2952
+ let polledAdopted = 0;
2489
2953
  let truncated = false;
2490
- for (const askId of ordered) {
2954
+ for (const { askId, tier } of ordered) {
2491
2955
  if (polled >= SUSPENDED_POLL_MAX_PER_TICK || Date.now() >= deadlineMs) {
2492
2956
  truncated = true;
2493
2957
  break;
2494
2958
  }
2495
2959
  polled++;
2496
- this.suspendedPollOffset = start + polled;
2960
+ if (tier === "waiting")
2961
+ this.suspendedPollOffset = waitingStart + ++polledWaiting;
2962
+ else
2963
+ this.adoptedPollOffset = adoptedStart + ++polledAdopted;
2497
2964
  let row;
2498
2965
  try {
2499
2966
  row = await this.withStoreDeadline(store.getAsk(askId), "getAsk(poll-suspended)", DURABLE_CONVERGE_READ_TIMEOUT_MS);
@@ -2502,20 +2969,26 @@ export class ToolApprovalCoordinator {
2502
2969
  this.noteStoreError(err, "getAsk(poll-suspended)");
2503
2970
  continue;
2504
2971
  }
2505
- if (row === null || row.state === "STREAM_PENDING")
2972
+ if (row === null) {
2973
+ this.forgetAdoptedAsk(askId);
2974
+ continue;
2975
+ }
2976
+ if (row.state === "STREAM_PENDING") {
2977
+ this.followAdoptedOwner(askId, row.owner);
2506
2978
  continue;
2979
+ }
2507
2980
  if (this.editedDecisionInFlight(askId))
2508
2981
  continue;
2509
2982
  const rowCause = causeOfAskRow(row);
2510
2983
  if (rowCause?.kind === "ending") {
2511
- const before = this.pendingByAskId.get(askId)?.size ?? 0;
2984
+ const before = this.waitingRegistrationCount(askId);
2512
2985
  this.settleVoidedSiblings([askId], rowCause.cause);
2513
2986
  settled += before;
2514
2987
  continue;
2515
2988
  }
2516
2989
  const outcome = replayTerminalAskRow(row);
2517
2990
  if (outcome === "unavailable") {
2518
- const before = this.pendingByAskId.get(askId)?.size ?? 0;
2991
+ const before = this.waitingRegistrationCount(askId);
2519
2992
  const parkCause = (rowCause?.kind === "park_trigger" ? rowCause.cause : undefined) ?? (Date.now() >= row.expiresAtMs ? APPROVAL_ENDING_CAUSES.TTL_EXPIRED : APPROVAL_ENDING_CAUSES.SHUTDOWN_BEFORE_PARK);
2520
2993
  this.settleSameAskIdParkRoute(askId, parkCause);
2521
2994
  settled += before;
@@ -2524,7 +2997,7 @@ export class ToolApprovalCoordinator {
2524
2997
  const delivered = this.notifyExternalDecision(askId, outcome).settled;
2525
2998
  settled += delivered;
2526
2999
  }
2527
- const backlog = Math.max(0, candidates.length - polled);
3000
+ const backlog = Math.max(0, waiting.length + adopted.length - polled);
2528
3001
  if (truncated) {
2529
3002
  defaultLogger.warn("approval_suspended_poll_truncated", {
2530
3003
  polled,
@@ -2533,6 +3006,7 @@ export class ToolApprovalCoordinator {
2533
3006
  maxPerTick: SUSPENDED_POLL_MAX_PER_TICK,
2534
3007
  budgetMs: SUSPENDED_POLL_BUDGET_MS,
2535
3008
  resumeFromOffset: this.suspendedPollOffset,
3009
+ resumeAdoptedFromOffset: this.adoptedPollOffset,
2536
3010
  });
2537
3011
  }
2538
3012
  return { polled, settled, backlog, truncated };
@@ -2582,8 +3056,11 @@ export class ToolApprovalCoordinator {
2582
3056
  sessionAllowedCount() {
2583
3057
  return this.allowAllSessions.size;
2584
3058
  }
2585
- parkTombstoneCount() {
2586
- return this.parkTombstones.size;
3059
+ presentedCardWatcherCount() {
3060
+ return this.presentedCardWatchers.size;
3061
+ }
3062
+ parkRedemptionCount() {
3063
+ return this.parkRedemptions.size;
2587
3064
  }
2588
3065
  }
2589
3066
  //# sourceMappingURL=tool-approval.js.map