@jr2/orchestrator 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +86 -9
- package/docs/concepts/architecture.md +41 -28
- package/docs/concepts/callers-and-hooks.md +205 -0
- package/docs/concepts/charts.md +7 -5
- package/docs/concepts/composition.md +10 -1
- package/docs/concepts/converge.md +4 -2
- package/docs/concepts/durable-runs.md +43 -16
- package/docs/concepts/effects.md +50 -22
- package/docs/concepts/gates.md +95 -25
- package/docs/concepts/machines-and-workflows.md +5 -4
- package/docs/concepts/pools-and-sources.md +187 -11
- package/docs/concepts/secrets-and-trust.md +73 -33
- package/docs/concepts/the-menu.md +11 -0
- package/docs/concepts/workspaces.md +6 -5
- package/docs/get-started/a-model-decides.md +14 -13
- package/docs/get-started/an-agent-in-a-sandbox.md +43 -34
- package/docs/get-started/connect-a-model.md +1 -1
- package/docs/get-started/create-an-instance.md +17 -14
- package/docs/guides/add-tools-to-the-sandbox.md +55 -11
- 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 +24 -8
- 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/fix-a-failed-converge.md +18 -6
- 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 +9 -6
- package/docs/guides/look-inside-a-sandbox.md +101 -31
- package/docs/guides/open-the-console.md +63 -14
- package/docs/guides/read-a-conversation.md +225 -0
- package/docs/guides/run-a-chart-by-name.md +17 -16
- package/docs/guides/run-one-worker-per-item.md +15 -9
- package/docs/guides/set-an-agents-access-and-dials.md +2 -0
- 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 +1 -1
- package/docs/guides/watch-a-run.md +127 -121
- 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 +17 -7
- package/docs/llms.txt +81 -15
- package/docs/reference/authoring-surface/agent.md +1 -1
- package/docs/reference/authoring-surface/answer.md +1 -1
- package/docs/reference/authoring-surface/arg.md +1 -1
- 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 +1 -1
- 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 +1 -1
- 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 +1 -1
- package/docs/reference/authoring-surface/file.md +1 -1
- 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 +1 -1
- package/docs/reference/authoring-surface/isfailure.md +1 -1
- package/docs/reference/authoring-surface/isready.md +1 -1
- package/docs/reference/authoring-surface/jr2setup.md +13 -3
- package/docs/reference/authoring-surface/mcp.md +107 -0
- package/docs/reference/authoring-surface/open.md +5 -3
- package/docs/reference/authoring-surface/openbody.md +1 -1
- package/docs/reference/authoring-surface/paramsof.md +1 -1
- package/docs/reference/authoring-surface/pool.md +77 -9
- package/docs/reference/authoring-surface/push.md +1 -1
- package/docs/reference/authoring-surface/readyof.md +1 -1
- package/docs/reference/authoring-surface/reread.md +19 -0
- package/docs/reference/authoring-surface/secret.md +1 -1
- 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 +56 -1
- 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 +14 -1
- package/docs/reference/authoring-surface/type-agentmcpserver.md +54 -0
- package/docs/reference/authoring-surface/type-anyframepath.md +1 -1
- package/docs/reference/authoring-surface/type-anyworktreepath.md +1 -1
- package/docs/reference/authoring-surface/type-binding.md +1 -1
- 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-commandoutput.md +1 -1
- package/docs/reference/authoring-surface/type-customize.md +16 -1
- 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 +1 -1
- package/docs/reference/authoring-surface/type-effectbinding.md +12 -2
- package/docs/reference/authoring-surface/type-framepath.md +1 -1
- 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 +1 -1
- package/docs/reference/authoring-surface/type-httpoutput.md +1 -1
- package/docs/reference/authoring-surface/type-jr2config.md +37 -2
- package/docs/reference/authoring-surface/type-lateroutcome.md +1 -1
- 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-openbodydeclaration.md +1 -1
- package/docs/reference/authoring-surface/type-openbodyoutput.md +1 -1
- package/docs/reference/authoring-surface/type-poolnextinput.md +1 -1
- package/docs/reference/authoring-surface/type-poolspec.md +11 -6
- package/docs/reference/authoring-surface/type-pushoutput.md +1 -1
- 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 +1 -1
- 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-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-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 +16 -7
- 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 +1 -1
- package/docs/reference/authoring-surface/type-worktreefound.md +3 -3
- package/docs/reference/authoring-surface/type-worktreeinput.md +1 -1
- package/docs/reference/authoring-surface/type-worktreeoptions.md +1 -1
- package/docs/reference/authoring-surface/type-worktreeoutput.md +1 -1
- package/docs/reference/authoring-surface/type-worktreepath.md +1 -1
- package/docs/reference/authoring-surface/type-worktreeready.md +1 -1
- package/docs/reference/authoring-surface/type-worktreerefused.md +4 -4
- package/docs/reference/authoring-surface/workspace.md +1 -1
- package/docs/reference/authoring-surface/worktree.md +1 -1
- package/docs/reference/authoring-surface.md +88 -167
- package/docs/reference/cli.md +413 -74
- package/docs/reference/config.md +235 -2
- package/docs/reference/decisions/0002-duplex-streaming-actor.md +71 -11
- package/docs/reference/decisions/0005-sandbox-pod-composition.md +14 -10
- package/docs/reference/decisions/0006-agent-control-surface.md +4 -4
- 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 +9 -2
- package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +20 -14
- 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 +18 -14
- package/docs/reference/decisions/0017-pool-over-a-source-port.md +226 -12
- 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 +32 -18
- package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +14 -1
- 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 +116 -34
- package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +38 -37
- 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 +7 -6
- package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +23 -7
- 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 -18
- 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 +4 -3
- 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 +5 -4
- package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +19 -19
- package/docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md +6 -1
- 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 +7 -0
- package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +13 -7
- package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +38 -30
- package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +64 -17
- package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +2 -1
- 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 +51 -23
- package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +94 -54
- package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +101 -47
- 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 +17 -13
- 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/0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md +4 -3
- package/docs/reference/decisions/0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md +37 -10
- 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 +6 -4
- package/docs/reference/glossary.md +96 -42
- package/docs/reference/hooks.md +1077 -0
- package/docs/reference/http-api.md +976 -149
- package/docs/why.md +2 -1
- package/package.json +10 -3
- package/src/actor.ts +36 -10
- package/src/agent.ts +50 -0
- package/src/assembly-checks.ts +530 -4
- package/src/callers.ts +245 -0
- package/src/chart.ts +52 -7
- package/src/config.ts +550 -1
- package/src/conversations.ts +453 -0
- package/src/custodian.ts +8 -2
- package/src/customize.ts +143 -4
- package/src/dispatch-effect.ts +142 -26
- package/src/durability.ts +2 -0
- package/src/duration.ts +20 -0
- package/src/effect-checks.ts +2 -1
- package/src/effect.ts +60 -7
- package/src/effector-wire.ts +272 -27
- package/src/gate-brand.ts +16 -0
- package/src/gate.ts +634 -48
- package/src/harness-client.ts +10 -4
- package/src/held-secrets.ts +72 -23
- package/src/holds.ts +2 -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 +76 -38
- package/src/http.ts +889 -94
- package/src/index.ts +16 -5
- package/src/instance.ts +196 -26
- package/src/internal.ts +22 -3
- package/src/machine-doc.ts +26 -1
- package/src/mcp-effect.ts +424 -0
- package/src/mount-digest.ts +41 -7
- package/src/names.ts +64 -2
- package/src/notices.ts +138 -10
- package/src/open-body.ts +25 -11
- package/src/open-effect.ts +17 -4
- package/src/open.ts +4 -2
- package/src/parts.ts +87 -9
- package/src/party.ts +2 -1
- package/src/pick.ts +2 -2
- package/src/pool.ts +1142 -163
- package/src/registration.ts +203 -13
- package/src/run-host.ts +1509 -171
- package/src/run-list.ts +191 -0
- package/src/sandbox-kube.ts +53 -5
- package/src/secret-placement.ts +239 -33
- package/src/server.ts +53 -4
- package/src/setup.ts +72 -32
- package/src/snapshot-store.ts +565 -48
- package/src/steer.ts +111 -0
- package/src/stub-harness.ts +6 -2
- package/src/tokens.ts +263 -22
- package/src/wire.ts +17 -0
- package/src/workspace.ts +50 -6
- package/src/worktree.ts +3 -3
- 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/src/snapshot-store.ts
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
// (WAL, `synchronous=NORMAL`), and the write stays synchronous so the row is on disk before the
|
|
6
6
|
// Machine's next side effect runs. `markLost` records a run whose live state could not be
|
|
7
7
|
// re-hydrated (e.g. its flue handle is gone), keeping any snapshot the row had; a non-live row
|
|
8
|
-
// lives
|
|
8
|
+
// lives for `runs.retain`, then `sweep` deletes it. `ended` lists the non-live rows for
|
|
9
|
+
// `GET /runs` off columns that `save` copies out of the blob, so a listing parses no snapshot.
|
|
9
10
|
|
|
10
11
|
import { DatabaseSync } from "node:sqlite";
|
|
11
12
|
import type { StoredSnapshot } from "./durability.ts";
|
|
@@ -14,7 +15,7 @@ export interface SnapshotStore {
|
|
|
14
15
|
init(): Promise<void>;
|
|
15
16
|
load(runId: string): Promise<StoredSnapshot | null>;
|
|
16
17
|
/** The `live` runs, in insertion order — what `restore()` reads (ADR-0065). Terminal rows are
|
|
17
|
-
* filtered by the store, not the host, so a boot never parses
|
|
18
|
+
* filtered by the store, not the host, so a boot never parses the kept history. */
|
|
18
19
|
live(): Promise<StoredSnapshot[]>;
|
|
19
20
|
/**
|
|
20
21
|
* The `live` runs by the Assembly hash their blob names (ADR-0069), each list in insertion order:
|
|
@@ -25,12 +26,13 @@ export interface SnapshotStore {
|
|
|
25
26
|
*/
|
|
26
27
|
liveAssemblies(): Promise<Map<string, string[]>>;
|
|
27
28
|
/**
|
|
28
|
-
* Delete every non-live row last written before `olderThan`, and answer
|
|
29
|
+
* Delete every non-live row last written before `olderThan`, and answer their run ids, so that
|
|
30
|
+
* the run's kept conversations go with it (ADR-0065, ADR-0023). A
|
|
29
31
|
* `live` row is never swept, whatever its age: a run parked on a Gate for a month is restored.
|
|
30
32
|
* A swept id is gone as if it never ran — `read()` answers undefined, and its abbreviations are
|
|
31
33
|
* free again (ADR-0009).
|
|
32
34
|
*/
|
|
33
|
-
sweep(olderThan: Date): Promise<
|
|
35
|
+
sweep(olderThan: Date): Promise<string[]>;
|
|
34
36
|
/**
|
|
35
37
|
* Persisted run ids sharing a prefix, sorted, capped at `limit` — the store half of abbreviated
|
|
36
38
|
* run ids (ADR-0009). Includes `lost` rows: the ambiguity set is the ids that EXIST, not the ones
|
|
@@ -39,12 +41,27 @@ export interface SnapshotStore {
|
|
|
39
41
|
*/
|
|
40
42
|
findIdsByPrefix(prefix: string, limit: number): Promise<string[]>;
|
|
41
43
|
/**
|
|
42
|
-
* The run whose blob carries the idempotency key `
|
|
43
|
-
* any status, or undefined: what answers a re-sent dispatch with the run it already
|
|
44
|
-
*
|
|
45
|
-
* with
|
|
44
|
+
* The run of `workflow` whose blob carries the idempotency key `key` (`dispatchedBy.effect`,
|
|
45
|
+
* ADR-0071), in any status, or undefined: what answers a re-sent dispatch with the run it already
|
|
46
|
+
* started. The key is scoped to the callee Workflow, so the same key in two Workflows is two runs.
|
|
47
|
+
* A row `markLost` wrote with no save before it has no snapshot, and so no key; a swept row is
|
|
48
|
+
* gone with its key.
|
|
46
49
|
*/
|
|
47
|
-
findByDispatch(
|
|
50
|
+
findByDispatch(workflow: string, key: string): Promise<string | undefined>;
|
|
51
|
+
/**
|
|
52
|
+
* The non-live runs that `query` selects, newest start first, at most `limit` (ADR-0009): what
|
|
53
|
+
* `GET /runs` lists of the history. Read off the summary columns, never by parsing a snapshot. A
|
|
54
|
+
* row with no snapshot (`markLost` with no save before it) is not listed: it names no Workflow,
|
|
55
|
+
* and `read()` does not know it either.
|
|
56
|
+
*/
|
|
57
|
+
ended(query: EndedQuery): Promise<EndedRow[]>;
|
|
58
|
+
/**
|
|
59
|
+
* The `live` rows that `query` selects (its `statuses` are not read), in the order of `ended`, at
|
|
60
|
+
* most `limit`: what `GET /runs` lists of a live run that the registry does not hold — a restore
|
|
61
|
+
* that a boot deferred (ADR-0069) or that failed and waits for the next boot (ADR-0030). The host
|
|
62
|
+
* leaves out the ids it hosts. Its `endedAt` is the row's last write.
|
|
63
|
+
*/
|
|
64
|
+
liveRows(query: EndedQuery): Promise<EndedRow[]>;
|
|
48
65
|
save(runId: string, snapshot: unknown, status?: string): Promise<void>;
|
|
49
66
|
/**
|
|
50
67
|
* Record a run whose live state could not be re-hydrated. An upsert: a row that was saved KEEPS
|
|
@@ -65,31 +82,212 @@ export interface SnapshotStore {
|
|
|
65
82
|
* existed. It keeps the row's idempotency key too (ADR-0071).
|
|
66
83
|
*/
|
|
67
84
|
markDrifted(runId: string, reason: string): Promise<void>;
|
|
85
|
+
/**
|
|
86
|
+
* Claim a Hook's delivery for the fan-in (ADR-0011): when the hook log holds no delivery of this
|
|
87
|
+
* Hook with this id OR this digest, it writes a `pending` line and answers the line's id, which
|
|
88
|
+
* `writeHookLine` finishes; else it answers `undefined`, a repeat, and writes nothing. The check and the write are one synchronous step, so of two deliveries
|
|
89
|
+
* that come at once, one claims. Only the lines of a claimed delivery count: a handshake, a
|
|
90
|
+
* `payload-refused` line and an `unreadable` line never make a repeat, so a payload that the parse
|
|
91
|
+
* refused is parsed again when the service sends it again.
|
|
92
|
+
*/
|
|
93
|
+
claimHookDelivery(hook: string, delivery: string, digest: string): number | undefined;
|
|
94
|
+
/** Write one line of the hook log (ADR-0011), or finish the `pending` line of `claimed`. */
|
|
95
|
+
writeHookLine(line: HookLogLine, claimed?: number): void;
|
|
96
|
+
/** The hook log of one Hook, newest first, at most `limit` lines. */
|
|
97
|
+
hookLog(hook: string, limit: number): HookLogLine[];
|
|
98
|
+
/**
|
|
99
|
+
* The head of each standing Workflow (ADR-0017): its newest run, live or not, and whether it is
|
|
100
|
+
* paused. Synchronous, as `writeStanding` is, so a hand-over's start, its end and the head that
|
|
101
|
+
* names the new run are one step.
|
|
102
|
+
*/
|
|
103
|
+
standingHeads(): StandingHead[];
|
|
104
|
+
/** Write the head of one standing Workflow. The sweep never deletes the run a head names:
|
|
105
|
+
* a paused Pool keeps its held items in that run's blob for as long as it is paused. */
|
|
106
|
+
writeStanding(head: StandingHead): void;
|
|
107
|
+
/**
|
|
108
|
+
* Keep the Machine an ended run ran on (ADR-0069), as structure with its Charts compiled, under
|
|
109
|
+
* `key`: a digest of the kept doc. One copy for each key, so a thousand runs of one Workflow on
|
|
110
|
+
* one Assembly keep one copy; the run's row holds only the key. Synchronous, so the terminal save and this are one step.
|
|
111
|
+
*/
|
|
112
|
+
keepMachine(runId: string, key: string, doc: unknown): void;
|
|
113
|
+
/** The Machine that {@link keepMachine} kept for an ended run, with the run's Workflow name (one
|
|
114
|
+
* copy can serve runs of two names: a Chart wrapper and its named Chart), or undefined. */
|
|
115
|
+
keptMachine(runId: string): { workflow: string; doc: unknown } | undefined;
|
|
68
116
|
close(): Promise<void>;
|
|
69
117
|
}
|
|
70
118
|
|
|
119
|
+
/**
|
|
120
|
+
* The head of a standing Workflow (ADR-0017): `run` is its newest run, `since` when that run
|
|
121
|
+
* started, `streak` how many runs in a row ended `error`, `failed` or at the bound with no run
|
|
122
|
+
* started by an item that the run took itself, and `paused` why jr2 starts no run of it: a Caller
|
|
123
|
+
* cancelled the run (`cancel`), or, after 3 hand-overs from such runs, the next one ended `error`
|
|
124
|
+
* or `failed` (`errors`) or at its bound (`bound`).
|
|
125
|
+
*/
|
|
126
|
+
export type StandingHead = {
|
|
127
|
+
workflow: string;
|
|
128
|
+
run: string;
|
|
129
|
+
since: string;
|
|
130
|
+
streak: number;
|
|
131
|
+
paused?: "cancel" | "errors" | "bound";
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* One line of the hook log (ADR-0011): what the door did with one delivery of one Hook, and when.
|
|
136
|
+
* `kind` says which line it is: `delivered` (the fan-in ran; `outcome` holds what it did), `seen` (a
|
|
137
|
+
* repeat), `hold-full` (the fan-in ran and every Source was full, ADR-0017: the line does not claim
|
|
138
|
+
* the id, so a send again is heard), `no-live-run` (the fan-in ran, and a standing Pool that hears the
|
|
139
|
+
* kind and key has no live run and is not paused: 503, and the line does not claim the id either),
|
|
140
|
+
* `handshake`, `payload-refused` (with zod's `issues`), `unreadable` (the kind could not
|
|
141
|
+
* read the request: `error`) or `follow-up` (the follow-up post of an answer that a person reads
|
|
142
|
+
* failed, or went to a url outside the kind's `posts`: `post` and `error`; its `status` is 0). A
|
|
143
|
+
* `reply` that threw is in the `replyErrors` of its delivery's `detail`. `status` is the status that
|
|
144
|
+
* the rules give, which is the status of the answer unless a person reads the answer: that answer is
|
|
145
|
+
* 200 (ADR-0011).
|
|
146
|
+
*/
|
|
147
|
+
export type HookLogLine = {
|
|
148
|
+
hook: string;
|
|
149
|
+
at: string;
|
|
150
|
+
kind:
|
|
151
|
+
| "pending"
|
|
152
|
+
| "delivered"
|
|
153
|
+
| "seen"
|
|
154
|
+
| "hold-full"
|
|
155
|
+
| "no-live-run"
|
|
156
|
+
| "handshake"
|
|
157
|
+
| "payload-refused"
|
|
158
|
+
| "unreadable"
|
|
159
|
+
| "too-large"
|
|
160
|
+
| "follow-up";
|
|
161
|
+
status: number;
|
|
162
|
+
delivery?: string;
|
|
163
|
+
digest?: string;
|
|
164
|
+
/** The event key, when the kind read one. */
|
|
165
|
+
key?: string;
|
|
166
|
+
/** What the line says: the outcome, zod's issues, the error. */
|
|
167
|
+
detail?: unknown;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/** What `ended` selects (ADR-0009). Each field left out selects every row. */
|
|
171
|
+
export type EndedQuery = {
|
|
172
|
+
/** The statuses to list; absent: every status that is not `live`. */
|
|
173
|
+
statuses?: readonly string[];
|
|
174
|
+
/** The Workflows to list. A name also opens its Charts, `<name>/<chart>`, as a scope grant does
|
|
175
|
+
* (ADR-0078). Absent: every Workflow. */
|
|
176
|
+
workflows?: readonly string[];
|
|
177
|
+
/** The one Workflow to list, by its exact name. */
|
|
178
|
+
workflow?: string;
|
|
179
|
+
/** List only the runs that started at or after this ISO-8601 time. */
|
|
180
|
+
since?: string;
|
|
181
|
+
/** List only the runs after this one in the order: an older start, or the same start and a
|
|
182
|
+
* smaller run id. The cursor of a page. */
|
|
183
|
+
after?: { startedAt: string; runId: string };
|
|
184
|
+
limit: number;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
/** One non-live run as `ended` reads it: the columns, and the summary that `save` copied out of
|
|
188
|
+
* the blob. `endedAt` is the row's terminal write. */
|
|
189
|
+
export type EndedRow = {
|
|
190
|
+
runId: string;
|
|
191
|
+
workflow: string;
|
|
192
|
+
status: string;
|
|
193
|
+
/** The store's reason (`drifted`, `lost`), else the Failure's reason, else the fault. */
|
|
194
|
+
reason?: string;
|
|
195
|
+
startedAt: string;
|
|
196
|
+
endedAt: string;
|
|
197
|
+
startedBy?: unknown;
|
|
198
|
+
assembly?: string;
|
|
199
|
+
dispatchedBy?: unknown;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
/** The part of a blob that a listing shows (ADR-0009), copied into the `summary` column by `save`. */
|
|
203
|
+
type RowSummary = {
|
|
204
|
+
startedBy?: unknown;
|
|
205
|
+
assembly?: string;
|
|
206
|
+
dispatchedBy?: unknown;
|
|
207
|
+
failure?: string;
|
|
208
|
+
fault?: string;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
/** The listing fields of a blob: its Workflow (null when it names none), its start, and the
|
|
212
|
+
* summary. A blob with no `startedAt` (written before the field) takes `fallback`. */
|
|
213
|
+
function listingOf(
|
|
214
|
+
snapshot: unknown,
|
|
215
|
+
fallback: string,
|
|
216
|
+
): { workflow: string | null; startedAt: string; summary: string } {
|
|
217
|
+
const blob = (typeof snapshot === "object" && snapshot !== null ? snapshot : {}) as Record<string, unknown>;
|
|
218
|
+
const text = (v: unknown): string | undefined => (typeof v === "string" ? v : undefined);
|
|
219
|
+
const failure = blob.failure as { reason?: unknown } | undefined;
|
|
220
|
+
const summary: RowSummary = {
|
|
221
|
+
...(blob.startedBy !== undefined ? { startedBy: blob.startedBy } : {}),
|
|
222
|
+
...(text(blob.assembly) !== undefined ? { assembly: text(blob.assembly) } : {}),
|
|
223
|
+
...(blob.dispatchedBy !== undefined ? { dispatchedBy: blob.dispatchedBy } : {}),
|
|
224
|
+
...(text(failure?.reason) !== undefined ? { failure: text(failure?.reason) } : {}),
|
|
225
|
+
...(text(blob.fault) !== undefined ? { fault: text(blob.fault) } : {}),
|
|
226
|
+
};
|
|
227
|
+
return {
|
|
228
|
+
workflow: text(blob.workflow) ?? null,
|
|
229
|
+
startedAt: text(blob.startedAt) ?? fallback,
|
|
230
|
+
summary: JSON.stringify(summary),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** The kinds of line that make a repeat: a delivery that the door claimed. */
|
|
235
|
+
const CLAIMED_KINDS = ["pending", "delivered", "seen"];
|
|
236
|
+
|
|
71
237
|
type Row = {
|
|
72
238
|
run_id: string;
|
|
73
239
|
status: string;
|
|
74
240
|
snapshot: string | null;
|
|
75
241
|
reason: string | null;
|
|
76
242
|
updated_at: string;
|
|
243
|
+
started_at: string | null;
|
|
77
244
|
};
|
|
78
245
|
|
|
246
|
+
/** A row as `load` and `live` give it. */
|
|
247
|
+
function storedOf(row: Row): StoredSnapshot {
|
|
248
|
+
const stored: StoredSnapshot = {
|
|
249
|
+
runId: row.run_id,
|
|
250
|
+
status: row.status,
|
|
251
|
+
snapshot: row.snapshot === null ? null : JSON.parse(row.snapshot),
|
|
252
|
+
};
|
|
253
|
+
if (row.reason !== null) stored.reason = row.reason;
|
|
254
|
+
if (row.started_at !== null) stored.startedAt = row.started_at;
|
|
255
|
+
return stored;
|
|
256
|
+
}
|
|
257
|
+
|
|
79
258
|
/** The lookup `findByDispatch` runs, exported so a test can ask the planner about THIS statement,
|
|
80
|
-
* not a copy of it: the
|
|
259
|
+
* not a copy of it: the pair of columns is indexed, so the plan is a seek. */
|
|
81
260
|
export const FIND_BY_DISPATCH_SQL = `SELECT run_id FROM machine_snapshots
|
|
82
|
-
WHERE dispatch_effect = ? ORDER BY rowid LIMIT 1`;
|
|
261
|
+
WHERE dispatch_workflow = ? AND dispatch_effect = ? ORDER BY rowid LIMIT 1`;
|
|
83
262
|
|
|
84
|
-
/** The idempotency key a blob carries
|
|
263
|
+
/** The idempotency key a blob carries (ADR-0071): `dispatchedBy.key`, else `dispatchedBy.effect`, or null. */
|
|
85
264
|
function dispatchEffectOf(snapshot: unknown): string | null {
|
|
86
265
|
if (typeof snapshot !== "object" || snapshot === null) return null;
|
|
87
266
|
const by = (snapshot as { dispatchedBy?: unknown }).dispatchedBy;
|
|
88
267
|
if (typeof by !== "object" || by === null) return null;
|
|
268
|
+
// A Pool's item id is the key when there is one (ADR-0017); else the caller's ledger row id.
|
|
269
|
+
const key = (by as { key?: unknown }).key;
|
|
270
|
+
if (typeof key === "string") return key;
|
|
89
271
|
const effect = (by as { effect?: unknown }).effect;
|
|
90
272
|
return typeof effect === "string" ? effect : null;
|
|
91
273
|
}
|
|
92
274
|
|
|
275
|
+
/** Whether a blob carries its run's start (`startedAt`). */
|
|
276
|
+
function hasStartedAt(snapshot: unknown): boolean {
|
|
277
|
+
return (
|
|
278
|
+
typeof snapshot === "object" &&
|
|
279
|
+
snapshot !== null &&
|
|
280
|
+
typeof (snapshot as { startedAt?: unknown }).startedAt === "string"
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** The Workflow a blob names (`workflow`): the scope of its idempotency key (ADR-0071), or null. */
|
|
285
|
+
function dispatchWorkflowOf(snapshot: unknown): string | null {
|
|
286
|
+
if (typeof snapshot !== "object" || snapshot === null) return null;
|
|
287
|
+
const workflow = (snapshot as { workflow?: unknown }).workflow;
|
|
288
|
+
return typeof workflow === "string" ? workflow : null;
|
|
289
|
+
}
|
|
290
|
+
|
|
93
291
|
export class SqliteSnapshotStore implements SnapshotStore {
|
|
94
292
|
private readonly db: DatabaseSync;
|
|
95
293
|
|
|
@@ -115,45 +313,144 @@ export class SqliteSnapshotStore implements SnapshotStore {
|
|
|
115
313
|
// its own when `save` writes the row, and the column is indexed. Not an expression index over
|
|
116
314
|
// `json_extract(snapshot, …)`: that runs on every row and THROWS on a blob SQLite's JSON parser
|
|
117
315
|
// refuses (nested about a thousand deep), so one such row would fail its own save and every
|
|
118
|
-
// later `init()` over the volume. A column also costs no JSON parse a save.
|
|
316
|
+
// later `init()` over the volume. A column also costs no JSON parse a save. The key is scoped
|
|
317
|
+
// to the callee Workflow, so the Workflow is copied out too, and the index is on the pair.
|
|
119
318
|
const columns = this.db.prepare(`PRAGMA table_info(machine_snapshots)`).all() as Array<{ name: string }>;
|
|
120
319
|
if (!columns.some((c) => c.name === "dispatch_effect")) {
|
|
121
320
|
this.db.exec(`ALTER TABLE machine_snapshots ADD COLUMN dispatch_effect TEXT`);
|
|
122
321
|
}
|
|
123
|
-
|
|
322
|
+
if (!columns.some((c) => c.name === "dispatch_workflow")) {
|
|
323
|
+
this.db.exec(`ALTER TABLE machine_snapshots ADD COLUMN dispatch_workflow TEXT`);
|
|
324
|
+
this.fillDispatchWorkflow();
|
|
325
|
+
}
|
|
326
|
+
// The listing columns (ADR-0009): the Workflow, the start and the summary, copied out of the
|
|
327
|
+
// blob by `save`, so `ended` lists the history with no JSON parse. `listed_at` is the
|
|
328
|
+
// `updated_at` of the save that wrote them. A row whose two times differ was written by a store
|
|
329
|
+
// that does not know the columns — a volume from before them, or an older image in the Kit bump
|
|
330
|
+
// window (ADR-0069) — and each boot fills those rows again, in one transaction (SQLite's DDL is
|
|
331
|
+
// transactional): a process that stops part of the way leaves no column and no row half done,
|
|
332
|
+
// so the next boot does it again.
|
|
333
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
334
|
+
try {
|
|
335
|
+
for (const name of ["workflow", "started_at", "summary", "listed_at"]) {
|
|
336
|
+
if (!columns.some((c) => c.name === name)) {
|
|
337
|
+
this.db.exec(`ALTER TABLE machine_snapshots ADD COLUMN ${name} TEXT`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
this.fillListing();
|
|
341
|
+
this.db.exec("COMMIT");
|
|
342
|
+
} catch (err) {
|
|
343
|
+
this.db.exec("ROLLBACK");
|
|
344
|
+
throw err;
|
|
345
|
+
}
|
|
346
|
+
this.db.exec(
|
|
347
|
+
`CREATE INDEX IF NOT EXISTS machine_snapshots_started ON machine_snapshots(started_at DESC, run_id DESC)`,
|
|
348
|
+
);
|
|
349
|
+
this.db.exec(`DROP INDEX IF EXISTS machine_snapshots_dispatch_effect`);
|
|
350
|
+
this.db.exec(
|
|
351
|
+
`CREATE INDEX IF NOT EXISTS machine_snapshots_dispatch_key ON machine_snapshots(dispatch_workflow, dispatch_effect)`,
|
|
352
|
+
);
|
|
353
|
+
// The hook log (ADR-0011): one row per line, kept for `runs.retain` like a finished run (ADR-0065).
|
|
354
|
+
this.db.exec(`
|
|
355
|
+
CREATE TABLE IF NOT EXISTS hook_log (
|
|
356
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
357
|
+
hook TEXT NOT NULL,
|
|
358
|
+
at TEXT NOT NULL,
|
|
359
|
+
kind TEXT NOT NULL,
|
|
360
|
+
status INTEGER NOT NULL,
|
|
361
|
+
delivery TEXT,
|
|
362
|
+
digest TEXT,
|
|
363
|
+
key TEXT,
|
|
364
|
+
detail TEXT
|
|
365
|
+
)
|
|
366
|
+
`);
|
|
367
|
+
this.db.exec(`CREATE INDEX IF NOT EXISTS hook_log_delivery ON hook_log(hook, delivery)`);
|
|
368
|
+
this.db.exec(`CREATE INDEX IF NOT EXISTS hook_log_digest ON hook_log(hook, digest)`);
|
|
369
|
+
this.db.exec(`CREATE INDEX IF NOT EXISTS hook_log_at ON hook_log(at)`);
|
|
370
|
+
// The head of each standing Workflow (ADR-0017): one row for each, kept while the Workflow is.
|
|
371
|
+
this.db.exec(`
|
|
372
|
+
CREATE TABLE IF NOT EXISTS standing (
|
|
373
|
+
workflow TEXT PRIMARY KEY,
|
|
374
|
+
run TEXT NOT NULL,
|
|
375
|
+
since TEXT NOT NULL,
|
|
376
|
+
streak INTEGER NOT NULL DEFAULT 0,
|
|
377
|
+
paused TEXT
|
|
378
|
+
)
|
|
379
|
+
`);
|
|
380
|
+
// The Machine each ended run ran on (ADR-0069): one row for each distinct doc, and the run's
|
|
381
|
+
// row names it. The run sweep deletes a doc that no kept row names.
|
|
382
|
+
this.db.exec(`CREATE TABLE IF NOT EXISTS run_machines (key TEXT PRIMARY KEY, doc TEXT NOT NULL)`);
|
|
383
|
+
const now = this.db.prepare(`PRAGMA table_info(machine_snapshots)`).all() as Array<{ name: string }>;
|
|
384
|
+
if (!now.some((c) => c.name === "machine_key")) {
|
|
385
|
+
this.db.exec(`ALTER TABLE machine_snapshots ADD COLUMN machine_key TEXT`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** A volume a store before the Workflow column wrote: copy the Workflow out of each keyed row, so
|
|
390
|
+
* its key keeps answering. The blob is parsed here, not by `json_extract`, which refuses a blob
|
|
391
|
+
* nested about a thousand deep. */
|
|
392
|
+
private fillDispatchWorkflow(): void {
|
|
393
|
+
const rows = this.db
|
|
394
|
+
.prepare(`SELECT run_id, snapshot FROM machine_snapshots WHERE dispatch_effect IS NOT NULL`)
|
|
395
|
+
.all() as Array<{ run_id: string; snapshot: string | null }>;
|
|
396
|
+
const update = this.db.prepare(`UPDATE machine_snapshots SET dispatch_workflow = ? WHERE run_id = ?`);
|
|
397
|
+
for (const row of rows) {
|
|
398
|
+
if (row.snapshot === null) continue;
|
|
399
|
+
let blob: unknown;
|
|
400
|
+
try {
|
|
401
|
+
blob = JSON.parse(row.snapshot);
|
|
402
|
+
} catch {
|
|
403
|
+
continue; // A blob that does not parse names no Workflow, and restore reports it.
|
|
404
|
+
}
|
|
405
|
+
update.run(dispatchWorkflowOf(blob), row.run_id);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** The rows whose listing columns a store that does not know them left out of date: copy them
|
|
410
|
+
* out of each blob again. A row whose blob does not parse keeps no Workflow, and is not listed. A
|
|
411
|
+
* blob with no `startedAt` keeps the start that the row has. */
|
|
412
|
+
private fillListing(): void {
|
|
413
|
+
const rows = this.db
|
|
414
|
+
.prepare(
|
|
415
|
+
`SELECT run_id, snapshot, updated_at FROM machine_snapshots
|
|
416
|
+
WHERE snapshot IS NOT NULL AND listed_at IS NOT updated_at`,
|
|
417
|
+
)
|
|
418
|
+
.all() as Array<{ run_id: string; snapshot: string; updated_at: string | null }>;
|
|
419
|
+
const update = this.db.prepare(
|
|
420
|
+
`UPDATE machine_snapshots SET workflow = ?,
|
|
421
|
+
started_at = CASE WHEN ? THEN ? ELSE COALESCE(started_at, ?) END,
|
|
422
|
+
summary = ?, listed_at = updated_at WHERE run_id = ?`,
|
|
423
|
+
);
|
|
424
|
+
for (const row of rows) {
|
|
425
|
+
let blob: unknown;
|
|
426
|
+
try {
|
|
427
|
+
blob = JSON.parse(row.snapshot);
|
|
428
|
+
} catch {
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
const listing = listingOf(blob, row.updated_at ?? new Date().toISOString());
|
|
432
|
+
const own = hasStartedAt(blob) ? 1 : 0;
|
|
433
|
+
update.run(listing.workflow, own, listing.startedAt, listing.startedAt, listing.summary, row.run_id);
|
|
434
|
+
}
|
|
124
435
|
}
|
|
125
436
|
|
|
126
437
|
async load(runId: string): Promise<StoredSnapshot | null> {
|
|
127
438
|
const row = this.db
|
|
128
|
-
.prepare(
|
|
439
|
+
.prepare(
|
|
440
|
+
`SELECT run_id, status, snapshot, reason, updated_at, started_at FROM machine_snapshots WHERE run_id = ?`,
|
|
441
|
+
)
|
|
129
442
|
.get(runId) as Row | undefined;
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const stored: StoredSnapshot = {
|
|
133
|
-
runId: row.run_id,
|
|
134
|
-
status: row.status,
|
|
135
|
-
snapshot: row.snapshot === null ? null : JSON.parse(row.snapshot),
|
|
136
|
-
};
|
|
137
|
-
if (row.reason !== null) stored.reason = row.reason;
|
|
138
|
-
return stored;
|
|
443
|
+
return row ? storedOf(row) : null;
|
|
139
444
|
}
|
|
140
445
|
|
|
141
446
|
async live(): Promise<StoredSnapshot[]> {
|
|
142
447
|
const rows = this.db
|
|
143
448
|
.prepare(
|
|
144
|
-
`SELECT run_id, status, snapshot, reason, updated_at FROM machine_snapshots
|
|
449
|
+
`SELECT run_id, status, snapshot, reason, updated_at, started_at FROM machine_snapshots
|
|
145
450
|
WHERE status = 'live' ORDER BY rowid`,
|
|
146
451
|
)
|
|
147
452
|
.all() as Row[];
|
|
148
|
-
return rows.map(
|
|
149
|
-
const stored: StoredSnapshot = {
|
|
150
|
-
runId: row.run_id,
|
|
151
|
-
status: row.status,
|
|
152
|
-
snapshot: row.snapshot === null ? null : JSON.parse(row.snapshot),
|
|
153
|
-
};
|
|
154
|
-
if (row.reason !== null) stored.reason = row.reason;
|
|
155
|
-
return stored;
|
|
156
|
-
});
|
|
453
|
+
return rows.map(storedOf);
|
|
157
454
|
}
|
|
158
455
|
|
|
159
456
|
async liveAssemblies(): Promise<Map<string, string[]>> {
|
|
@@ -189,24 +486,119 @@ export class SqliteSnapshotStore implements SnapshotStore {
|
|
|
189
486
|
return rows.map((row) => row.run_id);
|
|
190
487
|
}
|
|
191
488
|
|
|
192
|
-
async findByDispatch(
|
|
193
|
-
const row = this.db.prepare(FIND_BY_DISPATCH_SQL).get(
|
|
489
|
+
async findByDispatch(workflow: string, key: string): Promise<string | undefined> {
|
|
490
|
+
const row = this.db.prepare(FIND_BY_DISPATCH_SQL).get(workflow, key) as { run_id: string } | undefined;
|
|
194
491
|
return row?.run_id;
|
|
195
492
|
}
|
|
196
493
|
|
|
197
494
|
async save(runId: string, snapshot: unknown, status = "live"): Promise<void> {
|
|
495
|
+
const now = new Date().toISOString();
|
|
496
|
+
const listing = listingOf(snapshot, now);
|
|
497
|
+
// A blob with no `startedAt` keeps the start that the row has: its first save's time.
|
|
198
498
|
this.db
|
|
199
499
|
.prepare(
|
|
200
|
-
`INSERT INTO machine_snapshots
|
|
201
|
-
|
|
500
|
+
`INSERT INTO machine_snapshots
|
|
501
|
+
(run_id, status, snapshot, reason, updated_at, dispatch_effect, dispatch_workflow, workflow, started_at, summary, listed_at)
|
|
502
|
+
VALUES (?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?)
|
|
202
503
|
ON CONFLICT(run_id) DO UPDATE SET
|
|
203
504
|
status = excluded.status,
|
|
204
505
|
snapshot = excluded.snapshot,
|
|
205
506
|
reason = NULL,
|
|
206
507
|
updated_at = excluded.updated_at,
|
|
207
|
-
dispatch_effect = excluded.dispatch_effect
|
|
508
|
+
dispatch_effect = excluded.dispatch_effect,
|
|
509
|
+
dispatch_workflow = excluded.dispatch_workflow,
|
|
510
|
+
workflow = excluded.workflow,
|
|
511
|
+
started_at = CASE WHEN ? THEN excluded.started_at ELSE COALESCE(started_at, excluded.started_at) END,
|
|
512
|
+
summary = excluded.summary,
|
|
513
|
+
listed_at = excluded.listed_at`,
|
|
514
|
+
)
|
|
515
|
+
.run(
|
|
516
|
+
runId,
|
|
517
|
+
status,
|
|
518
|
+
JSON.stringify(snapshot),
|
|
519
|
+
now,
|
|
520
|
+
dispatchEffectOf(snapshot),
|
|
521
|
+
dispatchWorkflowOf(snapshot),
|
|
522
|
+
listing.workflow,
|
|
523
|
+
listing.startedAt,
|
|
524
|
+
listing.summary,
|
|
525
|
+
now,
|
|
526
|
+
hasStartedAt(snapshot) ? 1 : 0,
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
async ended(query: EndedQuery): Promise<EndedRow[]> {
|
|
531
|
+
return this.listRows(`status != 'live'`, query);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
async liveRows(query: EndedQuery): Promise<EndedRow[]> {
|
|
535
|
+
const { statuses: _statuses, ...rest } = query;
|
|
536
|
+
return this.listRows(`status = 'live'`, rest);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
private listRows(status: string, query: EndedQuery): EndedRow[] {
|
|
540
|
+
const where = [status, `workflow IS NOT NULL`, `started_at IS NOT NULL`];
|
|
541
|
+
const args: Array<string | number> = [];
|
|
542
|
+
if (query.statuses !== undefined) {
|
|
543
|
+
if (query.statuses.length === 0) return [];
|
|
544
|
+
where.push(`status IN (${query.statuses.map(() => "?").join(", ")})`);
|
|
545
|
+
args.push(...query.statuses);
|
|
546
|
+
}
|
|
547
|
+
if (query.workflows !== undefined) {
|
|
548
|
+
if (query.workflows.length === 0) return [];
|
|
549
|
+
const marks = query.workflows.map(() => "?").join(", ");
|
|
550
|
+
// A grant of a Chart wrapper opens its Charts, `<wrapper>/<chart>` (ADR-0078).
|
|
551
|
+
where.push(
|
|
552
|
+
`(workflow IN (${marks}) OR (instr(workflow, '/') > 0 AND substr(workflow, 1, instr(workflow, '/') - 1) IN (${marks})))`,
|
|
553
|
+
);
|
|
554
|
+
args.push(...query.workflows, ...query.workflows);
|
|
555
|
+
}
|
|
556
|
+
if (query.workflow !== undefined) {
|
|
557
|
+
where.push(`workflow = ?`);
|
|
558
|
+
args.push(query.workflow);
|
|
559
|
+
}
|
|
560
|
+
if (query.since !== undefined) {
|
|
561
|
+
where.push(`started_at >= ?`);
|
|
562
|
+
args.push(query.since);
|
|
563
|
+
}
|
|
564
|
+
if (query.after !== undefined) {
|
|
565
|
+
where.push(`(started_at < ? OR (started_at = ? AND run_id < ?))`);
|
|
566
|
+
args.push(query.after.startedAt, query.after.startedAt, query.after.runId);
|
|
567
|
+
}
|
|
568
|
+
const rows = this.db
|
|
569
|
+
.prepare(
|
|
570
|
+
`SELECT run_id, workflow, status, reason, started_at, updated_at, summary FROM machine_snapshots
|
|
571
|
+
WHERE ${where.join(" AND ")} ORDER BY started_at DESC, run_id DESC LIMIT ?`,
|
|
208
572
|
)
|
|
209
|
-
.
|
|
573
|
+
.all(...args, query.limit) as Array<{
|
|
574
|
+
run_id: string;
|
|
575
|
+
workflow: string;
|
|
576
|
+
status: string;
|
|
577
|
+
reason: string | null;
|
|
578
|
+
started_at: string;
|
|
579
|
+
updated_at: string;
|
|
580
|
+
summary: string | null;
|
|
581
|
+
}>;
|
|
582
|
+
return rows.map((row) => {
|
|
583
|
+
let summary: RowSummary = {};
|
|
584
|
+
try {
|
|
585
|
+
summary = row.summary === null ? {} : (JSON.parse(row.summary) as RowSummary);
|
|
586
|
+
} catch {
|
|
587
|
+
// A summary that does not parse lists the columns alone.
|
|
588
|
+
}
|
|
589
|
+
const reason = row.reason ?? summary.failure ?? summary.fault;
|
|
590
|
+
return {
|
|
591
|
+
runId: row.run_id,
|
|
592
|
+
workflow: row.workflow,
|
|
593
|
+
status: row.status,
|
|
594
|
+
...(reason !== undefined ? { reason } : {}),
|
|
595
|
+
startedAt: row.started_at,
|
|
596
|
+
endedAt: row.updated_at,
|
|
597
|
+
...(summary.startedBy !== undefined ? { startedBy: summary.startedBy } : {}),
|
|
598
|
+
...(summary.assembly !== undefined ? { assembly: summary.assembly } : {}),
|
|
599
|
+
...(summary.dispatchedBy !== undefined ? { dispatchedBy: summary.dispatchedBy } : {}),
|
|
600
|
+
};
|
|
601
|
+
});
|
|
210
602
|
}
|
|
211
603
|
|
|
212
604
|
async markLost(runId: string, reason: string): Promise<void> {
|
|
@@ -217,7 +609,8 @@ export class SqliteSnapshotStore implements SnapshotStore {
|
|
|
217
609
|
ON CONFLICT(run_id) DO UPDATE SET
|
|
218
610
|
status = 'lost',
|
|
219
611
|
reason = excluded.reason,
|
|
220
|
-
updated_at = excluded.updated_at
|
|
612
|
+
updated_at = excluded.updated_at,
|
|
613
|
+
listed_at = CASE WHEN listed_at IS updated_at THEN excluded.updated_at ELSE listed_at END`,
|
|
221
614
|
)
|
|
222
615
|
.run(runId, reason, new Date().toISOString());
|
|
223
616
|
}
|
|
@@ -225,18 +618,142 @@ export class SqliteSnapshotStore implements SnapshotStore {
|
|
|
225
618
|
/** An UPDATE, not an upsert: a drifted run is one we already have a snapshot for, and the whole
|
|
226
619
|
* point is to keep it. A row that is not there is not a drifted run — nothing to record. */
|
|
227
620
|
async markDrifted(runId: string, reason: string): Promise<void> {
|
|
621
|
+
const now = new Date().toISOString();
|
|
228
622
|
this.db
|
|
229
|
-
.prepare(
|
|
230
|
-
|
|
623
|
+
.prepare(
|
|
624
|
+
// The blob is not touched, so listing columns that were current stay current.
|
|
625
|
+
`UPDATE machine_snapshots SET status = 'drifted', reason = ?, updated_at = ?,
|
|
626
|
+
listed_at = CASE WHEN listed_at IS updated_at THEN ? ELSE listed_at END WHERE run_id = ?`,
|
|
627
|
+
)
|
|
628
|
+
.run(reason, now, now, runId);
|
|
231
629
|
}
|
|
232
630
|
|
|
233
631
|
/** `updated_at` is an ISO-8601 UTC string, so a string comparison is a time comparison. A
|
|
234
|
-
* terminal row's `updated_at` is its terminal write — the age
|
|
235
|
-
|
|
632
|
+
* terminal row's `updated_at` is its terminal write — the age `runs.retain` counts from. The hook
|
|
633
|
+
* log's lines go by the same time (ADR-0011); the count is of runs alone. */
|
|
634
|
+
async sweep(olderThan: Date): Promise<string[]> {
|
|
635
|
+
// The run a standing head names stays (ADR-0017): a paused Pool's held items are in its blob.
|
|
636
|
+
const swept = this.db
|
|
637
|
+
.prepare(
|
|
638
|
+
`DELETE FROM machine_snapshots WHERE status != 'live' AND updated_at < ?
|
|
639
|
+
AND run_id NOT IN (SELECT run FROM standing) RETURNING run_id`,
|
|
640
|
+
)
|
|
641
|
+
.all(olderThan.toISOString()) as Array<{ run_id: string }>;
|
|
642
|
+
this.db.prepare(`DELETE FROM hook_log WHERE at < ?`).run(olderThan.toISOString());
|
|
643
|
+
if (swept.length) {
|
|
644
|
+
this.db
|
|
645
|
+
.prepare(
|
|
646
|
+
`DELETE FROM run_machines WHERE key NOT IN
|
|
647
|
+
(SELECT machine_key FROM machine_snapshots WHERE machine_key IS NOT NULL)`,
|
|
648
|
+
)
|
|
649
|
+
.run();
|
|
650
|
+
}
|
|
651
|
+
return swept.map((r) => r.run_id);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
keepMachine(runId: string, key: string, doc: unknown): void {
|
|
655
|
+
this.db.prepare(`INSERT OR IGNORE INTO run_machines (key, doc) VALUES (?, ?)`).run(key, JSON.stringify(doc));
|
|
656
|
+
this.db.prepare(`UPDATE machine_snapshots SET machine_key = ? WHERE run_id = ?`).run(key, runId);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
keptMachine(runId: string): { workflow: string; doc: unknown } | undefined {
|
|
660
|
+
const row = this.db
|
|
661
|
+
.prepare(
|
|
662
|
+
`SELECT s.workflow, m.doc FROM machine_snapshots s JOIN run_machines m ON m.key = s.machine_key
|
|
663
|
+
WHERE s.run_id = ?`,
|
|
664
|
+
)
|
|
665
|
+
.get(runId) as { workflow: string | null; doc: string } | undefined;
|
|
666
|
+
return row?.workflow ? { workflow: row.workflow, doc: JSON.parse(row.doc) } : undefined;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
claimHookDelivery(hook: string, delivery: string, digest: string): number | undefined {
|
|
670
|
+
const kinds = CLAIMED_KINDS.map(() => "?").join(", ");
|
|
671
|
+
const held = this.db
|
|
672
|
+
.prepare(`SELECT 1 FROM hook_log WHERE hook = ? AND (delivery = ? OR digest = ?) AND kind IN (${kinds}) LIMIT 1`)
|
|
673
|
+
.get(hook, delivery, digest, ...CLAIMED_KINDS);
|
|
674
|
+
if (held) return undefined;
|
|
236
675
|
const result = this.db
|
|
237
|
-
.prepare(`
|
|
238
|
-
.run(
|
|
239
|
-
return Number(result.
|
|
676
|
+
.prepare(`INSERT INTO hook_log (hook, at, kind, status, delivery, digest) VALUES (?, ?, 'pending', 0, ?, ?)`)
|
|
677
|
+
.run(hook, new Date().toISOString(), delivery, digest);
|
|
678
|
+
return Number(result.lastInsertRowid);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
writeHookLine(line: HookLogLine, claimed?: number): void {
|
|
682
|
+
const detail = line.detail === undefined ? null : JSON.stringify(line.detail);
|
|
683
|
+
if (claimed !== undefined) {
|
|
684
|
+
this.db
|
|
685
|
+
.prepare(`UPDATE hook_log SET kind = ?, status = ?, key = ?, detail = ? WHERE id = ?`)
|
|
686
|
+
.run(line.kind, line.status, line.key ?? null, detail, claimed);
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
this.db
|
|
690
|
+
.prepare(
|
|
691
|
+
`INSERT INTO hook_log (hook, at, kind, status, delivery, digest, key, detail) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
692
|
+
)
|
|
693
|
+
.run(
|
|
694
|
+
line.hook,
|
|
695
|
+
line.at,
|
|
696
|
+
line.kind,
|
|
697
|
+
line.status,
|
|
698
|
+
line.delivery ?? null,
|
|
699
|
+
line.digest ?? null,
|
|
700
|
+
line.key ?? null,
|
|
701
|
+
detail,
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
hookLog(hook: string, limit: number): HookLogLine[] {
|
|
706
|
+
const rows = this.db
|
|
707
|
+
.prepare(
|
|
708
|
+
`SELECT hook, at, kind, status, delivery, digest, key, detail FROM hook_log WHERE hook = ? ORDER BY id DESC LIMIT ?`,
|
|
709
|
+
)
|
|
710
|
+
.all(hook, limit) as Array<{
|
|
711
|
+
hook: string;
|
|
712
|
+
at: string;
|
|
713
|
+
kind: HookLogLine["kind"];
|
|
714
|
+
status: number;
|
|
715
|
+
delivery: string | null;
|
|
716
|
+
digest: string | null;
|
|
717
|
+
key: string | null;
|
|
718
|
+
detail: string | null;
|
|
719
|
+
}>;
|
|
720
|
+
return rows.map((r) => ({
|
|
721
|
+
hook: r.hook,
|
|
722
|
+
at: r.at,
|
|
723
|
+
kind: r.kind,
|
|
724
|
+
status: r.status,
|
|
725
|
+
...(r.delivery !== null ? { delivery: r.delivery } : {}),
|
|
726
|
+
...(r.digest !== null ? { digest: r.digest } : {}),
|
|
727
|
+
...(r.key !== null ? { key: r.key } : {}),
|
|
728
|
+
...(r.detail !== null ? { detail: JSON.parse(r.detail) as unknown } : {}),
|
|
729
|
+
}));
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
standingHeads(): StandingHead[] {
|
|
733
|
+
const rows = this.db.prepare(`SELECT workflow, run, since, streak, paused FROM standing`).all() as Array<{
|
|
734
|
+
workflow: string;
|
|
735
|
+
run: string;
|
|
736
|
+
since: string;
|
|
737
|
+
streak: number;
|
|
738
|
+
paused: string | null;
|
|
739
|
+
}>;
|
|
740
|
+
return rows.map((r) => ({
|
|
741
|
+
workflow: r.workflow,
|
|
742
|
+
run: r.run,
|
|
743
|
+
since: r.since,
|
|
744
|
+
streak: Number(r.streak),
|
|
745
|
+
...(r.paused === "cancel" || r.paused === "errors" || r.paused === "bound" ? { paused: r.paused } : {}),
|
|
746
|
+
}));
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
writeStanding(head: StandingHead): void {
|
|
750
|
+
this.db
|
|
751
|
+
.prepare(
|
|
752
|
+
`INSERT INTO standing (workflow, run, since, streak, paused) VALUES (?, ?, ?, ?, ?)
|
|
753
|
+
ON CONFLICT(workflow) DO UPDATE SET
|
|
754
|
+
run = excluded.run, since = excluded.since, streak = excluded.streak, paused = excluded.paused`,
|
|
755
|
+
)
|
|
756
|
+
.run(head.workflow, head.run, head.since, head.streak, head.paused ?? null);
|
|
240
757
|
}
|
|
241
758
|
|
|
242
759
|
async close(): Promise<void> {
|