@sema-agent/server 7.150.0 → 7.152.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 (514) hide show
  1. package/MIGRATION.md +451 -0
  2. package/README.md +15 -3
  3. package/README.zh-CN.md +10 -2
  4. package/USAGE.md +144 -87
  5. package/deploy/sema-up/README.md +11 -4
  6. package/deploy/sema-up/chart/Chart.yaml +1 -1
  7. package/deploy/sema-up/chart/templates/NOTES.txt +5 -0
  8. package/deploy/sema-up/chart/templates/_helpers.tpl +15 -16
  9. package/deploy/sema-up/chart/templates/server-v4pro.yaml +5 -3
  10. package/deploy/sema-up/chart/templates/server.yaml +5 -3
  11. package/deploy/sema-up/chart/values-full-single.yaml +3 -1
  12. package/deploy/sema-up/chart/values.yaml +8 -1
  13. package/deploy/sema-up/cluster-up.sh +7 -6
  14. package/deploy/sema-up/kube-up.sh +6 -6
  15. package/deploy/sema-up/lib/flagshape.sh +36 -8
  16. package/deploy/sema-up/lib/helm-values-cluster.py +2 -0
  17. package/deploy/sema-up/lib/helm-values-kube.py +2 -0
  18. package/deploy/sema-up/machines.example.yaml +3 -1
  19. package/deploy/sema-up/sema-up.sh +22 -16
  20. package/dist/adoption/import.js +3 -3
  21. package/dist/approval-ask-audit-store.d.ts +1 -1
  22. package/dist/approval-ask-machine.d.ts +4 -10
  23. package/dist/approval-ask-retention.d.ts +1 -1
  24. package/dist/approval-card.d.ts +12 -14
  25. package/dist/approval-gate-currency.d.ts +9 -9
  26. package/dist/approval-gate-currency.js +3 -4
  27. package/dist/approval-hmac.d.ts +17 -1
  28. package/dist/approval-hmac.js +7 -1
  29. package/dist/approval-queue-kind.d.ts +1 -1
  30. package/dist/approval.d.ts +2 -2
  31. package/dist/approval.js +2 -2
  32. package/dist/auth-bridge.d.ts +20 -0
  33. package/dist/auth-bridge.js +9 -0
  34. package/dist/background-session-reap.d.ts +1 -1
  35. package/dist/bake-runner/main.d.ts +4 -2
  36. package/dist/bind-host.d.ts +3 -6
  37. package/dist/bind-host.js +4 -7
  38. package/dist/boot/budget-tracing.js +1 -1
  39. package/dist/boot/coordinators.d.ts +1 -1
  40. package/dist/boot/coordinators.js +9 -10
  41. package/dist/boot/device-lane.js +2 -0
  42. package/dist/boot/engine-lease.d.ts +5 -0
  43. package/dist/boot/engine-lease.js +1 -0
  44. package/dist/boot/execution-env.d.ts +9 -14
  45. package/dist/boot/execution-env.js +85 -22
  46. package/dist/boot/execution-lane.js +32 -5
  47. package/dist/boot/late-binds.d.ts +8 -4
  48. package/dist/boot/late-binds.js +7 -1
  49. package/dist/boot/leader.d.ts +31 -30
  50. package/dist/boot/leader.js +38 -28
  51. package/dist/boot/leg-path-adjudication.d.ts +40 -0
  52. package/dist/boot/leg-path-adjudication.js +18 -0
  53. package/dist/boot/memory-consolidation.d.ts +3 -2
  54. package/dist/boot/memory-consolidation.js +6 -6
  55. package/dist/boot/parked-revive-gate.js +3 -15
  56. package/dist/boot/reapers.d.ts +6 -1
  57. package/dist/boot/reapers.js +13 -2
  58. package/dist/boot/resolve-spec.d.ts +3 -3
  59. package/dist/boot/resolve-spec.js +43 -48
  60. package/dist/boot/runner-deps.d.ts +46 -35
  61. package/dist/boot/runner-deps.js +94 -91
  62. package/dist/boot/session-age-out.d.ts +36 -0
  63. package/dist/boot/session-age-out.js +101 -0
  64. package/dist/boot/session-faces.d.ts +5 -14
  65. package/dist/boot/session-faces.js +9 -5
  66. package/dist/boot/shutdown.d.ts +6 -0
  67. package/dist/boot/shutdown.js +10 -4
  68. package/dist/boot/stage-01-config.js +2 -0
  69. package/dist/boot/stage-02-web-admin-pull.js +4 -5
  70. package/dist/boot/stage-05-execution-env.d.ts +5 -3
  71. package/dist/boot/stage-05-execution-env.js +3 -2
  72. package/dist/boot/stage-06-runners.d.ts +9 -4
  73. package/dist/boot/stage-06-runners.js +24 -9
  74. package/dist/boot/stage-07-capability-layer.d.ts +8 -4
  75. package/dist/boot/stage-07-capability-layer.js +3 -2
  76. package/dist/boot/stage-08-reapers.d.ts +8 -4
  77. package/dist/boot/stage-08-reapers.js +4 -2
  78. package/dist/boot/stage-09-leader.d.ts +12 -5
  79. package/dist/boot/stage-09-leader.js +14 -5
  80. package/dist/boot/stage-10-http-server.d.ts +19 -5
  81. package/dist/boot/stage-10-http-server.js +14 -21
  82. package/dist/boot/stores.js +7 -4
  83. package/dist/boot/web-admin.js +5 -8
  84. package/dist/boot/workflow-orchestration.d.ts +7 -2
  85. package/dist/boot/workflow-orchestration.js +24 -7
  86. package/dist/bounded-rotation.d.ts +29 -0
  87. package/dist/bounded-rotation.js +24 -0
  88. package/dist/capabilities/approvals-push.d.ts +3 -3
  89. package/dist/capabilities/scenarios.d.ts +28 -13
  90. package/dist/capabilities/scenarios.js +2 -2
  91. package/dist/capability-facts.d.ts +43 -32
  92. package/dist/capability-facts.js +45 -51
  93. package/dist/card-rule-offers.d.ts +32 -0
  94. package/dist/card-rule-offers.js +130 -0
  95. package/dist/center-credential.d.ts +25 -6
  96. package/dist/center-credential.js +25 -5
  97. package/dist/config-catalog.d.ts +26 -7
  98. package/dist/config-catalog.js +26 -22
  99. package/dist/config-dir-trust.js +4 -2
  100. package/dist/config-invariants.d.ts +20 -8
  101. package/dist/config-invariants.js +55 -12
  102. package/dist/config-lkg.d.ts +5 -19
  103. package/dist/config-lkg.js +8 -62
  104. package/dist/config-provider.d.ts +1 -1
  105. package/dist/config-root-migration.d.ts +2 -2
  106. package/dist/config-types.d.ts +38 -42
  107. package/dist/config.d.ts +58 -7
  108. package/dist/config.js +90 -40
  109. package/dist/control-socket.d.ts +51 -0
  110. package/dist/control-socket.js +115 -0
  111. package/dist/deployment-governance.d.ts +2 -1
  112. package/dist/deployment-shape.d.ts +9 -1
  113. package/dist/deployment-shape.js +2 -1
  114. package/dist/device-protocol.d.ts +5 -4
  115. package/dist/device-protocol.js +1 -1
  116. package/dist/device-ws-hub.d.ts +7 -0
  117. package/dist/device-ws-hub.js +10 -3
  118. package/dist/device-ws-protocol.d.ts +63 -10
  119. package/dist/device-ws-protocol.js +56 -25
  120. package/dist/elicitation.js +2 -2
  121. package/dist/engine-identity.d.ts +19 -7
  122. package/dist/engine-identity.js +10 -2
  123. package/dist/entry-mode-clamp-notice.d.ts +2 -1
  124. package/dist/entry-problems.d.ts +40 -3
  125. package/dist/entry-problems.js +3 -1
  126. package/dist/execution-env-refusal.d.ts +29 -0
  127. package/dist/execution-env-refusal.js +8 -0
  128. package/dist/execution-lane-caps.d.ts +3 -1
  129. package/dist/field-conflict.d.ts +1 -1
  130. package/dist/file-history-capture.d.ts +11 -4
  131. package/dist/file-history-capture.js +9 -6
  132. package/dist/file-history-disclosure.d.ts +7 -5
  133. package/dist/file-history-disclosure.js +7 -5
  134. package/dist/fleet/drain-entry.d.ts +4 -0
  135. package/dist/fleet/engine-signal.d.ts +12 -29
  136. package/dist/fleet/engine-signal.js +1 -7
  137. package/dist/fleet-client.js +2 -5
  138. package/dist/gate-settlement.d.ts +1 -8
  139. package/dist/gate-settlement.js +0 -4
  140. package/dist/history-anchor.js +1 -1
  141. package/dist/hitl-delivery.d.ts +2 -3
  142. package/dist/hitl-delivery.js +0 -2
  143. package/dist/hooks/hook-llm.js +4 -2
  144. package/dist/hooks/hook-runner.d.ts +3 -2
  145. package/dist/hooks/hook-runner.js +18 -3
  146. package/dist/host-decision.d.ts +11 -11
  147. package/dist/host-lsp-manager.d.ts +2 -2
  148. package/dist/http/active-run-conflict.d.ts +35 -2
  149. package/dist/http/active-run-conflict.js +48 -19
  150. package/dist/http/admission.js +17 -2
  151. package/dist/http/approval-sweeps.d.ts +1 -1
  152. package/dist/http/approval-sweeps.js +10 -7
  153. package/dist/http/checkpoint-ctx.d.ts +4 -0
  154. package/dist/http/checkpoint-ctx.js +2 -0
  155. package/dist/http/dispatch.d.ts +8 -3
  156. package/dist/http/dispatch.js +59 -32
  157. package/dist/http/durable-run-leg.js +16 -17
  158. package/dist/http/health-reader.d.ts +16 -9
  159. package/dist/http/health-reader.js +2 -1
  160. package/dist/http/health-self-report.d.ts +9 -1
  161. package/dist/http/health-self-report.js +3 -1
  162. package/dist/http/owner-access.d.ts +33 -14
  163. package/dist/http/owner-access.js +40 -10
  164. package/dist/http/principal-gate.d.ts +49 -21
  165. package/dist/http/principal-gate.js +61 -11
  166. package/dist/http/request-channel.d.ts +23 -0
  167. package/dist/http/request-channel.js +9 -0
  168. package/dist/http/request-guards.js +17 -2
  169. package/dist/http/resume-ctx-read.js +42 -3
  170. package/dist/http/resume-legs.d.ts +4 -3
  171. package/dist/http/resume-legs.js +367 -117
  172. package/dist/http/route-ctx.d.ts +91 -15
  173. package/dist/http/route-ctx.js +17 -0
  174. package/dist/http/route-table.d.ts +5 -1
  175. package/dist/http/route-table.js +4 -0
  176. package/dist/http/routes/admin-config-refresh.js +3 -3
  177. package/dist/http/routes/admin-drain.d.ts +3 -0
  178. package/dist/http/routes/admin-drain.js +7 -7
  179. package/dist/http/routes/admin-memory-optout.d.ts +1 -1
  180. package/dist/http/routes/admin-memory-optout.js +3 -3
  181. package/dist/http/routes/adoption.js +3 -5
  182. package/dist/http/routes/agents-roster.js +2 -3
  183. package/dist/http/routes/approvals-assistant.d.ts +2 -1
  184. package/dist/http/routes/approvals-assistant.js +99 -51
  185. package/dist/http/routes/background.d.ts +3 -3
  186. package/dist/http/routes/capabilities.d.ts +4 -0
  187. package/dist/http/routes/capabilities.js +38 -10
  188. package/dist/http/routes/config-catalog.js +2 -2
  189. package/dist/http/routes/devices.js +1 -1
  190. package/dist/http/routes/diagnostics.d.ts +3 -3
  191. package/dist/http/routes/diagnostics.js +19 -7
  192. package/dist/http/routes/fleet.js +10 -8
  193. package/dist/http/routes/images.js +10 -10
  194. package/dist/http/routes/memory-bundle.js +3 -3
  195. package/dist/http/routes/memory-compliance.js +3 -3
  196. package/dist/http/routes/memory-consolidation.js +2 -2
  197. package/dist/http/routes/memory-origin.js +3 -3
  198. package/dist/http/routes/memory-policy.js +2 -2
  199. package/dist/http/routes/observability.js +3 -3
  200. package/dist/http/routes/projects.d.ts +3 -2
  201. package/dist/http/routes/projects.js +4 -5
  202. package/dist/http/routes/retention-ops.js +3 -3
  203. package/dist/http/routes/rules.js +5 -5
  204. package/dist/http/routes/run-permission-mode.d.ts +2 -1
  205. package/dist/http/routes/run-permission-mode.js +2 -2
  206. package/dist/http/routes/runs.d.ts +4 -9
  207. package/dist/http/routes/runs.js +171 -65
  208. package/dist/http/routes/session-sync.d.ts +22 -9
  209. package/dist/http/routes/session-sync.js +204 -107
  210. package/dist/http/routes/sessions-list.js +13 -7
  211. package/dist/http/routes/sessions.js +30 -15
  212. package/dist/http/routes/tasks.js +46 -41
  213. package/dist/http/routes/trace-usage.js +2 -2
  214. package/dist/http/routes/workflows.js +1 -1
  215. package/dist/http/server.d.ts +30 -30
  216. package/dist/http/server.js +16 -4
  217. package/dist/http/submit-ctx.d.ts +1 -1
  218. package/dist/http/submit-ctx.js +7 -4
  219. package/dist/http/wire-types.d.ts +14 -4
  220. package/dist/human-facing-args.d.ts +33 -0
  221. package/dist/human-facing-args.js +33 -0
  222. package/dist/identity-source.d.ts +20 -3
  223. package/dist/identity-source.js +14 -0
  224. package/dist/index.d.ts +5 -1
  225. package/dist/k8s-api-credentials.d.ts +29 -0
  226. package/dist/k8s-api-credentials.js +25 -0
  227. package/dist/lane-home-probe.d.ts +3 -7
  228. package/dist/lane-home-probe.js +2 -2
  229. package/dist/leader/diffup.d.ts +19 -0
  230. package/dist/leader/diffup.js +7 -0
  231. package/dist/leader/leader.d.ts +116 -11
  232. package/dist/leader/leader.js +69 -5
  233. package/dist/leader/repair-wire.js +2 -2
  234. package/dist/leader/run-entry.d.ts +2 -1
  235. package/dist/leader/run-entry.js +1 -1
  236. package/dist/leader/wire.d.ts +42 -132
  237. package/dist/leader/wire.js +81 -67
  238. package/dist/leg-facts.d.ts +13 -5
  239. package/dist/leg-facts.js +2 -1
  240. package/dist/memory-scope.d.ts +23 -11
  241. package/dist/memory-scope.js +18 -6
  242. package/dist/memory-sync-client.d.ts +30 -2
  243. package/dist/memory-sync-client.js +16 -9
  244. package/dist/model-entry-headers.d.ts +9 -10
  245. package/dist/model-entry-headers.js +0 -4
  246. package/dist/model-entry-refusal.d.ts +7 -4
  247. package/dist/model-entry-refusal.js +11 -5
  248. package/dist/model-plane-face.d.ts +1 -1
  249. package/dist/model-plane-face.js +3 -1
  250. package/dist/model-route-endpoint.d.ts +13 -7
  251. package/dist/model-route-endpoint.js +14 -11
  252. package/dist/model-select.d.ts +22 -0
  253. package/dist/model-select.js +9 -0
  254. package/dist/model-unreachable-notice.d.ts +11 -0
  255. package/dist/model-unreachable-notice.js +4 -4
  256. package/dist/object-store-buckets.d.ts +640 -6
  257. package/dist/object-store-buckets.js +166 -0
  258. package/dist/observability/err-text.d.ts +7 -0
  259. package/dist/observability/err-text.js +5 -0
  260. package/dist/observability/fail-open.d.ts +25 -17
  261. package/dist/observability/fail-open.js +25 -17
  262. package/dist/observability/logger.js +3 -1
  263. package/dist/observability/metrics.js +1 -0
  264. package/dist/operator-roster.d.ts +68 -0
  265. package/dist/operator-roster.js +32 -0
  266. package/dist/orchestration/workflow-notify-journal.d.ts +18 -1
  267. package/dist/orchestration/workflow-notify-journal.js +12 -1
  268. package/dist/orchestration/workflow-park-decision.d.ts +70 -18
  269. package/dist/orchestration/workflow-park-decision.js +49 -10
  270. package/dist/orchestration/workflow-park-holder.d.ts +65 -0
  271. package/dist/orchestration/workflow-park-holder.js +98 -0
  272. package/dist/orchestration/workflow-record-fence.d.ts +36 -0
  273. package/dist/orchestration/workflow-record-fence.js +42 -0
  274. package/dist/outbound-dispatcher.d.ts +71 -8
  275. package/dist/outbound-dispatcher.js +48 -7
  276. package/dist/owner-faces.d.ts +2 -2
  277. package/dist/parked-decide.d.ts +106 -55
  278. package/dist/parked-decide.js +153 -117
  279. package/dist/peer-directory.d.ts +2 -2
  280. package/dist/peer-inbound-face.d.ts +2 -6
  281. package/dist/peer-inbound-face.js +0 -3
  282. package/dist/permission-mode-admission.d.ts +74 -5
  283. package/dist/permission-mode-admission.js +56 -3
  284. package/dist/plugins/adoption-log-sql.d.ts +3 -1
  285. package/dist/plugins/adoption-log-sql.js +7 -1
  286. package/dist/plugins/approval-ask-store-file.d.ts +14 -2
  287. package/dist/plugins/approval-ask-store-file.js +11 -0
  288. package/dist/plugins/approval-ask-store-memory.d.ts +10 -1
  289. package/dist/plugins/approval-ask-store-memory.js +35 -10
  290. package/dist/plugins/approval-ask-store-sql.d.ts +69 -4
  291. package/dist/plugins/approval-ask-store-sql.js +45 -5
  292. package/dist/plugins/approval-exemption-store.js +3 -0
  293. package/dist/plugins/approval-nonce-store.js +2 -0
  294. package/dist/plugins/blob-backend.d.ts +83 -7
  295. package/dist/plugins/blob-backend.js +57 -6
  296. package/dist/plugins/breaker-state-sql.js +2 -0
  297. package/dist/plugins/caching-session-store.d.ts +9 -15
  298. package/dist/plugins/caching-session-store.js +6 -0
  299. package/dist/plugins/checkpoint-index-columns.d.ts +20 -16
  300. package/dist/plugins/checkpoint-index-columns.js +3 -6
  301. package/dist/plugins/checkpoint-store-face.d.ts +119 -61
  302. package/dist/plugins/checkpoint-store-face.js +134 -55
  303. package/dist/plugins/device-store-sql.js +6 -0
  304. package/dist/plugins/error-origin.d.ts +18 -0
  305. package/dist/plugins/error-origin.js +10 -0
  306. package/dist/plugins/execution-placement-sql.js +6 -6
  307. package/dist/plugins/image-bake-store-sql.js +6 -0
  308. package/dist/plugins/image-index-sql.js +4 -0
  309. package/dist/plugins/landed-attendants.d.ts +212 -0
  310. package/dist/plugins/landed-attendants.js +161 -0
  311. package/dist/plugins/leader-run-store-sql.js +4 -0
  312. package/dist/plugins/local-checkpoint-store.d.ts +13 -3
  313. package/dist/plugins/local-checkpoint-store.js +38 -49
  314. package/dist/plugins/local-session-live-reference.d.ts +40 -0
  315. package/dist/plugins/local-session-live-reference.js +39 -0
  316. package/dist/plugins/local-session-store.d.ts +71 -33
  317. package/dist/plugins/local-session-store.js +129 -27
  318. package/dist/plugins/memory-engine-pg.js +4 -0
  319. package/dist/plugins/memory-engine-sql-apply.js +5 -2
  320. package/dist/plugins/memory-engine-tidb.js +4 -0
  321. package/dist/plugins/memory-optout-grant-store-sql.js +4 -0
  322. package/dist/plugins/memory-sync-store-pg.js +5 -0
  323. package/dist/plugins/memory-sync-store-tidb.js +5 -0
  324. package/dist/plugins/one-time-migrations.d.ts +104 -80
  325. package/dist/plugins/one-time-migrations.js +226 -132
  326. package/dist/plugins/org-delivery-quota-store.js +4 -1
  327. package/dist/plugins/outcome-ledger-sql.js +2 -0
  328. package/dist/plugins/permission-rule-store-sql.js +11 -4
  329. package/dist/plugins/pg-cost-quota.js +4 -0
  330. package/dist/plugins/pg-pool.js +35 -10
  331. package/dist/plugins/pg-rate-limiter.js +2 -0
  332. package/dist/plugins/posix-shell-fs.d.ts +6 -1
  333. package/dist/plugins/posix-shell-fs.js +4 -1
  334. package/dist/plugins/remote-env-adb.d.ts +19 -15
  335. package/dist/plugins/remote-env-adb.js +41 -15
  336. package/dist/plugins/remote-env-connect-seat.d.ts +105 -0
  337. package/dist/plugins/remote-env-connect-seat.js +72 -0
  338. package/dist/plugins/remote-env-device.d.ts +22 -5
  339. package/dist/plugins/remote-env-device.js +106 -8
  340. package/dist/plugins/remote-env-e2b.d.ts +15 -6
  341. package/dist/plugins/remote-env-e2b.js +42 -32
  342. package/dist/plugins/remote-env-file-error.js +6 -2
  343. package/dist/plugins/remote-env-host.d.ts +46 -10
  344. package/dist/plugins/remote-env-host.js +124 -52
  345. package/dist/plugins/remote-env-k8s.d.ts +109 -41
  346. package/dist/plugins/remote-env-k8s.js +160 -54
  347. package/dist/plugins/remote-env-local-docker.d.ts +13 -8
  348. package/dist/plugins/remote-env-local-docker.js +34 -41
  349. package/dist/plugins/remote-env-ssh.d.ts +18 -12
  350. package/dist/plugins/remote-env-ssh.js +60 -59
  351. package/dist/plugins/remote-shell.d.ts +98 -1
  352. package/dist/plugins/remote-shell.js +218 -0
  353. package/dist/plugins/replica-registration.d.ts +1 -1
  354. package/dist/plugins/replica-registration.js +1 -1
  355. package/dist/plugins/resume-anchor-store-sql.d.ts +11 -2
  356. package/dist/plugins/resume-anchor-store-sql.js +12 -5
  357. package/dist/plugins/retention-lane-store-sql.js +4 -0
  358. package/dist/plugins/retention-sql.d.ts +20 -2
  359. package/dist/plugins/retention-sql.js +11 -0
  360. package/dist/plugins/run-store-sql.d.ts +21 -2
  361. package/dist/plugins/run-store-sql.js +31 -7
  362. package/dist/plugins/s3-presign.d.ts +23 -0
  363. package/dist/plugins/s3-presign.js +37 -3
  364. package/dist/plugins/scenario-prefix-switch-m42.d.ts +1 -1
  365. package/dist/plugins/scenario-prefix-switch-m42.js +2 -2
  366. package/dist/plugins/scheduler-support.d.ts +2 -2
  367. package/dist/plugins/scheduler-support.js +0 -3
  368. package/dist/plugins/schema-consistency.d.ts +4 -2
  369. package/dist/plugins/schema-consistency.js +19 -2
  370. package/dist/plugins/schema-indexes.js +2 -0
  371. package/dist/plugins/send-file-ledger.d.ts +2 -2
  372. package/dist/plugins/send-file-ledger.js +5 -2
  373. package/dist/plugins/send-user-file.d.ts +3 -4
  374. package/dist/plugins/send-user-file.js +56 -52
  375. package/dist/plugins/serving-requirement.d.ts +26 -7
  376. package/dist/plugins/serving-requirement.js +11 -4
  377. package/dist/plugins/session-birth.d.ts +6 -2
  378. package/dist/plugins/session-blob-claim-token.d.ts +15 -0
  379. package/dist/plugins/session-blob-claim-token.js +20 -0
  380. package/dist/plugins/session-blob-namespace.d.ts +46 -0
  381. package/dist/plugins/session-blob-namespace.js +116 -0
  382. package/dist/plugins/session-capture-record-store-sql.js +2 -0
  383. package/dist/plugins/session-ext-table.d.ts +14 -1
  384. package/dist/plugins/session-ext-table.js +5 -1
  385. package/dist/plugins/session-row-sql.d.ts +49 -2
  386. package/dist/plugins/session-row-sql.js +56 -13
  387. package/dist/plugins/session-store-sql.d.ts +34 -18
  388. package/dist/plugins/session-store-sql.js +98 -17
  389. package/dist/plugins/shared-memory-store-sql.d.ts +1 -0
  390. package/dist/plugins/shared-memory-store-sql.js +6 -2
  391. package/dist/plugins/snapshot-bucket-presence.d.ts +25 -2
  392. package/dist/plugins/snapshot-bucket-presence.js +8 -2
  393. package/dist/plugins/sql-driver.d.ts +2 -1
  394. package/dist/plugins/sql-driver.js +18 -3
  395. package/dist/plugins/sql-errors.d.ts +11 -6
  396. package/dist/plugins/sql-errors.js +5 -6
  397. package/dist/plugins/sql-json-column.d.ts +1 -1
  398. package/dist/plugins/stop-then-start-sentence.d.ts +10 -4
  399. package/dist/plugins/stop-then-start-sentence.js +14 -10
  400. package/dist/plugins/stop-then-start.d.ts +109 -0
  401. package/dist/plugins/stop-then-start.js +104 -0
  402. package/dist/plugins/store-backend.d.ts +84 -15
  403. package/dist/plugins/store-backend.js +236 -72
  404. package/dist/plugins/store-boundary.d.ts +57 -0
  405. package/dist/plugins/store-boundary.js +139 -0
  406. package/dist/plugins/store-contracts.d.ts +17 -0
  407. package/dist/plugins/store-fault-sentence-m53.d.ts +7 -0
  408. package/dist/plugins/store-fault-sentence-m53.js +25 -0
  409. package/dist/plugins/store-fault.d.ts +26 -7
  410. package/dist/plugins/store-fault.js +18 -2
  411. package/dist/plugins/task-attachment-store.d.ts +2 -1
  412. package/dist/plugins/task-attachment-store.js +14 -1
  413. package/dist/plugins/task-list-store-sql.d.ts +2 -2
  414. package/dist/plugins/task-list-store-sql.js +10 -8
  415. package/dist/plugins/tidb-cost-quota.js +4 -0
  416. package/dist/plugins/tidb-pool.js +21 -25
  417. package/dist/plugins/tidb-rate-limiter.js +4 -0
  418. package/dist/plugins/user-input-ledger-sql.js +4 -0
  419. package/dist/plugins/workflow-run-store-sql.d.ts +28 -10
  420. package/dist/plugins/workflow-run-store-sql.js +64 -4
  421. package/dist/plugins/workspace-custody.d.ts +4 -3
  422. package/dist/plugins/workspace-fill.d.ts +4 -7
  423. package/dist/plugins/workspace-fill.js +2 -1
  424. package/dist/plugins/write-behind-counter.js +5 -0
  425. package/dist/project-context.d.ts +0 -2
  426. package/dist/project-context.js +4 -8
  427. package/dist/project-registry.d.ts +46 -0
  428. package/dist/project-registry.js +44 -0
  429. package/dist/question.d.ts +2 -1
  430. package/dist/question.js +5 -13
  431. package/dist/request-form.d.ts +6 -1
  432. package/dist/request-form.js +4 -1
  433. package/dist/request-key-closure.js +1 -1
  434. package/dist/request-value-closure.d.ts +4 -0
  435. package/dist/request-value-closure.js +10 -0
  436. package/dist/rule-issue-reading.d.ts +1 -1
  437. package/dist/rules-consent.d.ts +1 -1
  438. package/dist/run-acceptance.d.ts +3 -2
  439. package/dist/run-acceptance.js +4 -6
  440. package/dist/run-local.js +9 -9
  441. package/dist/runs.d.ts +54 -16
  442. package/dist/runs.js +67 -13
  443. package/dist/runtime-governance.d.ts +5 -4
  444. package/dist/security.d.ts +30 -32
  445. package/dist/security.js +41 -7
  446. package/dist/sema-config-dir.d.ts +2 -1
  447. package/dist/server-secret-env.d.ts +12 -10
  448. package/dist/server-secret-env.js +24 -15
  449. package/dist/session-bindings.d.ts +82 -0
  450. package/dist/session-bindings.js +18 -0
  451. package/dist/session-children-retire.d.ts +2 -2
  452. package/dist/session-children-retire.js +2 -6
  453. package/dist/session-deletion.d.ts +16 -1
  454. package/dist/session-error-wire.d.ts +3 -3
  455. package/dist/session-error-wire.js +3 -3
  456. package/dist/session-incarnation.d.ts +10 -0
  457. package/dist/session-incarnation.js +10 -1
  458. package/dist/session-kind.d.ts +57 -0
  459. package/dist/session-kind.js +22 -0
  460. package/dist/session-row.d.ts +21 -1
  461. package/dist/session-row.js +7 -1
  462. package/dist/session-sync-kernel.d.ts +24 -14
  463. package/dist/session-sync-kernel.js +3 -6
  464. package/dist/session-sync.d.ts +25 -103
  465. package/dist/session-sync.js +14 -76
  466. package/dist/session-titler.d.ts +10 -3
  467. package/dist/session-titler.js +19 -6
  468. package/dist/ssh-host-key.d.ts +5 -0
  469. package/dist/ssh-host-key.js +3 -0
  470. package/dist/stores-sql/blob-index-sql.d.ts +30 -9
  471. package/dist/stores-sql/blob-index-sql.js +40 -21
  472. package/dist/stores-sql/checkpoint-store-sql.d.ts +54 -27
  473. package/dist/stores-sql/checkpoint-store-sql.js +70 -146
  474. package/dist/stores-sql/file-history-store-sql.d.ts +113 -15
  475. package/dist/stores-sql/file-history-store-sql.js +205 -95
  476. package/dist/stores-sql/pg-session-storage.d.ts +34 -7
  477. package/dist/stores-sql/pg-session-storage.js +37 -13
  478. package/dist/stores-sql/retention-store-sql.d.ts +1 -9
  479. package/dist/stores-sql/retention-store-sql.js +5 -19
  480. package/dist/stores-sql/session-policy-store-sql.d.ts +30 -7
  481. package/dist/stores-sql/session-policy-store-sql.js +61 -44
  482. package/dist/stores-sql/session-write-hooks.d.ts +11 -0
  483. package/dist/stores-sql/tidb-session-store.d.ts +58 -7
  484. package/dist/stores-sql/tidb-session-store.js +37 -13
  485. package/dist/submission-exit.d.ts +3 -2
  486. package/dist/submission-exit.js +9 -1
  487. package/dist/task-cwd.d.ts +2 -2
  488. package/dist/task-settings.d.ts +7 -5
  489. package/dist/task-settings.js +3 -3
  490. package/dist/terminal.d.ts +33 -2
  491. package/dist/terminal.js +139 -9
  492. package/dist/tool-approval.d.ts +465 -507
  493. package/dist/tool-approval.js +606 -1222
  494. package/dist/trace/core-keyset-guard.d.ts +1 -1
  495. package/dist/trace/ledger-events.d.ts +3 -3
  496. package/dist/trace/project.d.ts +2 -3
  497. package/dist/trace/redact.js +3 -2
  498. package/dist/trace/self-minted-wire.d.ts +571 -178
  499. package/dist/trace/self-minted-wire.js +189 -56
  500. package/dist/web-admin-client/apply-effective.js +2 -2
  501. package/dist/web-admin-client/read-warnings.d.ts +2 -2
  502. package/dist/web-admin-client/read-warnings.js +1 -1
  503. package/dist/web-admin-client/types.d.ts +1 -1
  504. package/dist/workspace-space.d.ts +10 -4
  505. package/dist/workspace-space.js +4 -1
  506. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +79 -28
  507. package/package.json +7 -3
  508. package/scripts/embed-minimal.mjs +3 -3
  509. package/wire-changes/server@7.151.0.json +762 -0
  510. package/wire-changes/server@7.152.0.json +900 -0
  511. package/dist/config-d-migration.d.ts +0 -37
  512. package/dist/config-d-migration.js +0 -60
  513. package/dist/http/session-inherit-env.d.ts +0 -36
  514. package/dist/http/session-inherit-env.js +0 -6
@@ -1,21 +1,21 @@
1
1
  import { readApprovalCard } from "./approval-content-kind.js";
2
- import { heldAskOf, liveHostSource, readDecider, retiredDecisionKeyFacts, POLICY_CANNOT_ALLOW_PERSON_ASK } from "./host-decision.js";
2
+ import { liveHostSource } from "./host-decision.js";
3
3
  import { AsyncLocalStorage } from "node:async_hooks";
4
- import { uuidv7, classifyAsk, foldArbitrationRow, policyCannotAnswer, screenAskClassMembers, MAX_RULE_TEXT_CHARS, READ_ROOT_CANDIDATE_DIR_MAX, parseRuleText, pathConfinableWriteToolNames } from "@sema-agent/core";
4
+ import { uuidv7, classifyAsk, foldArbitrationRow, screenAskClassMembers, MAX_RULE_TEXT_CHARS, READ_ROOT_CANDIDATE_DIR_MAX, pathConfinableWriteToolNames } from "@sema-agent/core";
5
5
  import { precheckCardRuleText } from "./rules-consent.js";
6
- import { CARD_RULE_FORM_OF_MATCH, cardRuleFormOf } from "./permission-rule-vocab.js";
6
+ import { cardBindingOf, readCardRuleOffers } from "./card-rule-offers.js";
7
+ import { displayRuleOfferText } from "./plugins/checkpoint-store-face.js";
7
8
  import { redactDeep, redactSecrets, redactSecretsWithArms } from "./trace/redact.js";
8
9
  import { seatText } from "./trace/exit-seat.js";
10
+ import { humanFacingArgs } from "./human-facing-args.js";
9
11
  import { hasBidiControls } from "./text-bidi.js";
10
- import { approvalPayloadFingerprint } from "./plugins/approval-payload-fingerprint.js";
11
12
  import { createLogger } from "./observability/logger.js";
12
- import { errorKindOf } from "./observability/err-text.js";
13
13
  import { createHitlOpenRace, deliverHitlNotice } from "./hitl-delivery.js";
14
14
  import { recordFailOpen } from "./observability/fail-open.js";
15
15
  import { ownerOfCheckpointScope } from "./security.js";
16
16
  import { keyAdmits, ownerCellOf, recoveredSubject } from "./owner-faces.js";
17
- import { deriveAskId, deriveAskScopedBatchId, deriveBatchId, MAX_DECISION_NOTE_CHARS, noteLandedOnRow, noteRecordedReceipt, readBoundInputHash } from "./approval-ask-machine.js";
18
- import { askDemandsAPerson, storedPersonDemandRecord, currentParkArm, MAX_RULE_OFFERS, buildApprovalCard, buildApprovalCardEnvelope, adoptStoredLiveFacts, buildApprovalRequestFrame, buildReplayFrame, buildRevokeFrame, readDenialLimitFallback, readProbeCause, readProbeReason, readReadRootCandidate, readStoredCardEnvelope, readRuleEvidence, readAllowRuleShadowed, readRuleOffersAbsence, readRuleOffersAbsenceSeat, readParkRowTrueSeat, readAskClassPair, readAskReasons, readClosedWordSeat, buildApprovalCompleteFrame, readRuleOfferList, } from "./approval-card.js";
17
+ import { deriveAskId, deriveAskScopedBatchId, deriveBatchId, readBoundInputHash } from "./approval-ask-machine.js";
18
+ import { askDemandsAPerson, storedPersonDemandRecord, currentParkArm, buildApprovalCard, buildApprovalCardEnvelope, adoptStoredLiveFacts, buildApprovalRequestFrame, buildReplayFrame, buildRevokeFrame, readDenialLimitFallback, readProbeCause, readProbeReason, readReadRootCandidate, readStoredCardEnvelope, readRuleEvidence, readAllowRuleShadowed, readRuleOffersAbsence, readRuleOffersAbsenceSeat, readParkRowTrueSeat, readAskClassPair, readAskReasons, readClosedWordSeat, buildApprovalCompleteFrame, storedCardDemandsAPerson } from "./approval-card.js";
19
19
  import { askRowCarriesOwner, POLICY_REFUSAL_DECISION, projectAskTerminalClaimTruth } from "./plugins/approval-ask-store-sql.js";
20
20
  import { APPROVAL_ENDING_CAUSES, SETTLE_OUTCOME, askRowEndingCause, askRowParkTrigger } from "./approval-deny-reasons.js";
21
21
  import { settleByPolicy } from "./approval-policy-settlement.js";
@@ -25,10 +25,9 @@ import { agreedGrantBinding, grantBindingOf } from "./standing-answer-grant.js";
25
25
  import { operatorAsk } from "./operator-ask.js";
26
26
  import { APPROVAL_EXEMPTED_EVENT } from "./plugins/approval-exemption-store.js";
27
27
  import { refusalBody } from "./http/refusal-body.js";
28
- import { withoutSettlementEvidence } from "./gate-settlement.js";
29
28
  import { extensionRequesterOf } from "./trace/extension-call-face.js";
29
+ import { boundedRotation } from "./bounded-rotation.js";
30
30
  const defaultLogger = createLogger();
31
- const MAX_APPROVAL_ARGS_BYTES = 16384;
32
31
  const DEFAULT_APPROVAL_TTL_MS = 5 * 60_000;
33
32
  const DEFAULT_UNREACHED_ASK_TTL_MS = 60 * 60_000;
34
33
  const SUSPENDED_POLL_MAX_PER_TICK = 64;
@@ -41,10 +40,8 @@ export const MAX_ALLOW_SESSIONS = 4096;
41
40
  const DEFAULT_ADMIT_MAX_PER_TASK = 32;
42
41
  const DEFAULT_ADMIT_MAX_PER_OWNER = 256;
43
42
  const DEFAULT_PARK_REDEMPTION_MAX = 2000;
44
- const DEFAULT_PARK_REDEMPTION_TTL_MS = 60 * 60_000;
45
43
  const DEFAULT_PARK_REDEMPTION_PER_PRINCIPAL_MAX = 128;
46
44
  const PARK_REDEMPTION_ADMISSION_RETRY_AFTER_SEC = 5;
47
- const PARKING_RETRY_AFTER_MS = 250;
48
45
  export function approvalDecidePath(sessionId) {
49
46
  return `/v1/approvals/${encodeURIComponent(sessionId)}/decide`;
50
47
  }
@@ -68,11 +65,12 @@ function durableRuleMaterial(material) {
68
65
  return { ruleCommand: material.command, ruleScopeRoot: material.scopeRoot ?? null };
69
66
  }
70
67
  export const NOT_PENDING_UNKNOWN_CAUSE = "unknown_or_other_replica";
68
+ export const ASK_NOT_FOUND_UNDER_TASK = "no such approval ask under this task (never existed, belongs to another task, or is not yours)";
71
69
  export const TOOL_APPROVAL_READ_STATES = ["pending", "decided", "parked", "void"];
72
- function decidedRowFacts(row) {
70
+ export function decidedRowFacts(row) {
73
71
  if (row.state !== "DECIDED" || (row.decision !== "approve" && row.decision !== "deny") || row.provisional || row.decidedAtMs === null)
74
72
  return undefined;
75
- return { decision: row.decision === "approve" ? "allow" : "deny", decidedAtMs: row.decidedAtMs, ...(row.decisionNote !== null ? { decisionNote: row.decisionNote } : {}) };
73
+ return { decision: row.decision, decidedAtMs: row.decidedAtMs, ...(row.decisionNote !== null ? { decisionNote: row.decisionNote } : {}) };
76
74
  }
77
75
  function buildAskRowReadProjection(row, nowMs) {
78
76
  const frame = buildReplayFrame(row, nowMs);
@@ -85,6 +83,22 @@ function buildAskRowReadProjection(row, nowMs) {
85
83
  };
86
84
  }
87
85
  export const ASK_DECISION_CONSUMER_ABSENT = "absent";
86
+ function standingParkedDecision(gate, row, idempotencyKey) {
87
+ if (gate?.kind !== "decided")
88
+ return undefined;
89
+ const decision = gate.decision;
90
+ if (row.decision === decision && row.decidedAtMs !== null && row.decidedAtMs >= gate.decidedAtMs) {
91
+ return {
92
+ kind: "standing",
93
+ decision,
94
+ decidedAtMs: row.decidedAtMs,
95
+ ...(row.decisionNote !== null ? { decisionNote: row.decisionNote } : {}),
96
+ ...(row.decisionActor !== null ? { actor: row.decisionActor } : {}),
97
+ byThisRequest: idempotencyKey !== undefined && row.idempotencyKey === idempotencyKey,
98
+ };
99
+ }
100
+ return { kind: "standing", decision, decidedAtMs: gate.decidedAtMs, ...(gate.decisionNote !== undefined ? { decisionNote: gate.decisionNote } : {}), byThisRequest: false };
101
+ }
88
102
  const MAX_INFLIGHT_TERMINAL_CLAIMS = 2;
89
103
  const DURABLE_CONVERGE_BACKOFF_MS = [50, 100, 200, 400, 800, 1600];
90
104
  export const DURABLE_CONVERGE_READ_TIMEOUT_MS = 2_000;
@@ -247,23 +261,11 @@ function mandatedFromSeat(seat) {
247
261
  return false;
248
262
  }
249
263
  }
250
- function askRowVisibleTo(row, principal) {
251
- return row !== null && askOwnerAdmitsAnswer(row.owner, principal);
252
- }
253
- function askOwnerAdmitsAnswer(owner, principal) {
254
- return keyAdmits("ask.answer", { subject: recoveredSubject(principal), operator: false }, ownerCellOf(owner));
255
- }
256
- function receiptAnswersNow(redeemedGeneration, openGenerationNow) {
257
- return redeemedGeneration === undefined || openGenerationNow === undefined || openGenerationNow <= redeemedGeneration;
258
- }
259
- function waitingLegsReached(entry, othersReached) {
260
- return (entry.adopted === true ? 0 : 1) + othersReached;
261
- }
262
- function withConsumerDisclosure(result, waitingLegs) {
263
- return waitingLegs > 0 ? result : { status: result.status, body: { ...result.body, consumerState: ASK_DECISION_CONSUMER_ABSENT } };
264
+ function askRowVisibleUnder(row, taskId, principal) {
265
+ return row !== null && row.taskId === taskId && keyAdmits("ask.answer", { subject: recoveredSubject(principal), operator: false }, ownerCellOf(row.owner));
264
266
  }
265
267
  function livePendingRowOf(e) {
266
- const listFrame = e.adopted === true ? e.cardFrame : (e.cardFrame ?? e.frame);
268
+ const listFrame = e.cardFrame;
267
269
  const approvalCard = e.adopted === true ? e.cardFrame.card.approvalCard : e.frame.approvalCard;
268
270
  return {
269
271
  approvalId: e.approvalId,
@@ -277,8 +279,8 @@ function livePendingRowOf(e) {
277
279
  ...(liveSessionAnswer(pendingEntryStandingAnswerFacts(e), e).home === "none" ? { rememberRefused: true } : {}),
278
280
  ...(listFrame.mandate !== undefined ? { mandate: listFrame.mandate } : {}),
279
281
  ...(listFrame.origin !== undefined ? { origin: listFrame.origin } : {}),
280
- ...(listFrame.type === "approval_request" && listFrame.liveFactsUnknown === true ? { liveFactsUnknown: true } : {}),
281
- ...(e.originTaskId !== undefined ? { fromSubagent: true, originTaskId: e.originTaskId } : {}),
282
+ ...(listFrame.liveFactsUnknown === true ? { liveFactsUnknown: true } : {}),
283
+ ...(e.sourceSessionId !== undefined ? { fromSubagent: true, sourceSessionId: e.sourceSessionId } : {}),
282
284
  ...(approvalCard !== undefined ? { approvalCard } : {}),
283
285
  frame: listFrame,
284
286
  ...(e.adopted === true ? { consumerState: ASK_DECISION_CONSUMER_ABSENT } : {}),
@@ -286,102 +288,185 @@ function livePendingRowOf(e) {
286
288
  }
287
289
  const _legFamiliesAreFrameFamilies = true;
288
290
  void _legFamiliesAreFrameFamilies;
289
- function canPresentWithoutDurableRow(c) {
290
- return c.family === "tool_approval";
291
- }
292
291
  export const PERSIST_RULE_TEXT_ERROR = `persistRule.rule must be a non-empty string of at most ${MAX_RULE_TEXT_CHARS} characters`;
293
292
  export const PERSIST_RULE_EDITED_FLAG_ERROR = "persistRule.edited must be a boolean when present";
294
293
  export const PERSIST_RULE_BATCH_INDEX_ERROR = "persistRule.batchOfferIndex must be a non-negative integer when present";
295
294
  export const PERSIST_RULE_BATCH_EXCLUSIVE_ERROR = "persistRule.batchOfferIndex is mutually exclusive with persistRule.rule / persistRule.edited — send exactly one arm";
296
- 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)';
297
- 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';
295
+ export const READ_ROOT_GRANT_ALLOW_ONLY = 'readRootGrant is only meaningful with a plain allow-this-call decision (not a deny, not a session 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)';
296
+ 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 a plain allow-this-call decision instead';
298
297
  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`;
298
+ export const READ_ROOT_GRANT_LIVE_ONLY = "readRootGrant can only be honored while the card is live (an engine leg is waiting on it); this ask is parked, so the directory could not be added — answer it again without readRootGrant";
299
+ export function readReadRootGrantSlot(raw, plainApprove) {
300
+ if (raw === undefined)
301
+ return { ok: true };
302
+ if (!plainApprove)
303
+ return { ok: false, error: READ_ROOT_GRANT_ALLOW_ONLY };
304
+ if (typeof raw !== "string" || raw === "" || raw.length > READ_ROOT_CANDIDATE_DIR_MAX)
305
+ return { ok: false, error: READ_ROOT_GRANT_SHAPE };
306
+ return { ok: true, grant: raw };
307
+ }
299
308
  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`)";
300
- export function parseToolApprovalResponse(body) {
301
- const retired = retiredDecisionKeyFacts(body);
302
- const parsed = parseToolApprovalFields(body, retired);
303
- return parsed.ok || retired === undefined ? parsed : { ...parsed, ...retired.extra };
309
+ export function readPersistRuleSlot(raw, decision) {
310
+ if (decision === "deny" || raw === undefined)
311
+ return { ok: true };
312
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw))
313
+ return { ok: false, error: "persistRule must be an object" };
314
+ if (raw.scope !== undefined)
315
+ return { ok: false, error: PERSIST_RULE_SCOPE_REFUSED };
316
+ const rule = raw.rule;
317
+ const edited = raw.edited;
318
+ const batchIdx = raw.batchOfferIndex;
319
+ if (batchIdx !== undefined) {
320
+ if (rule !== undefined || edited !== undefined)
321
+ return { ok: false, error: PERSIST_RULE_BATCH_EXCLUSIVE_ERROR };
322
+ if (typeof batchIdx !== "number" || !Number.isInteger(batchIdx) || batchIdx < 0)
323
+ return { ok: false, error: PERSIST_RULE_BATCH_INDEX_ERROR };
324
+ return { ok: true, slot: { kind: "batch", batchOfferIndex: batchIdx } };
325
+ }
326
+ if (typeof rule !== "string" || rule === "" || rule.length > MAX_RULE_TEXT_CHARS)
327
+ return { ok: false, error: PERSIST_RULE_TEXT_ERROR };
328
+ if (edited !== undefined && typeof edited !== "boolean")
329
+ return { ok: false, error: PERSIST_RULE_EDITED_FLAG_ERROR };
330
+ return { ok: true, slot: { kind: "text", rule, edited: edited === true } };
304
331
  }
305
- function parseToolApprovalFields(body, retired) {
306
- if (body === null || typeof body !== "object" || Array.isArray(body))
307
- return { ok: false, error: "body must be an object" };
308
- const d = body.decision;
309
- if (d === "allow" || d === "allow_session" || d === "deny") {
310
- const u = body.updatedInput;
311
- if (body.scope !== undefined)
312
- return { ok: false, error: PERSIST_RULE_SCOPE_REFUSED };
313
- const pr = body.persistRule;
314
- let persistRule;
315
- if (d !== "deny" && pr !== undefined) {
316
- if (pr === null || typeof pr !== "object" || Array.isArray(pr))
317
- return { ok: false, error: "persistRule must be an object" };
318
- if (pr.scope !== undefined)
319
- return { ok: false, error: PERSIST_RULE_SCOPE_REFUSED };
320
- const rule = pr.rule;
321
- const edited = pr.edited;
322
- const batchIdx = pr.batchOfferIndex;
323
- if (batchIdx !== undefined) {
324
- if (rule !== undefined || edited !== undefined)
325
- return { ok: false, error: PERSIST_RULE_BATCH_EXCLUSIVE_ERROR };
326
- if (typeof batchIdx !== "number" || !Number.isInteger(batchIdx) || batchIdx < 0) {
327
- return { ok: false, error: PERSIST_RULE_BATCH_INDEX_ERROR };
328
- }
329
- persistRule = { kind: "batch", batchOfferIndex: batchIdx };
330
- }
331
- else {
332
- if (typeof rule !== "string" || rule === "" || rule.length > MAX_RULE_TEXT_CHARS) {
333
- return { ok: false, error: PERSIST_RULE_TEXT_ERROR };
334
- }
335
- if (edited !== undefined && typeof edited !== "boolean")
336
- return { ok: false, error: PERSIST_RULE_EDITED_FLAG_ERROR };
337
- persistRule = { kind: "text", rule, edited: edited === true };
338
- }
332
+ function withheldForCredentialShape(command, where) {
333
+ if (!ruleMaterialCarriesCredentialShape(command))
334
+ return undefined;
335
+ defaultLogger.warn("approval rule material withheld: the adjudicated command carries credential-shaped bytes", { where, commandChars: command.length });
336
+ return { kind: "absent", refusal: "rule_material_absent" };
337
+ }
338
+ export function readCardRuleRecordOfAskRow(row, owner) {
339
+ const envelope = readStoredCardEnvelope(row.cardJson);
340
+ if (envelope === undefined)
341
+ return { kind: "absent", refusal: "rule_material_absent" };
342
+ if (envelope.card.governanceForced === true)
343
+ return { kind: "absent", refusal: "rule_governance_forced" };
344
+ if (owner === null)
345
+ return { kind: "absent", refusal: "rule_lane_unavailable" };
346
+ if (!askRowCarriesOwner(row, owner))
347
+ return { kind: "absent", refusal: "rule_material_absent" };
348
+ const command = row.ruleCommand;
349
+ if (command === null)
350
+ return { kind: "absent", refusal: "rule_material_absent" };
351
+ return withheldForCredentialShape(command, "ask_row") ?? {
352
+ kind: "record",
353
+ record: {
354
+ owner,
355
+ toolName: envelope.card.toolName,
356
+ command,
357
+ offers: envelope.card.ruleOffers ?? [],
358
+ ...(row.toolCallId !== "" ? { toolCallId: row.toolCallId } : {}),
359
+ ...(row.boundInputHash !== null ? { boundInputHash: row.boundInputHash } : {}),
360
+ ...(row.ruleScopeRoot !== null ? { scopeRoot: row.ruleScopeRoot } : {}),
361
+ },
362
+ };
363
+ }
364
+ export function cardRuleOffersOfAskRow(row) {
365
+ return readStoredCardEnvelope(row.cardJson)?.card.ruleOffers;
366
+ }
367
+ export function readCardRuleRecordOfParkRow(pa, facts) {
368
+ if (facts.governanceForced)
369
+ return { kind: "absent", refusal: "rule_governance_forced" };
370
+ if (facts.owner === null)
371
+ return { kind: "absent", refusal: "rule_lane_unavailable" };
372
+ const offers = readCardRuleOffers(pa.ruleOffers, pa.toolName);
373
+ if (offers === undefined)
374
+ return { kind: "absent", refusal: "rule_material_absent" };
375
+ const command = cardBindingOf(pa.args, offers);
376
+ if (command === undefined)
377
+ return { kind: "absent", refusal: "rule_material_absent" };
378
+ return withheldForCredentialShape(command, "park_row") ?? {
379
+ kind: "record",
380
+ record: {
381
+ owner: facts.owner,
382
+ toolName: pa.toolName,
383
+ command,
384
+ offers,
385
+ ...(typeof pa.toolCallId === "string" && pa.toolCallId !== "" ? { toolCallId: pa.toolCallId } : {}),
386
+ ...(typeof pa.boundInputHash === "string" && pa.boundInputHash !== "" ? { boundInputHash: pa.boundInputHash } : {}),
387
+ ...(facts.scopeRoot !== null ? { scopeRoot: facts.scopeRoot } : {}),
388
+ },
389
+ };
390
+ }
391
+ export async function redeemCardRule(lane, reading, slot, ctx) {
392
+ const refuse = (ruleRefusal) => ({ rulePersisted: false, ruleRefusal });
393
+ if (reading.kind === "absent" && reading.refusal === "rule_governance_forced")
394
+ return refuse("rule_governance_forced");
395
+ if (lane === undefined)
396
+ return refuse("rule_lane_unavailable");
397
+ if (reading.kind === "absent")
398
+ return refuse(reading.refusal);
399
+ if (!ctx.eligible)
400
+ return refuse("rule_store_error");
401
+ if (ctx.inputWasEdited)
402
+ return refuse("rule_input_edited");
403
+ const r = reading.record;
404
+ const common = {
405
+ principal: r.owner,
406
+ toolName: r.toolName,
407
+ command: r.command,
408
+ ...(r.toolCallId !== undefined ? { toolCallId: r.toolCallId } : {}),
409
+ ...(r.boundInputHash !== undefined ? { boundInputHash: r.boundInputHash } : {}),
410
+ ...(r.scopeRoot !== undefined ? { scope: { kind: "project", root: r.scopeRoot } } : {}),
411
+ };
412
+ let redemption;
413
+ if (slot.kind === "batch") {
414
+ const offer = r.offers[slot.batchOfferIndex];
415
+ if (offer === undefined || offer.kind !== "batch")
416
+ return refuse("rule_not_offered");
417
+ redemption = { kind: "batch", offer };
418
+ }
419
+ else if (slot.edited) {
420
+ redemption = { kind: "edited", text: slot.rule };
421
+ }
422
+ else {
423
+ const hits = r.offers.filter((o) => o.kind === "single" && (o.rule === slot.rule || displayRuleOfferText(o.rule, false) === slot.rule));
424
+ if (hits.length !== 1)
425
+ return refuse("rule_not_offered");
426
+ redemption = { kind: "offered", ruleText: hits[0].rule };
427
+ }
428
+ try {
429
+ const persisted = await lane.persistCardRule({ ...common, redemption });
430
+ if (!persisted.ok) {
431
+ defaultLogger.warn("permission rule was not persisted after an approval", { reason: persisted.reason, arm: redemption.kind, ...(persisted.detail !== undefined ? { detail: redactSecrets(persisted.detail) } : {}) });
432
+ return refuse(persisted.reason);
339
433
  }
340
- const rawNote = body.note;
341
- let note;
342
- if (rawNote !== undefined) {
343
- if (typeof rawNote !== "string" || rawNote.length > MAX_DECISION_NOTE_CHARS) {
344
- return { ok: false, error: `note must be a string of at most ${MAX_DECISION_NOTE_CHARS} characters` };
345
- }
346
- note = rawNote;
347
- }
348
- if (retired !== undefined)
349
- return { ok: false, error: retired.error };
350
- const deciderReading = readDecider(body, false);
351
- let decider;
352
- switch (deciderReading.kind) {
353
- case "outside_closed_set":
354
- return { ok: false, error: deciderReading.message };
355
- case "accepted":
356
- decider = deciderReading.decider;
357
- break;
358
- default: {
359
- const unreachable = deciderReading;
360
- throw new Error(`unhandled decider reading: ${JSON.stringify(unreachable)}`);
361
- }
434
+ if (persisted.kind === "batch" && slot.kind === "batch") {
435
+ return {
436
+ rulePersisted: true,
437
+ persistedRules: persisted.rules,
438
+ ...(persisted.members !== undefined ? { persistedRuleAnchors: persisted.members.map((m) => ({ offerIndex: slot.batchOfferIndex, memberIndex: m.memberIndex, rule: m.rule })) } : {}),
439
+ };
362
440
  }
363
- const rawGrant = body.readRootGrant;
364
- if (rawGrant !== undefined && d !== "allow")
365
- return { ok: false, error: READ_ROOT_GRANT_ALLOW_ONLY };
366
- if (rawGrant !== undefined && (typeof rawGrant !== "string" || rawGrant === "" || rawGrant.length > READ_ROOT_CANDIDATE_DIR_MAX))
367
- return { ok: false, error: READ_ROOT_GRANT_SHAPE };
368
- const readRootGrant = rawGrant;
369
- return {
370
- ok: true,
371
- value: d,
372
- ...(d !== "deny" && u !== undefined ? { updatedInput: u } : {}),
373
- ...(persistRule !== undefined ? { persistRule } : {}),
374
- ...(note !== undefined ? { note } : {}),
375
- decider,
376
- ...(readRootGrant !== undefined ? { readRootGrant } : {}),
377
- };
441
+ return { rulePersisted: true, ...(slot.kind === "text" && slot.edited && persisted.kind === "single" ? { persistedRule: persisted.rule } : {}) };
442
+ }
443
+ catch (err) {
444
+ defaultLogger.warn("permission rule store call failed — the approval stands, the rule was not persisted", { where: "persistCardRule", arm: redemption.kind, err: err instanceof Error ? err.message : String(err) });
445
+ return refuse("rule_store_error");
378
446
  }
379
- return { ok: false, error: 'decision must be one of "allow" | "allow_session" | "deny"' };
447
+ }
448
+ const heldLegEntries = new WeakMap();
449
+ export function askDecisionRecordOf(write) {
450
+ return {
451
+ decision: write.decision,
452
+ ...(write.actor !== undefined ? { decisionActor: write.actor } : {}),
453
+ decisionNote: write.note ?? null,
454
+ idempotencyKey: write.idempotencyKey ?? null,
455
+ ...liveHostSource(write.decider),
456
+ };
457
+ }
458
+ function liveDecideInputOf(write) {
459
+ return { ...askDecisionRecordOf(write), ...(write.decision === "approve" && write.updatedInput !== undefined ? { updatedInput: write.updatedInput } : {}) };
380
460
  }
381
461
  export function buildUnattendedPolicyRefusal(reason) {
382
462
  return { allow: false, settledBy: POLICY_REFUSAL_DECISION.settledBy, reason };
383
463
  }
384
464
  function unattendedRefusalReason(site) {
465
+ if (site.arm === "card_protocol_absent") {
466
+ return ("UNATTENDED_APPROVAL_POLICY=deny: this worker presents no approval cards — it has no durable approval ledger (no store backend, or no park " +
467
+ "facility), so no approval card is shown to anyone, and an approval request no one can answer is refused here instead of being parked " +
468
+ `for a person (arm: ${site.arm}).`);
469
+ }
385
470
  if (site.arm === "frame_family_undeclared") {
386
471
  return (`UNATTENDED_APPROVAL_POLICY=deny: this run has no live approval channel — the client that started it did not declare the "${site.family}" ` +
387
472
  "approval frame family (settings.approvalFrameFamilies), so no approval card is shown to it, and an approval request no one can answer " +
@@ -393,37 +478,34 @@ function unattendedRefusalReason(site) {
393
478
  function isChildAsk(req) {
394
479
  return req.fromSubagent === true;
395
480
  }
396
- export function approverFacingArgs(args) {
397
- return boundArgs(args).args;
481
+ export function approverFacingArgs(args, card) {
482
+ return boundArgs(args, card).args;
398
483
  }
399
- function boundArgs(args) {
400
- const redacted = redactDeep(args);
401
- let hasBidi = false;
484
+ function boundArgs(args, card) {
485
+ const shown = humanFacingArgs(args, card);
486
+ if (shown.kind === "omitted" && shown.bytes === null)
487
+ return { omitted: true, hasBidi: false };
488
+ let hasBidi;
402
489
  try {
403
490
  hasBidi = hasBidiControls(JSON.stringify(args));
404
- const text = JSON.stringify(redacted);
405
- if (Buffer.byteLength(text, "utf8") > MAX_APPROVAL_ARGS_BYTES)
406
- return { omitted: true, hasBidi };
407
491
  }
408
492
  catch {
409
493
  return { omitted: true, hasBidi: false };
410
494
  }
411
- return { args: redacted, omitted: false, hasBidi };
495
+ return shown.kind === "whole" ? { args: shown.value, omitted: false, hasBidi } : { omitted: true, hasBidi };
412
496
  }
413
497
  export function resolveStreamApprovalGate(input) {
414
498
  if (!input.toolApprovalEnabled)
415
499
  return { active: false, reason: "no_tool_approval" };
416
- if (!input.streamApprovalEnabled)
417
- return { active: false, reason: "protocol_disabled" };
418
500
  if (!input.backend)
419
501
  return { active: false, reason: "no_backend" };
420
502
  if (input.checkpointStore === undefined)
421
503
  return { active: false, reason: "no_park_facility" };
422
504
  return { active: true, askStore: input.backend.approvalAsk(), checkpointStore: input.checkpointStore };
423
505
  }
424
- export const APPROVAL_FRAME_FAMILIES = ["tool_approval", "approval_request"];
506
+ export const APPROVAL_FRAME_FAMILIES = ["approval_request"];
425
507
  export function presentedApprovalFrameFamily(streamApprovalOn) {
426
- return streamApprovalOn ? "approval_request" : "tool_approval";
508
+ return streamApprovalOn ? "approval_request" : undefined;
427
509
  }
428
510
  const APPROVAL_FRAME_FAMILIES_FIELD = "settings.approvalFrameFamilies";
429
511
  const APPROVAL_FRAME_FAMILY_WORDS = APPROVAL_FRAME_FAMILIES.map((w) => `"${w}"`).join(" | ");
@@ -470,6 +552,8 @@ export function resolveApprovalLeg(input) {
470
552
  if (input.windowMs === 0 || tightDeadline)
471
553
  return { active, unattended: { arm: "window_zero" } };
472
554
  const family = presentedApprovalFrameFamily(active);
555
+ if (family === undefined)
556
+ return { active, unattended: { arm: "card_protocol_absent" } };
473
557
  if (!input.declaredFamilies.has(family))
474
558
  return { active, unattended: { arm: "frame_family_undeclared", family } };
475
559
  return {
@@ -511,18 +595,16 @@ function settleArgsOfDecided(outcome) {
511
595
  ...(!outcome.allow && outcome.reason !== undefined ? { denyReason: outcome.reason } : {}),
512
596
  };
513
597
  }
514
- function verdictOfDecided(args) {
598
+ function verdictOfDecided(args, readRootGrant) {
515
599
  return {
516
600
  by: "human",
517
601
  allowed: args.allowed,
518
602
  ...(args.updatedInput !== undefined ? { updatedInput: args.updatedInput } : {}),
519
603
  ...(args.source !== undefined ? { hostSource: args.source } : {}),
520
604
  ...(args.denyReason !== undefined ? { denyReason: args.denyReason } : {}),
605
+ ...(args.allowed && readRootGrant !== undefined ? { readRootGrant } : {}),
521
606
  };
522
607
  }
523
- function settleArgsOf(outcome) {
524
- return outcome === "unavailable" ? "unavailable" : settleArgsOfDecided(outcome);
525
- }
526
608
  export class ToolApprovalCoordinator {
527
609
  als = new AsyncLocalStorage();
528
610
  pending = new Map();
@@ -562,8 +644,8 @@ export class ToolApprovalCoordinator {
562
644
  admitMaxPerOwner;
563
645
  admitByTask = new Map();
564
646
  admitByOwner = new Map();
565
- suspendedPollOffset = 0;
566
- adoptedPollOffset = 0;
647
+ suspendedPollCursor = { offset: 0 };
648
+ adoptedPollCursor = { offset: 0 };
567
649
  editedDecisionsInFlight = new Map();
568
650
  storeErrorWarned = false;
569
651
  storeErrorTally = 0;
@@ -573,7 +655,6 @@ export class ToolApprovalCoordinator {
573
655
  ruleScopeRootFor;
574
656
  parkRedemptions = new Map();
575
657
  parkRedemptionMax;
576
- parkRedemptionTtlMs;
577
658
  parkRedemptionPerPrincipalMax;
578
659
  parkedRedeem;
579
660
  parkedGate;
@@ -585,28 +666,27 @@ export class ToolApprovalCoordinator {
585
666
  parkCommits;
586
667
  parksAwaitingCommit = new Set();
587
668
  constructor(opts) {
588
- this.ruleConsent = opts?.ruleConsent;
589
- this.ruleScopeRootFor = opts?.ruleScopeRootFor;
590
- this.governanceAskMarks = opts?.governanceAskMarks;
591
- this.ttlMs = opts?.ttlMs ?? DEFAULT_APPROVAL_TTL_MS;
592
- this.unreachedTtlMs = opts?.unreachedTtlMs ?? DEFAULT_UNREACHED_ASK_TTL_MS;
593
- this.askStore = opts?.askStore;
594
- this.windowMarginMs = opts?.windowMarginMs ?? DEFAULT_WINDOW_MARGIN_MS;
595
- this.admitMaxPerTask = opts?.admitMaxPerTask ?? DEFAULT_ADMIT_MAX_PER_TASK;
596
- this.admitMaxPerOwner = opts?.admitMaxPerOwner ?? DEFAULT_ADMIT_MAX_PER_OWNER;
597
- this.unattendedPolicy = opts?.unattendedPolicy ?? "park";
598
- this.parkedRedeem = opts?.parkedRedeem;
599
- this.parkedGate = opts?.parkedGate;
600
- this.parkRedemptionMax = opts?.parkRedemptionMax ?? DEFAULT_PARK_REDEMPTION_MAX;
601
- this.parkRedemptionTtlMs = opts?.parkRedemptionTtlMs ?? DEFAULT_PARK_REDEMPTION_TTL_MS;
602
- this.parkRedemptionPerPrincipalMax = opts?.parkRedemptionPerPrincipalMax ?? DEFAULT_PARK_REDEMPTION_PER_PRINCIPAL_MAX;
603
- this.askAudit = opts?.askAudit;
604
- this.sessionGrants = opts?.sessionGrants;
605
- if (opts?.deploymentListAsks !== undefined && opts.sessionGrants === undefined) {
669
+ this.ruleConsent = opts.ruleConsent;
670
+ this.ruleScopeRootFor = opts.ruleScopeRootFor;
671
+ this.governanceAskMarks = opts.governanceAskMarks;
672
+ this.ttlMs = opts.ttlMs ?? DEFAULT_APPROVAL_TTL_MS;
673
+ this.unreachedTtlMs = opts.unreachedTtlMs ?? DEFAULT_UNREACHED_ASK_TTL_MS;
674
+ this.askStore = opts.askStore;
675
+ this.windowMarginMs = opts.windowMarginMs ?? DEFAULT_WINDOW_MARGIN_MS;
676
+ this.admitMaxPerTask = opts.admitMaxPerTask ?? DEFAULT_ADMIT_MAX_PER_TASK;
677
+ this.admitMaxPerOwner = opts.admitMaxPerOwner ?? DEFAULT_ADMIT_MAX_PER_OWNER;
678
+ this.unattendedPolicy = opts.unattendedPolicy ?? "park";
679
+ this.parkedRedeem = opts.parkedRedeem;
680
+ this.parkedGate = opts.parkedGate;
681
+ this.parkRedemptionMax = opts.parkRedemptionMax ?? DEFAULT_PARK_REDEMPTION_MAX;
682
+ this.parkRedemptionPerPrincipalMax = opts.parkRedemptionPerPrincipalMax ?? DEFAULT_PARK_REDEMPTION_PER_PRINCIPAL_MAX;
683
+ this.askAudit = opts.askAudit;
684
+ this.sessionGrants = opts.sessionGrants;
685
+ if (opts.deploymentListAsks !== undefined && opts.sessionGrants === undefined) {
606
686
  throw new Error("ToolApprovalCoordinator: deploymentListAsks needs sessionGrants — the deployment list's standing answers live in the session exemption store");
607
687
  }
608
- this.deploymentListAsks = opts?.deploymentListAsks;
609
- this.parkCommits = opts?.parkCommits;
688
+ this.deploymentListAsks = opts.deploymentListAsks;
689
+ this.parkCommits = opts.parkCommits;
610
690
  this.parkCommits?.onParkCommitted((calls) => this.noteParkCommitted(calls));
611
691
  }
612
692
  unattendedOutcome(site) {
@@ -652,9 +732,9 @@ export class ToolApprovalCoordinator {
652
732
  return this.unattendedOutcome(site.arm === "frame_family_undeclared" ? { arm: site.arm, family: site.family, ...facts } : { arm: site.arm, ...facts });
653
733
  }
654
734
  async landUnattendedOutcome(outcome, row) {
655
- const store = this.askStore;
656
- if (outcome === "unavailable" || !store)
735
+ if (outcome === "unavailable")
657
736
  return outcome;
737
+ const store = this.askStore;
658
738
  const dispatch = (res, where) => {
659
739
  if (res.claimed) {
660
740
  this.settleSameAskIdParkRoute(row.askId, APPROVAL_ENDING_CAUSES.UNDELIVERABLE);
@@ -678,8 +758,6 @@ export class ToolApprovalCoordinator {
678
758
  return read?.kind === "decided" ? read.outcome : outcome;
679
759
  }
680
760
  admit(taskId, owner) {
681
- if (!this.askStore)
682
- return () => undefined;
683
761
  const ownerKey = JSON.stringify(["owner", owner]);
684
762
  const taskCount = this.admitByTask.get(taskId) ?? 0;
685
763
  const ownerCount = this.admitByOwner.get(ownerKey) ?? 0;
@@ -738,103 +816,6 @@ export class ToolApprovalCoordinator {
738
816
  return a === b;
739
817
  return a.length === b.length && a.every((d, i) => d.segment === b[i].segment && d.reason === b[i].reason);
740
818
  }
741
- static ruleOfferTextsOverSize(offer) {
742
- const over = (s) => s.length > MAX_RULE_TEXT_CHARS;
743
- if (offer.kind === "single")
744
- return over(offer.rule) || over(offer.command);
745
- return offer.rules.some((r) => {
746
- switch (r.kind) {
747
- case "command":
748
- return over(r.rule) || over(r.command);
749
- case "directoryRead":
750
- return over(r.rule) || over(r.directory);
751
- default:
752
- throw new Error(`unknown rule-offer batch member kind: ${JSON.stringify(r)}`);
753
- }
754
- });
755
- }
756
- static ruleOfferDerivedFactsAgree(offer, cardTool) {
757
- const derive = (rule) => {
758
- const p = parseRuleText(rule, "allow");
759
- return "rule" in p ? p.rule : undefined;
760
- };
761
- if (offer.kind === "single") {
762
- const d = derive(offer.rule);
763
- return d !== undefined && d.rule === offer.rule && d.match === offer.match && d.command === offer.command && ToolApprovalCoordinator.singleOfferBelongsOnCard(d.match, d.tool, cardTool);
764
- }
765
- return offer.rules.every((m) => {
766
- const d = derive(m.rule);
767
- if (d === undefined || d.rule !== m.rule)
768
- return false;
769
- switch (m.kind) {
770
- case "command":
771
- return d.tool === "Bash" && d.match === m.match && d.command === m.command;
772
- case "directoryRead":
773
- return d.tool === "Read" && d.match === "subpath" && d.command === m.directory;
774
- default:
775
- throw new Error(`unknown rule-offer batch member kind: ${JSON.stringify(m)}`);
776
- }
777
- });
778
- }
779
- static singleOfferBelongsOnCard(match, ruleTool, cardTool) {
780
- const form = CARD_RULE_FORM_OF_MATCH[match];
781
- return ruleTool === cardTool && form !== undefined && form === cardRuleFormOf(cardTool);
782
- }
783
- static boundRuleOfferTexts(offers, cardTool) {
784
- const over = (s) => s.length > MAX_RULE_TEXT_CHARS;
785
- const clipSegment = (s) => (over(s) ? s.slice(0, MAX_RULE_TEXT_CHARS) : s);
786
- const out = [];
787
- for (const { offer } of readRuleOfferList(offers)) {
788
- if (ToolApprovalCoordinator.ruleOfferTextsOverSize(offer))
789
- return undefined;
790
- if (!ToolApprovalCoordinator.ruleOfferDerivedFactsAgree(offer, cardTool)) {
791
- defaultLogger.warn("rule_offer_dropped_derived_facts_disagree", {
792
- kind: offer.kind,
793
- note: "an offer's kind/match/command/directory seats do not agree with what the shared validator derives from its rule text, or it is not this card's tool / card form — dropped whole so the card never shows one grant while redeeming another (core/dist skew or forged material?)",
794
- });
795
- continue;
796
- }
797
- switch (offer.kind) {
798
- case "single":
799
- out.push({ kind: "single", rule: offer.rule, match: offer.match, command: offer.command });
800
- break;
801
- case "batch": {
802
- const rules = [];
803
- for (const r of offer.rules) {
804
- switch (r.kind) {
805
- case "command":
806
- rules.push({ kind: "command", rule: r.rule, match: r.match, command: r.command, segment: clipSegment(r.segment) });
807
- break;
808
- case "directoryRead":
809
- rules.push({ kind: "directoryRead", rule: r.rule, directory: r.directory, segment: clipSegment(r.segment) });
810
- break;
811
- default:
812
- throw new Error(`unknown rule-offer batch member kind: ${JSON.stringify(r)}`);
813
- }
814
- }
815
- out.push({
816
- kind: "batch",
817
- rules,
818
- uncoveredSegments: offer.uncoveredSegments,
819
- ...(offer.uncoveredDetail !== undefined ? { uncoveredDetail: offer.uncoveredDetail.map((d) => ({ segment: clipSegment(d.segment), reason: d.reason })) } : {}),
820
- });
821
- break;
822
- }
823
- default:
824
- throw new Error(`unknown rule-offer kind: ${JSON.stringify(offer)}`);
825
- }
826
- }
827
- return out;
828
- }
829
- static cardBindingOf(args, offers) {
830
- const valueOffers = offers.filter((o) => o.kind === "single" && CARD_RULE_FORM_OF_MATCH[o.match] !== "command");
831
- if (valueOffers.length > 0)
832
- return valueOffers.length === 1 && offers.length === 1 ? valueOffers[0].command : undefined;
833
- if (args === null || typeof args !== "object" || Array.isArray(args))
834
- return undefined;
835
- const command = args.command;
836
- return typeof command === "string" && command !== "" ? command : undefined;
837
- }
838
819
  buildRuleLaneMaterial(req, owner, governanceForced, sessionId) {
839
820
  if (this.ruleConsent === undefined)
840
821
  return undefined;
@@ -842,14 +823,10 @@ export class ToolApprovalCoordinator {
842
823
  return undefined;
843
824
  if (owner === null)
844
825
  return undefined;
845
- const rawOffers = req.ruleOffers;
846
- if (!Array.isArray(rawOffers) || rawOffers.length === 0)
847
- return undefined;
848
- const capped = rawOffers.length > MAX_RULE_OFFERS ? rawOffers.slice(0, MAX_RULE_OFFERS) : rawOffers;
849
- const offers = ToolApprovalCoordinator.boundRuleOfferTexts(capped, req.toolName);
826
+ const offers = readCardRuleOffers(req.ruleOffers, req.toolName);
850
827
  if (offers === undefined)
851
828
  return undefined;
852
- const command = ToolApprovalCoordinator.cardBindingOf(req.args, offers);
829
+ const command = cardBindingOf(req.args, offers);
853
830
  if (command === undefined)
854
831
  return undefined;
855
832
  const boundInputHash = readBoundInputHash(req);
@@ -1016,8 +993,6 @@ export class ToolApprovalCoordinator {
1016
993
  };
1017
994
  };
1018
995
  async sessionGrantUncontestedByRow(origin, req) {
1019
- if (!this.askStore)
1020
- return true;
1021
996
  const call = { taskId: origin.taskId, legKey: origin.legKey, toolCallId: req.toolCallId, sourceSessionId: req.sourceSessionId ?? origin.taskId };
1022
997
  try {
1023
998
  const rows = await this.withStoreDeadline(this.askStore.listByCall(call), "listByCall(session-grant-provenance)", DURABLE_CONVERGE_READ_TIMEOUT_MS);
@@ -1029,7 +1004,7 @@ export class ToolApprovalCoordinator {
1029
1004
  }
1030
1005
  }
1031
1006
  async probeRowWhileYielding(askId, budgetMs) {
1032
- if (!this.askStore || budgetMs <= 0)
1007
+ if (budgetMs <= 0)
1033
1008
  return { kind: "none" };
1034
1009
  const release = this.admitTerminalClaim(askId);
1035
1010
  if (release === undefined)
@@ -1067,8 +1042,6 @@ export class ToolApprovalCoordinator {
1067
1042
  return { kind: "decided", outcome: replayedOutcome };
1068
1043
  }
1069
1044
  hasOtherSuspendedOwner(askId, self) {
1070
- if (askId === undefined)
1071
- return false;
1072
1045
  for (const e of this.pendingByAskId.get(askId) ?? []) {
1073
1046
  if (e !== self && e.suspended === true && e.settledOutcome === undefined)
1074
1047
  return true;
@@ -1107,8 +1080,6 @@ export class ToolApprovalCoordinator {
1107
1080
  let inflight = 0;
1108
1081
  let ownInflight = 0;
1109
1082
  for (const r of this.parkRedemptions.values()) {
1110
- if (r.inflight === undefined)
1111
- continue;
1112
1083
  inflight++;
1113
1084
  if (r.owner === owner)
1114
1085
  ownInflight++;
@@ -1126,80 +1097,86 @@ export class ToolApprovalCoordinator {
1126
1097
  };
1127
1098
  }
1128
1099
  openParkRedemption(askId, owner) {
1129
- const opened = { owner, stampedAtMs: Date.now() };
1130
- this.parkRedemptions.delete(askId);
1100
+ const opened = { owner };
1131
1101
  this.parkRedemptions.set(askId, opened);
1132
1102
  return opened;
1133
1103
  }
1134
- retainParkRedemption(askId, memo) {
1135
- const now = Date.now();
1136
- const effectivePerPrincipalMax = Math.min(this.parkRedemptionPerPrincipalMax, this.parkRedemptionMax);
1137
- let receipts = 0;
1138
- let ownReceipts = 0;
1139
- let ownExpired;
1140
- let ownOldest;
1141
- let globalExpired;
1142
- for (const [k, r] of this.parkRedemptions) {
1143
- if (k === askId || r.redeemed === undefined || r.inflight !== undefined)
1144
- continue;
1145
- receipts++;
1146
- const expired = now - r.stampedAtMs > this.parkRedemptionTtlMs;
1147
- if (globalExpired === undefined && expired)
1148
- globalExpired = k;
1149
- if (r.owner !== memo.owner)
1150
- continue;
1151
- ownReceipts++;
1152
- if (ownOldest === undefined)
1153
- ownOldest = k;
1154
- if (ownExpired === undefined && expired)
1155
- ownExpired = k;
1156
- }
1157
- if (ownReceipts >= effectivePerPrincipalMax) {
1158
- const victim = ownExpired ?? ownOldest;
1159
- if (victim !== undefined)
1160
- this.parkRedemptions.delete(victim);
1161
- }
1162
- else if (receipts >= this.parkRedemptionMax) {
1163
- if (globalExpired === undefined) {
1164
- recordFailOpen("server.approval.park-redemption-cap-refused", `owner=${memo.owner ?? "<none>"}`);
1165
- if (this.parkRedemptions.get(askId) === memo)
1166
- this.parkRedemptions.delete(askId);
1167
- return false;
1168
- }
1169
- this.parkRedemptions.delete(globalExpired);
1170
- }
1171
- memo.stampedAtMs = now;
1172
- return true;
1173
- }
1174
1104
  lookupParkRedemption(askId) {
1175
- const r = this.parkRedemptions.get(askId);
1176
- if (r === undefined)
1177
- return undefined;
1178
- if (r.inflight === undefined && Date.now() - r.stampedAtMs > this.parkRedemptionTtlMs) {
1179
- this.parkRedemptions.delete(askId);
1180
- return undefined;
1181
- }
1182
- return r;
1105
+ return this.parkRedemptions.get(askId);
1183
1106
  }
1184
- notifyExternalDecision(askId, outcome) {
1107
+ notifyExternalDecision(askId, outcome, readRootGrant) {
1185
1108
  const entries = this.pendingByAskId.get(askId);
1186
1109
  if (!entries)
1187
1110
  return { settled: 0 };
1188
1111
  const args = settleArgsOfDecided(outcome);
1189
1112
  let settled = 0;
1190
1113
  for (const entry of [...entries]) {
1191
- entry.settle(verdictOfDecided(args));
1114
+ entry.settle(verdictOfDecided(args, readRootGrant));
1192
1115
  if (entry.adopted !== true)
1193
1116
  settled += 1;
1194
1117
  }
1195
1118
  return { settled };
1196
1119
  }
1120
+ holdLiveLeg(askId) {
1121
+ const hold = { askId };
1122
+ const entry = this.pendingByAskId.get(askId)?.values().next().value;
1123
+ if (entry !== undefined)
1124
+ heldLegEntries.set(hold, entry);
1125
+ return hold;
1126
+ }
1127
+ askDemandsAPerson(row, held) {
1128
+ return storedCardDemandsAPerson(row.cardJson) || (held.askId === row.askId && heldLegEntries.get(held)?.requiresRealApproval === true);
1129
+ }
1130
+ async settleLiveDecision(row, held, judgedOwner, principal, extras) {
1131
+ const leg = held.askId === row.askId ? heldLegEntries.get(held) : undefined;
1132
+ const answer = extras.remember === "session" && leg !== undefined ? liveSessionAnswer(pendingEntryStandingAnswerFacts(leg), leg) : undefined;
1133
+ let rememberApplied = extras.remember === "session" ? false : undefined;
1134
+ if (leg !== undefined && answer?.home === "seat") {
1135
+ const grantKey = sessionAllowKey(judgedOwner, answer.seat, sessionAllowCategory(leg.toolName), leg.grantBinding);
1136
+ if (this.allowAllSessions.size >= MAX_ALLOW_SESSIONS) {
1137
+ const oldest = this.allowAllSessions.values().next().value;
1138
+ if (oldest !== undefined)
1139
+ this.allowAllSessions.delete(oldest);
1140
+ }
1141
+ this.allowAllSessions.add(grantKey);
1142
+ rememberApplied = true;
1143
+ }
1144
+ else if (answer?.home === "none" && answer.occupantUnreadable) {
1145
+ defaultLogger.warn("session_grant_refused_occupant_unreadable", { sessionId: leg?.sessionId, toolName: leg?.toolName });
1146
+ }
1147
+ const { settled } = this.notifyExternalDecision(row.askId, outcomeOfDecidedRow(row), extras.readRootGrant);
1148
+ const grants = this.sessionGrants;
1149
+ if (leg !== undefined && answer?.home === "server_store" && grants !== undefined) {
1150
+ try {
1151
+ await this.withStoreDeadline(grants.grant(answer.sessionId, leg.toolName, principal ?? judgedOwner), "sessionGrants.grant(deployment-list)", DURABLE_CALL_TIMEOUT_MS);
1152
+ rememberApplied = true;
1153
+ defaultLogger.info("approval_exemption_granted", { sessionId: answer.sessionId, toolName: leg.toolName, grantedBy: principal ?? judgedOwner });
1154
+ }
1155
+ catch (err) {
1156
+ defaultLogger.warn("approval_exemption_grant_failed", { sessionId: answer.sessionId, toolName: leg.toolName, error: String(err).slice(0, 200) });
1157
+ }
1158
+ }
1159
+ const rule = extras.persistRule !== undefined && row.decision === "approve"
1160
+ ? await redeemCardRule(this.ruleConsent, readCardRuleRecordOfAskRow(row, judgedOwner), extras.persistRule, { inputWasEdited: extras.inputWasEdited, eligible: true })
1161
+ : undefined;
1162
+ return { settled, ...(rememberApplied !== undefined ? { rememberApplied } : {}), ...(rule !== undefined ? { rule } : {}) };
1163
+ }
1164
+ precheckEditedRuleOfAskRow(row, judgedOwner, persistRule, inputWasEdited) {
1165
+ if (persistRule.kind !== "text" || !persistRule.edited || inputWasEdited || this.ruleConsent === undefined)
1166
+ return undefined;
1167
+ const reading = readCardRuleRecordOfAskRow(row, judgedOwner);
1168
+ if (reading.kind !== "record")
1169
+ return undefined;
1170
+ const verdict = precheckCardRuleText(persistRule.rule, reading.record.command);
1171
+ if (verdict === undefined || verdict.ok)
1172
+ return undefined;
1173
+ return { message: verdict.message, ...(verdict.code !== undefined ? { ruleRejectCode: verdict.code } : {}) };
1174
+ }
1197
1175
  emitRevokeTo(ctxs, frame) {
1198
1176
  if (frame.askIds.length === 0)
1199
1177
  return;
1200
1178
  for (const c of ctxs)
1201
- if (c.family === "approval_request")
1202
- void deliverHitlNotice(c.transport, frame);
1179
+ void deliverHitlNotice(c.transport, frame);
1203
1180
  }
1204
1181
  emitRevoke(frame, target) {
1205
1182
  const targets = new Set();
@@ -1293,7 +1270,7 @@ export class ToolApprovalCoordinator {
1293
1270
  const aliveCtxs = ctxs.filter((c) => !c.abortSignal?.aborted);
1294
1271
  const reachability = aliveCtxs.length === 0 ? "unreached" : "reachable";
1295
1272
  if (reachability === "unreached") {
1296
- if (!this.unattendedRoutesToPark || !this.askStore) {
1273
+ if (!this.unattendedRoutesToPark) {
1297
1274
  return this.unattendedOutcome({ arm: "no_live_target", taskId: origin.taskId, ...(origin.owner !== null ? { owner: origin.owner } : {}), toolName: req.toolName, reachability });
1298
1275
  }
1299
1276
  }
@@ -1311,19 +1288,18 @@ export class ToolApprovalCoordinator {
1311
1288
  const id = uuidv7();
1312
1289
  const child = isChildAsk(req);
1313
1290
  const carrierLeg = child ? undefined : aliveCtxs.find((c) => c.taskId === origin.taskId);
1314
- const bounded = boundArgs(req.args);
1315
1291
  let effectiveGovernanceForced = governanceForced;
1316
1292
  let ruleLaneMaterial = this.buildRuleLaneMaterial(req, origin.owner, governanceForced, origin.sessionId);
1317
1293
  const mandateWord = readClosedWordSeat(req, "mandate");
1318
1294
  const noRulingCause = readClosedWordSeat(req, "classifierNoRuling");
1319
1295
  const grantAbsence = readClosedWordSeat(req, "boundGrantAbsence");
1320
1296
  const cardWord = readApprovalCard(req.approvalCard);
1297
+ const bounded = boundArgs(req.args, cardWord);
1321
1298
  const originWord = typeof req.origin === "string" && req.origin.length > 0 ? req.origin : undefined;
1322
1299
  const askClassPair = readAskClassPair(req);
1323
1300
  const askReasonsOfReq = readAskReasons(req);
1324
- const requester = extensionRequesterOf(req.requestedBy, "tool_approval");
1301
+ const requester = extensionRequesterOf(req.requestedBy, "approval_request");
1325
1302
  const frame = {
1326
- type: "tool_approval",
1327
1303
  approvalId: id,
1328
1304
  toolName: req.toolName,
1329
1305
  ...(typeof req.toolCallId === "string" && req.toolCallId !== "" ? { toolCallId: req.toolCallId } : {}),
@@ -1371,9 +1347,12 @@ export class ToolApprovalCoordinator {
1371
1347
  const windowBudgetMs = reachability === "unreached" ? this.unreachedTtlMs : this.ttlMs;
1372
1348
  const effectiveWindowMs = effectiveAskWindowMs(windowBudgetMs, this.windowMarginMs, origin.legDeadlineMonotonic, performance.now());
1373
1349
  const expiresAtMs = Math.ceil(Date.now() + effectiveWindowMs);
1374
- let askId;
1375
- let batchId;
1376
- const card = this.askStore ? buildApprovalCard(frame, req, askDemandsAPerson(req, undefined)) : undefined;
1350
+ const sourceSessionId = req.sourceSessionId ?? origin.taskId;
1351
+ const runId = origin.taskId;
1352
+ const legKey = origin.legKey;
1353
+ const askId = deriveAskId(sourceSessionId, runId, legKey, req);
1354
+ let batchId = this.batchIdFor(askId, sourceSessionId, runId, legKey);
1355
+ const card = buildApprovalCard(frame, req, askDemandsAPerson(req, undefined));
1377
1356
  let cardForFrame = card;
1378
1357
  let persistedExpiresAtMs = expiresAtMs;
1379
1358
  let factsOfRecord;
@@ -1384,11 +1363,10 @@ export class ToolApprovalCoordinator {
1384
1363
  let abandonedAs;
1385
1364
  const attemptCreatedAtMs = Date.now();
1386
1365
  const finishAbandonedRow = (orphan) => {
1387
- const store = this.askStore;
1388
- if (!store || abandonedAs === undefined)
1366
+ if (abandonedAs === undefined)
1389
1367
  return undefined;
1390
1368
  if (abandonedAs === "void") {
1391
- void store
1369
+ void this.askStore
1392
1370
  .transitionAsk(orphan.askId, "STREAM_PENDING", "VOID", { updatedAtMs: Date.now() })
1393
1371
  .catch((err) => this.noteStoreError(err, "transitionAsk(abandon-late-ensure)"));
1394
1372
  return undefined;
@@ -1405,142 +1383,131 @@ export class ToolApprovalCoordinator {
1405
1383
  ? this.unattendedOutcome({ arm: "replayed_parked", taskId: origin.taskId, ...(origin.owner !== null ? { owner: origin.owner } : {}), toolName: req.toolName, reachability })
1406
1384
  : replayed;
1407
1385
  };
1408
- if (this.askStore && card) {
1409
- const sourceSessionId = req.sourceSessionId ?? origin.taskId;
1410
- const runId = origin.taskId;
1411
- const legKey = origin.legKey;
1412
- askId = deriveAskId(sourceSessionId, runId, legKey, req);
1413
- batchId = this.batchIdFor(askId, sourceSessionId, runId, legKey);
1414
- try {
1415
- const existingOrCreated = await this.withStoreDeadline(this.askStore.ensureAsk({
1416
- askId,
1417
- taskId: origin.taskId,
1418
- sourceSessionId,
1419
- sessionId: origin.sessionId ?? origin.taskId,
1420
- owner: origin.owner,
1421
- executionLane: origin.executionLane,
1422
- batchId,
1423
- toolCallId: req.toolCallId,
1424
- legKey,
1425
- parentToolCallId: req.delegation?.parentToolCallId ?? null,
1426
- cardJson: buildApprovalCardEnvelope(askId, card, frame),
1427
- boundInputHash: readBoundInputHash(req),
1428
- ...durableRuleMaterial(ruleLaneMaterial),
1429
- schemaVersion: 1,
1430
- expiresAtMs,
1431
- createdAtMs: attemptCreatedAtMs,
1432
- }), "ensureAsk", DURABLE_CALL_TIMEOUT_MS, (late) => {
1433
- if (late.row.state !== "STREAM_PENDING") {
1434
- lateTerminalRow = late.row;
1435
- this.dispatchClaimTruth(late.row.askId, this.classifyClaimLoss(late.row.askId, projectAskTerminalClaimTruth(late.row), "late:ensureAsk"), APPROVAL_ENDING_CAUSES.UNDELIVERABLE);
1436
- return;
1437
- }
1438
- lateEnsuredRow = late.inserted ? { askId: late.row.askId, batchId: late.row.batchId } : undefined;
1439
- if (lateEnsuredRow !== undefined)
1440
- void finishAbandonedRow(lateEnsuredRow);
1441
- });
1442
- batchId = existingOrCreated.row.batchId;
1443
- if (existingOrCreated.row.state !== "STREAM_PENDING") {
1444
- releaseAdmission();
1445
- return replayTerminalRow(existingOrCreated.row);
1446
- }
1447
- const persistedEnvelope = readStoredCardEnvelope(existingOrCreated.row.cardJson);
1448
- const maxMintableWindowMs = Math.max(effectiveWindowMs, this.unreachedTtlMs);
1449
- const sanePersistedDeadline = Number.isFinite(existingOrCreated.row.expiresAtMs) && existingOrCreated.row.expiresAtMs <= Date.now() + maxMintableWindowMs + DURABLE_CALL_TIMEOUT_MS;
1450
- if (persistedEnvelope === undefined || !sanePersistedDeadline) {
1451
- this.noteStoreError(new Error(`persisted approval row is unusable (envelopeOk=${persistedEnvelope !== undefined}, deadlineOk=${sanePersistedDeadline}, ` +
1452
- `origin=${existingOrCreated.inserted ? "row-this-call-just-inserted" : "row-hit-idempotently"})`), "ensureAsk(persisted-row-unusable)");
1453
- releaseAdmission();
1454
- const unattended = this.unattendedOutcome({ arm: "persisted_row_unusable", taskId: origin.taskId, ...(origin.owner !== null ? { owner: origin.owner } : {}), toolName: req.toolName, reachability });
1455
- return existingOrCreated.inserted ? this.landUnattendedOutcome(unattended, existingOrCreated.row) : unattended;
1456
- }
1457
- persistedExpiresAtMs = existingOrCreated.row.expiresAtMs;
1458
- entryExecutionLane = existingOrCreated.row.executionLane;
1459
- cardForFrame = persistedEnvelope.card;
1460
- recordCard = persistedEnvelope.card;
1461
- factsOfRecord = existingOrCreated.inserted ? undefined : persistedEnvelope.liveFacts;
1462
- if (factsOfRecord !== undefined)
1463
- adoptStoredLiveFacts(frame, factsOfRecord);
1464
- if (cardForFrame.governanceForced === true)
1465
- frame.governanceForced = true;
1466
- else
1467
- delete frame.governanceForced;
1468
- if (cardForFrame.allowRuleShadowed !== undefined)
1469
- frame.allowRuleShadowed = cardForFrame.allowRuleShadowed;
1470
- else
1471
- delete frame.allowRuleShadowed;
1472
- if (cardForFrame.inputHasBidi === true)
1473
- frame.inputHasBidi = true;
1474
- else
1475
- delete frame.inputHasBidi;
1476
- if (cardForFrame.denialLimitFallback !== undefined)
1477
- frame.denialLimitFallback = cardForFrame.denialLimitFallback;
1478
- else
1479
- delete frame.denialLimitFallback;
1480
- if (cardForFrame.governanceForced === true && !effectiveGovernanceForced) {
1481
- effectiveGovernanceForced = true;
1482
- ruleLaneMaterial = undefined;
1483
- delete frame.ruleOffers;
1484
- if (cardForFrame.ruleOffers !== undefined) {
1485
- const { ruleOffers: _dropped, ...rest } = cardForFrame;
1486
- cardForFrame = rest;
1487
- }
1386
+ try {
1387
+ const existingOrCreated = await this.withStoreDeadline(this.askStore.ensureAsk({
1388
+ askId,
1389
+ taskId: origin.taskId,
1390
+ sourceSessionId,
1391
+ sessionId: origin.sessionId ?? origin.taskId,
1392
+ owner: origin.owner,
1393
+ executionLane: origin.executionLane,
1394
+ batchId,
1395
+ toolCallId: req.toolCallId,
1396
+ legKey,
1397
+ parentToolCallId: req.delegation?.parentToolCallId ?? null,
1398
+ cardJson: buildApprovalCardEnvelope(askId, card, frame),
1399
+ boundInputHash: readBoundInputHash(req),
1400
+ ...durableRuleMaterial(ruleLaneMaterial),
1401
+ schemaVersion: 1,
1402
+ expiresAtMs,
1403
+ createdAtMs: attemptCreatedAtMs,
1404
+ }), "ensureAsk", DURABLE_CALL_TIMEOUT_MS, (late) => {
1405
+ if (late.row.state !== "STREAM_PENDING") {
1406
+ lateTerminalRow = late.row;
1407
+ this.dispatchClaimTruth(late.row.askId, this.classifyClaimLoss(late.row.askId, projectAskTerminalClaimTruth(late.row), "late:ensureAsk"), APPROVAL_ENDING_CAUSES.UNDELIVERABLE);
1408
+ return;
1488
1409
  }
1489
- if (ruleLaneMaterial !== undefined && !ToolApprovalCoordinator.ruleOffersEqual(cardForFrame.ruleOffers, ruleLaneMaterial.offers)) {
1490
- this.noteStoreError(new Error("persisted approval row offers a different ruleOffers set than this leg — withdrawing the rule lane for this delivery"), "ensureAsk(rule-offers-drift)");
1491
- ruleLaneMaterial = undefined;
1492
- delete frame.ruleOffers;
1493
- if (cardForFrame.ruleOffers !== undefined) {
1494
- const { ruleOffers: _drifted, ...rest } = cardForFrame;
1495
- cardForFrame = rest;
1496
- }
1410
+ lateEnsuredRow = late.inserted ? { askId: late.row.askId, batchId: late.row.batchId } : undefined;
1411
+ if (lateEnsuredRow !== undefined)
1412
+ void finishAbandonedRow(lateEnsuredRow);
1413
+ });
1414
+ batchId = existingOrCreated.row.batchId;
1415
+ if (existingOrCreated.row.state !== "STREAM_PENDING") {
1416
+ releaseAdmission();
1417
+ return replayTerminalRow(existingOrCreated.row);
1418
+ }
1419
+ const persistedEnvelope = readStoredCardEnvelope(existingOrCreated.row.cardJson);
1420
+ const maxMintableWindowMs = Math.max(effectiveWindowMs, this.unreachedTtlMs);
1421
+ const sanePersistedDeadline = Number.isFinite(existingOrCreated.row.expiresAtMs) && existingOrCreated.row.expiresAtMs <= Date.now() + maxMintableWindowMs + DURABLE_CALL_TIMEOUT_MS;
1422
+ if (persistedEnvelope === undefined || !sanePersistedDeadline) {
1423
+ this.noteStoreError(new Error(`persisted approval row is unusable (envelopeOk=${persistedEnvelope !== undefined}, deadlineOk=${sanePersistedDeadline}, ` +
1424
+ `origin=${existingOrCreated.inserted ? "row-this-call-just-inserted" : "row-hit-idempotently"})`), "ensureAsk(persisted-row-unusable)");
1425
+ releaseAdmission();
1426
+ const unattended = this.unattendedOutcome({ arm: "persisted_row_unusable", taskId: origin.taskId, ...(origin.owner !== null ? { owner: origin.owner } : {}), toolName: req.toolName, reachability });
1427
+ return existingOrCreated.inserted ? this.landUnattendedOutcome(unattended, existingOrCreated.row) : unattended;
1428
+ }
1429
+ persistedExpiresAtMs = existingOrCreated.row.expiresAtMs;
1430
+ entryExecutionLane = existingOrCreated.row.executionLane;
1431
+ cardForFrame = persistedEnvelope.card;
1432
+ recordCard = persistedEnvelope.card;
1433
+ factsOfRecord = existingOrCreated.inserted ? undefined : persistedEnvelope.liveFacts;
1434
+ if (factsOfRecord !== undefined)
1435
+ adoptStoredLiveFacts(frame, factsOfRecord);
1436
+ if (cardForFrame.governanceForced === true)
1437
+ frame.governanceForced = true;
1438
+ else
1439
+ delete frame.governanceForced;
1440
+ if (cardForFrame.allowRuleShadowed !== undefined)
1441
+ frame.allowRuleShadowed = cardForFrame.allowRuleShadowed;
1442
+ else
1443
+ delete frame.allowRuleShadowed;
1444
+ if (cardForFrame.inputHasBidi === true)
1445
+ frame.inputHasBidi = true;
1446
+ else
1447
+ delete frame.inputHasBidi;
1448
+ if (cardForFrame.denialLimitFallback !== undefined)
1449
+ frame.denialLimitFallback = cardForFrame.denialLimitFallback;
1450
+ else
1451
+ delete frame.denialLimitFallback;
1452
+ if (cardForFrame.governanceForced === true && !effectiveGovernanceForced) {
1453
+ effectiveGovernanceForced = true;
1454
+ ruleLaneMaterial = undefined;
1455
+ delete frame.ruleOffers;
1456
+ if (cardForFrame.ruleOffers !== undefined) {
1457
+ const { ruleOffers: _dropped, ...rest } = cardForFrame;
1458
+ cardForFrame = rest;
1497
1459
  }
1498
1460
  }
1499
- catch (err) {
1500
- this.noteStoreError(err, "ensureAsk");
1501
- let confirmedRowExists = false;
1502
- let indeterminate = true;
1503
- for (let attempt = 0; attempt <= DURABLE_CONVERGE_BACKOFF_MS.length; attempt++) {
1504
- try {
1505
- const confirmedRow = await this.withStoreDeadline(this.askStore.getAsk(askId), "getAsk(ensureAsk-confirm)");
1506
- confirmedRowExists = confirmedRow !== null;
1507
- if (confirmedRow !== null)
1508
- batchId = confirmedRow.batchId;
1509
- if (confirmedRow !== null)
1510
- recordCard = storedPersonDemandRecord(confirmedRow.cardJson);
1511
- indeterminate = false;
1512
- break;
1513
- }
1514
- catch (readErr) {
1515
- this.noteStoreError(readErr, "getAsk(ensureAsk-confirm)");
1516
- if (attempt < DURABLE_CONVERGE_BACKOFF_MS.length)
1517
- await new Promise((r) => setTimeout(r, DURABLE_CONVERGE_BACKOFF_MS[attempt]).unref?.());
1518
- }
1519
- }
1520
- if (lateTerminalRow !== undefined) {
1521
- releaseAdmission();
1522
- return replayTerminalRow(lateTerminalRow);
1461
+ if (ruleLaneMaterial !== undefined && !ToolApprovalCoordinator.ruleOffersEqual(cardForFrame.ruleOffers, ruleLaneMaterial.offers)) {
1462
+ this.noteStoreError(new Error("persisted approval row offers a different ruleOffers set than this leg — withdrawing the rule lane for this delivery"), "ensureAsk(rule-offers-drift)");
1463
+ ruleLaneMaterial = undefined;
1464
+ delete frame.ruleOffers;
1465
+ if (cardForFrame.ruleOffers !== undefined) {
1466
+ const { ruleOffers: _drifted, ...rest } = cardForFrame;
1467
+ cardForFrame = rest;
1523
1468
  }
1524
- if (indeterminate) {
1525
- const unattended = this.unattendedOutcome({ arm: "ensure_indeterminate", taskId: origin.taskId, ...(origin.owner !== null ? { owner: origin.owner } : {}), toolName: req.toolName, reachability });
1526
- const landed = abandonRow(unattended);
1527
- releaseAdmission();
1528
- return landed ?? unattended;
1469
+ }
1470
+ }
1471
+ catch (err) {
1472
+ this.noteStoreError(err, "ensureAsk");
1473
+ let confirmedRowExists = false;
1474
+ let indeterminate = true;
1475
+ for (let attempt = 0; attempt <= DURABLE_CONVERGE_BACKOFF_MS.length; attempt++) {
1476
+ try {
1477
+ const confirmedRow = await this.withStoreDeadline(this.askStore.getAsk(askId), "getAsk(ensureAsk-confirm)");
1478
+ confirmedRowExists = confirmedRow !== null;
1479
+ if (confirmedRow !== null)
1480
+ batchId = confirmedRow.batchId;
1481
+ if (confirmedRow !== null)
1482
+ recordCard = storedPersonDemandRecord(confirmedRow.cardJson);
1483
+ indeterminate = false;
1484
+ break;
1529
1485
  }
1530
- if (!confirmedRowExists) {
1531
- askId = undefined;
1532
- batchId = undefined;
1533
- if (!aliveCtxs.some(canPresentWithoutDurableRow)) {
1534
- const unattended = this.unattendedOutcome({ arm: "no_row_no_presenter", taskId: origin.taskId, ...(origin.owner !== null ? { owner: origin.owner } : {}), toolName: req.toolName, reachability });
1535
- const landed = abandonRow(unattended);
1536
- releaseAdmission();
1537
- return landed ?? unattended;
1538
- }
1539
- void abandonRow(undefined);
1486
+ catch (readErr) {
1487
+ this.noteStoreError(readErr, "getAsk(ensureAsk-confirm)");
1488
+ if (attempt < DURABLE_CONVERGE_BACKOFF_MS.length)
1489
+ await new Promise((r) => setTimeout(r, DURABLE_CONVERGE_BACKOFF_MS[attempt]).unref?.());
1540
1490
  }
1541
1491
  }
1492
+ if (lateTerminalRow !== undefined) {
1493
+ releaseAdmission();
1494
+ return replayTerminalRow(lateTerminalRow);
1495
+ }
1496
+ if (indeterminate) {
1497
+ const unattended = this.unattendedOutcome({ arm: "ensure_indeterminate", taskId: origin.taskId, ...(origin.owner !== null ? { owner: origin.owner } : {}), toolName: req.toolName, reachability });
1498
+ const landed = abandonRow(unattended);
1499
+ releaseAdmission();
1500
+ return landed ?? unattended;
1501
+ }
1502
+ if (!confirmedRowExists) {
1503
+ const unattended = this.unattendedOutcome({ arm: "no_row_no_presenter", taskId: origin.taskId, ...(origin.owner !== null ? { owner: origin.owner } : {}), toolName: req.toolName, reachability });
1504
+ const landed = abandonRow(unattended);
1505
+ releaseAdmission();
1506
+ return landed ?? unattended;
1507
+ }
1542
1508
  }
1543
- const wireApprovalId = askId ?? id;
1509
+ const wireApprovalId = askId;
1510
+ const rowCard = cardForFrame;
1544
1511
  frame.approvalId = wireApprovalId;
1545
1512
  let done = false;
1546
1513
  let emitFailed = false;
@@ -1600,27 +1567,23 @@ export class ToolApprovalCoordinator {
1600
1567
  for (const [sig, fn] of abortListeners)
1601
1568
  sig.removeEventListener("abort", fn);
1602
1569
  this.pending.delete(id);
1603
- if (windowRouteUnavailable && askId !== undefined && batchId !== undefined && this.unattendedRoutesToPark) {
1570
+ if (windowRouteUnavailable && this.unattendedRoutesToPark) {
1604
1571
  parkRouted = true;
1605
1572
  }
1606
1573
  let lastLiveRegistration = false;
1607
- if (askId !== undefined) {
1608
- const siblings = this.pendingByAskId.get(askId);
1609
- if (siblings && selfEntry) {
1610
- siblings.delete(selfEntry);
1611
- if (siblings.size === 0) {
1612
- this.pendingByAskId.delete(askId);
1613
- lastLiveRegistration = true;
1614
- }
1574
+ const siblings = this.pendingByAskId.get(askId);
1575
+ if (siblings && selfEntry) {
1576
+ siblings.delete(selfEntry);
1577
+ if (siblings.size === 0) {
1578
+ this.pendingByAskId.delete(askId);
1579
+ lastLiveRegistration = true;
1615
1580
  }
1616
1581
  }
1617
1582
  lastOutcome = outcome;
1618
1583
  lastCause = verdict.by === "system" ? verdict.cause : undefined;
1619
- if (askId !== undefined) {
1620
- completeFrame = buildApprovalCompleteFrame({ askId, taskId: origin.taskId, approvalId: wireApprovalId, outcome, ...(lastCause !== undefined ? { cause: lastCause } : {}), parked: parkRouted, nowMs: Date.now() });
1621
- if (lastLiveRegistration)
1622
- this.notifyPresentedCardWatchers(askId, completeFrame);
1623
- }
1584
+ completeFrame = buildApprovalCompleteFrame({ askId, taskId: origin.taskId, approvalId: wireApprovalId, outcome, ...(lastCause !== undefined ? { cause: lastCause } : {}), parked: parkRouted, nowMs: Date.now() });
1585
+ if (lastLiveRegistration)
1586
+ this.notifyPresentedCardWatchers(askId, completeFrame);
1624
1587
  releaseAdmission();
1625
1588
  if (selfEntry)
1626
1589
  selfEntry.settledOutcome = outcome;
@@ -1652,8 +1615,6 @@ export class ToolApprovalCoordinator {
1652
1615
  resolveAllowed({ allowed, ...(allowed && updatedInput !== undefined ? { updatedInput } : {}), ...(allowed && readRootGrant !== undefined ? { readRootGrant } : {}) });
1653
1616
  };
1654
1617
  const broadcastClaimTruth = (read, armCause) => {
1655
- if (askId === undefined)
1656
- return;
1657
1618
  this.dispatchClaimTruth(askId, read, armCause);
1658
1619
  };
1659
1620
  const settleFromClaimLoss = (read, armCause) => {
@@ -1682,8 +1643,6 @@ export class ToolApprovalCoordinator {
1682
1643
  }
1683
1644
  };
1684
1645
  const lateClaimLossSettle = (current, intent, armCause) => {
1685
- if (askId === undefined)
1686
- return;
1687
1646
  const read = this.classifyClaimLoss(askId, current, `late:${intent}`);
1688
1647
  if (done) {
1689
1648
  broadcastClaimTruth(read, armCause);
@@ -1700,10 +1659,6 @@ export class ToolApprovalCoordinator {
1700
1659
  return this.withStoreDeadline(raw, label, timeoutMs, late);
1701
1660
  };
1702
1661
  const resolveLocalOutcome = async (arm) => {
1703
- if (askId === undefined) {
1704
- arm.settleByIntent();
1705
- return;
1706
- }
1707
1662
  const pinnedAskId = askId;
1708
1663
  let lastLoss;
1709
1664
  const ladderStartedAt = Date.now();
@@ -1799,10 +1754,6 @@ export class ToolApprovalCoordinator {
1799
1754
  };
1800
1755
  const windowExpired = (hostSelfReport, cause) => {
1801
1756
  void (async () => {
1802
- if (!this.askStore || askId === undefined || batchId === undefined) {
1803
- settleParkRouteIfUnsettled(hostSelfReport, cause);
1804
- return;
1805
- }
1806
1757
  const store = this.askStore;
1807
1758
  const pinnedAskId = askId;
1808
1759
  const pinnedBatchId = batchId;
@@ -1833,19 +1784,11 @@ export class ToolApprovalCoordinator {
1833
1784
  const forceParkNow = (cause) => {
1834
1785
  if (done)
1835
1786
  return;
1836
- if (!this.askStore || askId === undefined || batchId === undefined) {
1837
- settleParkRouteIfUnsettled(undefined, cause);
1838
- return;
1839
- }
1840
1787
  windowExpired(undefined, cause);
1841
1788
  };
1842
- timer = setTimeout(() => windowExpired("timeout", APPROVAL_ENDING_CAUSES.TTL_EXPIRED), this.askStore ? Math.max(0, persistedExpiresAtMs - Date.now()) : effectiveWindowMs);
1789
+ timer = setTimeout(() => windowExpired("timeout", APPROVAL_ENDING_CAUSES.TTL_EXPIRED), Math.max(0, persistedExpiresAtMs - Date.now()));
1843
1790
  timer.unref?.();
1844
1791
  const runCancel = async (cause) => {
1845
- if (!this.askStore || askId === undefined || batchId === undefined) {
1846
- settle({ by: "system", cause });
1847
- return;
1848
- }
1849
1792
  if (this.hasOtherSuspendedOwner(askId, selfEntry)) {
1850
1793
  settle({ by: "system", cause });
1851
1794
  return;
@@ -1907,31 +1850,27 @@ export class ToolApprovalCoordinator {
1907
1850
  const legGrantBinding = grantBindingOf(req);
1908
1851
  const entryGrantBinding = factsOfRecord === undefined ? legGrantBinding : agreedGrantBinding(legGrantBinding, grantBindingOf(factsOfRecord));
1909
1852
  const entryMandated = req.mandated === true || factsOfRecord?.mandated === true;
1910
- const entryDemandsAPerson = askDemandsAPerson(req, recordCard) || [...(askId !== undefined ? (this.pendingByAskId.get(askId) ?? []) : [])].some((s) => s.requiresRealApproval === true);
1911
- cardForFrame = projectDecisionFacts(pendingEntryStandingAnswerFacts({
1853
+ const entryDemandsAPerson = askDemandsAPerson(req, recordCard) || [...(this.pendingByAskId.get(askId) ?? [])].some((s) => s.requiresRealApproval === true);
1854
+ const presentedCard = projectDecisionFacts(pendingEntryStandingAnswerFacts({
1912
1855
  ...(effectiveGovernanceForced ? { governanceForced: true } : {}),
1913
1856
  ...(entryDemandsAPerson ? { requiresRealApproval: true } : {}),
1914
1857
  ...(entryMandated ? { mandated: true } : {}),
1915
1858
  grantBinding: entryGrantBinding,
1916
1859
  classes: entryClasses,
1917
1860
  ...(entryDeploymentListQuestion ? { deploymentListQuestion: true } : {}),
1918
- }), origin, frame, cardForFrame);
1919
- const cardFrame = askId !== undefined && cardForFrame !== undefined
1920
- ? buildApprovalRequestFrame({ askId, taskId: origin.taskId, approvalId: wireApprovalId, card: cardForFrame, expiresAtMs: persistedExpiresAtMs, nowMs: Date.now(), live: frame })
1921
- : undefined;
1861
+ }), origin, frame, rowCard);
1862
+ const cardFrame = buildApprovalRequestFrame({ askId: wireApprovalId, taskId: origin.taskId, approvalId: wireApprovalId, card: presentedCard, expiresAtMs: persistedExpiresAtMs, nowMs: Date.now(), live: frame });
1922
1863
  const pendingEntry = {
1923
1864
  settle,
1924
1865
  forceParkNow,
1925
1866
  settleParkRoute: (cause) => settleParkRouteIfUnsettled(undefined, cause),
1926
- owner: origin.owner,
1927
- ownerReadSeq: this.rowReadSeq,
1928
1867
  ...(primary ? { ctxTaskId: primary.taskId, originCtx: primary } : {}),
1929
1868
  ...(reachability === "unreached" ? { suspended: true } : {}),
1930
- ...(child ? { originTaskId: req.sourceSessionId } : primary ? { targetCtxs: new Set(aliveCtxs), onTargetGone } : {}),
1869
+ ...(child ? { sourceSessionId: req.sourceSessionId } : primary ? { targetCtxs: new Set(aliveCtxs), onTargetGone } : {}),
1931
1870
  ...(origin.sessionId ? { sessionId: origin.sessionId } : {}),
1932
1871
  ...(origin.incarnation !== undefined ? { incarnation: origin.incarnation } : {}),
1933
1872
  frame,
1934
- ...(cardFrame !== undefined ? { cardFrame } : {}),
1873
+ cardFrame,
1935
1874
  approvalId: wireApprovalId,
1936
1875
  registrationId: id,
1937
1876
  toolName: req.toolName,
@@ -1941,31 +1880,29 @@ export class ToolApprovalCoordinator {
1941
1880
  registeredAtMs: Date.now(),
1942
1881
  expiresAtMs: persistedExpiresAtMs,
1943
1882
  executionLane: entryExecutionLane,
1944
- ...(ruleLaneMaterial !== undefined ? { ruleLane: ruleLaneMaterial } : {}),
1945
1883
  ...(effectiveGovernanceForced ? { governanceForced: true } : {}),
1946
1884
  ...(entryDemandsAPerson ? { requiresRealApproval: true } : {}),
1947
1885
  ...(entryMandated ? { mandated: true } : {}),
1948
- ...(askId !== undefined && batchId !== undefined ? { askId, batchId } : {}),
1886
+ askId,
1887
+ batchId,
1949
1888
  };
1950
1889
  selfEntry = pendingEntry;
1951
1890
  this.pending.set(id, pendingEntry);
1952
- if (askId !== undefined) {
1953
- this.releaseAdoptedRegistrations(askId);
1954
- let siblings = this.pendingByAskId.get(askId);
1955
- if (!siblings) {
1956
- siblings = new Set();
1957
- this.pendingByAskId.set(askId, siblings);
1958
- }
1959
- siblings.add(pendingEntry);
1960
- if (entryDemandsAPerson) {
1961
- for (const s of siblings) {
1962
- if (s.requiresRealApproval === true)
1963
- continue;
1964
- s.requiresRealApproval = true;
1965
- const card = projectDecisionFacts(pendingEntryStandingAnswerFacts(s), s, s.frame, s.cardFrame?.card);
1966
- if (s.cardFrame !== undefined && card !== undefined && card !== s.cardFrame.card)
1967
- s.cardFrame = { ...s.cardFrame, card };
1968
- }
1891
+ this.releaseAdoptedRegistrations(askId);
1892
+ let siblings = this.pendingByAskId.get(askId);
1893
+ if (!siblings) {
1894
+ siblings = new Set();
1895
+ this.pendingByAskId.set(askId, siblings);
1896
+ }
1897
+ siblings.add(pendingEntry);
1898
+ if (entryDemandsAPerson) {
1899
+ for (const s of siblings) {
1900
+ if (s.requiresRealApproval === true)
1901
+ continue;
1902
+ s.requiresRealApproval = true;
1903
+ const card = projectDecisionFacts(pendingEntryStandingAnswerFacts(s), s, s.frame, s.cardFrame?.card);
1904
+ if (s.cardFrame !== undefined && card !== undefined && card !== s.cardFrame.card)
1905
+ s.cardFrame = { ...s.cardFrame, card };
1969
1906
  }
1970
1907
  }
1971
1908
  this.askAudit?.recordAsk({
@@ -1988,73 +1925,49 @@ export class ToolApprovalCoordinator {
1988
1925
  frame.serverNowMs = serverNowMs;
1989
1926
  frame.expiresInMs = Math.max(0, persistedExpiresAtMs - serverNowMs);
1990
1927
  }
1991
- const cardCtxs = aliveCtxs.filter((c) => c.family === "approval_request");
1992
- const legacyCtxs = aliveCtxs.filter((c) => c.family === "tool_approval");
1993
- if (cardFrame === undefined && cardCtxs.length > 0) {
1994
- defaultLogger.warn("approval card leg cannot present this ask: it has no confirmed durable identity (no approval_request frame is minted without one)", {
1995
- taskId: origin.taskId,
1996
- approvalId: wireApprovalId,
1997
- toolName: req.toolName,
1998
- });
1999
- }
2000
- const cardRace = cardFrame !== undefined && cardCtxs.length > 0
2001
- ? createHitlOpenRace({ targets: cardCtxs.map((c) => ({ transport: c.transport, frame: cardFrame })), settled: allowedP })
2002
- : undefined;
2003
- const legacyRace = legacyCtxs.length > 0 ? createHitlOpenRace({ targets: legacyCtxs.map((c) => ({ transport: c.transport, frame })), settled: allowedP }) : undefined;
2004
- if (cardRace !== undefined && cardFrame !== undefined)
1928
+ const cardCtxs = aliveCtxs;
1929
+ const cardRace = cardCtxs.length > 0 ? createHitlOpenRace({ targets: cardCtxs.map((c) => ({ transport: c.transport, frame: cardFrame })), settled: allowedP }) : undefined;
1930
+ if (cardRace !== undefined)
2005
1931
  cardCtxs.forEach((c, i) => this.notePresentedCardRegistration(cardFrame.askId, c.transport, cardRace.acceptedLive(i)));
2006
1932
  const emitAllP = (async () => {
2007
- const landings = await Promise.all([cardRace?.landing, legacyRace?.landing]);
2008
- const anySucceeded = reachability === "unreached" || this.hasOtherSuspendedOwner(askId, selfEntry) || landings.includes("delivered");
1933
+ const landing = await cardRace?.landing;
1934
+ const anySucceeded = reachability === "unreached" || this.hasOtherSuspendedOwner(askId, selfEntry) || landing === "delivered";
2009
1935
  emitSettled = true;
2010
1936
  if (!anySucceeded && !done) {
2011
- if (this.askStore && askId !== undefined && batchId !== undefined) {
2012
- const store = this.askStore;
2013
- const pinnedAskId = askId;
2014
- const pinnedBatchId = batchId;
2015
- const settleUnreachable = () => {
2016
- if (done)
2017
- return;
2018
- emitFailed = true;
2019
- settle({ by: "system", cause: APPROVAL_ENDING_CAUSES.BROADCAST_LOST });
2020
- };
2021
- await resolveLocalOutcome({
2022
- intent: "unreachable",
2023
- cause: APPROVAL_ENDING_CAUSES.BROADCAST_LOST,
2024
- storeLabel: "claimTerminal(emit-failed)",
2025
- claim: (timeoutMs) => boundedClaim(() => settleByPolicy(store, { askId: pinnedAskId, batchId: pinnedBatchId }, this.unattendedPolicy, APPROVAL_ENDING_CAUSES.BROADCAST_LOST), "claimTerminal(emit-failed)", timeoutMs, (late) => {
2026
- if (!late.claimed) {
2027
- lateClaimLossSettle(late.current, "unreachable", APPROVAL_ENDING_CAUSES.BROADCAST_LOST);
2028
- return;
2029
- }
2030
- this.settleSameAskIdParkRoute(pinnedAskId, APPROVAL_ENDING_CAUSES.BROADCAST_LOST);
2031
- this.settleVoidedSiblings(late.voidedSiblings, APPROVAL_ENDING_CAUSES.BATCH_SIBLING_WITHDRAWN);
2032
- }),
2033
- onWon: (res) => {
2034
- settleUnreachable();
2035
- this.settleSameAskIdParkRoute(pinnedAskId, APPROVAL_ENDING_CAUSES.BROADCAST_LOST);
2036
- this.settleVoidedSiblings(res.voidedSiblings, APPROVAL_ENDING_CAUSES.BATCH_SIBLING_WITHDRAWN);
2037
- },
2038
- settleByIntent: settleUnreachable,
2039
- });
2040
- }
2041
- else if (!done) {
1937
+ const store = this.askStore;
1938
+ const pinnedAskId = askId;
1939
+ const pinnedBatchId = batchId;
1940
+ const settleUnreachable = () => {
1941
+ if (done)
1942
+ return;
2042
1943
  emitFailed = true;
2043
1944
  settle({ by: "system", cause: APPROVAL_ENDING_CAUSES.BROADCAST_LOST });
2044
- }
1945
+ };
1946
+ await resolveLocalOutcome({
1947
+ intent: "unreachable",
1948
+ cause: APPROVAL_ENDING_CAUSES.BROADCAST_LOST,
1949
+ storeLabel: "claimTerminal(emit-failed)",
1950
+ claim: (timeoutMs) => boundedClaim(() => settleByPolicy(store, { askId: pinnedAskId, batchId: pinnedBatchId }, this.unattendedPolicy, APPROVAL_ENDING_CAUSES.BROADCAST_LOST), "claimTerminal(emit-failed)", timeoutMs, (late) => {
1951
+ if (!late.claimed) {
1952
+ lateClaimLossSettle(late.current, "unreachable", APPROVAL_ENDING_CAUSES.BROADCAST_LOST);
1953
+ return;
1954
+ }
1955
+ this.settleSameAskIdParkRoute(pinnedAskId, APPROVAL_ENDING_CAUSES.BROADCAST_LOST);
1956
+ this.settleVoidedSiblings(late.voidedSiblings, APPROVAL_ENDING_CAUSES.BATCH_SIBLING_WITHDRAWN);
1957
+ }),
1958
+ onWon: (res) => {
1959
+ settleUnreachable();
1960
+ this.settleSameAskIdParkRoute(pinnedAskId, APPROVAL_ENDING_CAUSES.BROADCAST_LOST);
1961
+ this.settleVoidedSiblings(res.voidedSiblings, APPROVAL_ENDING_CAUSES.BATCH_SIBLING_WITHDRAWN);
1962
+ },
1963
+ settleByIntent: settleUnreachable,
1964
+ });
2045
1965
  }
2046
1966
  })();
2047
1967
  await Promise.race([emitAllP, allowedP]);
2048
1968
  const settled = await allowedP;
2049
1969
  if (cardRace !== undefined && completeFrame !== undefined)
2050
1970
  cardRace.close(completeFrame);
2051
- legacyRace?.close({
2052
- type: "tool_approval_complete",
2053
- approvalId: wireApprovalId,
2054
- outcome: lastOutcome,
2055
- ...(lastCause !== undefined ? { cause: lastCause } : {}),
2056
- ...(parkRouted ? { parked: true } : {}),
2057
- });
2058
1971
  if (emitFailed)
2059
1972
  return this.unattendedOutcome({ arm: "emit_failed", taskId: origin.taskId, ...(origin.owner !== null ? { owner: origin.owner } : {}), toolName: req.toolName, reachability });
2060
1973
  if (windowRouteUnavailable)
@@ -2063,110 +1976,6 @@ export class ToolApprovalCoordinator {
2063
1976
  return this.unattendedOutcome({ arm: "emit_unsettled", taskId: origin.taskId, ...(origin.owner !== null ? { owner: origin.owner } : {}), toolName: req.toolName, reachability });
2064
1977
  return shapeOutcome(settled);
2065
1978
  }
2066
- respond(id, principal, body, httpReq, deployment) {
2067
- const parsed = parseToolApprovalResponse(body);
2068
- if (!parsed.ok)
2069
- return { status: 400, body: refusalBody({ errorCode: "request.body_shape", message: parsed.error, facts: { ...(parsed.field !== undefined ? { field: parsed.field } : {}) } }) };
2070
- if (parsed.readRootGrant !== undefined && deployment?.readRootGrantAccepted !== true) {
2071
- return { status: 400, body: refusalBody({ errorCode: "request.body_shape", message: READ_ROOT_GRANT_NOT_ACCEPTED }) };
2072
- }
2073
- const entry = this.liveEntryFor(id);
2074
- if (entry?.adopted === true)
2075
- return this.respondByRow(entry, id, principal, parsed, httpReq);
2076
- if (entry !== undefined && askOwnerAdmitsAnswer(entry.owner, principal))
2077
- return this.answerRegistration(entry, entry.owner, id, principal, parsed, httpReq);
2078
- if (entry !== undefined && this.registrationHasRow(entry))
2079
- return this.respondByRow(entry, id, principal, parsed, httpReq);
2080
- return this.respondLate(id, principal, parsed, httpReq);
2081
- }
2082
- registrationHasRow(entry) {
2083
- return this.askStore !== undefined && entry.askId !== undefined;
2084
- }
2085
- async respondByRow(entry, id, principal, parsed, httpReq) {
2086
- const store = this.askStore;
2087
- if (!store)
2088
- return this.askNotVisible();
2089
- const row = await this.askRowFollowingOwner(store, id, "getAsk(respond-by-row)");
2090
- if (!askRowVisibleTo(row, principal))
2091
- return this.askNotVisible();
2092
- if (row.state !== "STREAM_PENDING")
2093
- return this.dispatchRowTruth(row, id, parsed, httpReq, principal);
2094
- return this.answerRegistration(entry, row.owner, id, principal, parsed, httpReq);
2095
- }
2096
- answerRegistration(entry, judgedOwner, id, principal, parsed, httpReq) {
2097
- const preRule = this.precheckEditedRuleBeforeDecision(entry, judgedOwner, parsed);
2098
- if (preRule !== undefined)
2099
- return preRule;
2100
- if (parsed.value !== "deny" && policyCannotAnswer(heldAskOf(entry.requiresRealApproval === true), parsed.decider.decidedBy)) {
2101
- return { status: 400, body: refusalBody({ errorCode: "request.body_shape", message: POLICY_CANNOT_ALLOW_PERSON_ASK }) };
2102
- }
2103
- const ruleEligibility = { eligible: true };
2104
- const store = this.askStore;
2105
- if (store && entry.askId !== undefined && entry.batchId !== undefined) {
2106
- return this.respondWithCas(store, entry.askId, entry.batchId, judgedOwner, entry, parsed, ruleEligibility).then((cas) => cas.lost ? this.dispatchRowTruth(cas.row, id, parsed, httpReq, principal) : this.afterLiveDecision(cas.result, entry, judgedOwner, parsed, principal, ruleEligibility));
2107
- }
2108
- return this.afterLiveDecision(this.finishRespond(entry, judgedOwner, parsed), entry, judgedOwner, parsed, principal, ruleEligibility);
2109
- }
2110
- afterLiveDecision(decided, entry, judgedOwner, parsed, principal, ruleEligibility) {
2111
- const settled = this.recordDeploymentListAnswer(decided, entry, judgedOwner, parsed, principal, ruleEligibility);
2112
- if (parsed.persistRule === undefined)
2113
- return settled;
2114
- return this.persistRuleAfterDecision(settled, entry, judgedOwner, parsed.persistRule, parsed.updatedInput !== undefined, ruleEligibility);
2115
- }
2116
- recordDeploymentListAnswer(decided, entry, judgedOwner, parsed, principal, eligibility) {
2117
- const store = this.sessionGrants;
2118
- const answer = liveSessionAnswer(pendingEntryStandingAnswerFacts(entry), entry);
2119
- if (parsed.value !== "allow_session" || answer.home !== "server_store" || store === undefined)
2120
- return decided;
2121
- const sessionId = answer.sessionId;
2122
- return (async () => {
2123
- const result = await decided;
2124
- if (result.status !== 200)
2125
- return result;
2126
- if (!eligibility.eligible)
2127
- return { ...result, body: { ...result.body, rememberApplied: false } };
2128
- let rememberApplied = false;
2129
- try {
2130
- await this.withStoreDeadline(store.grant(sessionId, entry.toolName, principal ?? judgedOwner), "sessionGrants.grant(deployment-list)", DURABLE_CALL_TIMEOUT_MS);
2131
- rememberApplied = true;
2132
- defaultLogger.info("approval_exemption_granted", { sessionId, toolName: entry.toolName, grantedBy: principal ?? judgedOwner });
2133
- }
2134
- catch (err) {
2135
- defaultLogger.warn("approval_exemption_grant_failed", { sessionId, toolName: entry.toolName, error: String(err).slice(0, 200) });
2136
- }
2137
- return { ...result, body: { ...result.body, rememberApplied } };
2138
- })();
2139
- }
2140
- precheckEditedRuleBeforeDecision(entry, judgedOwner, parsed) {
2141
- const persistRule = parsed.persistRule;
2142
- if (persistRule === undefined || persistRule.kind !== "text" || !persistRule.edited)
2143
- return undefined;
2144
- if (parsed.updatedInput !== undefined)
2145
- return undefined;
2146
- if (entry.governanceForced === true)
2147
- return undefined;
2148
- const command = entry.ruleLane?.command;
2149
- if (this.ruleConsent === undefined || command === undefined || judgedOwner === null)
2150
- return undefined;
2151
- const verdict = precheckCardRuleText(persistRule.rule, command);
2152
- if (verdict === undefined || verdict.ok)
2153
- return undefined;
2154
- return {
2155
- status: 400,
2156
- body: refusalBody({
2157
- errorCode: "request.body_shape",
2158
- message: verdict.message,
2159
- facts: {
2160
- ...(verdict.code !== undefined ? { ruleRejectCode: verdict.code } : {}),
2161
- },
2162
- }),
2163
- };
2164
- }
2165
- editedArmEcho(persistRule, persisted) {
2166
- if (persistRule.kind !== "text" || !persistRule.edited || !persisted.ok || persisted.kind !== "single")
2167
- return {};
2168
- return { persistedRule: persisted.rule };
2169
- }
2170
1979
  notPending(cause) {
2171
1980
  return {
2172
1981
  status: 404,
@@ -2180,34 +1989,24 @@ export class ToolApprovalCoordinator {
2180
1989
  };
2181
1990
  }
2182
1991
  askNotVisible() {
2183
- return { status: 404, body: refusalBody({ errorCode: "not_found.ask", message: "no such approval ask (never existed, no longer kept, or not yours)" }) };
1992
+ return { status: 404, body: refusalBody({ errorCode: "not_found.ask", message: ASK_NOT_FOUND_UNDER_TASK }) };
2184
1993
  }
2185
- readApproval(id, principal) {
2186
- const entry = this.liveEntryFor(id);
2187
- if (entry !== undefined && this.registrationHasRow(entry))
2188
- return this.readByRow(entry, id, principal);
2189
- if (entry !== undefined && askOwnerAdmitsAnswer(entry.owner, principal)) {
2190
- const body = { state: "pending", ...livePendingRowOf(entry) };
2191
- return { status: 200, body };
2192
- }
2193
- const store = this.askStore;
2194
- if (!store)
2195
- return this.askNotVisible();
2196
- return this.readAskRowState(store, id, principal);
1994
+ readAsk(taskId, askId, principal) {
1995
+ const entry = this.liveEntryFor(askId);
1996
+ if (entry !== undefined)
1997
+ return this.readByRow(entry, taskId, askId, principal);
1998
+ return this.readAskRowState(taskId, askId, principal);
2197
1999
  }
2198
- async readByRow(entry, id, principal) {
2199
- const store = this.askStore;
2200
- if (!store)
2201
- return this.askNotVisible();
2202
- const row = await this.askRowFollowingOwner(store, id, "getAsk(read-by-row)");
2203
- if (askRowVisibleTo(row, principal) && row.state === "STREAM_PENDING") {
2000
+ async readByRow(entry, taskId, askId, principal) {
2001
+ const row = await this.askRowFor(this.askStore, askId, "getAsk(read-by-row)");
2002
+ if (askRowVisibleUnder(row, taskId, principal) && row.state === "STREAM_PENDING") {
2204
2003
  const body = { state: "pending", ...livePendingRowOf(entry) };
2205
2004
  return { status: 200, body };
2206
2005
  }
2207
- return this.readStateOfRow(row, principal);
2006
+ return this.readStateOfRow(row, taskId, principal);
2208
2007
  }
2209
- async readAskRowState(store, id, principal) {
2210
- return this.readStateOfRow(await this.askRowFor(store, id, "getAsk(read)"), principal);
2008
+ async readAskRowState(taskId, askId, principal) {
2009
+ return this.readStateOfRow(await this.askRowFor(this.askStore, askId, "getAsk(read)"), taskId, principal);
2211
2010
  }
2212
2011
  async readParkedGate(row) {
2213
2012
  const gate = this.parkedGate;
@@ -2221,12 +2020,8 @@ export class ToolApprovalCoordinator {
2221
2020
  return undefined;
2222
2021
  }
2223
2022
  }
2224
- async openGateGeneration(row) {
2225
- const gate = await this.readParkedGate(row);
2226
- return gate?.kind === "open" ? gate.generation : undefined;
2227
- }
2228
- async readStateOfRow(row, principal) {
2229
- if (!askRowVisibleTo(row, principal))
2023
+ async readStateOfRow(row, taskId, principal) {
2024
+ if (!askRowVisibleUnder(row, taskId, principal))
2230
2025
  return this.askNotVisible();
2231
2026
  const projection = buildAskRowReadProjection(row, Date.now());
2232
2027
  let body;
@@ -2286,12 +2081,6 @@ export class ToolApprovalCoordinator {
2286
2081
  }
2287
2082
  return { status: 200, body };
2288
2083
  }
2289
- respondLate(id, principal, parsed, httpReq) {
2290
- const store = this.askStore;
2291
- if (!store)
2292
- return this.askNotVisible();
2293
- return this.dispatchLateByRow(store, id, parsed, httpReq, principal);
2294
- }
2295
2084
  watchPresentedCards(askIds, deliver, connection) {
2296
2085
  const watcher = { deliver, connection, askIds: new Set(), registrationsOnConnection: new Map() };
2297
2086
  for (const askId of askIds) {
@@ -2356,16 +2145,10 @@ export class ToolApprovalCoordinator {
2356
2145
  }
2357
2146
  }
2358
2147
  liveEntryFor(id) {
2359
- const local = this.pending.get(id);
2360
- if (local !== undefined)
2361
- return local.askId === undefined ? local : undefined;
2362
2148
  for (const e of this.pendingByAskId.get(id) ?? [])
2363
2149
  return e;
2364
2150
  return undefined;
2365
2151
  }
2366
- async dispatchLateByRow(store, id, parsed, httpReq, principal) {
2367
- return this.dispatchRowTruth(await this.askRowFor(store, id, "getAsk(respond-late)"), id, parsed, httpReq, principal);
2368
- }
2369
2152
  async askRowFor(store, id, label) {
2370
2153
  try {
2371
2154
  return await this.withStoreDeadline(store.getAsk(id), label, DURABLE_CONVERGE_READ_TIMEOUT_MS);
@@ -2375,106 +2158,12 @@ export class ToolApprovalCoordinator {
2375
2158
  return null;
2376
2159
  }
2377
2160
  }
2378
- async askRowFollowingOwner(store, id, label) {
2379
- const readSeq = this.nextRowReadSeq();
2380
- const row = await this.askRowFor(store, id, label);
2381
- if (row?.state === "STREAM_PENDING")
2382
- this.followRowOwner(row.askId, row.owner, readSeq);
2383
- return row;
2384
- }
2385
- async dispatchRowTruth(row, id, parsed, httpReq, principal) {
2386
- if (!askRowVisibleTo(row, principal))
2387
- return this.askNotVisible();
2388
- switch (row.state) {
2389
- case "DECIDED":
2390
- return this.replayDecidedAsk(id, row, parsed);
2391
- case "PARKED":
2392
- return this.redeemParkedAsk(id, row, parsed, httpReq, principal);
2393
- case "PARKING": {
2394
- const parkSession = parkSessionOf(row);
2395
- return {
2396
- status: 202,
2397
- body: {
2398
- approvalId: id,
2399
- askId: row.askId,
2400
- reason: "parking_in_progress",
2401
- message: parkSession !== undefined
2402
- ? "parking in progress — not applied; retry after retryAfterMs or answer through decidePath"
2403
- : "parking in progress — not applied; retry after retryAfterMs or answer through the parent session's durable decide",
2404
- retryAfterMs: PARKING_RETRY_AFTER_MS,
2405
- ...(parkSession !== undefined ? { decidePath: approvalDecidePath(parkSession) } : {}),
2406
- ...this.lateRuleRefusal(parsed),
2407
- },
2408
- };
2409
- }
2410
- case "DENIED":
2411
- case "VOID": {
2412
- return this.notPending(askRowEndingCause(row));
2413
- }
2414
- case "STREAM_PENDING":
2415
- return this.notPending(NOT_PENDING_UNKNOWN_CAUSE);
2416
- default: {
2417
- const unhandled = row.state;
2418
- defaultLogger.error("tool approval respond hit an unknown ask state", { askId: row.askId, state: unhandled });
2419
- return this.notPending();
2420
- }
2421
- }
2422
- }
2423
- lateRuleRefusal(parsed) {
2424
- if (parsed.persistRule === undefined)
2425
- return {};
2426
- return { rulePersisted: false, ruleRefusal: "rule_lane_unavailable" };
2427
- }
2428
- replayDecidedAsk(id, row, parsed) {
2429
- const decided = decidedRowFacts(row);
2430
- if (decided === undefined) {
2431
- defaultLogger.error("tool approval late replay saw a DECIDED row that is not a usable human decision", {
2432
- askId: row.askId,
2433
- decision: row.decision,
2434
- provisional: row.provisional,
2435
- decidedAtMs: row.decidedAtMs,
2436
- });
2437
- return this.notPending();
2438
- }
2439
- this.notifyExternalDecision(row.askId, outcomeOfDecidedRow(row));
2440
- return {
2441
- status: 200,
2442
- body: {
2443
- approvalId: id,
2444
- askId: row.askId,
2445
- idempotent: true,
2446
- ...decided,
2447
- ...noteRecordedReceipt(parsed.note, noteLandedOnRow(parsed.note, row.decisionNote)),
2448
- ...this.lateRuleRefusal(parsed),
2449
- },
2450
- };
2451
- }
2452
- async redeemParkedAsk(id, row, parsed, httpReq, principal) {
2453
- const redeem = this.parkedRedeem?.();
2454
- if (redeem === undefined)
2455
- return this.notPending();
2456
- if (row.gateToken === null) {
2457
- defaultLogger.error("tool approval parked row carries no gate token — nothing to redeem", { askId: row.askId });
2458
- return this.notPending();
2459
- }
2460
- const openGenerationNow = this.lookupParkRedemption(row.askId) !== undefined ? await this.openGateGeneration(row) : undefined;
2461
- const prior = this.lookupParkRedemption(row.askId);
2462
- if (prior?.redeemed !== undefined && receiptAnswersNow(prior.redeemed.gateGeneration, openGenerationNow))
2463
- return this.replayRedeemReceipt(id, prior, row, prior.redeemed, parsed);
2464
- if (prior?.inflight !== undefined) {
2465
- const shared = await prior.inflight;
2466
- if (shared.status < 200 || shared.status >= 300)
2467
- return shared;
2468
- if (!receiptAnswersNow(shared.gateGeneration, openGenerationNow))
2469
- return this.redeemParkedAsk(id, row, parsed, httpReq, principal);
2470
- return this.replayRedeemReceipt(id, prior, row, { decision: shared.decision, body: shared.body, gateGeneration: shared.gateGeneration, ...(shared.rule?.terminal === true ? { rule: shared.rule.body } : {}) }, parsed);
2471
- }
2161
+ async runParkedFlight(redeem, store, row, gateToken, request) {
2162
+ const decision = request.write.decision;
2472
2163
  const admission = this.parkRedemptionAdmission(row.owner);
2473
2164
  if (admission !== undefined)
2474
- return admission;
2165
+ return { ...admission, body: admission.body, decision };
2475
2166
  const memo = this.openParkRedemption(row.askId, row.owner);
2476
- const decision = parsed.value === "deny" ? "deny" : "approve";
2477
- const gateToken = row.gateToken;
2478
2167
  const flight = (async () => {
2479
2168
  let res;
2480
2169
  try {
@@ -2484,12 +2173,14 @@ export class ToolApprovalCoordinator {
2484
2173
  gateBoundCallId: row.gateBoundCallId,
2485
2174
  gateBoundInputHash: row.gateBoundInputHash,
2486
2175
  decision,
2487
- ...(parsed.note !== undefined ? { note: parsed.note } : {}),
2488
- decider: parsed.decider,
2489
- ...(parsed.updatedInput !== undefined && parsed.value !== "deny" ? { updatedInput: parsed.updatedInput } : {}),
2490
- ...(parsed.value === "allow_session" ? { remember: "session" } : {}),
2491
- ...(principal !== undefined ? { principal } : {}),
2492
- ...(httpReq !== undefined ? { httpReq } : {}),
2176
+ ...(request.write.note !== undefined ? { note: request.write.note } : {}),
2177
+ decider: request.write.decider,
2178
+ ...(request.updatedInput !== undefined && decision === "approve" ? { updatedInput: request.updatedInput } : {}),
2179
+ ...(request.remember !== undefined ? { remember: request.remember } : {}),
2180
+ ...(request.persistRule !== undefined ? { persistRule: request.persistRule } : {}),
2181
+ ...(request.principal !== undefined ? { principal: request.principal } : {}),
2182
+ operator: request.operator,
2183
+ ...(request.httpReq !== undefined ? { httpReq: request.httpReq } : {}),
2493
2184
  });
2494
2185
  }
2495
2186
  catch (err) {
@@ -2498,353 +2189,109 @@ export class ToolApprovalCoordinator {
2498
2189
  }
2499
2190
  if (res.status < 200 || res.status >= 300)
2500
2191
  return { ...res, decision };
2501
- const rule = await this.persistRuleFromRow(row, parsed);
2502
- return { ...res, decision, ...(rule !== undefined ? { rule } : {}) };
2192
+ const recorded = await this.recordParkedOutcome(store, row, request.write);
2193
+ return { ...res, decision, ...(recorded !== undefined ? { recorded } : {}) };
2503
2194
  })();
2504
2195
  memo.inflight = flight;
2505
- let out;
2506
2196
  try {
2507
- out = await flight;
2197
+ return await flight;
2508
2198
  }
2509
2199
  finally {
2510
- memo.inflight = undefined;
2511
- }
2512
- if (out.status < 200 || out.status >= 300) {
2513
2200
  if (this.parkRedemptions.get(row.askId) === memo)
2514
2201
  this.parkRedemptions.delete(row.askId);
2515
- return { status: out.status, body: out.body };
2516
2202
  }
2517
- const rule = out.rule;
2518
- if (this.retainParkRedemption(row.askId, memo))
2519
- memo.redeemed = { decision, body: out.body, gateGeneration: out.gateGeneration, ...(rule?.terminal === true ? { rule: rule.body } : {}) };
2520
- return {
2521
- status: out.status,
2522
- body: {
2523
- ...out.body,
2524
- approvalId: id,
2525
- askId: row.askId,
2526
- accepted: "parked_redeemed",
2527
- ...(parsed.value === "allow_session" ? { rememberApplied: out.rememberApplied === true } : {}),
2528
- ...(rule?.body ?? {}),
2529
- },
2530
- };
2531
2203
  }
2532
- async persistRuleFromRow(row, parsed) {
2533
- const persistRule = parsed.persistRule;
2534
- if (persistRule === undefined)
2535
- return undefined;
2536
- const flag = (rulePersisted, ruleRefusal, echo) => ({
2537
- body: { rulePersisted, ...(ruleRefusal !== undefined ? { ruleRefusal } : {}), ...(echo ?? {}) },
2538
- terminal: true,
2539
- });
2540
- const lane = this.ruleConsent;
2541
- if (lane === undefined || row.owner === null)
2542
- return flag(false, "rule_lane_unavailable");
2543
- const owner = row.owner;
2544
- const envelope = readStoredCardEnvelope(row.cardJson);
2545
- if (envelope === undefined)
2546
- return flag(false, "rule_material_absent");
2547
- const card = envelope.card;
2548
- if (card.governanceForced === true)
2549
- return flag(false, "rule_governance_forced");
2550
- const command = row.ruleCommand;
2551
- if (command === null)
2552
- return flag(false, "rule_material_absent");
2553
- if (parsed.updatedInput !== undefined)
2554
- return flag(false, "rule_input_edited");
2555
- const offers = card.ruleOffers ?? [];
2556
- const scope = row.ruleScopeRoot !== null ? { kind: "project", root: row.ruleScopeRoot } : undefined;
2557
- const common = {
2558
- principal: owner,
2559
- toolName: card.toolName,
2560
- command,
2561
- ...(row.toolCallId !== "" ? { toolCallId: row.toolCallId } : {}),
2562
- ...(row.boundInputHash !== null ? { boundInputHash: row.boundInputHash } : {}),
2563
- ...(scope !== undefined ? { scope } : {}),
2564
- };
2204
+ async recordParkedOutcome(store, row, write) {
2565
2205
  try {
2566
- if (persistRule.kind === "batch") {
2567
- const offer = offers[persistRule.batchOfferIndex];
2568
- if (offer === undefined || offer.kind !== "batch")
2569
- return flag(false, "rule_not_offered");
2570
- const persisted = await lane.persistCardRule({ ...common, redemption: { kind: "batch", offer } });
2571
- if (!persisted.ok) {
2572
- defaultLogger.warn("permission rule batch was not persisted after a parked late decision", {
2573
- askId: row.askId,
2574
- reason: persisted.reason,
2575
- ...(persisted.detail !== undefined ? { detail: redactSecrets(persisted.detail) } : {}),
2576
- });
2577
- return flag(false, persisted.reason);
2578
- }
2579
- return flag(true, undefined, persisted.kind === "batch"
2580
- ? {
2581
- persistedRules: persisted.rules,
2582
- ...(persisted.members !== undefined
2583
- ? { persistedRuleAnchors: persisted.members.map((m) => ({ offerIndex: persistRule.batchOfferIndex, memberIndex: m.memberIndex, rule: m.rule })) }
2584
- : {}),
2585
- }
2586
- : {});
2587
- }
2588
- const ruleText = persistRule.rule;
2589
- if (!persistRule.edited && !offers.some((o) => o.kind === "single" && o.rule === ruleText))
2590
- return flag(false, "rule_not_offered");
2591
- const persisted = await lane.persistCardRule({
2592
- ...common,
2593
- redemption: persistRule.edited ? { kind: "edited", text: ruleText } : { kind: "offered", ruleText },
2594
- });
2595
- if (!persisted.ok) {
2596
- defaultLogger.warn("permission rule was not persisted after a parked late decision", {
2597
- askId: row.askId,
2598
- reason: persisted.reason,
2599
- ...(persisted.detail !== undefined ? { detail: redactSecrets(persisted.detail) } : {}),
2600
- });
2601
- return flag(false, persisted.reason);
2602
- }
2603
- return flag(true, undefined, persistRule.edited && persisted.kind === "single" ? { persistedRule: persisted.rule } : {});
2206
+ const out = await this.writeAskDecision(store, row.askId, { kind: "parked" }, row.owner, write);
2207
+ if (out.ok)
2208
+ return out.row;
2209
+ recordFailOpen("server.approval.parked-decision-not-recorded", `askId=${row.askId} reason=${out.reason}`);
2210
+ return undefined;
2604
2211
  }
2605
2212
  catch (err) {
2606
- this.noteStoreError(err, "persistCardRule(parked-late)");
2607
- return { ...flag(false, "rule_store_error"), terminal: false };
2608
- }
2609
- }
2610
- async replayRedeemReceipt(id, memo, row, receipt, parsed) {
2611
- let rule = receipt.decision === "approve" ? receipt.rule : this.lateRuleRefusal(parsed);
2612
- if (receipt.decision === "approve" && rule === undefined && parsed.persistRule !== undefined) {
2613
- const flight = memo.ruleInflight ?? this.persistRuleFromRow(row, parsed);
2614
- const isOwner = memo.ruleInflight === undefined;
2615
- if (isOwner)
2616
- memo.ruleInflight = flight;
2617
- let fresh;
2618
- try {
2619
- fresh = await flight;
2620
- }
2621
- finally {
2622
- if (isOwner)
2623
- memo.ruleInflight = undefined;
2624
- }
2625
- rule = fresh?.body;
2626
- if (fresh?.terminal === true && memo.redeemed !== undefined)
2627
- memo.redeemed.rule = fresh.body;
2213
+ this.noteStoreError(err, "recordParkedDecision");
2214
+ recordFailOpen("server.approval.parked-decision-not-recorded", `askId=${row.askId} reason=store_error`);
2215
+ return undefined;
2628
2216
  }
2629
- const sameDecision = (parsed.value === "deny") === (receipt.decision === "deny");
2630
- return {
2631
- status: 200,
2632
- body: {
2633
- ...(sameDecision ? receipt.body : withoutSettlementEvidence(receipt.body)),
2634
- approvalId: id,
2635
- askId: row.askId,
2636
- accepted: "parked_redeemed",
2637
- idempotent: true,
2638
- decision: receipt.decision === "approve" ? "allow" : "deny",
2639
- ...(rule ?? {}),
2640
- },
2641
- };
2642
2217
  }
2643
- async persistRuleAfterDecision(settled, entry, judgedOwner, persistRule, inputWasEdited, ruleEligibility) {
2644
- const result = await settled;
2645
- if (result.status !== 200)
2646
- return result;
2647
- const lane = this.ruleConsent;
2648
- const material = entry.ruleLane;
2649
- const owner = judgedOwner;
2650
- const withFlag = (rulePersisted, ruleRefusal, echo) => ({
2651
- status: result.status,
2652
- body: { ...result.body, rulePersisted, ...(ruleRefusal !== undefined ? { ruleRefusal } : {}), ...(echo ?? {}) },
2653
- });
2654
- if (entry.governanceForced === true)
2655
- return withFlag(false, "rule_governance_forced");
2656
- if (lane === undefined || material === undefined || owner === null)
2657
- return withFlag(false, "rule_lane_unavailable");
2658
- if (!ruleEligibility.eligible)
2659
- return withFlag(false, "rule_store_error");
2660
- if (inputWasEdited)
2661
- return withFlag(false, "rule_input_edited");
2662
- if (ruleMaterialCarriesCredentialShape(material.command)) {
2663
- defaultLogger.warn("approval rule material withheld on the live leg: the adjudicated command carries credential-shaped bytes", { commandChars: material.command.length });
2664
- return withFlag(false, "rule_material_absent");
2665
- }
2666
- if (persistRule.kind === "batch") {
2667
- const offer = material.offers[persistRule.batchOfferIndex];
2668
- if (offer === undefined || offer.kind !== "batch")
2669
- return withFlag(false, "rule_not_offered");
2670
- try {
2671
- const persisted = await lane.persistCardRule({
2672
- principal: owner,
2673
- toolName: entry.toolName,
2674
- command: material.command,
2675
- redemption: { kind: "batch", offer },
2676
- ...(material.toolCallId !== undefined ? { toolCallId: material.toolCallId } : {}),
2677
- ...(material.boundInputHash !== undefined ? { boundInputHash: material.boundInputHash } : {}),
2678
- ...(material.scopeRoot !== undefined ? { scope: { kind: "project", root: material.scopeRoot } } : {}),
2679
- });
2680
- if (!persisted.ok) {
2681
- defaultLogger.warn("permission rule batch was not persisted after an approval", { reason: persisted.reason, ...(persisted.detail !== undefined ? { detail: redactSecrets(persisted.detail) } : {}) });
2682
- return withFlag(false, persisted.reason);
2683
- }
2684
- return withFlag(true, undefined, persisted.kind === "batch"
2685
- ? {
2686
- persistedRules: persisted.rules,
2687
- ...(persisted.members !== undefined
2688
- ? { persistedRuleAnchors: persisted.members.map((m) => ({ offerIndex: persistRule.batchOfferIndex, memberIndex: m.memberIndex, rule: m.rule })) }
2689
- : {}),
2690
- }
2691
- : {});
2692
- }
2693
- catch (err) {
2694
- this.noteStoreError(err, "persistCardRule(batch)");
2695
- return withFlag(false, "rule_store_error");
2218
+ async decideParkedAsk(store, row, request, idempotencyKey) {
2219
+ for (;;) {
2220
+ const gate = await this.readParkedGate(row);
2221
+ const current = await this.askRowFor(store, row.askId, "getAsk(parked-decision)");
2222
+ if (current === null || current.state !== "PARKED" || !askRowCarriesOwner(current, row.owner))
2223
+ return { kind: "gone" };
2224
+ const inflight = this.lookupParkRedemption(row.askId)?.inflight;
2225
+ if (inflight !== undefined) {
2226
+ await inflight;
2227
+ continue;
2696
2228
  }
2229
+ const standing = standingParkedDecision(gate, current, idempotencyKey);
2230
+ if (standing !== undefined)
2231
+ return standing;
2232
+ if (request.write.decision === "approve" && request.policyMayNotApprove === true)
2233
+ return { kind: "policy_refused" };
2234
+ const redeem = this.parkedRedeem?.();
2235
+ if (redeem === undefined || row.gateToken === null) {
2236
+ if (row.gateToken === null)
2237
+ defaultLogger.error("tool approval parked row carries no gate token — nothing to redeem", { askId: row.askId });
2238
+ return { kind: "gate_only" };
2239
+ }
2240
+ const out = await this.runParkedFlight(redeem, store, current, row.gateToken, request);
2241
+ if (out.status === 200)
2242
+ return { kind: "accepted", body: out.body, ...(out.recorded !== undefined ? { recorded: out.recorded } : {}), ...(out.rememberApplied !== undefined ? { rememberApplied: out.rememberApplied } : {}), ...(out.rule !== undefined ? { rule: out.rule } : {}) };
2243
+ if ((await this.readParkedGate(row))?.kind === "decided")
2244
+ continue;
2245
+ return { kind: "refused", status: out.status, body: out.body };
2697
2246
  }
2698
- const ruleText = persistRule.rule;
2699
- if (!persistRule.edited && !material.offers.some((o) => o.kind === "single" && o.rule === ruleText))
2700
- return withFlag(false, "rule_not_offered");
2247
+ }
2248
+ async readParkedAskOfGate(sourceSessionId, binding) {
2249
+ const store = this.askStore;
2701
2250
  try {
2702
- const persisted = await lane.persistCardRule({
2703
- principal: owner,
2704
- toolName: entry.toolName,
2705
- command: material.command,
2706
- redemption: persistRule.edited ? { kind: "edited", text: ruleText } : { kind: "offered", ruleText },
2707
- ...(material.toolCallId !== undefined ? { toolCallId: material.toolCallId } : {}),
2708
- ...(material.boundInputHash !== undefined ? { boundInputHash: material.boundInputHash } : {}),
2709
- ...(material.scopeRoot !== undefined ? { scope: { kind: "project", root: material.scopeRoot } } : {}),
2710
- });
2711
- if (!persisted.ok) {
2712
- defaultLogger.warn("permission rule was not persisted after an approval", { reason: persisted.reason, ...(persisted.detail !== undefined ? { detail: redactSecrets(persisted.detail) } : {}) });
2713
- return withFlag(false, persisted.reason);
2251
+ const rows = await this.withStoreDeadline(store.listAsksAtGate(sourceSessionId, binding), "listAsksAtGate");
2252
+ const atGate = [];
2253
+ for (const row of rows) {
2254
+ if (row.state !== "PARKED") {
2255
+ atGate.push(row);
2256
+ continue;
2257
+ }
2258
+ const gate = await this.readParkedGate(row);
2259
+ if (gate === undefined)
2260
+ return { kind: "unreadable" };
2261
+ if (gate.kind === "open")
2262
+ atGate.push(row);
2714
2263
  }
2715
- return withFlag(true, undefined, this.editedArmEcho(persistRule, persisted));
2264
+ if (atGate.length === 0)
2265
+ return { kind: "none" };
2266
+ return atGate.length === 1 ? { kind: "row", row: atGate[0] } : { kind: "unreadable" };
2716
2267
  }
2717
2268
  catch (err) {
2718
- this.noteStoreError(err, "persistCardRule");
2719
- return withFlag(false, "rule_store_error");
2269
+ this.noteStoreError(err, "listAsksAtGate");
2270
+ return { kind: "unreadable" };
2720
2271
  }
2721
2272
  }
2722
- async respondWithCas(askStore, askId, batchId, owner, entry, parsed, ruleEligibility) {
2723
- let winnerRow;
2724
- let ambiguousConfirmedRow;
2725
- let noteLanded = false;
2726
- try {
2727
- const decided = await this.withStoreDeadline(askStore.decideAsk(askId, batchId, owner, {
2728
- decision: parsed.value === "deny" ? "deny" : "approve",
2729
- ...(parsed.note !== undefined ? { decisionNote: parsed.note } : {}),
2730
- ...(parsed.value !== "deny" && parsed.updatedInput !== undefined ? { updatedInput: parsed.updatedInput } : {}),
2731
- ...liveHostSource(parsed.decider),
2732
- }), "decideAsk", DURABLE_CALL_TIMEOUT_MS, (late) => {
2733
- if (late.ok)
2273
+ writeAskDecision(store, askId, form, owner, write) {
2274
+ switch (form.kind) {
2275
+ case "live":
2276
+ return this.withStoreDeadline(store.decideAsk(askId, form.batchId, owner, liveDecideInputOf(write)), "decideAsk", DURABLE_CALL_TIMEOUT_MS, (late) => {
2277
+ if (!late.ok)
2278
+ return;
2279
+ if (decidedRowFacts(late.row) === undefined) {
2280
+ defaultLogger.error("ask decision landed after its deadline on a row that is not an authoritative human decision — not settled", { askId, state: late.row.state, provisional: late.row.provisional });
2281
+ return;
2282
+ }
2734
2283
  this.notifyExternalDecision(askId, outcomeOfDecidedRow(late.row));
2735
- });
2736
- if (decided.ok) {
2737
- winnerRow = decided.row;
2738
- noteLanded = true;
2739
- }
2740
- if (!decided.ok) {
2741
- return { lost: true, row: decided.row };
2742
- }
2743
- }
2744
- catch (err) {
2745
- this.noteStoreError(err, "decideAsk");
2746
- ruleEligibility.eligible = false;
2747
- try {
2748
- const confirmSeq = this.nextRowReadSeq();
2749
- const row = await this.withStoreDeadline(askStore.getAsk(askId), "getAsk(decideAsk-confirm)", DURABLE_CONVERGE_READ_TIMEOUT_MS);
2750
- if (row?.state === "STREAM_PENDING")
2751
- this.followRowOwner(askId, row.owner, confirmSeq);
2752
- if (row === null || !askRowCarriesOwner(row, owner))
2753
- return { lost: true, row: null };
2754
- if (row.state === "DECIDED" && row.decision === (parsed.value === "deny" ? "deny" : "approve")) {
2755
- winnerRow = row;
2756
- ambiguousConfirmedRow = row;
2757
- noteLanded = noteLandedOnRow(parsed.note, row.decisionNote);
2758
- }
2759
- else if (row.state !== "STREAM_PENDING") {
2760
- return { lost: true, row };
2761
- }
2762
- }
2763
- catch (confirmErr) {
2764
- this.noteStoreError(confirmErr, "getAsk(decideAsk-confirm)");
2765
- return {
2766
- lost: false,
2767
- result: {
2768
- status: 500,
2769
- body: refusalBody({
2770
- errorCode: "internal.error",
2771
- message: "the approval store failed while recording this answer and could not be read back, so whether the answer was recorded is unknown — retry: a retry returns the recorded answer if it was recorded, or records it if it was not",
2772
- }),
2773
- },
2774
- };
2775
- }
2776
- }
2777
- const wantAllowed = parsed.value !== "deny";
2778
- const decidedWon = winnerRow !== undefined;
2779
- if (entry.adopted === true && !decidedWon)
2780
- return { lost: false, result: this.notPending() };
2781
- if (this.pending.get(entry.registrationId) !== entry) {
2782
- const othersReached = winnerRow !== undefined ? this.notifyExternalDecision(askId, outcomeOfDecidedRow(winnerRow)).settled : 0;
2783
- const converged = decidedWon && (entry.adopted === true || entry.settledOutcome === (wantAllowed ? SETTLE_OUTCOME.ALLOWED : SETTLE_OUTCOME.DENIED));
2784
- if (!converged) {
2785
- return { lost: false, result: this.notPending() };
2786
- }
2787
- return { lost: false, result: withConsumerDisclosure(this.finishRespond(entry, owner, parsed, { updatedInputDelivered: false, noteRecorded: noteLanded }), waitingLegsReached(entry, othersReached)) };
2788
- }
2789
- const payloadIsMine = ambiguousConfirmedRow === undefined ||
2790
- approvalPayloadFingerprint({ updatedInput: ambiguousConfirmedRow.updatedInput }) === approvalPayloadFingerprint({ updatedInput: parsed.updatedInput });
2791
- const result = this.finishRespond(entry, owner, parsed, {
2792
- noteRecorded: noteLanded,
2793
- ...(winnerRow !== undefined ? { settleOutcome: outcomeOfDecidedRow(winnerRow), settleOutcomeIsThisRequest: ambiguousConfirmedRow === undefined } : {}),
2794
- ...(entry.adopted === true ? { updatedInputDelivered: false } : ambiguousConfirmedRow !== undefined ? { updatedInputDelivered: payloadIsMine } : {}),
2795
- });
2796
- const othersReached = winnerRow !== undefined ? this.notifyExternalDecision(askId, outcomeOfDecidedRow(winnerRow)).settled : 0;
2797
- return { lost: false, result: withConsumerDisclosure(result, waitingLegsReached(entry, othersReached)) };
2798
- }
2799
- finishRespond(entry, judgedOwner, parsed, opts) {
2800
- let rememberApplied;
2801
- if (parsed.value === "allow_session") {
2802
- const answer = liveSessionAnswer(pendingEntryStandingAnswerFacts(entry), entry);
2803
- rememberApplied = answer.home === "seat";
2804
- if (answer.home === "seat") {
2805
- const grantKey = sessionAllowKey(judgedOwner, answer.seat, sessionAllowCategory(entry.toolName), entry.grantBinding);
2806
- if (this.allowAllSessions.size >= MAX_ALLOW_SESSIONS) {
2807
- const oldest = this.allowAllSessions.values().next().value;
2808
- if (oldest !== undefined)
2809
- this.allowAllSessions.delete(oldest);
2810
- }
2811
- this.allowAllSessions.add(grantKey);
2812
- }
2813
- else if (answer.home === "none" && answer.occupantUnreadable) {
2814
- defaultLogger.warn("session_grant_refused_occupant_unreadable", { sessionId: entry.sessionId, toolName: entry.toolName });
2284
+ });
2285
+ case "parked":
2286
+ return this.withStoreDeadline(store.recordParkedDecision(askId, owner, askDecisionRecordOf(write)), "recordParkedDecision", DURABLE_CALL_TIMEOUT_MS);
2287
+ default: {
2288
+ const unhandled = form;
2289
+ throw new Error(`writeAskDecision: unhandled form ${JSON.stringify(unhandled)}`);
2815
2290
  }
2816
2291
  }
2817
- const allowed = parsed.value !== "deny";
2818
- const settleArgs = opts?.settleOutcome !== undefined
2819
- ? settleArgsOfDecided(opts.settleOutcome)
2820
- : { allowed, updatedInput: parsed.updatedInput, source: liveHostSource(parsed.decider), ...(!allowed && parsed.note !== undefined ? { denyReason: parsed.note } : {}) };
2821
- const readRootGrant = opts?.settleOutcome === undefined || opts.settleOutcomeIsThisRequest === true ? parsed.readRootGrant : undefined;
2822
- entry.settle({
2823
- by: "human",
2824
- allowed: settleArgs.allowed,
2825
- updatedInput: settleArgs.updatedInput,
2826
- ...(settleArgs.source !== undefined ? { hostSource: settleArgs.source } : {}),
2827
- ...(settleArgs.denyReason !== undefined ? { denyReason: settleArgs.denyReason } : {}),
2828
- ...(readRootGrant !== undefined ? { readRootGrant } : {}),
2829
- });
2830
- const updatedInputDelivered = opts?.updatedInputDelivered ?? true;
2831
- const noteRecorded = opts?.noteRecorded ?? false;
2832
- return {
2833
- status: 200,
2834
- body: {
2835
- approvalId: entry.approvalId,
2836
- delivery: "applied",
2837
- decision: parsed.value,
2838
- ...(rememberApplied !== undefined ? { rememberApplied } : {}),
2839
- ...(parsed.updatedInput !== undefined && allowed && updatedInputDelivered ? { updatedInputForwarded: true } : {}),
2840
- ...noteRecordedReceipt(parsed.note, noteRecorded),
2841
- },
2842
- };
2843
2292
  }
2844
2293
  async adoptOpenAsks() {
2845
2294
  const store = this.askStore;
2846
- if (!store)
2847
- return { adopted: 0, alreadyLive: 0, cardUnreadable: 0, capped: false };
2848
2295
  let rows;
2849
2296
  try {
2850
2297
  rows = await this.withStoreDeadline(store.listByState("STREAM_PENDING", ADOPT_OPEN_ASKS_MAX + 1), "listByState(adopt)");
@@ -2912,11 +2359,9 @@ export class ToolApprovalCoordinator {
2912
2359
  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 }),
2913
2360
  forceParkNow: release,
2914
2361
  settleParkRoute: (cause) => leave({ outcome: SETTLE_OUTCOME.EXPIRED, cause, parked: this.unattendedRoutesToPark }),
2915
- owner: row.owner,
2916
- ownerReadSeq: this.rowReadSeq,
2917
2362
  executionLane: row.executionLane,
2918
2363
  ...leg,
2919
- ...(card.fromSubagent === true ? { originTaskId: row.sourceSessionId } : {}),
2364
+ ...(card.fromSubagent === true ? { sourceSessionId: row.sourceSessionId } : {}),
2920
2365
  toolName: card.toolName,
2921
2366
  ...facts,
2922
2367
  approvalId: row.askId,
@@ -2947,43 +2392,6 @@ export class ToolApprovalCoordinator {
2947
2392
  for (const watcher of watchers)
2948
2393
  watcher.askIds.delete(askId);
2949
2394
  }
2950
- followRowOwner(askId, owner, readSeq) {
2951
- for (const e of this.pendingByAskId.get(askId) ?? []) {
2952
- if (e.ownerReadSeq >= readSeq)
2953
- continue;
2954
- e.owner = owner;
2955
- e.ownerReadSeq = readSeq;
2956
- }
2957
- }
2958
- rowReadSeq = 0;
2959
- nextRowReadSeq() {
2960
- this.rowReadSeq += 1;
2961
- return this.rowReadSeq;
2962
- }
2963
- async followRowOwners() {
2964
- const store = this.askStore;
2965
- const out = { read: 0, followed: 0, unreadable: 0 };
2966
- if (!store)
2967
- return out;
2968
- for (const askId of [...this.pendingByAskId.keys()]) {
2969
- out.read += 1;
2970
- const readSeq = this.nextRowReadSeq();
2971
- let row;
2972
- try {
2973
- row = await this.withStoreDeadline(store.getAsk(askId), "getAsk(follow-row-owners)", DURABLE_CONVERGE_READ_TIMEOUT_MS);
2974
- }
2975
- catch (err) {
2976
- this.noteStoreError(err, "getAsk(follow-row-owners)");
2977
- out.unreadable += 1;
2978
- continue;
2979
- }
2980
- if (row?.state !== "STREAM_PENDING")
2981
- continue;
2982
- this.followRowOwner(askId, row.owner, readSeq);
2983
- out.followed += 1;
2984
- }
2985
- return out;
2986
- }
2987
2395
  waitingRegistrationCount(askId) {
2988
2396
  let n = 0;
2989
2397
  for (const e of this.pendingByAskId.get(askId) ?? [])
@@ -3001,9 +2409,7 @@ export class ToolApprovalCoordinator {
3001
2409
  const store = this.askStore;
3002
2410
  const ids = [...candidates.keys()];
3003
2411
  const listed = await Promise.all([...candidates.values()].map(async (entry) => {
3004
- if (store === undefined || !this.registrationHasRow(entry))
3005
- return inScope(entry.owner);
3006
- const ask = await this.askRowFollowingOwner(store, entry.askId, "getAsk(list-live)");
2412
+ const ask = await this.askRowFor(store, entry.askId, "getAsk(list-live)");
3007
2413
  return ask?.state === "STREAM_PENDING" && inScope(ask.owner);
3008
2414
  }));
3009
2415
  const rows = [];
@@ -3030,13 +2436,11 @@ export class ToolApprovalCoordinator {
3030
2436
  }
3031
2437
  async pollSuspendedDecisions() {
3032
2438
  const store = this.askStore;
3033
- if (!store)
3034
- return { polled: 0, settled: 0, backlog: 0, truncated: false };
3035
2439
  const waiting = [];
3036
2440
  const adopted = [];
3037
2441
  const seen = new Set();
3038
2442
  for (const entry of this.pending.values()) {
3039
- if (entry.askId === undefined || entry.settledOutcome !== undefined || seen.has(entry.askId))
2443
+ if (entry.settledOutcome !== undefined || seen.has(entry.askId))
3040
2444
  continue;
3041
2445
  if (entry.suspended === true)
3042
2446
  waiting.push(entry.askId);
@@ -3046,53 +2450,33 @@ export class ToolApprovalCoordinator {
3046
2450
  continue;
3047
2451
  seen.add(entry.askId);
3048
2452
  }
3049
- const waitingStart = waiting.length > 0 ? this.suspendedPollOffset % waiting.length : 0;
3050
- const adoptedStart = adopted.length > 0 ? this.adoptedPollOffset % adopted.length : 0;
3051
- const ordered = [
3052
- ...[...waiting.slice(waitingStart), ...waiting.slice(0, waitingStart)].map((askId) => ({ askId, tier: "waiting" })),
3053
- ...[...adopted.slice(adoptedStart), ...adopted.slice(0, adoptedStart)].map((askId) => ({ askId, tier: "adopted" })),
3054
- ];
3055
- const deadlineMs = Date.now() + SUSPENDED_POLL_BUDGET_MS;
3056
2453
  let settled = 0;
3057
- let polled = 0;
3058
- let polledWaiting = 0;
3059
- let polledAdopted = 0;
3060
- let truncated = false;
3061
- for (const { askId, tier } of ordered) {
3062
- if (polled >= SUSPENDED_POLL_MAX_PER_TICK || Date.now() >= deadlineMs) {
3063
- truncated = true;
3064
- break;
3065
- }
3066
- polled++;
3067
- if (tier === "waiting")
3068
- this.suspendedPollOffset = waitingStart + ++polledWaiting;
3069
- else
3070
- this.adoptedPollOffset = adoptedStart + ++polledAdopted;
2454
+ const { visited: polled, truncated } = await boundedRotation([
2455
+ { items: waiting, cursor: this.suspendedPollCursor },
2456
+ { items: adopted, cursor: this.adoptedPollCursor },
2457
+ ], { maxPerTick: SUSPENDED_POLL_MAX_PER_TICK, budgetMs: SUSPENDED_POLL_BUDGET_MS }, async (askId) => {
3071
2458
  let row;
3072
- const readSeq = this.nextRowReadSeq();
3073
2459
  try {
3074
2460
  row = await this.withStoreDeadline(store.getAsk(askId), "getAsk(poll-suspended)", DURABLE_CONVERGE_READ_TIMEOUT_MS);
3075
2461
  }
3076
2462
  catch (err) {
3077
2463
  this.noteStoreError(err, "getAsk(poll-suspended)");
3078
- continue;
2464
+ return;
3079
2465
  }
3080
2466
  if (row === null) {
3081
2467
  this.forgetAdoptedAsk(askId);
3082
- continue;
3083
- }
3084
- if (row.state === "STREAM_PENDING") {
3085
- this.followRowOwner(askId, row.owner, readSeq);
3086
- continue;
2468
+ return;
3087
2469
  }
2470
+ if (row.state === "STREAM_PENDING")
2471
+ return;
3088
2472
  if (this.editedDecisionInFlight(askId))
3089
- continue;
2473
+ return;
3090
2474
  const endingCause = askRowEndingCause(row);
3091
2475
  if (endingCause !== undefined) {
3092
2476
  const before = this.waitingRegistrationCount(askId);
3093
2477
  this.settleVoidedSiblings([askId], endingCause);
3094
2478
  settled += before;
3095
- continue;
2479
+ return;
3096
2480
  }
3097
2481
  const outcome = replayTerminalAskRow(row);
3098
2482
  if (outcome === "unavailable") {
@@ -3100,11 +2484,11 @@ export class ToolApprovalCoordinator {
3100
2484
  const parkCause = askRowParkTrigger(row) ?? (Date.now() >= row.expiresAtMs ? APPROVAL_ENDING_CAUSES.TTL_EXPIRED : APPROVAL_ENDING_CAUSES.SHUTDOWN_BEFORE_PARK);
3101
2485
  this.settleSameAskIdParkRoute(askId, parkCause);
3102
2486
  settled += before;
3103
- continue;
2487
+ return;
3104
2488
  }
3105
2489
  const delivered = this.notifyExternalDecision(askId, outcome).settled;
3106
2490
  settled += delivered;
3107
- }
2491
+ });
3108
2492
  const backlog = Math.max(0, waiting.length + adopted.length - polled);
3109
2493
  if (truncated) {
3110
2494
  defaultLogger.warn("approval_suspended_poll_truncated", {
@@ -3113,8 +2497,8 @@ export class ToolApprovalCoordinator {
3113
2497
  backlog,
3114
2498
  maxPerTick: SUSPENDED_POLL_MAX_PER_TICK,
3115
2499
  budgetMs: SUSPENDED_POLL_BUDGET_MS,
3116
- resumeFromOffset: this.suspendedPollOffset,
3117
- resumeAdoptedFromOffset: this.adoptedPollOffset,
2500
+ resumeFromOffset: this.suspendedPollCursor.offset,
2501
+ resumeAdoptedFromOffset: this.adoptedPollCursor.offset,
3118
2502
  });
3119
2503
  }
3120
2504
  return { polled, settled, backlog, truncated };