@jr2/orchestrator 0.4.1 → 0.5.1

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 (327) hide show
  1. package/README.md +20 -2
  2. package/console/components/app.ts +2 -0
  3. package/console/components/fleet.ts +14 -1
  4. package/console/components/machine-pane.ts +41 -2
  5. package/console/main.ts +13 -1
  6. package/console/store.ts +102 -6
  7. package/console/style.css +21 -0
  8. package/docs/agents-block.md +5 -0
  9. package/docs/concepts/agents-and-turns.md +442 -0
  10. package/docs/concepts/architecture.md +230 -0
  11. package/docs/concepts/charts.md +143 -0
  12. package/docs/concepts/composition.md +357 -0
  13. package/docs/concepts/converge.md +254 -0
  14. package/docs/concepts/durable-runs.md +189 -0
  15. package/docs/concepts/effects.md +603 -0
  16. package/docs/concepts/gates.md +208 -0
  17. package/docs/concepts/machines-and-workflows.md +289 -0
  18. package/docs/concepts/pools-and-sources.md +230 -0
  19. package/docs/concepts/secrets-and-trust.md +222 -0
  20. package/docs/concepts/the-menu.md +378 -0
  21. package/docs/concepts/workspaces.md +678 -0
  22. package/docs/get-started/a-model-decides.md +563 -0
  23. package/docs/get-started/an-agent-in-a-sandbox.md +381 -0
  24. package/docs/get-started/connect-a-model.md +248 -0
  25. package/docs/get-started/create-an-instance.md +194 -0
  26. package/docs/get-started/have-your-assistant-write-a-machine.md +922 -0
  27. package/docs/get-started/install.md +126 -0
  28. package/docs/guides/add-tools-to-the-sandbox.md +295 -0
  29. package/docs/guides/answer-a-gate.md +304 -0
  30. package/docs/guides/bind-open-parts.md +223 -0
  31. package/docs/guides/cancel-a-run.md +146 -0
  32. package/docs/guides/clean-up.md +206 -0
  33. package/docs/guides/fix-a-failed-converge.md +361 -0
  34. package/docs/guides/give-the-cluster-git-credentials.md +222 -0
  35. package/docs/guides/iterate-on-a-machine-on-kind.md +190 -0
  36. package/docs/guides/let-an-agent-write-a-chart.md +226 -0
  37. package/docs/guides/look-inside-a-sandbox.md +174 -0
  38. package/docs/guides/mirror-the-kit-images.md +176 -0
  39. package/docs/guides/move-from-kind-to-a-shared-cluster.md +302 -0
  40. package/docs/guides/open-the-console.md +91 -0
  41. package/docs/guides/publish-a-machine-as-an-npm-package.md +350 -0
  42. package/docs/guides/push-and-sign-a-branch.md +486 -0
  43. package/docs/guides/run-a-chart-by-name.md +270 -0
  44. package/docs/guides/run-a-command-or-call-a-url.md +544 -0
  45. package/docs/guides/run-one-worker-per-item.md +321 -0
  46. package/docs/guides/set-an-agents-access-and-dials.md +375 -0
  47. package/docs/guides/size-a-workspace.md +204 -0
  48. package/docs/guides/start-a-run-from-a-run.md +237 -0
  49. package/docs/guides/start-a-run.md +207 -0
  50. package/docs/guides/upgrade-the-kit-version.md +240 -0
  51. package/docs/guides/use-a-hosted-provider.md +208 -0
  52. package/docs/guides/use-a-local-model.md +313 -0
  53. package/docs/guides/watch-a-run.md +326 -0
  54. package/docs/guides/write-a-workflow.md +318 -0
  55. package/docs/index.md +44 -0
  56. package/docs/llms.txt +216 -0
  57. package/docs/reference/authoring-surface/agent.md +128 -0
  58. package/docs/reference/authoring-surface/answer.md +140 -0
  59. package/docs/reference/authoring-surface/arg.md +60 -0
  60. package/docs/reference/authoring-surface/basic.md +62 -0
  61. package/docs/reference/authoring-surface/bearer.md +53 -0
  62. package/docs/reference/authoring-surface/chartschema.md +23 -0
  63. package/docs/reference/authoring-surface/chartschemafor.md +48 -0
  64. package/docs/reference/authoring-surface/command.md +191 -0
  65. package/docs/reference/authoring-surface/customize.md +64 -0
  66. package/docs/reference/authoring-surface/defineconfig.md +41 -0
  67. package/docs/reference/authoring-surface/defineevent.md +114 -0
  68. package/docs/reference/authoring-surface/dispatch.md +126 -0
  69. package/docs/reference/authoring-surface/fail.md +83 -0
  70. package/docs/reference/authoring-surface/file.md +122 -0
  71. package/docs/reference/authoring-surface/frompick.md +277 -0
  72. package/docs/reference/authoring-surface/http.md +159 -0
  73. package/docs/reference/authoring-surface/isfailure.md +58 -0
  74. package/docs/reference/authoring-surface/jr2setup.md +237 -0
  75. package/docs/reference/authoring-surface/open.md +74 -0
  76. package/docs/reference/authoring-surface/openbody.md +86 -0
  77. package/docs/reference/authoring-surface/paramsof.md +125 -0
  78. package/docs/reference/authoring-surface/pool.md +186 -0
  79. package/docs/reference/authoring-surface/push.md +112 -0
  80. package/docs/reference/authoring-surface/secret.md +118 -0
  81. package/docs/reference/authoring-surface/sentby.md +83 -0
  82. package/docs/reference/authoring-surface/source.md +51 -0
  83. package/docs/reference/authoring-surface/text.md +62 -0
  84. package/docs/reference/authoring-surface/type-agentdeclaration.md +27 -0
  85. package/docs/reference/authoring-surface/type-agentdefinition.md +80 -0
  86. package/docs/reference/authoring-surface/type-agentlogic.md +15 -0
  87. package/docs/reference/authoring-surface/type-agentslots.md +12 -0
  88. package/docs/reference/authoring-surface/type-answeraction.md +12 -0
  89. package/docs/reference/authoring-surface/type-arg.md +19 -0
  90. package/docs/reference/authoring-surface/type-argnames.md +12 -0
  91. package/docs/reference/authoring-surface/type-binding.md +62 -0
  92. package/docs/reference/authoring-surface/type-bindingof.md +12 -0
  93. package/docs/reference/authoring-surface/type-bodyacceptsdoor.md +15 -0
  94. package/docs/reference/authoring-surface/type-bodyacceptsslots.md +17 -0
  95. package/docs/reference/authoring-surface/type-ceilings.md +41 -0
  96. package/docs/reference/authoring-surface/type-chainlink.md +19 -0
  97. package/docs/reference/authoring-surface/type-chartaction.md +46 -0
  98. package/docs/reference/authoring-surface/type-chartagent.md +28 -0
  99. package/docs/reference/authoring-surface/type-chartevent.md +20 -0
  100. package/docs/reference/authoring-surface/type-chartguard.md +20 -0
  101. package/docs/reference/authoring-surface/type-chartinvoke.md +28 -0
  102. package/docs/reference/authoring-surface/type-chartjson.md +14 -0
  103. package/docs/reference/authoring-surface/type-chartstate.md +46 -0
  104. package/docs/reference/authoring-surface/type-charttext.md +40 -0
  105. package/docs/reference/authoring-surface/type-charttransition.md +22 -0
  106. package/docs/reference/authoring-surface/type-charttransitions.md +14 -0
  107. package/docs/reference/authoring-surface/type-chartvalue.md +15 -0
  108. package/docs/reference/authoring-surface/type-childat.md +12 -0
  109. package/docs/reference/authoring-surface/type-childslots.md +12 -0
  110. package/docs/reference/authoring-surface/type-commandenv.md +15 -0
  111. package/docs/reference/authoring-surface/type-commandinput.md +19 -0
  112. package/docs/reference/authoring-surface/type-commandoptions.md +78 -0
  113. package/docs/reference/authoring-surface/type-commandoutput.md +104 -0
  114. package/docs/reference/authoring-surface/type-customize.md +109 -0
  115. package/docs/reference/authoring-surface/type-dispatchdefinition.md +39 -0
  116. package/docs/reference/authoring-surface/type-dispatchinput.md +14 -0
  117. package/docs/reference/authoring-surface/type-dispatchopenparts.md +14 -0
  118. package/docs/reference/authoring-surface/type-dispatchoutput.md +65 -0
  119. package/docs/reference/authoring-surface/type-duration.md +15 -0
  120. package/docs/reference/authoring-surface/type-effectbinding.md +93 -0
  121. package/docs/reference/authoring-surface/type-effectlogic.md +12 -0
  122. package/docs/reference/authoring-surface/type-envopenparts.md +14 -0
  123. package/docs/reference/authoring-surface/type-eventaudience.md +27 -0
  124. package/docs/reference/authoring-surface/type-eventdef.md +45 -0
  125. package/docs/reference/authoring-surface/type-eventfrom.md +16 -0
  126. package/docs/reference/authoring-surface/type-eventsemantics.md +26 -0
  127. package/docs/reference/authoring-surface/type-execoutcome.md +34 -0
  128. package/docs/reference/authoring-surface/type-failslot.md +14 -0
  129. package/docs/reference/authoring-surface/type-failure.md +19 -0
  130. package/docs/reference/authoring-surface/type-failureinput.md +20 -0
  131. package/docs/reference/authoring-surface/type-filebody.md +41 -0
  132. package/docs/reference/authoring-surface/type-frominput.md +14 -0
  133. package/docs/reference/authoring-surface/type-hostinjectedinput.md +58 -0
  134. package/docs/reference/authoring-surface/type-httpdefinition.md +101 -0
  135. package/docs/reference/authoring-surface/type-httpinput.md +12 -0
  136. package/docs/reference/authoring-surface/type-httpmethod.md +14 -0
  137. package/docs/reference/authoring-surface/type-httpopenparts.md +12 -0
  138. package/docs/reference/authoring-surface/type-httpoutput.md +89 -0
  139. package/docs/reference/authoring-surface/type-httprequestparts.md +33 -0
  140. package/docs/reference/authoring-surface/type-jr2config.md +126 -0
  141. package/docs/reference/authoring-surface/type-lends.md +38 -0
  142. package/docs/reference/authoring-surface/type-logicat.md +12 -0
  143. package/docs/reference/authoring-surface/type-markedif.md +12 -0
  144. package/docs/reference/authoring-surface/type-mergedactors.md +12 -0
  145. package/docs/reference/authoring-surface/type-namedsecret.md +25 -0
  146. package/docs/reference/authoring-surface/type-noslotspec.md +26 -0
  147. package/docs/reference/authoring-surface/type-noslotworkspaceoptions.md +32 -0
  148. package/docs/reference/authoring-surface/type-openbodydeclaration.md +40 -0
  149. package/docs/reference/authoring-surface/type-openbodylogic.md +14 -0
  150. package/docs/reference/authoring-surface/type-openbodyoutput.md +42 -0
  151. package/docs/reference/authoring-surface/type-openbodyslot.md +18 -0
  152. package/docs/reference/authoring-surface/type-opencommandinput.md +17 -0
  153. package/docs/reference/authoring-surface/type-openeffect.md +12 -0
  154. package/docs/reference/authoring-surface/type-openeffectslots.md +12 -0
  155. package/docs/reference/authoring-surface/type-openof.md +12 -0
  156. package/docs/reference/authoring-surface/type-opensecret.md +23 -0
  157. package/docs/reference/authoring-surface/type-opensecretmark.md +12 -0
  158. package/docs/reference/authoring-surface/type-permissivenoslotworkspaceoptions.md +31 -0
  159. package/docs/reference/authoring-surface/type-permissiveworkspaceoptions.md +32 -0
  160. package/docs/reference/authoring-surface/type-poolmachine.md +28 -0
  161. package/docs/reference/authoring-surface/type-poolspec.md +60 -0
  162. package/docs/reference/authoring-surface/type-pushinput.md +15 -0
  163. package/docs/reference/authoring-surface/type-pushoptions.md +79 -0
  164. package/docs/reference/authoring-surface/type-pushoutput.md +124 -0
  165. package/docs/reference/authoring-surface/type-pushwireoutput.md +12 -0
  166. package/docs/reference/authoring-surface/type-quantity.md +14 -0
  167. package/docs/reference/authoring-surface/type-reached.md +12 -0
  168. package/docs/reference/authoring-surface/type-reposlot.md +70 -0
  169. package/docs/reference/authoring-surface/type-reposlotsof.md +16 -0
  170. package/docs/reference/authoring-surface/type-sandboxoptions.md +77 -0
  171. package/docs/reference/authoring-surface/type-secret.md +25 -0
  172. package/docs/reference/authoring-surface/type-sender.md +19 -0
  173. package/docs/reference/authoring-surface/type-sentbyguards.md +12 -0
  174. package/docs/reference/authoring-surface/type-size.md +85 -0
  175. package/docs/reference/authoring-surface/type-slothandles.md +32 -0
  176. package/docs/reference/authoring-surface/type-sourcenextresult.md +17 -0
  177. package/docs/reference/authoring-surface/type-sourcespec.md +44 -0
  178. package/docs/reference/authoring-surface/type-syncstate.md +12 -0
  179. package/docs/reference/authoring-surface/type-text.md +19 -0
  180. package/docs/reference/authoring-surface/type-textpart.md +24 -0
  181. package/docs/reference/authoring-surface/type-thinkinglevel.md +39 -0
  182. package/docs/reference/authoring-surface/type-toparameterizedobject.md +14 -0
  183. package/docs/reference/authoring-surface/type-toprovidedactor.md +14 -0
  184. package/docs/reference/authoring-surface/type-unknown.md +21 -0
  185. package/docs/reference/authoring-surface/type-usercontainer.md +77 -0
  186. package/docs/reference/authoring-surface/type-withoutemptyindex.md +20 -0
  187. package/docs/reference/authoring-surface/type-workflowevent.md +46 -0
  188. package/docs/reference/authoring-surface/type-workspaceaccess.md +16 -0
  189. package/docs/reference/authoring-surface/type-workspaced.md +94 -0
  190. package/docs/reference/authoring-surface/type-workspacehandles.md +23 -0
  191. package/docs/reference/authoring-surface/type-workspacemachine.md +29 -0
  192. package/docs/reference/authoring-surface/type-workspaceof.md +13 -0
  193. package/docs/reference/authoring-surface/type-workspaceoptions.md +39 -0
  194. package/docs/reference/authoring-surface/type-workspacespec.md +37 -0
  195. package/docs/reference/authoring-surface/workspace.md +259 -0
  196. package/docs/reference/authoring-surface.md +197 -0
  197. package/docs/reference/cli.md +433 -0
  198. package/docs/reference/config.md +756 -0
  199. package/docs/reference/decisions/0001-operator-owned-generic-sandbox.md +113 -0
  200. package/docs/reference/decisions/0002-duplex-streaming-actor.md +110 -0
  201. package/docs/reference/decisions/0004-worktree-provisioning-model.md +124 -0
  202. package/docs/reference/decisions/0005-sandbox-pod-composition.md +230 -0
  203. package/docs/reference/decisions/0006-agent-control-surface.md +47 -0
  204. package/docs/reference/decisions/0007-durable-machine-state.md +109 -0
  205. package/docs/reference/decisions/0008-orchestrator-as-deployed-app.md +60 -0
  206. package/docs/reference/decisions/0009-cli-and-instance-interface.md +275 -0
  207. package/docs/reference/decisions/0010-bdd-acceptance-tests.md +160 -0
  208. package/docs/reference/decisions/0011-workflow-defined-events.md +141 -0
  209. package/docs/reference/decisions/0012-workspace-wrapper-machine.md +103 -0
  210. package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +157 -0
  211. package/docs/reference/decisions/0014-observation-is-open-run-state-is-not.md +95 -0
  212. package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +265 -0
  213. package/docs/reference/decisions/0016-agent-turn-mechanics-are-internal.md +124 -0
  214. package/docs/reference/decisions/0017-pool-over-a-source-port.md +69 -0
  215. package/docs/reference/decisions/0018-instance-agents-are-definitions-jr2-assembles-the-harness.md +111 -0
  216. package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +275 -0
  217. package/docs/reference/decisions/0020-private-ca-trust-is-a-named-concept.md +60 -0
  218. package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +100 -0
  219. package/docs/reference/decisions/0022-observation-is-a-level-triggered-feed.md +103 -0
  220. package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +176 -0
  221. package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +439 -0
  222. package/docs/reference/decisions/0025-cancel-ends-a-run-stop-parks-it.md +65 -0
  223. package/docs/reference/decisions/0026-a-turn-that-is-over-is-never-prompted.md +62 -0
  224. package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +237 -0
  225. package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +89 -0
  226. package/docs/reference/decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md +154 -0
  227. package/docs/reference/decisions/0030-a-snapshot-names-the-machine-it-was-written-under.md +100 -0
  228. package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +190 -0
  229. package/docs/reference/decisions/0032-the-console-unlocks-with-the-instance-token.md +61 -0
  230. package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +127 -0
  231. package/docs/reference/decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md +57 -0
  232. package/docs/reference/decisions/0035-a-runaway-turn-is-ended-rerolled-once-then-a-fault.md +92 -0
  233. package/docs/reference/decisions/0036-a-turn-compacts-its-own-context-mid-flight.md +138 -0
  234. package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +196 -0
  235. package/docs/reference/decisions/0038-jr2-up-builds-every-image-it-deploys.md +170 -0
  236. package/docs/reference/decisions/0039-image-garbage-collects-by-reachability.md +115 -0
  237. package/docs/reference/decisions/0041-a-build-the-host-already-holds-is-not-spent-again.md +64 -0
  238. package/docs/reference/decisions/0042-ready-is-not-routable-an-admission-retries-its-connection.md +142 -0
  239. package/docs/reference/decisions/0043-the-kit-is-tested-as-installed-a-local-registry-stands-in-for-npm.md +170 -0
  240. package/docs/reference/decisions/0044-kit-images-live-at-a-canonical-home-a-self-host-mirrors-it.md +85 -0
  241. package/docs/reference/decisions/0045-the-platform-joins-the-image-address-and-the-cluster-chooses-it.md +82 -0
  242. package/docs/reference/decisions/0046-a-converge-failure-carries-its-diagnosis.md +34 -0
  243. package/docs/reference/decisions/0047-the-git-ssh-key-source-is-the-users-choice.md +47 -0
  244. package/docs/reference/decisions/0048-the-orchestrator-boots-without-its-repos.md +36 -0
  245. package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +126 -0
  246. package/docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md +73 -0
  247. package/docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md +227 -0
  248. package/docs/reference/decisions/0052-a-sandbox-lands-where-an-ordinary-pod-lands-and-the-cache-follows-it.md +81 -0
  249. package/docs/reference/decisions/0053-a-fetch-inside-the-pod-asks-the-node-cache-and-the-cache-asks-the-remote.md +111 -0
  250. package/docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md +126 -0
  251. package/docs/reference/decisions/0055-a-release-is-a-pushed-tag-and-the-publish-guard-is-the-credential.md +107 -0
  252. package/docs/reference/decisions/0056-a-global-jr2-hands-off-to-the-instances-own-and-an-instance-has-one-kit-version.md +62 -0
  253. package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +152 -0
  254. package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +106 -0
  255. package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +384 -0
  256. package/docs/reference/decisions/0060-a-workspace-states-its-size-and-jr2-reserves-all-of-it.md +111 -0
  257. package/docs/reference/decisions/0061-a-memory-kill-ends-the-agents-processes-not-the-harness.md +83 -0
  258. package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +212 -0
  259. package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +83 -0
  260. package/docs/reference/decisions/0064-a-workspace-waits-for-capacity.md +79 -0
  261. package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-a-week.md +65 -0
  262. package/docs/reference/decisions/0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md +550 -0
  263. package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +430 -0
  264. package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +464 -0
  265. package/docs/reference/decisions/0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md +180 -0
  266. package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +323 -0
  267. package/docs/reference/decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md +183 -0
  268. package/docs/reference/decisions/0072-the-docs-ship-inside-the-kit-and-an-instances-agents-md-indexes-them.md +144 -0
  269. package/docs/reference/decisions/0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md +122 -0
  270. package/docs/reference/decisions/0074-an-unrouted-mechanism-event-is-refused-at-load-and-faults-the-run.md +76 -0
  271. package/docs/reference/decisions.md +83 -0
  272. package/docs/reference/glossary.md +648 -0
  273. package/docs/reference/http-api.md +1348 -0
  274. package/docs/reference/machines.md +190 -0
  275. package/docs/reference/mechanism-events.md +72 -0
  276. package/docs/why.md +206 -0
  277. package/package.json +5 -3
  278. package/src/actor.ts +541 -79
  279. package/src/agent.ts +114 -38
  280. package/src/ambient.ts +2 -1
  281. package/src/assembly-checks.ts +189 -7
  282. package/src/cause.ts +234 -0
  283. package/src/chart.ts +169 -37
  284. package/src/charts.ts +34 -6
  285. package/src/config.ts +430 -178
  286. package/src/custodian.ts +74 -22
  287. package/src/customize.ts +188 -46
  288. package/src/dispatch-effect.ts +945 -0
  289. package/src/door-compat.ts +217 -0
  290. package/src/durability.ts +11 -4
  291. package/src/effect.ts +341 -44
  292. package/src/effector-wire.ts +160 -11
  293. package/src/fail.ts +209 -0
  294. package/src/harness-client.ts +541 -47
  295. package/src/http-effect.ts +495 -53
  296. package/src/http.ts +427 -16
  297. package/src/index.ts +60 -82
  298. package/src/instance.ts +68 -8
  299. package/src/internal.ts +84 -0
  300. package/src/machine-doc.ts +21 -3
  301. package/src/mechanism-routes.ts +168 -0
  302. package/src/model-keys.generated.ts +53 -0
  303. package/src/model-keys.ts +18 -58
  304. package/src/notices.ts +66 -13
  305. package/src/open-body.ts +159 -23
  306. package/src/open-effect.ts +81 -8
  307. package/src/open.ts +59 -6
  308. package/src/parts.ts +184 -13
  309. package/src/party.ts +68 -2
  310. package/src/pick.ts +463 -11
  311. package/src/pool.ts +247 -36
  312. package/src/registration.ts +83 -4
  313. package/src/repos.ts +2 -1
  314. package/src/run-host.ts +565 -31
  315. package/src/run-output.ts +38 -0
  316. package/src/sandbox-effect.ts +273 -38
  317. package/src/sandbox-kube.ts +3 -1
  318. package/src/secret-placement.ts +52 -17
  319. package/src/server.ts +3 -0
  320. package/src/setup.ts +239 -56
  321. package/src/size.ts +121 -9
  322. package/src/snapshot-store.ts +54 -10
  323. package/src/stub-harness.ts +44 -16
  324. package/src/timers.ts +14 -1
  325. package/src/vocabulary.ts +69 -14
  326. package/src/wire.ts +90 -16
  327. package/src/workspace.ts +697 -175
package/src/run-host.ts CHANGED
@@ -36,6 +36,7 @@ import { placeAnswers, type AnswerSeat } from "./pick.ts";
36
36
  import { inputSchemaOf } from "./vocabulary.ts";
37
37
  import type { EchoEvent, EchoStatusChild, LiveConversation, PickAnswer } from "./wire.ts";
38
38
  import { answerOf, childErrorOf, replyOf, type Hold } from "./holds.ts";
39
+ import { PickCauses } from "./cause.ts";
39
40
  import { restOf, runningBelow } from "./rest.ts";
40
41
  import {
41
42
  acceptsNow,
@@ -52,6 +53,8 @@ import {
52
53
  wouldMove,
53
54
  targetsOf,
54
55
  leftAgain,
56
+ type DispatchedBy,
57
+ type DispatchStart,
55
58
  type Registration,
56
59
  type RetryTelemetry,
57
60
  type RunBinding,
@@ -69,6 +72,8 @@ import {
69
72
  } from "./workspace.ts";
70
73
  import { INSTANCE_HARNESS_SERVICE } from "./names.ts";
71
74
  import { composesSandbox } from "./parts.ts";
75
+ import { failurePayload, isFailure, type FailureInput } from "./fail.ts";
76
+ import { unroutedMechanismEvent, type MicrostepView } from "./mechanism-routes.ts";
72
77
  import { fingerprintOf } from "./fingerprint.ts";
73
78
  import {
74
79
  serializeMachine,
@@ -214,8 +219,44 @@ function chartDrift(machine: AnyStateMachine, routes: readonly BoundChartRoute[]
214
219
  return undefined;
215
220
  }
216
221
 
217
- /** The serializable identity of a run — what reconcile sees and what restore rebuilds from. */
218
- export type RunRecord = { runId: string; workflow: string; instanceId: string };
222
+ /** The serializable identity of a run — what reconcile sees and what restore rebuilds from.
223
+ * `dispatchedBy` is the run that started it through a `dispatch` Effect, and this run's depth in
224
+ * that chain, as the caller sent it (ADR-0071); absent for a run a human or a client started. */
225
+ export type RunRecord = { runId: string; workflow: string; instanceId: string; dispatchedBy?: DispatchedBy };
226
+
227
+ /** A start of a name no Workflow is registered under: the door answers 404 (ADR-0009), and an
228
+ * in-process `dispatch` settles `refused` with status 404 (ADR-0071). */
229
+ export class UnknownWorkflowError extends Error {
230
+ override name = "UnknownWorkflowError";
231
+ }
232
+
233
+ /** A start whose idempotency key a run of ANOTHER Workflow already carries (ADR-0071): the door
234
+ * answers 409, and an in-process `dispatch` settles `refused` with status 409. Never answered as
235
+ * the existing run: the caller asked for a run of one Workflow and would wait on a run of another. */
236
+ export class DispatchKeyConflictError extends Error {
237
+ override name = "DispatchKeyConflictError";
238
+ }
239
+
240
+ /** The run a key names (ADR-0071), with its Workflow so a start of another can be refused. */
241
+ export type DispatchedRun = { runId: string; instanceId: string; workflow: string };
242
+
243
+ /** The run `found` as the answer to a start of `workflow` under its key, or the 409 (ADR-0071). */
244
+ export function keyAnswer(
245
+ found: DispatchedRun,
246
+ workflow: string,
247
+ effect: string,
248
+ ): { runId: string; instanceId: string } {
249
+ if (found.workflow !== workflow) throw new DispatchKeyConflictError(keyConflict(found, workflow, effect));
250
+ return { runId: found.runId, instanceId: found.instanceId };
251
+ }
252
+
253
+ /** Why a start of `workflow` under `effect` is refused, when the key's run is of another. */
254
+ export function keyConflict(found: DispatchedRun, workflow: string, effect: string): string {
255
+ return (
256
+ `the key "${effect}" already started run ${found.runId} of "${found.workflow}", not of "${workflow}" — ` +
257
+ "one key is one start of one Workflow (ADR-0071)"
258
+ );
259
+ }
219
260
 
220
261
  /**
221
262
  * One live child MACHINE of a run, and its own children below it. A workflow's root machine is
@@ -306,24 +347,31 @@ function runWaiting(snapshot: unknown, logic?: unknown, path: string[] = []): Ru
306
347
  *
307
348
  * Left out: a Workspace in `placing`, which deletes its own Sandbox when its wait is aborted — and
308
349
  * only once any write in flight has settled, so a delete from here could race the apply and leave
309
- * the CR behind (ADR-0064); and one that is `done` or ended, whose `teardown` already ran.
350
+ * the CR behind (ADR-0064); and one that is `done` or ended, whose `teardown` or `failing` already
351
+ * ran. An actor that ended in `error` is not a Workspace here, but its children are read: a failed
352
+ * run's root (ADR-0073) is in `error`, and its active children are still in its snapshot, as
353
+ * `stopTree` reads them.
354
+ *
355
+ * With `placing`, a Workspace in `placing` is also named: the names that a live Workspace holds
356
+ * now, which a delete must leave alone.
310
357
  */
311
- function runSandboxes(actor: AnyActorRef, runId: string): string[] {
358
+ function runSandboxes(actor: AnyActorRef, runId: string, opts: { placing?: boolean } = {}): string[] {
312
359
  const out: string[] = [];
313
360
  const snap = actor.getSnapshot() as
314
361
  | { status?: string; value?: unknown; context?: { wsId?: unknown }; children?: Record<string, AnyActorRef> }
315
362
  | undefined;
316
- if (snap?.status !== "active") return out;
363
+ if (snap?.status !== "active" && snap?.status !== "error") return out;
317
364
  const logic = (actor as AnyActorRef & { logic?: unknown }).logic;
318
365
  if (
366
+ snap.status === "active" &&
319
367
  logic != null &&
320
368
  composesSandbox(logic as AnyStateMachine) &&
321
- snap.value !== "placing" &&
369
+ (opts.placing === true || snap.value !== "placing") &&
322
370
  typeof snap.context?.wsId === "string"
323
371
  ) {
324
372
  out.push(workspaceName(runId, snap.context.wsId));
325
373
  }
326
- for (const child of Object.values(snap.children ?? {})) out.push(...runSandboxes(child, runId));
374
+ for (const child of Object.values(snap.children ?? {})) out.push(...runSandboxes(child, runId, opts));
327
375
  return out;
328
376
  }
329
377
 
@@ -443,7 +491,8 @@ function heldOf(holds: Iterable<Hold>): RunHeld[] {
443
491
  }
444
492
 
445
493
  /** A run's current observable state. `fault` carries the error message when status is "error"
446
- * (e.g. a gate invoked with a name outside the workflow's manifest — ADR-0011). */
494
+ * (e.g. a gate invoked with a name outside the workflow's manifest — ADR-0011). A run that ends
495
+ * "failed" (ADR-0073) has no `fault`: its `output` is the Failure's `{ reason, detail? }`. */
447
496
  export type RunStatus = RunRecord & {
448
497
  status: string;
449
498
  value: unknown;
@@ -475,8 +524,24 @@ export type RunStatus = RunRecord & {
475
524
  * second Chart in its life (a planner whose first Chart reaches a ceiling picks again), so these,
476
525
  * not the run id, say which bound Machine `runMachine` draws. Absent when it binds none. */
477
526
  chartHashes?: string[];
527
+ /**
528
+ * The Machine's output, for a run that is no longer live (ADR-0071): what a `dispatch` with
529
+ * `wait` settles with, and what `GET /runs/<id>` serves its foreign caller. Never for a live run,
530
+ * whose Machine has none yet. For a `failed` run, the Failure's `{ reason, detail? }` (ADR-0073),
531
+ * not a value of the Machine's output type. Behind the token, like `context`: it is the
532
+ * workflow's data.
533
+ */
534
+ output?: unknown;
535
+ /** The runs this run's `dispatch` Effects started, oldest first (ADR-0071): the chain's other
536
+ * direction, read off the caller's own ledger rows. A foreign dispatch's run is a run of the
537
+ * Orchestrator its target names. Absent when it started none. */
538
+ dispatched?: Dispatched[];
478
539
  };
479
540
 
541
+ /** One run a `dispatch` Effect of this run started (ADR-0071): its slot, the run, and when the
542
+ * ledger row was written. */
543
+ export type Dispatched = { slot: string; run: string; at: string };
544
+
480
545
  /**
481
546
  * A run as an UNAUTHENTICATED observer may see it: which run, of what workflow, and where in the
482
547
  * Machine it is. That is the whole set — enough to light up a state in the Console, and nothing
@@ -506,6 +571,13 @@ export type RunObservation = {
506
571
  /** The hashes of the Charts it binds now (see {@link RunStatus.chartHashes}). Structure: a hash
507
572
  * of a Chart's text, which `runMachine` already serves on the open band. */
508
573
  chartHashes?: string[];
574
+ /** The run that dispatched this one, its slot, and the chain's depth (ADR-0071). Routing — two
575
+ * ids, a slot name and a number — the class of thing `runId` and `value` are, so ADR-0014's line
576
+ * is unmoved. The idempotency key is not: it names the caller's ledger, which is the caller's. */
577
+ dispatchedBy?: Omit<DispatchedBy, "effect">;
578
+ /** The runs it dispatched, by slot (ADR-0071): the same routing as `dispatchedBy`, the other
579
+ * direction. */
580
+ dispatched?: Array<{ slot: string; run: string }>;
509
581
  };
510
582
 
511
583
  /** Project a full status down to what an observer may see. The one place the line is drawn. */
@@ -518,9 +590,17 @@ export function observe(status: RunStatus): RunObservation {
518
590
  children: status.children,
519
591
  ...(status.bound ? { bound: true as const } : {}),
520
592
  ...(status.chartHashes ? { chartHashes: status.chartHashes } : {}),
593
+ ...(status.dispatchedBy ? { dispatchedBy: routingOf(status.dispatchedBy) } : {}),
594
+ ...(status.dispatched ? { dispatched: status.dispatched.map(({ slot, run }) => ({ slot, run })) } : {}),
521
595
  };
522
596
  }
523
597
 
598
+ /** A {@link DispatchedBy} without its idempotency key (ADR-0071): what the open band carries. */
599
+ function routingOf(by: DispatchedBy): Omit<DispatchedBy, "effect"> {
600
+ const { effect: _key, ...routing } = by;
601
+ return routing;
602
+ }
603
+
524
604
  /** An Agent registration's Menu (ADR-0029): the one it registered, or — registered with none, by a
525
605
  * plain `setup()` machine — its derived set. */
526
606
  function menuOf(reg: Registration): Map<string, EventDef> {
@@ -805,6 +885,9 @@ export type RunHostOptions = {
805
885
  sandbox?: SandboxPort;
806
886
  /** The Instance Harness base URL (ADR-0031) — where `workspace: "none"` Turns are admitted. */
807
887
  instanceHarness?: string;
888
+ /** The name this Orchestrator gives itself at another Orchestrator's door, as a foreign
889
+ * dispatch's `dispatchedBy.instance` (ADR-0071): stable and not secret. Deployed, its namespace. */
890
+ orchestratorName?: string;
808
891
  /** The Harness bearer for a placement (ADR-0058) — `startInstance` closes `harnessToken` over the
809
892
  * signing key. Handed to every run's binding, and to the echo pusher below. Absent, no Harness
810
893
  * call bears one: only the stub Harness answers such a host. */
@@ -812,6 +895,13 @@ export type RunHostOptions = {
812
895
  /** Where an Effector placement is, and its key (ADR-0068) — `startInstance` derives both, the key
813
896
  * from the signing key. Handed to every run's binding; absent, an http Effect faults pointedly. */
814
897
  effector?: (placement: EffectorPlacement) => EffectorTarget | undefined;
898
+ /**
899
+ * Why a listed named Chart cannot run now, or undefined (ADR-0070): the registry of named Charts
900
+ * keeps the unrunnable ones, never the host. An in-process `dispatch` of one is refused 409 with
901
+ * this reason, as its start through the door is (ADR-0071). Absent, such a name answers 404:
902
+ * the host registers only the runnable ones.
903
+ */
904
+ unrunnableChart?: (name: string) => string | undefined;
815
905
  /**
816
906
  * Build the run-narrative echo pusher for one Workspace's Harness (ADR-0023) — the seam a fake
817
907
  * echo server rides in tests; `startInstance` binds the real wire push (harness-client.ts).
@@ -840,6 +930,9 @@ export type RunHostOptions = {
840
930
  type RunBlob = {
841
931
  workflow: string;
842
932
  instanceId: string;
933
+ /** The run that dispatched this one (ADR-0071), its key included: the store finds a run by its
934
+ * key here (`findByDispatch`), so a re-sent start answers the run it already made. */
935
+ dispatchedBy?: DispatchedBy;
843
936
  /** The shape of the Machine that WROTE this snapshot (ADR-0030). `workflow` says which def to
844
937
  * look up; this says whether the def found there is still the one this snapshot can be read by.
845
938
  * Absent means written before the stamp existed, which restore treats as drift — the point is to
@@ -906,6 +999,9 @@ type RunBlob = {
906
999
  */
907
1000
  startedBy?: Record<string, StartedBy>;
908
1001
  fault?: string;
1002
+ /** The Failure that ended the run `failed` (ADR-0073), as plain data: the run's output. xstate
1003
+ * keeps the error of an errored snapshot, and serializes it to `{}`, so it is kept here. */
1004
+ failure?: FailureInput;
909
1005
  };
910
1006
 
911
1007
  type LiveRun = {
@@ -930,6 +1026,8 @@ type LiveRun = {
930
1026
  /** The error that killed the run, if it errored (xstate serializes Error to `{}`, so the
931
1027
  * message is captured here at the observer and persisted onto the blob for `read`). */
932
1028
  fault?: string;
1029
+ /** The Failure that ended the run, if it failed (ADR-0073): persisted as `RunBlob.failure`. */
1030
+ failure?: FailureInput;
933
1031
  /** Per-run observers fed by `persist()` (status) and the actor's `emit` (emit) — SSE/CLI watch. */
934
1032
  listeners: Set<(e: RunFeedEvent) => void>;
935
1033
  /** The run's feed-so-far (ADR-0023): what a Workspace attaching mid-run gets replayed as its
@@ -954,8 +1052,19 @@ export class RunHost {
954
1052
  private readonly resolveDef?: RunHostOptions["resolveDef"];
955
1053
  private readonly sandbox?: SandboxPort;
956
1054
  private readonly instanceHarness?: string;
1055
+ private readonly orchestratorName?: string;
957
1056
  private readonly harnessBearer?: (placement: string) => string;
958
1057
  private readonly effector?: (placement: EffectorPlacement) => EffectorTarget | undefined;
1058
+ private readonly unrunnableChart?: (name: string) => string | undefined;
1059
+ /**
1060
+ * Run id → what waits for that run to settle (ADR-0071): a `dispatch` with `wait`. Keyed by the
1061
+ * run, not kept on its {@link LiveRun}, because a restored caller may attach before its callee is
1062
+ * hosted again (`store.live()` row order) or while the callee is not hosted at all.
1063
+ */
1064
+ private readonly settleListeners = new Map<string, Set<(status: RunStatus) => void>>();
1065
+ /** Idempotency key → the start in flight under it (ADR-0071): a second start with the same key
1066
+ * before the first has spawned its run answers the first's run, never a second. */
1067
+ private readonly dispatching = new Map<string, Promise<DispatchedRun>>();
959
1068
  private readonly echoFactory?: (
960
1069
  endpoint: string,
961
1070
  bearer: string | undefined,
@@ -1025,8 +1134,10 @@ export class RunHost {
1025
1134
  this.resolveDef = opts.resolveDef;
1026
1135
  this.sandbox = opts.sandbox;
1027
1136
  this.instanceHarness = opts.instanceHarness;
1137
+ this.orchestratorName = opts.orchestratorName;
1028
1138
  this.harnessBearer = opts.harnessBearer;
1029
1139
  this.effector = opts.effector;
1140
+ this.unrunnableChart = opts.unrunnableChart;
1030
1141
  this.echoFactory = opts.echo;
1031
1142
  this.liveSetPush =
1032
1143
  opts.instanceHarness && opts.liveSet
@@ -1200,10 +1311,107 @@ export class RunHost {
1200
1311
  return schema ? (z.toJSONSchema(schema, { io: "input" }) as Record<string, unknown>) : null;
1201
1312
  }
1202
1313
 
1203
- /** Start a fresh run of a registered workflow; returns its durable ids. */
1204
- async start(workflow: string, input: Record<string, unknown> = {}): Promise<{ runId: string; instanceId: string }> {
1314
+ /**
1315
+ * Start a fresh run of a registered workflow; returns its durable ids.
1316
+ *
1317
+ * With `dispatchedBy` (ADR-0071), the start is a `dispatch` Effect's, and its `effect` is the
1318
+ * idempotency key: a run that already carries the key is answered, `existing`, and none is
1319
+ * started. That is what lets a caller re-send a start whose answer it lost. The key is looked for
1320
+ * among the runs this process hosts first — a run's first save is a microtask after its spawn —
1321
+ * then in the store. A key on a row the weekly sweep removed (ADR-0065) starts a new run: known,
1322
+ * and not guarded. The depth is recorded as sent: the caller states the callee's.
1323
+ */
1324
+ async start(
1325
+ workflow: string,
1326
+ input: Record<string, unknown> = {},
1327
+ opts: { dispatchedBy?: DispatchedBy } = {},
1328
+ ): Promise<{ runId: string; instanceId: string; existing?: true }> {
1329
+ const by = opts.dispatchedBy;
1330
+ if (!by) return this.startFresh(workflow, input);
1331
+ const inFlight = this.dispatching.get(by.effect);
1332
+ if (inFlight) return { ...keyAnswer(await inFlight, workflow, by.effect), existing: true };
1333
+ let existing = false;
1334
+ const started = (async (): Promise<DispatchedRun> => {
1335
+ const found = await this.dispatched(by.effect);
1336
+ if (found) {
1337
+ existing = true;
1338
+ return found;
1339
+ }
1340
+ return { ...(await this.startFresh(workflow, input, by)), workflow };
1341
+ })();
1342
+ this.dispatching.set(by.effect, started);
1343
+ try {
1344
+ const ids = keyAnswer(await started, workflow, by.effect);
1345
+ return existing ? { ...ids, existing: true } : ids;
1346
+ } finally {
1347
+ this.dispatching.delete(by.effect);
1348
+ }
1349
+ }
1350
+
1351
+ /**
1352
+ * The run that carries the idempotency key `effect` (ADR-0071), or undefined: a hosted run first,
1353
+ * then a stored one, in any status.
1354
+ */
1355
+ async dispatched(effect: string): Promise<DispatchedRun | undefined> {
1356
+ for (const run of this.runs.values()) {
1357
+ if (run.record.dispatchedBy?.effect === effect) {
1358
+ return { runId: run.record.runId, instanceId: run.record.instanceId, workflow: run.record.workflow };
1359
+ }
1360
+ }
1361
+ const runId = await this.store.findByDispatch(effect);
1362
+ if (runId === undefined) return undefined;
1363
+ const blob = (await this.store.load(runId))?.snapshot as RunBlob | null | undefined;
1364
+ return blob ? { runId, instanceId: blob.instanceId, workflow: blob.workflow } : undefined;
1365
+ }
1366
+
1367
+ /**
1368
+ * The in-process start of a `dispatch` Effect (ADR-0071, the binding's `dispatch.start`): every
1369
+ * refusal the door would answer, with its status, as output — the body may have come from a
1370
+ * model, and the report must reach it through the Chain. Any other throw is a defect, and is the
1371
+ * invoke's error.
1372
+ */
1373
+ private async dispatchStart(
1374
+ workflow: string,
1375
+ input: Record<string, unknown>,
1376
+ by: DispatchedBy,
1377
+ ): Promise<DispatchStart> {
1378
+ // The key first: a re-sent start answers its run whatever the Assembly holds now.
1379
+ const found = await this.dispatched(by.effect);
1380
+ if (found && found.workflow !== workflow) {
1381
+ return { ok: false, status: 409, report: [keyConflict(found, workflow, by.effect)] };
1382
+ }
1383
+ if (found) return { ok: true, runId: found.runId, existing: true };
1384
+ const unrunnable = this.unrunnableChart?.(workflow);
1385
+ if (unrunnable !== undefined) {
1386
+ return { ok: false, status: 409, report: [`named Chart "${workflow}" cannot run: ${unrunnable}`] };
1387
+ }
1388
+ try {
1389
+ const started = await this.start(workflow, input, { dispatchedBy: by });
1390
+ return { ok: true, runId: started.runId, existing: started.existing === true };
1391
+ } catch (err) {
1392
+ if (err instanceof ChartRefusal) return { ok: false, status: 422, report: err.report };
1393
+ if (err instanceof EventValidationError) {
1394
+ return {
1395
+ ok: false,
1396
+ status: 400,
1397
+ report: [err.message, `the door of "${workflow}" takes ${JSON.stringify(this.inputSchema(workflow))}`],
1398
+ };
1399
+ }
1400
+ if (err instanceof UnknownWorkflowError) return { ok: false, status: 404, report: [err.message] };
1401
+ if (err instanceof DispatchKeyConflictError) return { ok: false, status: 409, report: [err.message] };
1402
+ throw err;
1403
+ }
1404
+ }
1405
+
1406
+ /** A fresh run: the door, the Chart admission, the spawn. Synchronous after its call, so the run
1407
+ * is hosted — and its key findable in {@link dispatched} — before anything else runs. */
1408
+ private async startFresh(
1409
+ workflow: string,
1410
+ input: Record<string, unknown>,
1411
+ dispatchedBy?: DispatchedBy,
1412
+ ): Promise<{ runId: string; instanceId: string }> {
1205
1413
  const def = this.workflowDefs.get(workflow);
1206
- if (!def) throw new Error(`no workflow registered as "${workflow}"`);
1414
+ if (!def) throw new UnknownWorkflowError(`no workflow registered as "${workflow}"`);
1207
1415
 
1208
1416
  // A named Chart supplies every Chart field of its wrapper's door, and the caller the rest
1209
1417
  // (ADR-0070): what the caller sent under a Chart field is not the run's.
@@ -1228,7 +1436,7 @@ export class RunHost {
1228
1436
 
1229
1437
  const runId = this.newId();
1230
1438
  const instanceId = this.newId();
1231
- const record: RunRecord = { runId, workflow, instanceId };
1439
+ const record: RunRecord = { runId, workflow, instanceId, ...(dispatchedBy ? { dispatchedBy } : {}) };
1232
1440
 
1233
1441
  const machine = this.assemble(def, instanceId);
1234
1442
  // Every Chart field of a Chart wrapper's door is admitted BEFORE the run exists (ADR-0070): a
@@ -1319,6 +1527,7 @@ export class RunHost {
1319
1527
  const blob = stored.snapshot as RunBlob | null;
1320
1528
  if (!blob) {
1321
1529
  await this.store.markLost(stored.runId, "empty snapshot");
1530
+ this.settledUnhosted(stored.runId, "lost", "empty snapshot");
1322
1531
  lost.push(stored.runId);
1323
1532
  continue;
1324
1533
  }
@@ -1333,11 +1542,13 @@ export class RunHost {
1333
1542
  const found = await this.defFor(blob);
1334
1543
  if ("lost" in found) {
1335
1544
  await this.store.markLost(stored.runId, found.lost);
1545
+ this.settledUnhosted(stored.runId, "lost", found.lost, blob);
1336
1546
  lost.push(stored.runId);
1337
1547
  continue;
1338
1548
  }
1339
1549
  if ("drifted" in found) {
1340
1550
  await this.store.markDrifted(stored.runId, found.drifted);
1551
+ this.settledUnhosted(stored.runId, "drifted", found.drifted, blob);
1341
1552
  drifted.push(stored.runId);
1342
1553
  continue;
1343
1554
  }
@@ -1357,11 +1568,11 @@ export class RunHost {
1357
1568
  // a run that cannot be read.
1358
1569
  const expected = fingerprintOf(def.machine);
1359
1570
  if (blob.machine !== expected) {
1360
- await this.store.markDrifted(
1361
- stored.runId,
1571
+ const reason =
1362
1572
  `workflow "${blob.workflow}"${def.assembly !== undefined ? ` in Assembly ${def.assembly}` : ""} changed shape since this run was saved ` +
1363
- `(saved under ${blob.machine ?? "an unstamped Machine"}, now ${expected})`,
1364
- );
1573
+ `(saved under ${blob.machine ?? "an unstamped Machine"}, now ${expected})`;
1574
+ await this.store.markDrifted(stored.runId, reason);
1575
+ this.settledUnhosted(stored.runId, "drifted", reason, blob);
1365
1576
  drifted.push(stored.runId);
1366
1577
  continue;
1367
1578
  }
@@ -1372,15 +1583,24 @@ export class RunHost {
1372
1583
  const charts = chartDrift(def.machine, boundChartRoutesOf(blob.snapshot));
1373
1584
  if (charts !== undefined) {
1374
1585
  await this.store.markDrifted(stored.runId, charts);
1586
+ this.settledUnhosted(stored.runId, "drifted", charts, blob);
1375
1587
  drifted.push(stored.runId);
1376
1588
  continue;
1377
1589
  }
1378
1590
 
1379
- const record: RunRecord = { runId: stored.runId, workflow: blob.workflow, instanceId: blob.instanceId };
1591
+ // The run's place in a chain of dispatches comes back with it (ADR-0071): its key keeps
1592
+ // answering a re-sent start, and its depth is the one it started at.
1593
+ const record: RunRecord = {
1594
+ runId: stored.runId,
1595
+ workflow: blob.workflow,
1596
+ instanceId: blob.instanceId,
1597
+ ...(blob.dispatchedBy ? { dispatchedBy: blob.dispatchedBy } : {}),
1598
+ };
1380
1599
 
1381
1600
  try {
1382
1601
  if (!(await this.reconcile(record))) {
1383
1602
  await this.store.markLost(stored.runId, "reconcile: live world absent");
1603
+ this.settledUnhosted(stored.runId, "lost", "reconcile: live world absent", blob);
1384
1604
  lost.push(stored.runId);
1385
1605
  continue;
1386
1606
  }
@@ -1746,6 +1966,46 @@ export class RunHost {
1746
1966
  };
1747
1967
  }
1748
1968
 
1969
+ /**
1970
+ * The registrations whose Agents' picks `event` comes from (ADR-0024, `cause.ts`): the one whose
1971
+ * pick it is, in its own send; and each one whose pick, in its own send or out, it answers — an
1972
+ * event that the Machine invoking that Agent takes from below the Agent's state, the sources of a
1973
+ * reply or a child's error (a nested Agent's pick, a Gate's answer, a child's output or error).
1974
+ */
1975
+ private pickCause(
1976
+ runId: string,
1977
+ holds: Map<string, Hold>,
1978
+ target: AnyActorRef,
1979
+ source: AnyActorRef | undefined,
1980
+ event: AnyEventObject,
1981
+ ): Set<Registration> | undefined {
1982
+ const out = new Set<Registration>();
1983
+ const sending = this.sending;
1984
+ if (sending && sending.reg.runId === runId && sending.reg.invoker === target && event.type === sending.pick) {
1985
+ out.add(sending.reg);
1986
+ }
1987
+ const picking: Registration[] = [];
1988
+ if (sending && sending.reg.runId === runId) picking.push(sending.reg);
1989
+ for (const hold of holds.values()) {
1990
+ if (hold.answer !== undefined) continue;
1991
+ const live = this.table.lookup(agentAddress(hold.instanceId));
1992
+ const reg = this.holdRegs.get(hold) ?? live;
1993
+ if (reg !== undefined && reg === live) picking.push(reg);
1994
+ }
1995
+ for (const reg of picking) {
1996
+ if (reg.invoker !== target || !reg.invokeId) continue;
1997
+ const seat = seatOf(target.getSnapshot() as AnyMachineSnapshot, reg.invokeId);
1998
+ if (!seat) continue;
1999
+ if (
2000
+ replyOf(event, source, target, seat) !== undefined ||
2001
+ childErrorOf(event, source, target, seat) !== undefined
2002
+ ) {
2003
+ out.add(reg);
2004
+ }
2005
+ }
2006
+ return out.size > 0 ? out : undefined;
2007
+ }
2008
+
1749
2009
  /**
1750
2010
  * Answer every outstanding pick of `run` that can be answered now (ADR-0024). Called by the persist that
1751
2011
  * follows every transition — a microtask after the snapshot, so the mailbox has drained — and
@@ -2060,6 +2320,105 @@ export class RunHost {
2060
2320
  return () => run.listeners.delete(listener);
2061
2321
  }
2062
2322
 
2323
+ /**
2324
+ * Hear once that a run is no longer live (ADR-0071): `listener` gets its last status — `done`,
2325
+ * `failed`, `error` or `cancelled` with its `output`, or the store's `lost` or `drifted` — and is
2326
+ * then dropped. Returns the detach. What an in-process `dispatch` with `wait` waits on.
2327
+ *
2328
+ * Keyed by the run id, not by the run's hosting, so the order of a restore does not matter: a
2329
+ * caller restored before its callee attaches to a run this process does not host yet, and hears
2330
+ * it when the restored callee settles, or when restore marks it lost or drifted. A run that has
2331
+ * settled already is answered at once, from {@link read}. `subscribe` is the feed, and unchanged:
2332
+ * it attaches only to a hosted run.
2333
+ */
2334
+ onSettled(runId: string, listener: (status: RunStatus) => void): () => void {
2335
+ let listeners = this.settleListeners.get(runId);
2336
+ if (!listeners) this.settleListeners.set(runId, (listeners = new Set()));
2337
+ // Its own entry, so one listener attached twice is heard twice and detached once each.
2338
+ const entry = (status: RunStatus): void => listener(status);
2339
+ listeners.add(entry);
2340
+ const attached = (): boolean => this.settleListeners.get(runId)?.has(entry) === true;
2341
+ const detach = (): void => {
2342
+ const now = this.settleListeners.get(runId);
2343
+ now?.delete(entry);
2344
+ if (now?.size === 0) this.settleListeners.delete(runId);
2345
+ };
2346
+ if (!this.runs.has(runId)) {
2347
+ // Not hosted: settled already, or a live row a restore has not reached. Only the first is
2348
+ // answered here; a settle heard in the meantime has detached the entry.
2349
+ this.read(runId).then(
2350
+ (status) => {
2351
+ if (!status || !attached() || !settledStatus(status.status)) return;
2352
+ detach();
2353
+ hear(entry, status);
2354
+ },
2355
+ () => {},
2356
+ );
2357
+ }
2358
+ return detach;
2359
+ }
2360
+
2361
+ /** A hosted run's depth in a chain of dispatches (ADR-0071): 0 for a run nothing dispatched, and
2362
+ * undefined for a run this process does not host — not hosted is not a root run. */
2363
+ depthOf(runId: string): number | undefined {
2364
+ const run = this.runs.get(runId);
2365
+ return run && (run.record.dispatchedBy?.depth ?? 0);
2366
+ }
2367
+
2368
+ /** Tell everything waiting on `runId` that it settled, once (ADR-0071). */
2369
+ private fireSettled(runId: string, status: RunStatus): void {
2370
+ const listeners = this.settleListeners.get(runId);
2371
+ if (!listeners) return;
2372
+ this.settleListeners.delete(runId);
2373
+ for (const listener of listeners) hear(listener, status);
2374
+ }
2375
+
2376
+ /**
2377
+ * A run restore did not host settled: marked `lost` (its status is the row's identity and reason
2378
+ * alone: a lost run is not read back, though a row that was saved keeps its blob and its key) or
2379
+ * `drifted` (its snapshot is read back).
2380
+ * Only when something waits on it (ADR-0071).
2381
+ */
2382
+ private settledUnhosted(runId: string, status: "lost" | "drifted", reason: string, blob?: RunBlob): void {
2383
+ if (!this.settleListeners.has(runId)) return;
2384
+ const bare: RunStatus = {
2385
+ runId,
2386
+ workflow: blob?.workflow ?? "",
2387
+ instanceId: blob?.instanceId ?? "",
2388
+ ...(blob?.dispatchedBy ? { dispatchedBy: blob.dispatchedBy } : {}),
2389
+ status,
2390
+ value: undefined,
2391
+ context: undefined,
2392
+ children: [],
2393
+ waiting: [],
2394
+ held: [],
2395
+ reason,
2396
+ };
2397
+ if (status === "lost") return this.fireSettled(runId, bare);
2398
+ this.read(runId).then(
2399
+ (read) => this.fireSettled(runId, read ?? bare),
2400
+ () => this.fireSettled(runId, bare),
2401
+ );
2402
+ }
2403
+
2404
+ /** The binding's `dispatch.settled` (ADR-0071): {@link onSettled} as a promise that an abort —
2405
+ * the Effect's state exiting, its wait timing out — rejects and detaches. */
2406
+ private settledOf(runId: string, signal: AbortSignal): Promise<RunStatus> {
2407
+ return new Promise((resolve, reject) => {
2408
+ if (signal.aborted) return reject(signal.reason);
2409
+ let detach = (): void => {};
2410
+ const onAbort = (): void => {
2411
+ detach();
2412
+ reject(signal.reason);
2413
+ };
2414
+ signal.addEventListener("abort", onAbort, { once: true });
2415
+ detach = this.onSettled(runId, (status) => {
2416
+ signal.removeEventListener("abort", onAbort);
2417
+ resolve(status);
2418
+ });
2419
+ });
2420
+ }
2421
+
2063
2422
  /**
2064
2423
  * Observe a whole WORKFLOW (the `GET /workflows/:name/events` SSE — ADR-0022): every run of it
2065
2424
  * appearing, moving, emitting and leaving, for as long as the caller stays attached.
@@ -2111,8 +2470,9 @@ export class RunHost {
2111
2470
  * Read a run's status, **reading through to the store** when it is no longer live (ADR-0009). A
2112
2471
  * completed run's final snapshot is persisted before `persist()` drops it from the registry, so a
2113
2472
  * terminal run reports its `done`/`error` status + final context here rather than 404-ing. Returns
2114
- * undefined only for a genuinely unknown run (or one marked `lost`, whose snapshot was cleared) —
2115
- * and a swept one is unknown: a finished run's row lives a week (ADR-0065).
2473
+ * undefined only for a genuinely unknown run (or one marked `lost` with no row saved before, so no
2474
+ * snapshot: `markLost` keeps a saved row's blob, whose key must keep answering, ADR-0071) — and a
2475
+ * swept one is unknown: a finished run's row lives a week (ADR-0065).
2116
2476
  */
2117
2477
  async read(runId: string): Promise<RunStatus | undefined> {
2118
2478
  const live = this.status(runId);
@@ -2120,7 +2480,7 @@ export class RunHost {
2120
2480
  const stored = await this.store.load(runId);
2121
2481
  const blob = stored?.snapshot as RunBlob | null | undefined;
2122
2482
  if (!stored || !blob) return undefined;
2123
- const snap = (blob.snapshot ?? {}) as { status?: string; value?: unknown; context?: unknown };
2483
+ const snap = (blob.snapshot ?? {}) as { status?: string; value?: unknown; context?: unknown; output?: unknown };
2124
2484
  // The STORE row is the authority on the run's lifecycle, the snapshot on the Machine's: a
2125
2485
  // cancelled run's actor reports xstate's "stopped", which is mechanism, not an outcome
2126
2486
  // (ADR-0025). While the row still says "live", the Machine's own status is the answer.
@@ -2139,6 +2499,14 @@ export class RunHost {
2139
2499
  reason: stored.reason,
2140
2500
  ...(blob.assembly !== undefined ? { assembly: blob.assembly } : {}),
2141
2501
  ...boundFields(snap),
2502
+ ...(blob.dispatchedBy ? { dispatchedBy: blob.dispatchedBy } : {}),
2503
+ // The Machine's output once the row says the run is over (ADR-0071), never while it is live;
2504
+ // for a failed run, its Failure (ADR-0073).
2505
+ ...(stored.status !== "live" && blob.failure !== undefined ? { output: blob.failure } : {}),
2506
+ ...(stored.status !== "live" && blob.failure === undefined && snap.output !== undefined
2507
+ ? { output: snap.output }
2508
+ : {}),
2509
+ ...dispatchedField(blob.effects ?? {}),
2142
2510
  };
2143
2511
  }
2144
2512
 
@@ -2275,15 +2643,40 @@ export class RunHost {
2275
2643
  // The run is over, so nothing can use its Workspaces, and each holds its whole Size (ADR-0025).
2276
2644
  // An actor stop skips the wrapper's `teardown` state, so the host runs the same idempotent
2277
2645
  // delete here, after the stop, so no lease renews what was deleted. `keep` leaves them to the
2278
- // operator's idle timeout, for inspection. A delete that fails is the idle GC's, as in
2279
- // `teardown` (ADR-0012 backstop): the cancel has already happened.
2646
+ // operator's idle timeout, for inspection.
2647
+ await this.destroySandboxes(runId, sandboxes, "the cancel");
2648
+ }
2649
+
2650
+ /**
2651
+ * End a run `error` because a Machine of its tree dropped a mechanism event (ADR-0074): `fault`
2652
+ * says which. The actor stops as a cancel stops it, so each live Agent's Turn ends, and the run
2653
+ * is persisted terminal. Its Sandboxes stay as evidence, as a fault's do (ADR-0012): the Machine
2654
+ * did not choose this end. Once per run: a second dropped event in the same step adds nothing.
2655
+ */
2656
+ private faultUnrouted(runId: string, binding: RunBinding, fault: string): void {
2657
+ const run = this.runs.get(runId);
2658
+ if (!run || run.binding !== binding || run.fault !== undefined) return;
2659
+ run.fault = fault;
2660
+ run.actor.stop();
2661
+ this.persist(run, "error");
2662
+ }
2663
+
2664
+ /**
2665
+ * Delete the Sandboxes of a run that is over and stopped: a cancelled one (ADR-0025) and a failed
2666
+ * one (ADR-0073). After the stop, so no lease renews what was deleted. The delete is idempotent:
2667
+ * for a failed run, the Workspace a Failure went through deleted its own already, so this is a
2668
+ * backstop for the others. A delete that fails is the idle GC's, as in `teardown` (ADR-0012
2669
+ * backstop): the run has already ended. `why` names that end in the log line of a delete that
2670
+ * fails.
2671
+ */
2672
+ private async destroySandboxes(runId: string, sandboxes: readonly string[], why: string): Promise<void> {
2280
2673
  const port = this.sandbox;
2281
2674
  if (!port) return;
2282
2675
  await Promise.all(
2283
2676
  sandboxes.map((name) =>
2284
2677
  port.destroy(name).catch((err: unknown) => {
2285
2678
  console.error(
2286
- `run ${runId}: cancel could not delete Sandbox ${name} (the idle timeout will): ` +
2679
+ `run ${runId}: Sandbox ${name} was not deleted at ${why} (the idle timeout will delete it): ` +
2287
2680
  (err instanceof Error ? err.message : String(err)),
2288
2681
  );
2289
2682
  }),
@@ -2362,6 +2755,13 @@ export class RunHost {
2362
2755
  fault: run.fault,
2363
2756
  ...(run.def.assembly !== undefined ? { assembly: run.def.assembly } : {}),
2364
2757
  ...boundFields(snap),
2758
+ // A Machine that reached its final state has its output (ADR-0071); an active one has none.
2759
+ // A failed run's output is its Failure (ADR-0073).
2760
+ ...(run.failure !== undefined ? { output: run.failure } : {}),
2761
+ ...(run.failure === undefined && snap.status !== "active" && snap.output !== undefined
2762
+ ? { output: snap.output }
2763
+ : {}),
2764
+ ...dispatchedField(run.effects),
2365
2765
  };
2366
2766
  }
2367
2767
 
@@ -2393,6 +2793,9 @@ export class RunHost {
2393
2793
  ): AnyActor {
2394
2794
  const notices = new NoticeLedger(noticeState, this.newId);
2395
2795
  const holds = new Map(holdState.map((hold) => [hold.instanceId, hold]));
2796
+ // What took the Machine out of each Agent's state: that Agent's own pick, or something else
2797
+ // (ADR-0024). Read off the inspection stream below.
2798
+ const causes = new PickCauses();
2396
2799
  let live: LiveRun | undefined;
2397
2800
  let scheduled = false;
2398
2801
  const schedule = () => {
@@ -2406,9 +2809,20 @@ export class RunHost {
2406
2809
  const binding: RunBinding = {
2407
2810
  runId: record.runId,
2408
2811
  workflow: record.workflow,
2812
+ ...(this.orchestratorName !== undefined ? { orchestratorName: this.orchestratorName } : {}),
2813
+ depth: record.dispatchedBy?.depth ?? 0,
2814
+ // A `dispatch` with no target (ADR-0071): the run host itself, with no wire and no token.
2815
+ dispatch: {
2816
+ start: (workflow, input, by) => this.dispatchStart(workflow, input, by),
2817
+ settled: (runId, signal) => this.settledOf(runId, signal),
2818
+ },
2409
2819
  ...(def.assembly !== undefined ? { assembly: def.assembly } : {}),
2410
2820
  table: this.table,
2411
2821
  sandbox: this.sandbox,
2822
+ heldSandboxes: () => {
2823
+ const run = this.runs.get(record.runId);
2824
+ return new Set(run ? runSandboxes(run.actor, record.runId, { placing: true }) : []);
2825
+ },
2412
2826
  instanceHarness: this.instanceHarness,
2413
2827
  ...(this.harnessBearer ? { harnessBearer: this.harnessBearer } : {}),
2414
2828
  ...(this.effector ? { effector: this.effector } : {}),
@@ -2440,8 +2854,13 @@ export class RunHost {
2440
2854
  },
2441
2855
  // The notices' other halves (ADR-0062). A raise is saved at the write, like the ledgers; a
2442
2856
  // take is a reservation in memory, delivered by `recordAdmission` or released on failure.
2443
- raiseNotice: (notice, to, source) => {
2444
- if (!notices.raise(notice, to, source)) return;
2857
+ raiseNotice: (notice, to, source, older) => {
2858
+ if (!notices.raise(notice, to, source, older)) return;
2859
+ const run = this.runs.get(record.runId);
2860
+ if (run && run.notices === notices) this.persist(run);
2861
+ },
2862
+ restoreNotice: (notice, to, source) => {
2863
+ if (!notices.restore(notice, to, source)) return;
2445
2864
  const run = this.runs.get(record.runId);
2446
2865
  if (run && run.notices === notices) this.persist(run);
2447
2866
  },
@@ -2521,6 +2940,11 @@ export class RunHost {
2521
2940
  const hold = holds.get(instanceId);
2522
2941
  if (hold) hold.fetched = true;
2523
2942
  },
2943
+ // Read as the invocation stops, while its registration is still there (ADR-0024).
2944
+ turnEndedByPick: (instanceId) => {
2945
+ const reg = this.table.lookup(agentAddress(instanceId));
2946
+ return reg !== undefined && reg.runId === record.runId && causes.endedByPick(reg);
2947
+ },
2524
2948
  // A Chart by name (ADR-0070, D-C): among the Charts of the wrapper that is the run's Workflow.
2525
2949
  chartNamed: (name) => this.namedChart(wrapperName(record.workflow), name)?.text,
2526
2950
  };
@@ -2532,7 +2956,8 @@ export class RunHost {
2532
2956
  const childRefsOf = (snapshot: unknown): Set<AnyActorRef> =>
2533
2957
  new Set(Object.values((snapshot as { children?: Record<string, AnyActorRef> } | undefined)?.children ?? {}));
2534
2958
  const actor = createActor(machine, {
2535
- clock: this.clock,
2959
+ // Each timer keeps the cause of the relay that armed it (`cause.ts`, ADR-0024).
2960
+ clock: causes.clockOver(this.clock),
2536
2961
  ...options,
2537
2962
  inspect: (ev) => {
2538
2963
  // Bind the run's actor SYSTEM on the ROOT's creation event — the first inspection event,
@@ -2545,7 +2970,10 @@ export class RunHost {
2545
2970
  bindRun((ev.actorRef as AnyActorRef).system, binding);
2546
2971
  // …and watch its scheduler before anything is armed: whether an event the Machine took
2547
2972
  // was a timer it set, or an event an actor sent at once, decides `cutShort` below.
2548
- firedTimer = watchTimers((ev.actorRef as AnyActorRef).system);
2973
+ // The same watch keeps the cause of each timer: the relay that armed it (ADR-0024).
2974
+ firedTimer = watchTimers((ev.actorRef as AnyActorRef).system, {
2975
+ arming: (source) => causes.armingFrom(source),
2976
+ });
2549
2977
  }
2550
2978
  // A restored hold is the Turn of the FIRST registration of its address in this tree: bind
2551
2979
  // it as soon as one exists, before an event can end that Turn and start another at the same
@@ -2661,8 +3089,63 @@ export class RunHost {
2661
3089
  const reply = sending && replyTo(sending.reg);
2662
3090
  if (sending && reply) sending.chain.push(reply);
2663
3091
  if (sending) errorTo(sending.reg, sending.pick);
3092
+ // The cause of this relay (`cause.ts`, ADR-0024): read before the Machine takes it.
3093
+ const target = ev.actorRef as AnyActorRef;
3094
+ const source = ev.sourceRef as AnyActorRef | undefined;
3095
+ const event = ev.event as AnyEventObject;
3096
+ causes.relayed(
3097
+ source,
3098
+ target,
3099
+ event,
3100
+ () => this.pickCause(record.runId, holds, target, source, event),
3101
+ () => this.table.byRun(record.runId).filter((reg) => reg.kind === "agent"),
3102
+ );
3103
+ // A Failure ended a Machine that a Machine above can catch (ADR-0073). xstate does not stop
3104
+ // the children of a Machine that ends `error`, so the host stops them, as `stopTree` does for
3105
+ // the run: an Agent in that Machine settles its Turn, and a Gate or a timer there does not
3106
+ // wait for nobody. After the parent takes the event, so its exit is recorded first. A stop
3107
+ // skips `teardown`, so a Workspace in another region of that Machine does not delete its
3108
+ // Sandbox: the host names them before the stop and deletes them after it, as for a failed run.
3109
+ // A name that a live Workspace of the run holds after the stop is not deleted: a parent that
3110
+ // invokes the same Machine again gives its new Workspace the same invoke id, and so the same
3111
+ // name, and that Workspace is in `placing` by now.
3112
+ if (
3113
+ source !== undefined &&
3114
+ source._parent === target &&
3115
+ event.type === `xstate.error.actor.${source.id}` &&
3116
+ isFailure((event as { error?: unknown }).error)
3117
+ ) {
3118
+ queueMicrotask(() => {
3119
+ const sandboxes = runSandboxes(source, record.runId);
3120
+ stopTree(source);
3121
+ const held = this.runs.get(record.runId)?.binding.heldSandboxes?.() ?? new Set<string>();
3122
+ const ended = sandboxes.filter((name) => !held.has(name));
3123
+ void this.destroySandboxes(record.runId, ended, "the end of the failed Machine");
3124
+ });
3125
+ }
3126
+ }
3127
+ // A mechanism event that no transition took (ADR-0074): `agent.fault` or `workspace.lost`
3128
+ // that a Machine got and dropped, as a guard that was false when it came. The run would wait
3129
+ // for ever, so it faults. After the macrostep, so the stop does not cut xstate's own step.
3130
+ if (ev.type === "@xstate.microstep") {
3131
+ const unrouted = unroutedMechanismEvent(ev as unknown as MicrostepView);
3132
+ if (unrouted !== undefined) queueMicrotask(() => this.faultUnrouted(record.runId, binding, unrouted));
3133
+ }
3134
+ // The exit of each Agent's state (`cause.ts`, ADR-0024).
3135
+ if (ev.type === "@xstate.microstep") {
3136
+ const machine = ev.actorRef as AnyActorRef;
3137
+ const agents = this.table.byRun(record.runId).filter((reg) => reg.kind === "agent");
3138
+ causes.microstep(
3139
+ machine,
3140
+ (ev as unknown as { snapshot: AnyMachineSnapshot }).snapshot,
3141
+ ev.event as AnyEventObject,
3142
+ agents,
3143
+ );
3144
+ }
3145
+ if (ev.type === "@xstate.snapshot") {
3146
+ causes.taken(ev.actorRef as AnyActorRef, ev.event as AnyEventObject);
3147
+ schedule();
2664
3148
  }
2665
- if (ev.type === "@xstate.snapshot") schedule();
2666
3149
  },
2667
3150
  });
2668
3151
  live = this.track(record, actor, def, agents, epochs, notices, binding, holds, effects, startedBy);
@@ -2702,6 +3185,17 @@ export class RunHost {
2702
3185
  // `{}`), then persist: the snapshot's "error" status stores the run and untracks it.
2703
3186
  actor.subscribe({
2704
3187
  error: (err) => {
3188
+ // A Failure that no state caught (ADR-0073): the end the Machine chose, so the run ends
3189
+ // `failed`, with no fault and the Failure as its output, and its Sandboxes go as a cancel's
3190
+ // do (ADR-0025). Named BEFORE the stop, while the tree still says which Workspaces are live.
3191
+ if (isFailure(err)) {
3192
+ run.failure = failurePayload(err);
3193
+ const sandboxes = runSandboxes(actor, record.runId);
3194
+ this.persist(run, "failed");
3195
+ stopTree(actor);
3196
+ void this.destroySandboxes(record.runId, sandboxes, "the end of the failed run");
3197
+ return;
3198
+ }
2705
3199
  run.fault = err instanceof Error ? err.message : String(err);
2706
3200
  this.persist(run);
2707
3201
  // A faulted run stops its actors — each workspace's lease among them (ADR-0021), and a
@@ -2734,9 +3228,12 @@ export class RunHost {
2734
3228
  /**
2735
3229
  * Persist the run's snapshot after a transition; drop it from the registry once final.
2736
3230
  *
2737
- * `terminal` is the run-lifecycle verdict the MACHINE cannot supply: `cancel()` passes
2738
- * "cancelled", because xstate only knows its actor was stopped (ADR-0025). It is the stored
2739
- * status and the one the feeds report.
3231
+ * `terminal` is the run-lifecycle verdict the MACHINE cannot supply. Three callers pass it:
3232
+ * `cancel()` passes "cancelled", because xstate only knows its actor was stopped (ADR-0025); the
3233
+ * error observer passes "failed" for a Failure that no state caught, because xstate gives a
3234
+ * Failure and a defect the same `error` status (ADR-0073); and `faultUnrouted()` passes "error"
3235
+ * for a mechanism event that no transition took, because the host stops the actor and xstate
3236
+ * then knows only that it was stopped (ADR-0074). It is the stored status and the one the feeds report.
2740
3237
  */
2741
3238
  private persist(run: LiveRun, terminal?: string): void {
2742
3239
  // The holds first: an answer read at Rest, and a notice a route out raised, ride this same save.
@@ -2750,6 +3247,7 @@ export class RunHost {
2750
3247
  const blob: RunBlob = {
2751
3248
  workflow: run.record.workflow,
2752
3249
  instanceId: run.record.instanceId,
3250
+ ...(run.record.dispatchedBy ? { dispatchedBy: run.record.dispatchedBy } : {}),
2753
3251
  // Stamped on every save, off the registered TEMPLATE (never the per-run `.provide()` result —
2754
3252
  // providers do not change shape, and `fingerprintOf` memoizes per machine object). ADR-0030.
2755
3253
  machine: fingerprintOf(run.def.machine),
@@ -2766,6 +3264,7 @@ export class RunHost {
2766
3264
  effects: run.effects,
2767
3265
  startedBy: liveStartedBy(run.startedBy, run.actor),
2768
3266
  fault: run.fault,
3267
+ ...(run.failure !== undefined ? { failure: run.failure } : {}),
2769
3268
  };
2770
3269
  const status = terminal ?? (machineStatus === "active" ? "live" : machineStatus);
2771
3270
  // A save that fails is this run's fault, never the process's: an unhandled rejection would end
@@ -2794,12 +3293,30 @@ export class RunHost {
2794
3293
  this.announceGone(run, runStatus);
2795
3294
  this.untrack(run.record);
2796
3295
  run.listeners.clear();
3296
+ // After the save and the untrack: a listener that reads the run back reads it settled.
3297
+ this.fireSettled(run.record.runId, runStatus);
2797
3298
  } else {
2798
3299
  this.announce(run.record.workflow, { kind: "status", status: runStatus });
2799
3300
  }
2800
3301
  }
2801
3302
  }
2802
3303
 
3304
+ /** A status that is no longer live (ADR-0071): the store's terminal words. A live row reads its
3305
+ * Machine's `active`; the store's own `live` is never a status a reader is handed, but is not
3306
+ * settled either. */
3307
+ function settledStatus(status: string): boolean {
3308
+ return status !== "active" && status !== "live";
3309
+ }
3310
+
3311
+ /** Call a settle listener; one that throws is its own fault, never the run's or the host's. */
3312
+ function hear(listener: (status: RunStatus) => void, status: RunStatus): void {
3313
+ try {
3314
+ listener(status);
3315
+ } catch (err) {
3316
+ console.error(`run ${status.runId}: a settle listener threw: ${err instanceof Error ? err.message : String(err)}`);
3317
+ }
3318
+ }
3319
+
2803
3320
  /** Does work still run below the Agent's state of `reg` — a live child of an active state under it?
2804
3321
  * A timer that holds Rest is no work: when the state exits, nothing it started is stopped. */
2805
3322
  function stillRunning(reg: Registration): boolean {
@@ -2816,6 +3333,23 @@ function atOrBelow(actor: AnyActorRef, ref: AnyActorRef): boolean {
2816
3333
 
2817
3334
  /** A status's bound fields (ADR-0070): `bound`, and the hashes of the Charts bound, distinct and
2818
3335
  * sorted. Both absent when the snapshot binds no Chart. */
3336
+ /** The runs a run's `dispatch` Effects started (ADR-0071), from its ledger: an intent row the
3337
+ * start's answer re-wrote with its run, a settled row whose output names one (`started`,
3338
+ * `settled`), and an aborted row that kept it. A row that never learned its run names none. */
3339
+ function dispatchedField(effects: Record<string, EffectRow>): { dispatched?: Dispatched[] } {
3340
+ const out: Dispatched[] = [];
3341
+ for (const row of Object.values(effects)) {
3342
+ if (row.kind !== "dispatch") continue;
3343
+ const run =
3344
+ row.state === "settled"
3345
+ ? (row.output as { run?: unknown } | null | undefined)?.run
3346
+ : (row as { run?: unknown }).run;
3347
+ if (typeof run === "string" && run !== "") out.push({ slot: row.slot, run, at: row.at });
3348
+ }
3349
+ out.sort((a, b) => (a.at < b.at ? -1 : a.at > b.at ? 1 : 0));
3350
+ return out.length ? { dispatched: out } : {};
3351
+ }
3352
+
2819
3353
  function boundFields(snap: unknown): { bound?: true; chartHashes?: string[] } {
2820
3354
  const hashes = [...new Set(boundChartRoutesOf(snap).map((route) => route.chartHash))].sort();
2821
3355
  return hashes.length ? { bound: true, chartHashes: hashes } : {};