@jr2/orchestrator 0.6.0 → 0.7.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 (301) hide show
  1. package/console/components/app.ts +55 -15
  2. package/console/components/bottom-drawer.ts +259 -0
  3. package/console/components/drawer.ts +14 -6
  4. package/console/components/fleet.ts +36 -13
  5. package/console/components/machine-pane.ts +61 -5
  6. package/console/components/nav.ts +54 -8
  7. package/console/conversation.ts +161 -0
  8. package/console/login.ts +123 -0
  9. package/console/main.ts +600 -60
  10. package/console/store.ts +611 -64
  11. package/console/style.css +231 -0
  12. package/docs/agents-block.md +2 -2
  13. package/docs/concepts/agents-and-turns.md +86 -9
  14. package/docs/concepts/architecture.md +41 -28
  15. package/docs/concepts/callers-and-hooks.md +205 -0
  16. package/docs/concepts/charts.md +7 -5
  17. package/docs/concepts/composition.md +10 -1
  18. package/docs/concepts/converge.md +4 -2
  19. package/docs/concepts/durable-runs.md +43 -16
  20. package/docs/concepts/effects.md +50 -22
  21. package/docs/concepts/gates.md +95 -25
  22. package/docs/concepts/machines-and-workflows.md +5 -4
  23. package/docs/concepts/pools-and-sources.md +187 -11
  24. package/docs/concepts/secrets-and-trust.md +73 -33
  25. package/docs/concepts/the-menu.md +11 -0
  26. package/docs/concepts/workspaces.md +6 -5
  27. package/docs/get-started/a-model-decides.md +14 -13
  28. package/docs/get-started/an-agent-in-a-sandbox.md +43 -34
  29. package/docs/get-started/connect-a-model.md +1 -1
  30. package/docs/get-started/create-an-instance.md +17 -14
  31. package/docs/guides/add-tools-to-the-sandbox.md +55 -11
  32. package/docs/guides/answer-a-gate-from-a-forge.md +304 -0
  33. package/docs/guides/answer-a-gate.md +74 -37
  34. package/docs/guides/bind-open-parts.md +24 -8
  35. package/docs/guides/call-an-mcp-tool-from-a-machine.md +572 -0
  36. package/docs/guides/cancel-a-run.md +26 -16
  37. package/docs/guides/fix-a-failed-converge.md +18 -6
  38. package/docs/guides/give-a-caller-a-scope.md +203 -0
  39. package/docs/guides/give-an-agent-mcp-tools.md +422 -0
  40. package/docs/guides/iterate-on-a-machine-on-kind.md +10 -9
  41. package/docs/guides/let-an-agent-write-a-chart.md +9 -6
  42. package/docs/guides/look-inside-a-sandbox.md +101 -31
  43. package/docs/guides/open-the-console.md +63 -14
  44. package/docs/guides/read-a-conversation.md +225 -0
  45. package/docs/guides/run-a-chart-by-name.md +17 -16
  46. package/docs/guides/run-one-worker-per-item.md +15 -9
  47. package/docs/guides/set-an-agents-access-and-dials.md +2 -0
  48. package/docs/guides/start-a-run-for-each-alert.md +290 -0
  49. package/docs/guides/start-a-run-from-a-run.md +18 -16
  50. package/docs/guides/start-a-run.md +24 -18
  51. package/docs/guides/steer-an-agent.md +332 -0
  52. package/docs/guides/use-a-hosted-provider.md +1 -1
  53. package/docs/guides/watch-a-run.md +127 -121
  54. package/docs/guides/write-a-hook-kind.md +238 -0
  55. package/docs/guides/write-a-tool-as-an-mcp-server.md +351 -0
  56. package/docs/guides/write-a-workflow.md +17 -7
  57. package/docs/llms.txt +81 -15
  58. package/docs/reference/authoring-surface/agent.md +1 -1
  59. package/docs/reference/authoring-surface/answer.md +1 -1
  60. package/docs/reference/authoring-surface/arg.md +1 -1
  61. package/docs/reference/authoring-surface/basic.md +1 -1
  62. package/docs/reference/authoring-surface/bearer.md +1 -1
  63. package/docs/reference/authoring-surface/branchname.md +1 -1
  64. package/docs/reference/authoring-surface/carry.md +23 -0
  65. package/docs/reference/authoring-surface/chartschema.md +1 -1
  66. package/docs/reference/authoring-surface/chartschemafor.md +1 -1
  67. package/docs/reference/authoring-surface/command.md +1 -1
  68. package/docs/reference/authoring-surface/customize.md +4 -1
  69. package/docs/reference/authoring-surface/defineconfig.md +1 -1
  70. package/docs/reference/authoring-surface/defineevent.md +1 -1
  71. package/docs/reference/authoring-surface/dispatch.md +5 -4
  72. package/docs/reference/authoring-surface/fail.md +1 -1
  73. package/docs/reference/authoring-surface/file.md +1 -1
  74. package/docs/reference/authoring-surface/frompick.md +1 -1
  75. package/docs/reference/authoring-surface/gate.md +118 -0
  76. package/docs/reference/authoring-surface/group-agents.md +15 -0
  77. package/docs/reference/authoring-surface/group-charts.md +16 -0
  78. package/docs/reference/authoring-surface/group-composition.md +25 -0
  79. package/docs/reference/authoring-surface/group-config.md +17 -0
  80. package/docs/reference/authoring-surface/group-effects.md +48 -0
  81. package/docs/reference/authoring-surface/group-failure.md +13 -0
  82. package/docs/reference/authoring-surface/group-gates.md +16 -0
  83. package/docs/reference/authoring-surface/group-machine-and-events.md +15 -0
  84. package/docs/reference/authoring-surface/group-steering.md +12 -0
  85. package/docs/reference/authoring-surface/group-types-these-signatures-name.md +137 -0
  86. package/docs/reference/authoring-surface/group-workspaces-and-repos.md +15 -0
  87. package/docs/reference/authoring-surface/http.md +1 -1
  88. package/docs/reference/authoring-surface/isfailure.md +1 -1
  89. package/docs/reference/authoring-surface/isready.md +1 -1
  90. package/docs/reference/authoring-surface/jr2setup.md +13 -3
  91. package/docs/reference/authoring-surface/mcp.md +107 -0
  92. package/docs/reference/authoring-surface/open.md +5 -3
  93. package/docs/reference/authoring-surface/openbody.md +1 -1
  94. package/docs/reference/authoring-surface/paramsof.md +1 -1
  95. package/docs/reference/authoring-surface/pool.md +77 -9
  96. package/docs/reference/authoring-surface/push.md +1 -1
  97. package/docs/reference/authoring-surface/readyof.md +1 -1
  98. package/docs/reference/authoring-surface/reread.md +19 -0
  99. package/docs/reference/authoring-surface/secret.md +1 -1
  100. package/docs/reference/authoring-surface/sentby.md +1 -1
  101. package/docs/reference/authoring-surface/skip.md +22 -0
  102. package/docs/reference/authoring-surface/source.md +56 -1
  103. package/docs/reference/authoring-surface/steer.md +93 -0
  104. package/docs/reference/authoring-surface/text.md +1 -1
  105. package/docs/reference/authoring-surface/type-agentdefinition.md +14 -1
  106. package/docs/reference/authoring-surface/type-agentmcpserver.md +54 -0
  107. package/docs/reference/authoring-surface/type-anyframepath.md +1 -1
  108. package/docs/reference/authoring-surface/type-anyworktreepath.md +1 -1
  109. package/docs/reference/authoring-surface/type-binding.md +1 -1
  110. package/docs/reference/authoring-surface/type-caller.md +40 -0
  111. package/docs/reference/authoring-surface/type-callerrecord.md +32 -0
  112. package/docs/reference/authoring-surface/type-carry.md +19 -0
  113. package/docs/reference/authoring-surface/type-chainlink.md +3 -3
  114. package/docs/reference/authoring-surface/type-commandoutput.md +1 -1
  115. package/docs/reference/authoring-surface/type-customize.md +16 -1
  116. package/docs/reference/authoring-surface/type-delivered.md +17 -0
  117. package/docs/reference/authoring-surface/type-deliveredevent.md +18 -0
  118. package/docs/reference/authoring-surface/type-dispatchdefinition.md +4 -3
  119. package/docs/reference/authoring-surface/type-dispatchoutput.md +1 -1
  120. package/docs/reference/authoring-surface/type-effectbinding.md +12 -2
  121. package/docs/reference/authoring-surface/type-framepath.md +1 -1
  122. package/docs/reference/authoring-surface/type-gatebinding.md +15 -0
  123. package/docs/reference/authoring-surface/type-gatehooks.md +12 -0
  124. package/docs/reference/authoring-surface/type-gatelook.md +37 -0
  125. package/docs/reference/authoring-surface/type-gateoutcome.md +32 -0
  126. package/docs/reference/authoring-surface/type-gatereply.md +19 -0
  127. package/docs/reference/authoring-surface/type-gateslot.md +21 -0
  128. package/docs/reference/authoring-surface/type-gateslotinput.md +13 -0
  129. package/docs/reference/authoring-surface/type-gateslotparts.md +18 -0
  130. package/docs/reference/authoring-surface/type-gateslots.md +12 -0
  131. package/docs/reference/authoring-surface/type-gatetypesat.md +13 -0
  132. package/docs/reference/authoring-surface/type-hostinjectedinput.md +1 -1
  133. package/docs/reference/authoring-surface/type-httpoutput.md +1 -1
  134. package/docs/reference/authoring-surface/type-jr2config.md +37 -2
  135. package/docs/reference/authoring-surface/type-lateroutcome.md +1 -1
  136. package/docs/reference/authoring-surface/type-lends.md +5 -1
  137. package/docs/reference/authoring-surface/type-lookoutput.md +14 -0
  138. package/docs/reference/authoring-surface/type-mcpcalloutput.md +78 -0
  139. package/docs/reference/authoring-surface/type-mcpnotreturned.md +42 -0
  140. package/docs/reference/authoring-surface/type-mcpoptions.md +47 -0
  141. package/docs/reference/authoring-surface/type-mcpoutput.md +50 -0
  142. package/docs/reference/authoring-surface/type-mcpreadoutput.md +53 -0
  143. package/docs/reference/authoring-surface/type-mcpremoteserver.md +50 -0
  144. package/docs/reference/authoring-surface/type-mcpserver.md +41 -0
  145. package/docs/reference/authoring-surface/type-mcpstdioserver.md +42 -0
  146. package/docs/reference/authoring-surface/type-namedsecret.md +4 -4
  147. package/docs/reference/authoring-surface/type-openbodydeclaration.md +1 -1
  148. package/docs/reference/authoring-surface/type-openbodyoutput.md +1 -1
  149. package/docs/reference/authoring-surface/type-poolnextinput.md +1 -1
  150. package/docs/reference/authoring-surface/type-poolspec.md +11 -6
  151. package/docs/reference/authoring-surface/type-pushoutput.md +1 -1
  152. package/docs/reference/authoring-surface/type-pushsourcespec.md +38 -0
  153. package/docs/reference/authoring-surface/type-readsourcespec.md +47 -0
  154. package/docs/reference/authoring-surface/type-reposlot.md +1 -1
  155. package/docs/reference/authoring-surface/type-reread.md +18 -0
  156. package/docs/reference/authoring-surface/type-runsconfig.md +41 -0
  157. package/docs/reference/authoring-surface/type-sender.md +26 -1
  158. package/docs/reference/authoring-surface/type-sendercallerfield.md +14 -0
  159. package/docs/reference/authoring-surface/type-size.md +1 -1
  160. package/docs/reference/authoring-surface/type-skip.md +19 -0
  161. package/docs/reference/authoring-surface/type-sourcehooks.md +23 -0
  162. package/docs/reference/authoring-surface/type-sourcein.md +25 -0
  163. package/docs/reference/authoring-surface/type-sourcereply.md +18 -0
  164. package/docs/reference/authoring-surface/type-sourcespec.md +16 -7
  165. package/docs/reference/authoring-surface/type-sourceverdict.md +49 -0
  166. package/docs/reference/authoring-surface/type-standingpoolmachine.md +12 -0
  167. package/docs/reference/authoring-surface/type-standingpoolspec.md +51 -0
  168. package/docs/reference/authoring-surface/type-steerargs.md +20 -0
  169. package/docs/reference/authoring-surface/type-thinkinglevel.md +1 -1
  170. package/docs/reference/authoring-surface/type-usercontainer.md +1 -1
  171. package/docs/reference/authoring-surface/type-workspaced.md +1 -1
  172. package/docs/reference/authoring-surface/type-worktreefound.md +3 -3
  173. package/docs/reference/authoring-surface/type-worktreeinput.md +1 -1
  174. package/docs/reference/authoring-surface/type-worktreeoptions.md +1 -1
  175. package/docs/reference/authoring-surface/type-worktreeoutput.md +1 -1
  176. package/docs/reference/authoring-surface/type-worktreepath.md +1 -1
  177. package/docs/reference/authoring-surface/type-worktreeready.md +1 -1
  178. package/docs/reference/authoring-surface/type-worktreerefused.md +4 -4
  179. package/docs/reference/authoring-surface/workspace.md +1 -1
  180. package/docs/reference/authoring-surface/worktree.md +1 -1
  181. package/docs/reference/authoring-surface.md +88 -167
  182. package/docs/reference/cli.md +413 -74
  183. package/docs/reference/config.md +235 -2
  184. package/docs/reference/decisions/0002-duplex-streaming-actor.md +71 -11
  185. package/docs/reference/decisions/0005-sandbox-pod-composition.md +14 -10
  186. package/docs/reference/decisions/0006-agent-control-surface.md +4 -4
  187. package/docs/reference/decisions/0007-durable-machine-state.md +1 -1
  188. package/docs/reference/decisions/0009-cli-and-instance-interface.md +150 -49
  189. package/docs/reference/decisions/0010-bdd-acceptance-tests.md +5 -3
  190. package/docs/reference/decisions/0011-workflow-defined-events.md +256 -12
  191. package/docs/reference/decisions/0012-workspace-wrapper-machine.md +9 -2
  192. package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +20 -14
  193. package/docs/reference/decisions/0014-observation-is-open-run-state-is-not.md +53 -27
  194. package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +18 -14
  195. package/docs/reference/decisions/0017-pool-over-a-source-port.md +226 -12
  196. package/docs/reference/decisions/0018-instance-agents-are-definitions-jr2-assembles-the-harness.md +15 -7
  197. package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +32 -18
  198. package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +14 -1
  199. package/docs/reference/decisions/0022-observation-is-a-level-triggered-feed.md +4 -4
  200. package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +116 -34
  201. package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +38 -37
  202. package/docs/reference/decisions/0025-cancel-ends-a-run-stop-parks-it.md +10 -5
  203. package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +7 -6
  204. package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +23 -7
  205. package/docs/reference/decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md +18 -10
  206. package/docs/reference/decisions/0030-a-snapshot-names-the-machine-it-was-written-under.md +3 -2
  207. package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +23 -18
  208. package/docs/reference/decisions/0032-the-console-unlocks-with-a-callers-credential.md +101 -0
  209. package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +4 -3
  210. package/docs/reference/decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md +6 -6
  211. package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +5 -4
  212. package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +19 -19
  213. package/docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md +6 -1
  214. package/docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md +55 -40
  215. package/docs/reference/decisions/0055-a-release-is-a-pushed-tag-and-the-publish-guard-is-the-credential.md +14 -13
  216. package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +7 -0
  217. package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +13 -7
  218. package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +38 -30
  219. package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +64 -17
  220. package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +2 -1
  221. package/docs/reference/decisions/0064-a-workspace-waits-for-capacity.md +2 -2
  222. package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-kept-for-a-set-time.md +109 -0
  223. package/docs/reference/decisions/0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md +51 -23
  224. package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +94 -54
  225. package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +101 -47
  226. package/docs/reference/decisions/0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md +32 -9
  227. package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +17 -13
  228. package/docs/reference/decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md +56 -30
  229. package/docs/reference/decisions/0072-the-docs-ship-inside-the-kit-and-an-instances-agents-md-indexes-them.md +35 -25
  230. package/docs/reference/decisions/0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md +4 -3
  231. package/docs/reference/decisions/0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md +37 -10
  232. package/docs/reference/decisions/0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md +285 -0
  233. package/docs/reference/decisions/0079-an-agent-holds-mcp-tools-a-machine-calls-them-as-an-effect-and-the-machine-is-the-approval.md +198 -0
  234. package/docs/reference/decisions.md +6 -4
  235. package/docs/reference/glossary.md +96 -42
  236. package/docs/reference/hooks.md +1077 -0
  237. package/docs/reference/http-api.md +976 -149
  238. package/docs/why.md +2 -1
  239. package/package.json +10 -3
  240. package/src/actor.ts +36 -10
  241. package/src/agent.ts +50 -0
  242. package/src/assembly-checks.ts +530 -4
  243. package/src/callers.ts +245 -0
  244. package/src/chart.ts +52 -7
  245. package/src/config.ts +550 -1
  246. package/src/conversations.ts +453 -0
  247. package/src/custodian.ts +8 -2
  248. package/src/customize.ts +143 -4
  249. package/src/dispatch-effect.ts +142 -26
  250. package/src/durability.ts +2 -0
  251. package/src/duration.ts +20 -0
  252. package/src/effect-checks.ts +2 -1
  253. package/src/effect.ts +60 -7
  254. package/src/effector-wire.ts +272 -27
  255. package/src/gate-brand.ts +16 -0
  256. package/src/gate.ts +634 -48
  257. package/src/harness-client.ts +10 -4
  258. package/src/held-secrets.ts +72 -23
  259. package/src/holds.ts +2 -2
  260. package/src/hook-kinds/discord.ts +92 -0
  261. package/src/hook-kinds/door.ts +107 -0
  262. package/src/hook-kinds/github.ts +89 -0
  263. package/src/hook-kinds/gitlab.ts +72 -0
  264. package/src/hook-kinds/hook.ts +7 -0
  265. package/src/hook-kinds/index.ts +465 -0
  266. package/src/hook-kinds/slack.ts +151 -0
  267. package/src/hook-kinds/webhook.ts +113 -0
  268. package/src/hooks.ts +506 -0
  269. package/src/http-effect.ts +76 -38
  270. package/src/http.ts +889 -94
  271. package/src/index.ts +16 -5
  272. package/src/instance.ts +196 -26
  273. package/src/internal.ts +22 -3
  274. package/src/machine-doc.ts +26 -1
  275. package/src/mcp-effect.ts +424 -0
  276. package/src/mount-digest.ts +41 -7
  277. package/src/names.ts +64 -2
  278. package/src/notices.ts +138 -10
  279. package/src/open-body.ts +25 -11
  280. package/src/open-effect.ts +17 -4
  281. package/src/open.ts +4 -2
  282. package/src/parts.ts +87 -9
  283. package/src/party.ts +2 -1
  284. package/src/pick.ts +2 -2
  285. package/src/pool.ts +1142 -163
  286. package/src/registration.ts +203 -13
  287. package/src/run-host.ts +1509 -171
  288. package/src/run-list.ts +191 -0
  289. package/src/sandbox-kube.ts +53 -5
  290. package/src/secret-placement.ts +239 -33
  291. package/src/server.ts +53 -4
  292. package/src/setup.ts +72 -32
  293. package/src/snapshot-store.ts +565 -48
  294. package/src/steer.ts +111 -0
  295. package/src/stub-harness.ts +6 -2
  296. package/src/tokens.ts +263 -22
  297. package/src/wire.ts +17 -0
  298. package/src/workspace.ts +50 -6
  299. package/src/worktree.ts +3 -3
  300. package/docs/reference/decisions/0032-the-console-unlocks-with-the-instance-token.md +0 -61
  301. package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-a-week.md +0 -65
package/src/customize.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  // `.provide()`, nothing to learn (ADR-0015's rule, restated by ADR-0049 — a `.with()` method and
10
10
  // a callable-machine hybrid were both rejected there).
11
11
  //
12
- // Four moves, and each is the smallest one that works:
12
+ // Five moves, and each is the smallest one that works:
13
13
  //
14
14
  // - An AGENT override is xstate's own `provide`: `machine.provide({ actors: { coder:
15
15
  // agent({ ...stock, ...override }) } })`. The slot already IS the Agent (ADR-0049), so
@@ -18,6 +18,8 @@
18
18
  // package left `open`, or whose auth, header or env names `secret(open)`, carries a binder,
19
19
  // which builds the Effect again with the composer's parts, so the bound slot is checked as a
20
20
  // written one is. Once — a bound part is fixed from then on.
21
+ // - A GATE binding is the same substitution (ADR-0011, ADR-0054): a Gate slot is built again from
22
+ // its parts, with the look bound and the handlers patched by kind and event key.
21
23
  // - A CHILD override is the same call one level down, recursing. Each level is exactly the
22
24
  // one-level reach ADR-0015 found `provide` has — held by the composer who owns the child
23
25
  // object, never host-side injection into somebody else's Machine.
@@ -80,6 +82,18 @@ import { isEffect, type Arg } from "./effect.ts";
80
82
  import { attachLent, copyOpenBody, lentIn, openBodiesOf, openBodyDeclarationOf } from "./open-body.ts";
81
83
  import { effectChecks } from "./effect-checks.ts";
82
84
  import type { NamedSecret, Text } from "./http-effect.ts";
85
+ import type { EventsOf, HookKinds, NoWildcardKeys } from "./hook-kinds/index.ts";
86
+ import {
87
+ gateSlotOf,
88
+ hooksTableFault,
89
+ isGateSlot,
90
+ lookFault,
91
+ type AnyHandler,
92
+ type GateIn,
93
+ type GateLook,
94
+ type GateSlot,
95
+ type GateSlotParts,
96
+ } from "./gate.ts";
83
97
  import {
84
98
  binderOf,
85
99
  effectBindingShape,
@@ -141,6 +155,45 @@ type AgentSlots<M extends AnyStateMachine> = Extract<
141
155
  */
142
156
  type OpenEffectSlots<M extends AnyStateMachine> = Extract<SlotsOf<M>, { logic: OpenEffect<OpenEffectPart> }>["src"];
143
157
 
158
+ /** The Gate slots (`gate({ … })`, ADR-0011) — told by their brand.
159
+ *
160
+ * @internal
161
+ */
162
+ type GateSlots<M extends AnyStateMachine> = Extract<SlotsOf<M>, { logic: GateSlot<any, any> }>["src"];
163
+ /**
164
+ * The meta and the event type of the Gate slot under one key.
165
+ *
166
+ * @internal
167
+ */
168
+ type GateTypesAt<M extends AnyStateMachine, K extends string> =
169
+ LogicAt<M, K> extends GateSlot<infer Meta, infer Ev> ? { meta: Meta; ev: Ev } : never;
170
+
171
+ /**
172
+ * A patch of a Gate slot's handlers (ADR-0011, ADR-0054): by Hook kind id and event key, a handler
173
+ * that adds or replaces the one there, or `null` to remove it. A key that the kind's payloads do not
174
+ * name does not compile.
175
+ *
176
+ * @internal
177
+ */
178
+ export type GateHooksPatch<Meta, Ev> = {
179
+ readonly [Id in keyof HookKinds as HookKinds[Id] extends { readonly id: Id } ? Id : never]?: {
180
+ readonly [Key in keyof EventsOf<HookKinds[Id]>]?:
181
+ | ((d: GateIn<Meta> & { readonly payload: EventsOf<HookKinds[Id]>[Key] }) => Ev | undefined)
182
+ | null;
183
+ } & NoWildcardKeys<HookKinds[Id]>;
184
+ };
185
+
186
+ /**
187
+ * What `customize()` binds on one Gate slot: its look, and a patch of its handlers.
188
+ *
189
+ * @internal
190
+ */
191
+ export type GateBinding<Meta, Ev> = {
192
+ /** The look, bound once: the package left it `open`, or the composer replaces it. Or `"none"`. */
193
+ readonly look?: GateLook<Meta, Ev> | "none";
194
+ readonly hooks?: GateHooksPatch<Meta, Ev>;
195
+ };
196
+
144
197
  /** What one Open part binds to: a url, an argv, a secret's name, a dispatch's target — another
145
198
  * Orchestrator's origin and the secret that holds its Instance token, together (ADR-0071). */
146
199
  type PartBinding<P> = P extends "url"
@@ -151,7 +204,11 @@ type PartBinding<P> = P extends "url"
151
204
  ? { secret: string }
152
205
  : P extends "target"
153
206
  ? { target: { url: string; token: NamedSecret } }
154
- : never;
207
+ : P extends "server"
208
+ ? { server: string }
209
+ : P extends "tool"
210
+ ? { tool: string }
211
+ : never;
155
212
 
156
213
  /** A union of object types as their intersection: one binding object with every key. */
157
214
  type Intersected<U> = (U extends unknown ? (x: U) => void : never) extends (x: infer I) => void ? I : never;
@@ -275,6 +332,19 @@ export type Customize<M extends AnyStateMachine> = {
275
332
  effects?: [OpenEffectSlots<Reached<M>>] extends [never]
276
333
  ? never
277
334
  : { [K in OpenEffectSlots<Reached<M>>]?: BindingOf<LogicAt<Reached<M>, K>> };
335
+ /** Bind the look of a Gate slot (`gate({ … })`) that a package left `open`, and add, replace or
336
+ * remove its handlers by Hook kind id and event key (`null` removes one), by slot name:
337
+ * `{ review: { look, hooks: { gitlab: { "note.MergeRequest": null } } } }`. At run time,
338
+ * `customize()` refuses a slot that holds no Gate slot: `customize(): machine "<id>" carries no
339
+ * Gate slot "<slot>" — its Gate slots are: <slots>`. */
340
+ gates?: [GateSlots<Reached<M>>] extends [never]
341
+ ? never
342
+ : {
343
+ [K in GateSlots<Reached<M>>]?: GateBinding<
344
+ GateTypesAt<Reached<M>, K>["meta"],
345
+ GateTypesAt<Reached<M>, K>["ev"]
346
+ >;
347
+ };
278
348
  /** Retune a Machine that this Machine composes, by slot name. The value has the same shape as
279
349
  * these parts, one level down. At run time, `customize()` refuses a slot that holds no Machine:
280
350
  * `customize(): machine "<id>" composes no Machine under "<slot>"`. */
@@ -326,6 +396,7 @@ export type Customize<M extends AnyStateMachine> = {
326
396
  type LooseParts = {
327
397
  agents?: Record<string, Partial<AgentDefinition> | undefined>;
328
398
  effects?: Record<string, EffectBinding | undefined>;
399
+ gates?: Record<string, { look?: unknown; hooks?: Record<string, Record<string, AnyHandler | null>> } | undefined>;
329
400
  actors?: Record<string, LooseParts | undefined>;
330
401
  image?: string;
331
402
  user?: string | UserContainer;
@@ -350,6 +421,9 @@ type LooseParts = {
350
421
  * - `customize(): machine "<id>" carries no Agent slot "<slot>" — its Agents are: <slots>`
351
422
  * - `customize(): machine "<id>" composes no Machine under "<slot>" — the Machines it composes are: <slots>`
352
423
  * - `customize(): machine "<id>" carries no Effect slot "<slot>" — its Effects are: <slots>`
424
+ * - `customize(): machine "<id>" carries no Gate slot "<slot>" — its Gate slots are: <slots>`
425
+ * - `` customize(): Gate slot "<slot>"'s look is bound once, to a look or "none" — not `open` again ``, and the
426
+ * refusals of a look that `gate()` makes
353
427
  * - `customize(): machine "<id>" declares no Repo Slot "<slot>" — its slots are: <slots>`
354
428
  * - `customize(): machine "<id>" declares its Repo Slots open as a map — name at least one`
355
429
  * - `` customize(): `repos` say what a SANDBOX is made of, how big it is and which Repos it
@@ -384,11 +458,11 @@ type LooseParts = {
384
458
  * @see docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md
385
459
  */
386
460
  export function customize<M extends AnyStateMachine>(machine: M, parts: Customize<M>): M {
387
- const { agents, effects, actors, image, user, repos, resources } = parts as LooseParts;
461
+ const { agents, effects, gates, actors, image, user, repos, resources } = parts as LooseParts;
388
462
  let out: AnyStateMachine = machine;
389
463
  // Order matters in one direction only: the Sandbox seats and slots REBUILD the wrapper, so they
390
464
  // go last and carry the retuned implementations with them.
391
- if (agents || effects || actors) out = retune(out, { agents, effects, actors });
465
+ if (agents || effects || gates || actors) out = retune(out, { agents, effects, gates, actors });
392
466
  const seats: Seats = {
393
467
  ...(image !== undefined ? { image } : {}),
394
468
  ...(user !== undefined ? { user } : {}),
@@ -458,6 +532,9 @@ function retune(machine: AnyStateMachine, parts: LooseParts): AnyStateMachine {
458
532
  }
459
533
  const bound = Object.entries(parts.effects ?? {}).filter(([, b]) => b !== undefined);
460
534
  for (const [name, binding] of bound) actors[name] = bindEffect(machine, name, binding!);
535
+ for (const [name, binding] of Object.entries(parts.gates ?? {})) {
536
+ if (binding) actors[name] = bindGate(machine, name, binding);
537
+ }
461
538
  for (const [name, childParts] of Object.entries(parts.actors ?? {})) {
462
539
  if (!childParts) continue;
463
540
  const child = asMachine((machine.implementations.actors as Record<string, unknown>)[name]);
@@ -547,6 +624,12 @@ function bindEffect(machine: AnyStateMachine, name: string, binding: EffectBindi
547
624
  );
548
625
  }
549
626
  }
627
+ if ((part === "server" || part === "tool") && (typeof value !== "string" || value === "")) {
628
+ throw new Error(
629
+ `customize(): the mcp Effect "${name}"'s ${part} is bound to a name, a string: ` +
630
+ `${effectBindingShape(part)} (ADR-0054, ADR-0079)`,
631
+ );
632
+ }
550
633
  if (part === "secret" && typeof value !== "string") {
551
634
  throw new Error(
552
635
  `customize(): the ${logic.effect.kind} Effect "${name}"'s secret is bound to a secret's name, a string — ` +
@@ -557,6 +640,62 @@ function bindEffect(machine: AnyStateMachine, name: string, binding: EffectBindi
557
640
  return binder(binding);
558
641
  }
559
642
 
643
+ /**
644
+ * Bind a Gate slot's look and patch its handlers (ADR-0011, ADR-0054). The slot is built again from
645
+ * its parts, with the same `meta`, `delivers` and `reply`, so a run of the import and a run of the
646
+ * bound Machine each have their own. A look is bound to a value once: `open` again is refused, and
647
+ * the bound look meets the checks that `gate()` runs.
648
+ */
649
+ function bindGate(
650
+ machine: AnyStateMachine,
651
+ name: string,
652
+ binding: NonNullable<NonNullable<LooseParts["gates"]>[string]>,
653
+ ): unknown {
654
+ const logic = (machine.implementations.actors as Record<string, unknown>)[name];
655
+ if (!isGateSlot(logic)) {
656
+ throw new Error(
657
+ `customize(): machine "${machine.id}" carries no Gate slot "${name}" — its Gate slots are: ` +
658
+ `${listed(slotsWhere(machine, isGateSlot))} (ADR-0011). Only declared parts can be bound.`,
659
+ );
660
+ }
661
+ const parts: GateSlotParts = logic.gateSlot;
662
+ let look = parts.look;
663
+ if (binding.look !== undefined) {
664
+ if (binding.look === open) {
665
+ throw new Error(
666
+ `customize(): Gate slot "${name}"'s look is bound once, to a look or "none" — not \`open\` again (ADR-0054)`,
667
+ );
668
+ }
669
+ const fault = lookFault(binding.look);
670
+ if (fault) throw new Error(`customize(): Gate slot "${name}": ${fault} (ADR-0011)`);
671
+ look = binding.look as GateSlotParts["look"];
672
+ }
673
+ const hooks: Record<string, Record<string, AnyHandler>> = {};
674
+ for (const [kind, keys] of Object.entries(parts.hooks)) hooks[kind] = { ...keys };
675
+ for (const [kind, keys] of Object.entries(binding.hooks ?? {})) {
676
+ if (typeof keys !== "object" || keys === null) {
677
+ throw new Error(`customize(): Gate slot "${name}": hooks.${kind} is { <event key>: handler | null } (ADR-0011)`);
678
+ }
679
+ const into = (hooks[kind] ??= {});
680
+ for (const [key, handler] of Object.entries(keys)) {
681
+ if (handler === null) delete into[key];
682
+ else if (handler !== undefined) into[key] = handler;
683
+ }
684
+ if (Object.keys(into).length === 0) delete hooks[kind];
685
+ }
686
+ const fault = hooksTableFault(hooks);
687
+ if (fault) throw new Error(`customize(): Gate slot "${name}": ${fault} (ADR-0011)`);
688
+ const frozen: Record<string, Readonly<Record<string, AnyHandler>>> = {};
689
+ for (const [kind, keys] of Object.entries(hooks)) frozen[kind] = Object.freeze(keys);
690
+ return gateSlotOf(
691
+ Object.freeze({
692
+ ...parts,
693
+ hooks: Object.freeze(frozen),
694
+ ...(look !== undefined ? { look } : {}),
695
+ }),
696
+ );
697
+ }
698
+
560
699
  const isSecret = (value: unknown): boolean =>
561
700
  typeof value === "object" && value !== null && typeof (value as { secret?: unknown }).secret === "string";
562
701
 
@@ -19,6 +19,9 @@
19
19
  // through the binding, foreign in the `jr2-dispatched-by` header), and the door answers a
20
20
  // repeat with the run the key already started. So a dispatch is idempotent by construction: a
21
21
  // restore that cannot tell whether its start arrived sends it again, and gets the same run.
22
+ // The key is asked before the slot's schema reads the body: in-process through the run host's
23
+ // lookup, foreign as a key-only ask (no body) when the slot's schema refuses the body. With no
24
+ // run under the key, the schema's refusal is the invoke's error, and nothing starts.
22
25
  // - THE RUN ON THE ROW. Once the start's answer names the run, the intent row is written again
23
26
  // with it, saved at the write: a restore with it settles `started` at once, or resumes the
24
27
  // wait on that run, and never starts another.
@@ -78,9 +81,10 @@ export type DispatchDefinition<S extends z.ZodObject | undefined = undefined> =
78
81
  wait?: boolean;
79
82
  /** How long the Effect waits: by default 30s for the start, and 10m with `wait`. */
80
83
  timeout?: Duration;
81
- /** A different Orchestrator: `url` is its origin, and `token` is the secret that holds its
82
- * Instance token. Or `open`, which the composer binds with `customize(<import>, { effects: {
83
- * <slot>: { target: { url, token } } } })`. Without it, the run starts in this Orchestrator. */
84
+ /** A different Orchestrator: `url` is its origin, and `token` is the secret that holds the token
85
+ * of a Caller of it with `start` for the Workflow, and `read` too when `wait` is true. Or `open`,
86
+ * which the composer binds with `customize(<import>, { effects: { <slot>: { target: { url, token
87
+ * } } } })`. Without it, the run starts in this Orchestrator. */
84
88
  target?: { url: string; token: NamedSecret } | typeof open;
85
89
  };
86
90
 
@@ -208,7 +212,9 @@ function targetOf(workflow: string, target: unknown): { url: string; token: stri
208
212
  );
209
213
  }
210
214
  if (typeof token !== "object" || token === null || typeof (token as { secret?: unknown }).secret !== "string") {
211
- throw new Error(`${where}: the token is a secret("<name>") holding the target's Instance token (ADR-0071)`);
215
+ throw new Error(
216
+ `${where}: the token is a secret("<name>") holding a Caller token of the target with start for the Workflow (ADR-0071)`,
217
+ );
212
218
  }
213
219
  const name = (token as { secret: string }).secret;
214
220
  if (name === OPEN_SECRET) {
@@ -255,23 +261,75 @@ export function openDispatchOf<S extends z.ZodObject | undefined>(
255
261
  }
256
262
 
257
263
  /** The wire request for one foreign start (ADR-0071): the target's origin, the Workflow, the body,
258
- * the token by name, and who dispatched it. */
264
+ * the token by name, who dispatched it, and the party it acts for. With no body (`undefined`), the
265
+ * start is a key-only ask: the door answers the run under the key, and starts nothing. */
259
266
  export function dispatchStartOf(
260
267
  parts: DispatchParts,
261
- input: Record<string, unknown>,
268
+ input: Record<string, unknown> | undefined,
262
269
  dispatchedBy: DispatchedBy & { instance: string },
270
+ onBehalfOf?: string,
263
271
  ): DispatchStartRequest {
264
272
  if (parts.target === undefined) throw new Error("dispatchStartOf: an in-process dispatch has no wire request");
265
273
  return {
266
274
  kind: "dispatch",
267
275
  url: [{ text: parts.target.url }],
268
276
  workflow: parts.workflow,
269
- input,
277
+ ...(input !== undefined ? { input } : { keyOnly: true as const }),
270
278
  auth: { bearer: { secret: parts.target.token } },
271
279
  dispatchedBy,
272
280
  wait: parts.wait,
273
281
  timeoutMs: parts.timeoutMs,
282
+ ...(onBehalfOf !== undefined ? { onBehalfOf } : {}),
283
+ };
284
+ }
285
+
286
+ /**
287
+ * What a foreign start sends as `onBehalfOf` for the first Caller of the calling run's chain
288
+ * (ADR-0071, ADR-0078): the party that Caller says it acts for, else the name that jr2 shows, else
289
+ * `<via>/<name>`. Undefined when jr2 started the chain, or the text is not printable ASCII of at
290
+ * most 256 characters, which a header cannot carry: the start then goes with no claim.
291
+ */
292
+ export function onBehalfOfFor(
293
+ caller: { via: string; name: string; shown?: string; onBehalfOf?: string } | undefined,
294
+ ): string | undefined {
295
+ if (!caller) return undefined;
296
+ const text = caller.onBehalfOf ?? caller.shown ?? `${caller.via}/${caller.name}`;
297
+ return /^[\x20-\x7e]{1,256}$/.test(text) ? text : undefined;
298
+ }
299
+
300
+ // --- A Pool's worker ------------------------------------------------------------------------------
301
+
302
+ /** What a Pool of runs gives its `dispatch` worker (ADR-0017): the run's body, and the Hook delivery
303
+ * that carried the item, when one did. The key is the worker's own spawn id: the item's id. */
304
+ export type PoolWorkerInput = { body: unknown; hook?: DispatchedBy["hook"] };
305
+
306
+ /** A dispatch's snapshot as a Pool's worker holds it: the key and the Hook delivery ride the
307
+ * persisted snapshot, so a restore sends the same key again. */
308
+ type KeyedSnapshot<O, I> = EffectSnapshot<O, I> & { key?: string; hook?: DispatchedBy["hook"] };
309
+
310
+ /**
311
+ * The `dispatch` slot of a Pool of runs, as the Pool spawns it (ADR-0017): the same Effect, whose
312
+ * idempotency key is the item's id — the spawn id — and not its ledger row id. So every Pool, every
313
+ * hand-over and every restore that gives the same item to the callee Workflow gets the same run
314
+ * (ADR-0071). The input is {@link PoolWorkerInput}.
315
+ *
316
+ * @internal
317
+ */
318
+ export function poolWorker(logic: EffectLogic<any, DispatchOutput>): EffectLogic<PoolWorkerInput, DispatchOutput> {
319
+ const keyed = Object.create(
320
+ Object.getPrototypeOf(logic) as object,
321
+ Object.getOwnPropertyDescriptors(logic),
322
+ ) as EffectLogic<PoolWorkerInput, DispatchOutput>;
323
+ const initial = logic.getInitialSnapshot.bind(logic);
324
+ keyed.getInitialSnapshot = (scope, input) => {
325
+ const given = (input ?? { body: undefined }) as PoolWorkerInput;
326
+ return {
327
+ ...initial(scope, given.body),
328
+ key: scope.self.id,
329
+ ...(given.hook !== undefined ? { hook: given.hook } : {}),
330
+ } as KeyedSnapshot<DispatchOutput, PoolWorkerInput> as never;
274
331
  };
332
+ return keyed;
275
333
  }
276
334
 
277
335
  // --- The actor logic -----------------------------------------------------------------------------
@@ -418,6 +476,9 @@ async function drive<I, O>(
418
476
  mark({ stage: "output", text: `${named}: run ${runId}${existing ? " (the key found it: started before)" : ""}` });
419
477
  };
420
478
  const deadline = Date.parse(at) + parts.timeoutMs;
479
+ // A Pool's worker carries the item's id as its key, and the delivery that carried the item
480
+ // (ADR-0017). Any other dispatch's key is its ledger row id.
481
+ const { key, hook } = snapshot as KeyedSnapshot<O, I>;
421
482
 
422
483
  if (prior?.run !== undefined && !parts.wait) {
423
484
  // Started before the restart, and the answer is the run: nothing to send.
@@ -440,22 +501,58 @@ async function drive<I, O>(
440
501
  await waitIn(prior.run);
441
502
  return;
442
503
  }
443
- // Fresh, or in flight with no run on the row: the key makes a second start the same run.
444
- const input = inputOf();
504
+ // The key first, as the door asks it (ADR-0071): a run of the callee that carries it is
505
+ // answered before the input is read, so a re-send across a change of the slot's schema answers
506
+ // `existing`, never an error. The slot's bound holds the lookup as it holds the start.
507
+ const startBound = prior ? Math.max(deadline, Date.now() + START_FLOOR_MS) : deadline;
508
+ const asked = host.lookup
509
+ ? await beforeDeadline(
510
+ host.lookup(parts.workflow, key ?? id).then((run) => ({ run })),
511
+ startBound,
512
+ signal,
513
+ )
514
+ : { run: undefined };
515
+ if (signal.aborted) return;
516
+ if (asked === undefined) {
517
+ settle(
518
+ unknown(
519
+ `the run host did not answer the start of ${parts.workflow} within the slot's timeout; a run may have started`,
520
+ ),
521
+ );
522
+ return;
523
+ }
524
+ const found = asked.run;
525
+ if (found) {
526
+ if (!prior) binding.recordEffect?.(id, intent());
527
+ pin(found.runId, true);
528
+ if (!parts.wait) {
529
+ settle({ outcome: "started", run: found.runId });
530
+ return;
531
+ }
532
+ await waitIn(found.runId);
533
+ return;
534
+ }
535
+ // Fresh, or in flight with no run on the row: the key makes a second start the same run. With
536
+ // the key asked above and no run under it, the body is a first send's, and the slot's schema
537
+ // refuses it as the invoke's error (ADR-0071). A host with no lookup asks the key at its start.
538
+ const input = inputOf(prior !== undefined && !host.lookup);
445
539
  if (!prior) binding.recordEffect?.(id, intent());
446
540
  mark({
447
541
  stage: "started",
448
542
  text: prior ? `${named} (sent again under its key: a run it started is answered)` : named,
449
543
  });
450
544
  // The depth is the callee's: one below this run, which the host records as sent (ADR-0071).
451
- const by: DispatchedBy = { run: binding.runId, slot, effect: id, depth: (binding.depth ?? 0) + 1 };
545
+ const by: DispatchedBy = {
546
+ run: binding.runId,
547
+ slot,
548
+ effect: id,
549
+ depth: (binding.depth ?? 0) + 1,
550
+ ...(key !== undefined ? { key } : {}),
551
+ ...(hook !== undefined ? { hook } : {}),
552
+ };
452
553
  // The slot's bound holds the start too (ADR-0071): a run host that does not answer in it is
453
554
  // `unknown` — the start may have been taken, and a restore's re-send under the key would find it.
454
- const answer = await beforeDeadline(
455
- host.start(parts.workflow, input, by),
456
- prior ? Math.max(deadline, Date.now() + START_FLOOR_MS) : deadline,
457
- signal,
458
- );
555
+ const answer = await beforeDeadline(host.start(parts.workflow, input, by), startBound, signal);
459
556
  if (signal.aborted) return;
460
557
  if (answer === undefined) {
461
558
  settle(
@@ -489,8 +586,13 @@ async function drive<I, O>(
489
586
  const port = portFactory(target);
490
587
  // The wait's stop goes through this port when its state exits (ADR-0071).
491
588
  run.port = port;
589
+ // The party the start acts for: the first Caller of this run's chain, read once (ADR-0071).
590
+ const onBehalfOf = onBehalfOfFor(await binding?.firstCaller?.());
591
+ if (signal.aborted) return;
492
592
  // Fresh: nothing was sent. The body is checked before the intent row, so a defect in the input is
493
- // the Machine's error and sends nothing.
593
+ // the Machine's error and sends nothing. A body the slot's schema refuses is sent as a key-only
594
+ // ask (ADR-0071): the door answers a run under the key, and with none the schema's refusal is the
595
+ // invoke's error.
494
596
  const req = prior ? undefined : requestFor(false);
495
597
  const big = req && tooLarge(req);
496
598
  if (big) {
@@ -567,18 +669,27 @@ async function drive<I, O>(
567
669
  expired();
568
670
  }
569
671
 
570
- /** The run's body: the invoke's input, parsed by the slot's schema when it has one. */
571
- function inputOf(): Record<string, unknown> {
672
+ /** Why the slot's schema refuses the invoke's input, or undefined when it takes it (or the slot
673
+ * has no schema). */
674
+ function refusedBody(): string | undefined {
675
+ if (def.input === undefined) return undefined;
676
+ const parsed = def.input.safeParse(snapshot.input ?? {});
677
+ if (parsed.success) return undefined;
678
+ return (
679
+ `dispatch "${slot}": its input does not match the slot's input schema — ` +
680
+ parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")
681
+ );
682
+ }
683
+
684
+ /** The run's body: the invoke's input, parsed by the slot's schema when it has one. `again`: an
685
+ * in-process re-send to a run host that asks the key only at its start; a body the slot's schema
686
+ * refuses then goes as it is, and the run host answers the run under the key (ADR-0071). */
687
+ function inputOf(again = false): Record<string, unknown> {
572
688
  let input: unknown = snapshot.input ?? {};
573
689
  if (def.input !== undefined) {
574
690
  const parsed = def.input.safeParse(input);
575
- if (!parsed.success) {
576
- throw new Error(
577
- `dispatch "${slot}": its input does not match the slot's input schema — ` +
578
- parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; "),
579
- );
580
- }
581
- input = parsed.data;
691
+ if (parsed.success) input = parsed.data;
692
+ else if (!again) throw new Error(refusedBody());
582
693
  }
583
694
  if (typeof input !== "object" || input === null || Array.isArray(input)) {
584
695
  throw new Error(`dispatch "${slot}": its input is the run's body, a JSON object (ADR-0071)`);
@@ -600,7 +711,9 @@ async function drive<I, O>(
600
711
  instance: binding?.orchestratorName ?? "an Orchestrator with no name",
601
712
  };
602
713
  const timeoutMs = again ? Math.max(1, Math.floor(deadline - Date.now())) : parts.timeoutMs;
603
- return dispatchStartOf({ ...parts, timeoutMs }, inputOf(), by);
714
+ // A body the slot's schema refuses is never sent: the start asks the key alone (ADR-0071).
715
+ const body = refusedBody() === undefined ? inputOf() : undefined;
716
+ return dispatchStartOf({ ...parts, timeoutMs }, body, by, onBehalfOf);
604
717
  }
605
718
 
606
719
  /** The in-process wait (ADR-0071): the run host tells when the run leaves `live`; the slot's
@@ -677,6 +790,9 @@ async function drive<I, O>(
677
790
  if (state.state === "settled" && state.output) {
678
791
  const parsed = DispatchOutputShape.safeParse(state.output);
679
792
  if (!parsed.success) throw new EffectorMalformed("the Effector's output for a dispatch is not a dispatch's");
793
+ // A key-only ask, and no run has the key: nothing was started, and the slot's schema refusal
794
+ // is the invoke's error (ADR-0071).
795
+ if (parsed.data.outcome === "absent") throw new Error(refusedBody() ?? "the door found no run under the key");
680
796
  const output = parsed.data as DispatchOutput;
681
797
  if (output.outcome === "started" || output.outcome === "settled") pin(output.run, false);
682
798
  settle(output);
package/src/durability.ts CHANGED
@@ -18,6 +18,8 @@ export type StoredSnapshot = {
18
18
  status: string;
19
19
  snapshot: unknown;
20
20
  reason?: string;
21
+ /** When the run started (ADR-0009): the row's `started_at`. */
22
+ startedAt?: string;
21
23
  };
22
24
 
23
25
  type ChildEntry = { snapshot?: { input?: unknown; [k: string]: unknown }; [k: string]: unknown };
@@ -0,0 +1,20 @@
1
+ // A duration as a person writes it in config or a query: a number and one unit, such as `"90m"`,
2
+ // `"36h"`, `"30d"` or `"2w"`. `runs.retain` (ADR-0065) and the `since` of `GET /runs` (ADR-0009)
3
+ // read it.
4
+
5
+ const DURATION = /^(\d+(?:\.\d+)?)(s|m|h|d|w)$/;
6
+ const UNIT_MS: Record<string, number> = {
7
+ s: 1000,
8
+ m: 60_000,
9
+ h: 3_600_000,
10
+ d: 86_400_000,
11
+ w: 7 * 86_400_000,
12
+ };
13
+
14
+ /** The duration in milliseconds, or undefined when `text` is not a positive duration. */
15
+ export function periodMs(text: string): number | undefined {
16
+ const m = DURATION.exec(text.trim());
17
+ if (!m) return undefined;
18
+ const ms = Number(m[1]) * UNIT_MS[m[2]!]!;
19
+ return ms > 0 && Number.isFinite(ms) ? ms : undefined;
20
+ }
@@ -39,6 +39,7 @@
39
39
  import type { AnyStateMachine, UnknownActorLogic } from "xstate";
40
40
  import type { EventDef } from "@jr2/agent-protocol";
41
41
  import { isAgent } from "./agent.ts";
42
+ import { isGateInvoke } from "./gate-brand.ts";
42
43
  import { inputKeysOf, inputShapeOf, isEffect } from "./effect.ts";
43
44
  import { mayTake, partiesAt } from "./party.ts";
44
45
  import { isAnswer, isPickMapper } from "./pick.ts";
@@ -389,7 +390,7 @@ export function effectChecks(
389
390
  const below: Node[] = [];
390
391
  const descend = (p: Node): void => {
391
392
  for (const child of Object.values(p.states)) {
392
- if (invokesAgent(child) !== undefined || child.invoke.some((inv) => inv.src === "gate")) continue;
393
+ if (invokesAgent(child) !== undefined || child.invoke.some((inv) => isGateInvoke(inv, actors))) continue;
393
394
  below.push(child);
394
395
  descend(child);
395
396
  }