@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.
- package/README.md +20 -2
- package/console/components/app.ts +2 -0
- package/console/components/fleet.ts +14 -1
- package/console/components/machine-pane.ts +41 -2
- package/console/main.ts +13 -1
- package/console/store.ts +102 -6
- package/console/style.css +21 -0
- package/docs/agents-block.md +5 -0
- package/docs/concepts/agents-and-turns.md +442 -0
- package/docs/concepts/architecture.md +230 -0
- package/docs/concepts/charts.md +143 -0
- package/docs/concepts/composition.md +357 -0
- package/docs/concepts/converge.md +254 -0
- package/docs/concepts/durable-runs.md +189 -0
- package/docs/concepts/effects.md +603 -0
- package/docs/concepts/gates.md +208 -0
- package/docs/concepts/machines-and-workflows.md +289 -0
- package/docs/concepts/pools-and-sources.md +230 -0
- package/docs/concepts/secrets-and-trust.md +222 -0
- package/docs/concepts/the-menu.md +378 -0
- package/docs/concepts/workspaces.md +678 -0
- package/docs/get-started/a-model-decides.md +563 -0
- package/docs/get-started/an-agent-in-a-sandbox.md +381 -0
- package/docs/get-started/connect-a-model.md +248 -0
- package/docs/get-started/create-an-instance.md +194 -0
- package/docs/get-started/have-your-assistant-write-a-machine.md +922 -0
- package/docs/get-started/install.md +126 -0
- package/docs/guides/add-tools-to-the-sandbox.md +295 -0
- package/docs/guides/answer-a-gate.md +304 -0
- package/docs/guides/bind-open-parts.md +223 -0
- package/docs/guides/cancel-a-run.md +146 -0
- package/docs/guides/clean-up.md +206 -0
- package/docs/guides/fix-a-failed-converge.md +361 -0
- package/docs/guides/give-the-cluster-git-credentials.md +222 -0
- package/docs/guides/iterate-on-a-machine-on-kind.md +190 -0
- package/docs/guides/let-an-agent-write-a-chart.md +226 -0
- package/docs/guides/look-inside-a-sandbox.md +174 -0
- package/docs/guides/mirror-the-kit-images.md +176 -0
- package/docs/guides/move-from-kind-to-a-shared-cluster.md +302 -0
- package/docs/guides/open-the-console.md +91 -0
- package/docs/guides/publish-a-machine-as-an-npm-package.md +350 -0
- package/docs/guides/push-and-sign-a-branch.md +486 -0
- package/docs/guides/run-a-chart-by-name.md +270 -0
- package/docs/guides/run-a-command-or-call-a-url.md +544 -0
- package/docs/guides/run-one-worker-per-item.md +321 -0
- package/docs/guides/set-an-agents-access-and-dials.md +375 -0
- package/docs/guides/size-a-workspace.md +204 -0
- package/docs/guides/start-a-run-from-a-run.md +237 -0
- package/docs/guides/start-a-run.md +207 -0
- package/docs/guides/upgrade-the-kit-version.md +240 -0
- package/docs/guides/use-a-hosted-provider.md +208 -0
- package/docs/guides/use-a-local-model.md +313 -0
- package/docs/guides/watch-a-run.md +326 -0
- package/docs/guides/write-a-workflow.md +318 -0
- package/docs/index.md +44 -0
- package/docs/llms.txt +216 -0
- package/docs/reference/authoring-surface/agent.md +128 -0
- package/docs/reference/authoring-surface/answer.md +140 -0
- package/docs/reference/authoring-surface/arg.md +60 -0
- package/docs/reference/authoring-surface/basic.md +62 -0
- package/docs/reference/authoring-surface/bearer.md +53 -0
- package/docs/reference/authoring-surface/chartschema.md +23 -0
- package/docs/reference/authoring-surface/chartschemafor.md +48 -0
- package/docs/reference/authoring-surface/command.md +191 -0
- package/docs/reference/authoring-surface/customize.md +64 -0
- package/docs/reference/authoring-surface/defineconfig.md +41 -0
- package/docs/reference/authoring-surface/defineevent.md +114 -0
- package/docs/reference/authoring-surface/dispatch.md +126 -0
- package/docs/reference/authoring-surface/fail.md +83 -0
- package/docs/reference/authoring-surface/file.md +122 -0
- package/docs/reference/authoring-surface/frompick.md +277 -0
- package/docs/reference/authoring-surface/http.md +159 -0
- package/docs/reference/authoring-surface/isfailure.md +58 -0
- package/docs/reference/authoring-surface/jr2setup.md +237 -0
- package/docs/reference/authoring-surface/open.md +74 -0
- package/docs/reference/authoring-surface/openbody.md +86 -0
- package/docs/reference/authoring-surface/paramsof.md +125 -0
- package/docs/reference/authoring-surface/pool.md +186 -0
- package/docs/reference/authoring-surface/push.md +112 -0
- package/docs/reference/authoring-surface/secret.md +118 -0
- package/docs/reference/authoring-surface/sentby.md +83 -0
- package/docs/reference/authoring-surface/source.md +51 -0
- package/docs/reference/authoring-surface/text.md +62 -0
- package/docs/reference/authoring-surface/type-agentdeclaration.md +27 -0
- package/docs/reference/authoring-surface/type-agentdefinition.md +80 -0
- package/docs/reference/authoring-surface/type-agentlogic.md +15 -0
- package/docs/reference/authoring-surface/type-agentslots.md +12 -0
- package/docs/reference/authoring-surface/type-answeraction.md +12 -0
- package/docs/reference/authoring-surface/type-arg.md +19 -0
- package/docs/reference/authoring-surface/type-argnames.md +12 -0
- package/docs/reference/authoring-surface/type-binding.md +62 -0
- package/docs/reference/authoring-surface/type-bindingof.md +12 -0
- package/docs/reference/authoring-surface/type-bodyacceptsdoor.md +15 -0
- package/docs/reference/authoring-surface/type-bodyacceptsslots.md +17 -0
- package/docs/reference/authoring-surface/type-ceilings.md +41 -0
- package/docs/reference/authoring-surface/type-chainlink.md +19 -0
- package/docs/reference/authoring-surface/type-chartaction.md +46 -0
- package/docs/reference/authoring-surface/type-chartagent.md +28 -0
- package/docs/reference/authoring-surface/type-chartevent.md +20 -0
- package/docs/reference/authoring-surface/type-chartguard.md +20 -0
- package/docs/reference/authoring-surface/type-chartinvoke.md +28 -0
- package/docs/reference/authoring-surface/type-chartjson.md +14 -0
- package/docs/reference/authoring-surface/type-chartstate.md +46 -0
- package/docs/reference/authoring-surface/type-charttext.md +40 -0
- package/docs/reference/authoring-surface/type-charttransition.md +22 -0
- package/docs/reference/authoring-surface/type-charttransitions.md +14 -0
- package/docs/reference/authoring-surface/type-chartvalue.md +15 -0
- package/docs/reference/authoring-surface/type-childat.md +12 -0
- package/docs/reference/authoring-surface/type-childslots.md +12 -0
- package/docs/reference/authoring-surface/type-commandenv.md +15 -0
- package/docs/reference/authoring-surface/type-commandinput.md +19 -0
- package/docs/reference/authoring-surface/type-commandoptions.md +78 -0
- package/docs/reference/authoring-surface/type-commandoutput.md +104 -0
- package/docs/reference/authoring-surface/type-customize.md +109 -0
- package/docs/reference/authoring-surface/type-dispatchdefinition.md +39 -0
- package/docs/reference/authoring-surface/type-dispatchinput.md +14 -0
- package/docs/reference/authoring-surface/type-dispatchopenparts.md +14 -0
- package/docs/reference/authoring-surface/type-dispatchoutput.md +65 -0
- package/docs/reference/authoring-surface/type-duration.md +15 -0
- package/docs/reference/authoring-surface/type-effectbinding.md +93 -0
- package/docs/reference/authoring-surface/type-effectlogic.md +12 -0
- package/docs/reference/authoring-surface/type-envopenparts.md +14 -0
- package/docs/reference/authoring-surface/type-eventaudience.md +27 -0
- package/docs/reference/authoring-surface/type-eventdef.md +45 -0
- package/docs/reference/authoring-surface/type-eventfrom.md +16 -0
- package/docs/reference/authoring-surface/type-eventsemantics.md +26 -0
- package/docs/reference/authoring-surface/type-execoutcome.md +34 -0
- package/docs/reference/authoring-surface/type-failslot.md +14 -0
- package/docs/reference/authoring-surface/type-failure.md +19 -0
- package/docs/reference/authoring-surface/type-failureinput.md +20 -0
- package/docs/reference/authoring-surface/type-filebody.md +41 -0
- package/docs/reference/authoring-surface/type-frominput.md +14 -0
- package/docs/reference/authoring-surface/type-hostinjectedinput.md +58 -0
- package/docs/reference/authoring-surface/type-httpdefinition.md +101 -0
- package/docs/reference/authoring-surface/type-httpinput.md +12 -0
- package/docs/reference/authoring-surface/type-httpmethod.md +14 -0
- package/docs/reference/authoring-surface/type-httpopenparts.md +12 -0
- package/docs/reference/authoring-surface/type-httpoutput.md +89 -0
- package/docs/reference/authoring-surface/type-httprequestparts.md +33 -0
- package/docs/reference/authoring-surface/type-jr2config.md +126 -0
- package/docs/reference/authoring-surface/type-lends.md +38 -0
- package/docs/reference/authoring-surface/type-logicat.md +12 -0
- package/docs/reference/authoring-surface/type-markedif.md +12 -0
- package/docs/reference/authoring-surface/type-mergedactors.md +12 -0
- package/docs/reference/authoring-surface/type-namedsecret.md +25 -0
- package/docs/reference/authoring-surface/type-noslotspec.md +26 -0
- package/docs/reference/authoring-surface/type-noslotworkspaceoptions.md +32 -0
- package/docs/reference/authoring-surface/type-openbodydeclaration.md +40 -0
- package/docs/reference/authoring-surface/type-openbodylogic.md +14 -0
- package/docs/reference/authoring-surface/type-openbodyoutput.md +42 -0
- package/docs/reference/authoring-surface/type-openbodyslot.md +18 -0
- package/docs/reference/authoring-surface/type-opencommandinput.md +17 -0
- package/docs/reference/authoring-surface/type-openeffect.md +12 -0
- package/docs/reference/authoring-surface/type-openeffectslots.md +12 -0
- package/docs/reference/authoring-surface/type-openof.md +12 -0
- package/docs/reference/authoring-surface/type-opensecret.md +23 -0
- package/docs/reference/authoring-surface/type-opensecretmark.md +12 -0
- package/docs/reference/authoring-surface/type-permissivenoslotworkspaceoptions.md +31 -0
- package/docs/reference/authoring-surface/type-permissiveworkspaceoptions.md +32 -0
- package/docs/reference/authoring-surface/type-poolmachine.md +28 -0
- package/docs/reference/authoring-surface/type-poolspec.md +60 -0
- package/docs/reference/authoring-surface/type-pushinput.md +15 -0
- package/docs/reference/authoring-surface/type-pushoptions.md +79 -0
- package/docs/reference/authoring-surface/type-pushoutput.md +124 -0
- package/docs/reference/authoring-surface/type-pushwireoutput.md +12 -0
- package/docs/reference/authoring-surface/type-quantity.md +14 -0
- package/docs/reference/authoring-surface/type-reached.md +12 -0
- package/docs/reference/authoring-surface/type-reposlot.md +70 -0
- package/docs/reference/authoring-surface/type-reposlotsof.md +16 -0
- package/docs/reference/authoring-surface/type-sandboxoptions.md +77 -0
- package/docs/reference/authoring-surface/type-secret.md +25 -0
- package/docs/reference/authoring-surface/type-sender.md +19 -0
- package/docs/reference/authoring-surface/type-sentbyguards.md +12 -0
- package/docs/reference/authoring-surface/type-size.md +85 -0
- package/docs/reference/authoring-surface/type-slothandles.md +32 -0
- package/docs/reference/authoring-surface/type-sourcenextresult.md +17 -0
- package/docs/reference/authoring-surface/type-sourcespec.md +44 -0
- package/docs/reference/authoring-surface/type-syncstate.md +12 -0
- package/docs/reference/authoring-surface/type-text.md +19 -0
- package/docs/reference/authoring-surface/type-textpart.md +24 -0
- package/docs/reference/authoring-surface/type-thinkinglevel.md +39 -0
- package/docs/reference/authoring-surface/type-toparameterizedobject.md +14 -0
- package/docs/reference/authoring-surface/type-toprovidedactor.md +14 -0
- package/docs/reference/authoring-surface/type-unknown.md +21 -0
- package/docs/reference/authoring-surface/type-usercontainer.md +77 -0
- package/docs/reference/authoring-surface/type-withoutemptyindex.md +20 -0
- package/docs/reference/authoring-surface/type-workflowevent.md +46 -0
- package/docs/reference/authoring-surface/type-workspaceaccess.md +16 -0
- package/docs/reference/authoring-surface/type-workspaced.md +94 -0
- package/docs/reference/authoring-surface/type-workspacehandles.md +23 -0
- package/docs/reference/authoring-surface/type-workspacemachine.md +29 -0
- package/docs/reference/authoring-surface/type-workspaceof.md +13 -0
- package/docs/reference/authoring-surface/type-workspaceoptions.md +39 -0
- package/docs/reference/authoring-surface/type-workspacespec.md +37 -0
- package/docs/reference/authoring-surface/workspace.md +259 -0
- package/docs/reference/authoring-surface.md +197 -0
- package/docs/reference/cli.md +433 -0
- package/docs/reference/config.md +756 -0
- package/docs/reference/decisions/0001-operator-owned-generic-sandbox.md +113 -0
- package/docs/reference/decisions/0002-duplex-streaming-actor.md +110 -0
- package/docs/reference/decisions/0004-worktree-provisioning-model.md +124 -0
- package/docs/reference/decisions/0005-sandbox-pod-composition.md +230 -0
- package/docs/reference/decisions/0006-agent-control-surface.md +47 -0
- package/docs/reference/decisions/0007-durable-machine-state.md +109 -0
- package/docs/reference/decisions/0008-orchestrator-as-deployed-app.md +60 -0
- package/docs/reference/decisions/0009-cli-and-instance-interface.md +275 -0
- package/docs/reference/decisions/0010-bdd-acceptance-tests.md +160 -0
- package/docs/reference/decisions/0011-workflow-defined-events.md +141 -0
- package/docs/reference/decisions/0012-workspace-wrapper-machine.md +103 -0
- package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +157 -0
- package/docs/reference/decisions/0014-observation-is-open-run-state-is-not.md +95 -0
- package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +265 -0
- package/docs/reference/decisions/0016-agent-turn-mechanics-are-internal.md +124 -0
- package/docs/reference/decisions/0017-pool-over-a-source-port.md +69 -0
- package/docs/reference/decisions/0018-instance-agents-are-definitions-jr2-assembles-the-harness.md +111 -0
- package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +275 -0
- package/docs/reference/decisions/0020-private-ca-trust-is-a-named-concept.md +60 -0
- package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +100 -0
- package/docs/reference/decisions/0022-observation-is-a-level-triggered-feed.md +103 -0
- package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +176 -0
- package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +439 -0
- package/docs/reference/decisions/0025-cancel-ends-a-run-stop-parks-it.md +65 -0
- package/docs/reference/decisions/0026-a-turn-that-is-over-is-never-prompted.md +62 -0
- package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +237 -0
- package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +89 -0
- package/docs/reference/decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md +154 -0
- package/docs/reference/decisions/0030-a-snapshot-names-the-machine-it-was-written-under.md +100 -0
- package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +190 -0
- package/docs/reference/decisions/0032-the-console-unlocks-with-the-instance-token.md +61 -0
- package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +127 -0
- package/docs/reference/decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md +57 -0
- package/docs/reference/decisions/0035-a-runaway-turn-is-ended-rerolled-once-then-a-fault.md +92 -0
- package/docs/reference/decisions/0036-a-turn-compacts-its-own-context-mid-flight.md +138 -0
- package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +196 -0
- package/docs/reference/decisions/0038-jr2-up-builds-every-image-it-deploys.md +170 -0
- package/docs/reference/decisions/0039-image-garbage-collects-by-reachability.md +115 -0
- package/docs/reference/decisions/0041-a-build-the-host-already-holds-is-not-spent-again.md +64 -0
- package/docs/reference/decisions/0042-ready-is-not-routable-an-admission-retries-its-connection.md +142 -0
- package/docs/reference/decisions/0043-the-kit-is-tested-as-installed-a-local-registry-stands-in-for-npm.md +170 -0
- package/docs/reference/decisions/0044-kit-images-live-at-a-canonical-home-a-self-host-mirrors-it.md +85 -0
- package/docs/reference/decisions/0045-the-platform-joins-the-image-address-and-the-cluster-chooses-it.md +82 -0
- package/docs/reference/decisions/0046-a-converge-failure-carries-its-diagnosis.md +34 -0
- package/docs/reference/decisions/0047-the-git-ssh-key-source-is-the-users-choice.md +47 -0
- package/docs/reference/decisions/0048-the-orchestrator-boots-without-its-repos.md +36 -0
- package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +126 -0
- package/docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md +73 -0
- package/docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md +227 -0
- package/docs/reference/decisions/0052-a-sandbox-lands-where-an-ordinary-pod-lands-and-the-cache-follows-it.md +81 -0
- package/docs/reference/decisions/0053-a-fetch-inside-the-pod-asks-the-node-cache-and-the-cache-asks-the-remote.md +111 -0
- package/docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md +126 -0
- package/docs/reference/decisions/0055-a-release-is-a-pushed-tag-and-the-publish-guard-is-the-credential.md +107 -0
- package/docs/reference/decisions/0056-a-global-jr2-hands-off-to-the-instances-own-and-an-instance-has-one-kit-version.md +62 -0
- package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +152 -0
- package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +106 -0
- package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +384 -0
- package/docs/reference/decisions/0060-a-workspace-states-its-size-and-jr2-reserves-all-of-it.md +111 -0
- package/docs/reference/decisions/0061-a-memory-kill-ends-the-agents-processes-not-the-harness.md +83 -0
- package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +212 -0
- package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +83 -0
- package/docs/reference/decisions/0064-a-workspace-waits-for-capacity.md +79 -0
- package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-a-week.md +65 -0
- 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
- package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +430 -0
- package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +464 -0
- package/docs/reference/decisions/0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md +180 -0
- package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +323 -0
- package/docs/reference/decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md +183 -0
- package/docs/reference/decisions/0072-the-docs-ship-inside-the-kit-and-an-instances-agents-md-indexes-them.md +144 -0
- package/docs/reference/decisions/0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md +122 -0
- package/docs/reference/decisions/0074-an-unrouted-mechanism-event-is-refused-at-load-and-faults-the-run.md +76 -0
- package/docs/reference/decisions.md +83 -0
- package/docs/reference/glossary.md +648 -0
- package/docs/reference/http-api.md +1348 -0
- package/docs/reference/machines.md +190 -0
- package/docs/reference/mechanism-events.md +72 -0
- package/docs/why.md +206 -0
- package/package.json +5 -3
- package/src/actor.ts +541 -79
- package/src/agent.ts +114 -38
- package/src/ambient.ts +2 -1
- package/src/assembly-checks.ts +189 -7
- package/src/cause.ts +234 -0
- package/src/chart.ts +169 -37
- package/src/charts.ts +34 -6
- package/src/config.ts +430 -178
- package/src/custodian.ts +74 -22
- package/src/customize.ts +188 -46
- package/src/dispatch-effect.ts +945 -0
- package/src/door-compat.ts +217 -0
- package/src/durability.ts +11 -4
- package/src/effect.ts +341 -44
- package/src/effector-wire.ts +160 -11
- package/src/fail.ts +209 -0
- package/src/harness-client.ts +541 -47
- package/src/http-effect.ts +495 -53
- package/src/http.ts +427 -16
- package/src/index.ts +60 -82
- package/src/instance.ts +68 -8
- package/src/internal.ts +84 -0
- package/src/machine-doc.ts +21 -3
- package/src/mechanism-routes.ts +168 -0
- package/src/model-keys.generated.ts +53 -0
- package/src/model-keys.ts +18 -58
- package/src/notices.ts +66 -13
- package/src/open-body.ts +159 -23
- package/src/open-effect.ts +81 -8
- package/src/open.ts +59 -6
- package/src/parts.ts +184 -13
- package/src/party.ts +68 -2
- package/src/pick.ts +463 -11
- package/src/pool.ts +247 -36
- package/src/registration.ts +83 -4
- package/src/repos.ts +2 -1
- package/src/run-host.ts +565 -31
- package/src/run-output.ts +38 -0
- package/src/sandbox-effect.ts +273 -38
- package/src/sandbox-kube.ts +3 -1
- package/src/secret-placement.ts +52 -17
- package/src/server.ts +3 -0
- package/src/setup.ts +239 -56
- package/src/size.ts +121 -9
- package/src/snapshot-store.ts +54 -10
- package/src/stub-harness.ts +44 -16
- package/src/timers.ts +14 -1
- package/src/vocabulary.ts +69 -14
- package/src/wire.ts +90 -16
- 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
|
-
|
|
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
|
|
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
|
-
/**
|
|
1204
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2115
|
-
*
|
|
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.
|
|
2279
|
-
|
|
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}:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2738
|
-
* "cancelled", because xstate only knows its actor was stopped (ADR-0025)
|
|
2739
|
-
*
|
|
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 } : {};
|