@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/store.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// What the page BELIEVES, kept apart from what it has painted (canvas.ts's `shown`/`queue`).
|
|
2
2
|
//
|
|
3
3
|
// A pure reducer over everything the Console hears — the selected workflow's feed frames
|
|
4
|
-
// (ADR-0022), fleet snapshots,
|
|
5
|
-
// selections — no DOM, no fetch, no module state — so the interesting cases are testable under
|
|
4
|
+
// (ADR-0022), fleet snapshots, the Gate inbox (ADR-0032), the ended runs, the kept conversations,
|
|
5
|
+
// the token's state, the reader's own selections — no DOM, no fetch, no module state — so the interesting cases are testable under
|
|
6
6
|
// `node:test` with no jsdom and no build step. The renderer downstream is already a pure function
|
|
7
7
|
// of (doc, status); this is the other half.
|
|
8
8
|
//
|
|
@@ -12,9 +12,11 @@
|
|
|
12
12
|
// The feed is LEVEL-TRIGGERED: every `status` carries a whole observation and `runs` carries the
|
|
13
13
|
// whole live set, so nothing here merges patches or replays a log. Re-delivery of any frame is
|
|
14
14
|
// idempotent by construction, which is the entire reconnect story — there is no cursor to resume
|
|
15
|
-
// from and no gap to detect. The
|
|
16
|
-
// is a whole
|
|
17
|
-
// never bookkeeping this side did after a delivery (ADR-0032).
|
|
15
|
+
// from and no gap to detect. The Gate inbox rides the same idiom one band up: every `inbox` frame
|
|
16
|
+
// is a whole read of `GET /gates`, every open Gate the Caller may answer, so an emptied card is a
|
|
17
|
+
// fact the server stated, never bookkeeping this side did after a delivery (ADR-0032).
|
|
18
|
+
|
|
19
|
+
import { mergeEntries, type AgentLine, type ConversationEntry, type ConversationRow } from "./conversation.ts";
|
|
18
20
|
|
|
19
21
|
/** A run as the OPEN observation band reports it (ADR-0014): identity and where it is, nothing of
|
|
20
22
|
* what it is carrying. Mirrors `RunObservation` in run-host.ts. */
|
|
@@ -75,12 +77,49 @@ export type FleetEntry = {
|
|
|
75
77
|
shadows?: { origin: WorkflowOrigin; unrunnable?: string };
|
|
76
78
|
};
|
|
77
79
|
|
|
78
|
-
/** One open gate as `GET /
|
|
80
|
+
/** One open gate as `GET /gates` reports it — mirrors `GateView` in run-host.ts, with the time it
|
|
81
|
+
* opened. */
|
|
79
82
|
export type GateCard = {
|
|
80
83
|
gate: string;
|
|
81
84
|
path: string[];
|
|
82
85
|
accepts: Array<{ name: string; description?: string; input: unknown }>;
|
|
86
|
+
acceptsNow?: string[];
|
|
83
87
|
meta?: Record<string, unknown>;
|
|
88
|
+
openedAt?: string;
|
|
89
|
+
/** The Hook kinds that this Gate hears (ADR-0011). */
|
|
90
|
+
hook?: string[];
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/** One entry of `GET /gates` (ADR-0011): an open Gate that the Caller may answer, with its run —
|
|
94
|
+
* mirrors `GateInboxEntry` in run-host.ts. Never the run's context. */
|
|
95
|
+
export type InboxGate = GateCard & { runId: string; workflow: string };
|
|
96
|
+
|
|
97
|
+
/** One run of `GET /runs` (ADR-0009) — mirrors `RunSummary` in run-list.ts. The Console lists the
|
|
98
|
+
* ended ones of the selected Workflow under its live runs. */
|
|
99
|
+
export type RunSummaryView = {
|
|
100
|
+
runId: string;
|
|
101
|
+
workflow: string;
|
|
102
|
+
status: string;
|
|
103
|
+
/** Why a run is `drifted` or `lost`, a Failure's reason, or a fault (ADR-0030, ADR-0073). */
|
|
104
|
+
reason?: string;
|
|
105
|
+
startedAt?: string;
|
|
106
|
+
endedAt?: string;
|
|
107
|
+
/** The hash of the Assembly the run started under (ADR-0069). */
|
|
108
|
+
assembly?: string;
|
|
109
|
+
openGates?: number;
|
|
110
|
+
dispatchedBy?: DispatchedByView;
|
|
111
|
+
swept?: string;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/** One row of a Workflow's run list in the rail: a live run, a run that this page watched end, or
|
|
115
|
+
* an ended run that `GET /runs` lists. */
|
|
116
|
+
export type RailRun = {
|
|
117
|
+
runId: string;
|
|
118
|
+
status: string;
|
|
119
|
+
/** The run is not live. */
|
|
120
|
+
settled: boolean;
|
|
121
|
+
reason?: string;
|
|
122
|
+
dispatchedBy?: DispatchedByView;
|
|
84
123
|
};
|
|
85
124
|
|
|
86
125
|
/** One Workspace waiting for capacity as `GET /runs/:id` reports it (ADR-0064) — mirrors
|
|
@@ -88,13 +127,41 @@ export type GateCard = {
|
|
|
88
127
|
export type WaitingLine = { child: string; on: "node" | "quota"; message: string; since: string };
|
|
89
128
|
|
|
90
129
|
/** One outstanding pick as `GET /runs/:id` reports it (ADR-0024) — mirrors `RunHeld` in
|
|
91
|
-
* run-host.ts. There is no default time bound on a held Turn: "
|
|
130
|
+
* run-host.ts. There is no default time bound on a held Turn: "outstanding since" is how a human
|
|
131
|
+
* sees one.
|
|
92
132
|
* Guarded like the reason a Workspace waits: a pick names what an Agent did. */
|
|
93
133
|
export type HeldLine = { agent: string; pick: string; since: string };
|
|
94
134
|
|
|
95
135
|
/** The credential's state — never its value, which stays in sessionStorage (ADR-0032). */
|
|
96
136
|
export type TokenState = "none" | "checking" | "live" | "invalid";
|
|
97
137
|
|
|
138
|
+
/** One scope of a Caller (ADR-0078): the five acts. Mirrors `Scope` in config.ts. */
|
|
139
|
+
export type ScopeName = "start" | "answer" | "cancel" | "read" | "steer";
|
|
140
|
+
|
|
141
|
+
/** The Workflows a scope opens: every one, or the named ones. Mirrors `ScopeGrant` in config.ts. */
|
|
142
|
+
export type ScopeGrant = "all" | readonly string[];
|
|
143
|
+
|
|
144
|
+
/** What the `answer` scope opens (ADR-0078): every Gate, or a list of Workflow names and of
|
|
145
|
+
* `{ workflow, gates }` items that name single Gates. Mirrors `AnswerGrant` in config.ts. */
|
|
146
|
+
export type AnswerGrant = "all" | ReadonlyArray<string | { workflow: string; gates: readonly string[] }>;
|
|
147
|
+
|
|
148
|
+
/** The page's Caller as `GET /whoami` gives it (ADR-0078) — mirrors `Caller` in tokens.ts. What it
|
|
149
|
+
* `may` do decides which controls render (ADR-0032); the server still checks each request. */
|
|
150
|
+
export type CallerView = {
|
|
151
|
+
via: string;
|
|
152
|
+
name: string;
|
|
153
|
+
/** The name that jr2 shows, when it is not `name` (a JWT's `email`). */
|
|
154
|
+
shown?: string;
|
|
155
|
+
onBehalfOf?: string;
|
|
156
|
+
may: Partial<Record<Exclude<ScopeName, "answer">, ScopeGrant>> & { answer?: AnswerGrant };
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/** Who started a run, as `GET /runs/:id` gives it (ADR-0078) — mirrors `RunStarter` in callers.ts. */
|
|
160
|
+
export type StarterView =
|
|
161
|
+
| { kind: "caller"; via: string; name: string; shown?: string; onBehalfOf?: string }
|
|
162
|
+
| { kind: "run"; run: string; slot: string }
|
|
163
|
+
| { kind: "jr2"; why: string };
|
|
164
|
+
|
|
98
165
|
/** One frame folded into the store: the workflow feed's wire frames (ADR-0022), plus the page's
|
|
99
166
|
* own facts — fleet snapshots, gate re-fetches, token state, the reader's selections (ADR-0032). */
|
|
100
167
|
export type Frame =
|
|
@@ -110,9 +177,19 @@ export type Frame =
|
|
|
110
177
|
| { kind: "fleet"; workflow: string; runs: ObservedRun[] }
|
|
111
178
|
| { kind: "toggleWorkflow"; workflow: string }
|
|
112
179
|
| { kind: "token"; state: TokenState }
|
|
113
|
-
| { kind: "
|
|
180
|
+
| { kind: "caller"; caller: CallerView }
|
|
181
|
+
| { kind: "inbox"; gates: InboxGate[] }
|
|
182
|
+
| { kind: "history"; workflow: string; runs: RunSummaryView[] }
|
|
183
|
+
| { kind: "endedStatus"; status: ObservedRun }
|
|
184
|
+
| { kind: "assembly"; current: string | null; held: Array<{ hash: string; runs: string[] }> }
|
|
185
|
+
| { kind: "conversations"; runId: string; rows: ConversationRow[] }
|
|
186
|
+
| { kind: "agents"; runId: string; agents: AgentLine[] }
|
|
187
|
+
| { kind: "entries"; runId: string; instanceId: string; entries: ConversationEntry[] }
|
|
188
|
+
| { kind: "conversationEnd"; runId: string; instanceId: string; cut: boolean; swept?: string }
|
|
189
|
+
| { kind: "bottom"; open?: boolean; slot?: string | null }
|
|
114
190
|
| { kind: "waiting"; runId: string; workflow: string; waiting: WaitingLine[]; held?: HeldLine[] }
|
|
115
191
|
| { kind: "failure"; runId: string; reason: string | null }
|
|
192
|
+
| { kind: "starter"; runId: string; startedBy: StarterView | null }
|
|
116
193
|
| { kind: "inboxScope"; all: boolean }
|
|
117
194
|
| { kind: "startForm"; workflow: string | null };
|
|
118
195
|
|
|
@@ -137,16 +214,35 @@ export type Frame =
|
|
|
137
214
|
* `token` the credential's STATE, never its value — the secret stays in sessionStorage
|
|
138
215
|
* (ADR-0032), outside anything a test would snapshot. "none" | "checking" | "live" |
|
|
139
216
|
* "invalid". Anything but "live" means observer mode.
|
|
140
|
-
* `gates` the
|
|
141
|
-
*
|
|
217
|
+
* `gates` the Gate inbox: runId -> { workflow, gates }, from `GET /gates` (ADR-0011), which lists
|
|
218
|
+
* only the Gates that the Caller may answer. Each read replaces it whole. Guarded data,
|
|
219
|
+
* so it cannot outlive the credential that read it.
|
|
142
220
|
* `waiting` why each run's Workspaces in `placing` wait (ADR-0064): runId -> { workflow, waiting },
|
|
143
|
-
* off
|
|
221
|
+
* off a `GET /runs/:id` re-fetch, under the rules of `gates` — guarded, whole
|
|
144
222
|
* per run, gone with the credential. The open band sees `placing` lit and nothing more.
|
|
145
223
|
* `failures` the reason of each run this page knows that ended `failed` (ADR-0073): runId -> reason,
|
|
146
224
|
* off the SAME `GET /runs/:id` re-fetch, under the same rules — guarded (the open band
|
|
147
225
|
* sees the `failed` status and its color, never the output), gone with the credential.
|
|
148
226
|
* A failed run is a settled run, so `gone` keeps it; it goes with the run when the run
|
|
149
227
|
* leaves both `runs` and `settled`.
|
|
228
|
+
* `caller` the page's Caller as `GET /whoami` answered it (ADR-0078): who the page acts as, and
|
|
229
|
+
* what it may do. Null without a live credential; gone with it. The token's VALUE is
|
|
230
|
+
* never here — this is what the server said the value is.
|
|
231
|
+
* `starters` who started each run this page knows (ADR-0078): runId -> starter, off the SAME
|
|
232
|
+
* `GET /runs/:id` re-fetch as `failures`, under the same rules — guarded, gone with the
|
|
233
|
+
* credential, gone with the run.
|
|
234
|
+
* `opened` the last status of each ended run that the person opened from the rail and that this
|
|
235
|
+
* page did not watch end (`GET /runs/:id`, ADR-0032). Guarded (`read`): gone with the
|
|
236
|
+
* credential, with a change of Caller and with the selection. At most {@link SETTLED_CAP}.
|
|
237
|
+
* `history` the selected Workflow's ended runs, from `GET /runs?status=ended&workflow=` (ADR-0009):
|
|
238
|
+
* what the rail lists under the live runs. Needs `read`; guarded, gone with the
|
|
239
|
+
* credential and with the selection.
|
|
240
|
+
* `assembly` the current Assembly's hash and the hash each live run holds (`GET /assembly`, open):
|
|
241
|
+
* what marks a run "older Assembly" (ADR-0069).
|
|
242
|
+
* `talk` the SELECTED run's kept conversations (ADR-0023): its rows, its Agent slots for
|
|
243
|
+
* Steering, and the entries read so far by Instance ID. Guarded, and gone with the
|
|
244
|
+
* credential or when the selection moves to another run.
|
|
245
|
+
* `bottom` the bottom drawer: open or closed, and the Agent slot it shows (null: the first).
|
|
150
246
|
* `inboxAll` the widen control: false = the inbox follows the selection, true = every workflow.
|
|
151
247
|
* `startFormFor` which workflow's start-run form is open, if any.
|
|
152
248
|
* `selectedNodeId` the diagram node the reader clicked — an elk node id (scope + state id), one at
|
|
@@ -158,6 +254,7 @@ export type Frame =
|
|
|
158
254
|
export type Store = {
|
|
159
255
|
runs: Map<string, ObservedRun>;
|
|
160
256
|
settled: Map<string, ObservedRun>;
|
|
257
|
+
opened: Map<string, ObservedRun>;
|
|
161
258
|
emits: Array<{ runId: string; type: string }>;
|
|
162
259
|
selectedRunId: string | null;
|
|
163
260
|
selectedNodeId: string | null;
|
|
@@ -171,10 +268,26 @@ export type Store = {
|
|
|
171
268
|
gates: Map<string, { workflow: string; gates: GateCard[] }>;
|
|
172
269
|
waiting: Map<string, { workflow: string; waiting: WaitingLine[]; held: HeldLine[] }>;
|
|
173
270
|
failures: Map<string, string>;
|
|
271
|
+
caller: CallerView | null;
|
|
272
|
+
starters: Map<string, StarterView>;
|
|
273
|
+
history: { workflow: string; runs: RunSummaryView[] } | null;
|
|
274
|
+
assembly: { current: string | null; held: Map<string, string> };
|
|
275
|
+
talk: Talk | null;
|
|
276
|
+
bottom: { open: boolean; slot: string | null };
|
|
174
277
|
inboxAll: boolean;
|
|
175
278
|
startFormFor: string | null;
|
|
176
279
|
};
|
|
177
280
|
|
|
281
|
+
/** The selected run's kept conversations (see {@link Store}). `ended` holds each conversation whose
|
|
282
|
+
* stream ended, and whether it was cut or swept. */
|
|
283
|
+
export type Talk = {
|
|
284
|
+
runId: string;
|
|
285
|
+
rows: ConversationRow[];
|
|
286
|
+
agents: AgentLine[];
|
|
287
|
+
entries: Map<string, ConversationEntry[]>;
|
|
288
|
+
ended: Map<string, { cut: boolean; swept?: string }>;
|
|
289
|
+
};
|
|
290
|
+
|
|
178
291
|
/** How many finished runs stay on the page. Enough to see what just happened, bounded so a page
|
|
179
292
|
* left open for a week does not grow without end. */
|
|
180
293
|
export const SETTLED_CAP = 20;
|
|
@@ -187,6 +300,7 @@ export function emptyStore(): Store {
|
|
|
187
300
|
return {
|
|
188
301
|
runs: new Map(),
|
|
189
302
|
settled: new Map(),
|
|
303
|
+
opened: new Map(),
|
|
190
304
|
emits: [],
|
|
191
305
|
selectedRunId: null,
|
|
192
306
|
selectedNodeId: null,
|
|
@@ -200,31 +314,47 @@ export function emptyStore(): Store {
|
|
|
200
314
|
gates: new Map(),
|
|
201
315
|
waiting: new Map(),
|
|
202
316
|
failures: new Map(),
|
|
317
|
+
caller: null,
|
|
318
|
+
starters: new Map(),
|
|
319
|
+
history: null,
|
|
320
|
+
assembly: { current: null, held: new Map() },
|
|
321
|
+
talk: null,
|
|
322
|
+
bottom: { open: false, slot: null },
|
|
203
323
|
inboxAll: false,
|
|
204
324
|
startFormFor: null,
|
|
205
325
|
};
|
|
206
326
|
}
|
|
207
327
|
|
|
208
328
|
/** Fold one frame into the store, returning a new one. Unknown frame kinds are ignored, so a server
|
|
209
|
-
* that learns a new frame type does not break a page still running yesterday's script.
|
|
329
|
+
* that learns a new frame type does not break a page still running yesterday's script. The kept
|
|
330
|
+
* conversations belong to the selected run: when a frame moves the selection to another run, they
|
|
331
|
+
* go. */
|
|
210
332
|
export function applyFrame(store: Store, frame: Frame): Store {
|
|
333
|
+
const next = foldFrame(store, frame);
|
|
334
|
+
if (next.talk && next.talk.runId !== next.selectedRunId) return { ...next, talk: null };
|
|
335
|
+
return next;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function foldFrame(store: Store, frame: Frame): Store {
|
|
211
339
|
switch (frame.kind) {
|
|
212
340
|
case "runs": {
|
|
213
341
|
// The server's whole truth about what is live. Runs that ended during a disconnect are simply
|
|
214
342
|
// ABSENT — no `gone` was delivered for them and none is needed; they were never ours to settle.
|
|
215
|
-
// The same wholeness settles the
|
|
216
|
-
// never be re-fetched again, so its
|
|
217
|
-
//
|
|
343
|
+
// The same wholeness settles the waits: a selected-workflow run the frame does not mention can
|
|
344
|
+
// never be re-fetched again, so its waits leave with it. A failure stays while its run is
|
|
345
|
+
// live, settled here or listed as ended.
|
|
218
346
|
const runs = new Map(frame.runs.map((r): [string, ObservedRun] => [r.runId, r]));
|
|
219
|
-
const
|
|
347
|
+
const orphan = (id: string): boolean => !runs.has(id) && !store.settled.has(id) && !inHistory(store, id);
|
|
220
348
|
let failures = store.failures;
|
|
221
|
-
for (const id of
|
|
349
|
+
for (const id of [...failures.keys()].filter(orphan)) failures = mapWithout(failures, id);
|
|
350
|
+
let starters = store.starters;
|
|
351
|
+
for (const id of [...starters.keys()].filter(orphan)) starters = mapWithout(starters, id);
|
|
222
352
|
return select({
|
|
223
353
|
...store,
|
|
224
354
|
runs,
|
|
225
|
-
gates: dropAbsent(store.gates, store.workflow, new Set(frame.runs.map((r) => r.runId))),
|
|
226
355
|
waiting: dropAbsent(store.waiting, store.workflow, new Set(frame.runs.map((r) => r.runId))),
|
|
227
356
|
failures,
|
|
357
|
+
starters,
|
|
228
358
|
});
|
|
229
359
|
}
|
|
230
360
|
|
|
@@ -242,14 +372,7 @@ export function applyFrame(store: Store, frame: Frame): Store {
|
|
|
242
372
|
if (!departing) return store.gates === gates && store.waiting === waiting ? store : { ...store, gates, waiting };
|
|
243
373
|
const runs = new Map(store.runs);
|
|
244
374
|
runs.delete(frame.runId);
|
|
245
|
-
|
|
246
|
-
let failures = store.failures;
|
|
247
|
-
while (settled.size > SETTLED_CAP) {
|
|
248
|
-
const evicted = settled.keys().next().value!;
|
|
249
|
-
settled.delete(evicted);
|
|
250
|
-
failures = mapWithout(failures, evicted);
|
|
251
|
-
}
|
|
252
|
-
return select({ ...store, runs, settled, gates, waiting, failures });
|
|
375
|
+
return select(settle({ ...store, runs, gates, waiting }, departing));
|
|
253
376
|
}
|
|
254
377
|
|
|
255
378
|
case "emit":
|
|
@@ -281,12 +404,17 @@ export function applyFrame(store: Store, frame: Frame): Store {
|
|
|
281
404
|
expanded,
|
|
282
405
|
runs: new Map(),
|
|
283
406
|
settled: new Map(),
|
|
407
|
+
opened: new Map(),
|
|
284
408
|
emits: [],
|
|
285
409
|
selectedRunId: null,
|
|
286
410
|
selectedNodeId: null,
|
|
287
411
|
connection: "connecting",
|
|
288
|
-
// Each failure belongs to a run of the old selection's `runs` or `settled`,
|
|
412
|
+
// Each failure and starter belongs to a run of the old selection's `runs` or `settled`,
|
|
413
|
+
// which go; so does its list of ended runs.
|
|
289
414
|
failures: new Map(),
|
|
415
|
+
starters: new Map(),
|
|
416
|
+
history: null,
|
|
417
|
+
bottom: { ...store.bottom, slot: null },
|
|
290
418
|
};
|
|
291
419
|
}
|
|
292
420
|
|
|
@@ -302,12 +430,11 @@ export function applyFrame(store: Store, frame: Frame): Store {
|
|
|
302
430
|
|
|
303
431
|
case "fleet": {
|
|
304
432
|
// One unselected workflow's REST snapshot — the same wholeness contract as a `runs` frame,
|
|
305
|
-
// so it settles that workflow's
|
|
306
|
-
//
|
|
433
|
+
// so it settles that workflow's waits the same way (a run the snapshot does not carry will
|
|
434
|
+
// never be re-fetched again).
|
|
307
435
|
const fleet = new Map(store.fleet).set(frame.workflow, frame.runs);
|
|
308
436
|
const present = new Set(frame.runs.map((r) => r.runId));
|
|
309
|
-
|
|
310
|
-
return { ...store, fleet, gates, waiting: dropAbsent(store.waiting, frame.workflow, present) };
|
|
437
|
+
return { ...store, fleet, waiting: dropAbsent(store.waiting, frame.workflow, present) };
|
|
311
438
|
}
|
|
312
439
|
|
|
313
440
|
case "toggleWorkflow": {
|
|
@@ -320,24 +447,152 @@ export function applyFrame(store: Store, frame: Frame): Store {
|
|
|
320
447
|
// Guarded data cannot outlive the credential that read it: leaving "live" for "none" or
|
|
321
448
|
// "invalid" (the any-later-401 drop of ADR-0032) empties the inbox. "checking" keeps it — a
|
|
322
449
|
// re-validation of a token that turns out fine should not flash the inbox empty.
|
|
450
|
+
// The Caller goes with it: a page with no credential acts as nobody. A selected run that only
|
|
451
|
+
// the guarded listing named goes too.
|
|
323
452
|
if (frame.state === "none" || frame.state === "invalid") {
|
|
324
|
-
return {
|
|
453
|
+
return select({
|
|
454
|
+
...store,
|
|
455
|
+
token: frame.state,
|
|
456
|
+
gates: new Map(),
|
|
457
|
+
waiting: new Map(),
|
|
458
|
+
failures: new Map(),
|
|
459
|
+
caller: null,
|
|
460
|
+
starters: new Map(),
|
|
461
|
+
history: null,
|
|
462
|
+
opened: new Map(),
|
|
463
|
+
talk: null,
|
|
464
|
+
});
|
|
325
465
|
}
|
|
326
466
|
return { ...store, token: frame.state };
|
|
327
467
|
|
|
328
|
-
case "
|
|
329
|
-
//
|
|
330
|
-
//
|
|
331
|
-
//
|
|
332
|
-
|
|
333
|
-
|
|
468
|
+
case "caller": {
|
|
469
|
+
// What `GET /whoami` said the credential is (ADR-0078). It comes before the `token` frame
|
|
470
|
+
// that makes the page live, so the first live render already knows which controls to show.
|
|
471
|
+
// A card that the new Caller's `answer` does not open goes, as the `gates` fold would refuse it.
|
|
472
|
+
const next = {
|
|
473
|
+
...store,
|
|
474
|
+
caller: frame.caller,
|
|
475
|
+
gates: keepWhere(store.gates, (entry) => grantOpens(frame.caller?.may.answer, entry.workflow)),
|
|
476
|
+
};
|
|
477
|
+
if (sameCaller(store.caller, frame.caller)) return next;
|
|
478
|
+
// Another Caller: what the old one read does not belong to the new one (the page reads it
|
|
479
|
+
// again for the new one when it goes live). The selection stays only where the page can
|
|
480
|
+
// still say something about it.
|
|
481
|
+
return select({
|
|
482
|
+
...next,
|
|
483
|
+
waiting: new Map(),
|
|
484
|
+
failures: new Map(),
|
|
485
|
+
starters: new Map(),
|
|
486
|
+
history: null,
|
|
487
|
+
opened: new Map(),
|
|
488
|
+
talk: null,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
case "inbox": {
|
|
493
|
+
// A whole read of `GET /gates` (ADR-0011, ADR-0032): every open Gate that the Caller may
|
|
494
|
+
// answer, oldest first. It replaces the inbox, so a card that is not in it LEAVES: that is
|
|
495
|
+
// how a delivery's success shows, and the only way it does. No credential, no inbox: a read
|
|
496
|
+
// that was in flight when the token dropped must not fill the map the `token` frame just
|
|
497
|
+
// emptied (guarded data outliving the credential that read it).
|
|
334
498
|
if (store.token !== "live") return store;
|
|
335
|
-
|
|
336
|
-
const
|
|
337
|
-
|
|
499
|
+
const gates = new Map<string, { workflow: string; gates: GateCard[] }>();
|
|
500
|
+
for (const { runId, workflow, ...card } of frame.gates) {
|
|
501
|
+
// The server lists only what the Caller may answer; a card is a send form, so the page
|
|
502
|
+
// checks the Workflow again and holds no form that would meet a 403 (ADR-0032).
|
|
503
|
+
if (!may(store, "answer", workflow)) continue;
|
|
504
|
+
const entry = gates.get(runId) ?? { workflow, gates: [] };
|
|
505
|
+
entry.gates.push(card);
|
|
506
|
+
gates.set(runId, entry);
|
|
507
|
+
}
|
|
338
508
|
return { ...store, gates };
|
|
339
509
|
}
|
|
340
510
|
|
|
511
|
+
case "history": {
|
|
512
|
+
// The selected Workflow's ended runs (`GET /runs?status=ended`, ADR-0009): whole, newest
|
|
513
|
+
// first. Guarded (`read`), and only for the Workflow the page still shows.
|
|
514
|
+
if (store.token !== "live" || frame.workflow !== store.workflow || !may(store, "read", frame.workflow)) {
|
|
515
|
+
return store;
|
|
516
|
+
}
|
|
517
|
+
const live = new Set(store.runs.keys());
|
|
518
|
+
return select({
|
|
519
|
+
...store,
|
|
520
|
+
history: { workflow: frame.workflow, runs: frame.runs.filter((r) => !live.has(r.runId)) },
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
case "endedStatus": {
|
|
525
|
+
// The last status of an ended run that the person opened from the rail (ADR-0032): read off
|
|
526
|
+
// `GET /runs/:id`, held with the runs this page watched end, so the graph lights where the run
|
|
527
|
+
// ended. Not for a live run (the feed speaks for it), and not for another Workflow's run.
|
|
528
|
+
const status = frame.status;
|
|
529
|
+
if (store.token !== "live" || store.runs.has(status.runId) || status.workflow !== store.workflow) return store;
|
|
530
|
+
// A run this page watched end: its last status joins what the open feed said of it.
|
|
531
|
+
if (store.settled.has(status.runId)) return select(settle(store, status));
|
|
532
|
+
// A run only the guarded listing names: held apart, so it goes with the credential.
|
|
533
|
+
if (!inHistory(store, status.runId) || !may(store, "read", status.workflow)) return store;
|
|
534
|
+
const opened = new Map(store.opened);
|
|
535
|
+
opened.delete(status.runId);
|
|
536
|
+
opened.set(status.runId, status);
|
|
537
|
+
while (opened.size > SETTLED_CAP) opened.delete(opened.keys().next().value!);
|
|
538
|
+
return select({ ...store, opened });
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
case "assembly": {
|
|
542
|
+
// Open structure (`GET /assembly`, ADR-0069): the current Assembly, and each live run's.
|
|
543
|
+
const held = new Map<string, string>();
|
|
544
|
+
for (const { hash, runs } of frame.held) for (const runId of runs) held.set(runId, hash);
|
|
545
|
+
// `GET /assembly` names live runs only. A run that this page watched end keeps the hash it
|
|
546
|
+
// held while it was live, so it stays drawn on its own Machine, with its mark.
|
|
547
|
+
for (const runId of store.settled.keys()) {
|
|
548
|
+
const was = store.assembly.held.get(runId);
|
|
549
|
+
if (was !== undefined && !held.has(runId)) held.set(runId, was);
|
|
550
|
+
}
|
|
551
|
+
return { ...store, assembly: { current: frame.current, held } };
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
case "conversations": {
|
|
555
|
+
// The selected run's kept conversations, whole (ADR-0023). Guarded (`read`), and only for
|
|
556
|
+
// the run the page shows: a list that comes back after the selection moved is dropped.
|
|
557
|
+
if (store.token !== "live" || frame.runId !== store.selectedRunId) return store;
|
|
558
|
+
const talk = store.talk ?? emptyTalk(frame.runId);
|
|
559
|
+
return { ...store, talk: { ...talk, rows: frame.rows } };
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
case "agents": {
|
|
563
|
+
// The selected run's Agent slots as Steering sees them (ADR-0002): which ones may be steered.
|
|
564
|
+
if (store.token !== "live" || frame.runId !== store.selectedRunId) return store;
|
|
565
|
+
const talk = store.talk ?? emptyTalk(frame.runId);
|
|
566
|
+
return { ...store, talk: { ...talk, agents: frame.agents } };
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
case "entries": {
|
|
570
|
+
// Entries of one conversation, off a page or the stream: added in sequence order, each once.
|
|
571
|
+
if (store.token !== "live" || frame.runId !== store.selectedRunId || !frame.entries.length) return store;
|
|
572
|
+
const talk = store.talk ?? emptyTalk(frame.runId);
|
|
573
|
+
const kept = talk.entries.get(frame.instanceId) ?? [];
|
|
574
|
+
const merged = mergeEntries(kept, frame.entries);
|
|
575
|
+
if (merged === kept) return store;
|
|
576
|
+
return { ...store, talk: { ...talk, entries: new Map(talk.entries).set(frame.instanceId, merged) } };
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
case "conversationEnd": {
|
|
580
|
+
// The stream of one conversation ended: its Turn closed, or it was cut or swept.
|
|
581
|
+
if (store.token !== "live" || frame.runId !== store.selectedRunId) return store;
|
|
582
|
+
const talk = store.talk ?? emptyTalk(frame.runId);
|
|
583
|
+
const end = { cut: frame.cut, ...(frame.swept !== undefined ? { swept: frame.swept } : {}) };
|
|
584
|
+
return { ...store, talk: { ...talk, ended: new Map(talk.ended).set(frame.instanceId, end) } };
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
case "bottom":
|
|
588
|
+
return {
|
|
589
|
+
...store,
|
|
590
|
+
bottom: {
|
|
591
|
+
open: frame.open ?? store.bottom.open,
|
|
592
|
+
slot: frame.slot === undefined ? store.bottom.slot : frame.slot,
|
|
593
|
+
},
|
|
594
|
+
};
|
|
595
|
+
|
|
341
596
|
case "waiting": {
|
|
342
597
|
// The same guarded re-fetch's other half (ADR-0064), folded the same way: whole per run, and
|
|
343
598
|
// nothing folded without the credential that read it.
|
|
@@ -365,11 +620,25 @@ export function applyFrame(store: Store, frame: Frame): Store {
|
|
|
365
620
|
const failures = mapWithout(store.failures, frame.runId);
|
|
366
621
|
return failures === store.failures ? store : { ...store, failures };
|
|
367
622
|
}
|
|
368
|
-
if (!store.runs.has(frame.runId) && !store.settled.has(frame.runId)
|
|
623
|
+
if (!store.runs.has(frame.runId) && !store.settled.has(frame.runId) && !store.opened.has(frame.runId))
|
|
624
|
+
return store;
|
|
369
625
|
if (store.failures.get(frame.runId) === frame.reason) return store;
|
|
370
626
|
return { ...store, failures: new Map(store.failures).set(frame.runId, frame.reason) };
|
|
371
627
|
}
|
|
372
628
|
|
|
629
|
+
case "starter": {
|
|
630
|
+
// Who started the run (ADR-0078), off the same guarded re-fetch as the failure, folded by the
|
|
631
|
+
// same rules: nothing without the credential, nothing for a run the page does not hold.
|
|
632
|
+
if (store.token !== "live") return store;
|
|
633
|
+
if (frame.startedBy === null) {
|
|
634
|
+
const starters = mapWithout(store.starters, frame.runId);
|
|
635
|
+
return starters === store.starters ? store : { ...store, starters };
|
|
636
|
+
}
|
|
637
|
+
if (!store.runs.has(frame.runId) && !store.settled.has(frame.runId) && !store.opened.has(frame.runId))
|
|
638
|
+
return store;
|
|
639
|
+
return { ...store, starters: new Map(store.starters).set(frame.runId, frame.startedBy) };
|
|
640
|
+
}
|
|
641
|
+
|
|
373
642
|
case "inboxScope":
|
|
374
643
|
return { ...store, inboxAll: frame.all };
|
|
375
644
|
|
|
@@ -381,6 +650,49 @@ export function applyFrame(store: Store, frame: Frame): Store {
|
|
|
381
650
|
}
|
|
382
651
|
}
|
|
383
652
|
|
|
653
|
+
/** No conversation read yet for `runId`. */
|
|
654
|
+
function emptyTalk(runId: string): Talk {
|
|
655
|
+
return { runId, rows: [], agents: [], entries: new Map(), ended: new Map() };
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/** `runId` is one of the selected Workflow's ended runs that `GET /runs` listed, and the Caller
|
|
659
|
+
* may still read them. */
|
|
660
|
+
function inHistory(store: Store, runId: string): boolean {
|
|
661
|
+
return listedEnded(store).some((r) => r.runId === runId);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/** The ended runs that `GET /runs` listed, when the Caller may read the selected Workflow; else
|
|
665
|
+
* none (ADR-0032: without `read`, the rail lists live runs only). */
|
|
666
|
+
function listedEnded(store: Store): RunSummaryView[] {
|
|
667
|
+
// The Caller's grant, not `may`: a re-validation (`checking`) of the same Caller keeps the rows.
|
|
668
|
+
if (!store.history || !grantOpens(store.caller?.may.read, store.history.workflow)) return [];
|
|
669
|
+
return store.history.runs;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/** The same Caller, so what the page read for one is still the other's. */
|
|
673
|
+
function sameCaller(a: CallerView | null, b: CallerView | null): boolean {
|
|
674
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/** Hold `run` with the runs that this page watched end, newest last, at most {@link SETTLED_CAP}.
|
|
678
|
+
* A run that the cap drops takes its failure and starter with it, unless `GET /runs` still lists
|
|
679
|
+
* it. */
|
|
680
|
+
function settle(store: Store, run: ObservedRun): Store {
|
|
681
|
+
const settled = new Map(store.settled);
|
|
682
|
+
settled.delete(run.runId);
|
|
683
|
+
settled.set(run.runId, run);
|
|
684
|
+
let failures = store.failures;
|
|
685
|
+
let starters = store.starters;
|
|
686
|
+
while (settled.size > SETTLED_CAP) {
|
|
687
|
+
const evicted = settled.keys().next().value!;
|
|
688
|
+
settled.delete(evicted);
|
|
689
|
+
if (inHistory(store, evicted)) continue;
|
|
690
|
+
failures = mapWithout(failures, evicted);
|
|
691
|
+
starters = mapWithout(starters, evicted);
|
|
692
|
+
}
|
|
693
|
+
return { ...store, settled, failures, starters };
|
|
694
|
+
}
|
|
695
|
+
|
|
384
696
|
/** A copy of `map` without `key` — or `map` itself when the key was never there, so a caller can
|
|
385
697
|
* cheaply tell "nothing changed". */
|
|
386
698
|
function mapWithout<K, V>(map: Map<K, V>, key: K): Map<K, V> {
|
|
@@ -390,6 +702,17 @@ function mapWithout<K, V>(map: Map<K, V>, key: K): Map<K, V> {
|
|
|
390
702
|
return next;
|
|
391
703
|
}
|
|
392
704
|
|
|
705
|
+
/** `map` without the entries `keep` refuses — or `map` itself when it keeps them all. */
|
|
706
|
+
function keepWhere<K, V>(map: Map<K, V>, keep: (entry: V) => boolean): Map<K, V> {
|
|
707
|
+
let next = map;
|
|
708
|
+
for (const [key, entry] of map) {
|
|
709
|
+
if (keep(entry)) continue;
|
|
710
|
+
if (next === map) next = new Map(map);
|
|
711
|
+
next.delete(key);
|
|
712
|
+
}
|
|
713
|
+
return next;
|
|
714
|
+
}
|
|
715
|
+
|
|
393
716
|
/** Drop `workflow`'s guarded entries (inbox cards, waits) for runs a whole-set frame did not
|
|
394
717
|
* mention. Those runs will never be re-fetched again (frames and snapshots are per-run triggers),
|
|
395
718
|
* so an entry kept here would be stale forever — this is the level-triggered idiom applied to the
|
|
@@ -417,9 +740,8 @@ function dropAbsent<V extends { workflow: string }>(
|
|
|
417
740
|
* A settled run still counts as selectable — a reader watching a run to its end keeps watching it.
|
|
418
741
|
*/
|
|
419
742
|
function select(store: Store): Store {
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
}
|
|
743
|
+
const id = store.selectedRunId;
|
|
744
|
+
if (id && (store.runs.has(id) || store.settled.has(id) || store.opened.has(id) || inHistory(store, id))) return store;
|
|
423
745
|
const next = store.runs.keys().next();
|
|
424
746
|
return { ...store, selectedRunId: next.done ? null : next.value };
|
|
425
747
|
}
|
|
@@ -427,24 +749,58 @@ function select(store: Store): Store {
|
|
|
427
749
|
/** The run the page is currently showing, live or settled — whichever half still holds it. */
|
|
428
750
|
export function selectedRun(store: Store): ObservedRun | null {
|
|
429
751
|
if (store.selectedRunId === null) return null;
|
|
430
|
-
|
|
752
|
+
const id = store.selectedRunId;
|
|
753
|
+
return (
|
|
754
|
+
store.runs.get(id) ?? store.settled.get(id) ?? (inHistory(store, id) ? store.opened.get(id) : undefined) ?? null
|
|
755
|
+
);
|
|
431
756
|
}
|
|
432
757
|
|
|
433
|
-
/**
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
758
|
+
/** A run row of the rail, from a run the page holds. */
|
|
759
|
+
function railRun(run: ObservedRun, settled: boolean): RailRun {
|
|
760
|
+
return {
|
|
761
|
+
runId: run.runId,
|
|
762
|
+
status: run.status,
|
|
763
|
+
settled,
|
|
764
|
+
...(run.dispatchedBy ? { dispatchedBy: run.dispatchedBy } : {}),
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/** The selected workflow's run list as the rail draws it: live runs first, then the ended runs —
|
|
769
|
+
* those this page watched end (newest first), then those `GET /runs` lists that the page did not
|
|
770
|
+
* watch (ADR-0032). One ordered list keeps the renderer from having to know there are three
|
|
771
|
+
* sources behind it. An ended row has the listing's status and reason when the listing has it. */
|
|
772
|
+
export function runList(store: Store): RailRun[] {
|
|
773
|
+
const listed = new Map(listedEnded(store).map((r) => [r.runId, r]));
|
|
774
|
+
const ended = (run: ObservedRun): RailRun => {
|
|
775
|
+
const summary = listed.get(run.runId);
|
|
776
|
+
return summary ? summaryRow(summary, run.dispatchedBy) : railRun(run, true);
|
|
777
|
+
};
|
|
778
|
+
const watched = [...store.settled.values()].reverse();
|
|
779
|
+
const seen = new Set(watched.map((r) => r.runId));
|
|
437
780
|
return [
|
|
438
|
-
...[...store.runs.values()].map((run) => (
|
|
439
|
-
...
|
|
781
|
+
...[...store.runs.values()].map((run) => railRun(run, false)),
|
|
782
|
+
...watched.filter((run) => !store.runs.has(run.runId)).map(ended),
|
|
783
|
+
...[...listed.values()].filter((r) => !seen.has(r.runId) && !store.runs.has(r.runId)).map((r) => summaryRow(r)),
|
|
440
784
|
];
|
|
441
785
|
}
|
|
442
786
|
|
|
443
|
-
/**
|
|
444
|
-
|
|
445
|
-
|
|
787
|
+
/** An ended run's rail row, from its `GET /runs` summary. */
|
|
788
|
+
function summaryRow(summary: RunSummaryView, dispatchedBy = summary.dispatchedBy): RailRun {
|
|
789
|
+
return {
|
|
790
|
+
runId: summary.runId,
|
|
791
|
+
status: summary.status,
|
|
792
|
+
settled: true,
|
|
793
|
+
...(summary.reason !== undefined ? { reason: summary.reason } : {}),
|
|
794
|
+
...(dispatchedBy ? { dispatchedBy } : {}),
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/** One workflow's rail rows: the feed's view (and the ended runs) for the selection, the REST
|
|
799
|
+
* snapshot for the rest. Same shape either way, so the painter draws a run row without knowing
|
|
800
|
+
* which side fed it. */
|
|
801
|
+
export function fleetRuns(store: Store, workflow: string): RailRun[] {
|
|
446
802
|
if (workflow === store.workflow) return runList(store);
|
|
447
|
-
return (store.fleet.get(workflow) ?? []).map((run) => (
|
|
803
|
+
return (store.fleet.get(workflow) ?? []).map((run) => railRun(run, false));
|
|
448
804
|
}
|
|
449
805
|
|
|
450
806
|
/**
|
|
@@ -495,26 +851,82 @@ export function fleetEntries(listing: readonly WorkflowListing[]): FleetEntry[]
|
|
|
495
851
|
|
|
496
852
|
/**
|
|
497
853
|
* Which Machine doc the center pane draws (ADR-0070), as a cache key and the route that serves it.
|
|
498
|
-
* A run that binds a Chart draws its own Machine, the Chart compiled into the Open body, so
|
|
499
|
-
*
|
|
854
|
+
* A run that binds a Chart draws its own Machine, the Chart compiled into the Open body, and so does
|
|
855
|
+
* a run on an older Assembly or a run that is not live (`own`), so their doc is per run id; every
|
|
856
|
+
* other selection draws the
|
|
857
|
+
* Workflow's Machine, shared by all its runs. The
|
|
500
858
|
* Workflow name rides the route as one encoded segment (`<wrapper>%2F<name>` for a named Chart).
|
|
501
859
|
*/
|
|
502
860
|
export function machineDocSource(
|
|
503
861
|
workflow: string,
|
|
504
862
|
run: Pick<ObservedRun, "runId" | "bound" | "chartHashes"> | null,
|
|
863
|
+
own = false,
|
|
864
|
+
current: string | null = null,
|
|
505
865
|
): { key: string; path: string } {
|
|
506
866
|
const base = `/workflows/${encodeURIComponent(workflow)}`;
|
|
507
|
-
|
|
867
|
+
// A run on an older Assembly, or an ended run, is drawn on its own Machine too (ADR-0069): the
|
|
868
|
+
// current Machine's states are not the states it moves through, and an ended run's status binds
|
|
869
|
+
// no Chart any more.
|
|
870
|
+
if (run && (run.bound === true || own)) {
|
|
508
871
|
// The run id and the Charts it binds now: a second Chart in the run's life is a new doc.
|
|
509
872
|
const charts = run.chartHashes?.length ? `:${run.chartHashes.join(",")}` : "";
|
|
510
873
|
return { key: `run:${run.runId}${charts}`, path: `${base}/runs/${encodeURIComponent(run.runId)}/machine` };
|
|
511
874
|
}
|
|
512
|
-
|
|
875
|
+
// The current Assembly is in the key: after a publish, the Workflow's Machine is a new doc.
|
|
876
|
+
return { key: `workflow:${workflow}@${current ?? ""}`, path: `${base}/machine` };
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/** The Assembly hash that `runId` holds, when it is not the current one (ADR-0069): from
|
|
880
|
+
* `GET /assembly` for a live run, and from the `GET /runs` summary for an ended one. Null when the
|
|
881
|
+
* run is on the current Assembly, or the page does not know the run's Assembly. */
|
|
882
|
+
export function olderAssembly(store: Store, runId: string): string | null {
|
|
883
|
+
const current = store.assembly.current;
|
|
884
|
+
if (current === null) return null;
|
|
885
|
+
const hash = heldAssembly(store, runId);
|
|
886
|
+
return hash !== undefined && hash !== current ? hash : null;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/** The Assembly hash that `runId` holds, as far as the page knows: from `GET /assembly` (kept for a
|
|
890
|
+
* run this page watched end), else from the `GET /runs` summary. */
|
|
891
|
+
export function heldAssembly(store: Store, runId: string): string | undefined {
|
|
892
|
+
return store.assembly.held.get(runId) ?? listedEnded(store).find((r) => r.runId === runId)?.assembly;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/** The mark of a run on an older Assembly, as the rail and the Machine pane show it (ADR-0032):
|
|
896
|
+
* "older Assembly · <short hash>". Null for a run on the current one. */
|
|
897
|
+
export function olderAssemblyMark(store: Store, runId: string): string | null {
|
|
898
|
+
const hash = olderAssembly(store, runId);
|
|
899
|
+
return hash === null ? null : `older Assembly · ${hash.slice(0, 8)}`;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/** Why the SELECTED run is `drifted` (ADR-0030): it has no Machine to draw, so the pane shows this
|
|
903
|
+
* in place of the graph. Null for every other run. */
|
|
904
|
+
export function selectedRunDrift(store: Store): string | null {
|
|
905
|
+
const id = store.selectedRunId;
|
|
906
|
+
if (id === null || store.runs.has(id)) return null;
|
|
907
|
+
const summary = listedEnded(store).find((r) => r.runId === id);
|
|
908
|
+
const status = summary?.status ?? store.settled.get(id)?.status ?? store.opened.get(id)?.status;
|
|
909
|
+
if (status !== "drifted") return null;
|
|
910
|
+
return summary?.reason ?? "this run's Machine cannot be read";
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/** The doc that the center pane draws for the selection: the selected run's own Machine when it
|
|
914
|
+
* binds a Chart or is on an older Assembly, else the Workflow's (ADR-0069, ADR-0070). Null with no
|
|
915
|
+
* selected Workflow. */
|
|
916
|
+
export function selectedDocSource(store: Store): { key: string; path: string } | null {
|
|
917
|
+
if (!store.workflow) return null;
|
|
918
|
+
const run = selectedRun(store);
|
|
919
|
+
const id = store.selectedRunId;
|
|
920
|
+
// A run that is not live is drawn on the Machine its terminal write kept (ADR-0069): its status
|
|
921
|
+
// binds no Chart any more, and its Assembly can be gone. So is a run on an older Assembly.
|
|
922
|
+
const own = id !== null && (!store.runs.has(id) || olderAssembly(store, id) !== null);
|
|
923
|
+
const subject = run ?? (own && id !== null ? { runId: id } : null);
|
|
924
|
+
return machineDocSource(store.workflow, subject, own, store.assembly.current);
|
|
513
925
|
}
|
|
514
926
|
|
|
515
927
|
/** The fleet row's badge for a dispatched run (ADR-0071): `↳ <run8>`, the caller's run id cut as
|
|
516
928
|
* the row cuts its own. Null for a run nobody dispatched. */
|
|
517
|
-
export function dispatchBadge(run: ObservedRun): string | null {
|
|
929
|
+
export function dispatchBadge(run: ObservedRun | RailRun): string | null {
|
|
518
930
|
const by = run.dispatchedBy;
|
|
519
931
|
return by ? `↳ ${by.run.slice(0, 8)}` : null;
|
|
520
932
|
}
|
|
@@ -584,8 +996,8 @@ export function selectedRunFailure(store: Store): string | null {
|
|
|
584
996
|
return store.failures.get(store.selectedRunId) ?? null;
|
|
585
997
|
}
|
|
586
998
|
|
|
587
|
-
/** The SELECTED run's outstanding picks, each with since when it is
|
|
588
|
-
* observer mode. */
|
|
999
|
+
/** The SELECTED run's outstanding picks, each with since when it is outstanding (ADR-0024) — empty
|
|
1000
|
+
* in observer mode. */
|
|
589
1001
|
export function selectedRunHeld(store: Store): HeldLine[] {
|
|
590
1002
|
if (store.selectedRunId === null) return [];
|
|
591
1003
|
return store.waiting.get(store.selectedRunId)?.held ?? [];
|
|
@@ -628,9 +1040,144 @@ export function docSeats(doc: SeatedBody | null): { seats: Seat[]; warnings: str
|
|
|
628
1040
|
}
|
|
629
1041
|
|
|
630
1042
|
/** What the nav badge counts: every open gate everywhere — attention is global even when the
|
|
631
|
-
* inbox view is filtered.
|
|
1043
|
+
* inbox view is filtered. Only gates that the Caller may answer are in the inbox, so the badge
|
|
1044
|
+
* counts only those. */
|
|
632
1045
|
export function gateCount(store: Store): number {
|
|
633
1046
|
let n = 0;
|
|
634
1047
|
for (const entry of store.gates.values()) n += entry.gates.length;
|
|
635
1048
|
return n;
|
|
636
1049
|
}
|
|
1050
|
+
|
|
1051
|
+
// ---- The Caller (ADR-0032, ADR-0078) ------------------------------------------------------------
|
|
1052
|
+
|
|
1053
|
+
/** Whether a grant opens `workflow`: for `answer`, at least one Gate of it (ADR-0078). A Chart
|
|
1054
|
+
* wrapper's name opens each of its named Charts (`plan` opens `plan/weekly`), because a named Chart
|
|
1055
|
+
* runs its wrapper's Machine (ADR-0070). The same rule as `grantOpens` in tokens.ts, which is what
|
|
1056
|
+
* the server checks. The server lists only the Gates of a run that the Caller may answer, so the
|
|
1057
|
+
* Console does not check the Gate's name. */
|
|
1058
|
+
function grantOpens(grant: AnswerGrant | undefined, workflow: string): boolean {
|
|
1059
|
+
if (grant === undefined) return false;
|
|
1060
|
+
if (grant === "all") return true;
|
|
1061
|
+
const cut = workflow.indexOf("/");
|
|
1062
|
+
return grant.some((item) => {
|
|
1063
|
+
const name = typeof item === "string" ? item : item.workflow;
|
|
1064
|
+
return name === workflow || (cut > 0 && name === workflow.slice(0, cut));
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
/** One item of a grant as the nav's title says it: a Workflow name, or `deploy (approve, ship)`
|
|
1069
|
+
* for an item that names single Gates. */
|
|
1070
|
+
function grantItem(item: string | { workflow: string; gates: readonly string[] }): string {
|
|
1071
|
+
return typeof item === "string" ? item : `${item.workflow} (${item.gates.join(", ")})`;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
/**
|
|
1075
|
+
* Whether the page's Caller has `scope` for `workflow` — what decides that a control renders
|
|
1076
|
+
* (ADR-0032): the start button with `start`, the Gate cards with `answer`, cancel with `cancel`, a
|
|
1077
|
+
* run's full state with `read`, the Steering box with `steer`. False with no live credential. With
|
|
1078
|
+
* no `workflow`, whether the scope opens at least one Workflow. A hidden control is so that a
|
|
1079
|
+
* person does not meet a 403, never a guard: the server checks each request.
|
|
1080
|
+
*/
|
|
1081
|
+
export function may(store: Store, scope: ScopeName, workflow?: string): boolean {
|
|
1082
|
+
if (store.token !== "live" || !store.caller) return false;
|
|
1083
|
+
const grant = store.caller.may[scope];
|
|
1084
|
+
if (workflow === undefined) return grant !== undefined && (grant === "all" || grant.length > 0);
|
|
1085
|
+
return grantOpens(grant, workflow);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
/** Who the page acts as, as the nav shows it (ADR-0032): the name that jr2 shows (a JWT's `email`),
|
|
1089
|
+
* else `<via>/<name>`, and the party it says it acts for. Null with no live credential. */
|
|
1090
|
+
export function callerLabel(store: Store): string | null {
|
|
1091
|
+
const caller = store.token === "live" ? store.caller : null;
|
|
1092
|
+
if (!caller) return null;
|
|
1093
|
+
const who = caller.shown ?? `${caller.via}/${caller.name}`;
|
|
1094
|
+
return caller.onBehalfOf !== undefined ? `${who} for ${caller.onBehalfOf}` : who;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
/** The scopes of the page's Caller, one line for the nav's title: `start: all · answer: a, b`. */
|
|
1098
|
+
export function callerScopes(caller: CallerView | null): string {
|
|
1099
|
+
if (!caller) return "";
|
|
1100
|
+
const lines = (["start", "answer", "cancel", "read", "steer"] as const).flatMap((scope) => {
|
|
1101
|
+
const grant = caller.may[scope];
|
|
1102
|
+
if (grant === undefined || (grant !== "all" && grant.length === 0)) return [];
|
|
1103
|
+
return [`${scope}: ${grant === "all" ? "all" : grant.map(grantItem).join(", ")}`];
|
|
1104
|
+
});
|
|
1105
|
+
return lines.length ? lines.join(" · ") : "no scope";
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
/** The SELECTED run's starter as the Machine pane's strip says it (ADR-0078): `started by
|
|
1109
|
+
* token/ci`, `started by run <run8> (<slot>)`, `started by jr2 (boot)`. Null when the page does
|
|
1110
|
+
* not know it: no live credential with `read`, or a run from an older Orchestrator. */
|
|
1111
|
+
export function selectedRunStarter(store: Store): string | null {
|
|
1112
|
+
if (store.selectedRunId === null) return null;
|
|
1113
|
+
const by = store.starters.get(store.selectedRunId);
|
|
1114
|
+
if (!by) return null;
|
|
1115
|
+
if (by.kind === "run") return `started by run ${by.run.slice(0, 8)} (${by.slot})`;
|
|
1116
|
+
if (by.kind === "jr2") return `started by jr2 (${by.why})`;
|
|
1117
|
+
const shown = by.shown !== undefined ? ` (${by.shown})` : "";
|
|
1118
|
+
const behalf = by.onBehalfOf !== undefined ? ` on behalf of ${by.onBehalfOf}` : "";
|
|
1119
|
+
return `started by ${by.via}/${by.name}${shown}${behalf}`;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// ---- The kept conversations (ADR-0023, ADR-0032) ------------------------------------------------
|
|
1123
|
+
|
|
1124
|
+
/** One tab of the bottom drawer: an Agent slot of the selected run, a dot when its Turn is open,
|
|
1125
|
+
* and whether a Steering message can go to it. */
|
|
1126
|
+
export type SlotTab = { slot: string; open: boolean; steerable: boolean };
|
|
1127
|
+
|
|
1128
|
+
/** A conversation's slot: the ledger's slot, else the Agent's name. */
|
|
1129
|
+
function slotOf(row: ConversationRow): string {
|
|
1130
|
+
return row.slot ?? row.agent;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
/** The bottom drawer's tabs: each Agent slot of the selected run, in the order its first
|
|
1134
|
+
* conversation began, then the slots that `GET /runs/:id/agents` names and no conversation has
|
|
1135
|
+
* yet. */
|
|
1136
|
+
export function slotTabs(store: Store): SlotTab[] {
|
|
1137
|
+
const talk = store.talk;
|
|
1138
|
+
if (!talk) return [];
|
|
1139
|
+
const tabs = new Map<string, SlotTab>();
|
|
1140
|
+
for (const row of talk.rows) {
|
|
1141
|
+
const slot = slotOf(row);
|
|
1142
|
+
const tab = tabs.get(slot) ?? { slot, open: false, steerable: false };
|
|
1143
|
+
tab.open ||= row.open;
|
|
1144
|
+
tabs.set(slot, tab);
|
|
1145
|
+
}
|
|
1146
|
+
for (const line of talk.agents) {
|
|
1147
|
+
const tab = tabs.get(line.agent) ?? { slot: line.agent, open: false, steerable: false };
|
|
1148
|
+
tab.open ||= line.open;
|
|
1149
|
+
tab.steerable = line.steerable;
|
|
1150
|
+
tabs.set(line.agent, tab);
|
|
1151
|
+
}
|
|
1152
|
+
return [...tabs.values()];
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
/** The slot that the bottom drawer shows: the one the person chose while the run has it, else the
|
|
1156
|
+
* first slot with an open Turn, else the first slot. Null when the run has none. */
|
|
1157
|
+
export function activeSlot(store: Store): string | null {
|
|
1158
|
+
const tabs = slotTabs(store);
|
|
1159
|
+
const chosen = store.bottom.slot;
|
|
1160
|
+
if (chosen !== null && tabs.some((t) => t.slot === chosen)) return chosen;
|
|
1161
|
+
return (tabs.find((t) => t.open) ?? tabs[0])?.slot ?? null;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
/** One conversation of a slot as the drawer shows it: its row, the entries read so far, and how
|
|
1165
|
+
* its stream ended, if it did. */
|
|
1166
|
+
export type SlotConversation = {
|
|
1167
|
+
row: ConversationRow;
|
|
1168
|
+
entries: ConversationEntry[];
|
|
1169
|
+
ended?: { cut: boolean; swept?: string };
|
|
1170
|
+
};
|
|
1171
|
+
|
|
1172
|
+
/** The conversations of `slot`, the first first: a slot whose conversation began again (a new
|
|
1173
|
+
* conversation after a fault, ADR-0057) has more than one. */
|
|
1174
|
+
export function slotConversations(store: Store, slot: string | null): SlotConversation[] {
|
|
1175
|
+
const talk = store.talk;
|
|
1176
|
+
if (!talk || slot === null) return [];
|
|
1177
|
+
return talk.rows
|
|
1178
|
+
.filter((row) => slotOf(row) === slot)
|
|
1179
|
+
.map((row) => {
|
|
1180
|
+
const ended = talk.ended.get(row.instanceId);
|
|
1181
|
+
return { row, entries: talk.entries.get(row.instanceId) ?? [], ...(ended ? { ended } : {}) };
|
|
1182
|
+
});
|
|
1183
|
+
}
|