@jr2/orchestrator 0.4.1 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -2
- package/console/components/app.ts +2 -0
- package/console/components/fleet.ts +14 -1
- package/console/components/machine-pane.ts +41 -2
- package/console/main.ts +13 -1
- package/console/store.ts +102 -6
- package/console/style.css +21 -0
- package/docs/agents-block.md +5 -0
- package/docs/concepts/agents-and-turns.md +442 -0
- package/docs/concepts/architecture.md +230 -0
- package/docs/concepts/charts.md +143 -0
- package/docs/concepts/composition.md +357 -0
- package/docs/concepts/converge.md +254 -0
- package/docs/concepts/durable-runs.md +189 -0
- package/docs/concepts/effects.md +603 -0
- package/docs/concepts/gates.md +208 -0
- package/docs/concepts/machines-and-workflows.md +289 -0
- package/docs/concepts/pools-and-sources.md +230 -0
- package/docs/concepts/secrets-and-trust.md +222 -0
- package/docs/concepts/the-menu.md +378 -0
- package/docs/concepts/workspaces.md +678 -0
- package/docs/get-started/a-model-decides.md +563 -0
- package/docs/get-started/an-agent-in-a-sandbox.md +381 -0
- package/docs/get-started/connect-a-model.md +248 -0
- package/docs/get-started/create-an-instance.md +194 -0
- package/docs/get-started/have-your-assistant-write-a-machine.md +922 -0
- package/docs/get-started/install.md +126 -0
- package/docs/guides/add-tools-to-the-sandbox.md +295 -0
- package/docs/guides/answer-a-gate.md +304 -0
- package/docs/guides/bind-open-parts.md +223 -0
- package/docs/guides/cancel-a-run.md +146 -0
- package/docs/guides/clean-up.md +206 -0
- package/docs/guides/fix-a-failed-converge.md +361 -0
- package/docs/guides/give-the-cluster-git-credentials.md +222 -0
- package/docs/guides/iterate-on-a-machine-on-kind.md +190 -0
- package/docs/guides/let-an-agent-write-a-chart.md +226 -0
- package/docs/guides/look-inside-a-sandbox.md +174 -0
- package/docs/guides/mirror-the-kit-images.md +176 -0
- package/docs/guides/move-from-kind-to-a-shared-cluster.md +302 -0
- package/docs/guides/open-the-console.md +91 -0
- package/docs/guides/publish-a-machine-as-an-npm-package.md +350 -0
- package/docs/guides/push-and-sign-a-branch.md +486 -0
- package/docs/guides/run-a-chart-by-name.md +270 -0
- package/docs/guides/run-a-command-or-call-a-url.md +544 -0
- package/docs/guides/run-one-worker-per-item.md +321 -0
- package/docs/guides/set-an-agents-access-and-dials.md +375 -0
- package/docs/guides/size-a-workspace.md +204 -0
- package/docs/guides/start-a-run-from-a-run.md +237 -0
- package/docs/guides/start-a-run.md +207 -0
- package/docs/guides/upgrade-the-kit-version.md +240 -0
- package/docs/guides/use-a-hosted-provider.md +208 -0
- package/docs/guides/use-a-local-model.md +313 -0
- package/docs/guides/watch-a-run.md +326 -0
- package/docs/guides/write-a-workflow.md +318 -0
- package/docs/index.md +44 -0
- package/docs/llms.txt +216 -0
- package/docs/reference/authoring-surface/agent.md +128 -0
- package/docs/reference/authoring-surface/answer.md +140 -0
- package/docs/reference/authoring-surface/arg.md +60 -0
- package/docs/reference/authoring-surface/basic.md +62 -0
- package/docs/reference/authoring-surface/bearer.md +53 -0
- package/docs/reference/authoring-surface/chartschema.md +23 -0
- package/docs/reference/authoring-surface/chartschemafor.md +48 -0
- package/docs/reference/authoring-surface/command.md +191 -0
- package/docs/reference/authoring-surface/customize.md +64 -0
- package/docs/reference/authoring-surface/defineconfig.md +41 -0
- package/docs/reference/authoring-surface/defineevent.md +114 -0
- package/docs/reference/authoring-surface/dispatch.md +126 -0
- package/docs/reference/authoring-surface/fail.md +83 -0
- package/docs/reference/authoring-surface/file.md +122 -0
- package/docs/reference/authoring-surface/frompick.md +277 -0
- package/docs/reference/authoring-surface/http.md +159 -0
- package/docs/reference/authoring-surface/isfailure.md +58 -0
- package/docs/reference/authoring-surface/jr2setup.md +237 -0
- package/docs/reference/authoring-surface/open.md +74 -0
- package/docs/reference/authoring-surface/openbody.md +86 -0
- package/docs/reference/authoring-surface/paramsof.md +125 -0
- package/docs/reference/authoring-surface/pool.md +186 -0
- package/docs/reference/authoring-surface/push.md +112 -0
- package/docs/reference/authoring-surface/secret.md +118 -0
- package/docs/reference/authoring-surface/sentby.md +83 -0
- package/docs/reference/authoring-surface/source.md +51 -0
- package/docs/reference/authoring-surface/text.md +62 -0
- package/docs/reference/authoring-surface/type-agentdeclaration.md +27 -0
- package/docs/reference/authoring-surface/type-agentdefinition.md +80 -0
- package/docs/reference/authoring-surface/type-agentlogic.md +15 -0
- package/docs/reference/authoring-surface/type-agentslots.md +12 -0
- package/docs/reference/authoring-surface/type-answeraction.md +12 -0
- package/docs/reference/authoring-surface/type-arg.md +19 -0
- package/docs/reference/authoring-surface/type-argnames.md +12 -0
- package/docs/reference/authoring-surface/type-binding.md +62 -0
- package/docs/reference/authoring-surface/type-bindingof.md +12 -0
- package/docs/reference/authoring-surface/type-bodyacceptsdoor.md +15 -0
- package/docs/reference/authoring-surface/type-bodyacceptsslots.md +17 -0
- package/docs/reference/authoring-surface/type-ceilings.md +41 -0
- package/docs/reference/authoring-surface/type-chainlink.md +19 -0
- package/docs/reference/authoring-surface/type-chartaction.md +46 -0
- package/docs/reference/authoring-surface/type-chartagent.md +28 -0
- package/docs/reference/authoring-surface/type-chartevent.md +20 -0
- package/docs/reference/authoring-surface/type-chartguard.md +20 -0
- package/docs/reference/authoring-surface/type-chartinvoke.md +28 -0
- package/docs/reference/authoring-surface/type-chartjson.md +14 -0
- package/docs/reference/authoring-surface/type-chartstate.md +46 -0
- package/docs/reference/authoring-surface/type-charttext.md +40 -0
- package/docs/reference/authoring-surface/type-charttransition.md +22 -0
- package/docs/reference/authoring-surface/type-charttransitions.md +14 -0
- package/docs/reference/authoring-surface/type-chartvalue.md +15 -0
- package/docs/reference/authoring-surface/type-childat.md +12 -0
- package/docs/reference/authoring-surface/type-childslots.md +12 -0
- package/docs/reference/authoring-surface/type-commandenv.md +15 -0
- package/docs/reference/authoring-surface/type-commandinput.md +19 -0
- package/docs/reference/authoring-surface/type-commandoptions.md +78 -0
- package/docs/reference/authoring-surface/type-commandoutput.md +104 -0
- package/docs/reference/authoring-surface/type-customize.md +109 -0
- package/docs/reference/authoring-surface/type-dispatchdefinition.md +39 -0
- package/docs/reference/authoring-surface/type-dispatchinput.md +14 -0
- package/docs/reference/authoring-surface/type-dispatchopenparts.md +14 -0
- package/docs/reference/authoring-surface/type-dispatchoutput.md +65 -0
- package/docs/reference/authoring-surface/type-duration.md +15 -0
- package/docs/reference/authoring-surface/type-effectbinding.md +93 -0
- package/docs/reference/authoring-surface/type-effectlogic.md +12 -0
- package/docs/reference/authoring-surface/type-envopenparts.md +14 -0
- package/docs/reference/authoring-surface/type-eventaudience.md +27 -0
- package/docs/reference/authoring-surface/type-eventdef.md +45 -0
- package/docs/reference/authoring-surface/type-eventfrom.md +16 -0
- package/docs/reference/authoring-surface/type-eventsemantics.md +26 -0
- package/docs/reference/authoring-surface/type-execoutcome.md +34 -0
- package/docs/reference/authoring-surface/type-failslot.md +14 -0
- package/docs/reference/authoring-surface/type-failure.md +19 -0
- package/docs/reference/authoring-surface/type-failureinput.md +20 -0
- package/docs/reference/authoring-surface/type-filebody.md +41 -0
- package/docs/reference/authoring-surface/type-frominput.md +14 -0
- package/docs/reference/authoring-surface/type-hostinjectedinput.md +58 -0
- package/docs/reference/authoring-surface/type-httpdefinition.md +101 -0
- package/docs/reference/authoring-surface/type-httpinput.md +12 -0
- package/docs/reference/authoring-surface/type-httpmethod.md +14 -0
- package/docs/reference/authoring-surface/type-httpopenparts.md +12 -0
- package/docs/reference/authoring-surface/type-httpoutput.md +89 -0
- package/docs/reference/authoring-surface/type-httprequestparts.md +33 -0
- package/docs/reference/authoring-surface/type-jr2config.md +126 -0
- package/docs/reference/authoring-surface/type-lends.md +38 -0
- package/docs/reference/authoring-surface/type-logicat.md +12 -0
- package/docs/reference/authoring-surface/type-markedif.md +12 -0
- package/docs/reference/authoring-surface/type-mergedactors.md +12 -0
- package/docs/reference/authoring-surface/type-namedsecret.md +25 -0
- package/docs/reference/authoring-surface/type-noslotspec.md +26 -0
- package/docs/reference/authoring-surface/type-noslotworkspaceoptions.md +32 -0
- package/docs/reference/authoring-surface/type-openbodydeclaration.md +40 -0
- package/docs/reference/authoring-surface/type-openbodylogic.md +14 -0
- package/docs/reference/authoring-surface/type-openbodyoutput.md +42 -0
- package/docs/reference/authoring-surface/type-openbodyslot.md +18 -0
- package/docs/reference/authoring-surface/type-opencommandinput.md +17 -0
- package/docs/reference/authoring-surface/type-openeffect.md +12 -0
- package/docs/reference/authoring-surface/type-openeffectslots.md +12 -0
- package/docs/reference/authoring-surface/type-openof.md +12 -0
- package/docs/reference/authoring-surface/type-opensecret.md +23 -0
- package/docs/reference/authoring-surface/type-opensecretmark.md +12 -0
- package/docs/reference/authoring-surface/type-permissivenoslotworkspaceoptions.md +31 -0
- package/docs/reference/authoring-surface/type-permissiveworkspaceoptions.md +32 -0
- package/docs/reference/authoring-surface/type-poolmachine.md +28 -0
- package/docs/reference/authoring-surface/type-poolspec.md +60 -0
- package/docs/reference/authoring-surface/type-pushinput.md +15 -0
- package/docs/reference/authoring-surface/type-pushoptions.md +79 -0
- package/docs/reference/authoring-surface/type-pushoutput.md +124 -0
- package/docs/reference/authoring-surface/type-pushwireoutput.md +12 -0
- package/docs/reference/authoring-surface/type-quantity.md +14 -0
- package/docs/reference/authoring-surface/type-reached.md +12 -0
- package/docs/reference/authoring-surface/type-reposlot.md +70 -0
- package/docs/reference/authoring-surface/type-reposlotsof.md +16 -0
- package/docs/reference/authoring-surface/type-sandboxoptions.md +77 -0
- package/docs/reference/authoring-surface/type-secret.md +25 -0
- package/docs/reference/authoring-surface/type-sender.md +19 -0
- package/docs/reference/authoring-surface/type-sentbyguards.md +12 -0
- package/docs/reference/authoring-surface/type-size.md +85 -0
- package/docs/reference/authoring-surface/type-slothandles.md +32 -0
- package/docs/reference/authoring-surface/type-sourcenextresult.md +17 -0
- package/docs/reference/authoring-surface/type-sourcespec.md +44 -0
- package/docs/reference/authoring-surface/type-syncstate.md +12 -0
- package/docs/reference/authoring-surface/type-text.md +19 -0
- package/docs/reference/authoring-surface/type-textpart.md +24 -0
- package/docs/reference/authoring-surface/type-thinkinglevel.md +39 -0
- package/docs/reference/authoring-surface/type-toparameterizedobject.md +14 -0
- package/docs/reference/authoring-surface/type-toprovidedactor.md +14 -0
- package/docs/reference/authoring-surface/type-unknown.md +21 -0
- package/docs/reference/authoring-surface/type-usercontainer.md +77 -0
- package/docs/reference/authoring-surface/type-withoutemptyindex.md +20 -0
- package/docs/reference/authoring-surface/type-workflowevent.md +46 -0
- package/docs/reference/authoring-surface/type-workspaceaccess.md +16 -0
- package/docs/reference/authoring-surface/type-workspaced.md +94 -0
- package/docs/reference/authoring-surface/type-workspacehandles.md +23 -0
- package/docs/reference/authoring-surface/type-workspacemachine.md +29 -0
- package/docs/reference/authoring-surface/type-workspaceof.md +13 -0
- package/docs/reference/authoring-surface/type-workspaceoptions.md +39 -0
- package/docs/reference/authoring-surface/type-workspacespec.md +37 -0
- package/docs/reference/authoring-surface/workspace.md +259 -0
- package/docs/reference/authoring-surface.md +197 -0
- package/docs/reference/cli.md +433 -0
- package/docs/reference/config.md +756 -0
- package/docs/reference/decisions/0001-operator-owned-generic-sandbox.md +113 -0
- package/docs/reference/decisions/0002-duplex-streaming-actor.md +110 -0
- package/docs/reference/decisions/0004-worktree-provisioning-model.md +124 -0
- package/docs/reference/decisions/0005-sandbox-pod-composition.md +230 -0
- package/docs/reference/decisions/0006-agent-control-surface.md +47 -0
- package/docs/reference/decisions/0007-durable-machine-state.md +109 -0
- package/docs/reference/decisions/0008-orchestrator-as-deployed-app.md +60 -0
- package/docs/reference/decisions/0009-cli-and-instance-interface.md +275 -0
- package/docs/reference/decisions/0010-bdd-acceptance-tests.md +160 -0
- package/docs/reference/decisions/0011-workflow-defined-events.md +141 -0
- package/docs/reference/decisions/0012-workspace-wrapper-machine.md +103 -0
- package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +157 -0
- package/docs/reference/decisions/0014-observation-is-open-run-state-is-not.md +95 -0
- package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +265 -0
- package/docs/reference/decisions/0016-agent-turn-mechanics-are-internal.md +124 -0
- package/docs/reference/decisions/0017-pool-over-a-source-port.md +69 -0
- package/docs/reference/decisions/0018-instance-agents-are-definitions-jr2-assembles-the-harness.md +111 -0
- package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +275 -0
- package/docs/reference/decisions/0020-private-ca-trust-is-a-named-concept.md +60 -0
- package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +100 -0
- package/docs/reference/decisions/0022-observation-is-a-level-triggered-feed.md +103 -0
- package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +176 -0
- package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +439 -0
- package/docs/reference/decisions/0025-cancel-ends-a-run-stop-parks-it.md +65 -0
- package/docs/reference/decisions/0026-a-turn-that-is-over-is-never-prompted.md +62 -0
- package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +237 -0
- package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +89 -0
- package/docs/reference/decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md +154 -0
- package/docs/reference/decisions/0030-a-snapshot-names-the-machine-it-was-written-under.md +100 -0
- package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +190 -0
- package/docs/reference/decisions/0032-the-console-unlocks-with-the-instance-token.md +61 -0
- package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +127 -0
- package/docs/reference/decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md +57 -0
- package/docs/reference/decisions/0035-a-runaway-turn-is-ended-rerolled-once-then-a-fault.md +92 -0
- package/docs/reference/decisions/0036-a-turn-compacts-its-own-context-mid-flight.md +138 -0
- package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +196 -0
- package/docs/reference/decisions/0038-jr2-up-builds-every-image-it-deploys.md +170 -0
- package/docs/reference/decisions/0039-image-garbage-collects-by-reachability.md +115 -0
- package/docs/reference/decisions/0041-a-build-the-host-already-holds-is-not-spent-again.md +64 -0
- package/docs/reference/decisions/0042-ready-is-not-routable-an-admission-retries-its-connection.md +142 -0
- package/docs/reference/decisions/0043-the-kit-is-tested-as-installed-a-local-registry-stands-in-for-npm.md +170 -0
- package/docs/reference/decisions/0044-kit-images-live-at-a-canonical-home-a-self-host-mirrors-it.md +85 -0
- package/docs/reference/decisions/0045-the-platform-joins-the-image-address-and-the-cluster-chooses-it.md +82 -0
- package/docs/reference/decisions/0046-a-converge-failure-carries-its-diagnosis.md +34 -0
- package/docs/reference/decisions/0047-the-git-ssh-key-source-is-the-users-choice.md +47 -0
- package/docs/reference/decisions/0048-the-orchestrator-boots-without-its-repos.md +36 -0
- package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +126 -0
- package/docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md +73 -0
- package/docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md +227 -0
- package/docs/reference/decisions/0052-a-sandbox-lands-where-an-ordinary-pod-lands-and-the-cache-follows-it.md +81 -0
- package/docs/reference/decisions/0053-a-fetch-inside-the-pod-asks-the-node-cache-and-the-cache-asks-the-remote.md +111 -0
- package/docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md +126 -0
- package/docs/reference/decisions/0055-a-release-is-a-pushed-tag-and-the-publish-guard-is-the-credential.md +107 -0
- package/docs/reference/decisions/0056-a-global-jr2-hands-off-to-the-instances-own-and-an-instance-has-one-kit-version.md +62 -0
- package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +152 -0
- package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +106 -0
- package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +384 -0
- package/docs/reference/decisions/0060-a-workspace-states-its-size-and-jr2-reserves-all-of-it.md +111 -0
- package/docs/reference/decisions/0061-a-memory-kill-ends-the-agents-processes-not-the-harness.md +83 -0
- package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +212 -0
- package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +83 -0
- package/docs/reference/decisions/0064-a-workspace-waits-for-capacity.md +79 -0
- package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-a-week.md +65 -0
- package/docs/reference/decisions/0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md +550 -0
- package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +430 -0
- package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +464 -0
- package/docs/reference/decisions/0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md +180 -0
- package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +323 -0
- package/docs/reference/decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md +183 -0
- package/docs/reference/decisions/0072-the-docs-ship-inside-the-kit-and-an-instances-agents-md-indexes-them.md +144 -0
- package/docs/reference/decisions/0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md +122 -0
- package/docs/reference/decisions/0074-an-unrouted-mechanism-event-is-refused-at-load-and-faults-the-run.md +76 -0
- package/docs/reference/decisions.md +83 -0
- package/docs/reference/glossary.md +648 -0
- package/docs/reference/http-api.md +1348 -0
- package/docs/reference/machines.md +190 -0
- package/docs/reference/mechanism-events.md +72 -0
- package/docs/why.md +206 -0
- package/package.json +5 -3
- package/src/actor.ts +541 -79
- package/src/agent.ts +114 -38
- package/src/ambient.ts +2 -1
- package/src/assembly-checks.ts +189 -7
- package/src/cause.ts +234 -0
- package/src/chart.ts +169 -37
- package/src/charts.ts +34 -6
- package/src/config.ts +430 -178
- package/src/custodian.ts +74 -22
- package/src/customize.ts +188 -46
- package/src/dispatch-effect.ts +945 -0
- package/src/door-compat.ts +217 -0
- package/src/durability.ts +11 -4
- package/src/effect.ts +341 -44
- package/src/effector-wire.ts +160 -11
- package/src/fail.ts +209 -0
- package/src/harness-client.ts +541 -47
- package/src/http-effect.ts +495 -53
- package/src/http.ts +427 -16
- package/src/index.ts +60 -82
- package/src/instance.ts +68 -8
- package/src/internal.ts +84 -0
- package/src/machine-doc.ts +21 -3
- package/src/mechanism-routes.ts +168 -0
- package/src/model-keys.generated.ts +53 -0
- package/src/model-keys.ts +18 -58
- package/src/notices.ts +66 -13
- package/src/open-body.ts +159 -23
- package/src/open-effect.ts +81 -8
- package/src/open.ts +59 -6
- package/src/parts.ts +184 -13
- package/src/party.ts +68 -2
- package/src/pick.ts +463 -11
- package/src/pool.ts +247 -36
- package/src/registration.ts +83 -4
- package/src/repos.ts +2 -1
- package/src/run-host.ts +565 -31
- package/src/run-output.ts +38 -0
- package/src/sandbox-effect.ts +273 -38
- package/src/sandbox-kube.ts +3 -1
- package/src/secret-placement.ts +52 -17
- package/src/server.ts +3 -0
- package/src/setup.ts +239 -56
- package/src/size.ts +121 -9
- package/src/snapshot-store.ts +54 -10
- package/src/stub-harness.ts +44 -16
- package/src/timers.ts +14 -1
- package/src/vocabulary.ts +69 -14
- package/src/wire.ts +90 -16
- package/src/workspace.ts +697 -175
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# jr2 briefs the Agent on its seat, never on its task
|
|
2
|
+
|
|
3
|
+
The model saw two sources of text: the Agent's `instructions` (identity, ADR-0018) and the Frame's prompt (the Machine
|
|
4
|
+
state's, ADR-0057). jr2 itself spoke only after a failure — the no-signal nudge — so an Agent had no way to learn facts
|
|
5
|
+
only the mechanism knows. ADR-0057 opens with that failure: a weak model never learned its working directory, wrote
|
|
6
|
+
outside the worktree and still called `finish`. ADR-0060 adds more such facts: `os.cpus()` reports the node, not the
|
|
7
|
+
Sandbox's CPUs. ADR-0061 adds events: a kernel memory kill ends a conversation, and the next Agent inherits a Workspace
|
|
8
|
+
whose processes are gone, with no way to know why.
|
|
9
|
+
|
|
10
|
+
## Decision
|
|
11
|
+
|
|
12
|
+
- **jr2 has its own text to the model, the Briefing.** It says a fact only if a correct Agent would act differently
|
|
13
|
+
knowing it **and** cannot find it out for itself: how it acts on the workflow, the Sandbox's CPUs and Size, the
|
|
14
|
+
working directory, that a Menu-only Agent has no Working tools, and what happened to the last Turn. Nothing about the
|
|
15
|
+
task — that is the Frame's — and nothing about style or care — that is identity's. Stand-ins are left out: the only
|
|
16
|
+
change that knowing could cause is an attempt to go around the Custodian.
|
|
17
|
+
- **Two parts, placed by how often they change.** The standing part is the same on every Turn of a seat; it follows
|
|
18
|
+
`instructions` in the system prompt, in its own delimited section, and is byte-stable (no timestamps, no counters, no
|
|
19
|
+
Menu names). The Turn part follows the Frame's prompt in a delimited block: the working directory, the notices, a
|
|
20
|
+
**pick's briefing** for each Allowed pick that has one (jr2's text on the pick's parameter, which the Orchestrator's
|
|
21
|
+
surface marks on the pick: the Chart briefing of [ADR-0070](0070-a-machine-lends-its-parts-to-a-chart.md), said only
|
|
22
|
+
on a Turn that may make that pick), and the Turn's Allowed picks
|
|
23
|
+
([ADR-0029](0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md)). So the Briefing never
|
|
24
|
+
breaks the provider's prompt cache — the Turn part sits in the newest message, which is uncached anyway, and stays
|
|
25
|
+
unchanged in the history after.
|
|
26
|
+
- **How the Agent acts on the workflow is one standing statement**, true whether a pick ends the Turn or keeps it open
|
|
27
|
+
([ADR-0024](0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md)): "You act on the workflow by picking from
|
|
28
|
+
your Menu: a call to one of the workflow's tools that the newest "Allowed now" lists. Each pick's answer says if your
|
|
29
|
+
Turn is over. If it is over, stop and call no other tool. If it is not, continue. If nothing is allowed, stop: the
|
|
30
|
+
workflow prompts you again when something is." The last sentence is safe because the wake asks a model that stopped
|
|
31
|
+
with nothing allowed when a pick opens ([ADR-0016](0016-agent-turn-mechanics-are-internal.md)). A line that says a
|
|
32
|
+
Turn ends with exactly one pick would be false for a pick that keeps the Turn open.
|
|
33
|
+
- **The Turn part is written once per Submission, and restated when its Allowed picks change.** When something other
|
|
34
|
+
than the Agent's own pick changes them inside the Turn, the Harness appends the Allowed picks again at the next step
|
|
35
|
+
boundary, in a `<jr2-turn>` block of their own: "The workflow's state changed since you last read the Allowed picks.
|
|
36
|
+
Allowed now: …" ([ADR-0029](0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md)). It is
|
|
37
|
+
appended, never a rewrite of the first, so the cache argument above holds for it too. It carries no notice: notices
|
|
38
|
+
are for the next Turn in their scope. It carries a pick's briefing when the restatement opens that pick.
|
|
39
|
+
- **The Turn part comes last, because a model obeys what it read last.** Measured on the home-lab vLLM model: with a
|
|
40
|
+
prompt that argued for a pick the Turn did not allow, the Allowed picks placed ahead of the prompt were obeyed 0/10
|
|
41
|
+
(thinking off) and 5/10 (thinking on); placed after it, 10/10 both. Wording made no difference. The author's task
|
|
42
|
+
still reads first, as it would with no jr2.
|
|
43
|
+
- **Always on.** No author can turn it off: every fact in it passes the rule above, so removing one can only make the
|
|
44
|
+
Agent act worse. An `instructions` that repeats a fact is harmless.
|
|
45
|
+
- **A notice is delivered once**, on the next admission in its scope that takes it, then cleared. That is the next Turn,
|
|
46
|
+
or, for a guard kill that the Agent of an open Turn was not told, that Turn's nudge or wake (below).
|
|
47
|
+
- **Conversation scope** ("this conversation is new; earlier context is gone") goes to that Agent's next Turn. So does
|
|
48
|
+
a pick's answer that never reached its call
|
|
49
|
+
([ADR-0024](0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md)): the route left the Agent's state, and
|
|
50
|
+
the next Turn of the conversation the picking Turn continued hears "the workflow left the state of your last turn
|
|
51
|
+
while your pick was outstanding", with the answer — or, when something other than the route left the state while the
|
|
52
|
+
pick's work ran, "your pick did not finish: the workflow left the state that asked for your last turn while it ran,
|
|
53
|
+
and that stopped it" (`stopped`), with an earlier link's reply when one came. No pick-answer notice says the work
|
|
54
|
+
ran to its end. A fresh picking Turn has no such conversation, and its answer is no notice.
|
|
55
|
+
- **Workspace scope** ("the Sandbox's processes were killed at its memory limit during `coder`'s Turn; `/dev/shm` was
|
|
56
|
+
cleared. Anything that was running in the Sandbox at that time is gone.") goes to the next admission of any Agent in
|
|
57
|
+
that Workspace that takes it. One kill is one notice, however many Turns saw it. Only a kill read on a stream while
|
|
58
|
+
its wait is live names the Turn whose Submission ran. Both forms end with the whole Sandbox, because every process
|
|
59
|
+
in it died, a shared Workspace's too, and in the past tense, because the Agent may hear it after processes started
|
|
60
|
+
again. A kill read from the pod's list names no Turn and says nothing of when or whose: "the Sandbox's processes
|
|
61
|
+
were killed at its memory limit (peak X of Y); /dev/shm was cleared. Anything that was running in the Sandbox at
|
|
62
|
+
that time is gone." The list cannot say whose Turn a kill ended: the kill may land after a state exits, or end an
|
|
63
|
+
attach, a sync or a command. A kernel kill read from the Sandbox's last end (below) says the same, with the limit
|
|
64
|
+
and no peak: "the Sandbox's processes were killed at its memory limit of Y; …". A Menu-only Agent outside a
|
|
65
|
+
Workspace receives conversation notices only.
|
|
66
|
+
- A notice never replaces a fault. The Machine still receives `agent.fault` and decides (ADR-0016); the notice makes
|
|
67
|
+
sure the next Agent is not working blind.
|
|
68
|
+
- **The Orchestrator is the only keeper of notices**: pending notices are plain data in the run's persisted state
|
|
69
|
+
(ADR-0007). It sees kernel kills and fresh conversations itself. A kernel kill (ADR-0061, layer 5) has two paths,
|
|
70
|
+
under one source, the Workspace and the time the Harness container ended. A Turn that the kill ended faults on it, and
|
|
71
|
+
the fault raises the notice with the Turn's Agent. A kill while no Turn runs faults nothing, so before each admission
|
|
72
|
+
into a Workspace the Agent actor also reads the container's last end from the Sandbox watch, which the operator
|
|
73
|
+
publishes: a last end `OOMKilled` raises the notice, which names no Turn. The ledger's `seen` makes the two paths one
|
|
74
|
+
notice. An actor whose own watch saw the restart reads the last end only when it re-admits the Turn after the cut.
|
|
75
|
+
When the Turn faults, the fault's notice names the Turn's Agent. A watch that has not listed yet says nothing, and the
|
|
76
|
+
next admission reads again: the last end stays until the container ends again. A nudge and a wake take no kernel kill;
|
|
77
|
+
it stays for the next Turn. The re-admission after a cut takes it, as it takes every notice in scope that is not a
|
|
78
|
+
guard kill. The Harness reports a guard kill (ADR-0061) on two paths, under one id that the Harness gives the kill:
|
|
79
|
+
- **On a stream.** While a Submission runs, the kill is an event on that conversation's updates stream. The Agent
|
|
80
|
+
actor reads a stream while it waits on the Submission. It stops when the Agent's state exits, before its abort
|
|
81
|
+
reaches the Harness. So a stream does not always reach a reader: a kill in that window is on a stream that no one
|
|
82
|
+
reads. A stopped actor raises nothing from its stream.
|
|
83
|
+
- **In the pod's list.** The Harness keeps every kill, with `told`: the conversations whose `bash` call returned the
|
|
84
|
+
kill's verdict to the model. A conversation goes into it when the answer returns, not when the guard fires: a call
|
|
85
|
+
cut short (the container's end, a restart) told no one. A model that writes text, or waits, is not told. The Harness
|
|
86
|
+
keeps the list in a file beside its conversations and answers it on `GET /memory-kills`. A kill while no Submission
|
|
87
|
+
runs is on this path alone: for example, a dev server killed while the workflow waits at a Gate. Before each
|
|
88
|
+
admission into a Workspace, the Agent actor reads the list. The actor sets the read's limit of 5 seconds, and it is
|
|
89
|
+
the only limit. A read that never left the Orchestrator, or a draining Harness's 503, is sent again in that time. A
|
|
90
|
+
read that fails or times out is none, and the next admission reads again: the list keeps the kill. The Orchestrator
|
|
91
|
+
logs one line when the reads from a Harness start to fail. It logs no line when a read works again, and it logs the
|
|
92
|
+
failure again only when a read fails after a read that worked. The Agent's uid can write the file, as it can the
|
|
93
|
+
conversation records: it can change only what its own Workspace is told. So a `peak` or a `limit` that is not a size
|
|
94
|
+
in the guard's form, or is long, is left out and logged, on the Harness's read of the file and on the Orchestrator's
|
|
95
|
+
read of the list and of a stream. The kill is kept, so a real kill is never lost, and its notice says no sizes: "the
|
|
96
|
+
Sandbox's processes were killed at its memory limit; /dev/shm was cleared. …". Only a kill with no id or no time, or
|
|
97
|
+
one outside the bounds below, is dropped. A stream event from an older Harness has no id; its source is then its
|
|
98
|
+
place on the stream, exactly as the Orchestrator raised it before.
|
|
99
|
+
|
|
100
|
+
Both paths raise the notice with the same source, the Workspace and the kill's id. The ledger keeps every source it
|
|
101
|
+
raised (`seen`) in the run's persisted state. So a kill read on both paths, or on two streams, is one notice. Each
|
|
102
|
+
read of the list raises one notice at most: the newest kill's, unless it is raised already. The read marks the older
|
|
103
|
+
kills in it seen, and they give no notice, so a burst of kills in one read tells the Agent one thing. Kills read on a
|
|
104
|
+
stream are not folded: each one with different sizes gives its own notice. Two notices with the same words keep two
|
|
105
|
+
entries in the ledger, and an admission that takes both says them once. The run is saved when a raise changes the
|
|
106
|
+
ledger, and only then. A read removes nothing, so a crash between the read and the save that keeps it loses no kill. A
|
|
107
|
+
restart of the Orchestrator raises no kill twice. A restart of the Harness container keeps the file, which is on the
|
|
108
|
+
pod's emptyDir with its conversations (ADR-0031). A lost pod takes the file with it, but then the Workspace is lost
|
|
109
|
+
(ADR-0021). The list keeps the newest 16 kills, each with at most 32 conversations in `told`, and the Orchestrator
|
|
110
|
+
reads at most 256 KiB of it. The Agent's uid can write the file, so the two sides hold the same bounds: an `id` and an
|
|
111
|
+
`at` of at most 64, an `agent` and an `instanceId` of at most 128, and an `at` that parses as a time. A kill outside
|
|
112
|
+
them is dropped, and a `told` entry outside them is left out. The Harness counts the bytes of the JSON text, and the
|
|
113
|
+
Orchestrator counts characters, which are never more, so the Harness keeps no kill that the Orchestrator drops. The
|
|
114
|
+
Harness reads at most the last 256 KiB of the file. With these bounds the largest list is 149,259 bytes. When more
|
|
115
|
+
come between two admissions, the oldest is lost, and a newer kill tells the same thing. When more than 16 come in one
|
|
116
|
+
Turn, the Turn's mark leaves the list too, and every kill in the list then counts as after the mark; a nudge or a wake
|
|
117
|
+
of that Turn may then carry a kill from before it. The Instance Harness keeps no kill, because it holds no Workspace.
|
|
118
|
+
|
|
119
|
+
A notice counts as delivered when its admission is ledgered, so a restart before that point delivers it again and a
|
|
120
|
+
restart after it does not. Delivered is not yet heard: if the Orchestrator ends the Turn before the Harness reads its
|
|
121
|
+
surface, the Harness never prompts it (ADR-0026), so the Orchestrator raises that admission's notices again. This
|
|
122
|
+
holds for a nudge and a wake too: each one must have its surface read again before its notices count as heard. An
|
|
123
|
+
admission that fails leaves the earlier admission's notices heard. A nudge and a wake re-prompt the Turn within
|
|
124
|
+
itself, so they take no notice of the kind a Turn takes. They take only the guard kills of the list that come after
|
|
125
|
+
the newest kill whose `told` names their conversation. That `bash` answer told the Agent that all its processes died,
|
|
126
|
+
so it covers every kill up to it. They also take no kill from before the Turn started. Such a kill would tell the
|
|
127
|
+
Agent that processes it started in this Turn died. The Turn's first read of the list sets its mark: the id of the
|
|
128
|
+
newest kill it saw, or "none". The mark is ledgered with every admission of the Turn, and a restore reads it back, so
|
|
129
|
+
a nudge or a wake takes only kills after it. Only when the first read failed is the Turn's start the mark, ledgered
|
|
130
|
+
the same way, with one second allowed for a pod clock that runs behind. A read that failed is "could not tell", never
|
|
131
|
+
an empty list, so it sets no mark. The kills they do not take stay pending for the next Turn. The re-admission after a
|
|
132
|
+
Harness restart cut the Turn continues the same Turn, so it takes every notice in scope except the guard kills that
|
|
133
|
+
this rule leaves out. When its read could not tell, it takes no guard kill at all, as a nudge then takes none: it
|
|
134
|
+
cannot know which kills its own `bash` reported. The notices that the Turn's admissions carried since the Harness last
|
|
135
|
+
read the Turn's surface are pending again first, each with the source it was raised with, and this re-admission
|
|
136
|
+
carries them whatever else its scope leaves out. A notice with no source merges only into a pending one with no
|
|
137
|
+
source, so an equal notice of another kill never takes its place. When such an unread admission carried the Turn's
|
|
138
|
+
task, the task follows the re-admit text. When the rebuilt Harness lost the conversation, nothing of the Turn is left:
|
|
139
|
+
every notice that the Turn's admissions carried is pending again whatever the surface reads said, the re-admission
|
|
140
|
+
takes every notice in scope, as a Turn's admission does, and the Turn's task follows the re-admit text. It says once
|
|
141
|
+
that the conversation is new. A restore drops the prompt from a Turn's input, so the task rides the ledgered admission
|
|
142
|
+
too; a lost conversation with no task to send faults the Turn instead. Whether the Harness still holds the
|
|
143
|
+
conversation is asked again for up to 30 seconds while the new container does not listen yet or drains, and only then
|
|
144
|
+
is the answer taken as "held". The continue path asks the same way, so a continued Turn after a restart can wait up to
|
|
145
|
+
30 seconds before its admission. A Harness in CrashLoopBackOff can take longer than that: the answer is then "held",
|
|
146
|
+
and it can be wrong. So every admission to a conversation that the Harness was taken to keep says so with
|
|
147
|
+
`kept: true`: the re-admission after a cut, a continued Turn, and a nudge or a wake. A Harness that keeps no such
|
|
148
|
+
conversation refuses it with 404 and `kept: false` in the body, and seats none. The Orchestrator takes only that 404
|
|
149
|
+
as the refusal; another 404 is a failed admission. Without the refusal the Harness would seat a new conversation, and
|
|
150
|
+
a model on an empty context would read "continue your task" with no task. On the refusal the Turn takes the path of a
|
|
151
|
+
lost conversation. The re-admission is admitted as a Turn, with the task after the re-admit text and the notice that
|
|
152
|
+
the conversation is new, or faults when it has no task to send. A continued Turn is told that its conversation is new,
|
|
153
|
+
and is admitted again. A nudge or a wake is admitted again as a Turn: its prompt is the task alone, because nothing
|
|
154
|
+
was cut, with the notice that the conversation is new and every notice in scope. Every notice that the Turn's
|
|
155
|
+
admissions carried is pending again first, as for a lost conversation after a cut. With no task to send, the Turn
|
|
156
|
+
faults. The Instance Harness's restart is on no watch, so this refusal is the only way a nudge or a wake learns of it.
|
|
157
|
+
What the Turn's admissions carried, and whether the Harness read their surface, is kept in the Orchestrator's memory
|
|
158
|
+
alone: after a restore of the Orchestrator before a surface read, nothing is raised again. A fresh Turn's "this
|
|
159
|
+
conversation is new", which rides an admission alone, is carried again by the next re-admission when the Harness has
|
|
160
|
+
not read the one before.
|
|
161
|
+
|
|
162
|
+
A continued Turn of the same conversation is told, as a notice, a kill that its own `bash` call answered in an earlier
|
|
163
|
+
Turn. The Agent reads that kill twice, which is true and costs little. The `told` rule could later extend to the first
|
|
164
|
+
admission of a continued Turn too; it does not now, because a fresh Turn's conversation holds no earlier answer, and
|
|
165
|
+
the rule would then need the conversation in the ledger's scope.
|
|
166
|
+
|
|
167
|
+
- **The Harness writes every word.** The admit body carries typed notices
|
|
168
|
+
(`notices: [{ kind: "memory-limit", scope, agent?, limit?, peak? } | { kind: "conversation-new", reason } | { kind: "pick-answer", pick, cut, answer?, stopped? } …]`,
|
|
169
|
+
where `peak` comes only with `limit`), and the Harness renders both parts. An outstanding pick's answer is data too —
|
|
170
|
+
the last reply since the pick, a party's event with its payload or a child's output (ADR-0024) — and its words ("Its
|
|
171
|
+
answer: `approve` from Gate `review`, with {…}", "No reply came back for it.") are the Harness's, in the receipt and
|
|
172
|
+
in the notice alike; "it moved the workflow, and nothing ran" is a guard straight back's receipt alone. The standing
|
|
173
|
+
part needs no wire: the Harness already holds its limits (cgroup and Downward API env), the definition's `workspace`
|
|
174
|
+
access, and the Menu. The wording lives in one place, and Harness conformance checks it (ADR-0027).
|
|
175
|
+
|
|
176
|
+
## Considered options
|
|
177
|
+
|
|
178
|
+
- **Put the facts in the Frame.** Rejected: the Frame is the Machine state's (ADR-0057), and a kit that writes into it
|
|
179
|
+
makes the author's prompt say things the author did not write.
|
|
180
|
+
- **Leave it to authors' `instructions`.** Rejected: a definition is written before any run, so it cannot know a Size a
|
|
181
|
+
composer retuned, a working directory, or what happened to the last Turn — and packaged Agents would each word it
|
|
182
|
+
differently or not at all.
|
|
183
|
+
- **An author switch** (`agent({ briefing: false })`, or notices only). Rejected: under the rule, each fact is one a
|
|
184
|
+
correct Agent needs; the switch only lets the ADR-0057 failure back in.
|
|
185
|
+
- **Everything in the system prompt.** Rejected: per-Turn content there breaks the prompt cache on every Turn.
|
|
186
|
+
- **Finished text from the Orchestrator.** Rejected: wording split across two processes, and untestable where the turn
|
|
187
|
+
loop is tested.
|
|
188
|
+
- **Report a guard kill on the stream alone.** Rejected: a kill while no Submission runs, or after the wait stopped,
|
|
189
|
+
reaches no reader. A dev server killed while the workflow waits at a Gate is then never told to the next Turn, which
|
|
190
|
+
works blind.
|
|
191
|
+
- **Put that kill on every conversation's stream.** Rejected: the next Admission reads its conversation's stream from
|
|
192
|
+
its own offset, which is after the event.
|
|
193
|
+
- **Keep in the list only the kills that no stream carried.** Rejected: a kill on a stream whose wait stopped is then in
|
|
194
|
+
no place that is read. And two streams that carry one kill would give two notices.
|
|
195
|
+
- **The Harness pushes the kill to the Orchestrator.** Rejected: the Harness container is the Agent's, and ADR-0013
|
|
196
|
+
closes a route from it to the Orchestrator. A push also needs a retry and an acknowledgement, and an idempotent read
|
|
197
|
+
does not.
|
|
198
|
+
- **Read the kill on the Sandbox watch.** Rejected: the operator publishes what the kubelet says about the container,
|
|
199
|
+
and a guard kill does not end the container. The Harness has no Kubernetes credentials to publish it.
|
|
200
|
+
- **The Harness words the kill into the next Turn itself.** Rejected: a second keeper of notices, outside the run's
|
|
201
|
+
persisted state, which cannot know when an admission is ledgered or heard.
|
|
202
|
+
|
|
203
|
+
## Consequences
|
|
204
|
+
|
|
205
|
+
- ADR-0060's CPU fact and ADR-0061's memory-limit notice are the Briefing's first users.
|
|
206
|
+
- Each admission into a Workspace costs one more request to its Harness, `GET /memory-kills`. The first read from a
|
|
207
|
+
Harness that fails is said in one line of the Orchestrator's log.
|
|
208
|
+
- The no-signal nudge stays a re-prompt within a Turn (ADR-0016); it is not a notice.
|
|
209
|
+
- The Briefing is how a Turn learns its Allowed picks: the tools block is the whole Menu, fixed for a conversation so it
|
|
210
|
+
does not break the cache
|
|
211
|
+
([ADR-0029](0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md)).
|
|
212
|
+
- GLOSSARY.md gains **Briefing**.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# The Orchestrator watches the cluster
|
|
2
|
+
|
|
3
|
+
The Orchestrator talked to Kubernetes by starting a `kubectl` process for every call (`sandbox-kubectl.ts`). A short
|
|
4
|
+
process can only ask "what is the state now?", so waiting became polling: `get sandbox` every second while a Workspace
|
|
5
|
+
provisioned (and `get pod` every fifth time), `get sandbox` every second while a fetch ask waited (ADR-0053), and
|
|
6
|
+
detection of a lost Workspace only on the Lease's 5-minute renewal (ADR-0021 chose that and named the watch as the thing
|
|
7
|
+
to revisit "if something needs sub-minute loss detection"). The attach was a `kubectl exec` stream per Workspace through
|
|
8
|
+
the API server. The scaling review (2026-09-27) measured 36 concurrent `kubectl` processes and about one core of
|
|
9
|
+
Orchestrator CPU at 40 Workspaces starting, and `workspace.lost` arriving 4.6–4.7 minutes after an eviction or drain.
|
|
10
|
+
Decisions since then need pod facts at once: the scheduler's reason for a Pending pod, a lost pod's reason, and
|
|
11
|
+
`OOMKilled` to name a memory fault (ADR-0060, ADR-0061).
|
|
12
|
+
|
|
13
|
+
## Decision
|
|
14
|
+
|
|
15
|
+
- **One in-process client on Node's built-in `fetch`, with no new dependency.** The Orchestrator is always in-cluster
|
|
16
|
+
(ADR-0019). The Deployment sets `NODE_EXTRA_CA_CERTS` to the ServiceAccount's `ca.crt` (concatenated with the ADR-0020
|
|
17
|
+
bundle if the Orchestrator ever needs both). The client does plain REST for jr2's own kinds — Sandbox, Repo, Secret —
|
|
18
|
+
including server-side apply, and one watch loop. Its rules, each observed on a real cluster (2026-09-29):
|
|
19
|
+
- The token is read from its file on every request, never cached: the kubelet replaced a 10-minute token only about 50
|
|
20
|
+
seconds before it expired. A `401` re-reads the file and retries once.
|
|
21
|
+
- A watch asks for bookmarks and `timeoutSeconds=240`. Built-in `fetch` aborts a response body that sends nothing for
|
|
22
|
+
300 seconds (`UND_ERR_BODY_TIMEOUT`, at 300.8s), and that timeout cannot be changed without undici's dispatcher; the
|
|
23
|
+
server closing first, and bookmarks about every 60 seconds, keep a healthy watch clear of it. The abort itself is a
|
|
24
|
+
resume, not a failure.
|
|
25
|
+
- Every bookmark's `resourceVersion` is kept, so a resume after a quiet period starts from a recent point.
|
|
26
|
+
- A resume that is too old arrives as an `ERROR` event with code 410 **inside an HTTP 200**. Any `ERROR` event
|
|
27
|
+
re-lists.
|
|
28
|
+
- Concurrent writes are capped (8–16). 200 parallel writes each opened a TLS connection and cost about 130% CPU.
|
|
29
|
+
- **The Orchestrator watches Sandboxes only**, one watch on the Instance's namespace — the namespace is the Instance's
|
|
30
|
+
identity (ADR-0019), and `jr2 up` refuses one another Instance owns — selected by the `jr2.dev/run` label every
|
|
31
|
+
Sandbox it provisions carries. Observed: event latency p50 11ms, p95 92ms, max 186ms; 0 events lost across a
|
|
32
|
+
200-object burst and a CRD run; about 75 MiB and 3% CPU for the watcher. **It never reads a Pod.** The operator owns
|
|
33
|
+
the pod (ADR-0001) and already reads it on every reconcile, so it publishes what the Orchestrator needs onto the
|
|
34
|
+
Sandbox's status: a scheduling condition with the scheduler's message — or a quota's refusal of the pod create
|
|
35
|
+
([ADR-0064](0064-a-workspace-waits-for-capacity.md)) — the Harness container's restarts and last terminated reason,
|
|
36
|
+
and `podUID`.
|
|
37
|
+
- **The watch drives everything that polled.**
|
|
38
|
+
- Placing and provisioning wait on watch events: an Unschedulable pod or a quota refusal is a wait with its reason
|
|
39
|
+
(ADR-0064), a crash-looping Harness is seen as it happens — and named: the Harness container's termination message
|
|
40
|
+
falls back to its last log lines (`terminationMessagePolicy: FallbackToLogsOnError`), so the fault that ends the
|
|
41
|
+
provision carries the Harness's own last words, and the Harness says them plainly at startup (a port another
|
|
42
|
+
container in the pod took: `EADDRINUSE` names the port and the cause). No reason is invented for what a log line
|
|
43
|
+
already says. And readiness proves the Harness, not the port: the probe is a GET of the Harness's own `/healthz`,
|
|
44
|
+
because a pod has one network namespace and a socket probe passes for whatever listens there — the nginx that took
|
|
45
|
+
`:8080` made the pod Ready and the attach met its 404 page (scaling review R16, 2026-09-30). A pod with a Sandbox
|
|
46
|
+
Effector is Ready only when that Effector serves too: its container is probed on its own `/healthz` (ADR-0068).
|
|
47
|
+
- Continuity: a gone or `Lost` Sandbox is `workspace.lost` within seconds, and a Harness restart ends the Turns
|
|
48
|
+
waiting on it (ADR-0021). A dropped watch is unknown, never loss; the loop re-lists and reconciles.
|
|
49
|
+
- A fetch ask (ADR-0053) is answered by the watch event that carries its landing.
|
|
50
|
+
- Restore (ADR-0012): the first list after start is the reconcile.
|
|
51
|
+
- **The Lease stays a write.** It is still the assertion that lets the operator reap an orphaned Sandbox (ADR-0001): one
|
|
52
|
+
merge patch per Workspace every 5 minutes, ±20% jitter so Leases never renew in lockstep after a restart. It no longer
|
|
53
|
+
carries detection.
|
|
54
|
+
- **The attach is a Harness call, not an exec.** The Harness runs the attach — ACL stamp, `git clone --shared` from the
|
|
55
|
+
node cache, remotes, `git worktree add` — on `POST /attach { slots, branch }`, authenticated like every Harness call
|
|
56
|
+
(ADR-0058). No stream per Workspace crosses the API server, and **`pods/exec` leaves the Orchestrator's RBAC**. It
|
|
57
|
+
also binds work late: a pod already running receives its Repos and branch, the shape standby Sandboxes and Agent
|
|
58
|
+
Substrate need.
|
|
59
|
+
|
|
60
|
+
## Considered options
|
|
61
|
+
|
|
62
|
+
- **`@kubernetes/client-node`.** Informers built in, but a large dependency tree and generated models for every kind jr2
|
|
63
|
+
never touches, around kubeconfig handling in-cluster code does not need. A contained swap if jr2 ever needs exec or
|
|
64
|
+
many more kinds: every call stays behind the Sandbox and Repo ports.
|
|
65
|
+
- **`node:https` with its own agent.** Trust scoped to this client alone, but transport code built-in `fetch` makes
|
|
66
|
+
unnecessary; the Orchestrator sets `NODE_EXTRA_CA_CERTS` for nothing else.
|
|
67
|
+
- **Keep `kubectl`, add `kubectl get -w`.** Fixes detection, not the process per write, and adds a long-lived child per
|
|
68
|
+
watch.
|
|
69
|
+
- **Watch Pods as well.** Two watches, and the Orchestrator interpreting pod internals the operator already reads.
|
|
70
|
+
- **A jr2 WebSocket API.** Nothing to add: a Kubernetes watch is already a pushed stream of events.
|
|
71
|
+
- **Keep the attach as exec through the new client.** The exec protocol is the hardest part of any Kubernetes client, it
|
|
72
|
+
keeps one stream per Workspace through the API server, and it keeps `pods/exec`.
|
|
73
|
+
- **Attach from an init container.** Declarative, but it fixes the Repos and branch at pod creation, which rules out
|
|
74
|
+
late binding, and it must wait on a cache that exists only after scheduling.
|
|
75
|
+
|
|
76
|
+
## Consequences
|
|
77
|
+
|
|
78
|
+
- ADR-0021 is rewritten in place: the watch answers, the Lease only asserts. ADR-0012, ADR-0004 and ADR-0053 now read
|
|
79
|
+
the watch and the Harness attach. ADR-0001: the operator publishes pod facts on the Sandbox's status, and writes
|
|
80
|
+
status only on change — every write is now a watch event at the Orchestrator.
|
|
81
|
+
- The Harness gains a route that is not about a conversation. It is jr2's own work in the container ADR-0005 gives it.
|
|
82
|
+
- The CLI keeps `kubectl` on the user's machine (`jr2 up`, `jr2 ssh`, ADR-0009); only the Orchestrator stops using it.
|
|
83
|
+
- Tests fake the client with a small HTTP server instead of a `kubectl` stub.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# A Workspace waits for capacity
|
|
2
|
+
|
|
3
|
+
Since ADR-0060 a Sandbox reserves its whole Size, so a full cluster shows as a Pending pod, not as eviction. But the
|
|
4
|
+
provision's 120-second budget started at the CR's write, so time spent Unschedulable used it up: the run errored, and
|
|
5
|
+
the CR, pod and Secret stayed until the operator's idle GC. A burst of 10 Workspaces at 500m each on the home cluster
|
|
6
|
+
(2026-09-29) placed 2; the other 8 errored at 120s and left Pending pods behind, one of which could take a node later
|
|
7
|
+
for nobody. Worse, the Lease was renewed only in `running`, so the operator counted creation as the Lease and would reap
|
|
8
|
+
a Sandbox that waited past its 30-minute idle timeout. The scaling review asked two more questions: should jr2 cap live
|
|
9
|
+
Workspaces for the Instance (R4), and concurrent Turns per model provider (R5)?
|
|
10
|
+
|
|
11
|
+
## Decision
|
|
12
|
+
|
|
13
|
+
- **A Sandbox with no node waits, and jr2 sets no deadline on the wait.** jr2 cannot tell "full now" from "never fits":
|
|
14
|
+
the cluster autoscaler, a finished neighbour or a drain can each make room, and the node set moves (ADR-0052).
|
|
15
|
+
`jr2 up` already warns when no node could hold a Size (ADR-0060). A Machine that wants a bound puts `after` on the
|
|
16
|
+
state that holds the Workspace; stopping it is the body's policy, as for `workspace.lost` (ADR-0021).
|
|
17
|
+
- **A quota refusal is the same wait.** A ResourceQuota (or Kueue) is the cluster owner's tool; jr2 neither writes one
|
|
18
|
+
nor replaces it. The operator publishes the refusal on the Sandbox's status and keeps retrying the pod create.
|
|
19
|
+
- **The Workspace Machine has a state for the wait: `placing`**, before `provisioning`. `placing` writes the CR and
|
|
20
|
+
waits for the pod to be scheduled, with no deadline. `provisioning` waits for `Ready` under the 120-second budget,
|
|
21
|
+
which now measures only what jr2 controls: the image pull, the Harness start and the Repos. A restart re-enters
|
|
22
|
+
`placing`: the apply is idempotent and the watch's first list answers at once (ADR-0063).
|
|
23
|
+
- **The wait is visible; its reason is behind the token.**
|
|
24
|
+
- The state is structure, so the Console lights `placing` and `observe()` carries it with no new field.
|
|
25
|
+
- The authenticated `RunStatus` gains `waiting: [{ child, on: "node" | "quota", message, since }]`. `message` is the
|
|
26
|
+
scheduler's or the quota's own words, which can name nodes and taints. `jr2 status <run>` prints it.
|
|
27
|
+
- The feed has two lines per wait: when it starts, with the reason, and when it ends ("placed after 3m12s"). The
|
|
28
|
+
duration of a wait this Orchestrator process opened is read on the monotonic clock, so it is never negative. A wait
|
|
29
|
+
opened before an Orchestrator restart has only its stored wall-clock start, so its duration is two wall-clock reads,
|
|
30
|
+
clamped at 0. A changed reason updates `waiting`; it adds no feed line.
|
|
31
|
+
- `jr2 runs` marks a run that has a Workspace in `placing`.
|
|
32
|
+
- The Machine receives no event: scheduling detail is not the body's concern, and `after` covers a bound.
|
|
33
|
+
- **The Lease covers the Sandbox from its write to its teardown**, in every state, so the idle GC reaps only what the
|
|
34
|
+
Orchestrator has abandoned (ADR-0001).
|
|
35
|
+
- **Delete what never ran; keep what ran.** A Sandbox whose wait ends without a node — a parent `after`, a stopped run,
|
|
36
|
+
a faulted run — is deleted at once: nothing on it can be inspected, and left alone it could take a node later. The
|
|
37
|
+
delete does not touch a name that a live Workspace of the run holds then: a parent that re-enters the state, or
|
|
38
|
+
invokes the same Machine again, gives its new Workspace the same invoke id and so the same name, and that Workspace
|
|
39
|
+
has placed it again ([ADR-0073](0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md)). A
|
|
40
|
+
Sandbox that was placed and then faulted keeps ADR-0021's rule: it stays inspectable (logs, `jr2 ssh`) and ages out at
|
|
41
|
+
the idle timeout.
|
|
42
|
+
- **jr2 caps nothing of its own.** Not live Workspaces (R4), not concurrent Turns per provider (R5). See the options.
|
|
43
|
+
- **A provider's refusal is named.** A Turn that ends because the Harness's provider retries ran out has a fault reason
|
|
44
|
+
with the fixed prefix `provider limit`, as a memory kill has `memory limit` (ADR-0061), so `jr2 status` and the
|
|
45
|
+
Machine can tell provider pressure from a bug.
|
|
46
|
+
|
|
47
|
+
## Considered options
|
|
48
|
+
|
|
49
|
+
- **Keep a deadline on the wait, or make it a config value.** Rejected: any number either faults a Workspace the
|
|
50
|
+
autoscaler was about to place or waits for one that never fits, and the Machine's own `after` already states the
|
|
51
|
+
body's bound where the body can act on it.
|
|
52
|
+
- **An Instance-wide cap on live Workspaces (`sandbox.max`)**, first come first served, persisted: Agent Substrate's
|
|
53
|
+
request parking in jr2's terms. Rejected: a count of Workspaces of mixed Sizes is not a measure of resources. A spend
|
|
54
|
+
ceiling is a ResourceQuota on `requests.cpu` and `requests.memory`, exact in resource units and refused before any pod
|
|
55
|
+
exists, so the autoscaler never scales for it. Placement order among Pending pods of one priority is not promised, and
|
|
56
|
+
nothing in jr2 needs strict order. Substrate's parking is driven by capacity, not a count.
|
|
57
|
+
- **An Instance-wide cap on concurrent Turns per provider (R5).** Rejected:
|
|
58
|
+
- A Turn is the wrong unit: it is many model calls with tool runs between them, and a slot held while `tsc` runs
|
|
59
|
+
blocks the provider while it has nothing to do.
|
|
60
|
+
- A fixed number is the wrong measure: hosted providers limit tokens and requests per minute, vLLM by batch and KV
|
|
61
|
+
cache.
|
|
62
|
+
- The wait already happens in the right place: vLLM queues in arrival order, and the Harness retries twice, through
|
|
63
|
+
pi-ai, with backoff, and honors `retry-after` up to 60 s (pi-ai 1.4.0). pi-ai retries what the SDKs retry: a 408, a
|
|
64
|
+
409, a 429, each 5xx, and a connection error (an error with no status). So a quota 429 is asked again too; its
|
|
65
|
+
settlement keeps the quota's own words, never `provider limit`. The scaling review measured slow Turns at 40
|
|
66
|
+
concurrent (about 90s against 14s alone), which is throughput a cap cannot add, and no failed ones.
|
|
67
|
+
- A slow provider cannot make a Runaway: ADR-0035 counts steps and repeats, not time.
|
|
68
|
+
- If a hosted provider's 429 storm is ever measured, the answer is shared backoff at the Custodian (Envoy's global
|
|
69
|
+
rate limit), not a Turn count.
|
|
70
|
+
- **Watch for a Pending pod from the Orchestrator.** Rejected by ADR-0063: the operator reads the pod and publishes the
|
|
71
|
+
scheduling condition.
|
|
72
|
+
|
|
73
|
+
## Consequences
|
|
74
|
+
|
|
75
|
+
- ADR-0012's lifecycle gains `placing`. ADR-0021: the Lease is no longer only in `running`, so "attach must stay well
|
|
76
|
+
inside the idle timeout" goes away. ADR-0001: the operator publishes a quota refusal like an Unschedulable pod.
|
|
77
|
+
- A Workspace parked in `placing` holds no node, only a CR and a Secret. A Workspace that waits a long time still holds
|
|
78
|
+
its run's place in any `pool()`: the Pool's cap is the Machine's own concurrency policy.
|
|
79
|
+
- GLOSSARY.md gains **Placing**, and **Lease** covers the Sandbox's whole life.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# A persist survives a crash, and history is a week
|
|
2
|
+
|
|
3
|
+
Every transition of a run persists its whole snapshot (ADR-0007). The scaling review (R6) read the store and listed its
|
|
4
|
+
costs: `JSON.stringify` of the whole run, a synchronous upsert with an fsync on the event loop, a Pool's run as one blob
|
|
5
|
+
holding every worker, terminal rows never deleted, and `restore()` parsing every row before the server listens. It also
|
|
6
|
+
found the words "Postgres opt-in" in GLOSSARY.md, ADR-0008 and ADR-0009, with no such store in the code. Measured on the
|
|
7
|
+
home cluster's chaos Instance (2026-09-30, `local-path` volume): a persist costs **4.5 ms, whatever the blob's size** (1
|
|
8
|
+
KB or 100 KB), because the cost is the fsync, not the JSON; with the write-ahead log and `synchronous=NORMAL` the same
|
|
9
|
+
upserts cost 0.04 ms (1 KB) and 0.18 ms (100 KB). A run's blob is about 1 KB; a Pool of 40 workers is about 100 KB.
|
|
10
|
+
|
|
11
|
+
## Decision
|
|
12
|
+
|
|
13
|
+
- **A persist survives a process crash, not a power loss.** The store runs sqlite in WAL mode with `synchronous=NORMAL`:
|
|
14
|
+
a write is durable once the OS holds it, and sqlite fsyncs only at a checkpoint. A node's power loss can lose the
|
|
15
|
+
transitions since the last checkpoint; the file stays consistent, and `restore()` treats the older snapshot as it
|
|
16
|
+
treats any restart — reconcile against the live world, re-attach, or `lost`. The window in which a crash re-POSTs a
|
|
17
|
+
prompt (ADR-0007's at-most-once sliver) widens from one microtask to one checkpoint, and only on a power loss, which
|
|
18
|
+
also ends every Sandbox on that node.
|
|
19
|
+
- **The write stays synchronous and on the event loop.** At 0.2 ms for a Pool's blob there is nothing to move off the
|
|
20
|
+
loop, and the ordering matters: the row is on disk before the Machine's next side effect runs, which is what lets an
|
|
21
|
+
epoch bump (ADR-0057), an admission (ADR-0016) or an Effect's intent row (ADR-0067) be saved "at the write" — the
|
|
22
|
+
intent row before its call leaves. A worker thread, an asynchronous driver or a network store would land the row after
|
|
23
|
+
the next `invoke` fires, on every persist.
|
|
24
|
+
- **One blob per run stays.** The snapshot is a tree that xstate restores from its root, and one row keeps it atomic —
|
|
25
|
+
with the ledgers beside it (admissions, epochs, notices, timers, outstanding picks, Effects); a Pool's cost at 40
|
|
26
|
+
workers is 0.2 ms, so its workers do not become rows of their own. The Effect ledger keeps a row for every Effect the
|
|
27
|
+
run started (ADR-0067), so a Machine that loops through an Effect grows its blob by one row per pass: id, kind and
|
|
28
|
+
outcome. The output's tail is dropped once the save that carries the Effect's transition is on disk; a push row keeps
|
|
29
|
+
its published commit, its source, its `squashed` and its sync's answer whole — every later push of its Repo Slot sends
|
|
30
|
+
them as one of its pairs — and its bundle only while it is its Repo Slot's newest push and its sync did not finish —
|
|
31
|
+
one bundle per Repo Slot at most. An outstanding pick's hold is the one place tails accumulate: its `chain` keeps
|
|
32
|
+
every reply since the pick whole until the pick is answered, then it is dropped (ADR-0067).
|
|
33
|
+
- **`restore()` reads live rows only.** The store answers `WHERE status = 'live'`; terminal rows are not parsed at boot.
|
|
34
|
+
- **A finished run's row lives 7 days, fixed.** Every non-live status — `done`, `failed`, `error`, `cancelled`, `lost`,
|
|
35
|
+
`drifted` — ages from its terminal write, and a sweep at boot and every hour deletes what is older. A `live` row is
|
|
36
|
+
never swept: a run parked on a Gate for a month stays and is restored. The value is fixed like the Sandbox idle
|
|
37
|
+
timeout, not config: jr2 is not the record of a run's results (a Machine sends its output where it belongs — a PR, a
|
|
38
|
+
Source), and `jr2 status` on an old run is a debugging act. A swept id frees its abbreviations (ADR-0009).
|
|
39
|
+
- **The store is sqlite on the Instance's volume. No Postgres is owed.** The `SnapshotStore` interface stays as the
|
|
40
|
+
seam, with one implementation. `DATABASE_URL` is not a thing jr2 reads.
|
|
41
|
+
|
|
42
|
+
## Considered options
|
|
43
|
+
|
|
44
|
+
- **Keep `synchronous=FULL`, move the write to a worker thread.** Rejected: the thread hides 4.5 ms from the loop but
|
|
45
|
+
does not make the disk faster, so writes queue under load and the loop no longer knows how far behind the store is —
|
|
46
|
+
and it opens the ordering gap on every write, which NORMAL opens only on a power loss.
|
|
47
|
+
- **Batch persists in a window.** Rejected: one persist per macrostep already coalesces a transition's events, and with
|
|
48
|
+
the fsync gone there is nothing left to amortize.
|
|
49
|
+
- **A row per worker.** Rejected: it splits an atomic tree across rows (a worker's row saved, the Pool's not), makes
|
|
50
|
+
restore reassemble what xstate restores whole, and buys nothing measurable at 0.2 ms per blob.
|
|
51
|
+
- **Configurable retention (`runs.retain`), or a count cap per workflow.** Rejected: a count cap does not bound a busy
|
|
52
|
+
workflow's disk and punishes a quiet one; a config key is owed only when a history surface (a list, a query) exists to
|
|
53
|
+
use it, and none does — `jr2 runs` and the Console list live runs.
|
|
54
|
+
- **Build the Postgres store.** Rejected: the only thing it buys is an Orchestrator pod that reschedules without its
|
|
55
|
+
volume, which is a storage-class question (a network RWO volume follows the pod) and belongs to the Orchestrator pod's
|
|
56
|
+
own decision (R10), not to the store. Its cost is a dependency the owner operates, a second store in every test tier,
|
|
57
|
+
and the asynchronous write above.
|
|
58
|
+
|
|
59
|
+
## Consequences
|
|
60
|
+
|
|
61
|
+
- ADR-0008 and ADR-0009 lose "Postgres opt-in" and `DATABASE_URL`; ADR-0008's Orchestrator persists its snapshots "to
|
|
62
|
+
sqlite on its volume". ADR-0007 points here for the store's durability.
|
|
63
|
+
- The Instance's volume gains `state.db-wal` and `state.db-shm` beside `state.db`.
|
|
64
|
+
- A `drifted` run (ADR-0030) is swept a week after the deploy that changed its Machine, not a week after it ended; a
|
|
65
|
+
week is the time a human has to read it.
|