@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
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
// The kept conversations (ADR-0023, ADR-0065): what the Harness sends of each conversation while
|
|
2
|
+
// its Turn runs, kept in `conversations.db` beside `state.db` on the state volume, for as long as
|
|
3
|
+
// the run is kept. A second SQLite file, because its writes are larger and more frequent than a
|
|
4
|
+
// snapshot's, and its sweep frees pages: neither may slow the snapshot's synchronous write.
|
|
5
|
+
//
|
|
6
|
+
// conversations one row per conversation: its run, its Agent, the bytes kept, `cut`, `swept`
|
|
7
|
+
// entries one row per entry, keyed by (instance_id, seq): a batch sent again adds nothing
|
|
8
|
+
//
|
|
9
|
+
// The rows are plain JSON (`entry` is the entry as the Harness sent it, with the Agent from the
|
|
10
|
+
// ledger), so a person or a tool can query the file with SQLite alone. The file is made with
|
|
11
|
+
// `auto_vacuum = INCREMENTAL`, and each delete frees its pages, so the file gets smaller after a
|
|
12
|
+
// sweep. There is no compression now: when it comes, it is Node's own zstd on the entry text, with
|
|
13
|
+
// the queryable columns kept.
|
|
14
|
+
//
|
|
15
|
+
// The cap is per conversation: an entry that would take it past `capBytes` is not kept. One marker
|
|
16
|
+
// entry takes its place, and every later entry of that conversation is dropped. The cap keeps the
|
|
17
|
+
// start of the conversation, which tells why the Agent did what it did.
|
|
18
|
+
|
|
19
|
+
import { readdirSync, statfsSync, statSync } from "node:fs";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import { DatabaseSync } from "node:sqlite";
|
|
22
|
+
import type { SnapshotStore } from "./snapshot-store.ts";
|
|
23
|
+
|
|
24
|
+
/** One entry of a kept conversation (ADR-0023): one body of the print, its label as `kind` and the
|
|
25
|
+
* rest as `text`. `seq` goes up for as long as the Instance ID is kept; a new record starts it at
|
|
26
|
+
* the clock (the Harness's keep.ts). */
|
|
27
|
+
export type ConversationEntry = {
|
|
28
|
+
id: string;
|
|
29
|
+
seq: number;
|
|
30
|
+
at: string;
|
|
31
|
+
agent: string;
|
|
32
|
+
kind: string;
|
|
33
|
+
text: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** What an entry is, as the Harness sends it: the Agent comes from the ledger, not the body. */
|
|
37
|
+
export type SentEntry = Omit<ConversationEntry, "agent">;
|
|
38
|
+
|
|
39
|
+
/** One kept conversation, as `GET /runs/:runId/conversations` lists it (without `open`, which is the
|
|
40
|
+
* host's to say). */
|
|
41
|
+
export type KeptConversation = {
|
|
42
|
+
agent: string;
|
|
43
|
+
instanceId: string;
|
|
44
|
+
bytes: number;
|
|
45
|
+
cut: boolean;
|
|
46
|
+
/** Why the conversation's entries are gone while its run is kept: the space guard swept them. */
|
|
47
|
+
swept?: typeof SWEPT_NO_SPACE;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** The one reason the guard sweeps a conversation (ADR-0065). */
|
|
51
|
+
export const SWEPT_NO_SPACE = "no space";
|
|
52
|
+
|
|
53
|
+
/** What a run says when the guard swept its conversations (ADR-0065). */
|
|
54
|
+
export const CONVERSATION_SWEPT = "conversation swept: no space" as const;
|
|
55
|
+
|
|
56
|
+
/** The cap of one kept conversation (ADR-0023). */
|
|
57
|
+
export const CONVERSATION_CAP_BYTES = 8 * 1024 * 1024;
|
|
58
|
+
|
|
59
|
+
/** The `kind` of the marker entry that a cut leaves (ADR-0023). */
|
|
60
|
+
export const CUT_KIND = "cut";
|
|
61
|
+
|
|
62
|
+
/** An append's outcome: how many entries are new, and whether the conversation is cut. */
|
|
63
|
+
export type AppendResult = { kept: number; cut: boolean };
|
|
64
|
+
|
|
65
|
+
type ConversationRow = {
|
|
66
|
+
run_id: string;
|
|
67
|
+
agent: string;
|
|
68
|
+
bytes: number;
|
|
69
|
+
cut: number;
|
|
70
|
+
swept: string | null;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/** The listener of one conversation's new entries (the SSE follow). */
|
|
74
|
+
type EntryListener = (entries: ConversationEntry[]) => void;
|
|
75
|
+
|
|
76
|
+
export class ConversationStore {
|
|
77
|
+
private readonly db: DatabaseSync;
|
|
78
|
+
private readonly capBytes: number;
|
|
79
|
+
private readonly listeners = new Map<string, Set<EntryListener>>();
|
|
80
|
+
/** Set by the guard: the volume is past its last bound, and no entry is kept (ADR-0065). */
|
|
81
|
+
#full = false;
|
|
82
|
+
|
|
83
|
+
constructor(path: string, opts: { capBytes?: number } = {}) {
|
|
84
|
+
this.db = new DatabaseSync(path);
|
|
85
|
+
this.capBytes = opts.capBytes ?? CONVERSATION_CAP_BYTES;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
init(): void {
|
|
89
|
+
// Before the first table, or it has no effect: a file's vacuum mode is set when it is made.
|
|
90
|
+
this.db.exec(`PRAGMA auto_vacuum = INCREMENTAL`);
|
|
91
|
+
this.db.exec(`PRAGMA journal_mode = WAL`);
|
|
92
|
+
this.db.exec(`PRAGMA synchronous = NORMAL`);
|
|
93
|
+
this.db.exec(`
|
|
94
|
+
CREATE TABLE IF NOT EXISTS conversations (
|
|
95
|
+
instance_id TEXT PRIMARY KEY,
|
|
96
|
+
run_id TEXT NOT NULL,
|
|
97
|
+
agent TEXT NOT NULL,
|
|
98
|
+
bytes INTEGER NOT NULL DEFAULT 0,
|
|
99
|
+
cut INTEGER NOT NULL DEFAULT 0,
|
|
100
|
+
swept TEXT,
|
|
101
|
+
created_at TEXT NOT NULL,
|
|
102
|
+
updated_at TEXT NOT NULL
|
|
103
|
+
)
|
|
104
|
+
`);
|
|
105
|
+
this.db.exec(`CREATE INDEX IF NOT EXISTS conversations_run ON conversations (run_id, created_at)`);
|
|
106
|
+
this.db.exec(`
|
|
107
|
+
CREATE TABLE IF NOT EXISTS entries (
|
|
108
|
+
instance_id TEXT NOT NULL,
|
|
109
|
+
seq INTEGER NOT NULL,
|
|
110
|
+
id TEXT NOT NULL,
|
|
111
|
+
entry TEXT NOT NULL,
|
|
112
|
+
PRIMARY KEY (instance_id, seq)
|
|
113
|
+
) WITHOUT ROWID
|
|
114
|
+
`);
|
|
115
|
+
// The entry id dedupes (ADR-0023): a batch sent again adds nothing, and an entry of a new
|
|
116
|
+
// record whose seq meets an old one (a pod on a node whose clock is behind) is still kept.
|
|
117
|
+
this.db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS entries_id ON entries (instance_id, id)`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The volume is past the guard's last bound: no entry is kept, and each conversation that gets
|
|
121
|
+
* one more is cut with a marker, as at its cap (ADR-0065). */
|
|
122
|
+
get full(): boolean {
|
|
123
|
+
return this.#full;
|
|
124
|
+
}
|
|
125
|
+
set full(value: boolean) {
|
|
126
|
+
this.#full = value;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Keep a batch of one conversation's entries. An entry whose `(instanceId, seq)` is kept already
|
|
131
|
+
* adds nothing, so a batch sent again is harmless. An entry that would take the conversation past
|
|
132
|
+
* its cap, or that comes while the volume is full, is replaced by one marker, and the conversation
|
|
133
|
+
* is cut: no later entry is kept. A conversation that the guard swept keeps nothing more.
|
|
134
|
+
*/
|
|
135
|
+
append(runId: string, instanceId: string, agent: string, sent: readonly SentEntry[]): AppendResult {
|
|
136
|
+
const now = new Date().toISOString();
|
|
137
|
+
const added: ConversationEntry[] = [];
|
|
138
|
+
let kept = 0;
|
|
139
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
140
|
+
let row: ConversationRow;
|
|
141
|
+
try {
|
|
142
|
+
const found = this.row(instanceId);
|
|
143
|
+
if (!found) {
|
|
144
|
+
this.db
|
|
145
|
+
.prepare(
|
|
146
|
+
`INSERT INTO conversations (instance_id, run_id, agent, bytes, cut, created_at, updated_at)
|
|
147
|
+
VALUES (?, ?, ?, 0, 0, ?, ?)`,
|
|
148
|
+
)
|
|
149
|
+
.run(instanceId, runId, agent, now, now);
|
|
150
|
+
}
|
|
151
|
+
row = found ?? { run_id: runId, agent, bytes: 0, cut: 0, swept: null };
|
|
152
|
+
if (row.cut || row.swept !== null) {
|
|
153
|
+
this.db.exec("COMMIT");
|
|
154
|
+
return { kept: 0, cut: row.cut === 1 };
|
|
155
|
+
}
|
|
156
|
+
const hasId = this.db.prepare(`SELECT 1 FROM entries WHERE instance_id = ? AND id = ?`);
|
|
157
|
+
const hasSeq = this.db.prepare(`SELECT 1 FROM entries WHERE instance_id = ? AND seq = ?`);
|
|
158
|
+
const last = this.db.prepare(`SELECT MAX(seq) AS seq FROM entries WHERE instance_id = ?`);
|
|
159
|
+
const insertRow = this.db.prepare(`INSERT INTO entries (instance_id, seq, id, entry) VALUES (?, ?, ?, ?)`);
|
|
160
|
+
for (const e of [...sent].sort((a, b) => a.seq - b.seq)) {
|
|
161
|
+
if (hasId.get(instanceId, e.id)) continue;
|
|
162
|
+
// Another entry has this seq: a new record whose clock is behind. Keep it after the last.
|
|
163
|
+
const seq = hasSeq.get(instanceId, e.seq)
|
|
164
|
+
? ((last.get(instanceId) as { seq: number | null }).seq ?? 0) + 1
|
|
165
|
+
: e.seq;
|
|
166
|
+
const entry: ConversationEntry = { id: e.id, seq, at: e.at, agent, kind: e.kind, text: e.text };
|
|
167
|
+
const json = JSON.stringify(entry);
|
|
168
|
+
const size = Buffer.byteLength(json);
|
|
169
|
+
if (this.#full || row.bytes + size > this.capBytes) {
|
|
170
|
+
const marker: ConversationEntry = {
|
|
171
|
+
id: `${CUT_KIND}-${seq}`,
|
|
172
|
+
seq,
|
|
173
|
+
at: now,
|
|
174
|
+
agent,
|
|
175
|
+
kind: CUT_KIND,
|
|
176
|
+
text: this.#full
|
|
177
|
+
? "the Orchestrator keeps no more of this conversation: its state volume is full (ADR-0065)"
|
|
178
|
+
: `the Orchestrator keeps no more of this conversation: it is at its cap of ${capText(this.capBytes)} (ADR-0023)`,
|
|
179
|
+
};
|
|
180
|
+
insertRow.run(instanceId, seq, marker.id, JSON.stringify(marker));
|
|
181
|
+
added.push(marker);
|
|
182
|
+
row.cut = 1;
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
insertRow.run(instanceId, seq, e.id, json);
|
|
186
|
+
added.push(entry);
|
|
187
|
+
kept += 1;
|
|
188
|
+
row.bytes += size;
|
|
189
|
+
}
|
|
190
|
+
this.db
|
|
191
|
+
.prepare(`UPDATE conversations SET bytes = ?, cut = ?, updated_at = ? WHERE instance_id = ?`)
|
|
192
|
+
.run(row.bytes, row.cut, now, instanceId);
|
|
193
|
+
this.db.exec("COMMIT");
|
|
194
|
+
} catch (err) {
|
|
195
|
+
this.db.exec("ROLLBACK");
|
|
196
|
+
throw err;
|
|
197
|
+
}
|
|
198
|
+
if (added.length > 0) for (const listener of this.listeners.get(instanceId) ?? []) listener(added);
|
|
199
|
+
return { kept, cut: row.cut === 1 };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** A run's kept conversations, the first kept first. */
|
|
203
|
+
list(runId: string): KeptConversation[] {
|
|
204
|
+
const rows = this.db
|
|
205
|
+
.prepare(
|
|
206
|
+
`SELECT instance_id, agent, bytes, cut, swept FROM conversations WHERE run_id = ?
|
|
207
|
+
ORDER BY created_at, instance_id`,
|
|
208
|
+
)
|
|
209
|
+
.all(runId) as Array<{ instance_id: string; agent: string; bytes: number; cut: number; swept: string | null }>;
|
|
210
|
+
return rows.map((r) => ({
|
|
211
|
+
agent: r.agent,
|
|
212
|
+
instanceId: r.instance_id,
|
|
213
|
+
bytes: r.bytes,
|
|
214
|
+
cut: r.cut === 1,
|
|
215
|
+
...(r.swept !== null ? { swept: SWEPT_NO_SPACE } : {}),
|
|
216
|
+
}));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** One kept conversation of a run, or undefined when the run has none of that id. */
|
|
220
|
+
conversation(runId: string, instanceId: string): KeptConversation | undefined {
|
|
221
|
+
return this.list(runId).find((c) => c.instanceId === instanceId);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** The entries of a conversation after `after` (-1: from the first), in order, at most `limit`. */
|
|
225
|
+
entries(instanceId: string, after: number, limit: number): ConversationEntry[] {
|
|
226
|
+
const rows = this.db
|
|
227
|
+
.prepare(`SELECT entry FROM entries WHERE instance_id = ? AND seq > ? ORDER BY seq LIMIT ?`)
|
|
228
|
+
.all(instanceId, after, limit) as Array<{ entry: string }>;
|
|
229
|
+
return rows.map((r) => JSON.parse(r.entry) as ConversationEntry);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** The runs, of `runIds`, whose conversations the guard swept (ADR-0065). */
|
|
233
|
+
sweptRuns(runIds: readonly string[]): Set<string> {
|
|
234
|
+
const swept = new Set<string>();
|
|
235
|
+
const query = this.db.prepare(`SELECT 1 FROM conversations WHERE run_id = ? AND swept IS NOT NULL LIMIT 1`);
|
|
236
|
+
for (const runId of runIds) if (query.get(runId)) swept.add(runId);
|
|
237
|
+
return swept;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Hear the new entries of one conversation. Returns the unsubscribe. */
|
|
241
|
+
follow(instanceId: string, listener: EntryListener): () => void {
|
|
242
|
+
let set = this.listeners.get(instanceId);
|
|
243
|
+
if (!set) this.listeners.set(instanceId, (set = new Set()));
|
|
244
|
+
set.add(listener);
|
|
245
|
+
return () => {
|
|
246
|
+
set.delete(listener);
|
|
247
|
+
if (set.size === 0 && this.listeners.get(instanceId) === set) this.listeners.delete(instanceId);
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Delete the conversations of runs that the retention sweep deleted (ADR-0065), and free their
|
|
252
|
+
* pages. */
|
|
253
|
+
deleteRuns(runIds: readonly string[]): void {
|
|
254
|
+
if (runIds.length === 0) return;
|
|
255
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
256
|
+
try {
|
|
257
|
+
const entries = this.db.prepare(
|
|
258
|
+
`DELETE FROM entries WHERE instance_id IN (SELECT instance_id FROM conversations WHERE run_id = ?)`,
|
|
259
|
+
);
|
|
260
|
+
const rows = this.db.prepare(`DELETE FROM conversations WHERE run_id = ?`);
|
|
261
|
+
for (const runId of runIds) {
|
|
262
|
+
entries.run(runId);
|
|
263
|
+
rows.run(runId);
|
|
264
|
+
}
|
|
265
|
+
this.db.exec("COMMIT");
|
|
266
|
+
} catch (err) {
|
|
267
|
+
this.db.exec("ROLLBACK");
|
|
268
|
+
throw err;
|
|
269
|
+
}
|
|
270
|
+
this.vacuum();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Each run that has a kept conversation, swept or not. */
|
|
274
|
+
runIds(): string[] {
|
|
275
|
+
const rows = this.db.prepare(`SELECT DISTINCT run_id FROM conversations ORDER BY run_id`).all() as Array<{
|
|
276
|
+
run_id: string;
|
|
277
|
+
}>;
|
|
278
|
+
return rows.map((r) => r.run_id);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** The runs that have conversations with entries kept, the run whose first conversation is
|
|
282
|
+
* oldest first: the order in which the guard sweeps (ADR-0065). */
|
|
283
|
+
runsOldestFirst(): string[] {
|
|
284
|
+
const rows = this.db
|
|
285
|
+
.prepare(`SELECT run_id FROM conversations WHERE swept IS NULL GROUP BY run_id ORDER BY MIN(created_at), run_id`)
|
|
286
|
+
.all() as Array<{ run_id: string }>;
|
|
287
|
+
return rows.map((r) => r.run_id);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Delete the entries of a run's conversations for space (ADR-0065). The rows stay, marked
|
|
291
|
+
* swept, so the list says why the conversation is gone. */
|
|
292
|
+
sweepForSpace(runId: string): void {
|
|
293
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
294
|
+
try {
|
|
295
|
+
this.db
|
|
296
|
+
.prepare(`DELETE FROM entries WHERE instance_id IN (SELECT instance_id FROM conversations WHERE run_id = ?)`)
|
|
297
|
+
.run(runId);
|
|
298
|
+
this.db
|
|
299
|
+
.prepare(`UPDATE conversations SET swept = ?, bytes = 0, updated_at = ? WHERE run_id = ?`)
|
|
300
|
+
.run(SWEPT_NO_SPACE, new Date().toISOString(), runId);
|
|
301
|
+
this.db.exec("COMMIT");
|
|
302
|
+
} catch (err) {
|
|
303
|
+
this.db.exec("ROLLBACK");
|
|
304
|
+
throw err;
|
|
305
|
+
}
|
|
306
|
+
this.vacuum();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Give the freed pages back to the file system: the file, and the WAL beside it, get smaller. */
|
|
310
|
+
private vacuum(): void {
|
|
311
|
+
this.db.exec(`PRAGMA incremental_vacuum`);
|
|
312
|
+
this.db.exec(`PRAGMA wal_checkpoint(TRUNCATE)`);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private row(instanceId: string): ConversationRow | undefined {
|
|
316
|
+
return this.db
|
|
317
|
+
.prepare(`SELECT run_id, agent, bytes, cut, swept FROM conversations WHERE instance_id = ?`)
|
|
318
|
+
.get(instanceId) as ConversationRow | undefined;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
close(): void {
|
|
322
|
+
this.listeners.clear();
|
|
323
|
+
this.db.close();
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function capText(bytes: number): string {
|
|
328
|
+
return bytes % (1024 * 1024) === 0 ? `${bytes / (1024 * 1024)} MiB` : `${bytes} bytes`;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ---- The space guard (ADR-0065) ------------------------------------------------------------------
|
|
332
|
+
|
|
333
|
+
/** Above this part of the volume, the guard sweeps the conversations of ended runs. */
|
|
334
|
+
export const GUARD_SWEEP_AT = 0.8;
|
|
335
|
+
/** Above this part, with no conversation of an ended run left, no entry is kept. */
|
|
336
|
+
export const GUARD_FULL_AT = 0.95;
|
|
337
|
+
|
|
338
|
+
/** What the guard reads and does. Injected, so a test sets the sizes. */
|
|
339
|
+
export type SpaceGuardPort = {
|
|
340
|
+
conversations: ConversationStore;
|
|
341
|
+
/** The bytes of the files on the state volume: both databases with their `-wal` files, the
|
|
342
|
+
* Assemblies and the Charts. */
|
|
343
|
+
used(): number;
|
|
344
|
+
/** The size of the state volume: the smaller of the claim and what `statfs` says. */
|
|
345
|
+
capacity(): number;
|
|
346
|
+
/** Whether a run is live. A live run's conversations are never swept for space. */
|
|
347
|
+
isLive(runId: string): Promise<boolean>;
|
|
348
|
+
/** Hears each run whose conversations went. */
|
|
349
|
+
onSwept?: (runId: string) => void;
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* One pass of the space guard (ADR-0065). Above 80% of the volume, it deletes the kept
|
|
354
|
+
* conversations of ended runs, the oldest run first, until the volume is below 80%. A live run's
|
|
355
|
+
* conversations are never swept. Above 95% with no ended run's conversation left, it sets the
|
|
356
|
+
* store full, and each open conversation is cut at its next entry; below that, the store keeps
|
|
357
|
+
* entries again. Answers the runs whose conversations went.
|
|
358
|
+
*/
|
|
359
|
+
export async function guardSpace(port: SpaceGuardPort): Promise<string[]> {
|
|
360
|
+
const capacity = port.capacity();
|
|
361
|
+
if (!(capacity > 0)) return [];
|
|
362
|
+
const swept: string[] = [];
|
|
363
|
+
let used = port.used();
|
|
364
|
+
if (used > GUARD_SWEEP_AT * capacity) {
|
|
365
|
+
for (const runId of port.conversations.runsOldestFirst()) {
|
|
366
|
+
if (used < GUARD_SWEEP_AT * capacity) break;
|
|
367
|
+
if (await port.isLive(runId)) continue;
|
|
368
|
+
port.conversations.sweepForSpace(runId);
|
|
369
|
+
swept.push(runId);
|
|
370
|
+
port.onSwept?.(runId);
|
|
371
|
+
used = port.used();
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
port.conversations.full = used > GUARD_FULL_AT * capacity;
|
|
375
|
+
return swept;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** The bytes of every file under `dir`, at any depth. A file that goes while it is read counts 0. */
|
|
379
|
+
export function bytesUnder(dir: string): number {
|
|
380
|
+
let total = 0;
|
|
381
|
+
let names: import("node:fs").Dirent[];
|
|
382
|
+
try {
|
|
383
|
+
names = readdirSync(dir, { withFileTypes: true });
|
|
384
|
+
} catch {
|
|
385
|
+
return 0;
|
|
386
|
+
}
|
|
387
|
+
for (const entry of names) {
|
|
388
|
+
const path = join(dir, entry.name);
|
|
389
|
+
if (entry.isDirectory()) total += bytesUnder(path);
|
|
390
|
+
else if (entry.isFile()) {
|
|
391
|
+
try {
|
|
392
|
+
total += statSync(path).size;
|
|
393
|
+
} catch {
|
|
394
|
+
// Gone between the read and the stat.
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return total;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** The size of the file system that holds `dir` (`statfs`), or Infinity when it cannot be read. */
|
|
402
|
+
export function fileSystemBytes(dir: string): number {
|
|
403
|
+
try {
|
|
404
|
+
const fs = statfsSync(dir);
|
|
405
|
+
return fs.blocks * fs.bsize;
|
|
406
|
+
} catch {
|
|
407
|
+
return Number.POSITIVE_INFINITY;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const QUANTITY = /^(\d+(?:\.\d+)?)(Ki|Mi|Gi|Ti|Pi|Ei|k|M|G|T|P|E)?$/;
|
|
412
|
+
const UNIT: Record<string, number> = {
|
|
413
|
+
"": 1,
|
|
414
|
+
Ki: 2 ** 10,
|
|
415
|
+
Mi: 2 ** 20,
|
|
416
|
+
Gi: 2 ** 30,
|
|
417
|
+
Ti: 2 ** 40,
|
|
418
|
+
Pi: 2 ** 50,
|
|
419
|
+
Ei: 2 ** 60,
|
|
420
|
+
k: 1e3,
|
|
421
|
+
M: 1e6,
|
|
422
|
+
G: 1e9,
|
|
423
|
+
T: 1e12,
|
|
424
|
+
P: 1e15,
|
|
425
|
+
E: 1e18,
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
/** A Kubernetes storage quantity (`"5Gi"`) in bytes, or undefined for one that does not parse. */
|
|
429
|
+
export function quantityBytes(quantity: string): number | undefined {
|
|
430
|
+
const m = QUANTITY.exec(quantity);
|
|
431
|
+
if (!m) return undefined;
|
|
432
|
+
return Number.parseFloat(m[1]!) * UNIT[m[2] ?? ""]!;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Delete the kept conversations whose run has no row in the run store, and answer those run ids.
|
|
437
|
+
* Such a conversation stays when a crash comes between the store's sweep and `deleteRuns`, or when
|
|
438
|
+
* a batch is placed just before the sweep deletes its row. The retention sweep never names that
|
|
439
|
+
* run again, so each sweep calls this (ADR-0065).
|
|
440
|
+
*/
|
|
441
|
+
export async function deleteOrphanConversations(
|
|
442
|
+
conversations: ConversationStore,
|
|
443
|
+
runs: Pick<SnapshotStore, "findIdsByPrefix">,
|
|
444
|
+
): Promise<string[]> {
|
|
445
|
+
const orphans: string[] = [];
|
|
446
|
+
for (const runId of conversations.runIds()) {
|
|
447
|
+
// The ids that share a prefix come sorted, so the run's own id, when it exists, is first.
|
|
448
|
+
const [first] = await runs.findIdsByPrefix(runId, 1);
|
|
449
|
+
if (first !== runId) orphans.push(runId);
|
|
450
|
+
}
|
|
451
|
+
conversations.deleteRuns(orphans);
|
|
452
|
+
return orphans;
|
|
453
|
+
}
|
package/src/custodian.lua
CHANGED
|
@@ -10,15 +10,18 @@
|
|
|
10
10
|
--
|
|
11
11
|
-- Four roles, one per route family, read from the route's metadata:
|
|
12
12
|
--
|
|
13
|
-
-- control
|
|
13
|
+
-- control toward the Orchestrator, plain HTTP: the agent routes on the control socket, and
|
|
14
|
+
-- the ask on the loopback fetch listener (ADR-0076). Each listener has only its own
|
|
15
|
+
-- routes, so a route of the other one gets the deny route's 404
|
|
14
16
|
-- egress the CONNECT listener: dial guard, then tunnel or hand to an intercept listener
|
|
15
17
|
-- intercept one bound host, TLS already ended: swap the Stand-in, strip the rest
|
|
16
18
|
-- health the kubelet's probe
|
|
17
19
|
|
|
18
20
|
local VALUES = "/etc/jr2/custodian/values/"
|
|
19
21
|
|
|
20
|
-
-- The value of one held secret, read once. One trailing line end is taken off,
|
|
21
|
-
-- `kubectl create secret --from-file` writes one; anything else a header cannot carry stops
|
|
22
|
+
-- The value of one held secret, or its Stand-in, read once. One trailing line end is taken off,
|
|
23
|
+
-- because `kubectl create secret --from-file` writes one; anything else a header cannot carry stops
|
|
24
|
+
-- us. The line names the file, never what it holds.
|
|
22
25
|
local function load(name)
|
|
23
26
|
local f = io.open(VALUES .. name, "rb")
|
|
24
27
|
if f == nil then
|
|
@@ -36,13 +39,20 @@ local function load(name)
|
|
|
36
39
|
return v
|
|
37
40
|
end
|
|
38
41
|
|
|
42
|
+
-- Every Stand-in is secret but the fetch Stand-in (ADR-0059, ADR-0076): the pod's maker minted it,
|
|
43
|
+
-- and it is a value too, read from `<NAME>.standin` beside the secret's own. The one public Stand-in
|
|
44
|
+
-- rides the table (`standIn`), because `jr2-upload-pack` holds it as a constant.
|
|
39
45
|
local STAND_IN = {}
|
|
40
46
|
local VALUE = {}
|
|
41
47
|
for _, s in ipairs(HELD.secrets) do
|
|
42
|
-
STAND_IN[s.name] =
|
|
48
|
+
STAND_IN[s.name] = s.standIn or load(s.name .. ".standin")
|
|
43
49
|
VALUE[s.name] = load(s.name)
|
|
44
50
|
end
|
|
45
51
|
|
|
52
|
+
-- The slot the Harness states on a control request (ADR-0076). It goes to the Orchestrator on a
|
|
53
|
+
-- control route, unchanged, and to no other host.
|
|
54
|
+
local SLOT_HEADER = "x-jr2-slot"
|
|
55
|
+
|
|
46
56
|
-- The headers every bound target treats as a credential (`CREDENTIAL_HEADERS`, held-secrets.ts).
|
|
47
57
|
local CREDENTIAL = {}
|
|
48
58
|
for _, h in ipairs(HELD.credential) do CREDENTIAL[h] = true end
|
|
@@ -258,7 +268,7 @@ local function intercept(handle, headers, target)
|
|
|
258
268
|
if path:lower():find("%%2[ef]") then
|
|
259
269
|
return refuse(handle, 400, "path", "an encoded / or . in the path")
|
|
260
270
|
end
|
|
261
|
-
for _, h in ipairs({ "te", "trailer", "keep-alive", "proxy-connection" }) do headers:remove(h) end
|
|
271
|
+
for _, h in ipairs({ "te", "trailer", "keep-alive", "proxy-connection", SLOT_HEADER }) do headers:remove(h) end
|
|
262
272
|
return hold(handle, headers, HELD.targets[target], target, path)
|
|
263
273
|
end
|
|
264
274
|
|
|
@@ -273,6 +283,8 @@ local function control(handle, headers, route)
|
|
|
273
283
|
'{"error":"this pod serves no Sandbox, so it mounts no Repo to fetch (ADR-0053)"}')
|
|
274
284
|
end
|
|
275
285
|
headers:replace(":path", "/sandboxes/" .. sandbox .. "/fetch")
|
|
286
|
+
-- The fetch acts for the Sandbox, not for an Agent: no slot rides it.
|
|
287
|
+
headers:remove(SLOT_HEADER)
|
|
276
288
|
end
|
|
277
289
|
return hold(handle, headers, HELD.control, "orchestrator", headers:get(":path"))
|
|
278
290
|
end
|