@jr2/orchestrator 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/console/components/app.ts +55 -15
- package/console/components/bottom-drawer.ts +259 -0
- package/console/components/drawer.ts +14 -6
- package/console/components/fleet.ts +36 -13
- package/console/components/machine-pane.ts +61 -5
- package/console/components/nav.ts +54 -8
- package/console/conversation.ts +161 -0
- package/console/login.ts +123 -0
- package/console/main.ts +600 -60
- package/console/store.ts +611 -64
- package/console/style.css +231 -0
- package/docs/agents-block.md +2 -2
- package/docs/concepts/agents-and-turns.md +142 -29
- package/docs/concepts/architecture.md +60 -34
- package/docs/concepts/callers-and-hooks.md +205 -0
- package/docs/concepts/charts.md +13 -8
- package/docs/concepts/composition.md +10 -1
- package/docs/concepts/converge.md +7 -2
- package/docs/concepts/durable-runs.md +43 -16
- package/docs/concepts/effects.md +133 -50
- package/docs/concepts/gates.md +95 -25
- package/docs/concepts/machines-and-workflows.md +14 -4
- package/docs/concepts/pools-and-sources.md +193 -17
- package/docs/concepts/secrets-and-trust.md +186 -62
- package/docs/concepts/the-menu.md +14 -0
- package/docs/concepts/workspaces.md +135 -56
- package/docs/get-started/a-model-decides.md +14 -13
- package/docs/get-started/an-agent-in-a-sandbox.md +46 -35
- package/docs/get-started/connect-a-model.md +1 -1
- package/docs/get-started/create-an-instance.md +17 -14
- package/docs/get-started/have-your-assistant-write-a-machine.md +111 -40
- package/docs/get-started/install.md +4 -0
- package/docs/guides/add-tools-to-the-sandbox.md +72 -15
- package/docs/guides/answer-a-gate-from-a-forge.md +304 -0
- package/docs/guides/answer-a-gate.md +74 -37
- package/docs/guides/bind-open-parts.md +29 -12
- package/docs/guides/call-an-mcp-tool-from-a-machine.md +572 -0
- package/docs/guides/cancel-a-run.md +26 -16
- package/docs/guides/cut-a-worktree.md +560 -0
- package/docs/guides/fix-a-failed-converge.md +19 -7
- package/docs/guides/give-a-caller-a-scope.md +203 -0
- package/docs/guides/give-an-agent-mcp-tools.md +422 -0
- package/docs/guides/iterate-on-a-machine-on-kind.md +10 -9
- package/docs/guides/let-an-agent-write-a-chart.md +14 -7
- package/docs/guides/look-inside-a-sandbox.md +115 -42
- package/docs/guides/move-from-kind-to-a-shared-cluster.md +16 -0
- package/docs/guides/open-the-console.md +63 -14
- package/docs/guides/push-and-sign-a-branch.md +67 -21
- package/docs/guides/read-a-conversation.md +225 -0
- package/docs/guides/run-a-chart-by-name.md +17 -16
- package/docs/guides/run-a-command-or-call-a-url.md +84 -41
- package/docs/guides/run-one-worker-per-item.md +34 -27
- package/docs/guides/set-an-agents-access-and-dials.md +69 -22
- package/docs/guides/size-a-workspace.md +19 -6
- package/docs/guides/start-a-run-for-each-alert.md +290 -0
- package/docs/guides/start-a-run-from-a-run.md +18 -16
- package/docs/guides/start-a-run.md +24 -18
- package/docs/guides/steer-an-agent.md +332 -0
- package/docs/guides/use-a-hosted-provider.md +7 -1
- package/docs/guides/watch-a-run.md +146 -117
- package/docs/guides/write-a-hook-kind.md +238 -0
- package/docs/guides/write-a-tool-as-an-mcp-server.md +351 -0
- package/docs/guides/write-a-workflow.md +18 -8
- package/docs/llms.txt +110 -23
- package/docs/reference/authoring-surface/agent.md +35 -12
- package/docs/reference/authoring-surface/answer.md +20 -4
- package/docs/reference/authoring-surface/arg.md +28 -6
- package/docs/reference/authoring-surface/basic.md +1 -1
- package/docs/reference/authoring-surface/bearer.md +1 -1
- package/docs/reference/authoring-surface/branchname.md +15 -0
- package/docs/reference/authoring-surface/carry.md +23 -0
- package/docs/reference/authoring-surface/chartschema.md +1 -1
- package/docs/reference/authoring-surface/chartschemafor.md +1 -1
- package/docs/reference/authoring-surface/command.md +37 -22
- package/docs/reference/authoring-surface/customize.md +4 -1
- package/docs/reference/authoring-surface/defineconfig.md +1 -1
- package/docs/reference/authoring-surface/defineevent.md +1 -1
- package/docs/reference/authoring-surface/dispatch.md +5 -4
- package/docs/reference/authoring-surface/fail.md +18 -5
- package/docs/reference/authoring-surface/file.md +37 -17
- package/docs/reference/authoring-surface/frompick.md +1 -1
- package/docs/reference/authoring-surface/gate.md +118 -0
- package/docs/reference/authoring-surface/group-agents.md +15 -0
- package/docs/reference/authoring-surface/group-charts.md +16 -0
- package/docs/reference/authoring-surface/group-composition.md +25 -0
- package/docs/reference/authoring-surface/group-config.md +17 -0
- package/docs/reference/authoring-surface/group-effects.md +48 -0
- package/docs/reference/authoring-surface/group-failure.md +13 -0
- package/docs/reference/authoring-surface/group-gates.md +16 -0
- package/docs/reference/authoring-surface/group-machine-and-events.md +15 -0
- package/docs/reference/authoring-surface/group-steering.md +12 -0
- package/docs/reference/authoring-surface/group-types-these-signatures-name.md +137 -0
- package/docs/reference/authoring-surface/group-workspaces-and-repos.md +15 -0
- package/docs/reference/authoring-surface/http.md +109 -9
- package/docs/reference/authoring-surface/isfailure.md +1 -1
- package/docs/reference/authoring-surface/isready.md +14 -0
- package/docs/reference/authoring-surface/jr2setup.md +14 -4
- package/docs/reference/authoring-surface/mcp.md +107 -0
- package/docs/reference/authoring-surface/open.md +41 -8
- package/docs/reference/authoring-surface/openbody.md +1 -1
- package/docs/reference/authoring-surface/paramsof.md +23 -5
- package/docs/reference/authoring-surface/pool.md +79 -11
- package/docs/reference/authoring-surface/push.md +31 -13
- package/docs/reference/authoring-surface/readyof.md +21 -0
- package/docs/reference/authoring-surface/reread.md +19 -0
- package/docs/reference/authoring-surface/secret.md +26 -5
- package/docs/reference/authoring-surface/sentby.md +1 -1
- package/docs/reference/authoring-surface/skip.md +22 -0
- package/docs/reference/authoring-surface/source.md +59 -4
- package/docs/reference/authoring-surface/steer.md +93 -0
- package/docs/reference/authoring-surface/text.md +1 -1
- package/docs/reference/authoring-surface/type-agentdefinition.md +16 -3
- package/docs/reference/authoring-surface/type-agentmcpserver.md +54 -0
- package/docs/reference/authoring-surface/type-agentslots.md +4 -1
- package/docs/reference/authoring-surface/type-anyframepath.md +14 -0
- package/docs/reference/authoring-surface/type-anyworktreepath.md +15 -0
- package/docs/reference/authoring-surface/type-bindactors.md +14 -0
- package/docs/reference/authoring-surface/type-binding.md +6 -7
- package/docs/reference/authoring-surface/type-caller.md +40 -0
- package/docs/reference/authoring-surface/type-callerrecord.md +32 -0
- package/docs/reference/authoring-surface/type-carry.md +19 -0
- package/docs/reference/authoring-surface/type-chainlink.md +3 -3
- package/docs/reference/authoring-surface/type-commandinput.md +4 -3
- package/docs/reference/authoring-surface/type-commandoptions.md +0 -22
- package/docs/reference/authoring-surface/type-commandoutput.md +23 -5
- package/docs/reference/authoring-surface/type-customize.md +20 -3
- package/docs/reference/authoring-surface/type-delivered.md +17 -0
- package/docs/reference/authoring-surface/type-deliveredevent.md +18 -0
- package/docs/reference/authoring-surface/type-dispatchdefinition.md +4 -3
- package/docs/reference/authoring-surface/type-dispatchoutput.md +9 -2
- package/docs/reference/authoring-surface/type-doorlessinput.md +19 -0
- package/docs/reference/authoring-surface/type-effectbinding.md +12 -2
- package/docs/reference/authoring-surface/type-filebody.md +4 -8
- package/docs/reference/authoring-surface/type-framepath.md +14 -0
- package/docs/reference/authoring-surface/type-gatebinding.md +15 -0
- package/docs/reference/authoring-surface/type-gatehooks.md +12 -0
- package/docs/reference/authoring-surface/type-gatelook.md +37 -0
- package/docs/reference/authoring-surface/type-gateoutcome.md +32 -0
- package/docs/reference/authoring-surface/type-gatereply.md +19 -0
- package/docs/reference/authoring-surface/type-gateslot.md +21 -0
- package/docs/reference/authoring-surface/type-gateslotinput.md +13 -0
- package/docs/reference/authoring-surface/type-gateslotparts.md +18 -0
- package/docs/reference/authoring-surface/type-gateslots.md +12 -0
- package/docs/reference/authoring-surface/type-gatetypesat.md +13 -0
- package/docs/reference/authoring-surface/type-hostinjectedinput.md +3 -5
- package/docs/reference/authoring-surface/type-httpdefinition.md +6 -3
- package/docs/reference/authoring-surface/type-httpfileinput.md +20 -0
- package/docs/reference/authoring-surface/type-httpoutput.md +8 -2
- package/docs/reference/authoring-surface/type-jr2config.md +37 -2
- package/docs/reference/authoring-surface/type-lateroutcome.md +21 -0
- package/docs/reference/authoring-surface/type-lends.md +5 -1
- package/docs/reference/authoring-surface/type-lookoutput.md +14 -0
- package/docs/reference/authoring-surface/type-mcpcalloutput.md +78 -0
- package/docs/reference/authoring-surface/type-mcpnotreturned.md +42 -0
- package/docs/reference/authoring-surface/type-mcpoptions.md +47 -0
- package/docs/reference/authoring-surface/type-mcpoutput.md +50 -0
- package/docs/reference/authoring-surface/type-mcpreadoutput.md +53 -0
- package/docs/reference/authoring-surface/type-mcpremoteserver.md +50 -0
- package/docs/reference/authoring-surface/type-mcpserver.md +41 -0
- package/docs/reference/authoring-surface/type-mcpstdioserver.md +42 -0
- package/docs/reference/authoring-surface/type-namedsecret.md +4 -4
- package/docs/reference/authoring-surface/type-namestart.md +77 -0
- package/docs/reference/authoring-surface/type-noslotworkspaceoptions.md +1 -6
- package/docs/reference/authoring-surface/type-notworktreekeys.md +12 -0
- package/docs/reference/authoring-surface/type-openbodydeclaration.md +1 -1
- package/docs/reference/authoring-surface/type-openbodyoutput.md +1 -1
- package/docs/reference/authoring-surface/type-opencommandinput.md +7 -2
- package/docs/reference/authoring-surface/type-permissivenoslotworkspaceoptions.md +1 -6
- package/docs/reference/authoring-surface/type-permissiveworkspaceoptions.md +5 -10
- package/docs/reference/authoring-surface/type-poolnextinput.md +22 -0
- package/docs/reference/authoring-surface/type-poolspec.md +11 -6
- package/docs/reference/authoring-surface/type-pushinput.md +22 -3
- package/docs/reference/authoring-surface/type-pushoutput.md +33 -8
- package/docs/reference/authoring-surface/type-pushsourcespec.md +38 -0
- package/docs/reference/authoring-surface/type-readsourcespec.md +47 -0
- package/docs/reference/authoring-surface/type-reposlot.md +6 -6
- package/docs/reference/authoring-surface/type-reread.md +18 -0
- package/docs/reference/authoring-surface/type-runsconfig.md +41 -0
- package/docs/reference/authoring-surface/type-sandboxoptions.md +11 -4
- package/docs/reference/authoring-surface/type-sender.md +26 -1
- package/docs/reference/authoring-surface/type-sendercallerfield.md +14 -0
- package/docs/reference/authoring-surface/type-size.md +1 -1
- package/docs/reference/authoring-surface/type-skip.md +19 -0
- package/docs/reference/authoring-surface/type-slothandles.md +5 -13
- package/docs/reference/authoring-surface/type-sourcehooks.md +23 -0
- package/docs/reference/authoring-surface/type-sourcein.md +25 -0
- package/docs/reference/authoring-surface/type-sourcereply.md +18 -0
- package/docs/reference/authoring-surface/type-sourcespec.md +26 -17
- package/docs/reference/authoring-surface/type-sourceverdict.md +49 -0
- package/docs/reference/authoring-surface/type-standingpoolmachine.md +12 -0
- package/docs/reference/authoring-surface/type-standingpoolspec.md +51 -0
- package/docs/reference/authoring-surface/type-steerargs.md +20 -0
- package/docs/reference/authoring-surface/type-thinkinglevel.md +1 -1
- package/docs/reference/authoring-surface/type-usercontainer.md +1 -1
- package/docs/reference/authoring-surface/type-workspaced.md +30 -9
- package/docs/reference/authoring-surface/type-workspacehandles.md +5 -6
- package/docs/reference/authoring-surface/type-workspaceoptions.md +1 -13
- package/docs/reference/authoring-surface/type-worktreefound.md +16 -0
- package/docs/reference/authoring-surface/type-worktreeinput.md +56 -0
- package/docs/reference/authoring-surface/type-worktreekey.md +14 -0
- package/docs/reference/authoring-surface/type-worktreeoptions.md +29 -0
- package/docs/reference/authoring-surface/type-worktreeoutput.md +45 -0
- package/docs/reference/authoring-surface/type-worktreepath.md +16 -0
- package/docs/reference/authoring-surface/type-worktreeready.md +37 -0
- package/docs/reference/authoring-surface/type-worktreerefused.md +21 -0
- package/docs/reference/authoring-surface/workspace.md +66 -38
- package/docs/reference/authoring-surface/worktree.md +71 -0
- package/docs/reference/authoring-surface.md +88 -147
- package/docs/reference/cli.md +413 -74
- package/docs/reference/config.md +248 -6
- package/docs/reference/decisions/0001-operator-owned-generic-sandbox.md +10 -9
- package/docs/reference/decisions/0002-duplex-streaming-actor.md +81 -14
- package/docs/reference/decisions/0004-worktree-provisioning-model.md +27 -21
- package/docs/reference/decisions/0005-sandbox-pod-composition.md +39 -25
- package/docs/reference/decisions/0006-agent-control-surface.md +6 -3
- package/docs/reference/decisions/0007-durable-machine-state.md +1 -1
- package/docs/reference/decisions/0009-cli-and-instance-interface.md +150 -49
- package/docs/reference/decisions/0010-bdd-acceptance-tests.md +5 -3
- package/docs/reference/decisions/0011-workflow-defined-events.md +256 -12
- package/docs/reference/decisions/0012-workspace-wrapper-machine.md +39 -32
- package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +54 -30
- package/docs/reference/decisions/0014-observation-is-open-run-state-is-not.md +53 -27
- package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +26 -16
- package/docs/reference/decisions/0016-agent-turn-mechanics-are-internal.md +9 -7
- package/docs/reference/decisions/0017-pool-over-a-source-port.md +226 -10
- package/docs/reference/decisions/0018-instance-agents-are-definitions-jr2-assembles-the-harness.md +15 -7
- package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +53 -27
- package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +16 -2
- package/docs/reference/decisions/0022-observation-is-a-level-triggered-feed.md +4 -4
- package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +124 -43
- package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +36 -38
- package/docs/reference/decisions/0025-cancel-ends-a-run-stop-parks-it.md +10 -5
- package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +16 -7
- package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +36 -19
- package/docs/reference/decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md +18 -10
- package/docs/reference/decisions/0030-a-snapshot-names-the-machine-it-was-written-under.md +3 -2
- package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +23 -15
- package/docs/reference/decisions/0032-the-console-unlocks-with-a-callers-credential.md +101 -0
- package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +22 -22
- package/docs/reference/decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md +6 -6
- package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +13 -12
- package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +23 -23
- package/docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md +6 -1
- package/docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md +57 -46
- package/docs/reference/decisions/0052-a-sandbox-lands-where-an-ordinary-pod-lands-and-the-cache-follows-it.md +5 -0
- package/docs/reference/decisions/0053-a-fetch-inside-the-pod-asks-the-node-cache-and-the-cache-asks-the-remote.md +20 -8
- package/docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md +55 -40
- package/docs/reference/decisions/0055-a-release-is-a-pushed-tag-and-the-publish-guard-is-the-credential.md +14 -13
- package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +31 -24
- package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +18 -10
- package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +137 -86
- package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +165 -91
- package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +7 -7
- package/docs/reference/decisions/0064-a-workspace-waits-for-capacity.md +2 -2
- package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-kept-for-a-set-time.md +109 -0
- package/docs/reference/decisions/0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md +53 -23
- package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +221 -153
- package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +161 -96
- package/docs/reference/decisions/0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md +32 -9
- package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +48 -33
- package/docs/reference/decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md +56 -30
- package/docs/reference/decisions/0072-the-docs-ship-inside-the-kit-and-an-instances-agents-md-indexes-them.md +35 -25
- package/docs/reference/decisions/0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md +1 -1
- package/docs/reference/decisions/0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md +296 -0
- package/docs/reference/decisions/0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md +347 -0
- package/docs/reference/decisions/0077-jr2-requires-landlock-on-every-sandbox-node.md +79 -0
- package/docs/reference/decisions/0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md +285 -0
- package/docs/reference/decisions/0079-an-agent-holds-mcp-tools-a-machine-calls-them-as-an-effect-and-the-machine-is-the-approval.md +198 -0
- package/docs/reference/decisions.md +9 -4
- package/docs/reference/glossary.md +138 -45
- package/docs/reference/hooks.md +1077 -0
- package/docs/reference/http-api.md +988 -123
- package/docs/reference/machines.md +0 -1
- package/docs/why.md +7 -5
- package/package.json +10 -3
- package/src/actor.ts +194 -42
- package/src/agent.ts +56 -6
- package/src/ambient.ts +3 -5
- package/src/assembly-checks.ts +530 -4
- package/src/callers.ts +245 -0
- package/src/chart.ts +238 -26
- package/src/charts.ts +9 -2
- package/src/config.ts +566 -7
- package/src/conversations.ts +453 -0
- package/src/custodian.lua +17 -5
- package/src/custodian.ts +257 -77
- package/src/customize.ts +154 -7
- package/src/dispatch-effect.ts +142 -26
- package/src/durability.ts +2 -0
- package/src/duration.ts +20 -0
- package/src/effect-checks.ts +52 -4
- package/src/effect.ts +401 -132
- package/src/effector-wire.ts +278 -30
- package/src/fail.ts +17 -4
- package/src/gate-brand.ts +16 -0
- package/src/gate.ts +634 -48
- package/src/harness-client.ts +84 -35
- package/src/harness-env.ts +33 -0
- package/src/held-secrets.ts +197 -39
- package/src/holds.ts +5 -2
- package/src/hook-kinds/discord.ts +92 -0
- package/src/hook-kinds/door.ts +107 -0
- package/src/hook-kinds/github.ts +89 -0
- package/src/hook-kinds/gitlab.ts +72 -0
- package/src/hook-kinds/hook.ts +7 -0
- package/src/hook-kinds/index.ts +465 -0
- package/src/hook-kinds/slack.ts +151 -0
- package/src/hook-kinds/webhook.ts +113 -0
- package/src/hooks.ts +506 -0
- package/src/http-effect.ts +164 -70
- package/src/http.ts +933 -99
- package/src/index.ts +30 -5
- package/src/instance.ts +196 -26
- package/src/internal.ts +23 -3
- package/src/kube-client.ts +6 -3
- package/src/machine-doc.ts +29 -5
- package/src/mcp-effect.ts +424 -0
- package/src/model-keys.ts +24 -0
- package/src/mount-digest.ts +41 -7
- package/src/names.ts +82 -7
- package/src/notices.ts +576 -59
- package/src/open-body.ts +110 -30
- package/src/open-effect.ts +17 -4
- package/src/open.ts +40 -7
- package/src/parts.ts +125 -35
- package/src/party.ts +35 -1
- package/src/pick.ts +43 -9
- package/src/pool.ts +1172 -172
- package/src/registration.ts +249 -17
- package/src/repo-identity.ts +7 -3
- package/src/repos.ts +20 -2
- package/src/run-host.ts +1614 -190
- package/src/run-list.ts +191 -0
- package/src/sandbox-effect.ts +217 -66
- package/src/sandbox-kube.ts +116 -21
- package/src/secret-placement.ts +248 -35
- package/src/server.ts +53 -4
- package/src/setup.ts +158 -38
- package/src/snapshot-store.ts +565 -48
- package/src/steer.ts +111 -0
- package/src/stub-harness.ts +6 -2
- package/src/tokens.ts +362 -46
- package/src/vocabulary.ts +23 -4
- package/src/wire.ts +27 -10
- package/src/workspace.ts +314 -259
- package/src/worktree.ts +249 -0
- package/docs/reference/authoring-surface/type-noslotspec.md +0 -26
- package/docs/reference/authoring-surface/type-workspacespec.md +0 -37
- package/docs/reference/decisions/0032-the-console-unlocks-with-the-instance-token.md +0 -61
- package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-a-week.md +0 -65
package/console/main.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// registered workflow, each expandable to its runs); the center is the selected workflow's Machine,
|
|
3
3
|
// fetched from GET /workflows/:name/machine, laid out with elkjs (loaded as a UMD script ->
|
|
4
4
|
// window.ELK), rendered as nested SVG (canvas.ts) and live-highlighted by the selected run; the
|
|
5
|
-
// right drawer is Attention (the gate inbox) and Activity (the emit log)
|
|
5
|
+
// right drawer is Attention (the gate inbox) and Activity (the emit log); the bottom drawer is the
|
|
6
|
+
// selected run's kept conversations, one tab for each Agent slot, with the Steering box.
|
|
6
7
|
//
|
|
7
8
|
// The ADDRESS carries the selection: `/` and `/workflows/:name` serve these same bytes, the path
|
|
8
9
|
// says which workflow is selected, pushState moves it on click and popstate restores it. ONE
|
|
@@ -13,11 +14,19 @@
|
|
|
13
14
|
// makes reattach convergent with no cursor (store.ts folds every frame idempotently).
|
|
14
15
|
//
|
|
15
16
|
// The page as SERVED is ADR-0014's observer: open structure and observation, no credential in the
|
|
16
|
-
// bytes. Control is EARNED per tab: the
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
17
|
+
// bytes. Control is EARNED per tab: the person types a Caller's token into the nav, or logs in
|
|
18
|
+
// against an issuer the Instance declares (OIDC code + PKCE, login.ts), and the credential lives in
|
|
19
|
+
// sessionStorage — survives a reload, dies with the tab, never a cookie. The page asks
|
|
20
|
+
// `GET /whoami` what the credential may do, and renders only the controls its scopes open
|
|
21
|
+
// (ADR-0078): start (POST /workflows/:name/runs), the Gate cards (GET /gates read again on each
|
|
22
|
+
// frame, POST /runs/:id/gates/:gate/events), cancel (POST /runs/:id/events), the ended runs in the
|
|
23
|
+
// rail (GET /runs?status=ended), the conversations (GET /runs/:id/conversations and its routes,
|
|
24
|
+
// ADR-0023), the Steering box (GET /runs/:id/agents, POST /runs/:id/steering). Any later 401 drops
|
|
25
|
+
// the whole page back to observer mode. That is ADR-0032 end to end — the bands themselves never
|
|
26
|
+
// moved.
|
|
27
|
+
//
|
|
28
|
+
// The guarded streams (a conversation's `/events`) are read with `fetch`, never an EventSource: an
|
|
29
|
+
// EventSource cannot send the credential, and the credential never rides a URL or a cookie.
|
|
21
30
|
//
|
|
22
31
|
// STORE-FIRST, VDOM-PAINTED (ADR-0034): everything the page believes lives in the pure reducer
|
|
23
32
|
// (store.ts) and arrives there as frames — wire frames and page facts alike. `dispatch` is the one
|
|
@@ -28,26 +37,49 @@
|
|
|
28
37
|
//
|
|
29
38
|
// A run that binds a Chart (ADR-0070) draws ITS Machine, not the Workflow's: the definition's doc
|
|
30
39
|
// has the Open body as one opaque child, and a Chart one cannot watch is a Chart one cannot trust.
|
|
31
|
-
//
|
|
32
|
-
//
|
|
40
|
+
// A run on an older Assembly (ADR-0069) draws its own Machine too: the current Machine's states are
|
|
41
|
+
// not the ones it moves through. So the doc the center pane draws is chosen per selection
|
|
42
|
+
// (`selectedDocSource`): the Workflow's Machine, or the run's own from
|
|
33
43
|
// `GET /workflows/:name/runs/:runId/machine`, cached per run id. The live child still joins its
|
|
34
|
-
// subgraph by `src`, which the per-run doc keeps as the body's slot key.
|
|
44
|
+
// subgraph by `src`, which the per-run doc keeps as the body's slot key. A `drifted` run has no
|
|
45
|
+
// Machine to draw: the pane says why in place of the graph.
|
|
35
46
|
|
|
36
47
|
import { h, render } from "preact";
|
|
37
48
|
import {
|
|
49
|
+
activeSlot,
|
|
38
50
|
applyFrame,
|
|
39
51
|
emptyStore,
|
|
40
52
|
failureReason,
|
|
41
53
|
machineDocSource,
|
|
42
|
-
|
|
54
|
+
may,
|
|
55
|
+
selectedDocSource,
|
|
56
|
+
selectedRunDrift,
|
|
57
|
+
slotConversations,
|
|
58
|
+
type CallerView,
|
|
43
59
|
type Frame,
|
|
44
|
-
type
|
|
60
|
+
type InboxGate,
|
|
45
61
|
type ObservedRun,
|
|
62
|
+
type RunSummaryView,
|
|
46
63
|
type Store,
|
|
47
64
|
type HeldLine,
|
|
65
|
+
type StarterView,
|
|
48
66
|
type WaitingLine,
|
|
49
67
|
type WorkflowListing,
|
|
50
68
|
} from "./store.ts";
|
|
69
|
+
import { readSse, type AgentLine, type ConversationEntry, type ConversationRow } from "./conversation.ts";
|
|
70
|
+
import {
|
|
71
|
+
authorizeUrl,
|
|
72
|
+
credentialOf,
|
|
73
|
+
loginReturn,
|
|
74
|
+
loginScopes,
|
|
75
|
+
pkceChallenge,
|
|
76
|
+
randomValue,
|
|
77
|
+
redirectUri,
|
|
78
|
+
tokenRequestBody,
|
|
79
|
+
type ConsoleConfig,
|
|
80
|
+
type LoginIssuer,
|
|
81
|
+
type PendingLogin,
|
|
82
|
+
} from "./login.ts";
|
|
51
83
|
import { zoomFit, zoomIn, zoomOut, zoomReset, type MachineDoc } from "./canvas.ts";
|
|
52
84
|
import { App, type AppApi, type MachineView } from "./components/app.ts";
|
|
53
85
|
|
|
@@ -56,28 +88,39 @@ let store: Store = emptyStore();
|
|
|
56
88
|
let view: MachineView = { doc: null, placeholder: null, note: null };
|
|
57
89
|
/** The nav's zoom readout — canvas.ts reports through `api.onScale`; a render carries it. */
|
|
58
90
|
let zoomPct = 100;
|
|
91
|
+
/** The issuers the nav offers a login against (`GET /console/config`), and why the last login
|
|
92
|
+
* failed, if it did. Working state of the bootstrap, like the zoom: what the page KNOWS of a run
|
|
93
|
+
* stays in the store. */
|
|
94
|
+
let logins: LoginIssuer[] = [];
|
|
95
|
+
let loginNote: string | null = null;
|
|
59
96
|
|
|
60
97
|
const root = document.getElementById("root")!;
|
|
61
98
|
|
|
62
99
|
/** One render from the top, whatever moved — store or working state; the vdom diffs the rest. */
|
|
63
100
|
function rerender(): void {
|
|
64
|
-
render(h(App, { store, view, zoomPct, initialToken, schemas: inputSchemas, api }), root);
|
|
101
|
+
render(h(App, { store, view, zoomPct, initialToken, logins, loginNote, schemas: inputSchemas, api }), root);
|
|
65
102
|
}
|
|
66
103
|
|
|
67
104
|
/** Fold one frame and re-render. Every state change in this file goes through here — a page that
|
|
68
105
|
* writes anywhere else holds a private belief, which is the bug class store.ts exists to end. */
|
|
69
106
|
function dispatch(frame: Frame): void {
|
|
107
|
+
const before = store;
|
|
70
108
|
store = applyFrame(store, frame);
|
|
71
109
|
rerender();
|
|
72
110
|
// A frame can change which doc the pane draws: a new selection, a run selected, a run that now
|
|
73
111
|
// binds a Chart. The check is a key compare; only a change fetches.
|
|
74
112
|
void syncMachine();
|
|
113
|
+
// A frame can change the selected run, or the slot the bottom drawer shows: read what it needs.
|
|
114
|
+
syncTalk();
|
|
115
|
+
// A new selection: read its details (waits, picks, failure, starter) and, for an ended run opened
|
|
116
|
+
// from the rail, its last status, so the graph lights where it ended.
|
|
117
|
+
if (store.selectedRunId !== before.selectedRunId) readSelected();
|
|
75
118
|
}
|
|
76
119
|
|
|
77
|
-
// ---- The
|
|
120
|
+
// ---- The credential (ADR-0032) ------------------------------------------------------------------
|
|
78
121
|
// The VALUE lives here (sessionStorage) and rides only in Authorization headers; the store holds
|
|
79
|
-
// its STATE. Entry is validated with
|
|
80
|
-
//
|
|
122
|
+
// its STATE and the Caller it is. Entry is validated with `GET /whoami`, which answers any Caller
|
|
123
|
+
// and says what it may do (ADR-0078) — a typed token and a login's JWT alike.
|
|
81
124
|
|
|
82
125
|
const TOKEN_KEY = "jr2.console.token";
|
|
83
126
|
const token = (): string => sessionStorage.getItem(TOKEN_KEY) ?? "";
|
|
@@ -110,21 +153,27 @@ let tokenSerial = 0;
|
|
|
110
153
|
async function validateToken(): Promise<void> {
|
|
111
154
|
const serial = ++tokenSerial;
|
|
112
155
|
dispatch({ kind: "token", state: "checking" });
|
|
113
|
-
let
|
|
156
|
+
let caller: CallerView | null = null;
|
|
114
157
|
try {
|
|
115
|
-
res = await fetch("/
|
|
158
|
+
const res = await fetch("/whoami", { headers: { authorization: `Bearer ${token()}` } });
|
|
159
|
+
if (res.ok) caller = (await res.json()) as CallerView;
|
|
116
160
|
} catch {
|
|
117
|
-
|
|
161
|
+
// network trouble reads as invalid; re-entering the token retries
|
|
118
162
|
}
|
|
119
163
|
if (serial !== tokenSerial) return; // a newer entry owns the badge — this answer is nobody's
|
|
120
|
-
if (!
|
|
164
|
+
if (!caller) return dispatch({ kind: "token", state: "invalid" });
|
|
165
|
+
dispatch({ kind: "caller", caller });
|
|
121
166
|
dispatch({ kind: "token", state: "live" });
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
167
|
+
// Seed the inbox, the ended runs, the conversations and the run details from what the page
|
|
168
|
+
// already holds: the frames that carried them came before the credential, so they read nothing.
|
|
169
|
+
refreshInbox();
|
|
170
|
+
if (store.workflow) void refreshHistory(store.workflow);
|
|
171
|
+
if (store.selectedRunId) void refreshTalk(store.selectedRunId);
|
|
172
|
+
readSelected();
|
|
125
173
|
}
|
|
126
174
|
|
|
127
175
|
function setToken(value: string): void {
|
|
176
|
+
loginNote = null;
|
|
128
177
|
if (!value) {
|
|
129
178
|
tokenSerial++; // strand any in-flight validation of the token this just cleared
|
|
130
179
|
sessionStorage.removeItem(TOKEN_KEY);
|
|
@@ -135,45 +184,477 @@ function setToken(value: string): void {
|
|
|
135
184
|
void validateToken();
|
|
136
185
|
}
|
|
137
186
|
|
|
138
|
-
// ----
|
|
187
|
+
// ---- The login (ADR-0032, ADR-0078) -------------------------------------------------------------
|
|
188
|
+
// OIDC, the code flow with PKCE, as a public client: the page goes to the issuer, the issuer comes
|
|
189
|
+
// back to `/` with a code, and the page exchanges the code for an ID token, which it then keeps and
|
|
190
|
+
// sends as it keeps and sends a typed token. What it holds between the two halves (the verifier,
|
|
191
|
+
// the state, where to go back to) is in sessionStorage too, and goes when the return reads it.
|
|
192
|
+
|
|
193
|
+
const LOGIN_KEY = "jr2.console.login";
|
|
194
|
+
|
|
195
|
+
/** The issuers that offer a login, off the open route. A failure offers none: a typed token still
|
|
196
|
+
* works. */
|
|
197
|
+
async function loadLogins(): Promise<void> {
|
|
198
|
+
try {
|
|
199
|
+
const res = await fetch("/console/config");
|
|
200
|
+
if (res.ok) logins = ((await res.json()) as ConsoleConfig).login ?? [];
|
|
201
|
+
} catch {
|
|
202
|
+
logins = [];
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Go to the issuer. Its discovery document names where; the page asks it for the scopes it lists. */
|
|
207
|
+
async function login(issuer: LoginIssuer): Promise<void> {
|
|
208
|
+
loginNote = null;
|
|
209
|
+
try {
|
|
210
|
+
const res = await fetch(`${issuer.issuer.replace(/\/+$/, "")}/.well-known/openid-configuration`);
|
|
211
|
+
if (!res.ok) throw new Error(`the issuer's discovery document answered ${res.status}`);
|
|
212
|
+
const discovery = (await res.json()) as {
|
|
213
|
+
authorization_endpoint?: string;
|
|
214
|
+
token_endpoint?: string;
|
|
215
|
+
scopes_supported?: string[];
|
|
216
|
+
};
|
|
217
|
+
if (!discovery.authorization_endpoint || !discovery.token_endpoint) {
|
|
218
|
+
throw new Error("the issuer's discovery document names no authorization or token endpoint");
|
|
219
|
+
}
|
|
220
|
+
if (!globalThis.crypto?.subtle) throw new Error("a login needs the Console on https or on localhost");
|
|
221
|
+
const pending: PendingLogin = {
|
|
222
|
+
issuer: issuer.issuer,
|
|
223
|
+
clientId: issuer.clientId,
|
|
224
|
+
state: randomValue(),
|
|
225
|
+
verifier: randomValue(),
|
|
226
|
+
tokenEndpoint: discovery.token_endpoint,
|
|
227
|
+
returnTo: location.pathname,
|
|
228
|
+
};
|
|
229
|
+
sessionStorage.setItem(LOGIN_KEY, JSON.stringify(pending));
|
|
230
|
+
location.assign(
|
|
231
|
+
authorizeUrl({
|
|
232
|
+
endpoint: discovery.authorization_endpoint,
|
|
233
|
+
clientId: issuer.clientId,
|
|
234
|
+
redirectUri: redirectUri(location.origin),
|
|
235
|
+
scope: loginScopes(discovery.scopes_supported),
|
|
236
|
+
state: pending.state,
|
|
237
|
+
challenge: await pkceChallenge(pending.verifier),
|
|
238
|
+
}),
|
|
239
|
+
);
|
|
240
|
+
} catch (err) {
|
|
241
|
+
loginNote = `login failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
242
|
+
rerender();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** The return from the issuer, at boot: exchange the code, keep the ID token, and put the address
|
|
247
|
+
* back where the person logged in from. A return that does not match the pending login (another
|
|
248
|
+
* tab's, or a forged one) is refused, and nothing is kept. */
|
|
249
|
+
async function finishLogin(): Promise<void> {
|
|
250
|
+
const back = loginReturn(location.search);
|
|
251
|
+
if (!back) return;
|
|
252
|
+
const raw = sessionStorage.getItem(LOGIN_KEY);
|
|
253
|
+
sessionStorage.removeItem(LOGIN_KEY);
|
|
254
|
+
let pending: PendingLogin | null = null;
|
|
255
|
+
try {
|
|
256
|
+
pending = raw ? (JSON.parse(raw) as PendingLogin) : null;
|
|
257
|
+
} catch {
|
|
258
|
+
pending = null;
|
|
259
|
+
}
|
|
260
|
+
history.replaceState({}, "", pending?.returnTo ?? "/");
|
|
261
|
+
if (!pending || back.state !== pending.state) {
|
|
262
|
+
loginNote = "login failed: the return does not match a login that this tab started";
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if ("error" in back) {
|
|
266
|
+
loginNote = `login failed: ${back.error}`;
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
try {
|
|
270
|
+
const res = await fetch(pending.tokenEndpoint, {
|
|
271
|
+
method: "POST",
|
|
272
|
+
headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
|
|
273
|
+
body: tokenRequestBody({
|
|
274
|
+
code: back.code,
|
|
275
|
+
redirectUri: redirectUri(location.origin),
|
|
276
|
+
clientId: pending.clientId,
|
|
277
|
+
verifier: pending.verifier,
|
|
278
|
+
}),
|
|
279
|
+
});
|
|
280
|
+
const jwt = res.ok ? credentialOf(await res.json()) : null;
|
|
281
|
+
if (!jwt) throw new Error(`the issuer's token endpoint answered ${res.status} with no ID token`);
|
|
282
|
+
sessionStorage.setItem(TOKEN_KEY, jwt);
|
|
283
|
+
} catch (err) {
|
|
284
|
+
loginNote = `login failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ---- The Gate inbox (frame-triggered — ADR-0011, ADR-0032) --------------------------------------
|
|
289
|
+
|
|
290
|
+
/** One read of `key` at a time, and one more after it when something asked meanwhile, with the
|
|
291
|
+
* arguments of the last ask: a burst of frames that each ask for one run's reads makes two reads,
|
|
292
|
+
* not one for each frame. Fetch bookkeeping, not belief. */
|
|
293
|
+
function coalesced<A extends unknown[]>(
|
|
294
|
+
read: (key: string, ...args: A) => Promise<void>,
|
|
295
|
+
): (key: string, ...args: A) => void {
|
|
296
|
+
const asked = new Map<string, A | null>(); // a key in flight: the arguments of the ask after it
|
|
297
|
+
const ask = (key: string, ...args: A): void => {
|
|
298
|
+
if (asked.has(key)) {
|
|
299
|
+
asked.set(key, args);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
asked.set(key, null);
|
|
303
|
+
read(key, ...args)
|
|
304
|
+
.catch(() => {
|
|
305
|
+
// transient — the next frame reads again
|
|
306
|
+
})
|
|
307
|
+
.finally(() => {
|
|
308
|
+
const again = asked.get(key);
|
|
309
|
+
asked.delete(key);
|
|
310
|
+
if (again) ask(key, ...again);
|
|
311
|
+
});
|
|
312
|
+
};
|
|
313
|
+
return ask;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** The inbox read in flight, and whether a frame asked for another while it was out. Fetch
|
|
317
|
+
* bookkeeping like `docs`, not belief: a feed frame and a fleet snapshot each ask, so one read at a
|
|
318
|
+
* time, and one more after it when something asked meanwhile, keeps a burst of frames to two reads.
|
|
319
|
+
* The read that STARTED last speaks: an answer that a newer read superseded is dropped. */
|
|
320
|
+
let inboxReading = false;
|
|
321
|
+
let inboxAgain = false;
|
|
322
|
+
|
|
323
|
+
/** Read `GET /gates`, every open Gate that the Caller may answer, and fold the WHOLE answer in.
|
|
324
|
+
* Called on every frame that touches a run — that is the synchronization: a Gate opens with a
|
|
325
|
+
* state entry and closes with its exit, and every entry/exit lands a frame. A delivery's success
|
|
326
|
+
* is the next read coming back without the card; nothing here concludes anything on its own. */
|
|
327
|
+
function refreshInbox(): void {
|
|
328
|
+
// Observer mode, or a Caller with no `answer`: the server would refuse the read (ADR-0032).
|
|
329
|
+
if (!may(store, "answer")) return;
|
|
330
|
+
if (inboxReading) {
|
|
331
|
+
inboxAgain = true;
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
inboxReading = true;
|
|
335
|
+
// The credential this read speaks for: an answer that lands after another Caller went live is
|
|
336
|
+
// the old Caller's, and is dropped (the read that follows speaks for the new one).
|
|
337
|
+
const serial = tokenSerial;
|
|
338
|
+
void (async () => {
|
|
339
|
+
try {
|
|
340
|
+
const res = await gfetch("/gates");
|
|
341
|
+
if (serial !== tokenSerial) inboxAgain = true;
|
|
342
|
+
else if (res?.ok) dispatch({ kind: "inbox", gates: ((await res.json()) as { gates: InboxGate[] }).gates });
|
|
343
|
+
} catch {
|
|
344
|
+
// transient — the next frame reads again
|
|
345
|
+
} finally {
|
|
346
|
+
inboxReading = false;
|
|
347
|
+
if (inboxAgain) {
|
|
348
|
+
inboxAgain = false;
|
|
349
|
+
refreshInbox();
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
})();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ---- Run details: waits, picks, failure, starter (frame-triggered — ADR-0064, ADR-0024) ---------
|
|
139
356
|
|
|
140
357
|
/** Per-run re-fetch serials — fetch bookkeeping like `docs`, not belief. The browser runs these
|
|
141
|
-
* requests on parallel connections, so answers can land out of ORDER
|
|
142
|
-
*
|
|
143
|
-
* launched (gate still open), and folding the straggler would resurrect a card the server already
|
|
144
|
-
* emptied — pinning the ⚑ and inviting a second delivery until the run's next frame, which a run
|
|
145
|
-
* parked in a long agent step may not land for minutes. Only the latest-STARTED re-fetch may
|
|
358
|
+
* requests on parallel connections, so answers can land out of ORDER, and an older answer folded
|
|
359
|
+
* last would bring back a wait the server already cleared. Only the latest-STARTED re-fetch may
|
|
146
360
|
* speak for a run; superseded answers are dropped before they reach the store. */
|
|
147
361
|
const runFetchSerial = new Map<string, number>();
|
|
148
362
|
|
|
149
|
-
/** Re-read one run's
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
363
|
+
/** Re-read one run's status off the guarded surface: why its Workspaces in `placing` wait
|
|
364
|
+
* (ADR-0064), its outstanding picks (ADR-0024), its Failure's reason (ADR-0073) and who started it
|
|
365
|
+
* (ADR-0078), each folded as its own frame. Called for the selected run only, on its selection and
|
|
366
|
+
* on every frame that touches it: a Workspace enters and leaves `placing` with a frame. For an ended run that the page does not
|
|
367
|
+
* watch, the same answer is its last status (ADR-0032). */
|
|
368
|
+
async function readRun(runId: string, workflow: string): Promise<void> {
|
|
369
|
+
// Observer mode, or a Caller that may not read this Workflow: the server would refuse the read,
|
|
370
|
+
// so the page does not make it (ADR-0032).
|
|
371
|
+
if (!may(store, "read", workflow)) return;
|
|
157
372
|
const serial = (runFetchSerial.get(runId) ?? 0) + 1;
|
|
158
373
|
runFetchSerial.set(runId, serial);
|
|
159
374
|
const res = await gfetch(`/runs/${encodeURIComponent(runId)}`);
|
|
160
375
|
if (!res) return; // 401 already dropped the page to observer
|
|
161
|
-
// !ok:
|
|
376
|
+
// !ok: swept — nothing waits.
|
|
162
377
|
const body = res.ok
|
|
163
|
-
? ((await res.json()) as {
|
|
164
|
-
status?: string;
|
|
378
|
+
? ((await res.json()) as ObservedRun & {
|
|
165
379
|
output?: unknown;
|
|
166
|
-
gates?: GateCard[];
|
|
167
380
|
waiting?: WaitingLine[];
|
|
168
381
|
held?: HeldLine[];
|
|
382
|
+
startedBy?: StarterView;
|
|
169
383
|
})
|
|
170
|
-
:
|
|
384
|
+
: null;
|
|
171
385
|
if (runFetchSerial.get(runId) !== serial) return; // superseded — a newer re-fetch owns the answer
|
|
172
|
-
dispatch({ kind: "
|
|
173
|
-
|
|
174
|
-
//
|
|
175
|
-
// the
|
|
176
|
-
if (
|
|
386
|
+
dispatch({ kind: "waiting", runId, workflow, waiting: body?.waiting ?? [], held: body?.held ?? [] });
|
|
387
|
+
if (!body) return; // a 404 says nothing of the run's end or its starter, so what the page has stays
|
|
388
|
+
// An ended run: its last status, held with the runs the page watched end (the store keeps it
|
|
389
|
+
// only for a run that is not live and that the rail lists).
|
|
390
|
+
if (body.status !== "active") {
|
|
391
|
+
dispatch({
|
|
392
|
+
kind: "endedStatus",
|
|
393
|
+
status: {
|
|
394
|
+
runId: body.runId,
|
|
395
|
+
workflow: body.workflow,
|
|
396
|
+
status: body.status,
|
|
397
|
+
value: body.value,
|
|
398
|
+
children: body.children ?? [],
|
|
399
|
+
...(body.bound ? { bound: true as const } : {}),
|
|
400
|
+
...(body.chartHashes ? { chartHashes: body.chartHashes } : {}),
|
|
401
|
+
...(body.dispatchedBy ? { dispatchedBy: body.dispatchedBy } : {}),
|
|
402
|
+
...(body.dispatched ? { dispatched: body.dispatched } : {}),
|
|
403
|
+
},
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
// The reason of a run that ended `failed` (ADR-0073), and who started the run (ADR-0078).
|
|
407
|
+
dispatch({ kind: "failure", runId, reason: failureReason(body) });
|
|
408
|
+
dispatch({ kind: "starter", runId, startedBy: body.startedBy ?? null });
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** {@link readRun}, one read for each run at a time: status frames come on every save. */
|
|
412
|
+
const refreshRun = coalesced(readRun);
|
|
413
|
+
|
|
414
|
+
/** Read the selected run's details. The Console shows waits, picks, a failure and a starter for the
|
|
415
|
+
* selected run only, so it reads no other run's. */
|
|
416
|
+
function readSelected(): void {
|
|
417
|
+
const id = store.selectedRunId;
|
|
418
|
+
if (!id || !store.workflow) return;
|
|
419
|
+
void refreshRun(id, store.workflow);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// ---- The ended runs of the selection (ADR-0009, ADR-0032) ---------------------------------------
|
|
423
|
+
|
|
424
|
+
/** Serial of the history reads, so an answer for a Workflow the reader left is dropped. */
|
|
425
|
+
let historySerial = 0;
|
|
426
|
+
|
|
427
|
+
/** Read the selected Workflow's ended runs (`GET /runs?status=ended&workflow=`), the first page:
|
|
428
|
+
* what the rail lists under the live runs. Needs `read`; without it the rail lists live runs only. */
|
|
429
|
+
async function refreshHistory(workflow: string): Promise<void> {
|
|
430
|
+
if (!may(store, "read", workflow)) return;
|
|
431
|
+
const serial = ++historySerial;
|
|
432
|
+
try {
|
|
433
|
+
const res = await gfetch(`/runs?status=ended&workflow=${encodeURIComponent(workflow)}`);
|
|
434
|
+
if (!res?.ok || serial !== historySerial) return;
|
|
435
|
+
const { runs } = (await res.json()) as { runs: RunSummaryView[] };
|
|
436
|
+
dispatch({ kind: "history", workflow, runs });
|
|
437
|
+
} catch {
|
|
438
|
+
// transient — the next snapshot reads again
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ---- The Assembly (ADR-0069) --------------------------------------------------------------------
|
|
443
|
+
|
|
444
|
+
/** Read the current Assembly and the one each live run holds (open): what marks a run "older
|
|
445
|
+
* Assembly". An Orchestrator with no Assembly answers `current: null`, and nothing is marked. */
|
|
446
|
+
async function readAssembly(): Promise<void> {
|
|
447
|
+
try {
|
|
448
|
+
const res = await fetch("/assembly");
|
|
449
|
+
if (!res.ok) return;
|
|
450
|
+
const body = (await res.json()) as {
|
|
451
|
+
current: { hash: string } | null;
|
|
452
|
+
held?: Array<{ hash: string; runs: string[] }>;
|
|
453
|
+
};
|
|
454
|
+
dispatch({ kind: "assembly", current: body.current?.hash ?? null, held: body.held ?? [] });
|
|
455
|
+
} catch {
|
|
456
|
+
// transient — the next snapshot reads again
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/** {@link readAssembly}, one read at a time. */
|
|
461
|
+
const assemblyRead = coalesced(() => readAssembly());
|
|
462
|
+
function refreshAssembly(): void {
|
|
463
|
+
assemblyRead("assembly");
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** The live runs a frame named that the Assembly was read again for: once for each run, so a run
|
|
467
|
+
* that holds no Assembly does not make a read on each frame. */
|
|
468
|
+
const assemblyAsked = new Set<string>();
|
|
469
|
+
|
|
470
|
+
/** A live run that the page does not know the Assembly of: it started after the last read, maybe
|
|
471
|
+
* on an Assembly published since. Its Machine is drawn from the Assembly it holds, so read again. */
|
|
472
|
+
function heldUnknown(runs: ObservedRun[]): void {
|
|
473
|
+
if (store.assembly.current === null) return; // an Orchestrator with no Assembly marks nothing
|
|
474
|
+
let ask = false;
|
|
475
|
+
for (const r of runs) {
|
|
476
|
+
if (r.status !== "active" || store.assembly.held.has(r.runId) || assemblyAsked.has(r.runId)) continue;
|
|
477
|
+
assemblyAsked.add(r.runId);
|
|
478
|
+
ask = true;
|
|
479
|
+
}
|
|
480
|
+
if (ask) refreshAssembly();
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// ---- The kept conversations (ADR-0023, ADR-0032) ------------------------------------------------
|
|
484
|
+
// The bottom drawer shows the selected run's conversations, one tab for each Agent slot. The list
|
|
485
|
+
// is read on each frame of the run; the entries of the shown slot are read when the drawer is open:
|
|
486
|
+
// a closed conversation as pages, an open one as a stream that follows the Turn. Fetch bookkeeping,
|
|
487
|
+
// not belief: the entries themselves are in the store.
|
|
488
|
+
|
|
489
|
+
/** The run the conversations were last read for. */
|
|
490
|
+
let talkRun: string | null = null;
|
|
491
|
+
/** The open streams, by Instance ID. */
|
|
492
|
+
const followers = new Map<string, AbortController>();
|
|
493
|
+
/** The conversations whose kept pages are read, or are being read. */
|
|
494
|
+
const loaded = new Set<string>();
|
|
495
|
+
/** The conversations whose stream ended and that the list has not said again are open: a stream
|
|
496
|
+
* starts again only after a list read after its end, so a stale `open` does not start a loop. */
|
|
497
|
+
const ended = new Set<string>();
|
|
498
|
+
/** The size of each conversation at the last list read: a closed conversation that grew after the
|
|
499
|
+
* page read it (a batch the Harness sent late) is read again after what the page holds. */
|
|
500
|
+
const rowBytes = new Map<string, number>();
|
|
501
|
+
/** Serial of the list reads of the selected run. */
|
|
502
|
+
let talkSerial = 0;
|
|
503
|
+
|
|
504
|
+
/** The entries the Console asks for in one page of a conversation route (`limit`; at most `CONVERSATION_PAGE_MAX` in http.ts). */
|
|
505
|
+
const ENTRY_PAGE = 1000;
|
|
506
|
+
|
|
507
|
+
function stopFollowers(keep: (instanceId: string) => boolean = () => false): void {
|
|
508
|
+
for (const [iid, ctl] of followers) {
|
|
509
|
+
if (keep(iid)) continue;
|
|
510
|
+
ctl.abort();
|
|
511
|
+
followers.delete(iid);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/** Bring the reads in line with the selection: another run starts from nothing; the same run reads
|
|
516
|
+
* what the shown slot needs. Safe to call on every frame — a read starts only once. */
|
|
517
|
+
function syncTalk(): void {
|
|
518
|
+
const runId = store.selectedRunId;
|
|
519
|
+
if (runId !== talkRun) {
|
|
520
|
+
stopFollowers();
|
|
521
|
+
loaded.clear();
|
|
522
|
+
ended.clear();
|
|
523
|
+
rowBytes.clear();
|
|
524
|
+
talkRun = runId;
|
|
525
|
+
if (runId) void refreshTalk(runId);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
if (store.token !== "live") {
|
|
529
|
+
// A credential that went drops the entries (`store.talk`), so the next live one reads each
|
|
530
|
+
// conversation again.
|
|
531
|
+
loaded.clear();
|
|
532
|
+
ended.clear();
|
|
533
|
+
rowBytes.clear();
|
|
534
|
+
}
|
|
535
|
+
if (!runId || !store.bottom.open || store.token !== "live") return stopFollowers();
|
|
536
|
+
const shown = slotConversations(store, activeSlot(store));
|
|
537
|
+
const ids = new Set(shown.map((c) => c.row.instanceId));
|
|
538
|
+
stopFollowers((iid) => ids.has(iid));
|
|
539
|
+
for (const { row, ended: end } of shown) {
|
|
540
|
+
const iid = row.instanceId;
|
|
541
|
+
if (followers.has(iid) || row.swept || end?.cut || end?.swept) continue;
|
|
542
|
+
if (row.open && !ended.has(iid)) void follow(runId, iid);
|
|
543
|
+
else if (!loaded.has(iid)) void loadConversation(runId, iid);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/** Read the selected run's conversation list (`read`) and its Agent slots for Steering (`steer`,
|
|
548
|
+
* live runs only). The answer for a run the reader left is dropped. */
|
|
549
|
+
async function readTalk(runId: string): Promise<void> {
|
|
550
|
+
const workflow = store.workflow;
|
|
551
|
+
if (!workflow || store.token !== "live") return;
|
|
552
|
+
const serial = ++talkSerial;
|
|
553
|
+
if (may(store, "read", workflow)) {
|
|
554
|
+
try {
|
|
555
|
+
const res = await gfetch(`/runs/${encodeURIComponent(runId)}/conversations`);
|
|
556
|
+
if (res?.ok && serial === talkSerial && store.selectedRunId === runId) {
|
|
557
|
+
const rows = (await res.json()) as ConversationRow[];
|
|
558
|
+
ended.clear();
|
|
559
|
+
for (const row of rows) {
|
|
560
|
+
const before = rowBytes.get(row.instanceId);
|
|
561
|
+
if (!row.open && before !== undefined && row.bytes > before) loaded.delete(row.instanceId);
|
|
562
|
+
rowBytes.set(row.instanceId, row.bytes);
|
|
563
|
+
}
|
|
564
|
+
dispatch({ kind: "conversations", runId, rows });
|
|
565
|
+
}
|
|
566
|
+
} catch {
|
|
567
|
+
// transient — the next frame reads again
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
if (may(store, "steer", workflow) && store.runs.has(runId)) {
|
|
571
|
+
try {
|
|
572
|
+
const res = await gfetch(`/runs/${encodeURIComponent(runId)}/agents`);
|
|
573
|
+
if (res?.ok && store.selectedRunId === runId) {
|
|
574
|
+
dispatch({ kind: "agents", runId, agents: (await res.json()) as AgentLine[] });
|
|
575
|
+
}
|
|
576
|
+
} catch {
|
|
577
|
+
// transient — the next frame reads again
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/** {@link readTalk}, one read for each run at a time: status frames come on every save. */
|
|
583
|
+
const refreshTalk = coalesced(readTalk);
|
|
584
|
+
|
|
585
|
+
/** The last sequence number the page holds of a conversation, or -1. */
|
|
586
|
+
function lastSeq(instanceId: string): number {
|
|
587
|
+
const kept = store.talk?.entries.get(instanceId);
|
|
588
|
+
return kept?.length ? kept[kept.length - 1]!.seq : -1;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/** The conversation route of one Instance ID: the ID is one URL-encoded segment. */
|
|
592
|
+
function conversationPath(runId: string, instanceId: string): string {
|
|
593
|
+
return `/runs/${encodeURIComponent(runId)}/conversations/${encodeURIComponent(instanceId)}`;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/** Read a conversation that is not open, a page at a time, after what the page holds. */
|
|
597
|
+
async function loadConversation(runId: string, instanceId: string): Promise<void> {
|
|
598
|
+
loaded.add(instanceId);
|
|
599
|
+
try {
|
|
600
|
+
for (;;) {
|
|
601
|
+
const res = await gfetch(
|
|
602
|
+
`${conversationPath(runId, instanceId)}?after=${lastSeq(instanceId)}&limit=${ENTRY_PAGE}`,
|
|
603
|
+
);
|
|
604
|
+
if (!res?.ok || store.selectedRunId !== runId) return;
|
|
605
|
+
const entries = (await res.json()) as ConversationEntry[];
|
|
606
|
+
dispatch({ kind: "entries", runId, instanceId, entries });
|
|
607
|
+
if (entries.length < ENTRY_PAGE) return;
|
|
608
|
+
}
|
|
609
|
+
} catch {
|
|
610
|
+
loaded.delete(instanceId); // the next sync reads again
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/** Follow an open conversation (`/events`): what is kept after what the page holds, then each new
|
|
615
|
+
* entry, until the server ends the stream (the Turn closed, or the conversation was cut or swept)
|
|
616
|
+
* or the page stops it. Then the list is read again, so the tab's dot and the next Turn are seen. */
|
|
617
|
+
async function follow(runId: string, instanceId: string): Promise<void> {
|
|
618
|
+
const ctl = new AbortController();
|
|
619
|
+
followers.set(instanceId, ctl);
|
|
620
|
+
let finished = false;
|
|
621
|
+
try {
|
|
622
|
+
const res = await fetch(`${conversationPath(runId, instanceId)}/events?after=${lastSeq(instanceId)}`, {
|
|
623
|
+
headers: { authorization: `Bearer ${token()}`, accept: "text/event-stream" },
|
|
624
|
+
signal: ctl.signal,
|
|
625
|
+
});
|
|
626
|
+
if (res.status === 401) return dispatch({ kind: "token", state: "invalid" });
|
|
627
|
+
if (!res.ok || !res.body) {
|
|
628
|
+
finished = true;
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
|
|
632
|
+
let buffer = "";
|
|
633
|
+
for (;;) {
|
|
634
|
+
const { value, done } = await reader.read();
|
|
635
|
+
if (done) break;
|
|
636
|
+
const { events, rest } = readSse(buffer + value);
|
|
637
|
+
buffer = rest;
|
|
638
|
+
for (const ev of events) {
|
|
639
|
+
if (ev.event === "entry") {
|
|
640
|
+
dispatch({ kind: "entries", runId, instanceId, entries: [JSON.parse(ev.data) as ConversationEntry] });
|
|
641
|
+
} else if (ev.event === "end") {
|
|
642
|
+
const end = JSON.parse(ev.data) as { cut?: boolean; swept?: string };
|
|
643
|
+
dispatch({ kind: "conversationEnd", runId, instanceId, cut: end.cut === true, swept: end.swept });
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
finished = true;
|
|
648
|
+
} catch {
|
|
649
|
+
// aborted by the page, or the connection dropped: the next sync decides again
|
|
650
|
+
} finally {
|
|
651
|
+
if (followers.get(instanceId) === ctl) followers.delete(instanceId);
|
|
652
|
+
if (finished && store.selectedRunId === runId) {
|
|
653
|
+
ended.add(instanceId);
|
|
654
|
+
loaded.add(instanceId);
|
|
655
|
+
void refreshTalk(runId);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
177
658
|
}
|
|
178
659
|
|
|
179
660
|
// ---- The two writes (start-run + gate delivery — ADR-0033) --------------------------------------
|
|
@@ -189,6 +670,7 @@ async function startRun(name: string, body: Record<string, unknown>): Promise<st
|
|
|
189
670
|
if (!res) return "unauthorized — the page dropped to observer";
|
|
190
671
|
if (!res.ok) return ((await res.json()) as { error?: string }).error ?? `HTTP ${res.status}`;
|
|
191
672
|
dispatch({ kind: "startForm", workflow: null });
|
|
673
|
+
refreshInbox(); // a run can open a Gate at its first state
|
|
192
674
|
// The run ANNOUNCES itself: the feed if this workflow is selected, the next snapshot if not —
|
|
193
675
|
// but a reader who just started a run should not wait 10s to see it in the rail.
|
|
194
676
|
if (name !== store.workflow) void snapshotWorkflow(name);
|
|
@@ -209,7 +691,33 @@ async function deliverGate(
|
|
|
209
691
|
if (!res) return "unauthorized — the page dropped to observer";
|
|
210
692
|
if (!res.ok) return ((await res.json()) as { error?: string }).error ?? `HTTP ${res.status}`;
|
|
211
693
|
// Deliberately NO local bookkeeping: the delivery moves the Machine, the move lands a frame, the
|
|
212
|
-
// frame triggers the
|
|
694
|
+
// frame triggers the inbox read, and the read leaves the card out (ADR-0032).
|
|
695
|
+
return null;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// ---- Cancel and steer (ADR-0002, ADR-0078) ------------------------------------------------------
|
|
699
|
+
// Same contract as the two writes above. A cancel lands a frame like any transition; a Steering
|
|
700
|
+
// message moves nothing the page can see, so its answer is the only news of it.
|
|
701
|
+
|
|
702
|
+
async function cancelRun(runId: string): Promise<string | null> {
|
|
703
|
+
const res = await gfetch(`/runs/${encodeURIComponent(runId)}/events`, {
|
|
704
|
+
method: "POST",
|
|
705
|
+
headers: { "content-type": "application/json" },
|
|
706
|
+
body: JSON.stringify({ type: "CANCEL" }),
|
|
707
|
+
});
|
|
708
|
+
if (!res) return "unauthorized — the page dropped to observer";
|
|
709
|
+
if (!res.ok) return ((await res.json()) as { error?: string }).error ?? `HTTP ${res.status}`;
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
async function steer(runId: string, agent: string, text: string): Promise<string | null> {
|
|
714
|
+
const res = await gfetch(`/runs/${encodeURIComponent(runId)}/steering`, {
|
|
715
|
+
method: "POST",
|
|
716
|
+
headers: { "content-type": "application/json" },
|
|
717
|
+
body: JSON.stringify({ agent, text }),
|
|
718
|
+
});
|
|
719
|
+
if (!res) return "unauthorized — the page dropped to observer";
|
|
720
|
+
if (!res.ok) return ((await res.json().catch(() => ({}))) as { error?: string }).error ?? `HTTP ${res.status}`;
|
|
213
721
|
return null;
|
|
214
722
|
}
|
|
215
723
|
|
|
@@ -266,6 +774,7 @@ async function selectWorkflow(
|
|
|
266
774
|
}
|
|
267
775
|
dispatch({ kind: "select", workflow: name }); // the dispatch starts the doc's fetch (syncMachine)
|
|
268
776
|
if (runId) dispatch({ kind: "selectRun", runId });
|
|
777
|
+
if (moved && name) void refreshHistory(name);
|
|
269
778
|
if (!reconnect || !name) return;
|
|
270
779
|
connectFeed(name);
|
|
271
780
|
await syncMachine();
|
|
@@ -299,19 +808,29 @@ async function fetchDoc(source: { key: string; path: string }): Promise<MachineD
|
|
|
299
808
|
async function syncMachine(): Promise<void> {
|
|
300
809
|
const name = store.workflow;
|
|
301
810
|
if (!name) return;
|
|
302
|
-
|
|
303
|
-
|
|
811
|
+
// A drifted run has no Machine that this Orchestrator can read (ADR-0030): say why, and draw none.
|
|
812
|
+
const drift = selectedRunDrift(store);
|
|
813
|
+
if (drift !== null) {
|
|
814
|
+
const key = `drift:${store.selectedRunId}`;
|
|
815
|
+
if (shownKey === key) return;
|
|
816
|
+
shownKey = key;
|
|
817
|
+
view = { doc: null, placeholder: null, note: { text: `drifted: ${drift}`, notice: true } };
|
|
818
|
+
rerender();
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
const source = selectedDocSource(store);
|
|
822
|
+
if (!source || source.key === shownKey) return;
|
|
304
823
|
shownKey = source.key;
|
|
305
824
|
let doc = await fetchDoc(source);
|
|
306
825
|
if (shownKey !== source.key) return; // the reader moved on while the fetch was out
|
|
307
826
|
let note: MachineView["note"] = null;
|
|
308
827
|
if (!doc && source.key.startsWith("run:")) {
|
|
309
828
|
// The run's own Machine could not be read (an Orchestrator older than the route, or the run
|
|
310
|
-
// left the store): draw the Workflow's, and say the
|
|
311
|
-
doc = await fetchDoc(machineDocSource(name, null));
|
|
829
|
+
// left the store): draw the Workflow's, and say the run's own states are not in it.
|
|
830
|
+
doc = await fetchDoc(machineDocSource(name, null, false, store.assembly.current));
|
|
312
831
|
if (shownKey !== source.key) return;
|
|
313
832
|
note = {
|
|
314
|
-
text: "this run
|
|
833
|
+
text: "this run's own Machine could not be read: this is the current Machine, and the run's states can differ",
|
|
315
834
|
notice: true,
|
|
316
835
|
};
|
|
317
836
|
}
|
|
@@ -365,20 +884,30 @@ function connectFeed(name: string): void {
|
|
|
365
884
|
feed.addEventListener("runs", (e) => {
|
|
366
885
|
const runs = JSON.parse((e as MessageEvent<string>).data) as ObservedRun[];
|
|
367
886
|
dispatch({ kind: "runs", runs });
|
|
368
|
-
|
|
887
|
+
heldUnknown(runs);
|
|
888
|
+
refreshInbox();
|
|
889
|
+
readSelected();
|
|
890
|
+
if (store.selectedRunId) void refreshTalk(store.selectedRunId);
|
|
369
891
|
});
|
|
370
892
|
feed.addEventListener("status", (e) => {
|
|
371
893
|
const status = JSON.parse((e as MessageEvent<string>).data) as ObservedRun;
|
|
372
894
|
dispatch({ kind: "status", status });
|
|
373
|
-
|
|
895
|
+
heldUnknown([status]);
|
|
896
|
+
refreshInbox();
|
|
897
|
+
if (status.runId === store.selectedRunId) void refreshRun(status.runId, name);
|
|
898
|
+
// A Turn opens and closes with a state entry and exit: the slot's dot and its stream follow.
|
|
899
|
+
if (status.runId === store.selectedRunId) void refreshTalk(status.runId);
|
|
374
900
|
});
|
|
375
901
|
feed.addEventListener("gone", (e) => {
|
|
376
902
|
const { runId } = JSON.parse((e as MessageEvent<string>).data) as { runId: string };
|
|
377
903
|
dispatch({ kind: "gone", runId });
|
|
378
904
|
// The reducer already dropped the card (a gate exists only while its state is entered —
|
|
379
|
-
// ADR-0011); the
|
|
380
|
-
//
|
|
381
|
-
|
|
905
|
+
// ADR-0011); the inbox read is the same frame-triggered discipline as `runs`/`status`. The run
|
|
906
|
+
// is an ended run now: the rail's list of them is read again.
|
|
907
|
+
refreshInbox();
|
|
908
|
+
if (runId === store.selectedRunId) void refreshRun(runId, name);
|
|
909
|
+
void refreshHistory(name);
|
|
910
|
+
if (runId === store.selectedRunId) void refreshTalk(runId);
|
|
382
911
|
});
|
|
383
912
|
feed.addEventListener("emit", (e) => {
|
|
384
913
|
const { runId, type } = JSON.parse((e as MessageEvent<string>).data) as { runId: string; type: string };
|
|
@@ -390,8 +919,9 @@ function connectFeed(name: string): void {
|
|
|
390
919
|
|
|
391
920
|
// ---- Fleet snapshots ----------------------------------------------------------------------------
|
|
392
921
|
// The unselected workflows have no feed — never more than one EventSource. They get REST snapshots
|
|
393
|
-
// (`GET /workflows/:name/runs`) on an interval and on tab focus, folded through the same reducer
|
|
394
|
-
//
|
|
922
|
+
// (`GET /workflows/:name/runs`) on an interval and on tab focus, folded through the same reducer.
|
|
923
|
+
// Each round reads the inbox once more (it is fleet-wide), the Assembly, and the selection's ended
|
|
924
|
+
// runs and conversations.
|
|
395
925
|
|
|
396
926
|
const SNAPSHOT_MS = 10_000;
|
|
397
927
|
|
|
@@ -401,7 +931,6 @@ async function snapshotWorkflow(name: string): Promise<void> {
|
|
|
401
931
|
if (!res.ok) return;
|
|
402
932
|
const runs = (await res.json()) as ObservedRun[];
|
|
403
933
|
dispatch({ kind: "fleet", workflow: name, runs });
|
|
404
|
-
for (const r of runs) void refreshRun(r.runId, name);
|
|
405
934
|
} catch {
|
|
406
935
|
// transient — the next tick tries again
|
|
407
936
|
}
|
|
@@ -414,6 +943,10 @@ async function snapshotFleet(): Promise<void> {
|
|
|
414
943
|
} catch {
|
|
415
944
|
// transient — the next tick tries again
|
|
416
945
|
}
|
|
946
|
+
void refreshAssembly();
|
|
947
|
+
refreshInbox();
|
|
948
|
+
if (store.workflow) void refreshHistory(store.workflow);
|
|
949
|
+
if (store.selectedRunId) void refreshTalk(store.selectedRunId);
|
|
417
950
|
for (const name of store.workflows) {
|
|
418
951
|
if (name === store.workflow) continue; // the selection's feed is fresher than any snapshot
|
|
419
952
|
await snapshotWorkflow(name);
|
|
@@ -429,7 +962,10 @@ const api: AppApi = {
|
|
|
429
962
|
toggleStartForm,
|
|
430
963
|
startRun,
|
|
431
964
|
deliverGate,
|
|
965
|
+
cancelRun,
|
|
966
|
+
steer,
|
|
432
967
|
setToken,
|
|
968
|
+
login: (issuer) => void login(issuer),
|
|
433
969
|
onScale: (pct) => {
|
|
434
970
|
if (pct === zoomPct) return;
|
|
435
971
|
zoomPct = pct;
|
|
@@ -445,7 +981,10 @@ async function boot(): Promise<void> {
|
|
|
445
981
|
addEventListener("popstate", () => void selectWorkflow(workflowFromPath(), { push: false }));
|
|
446
982
|
addEventListener("focus", () => void snapshotFleet());
|
|
447
983
|
setInterval(() => void snapshotFleet(), SNAPSHOT_MS);
|
|
984
|
+
// A return from the issuer first: it puts the address back, which the selection below reads.
|
|
985
|
+
await finishLogin();
|
|
448
986
|
rerender(); // the shell first — the fetches below fill it in
|
|
987
|
+
void loadLogins().then(rerender);
|
|
449
988
|
|
|
450
989
|
// The fleet first (the rail names everything), then the token (unlock is async and must not gate
|
|
451
990
|
// observation), then the selection the address carries — deep links work on load.
|
|
@@ -456,6 +995,7 @@ async function boot(): Promise<void> {
|
|
|
456
995
|
// the interval retries; the rail says "no workflows registered" until then
|
|
457
996
|
}
|
|
458
997
|
if (token()) void validateToken();
|
|
998
|
+
void refreshAssembly();
|
|
459
999
|
await selectWorkflow(workflowFromPath(), { push: false });
|
|
460
1000
|
void snapshotFleet();
|
|
461
1001
|
}
|