@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.
Files changed (327) hide show
  1. package/README.md +20 -2
  2. package/console/components/app.ts +2 -0
  3. package/console/components/fleet.ts +14 -1
  4. package/console/components/machine-pane.ts +41 -2
  5. package/console/main.ts +13 -1
  6. package/console/store.ts +102 -6
  7. package/console/style.css +21 -0
  8. package/docs/agents-block.md +5 -0
  9. package/docs/concepts/agents-and-turns.md +442 -0
  10. package/docs/concepts/architecture.md +230 -0
  11. package/docs/concepts/charts.md +143 -0
  12. package/docs/concepts/composition.md +357 -0
  13. package/docs/concepts/converge.md +254 -0
  14. package/docs/concepts/durable-runs.md +189 -0
  15. package/docs/concepts/effects.md +603 -0
  16. package/docs/concepts/gates.md +208 -0
  17. package/docs/concepts/machines-and-workflows.md +289 -0
  18. package/docs/concepts/pools-and-sources.md +230 -0
  19. package/docs/concepts/secrets-and-trust.md +222 -0
  20. package/docs/concepts/the-menu.md +378 -0
  21. package/docs/concepts/workspaces.md +678 -0
  22. package/docs/get-started/a-model-decides.md +563 -0
  23. package/docs/get-started/an-agent-in-a-sandbox.md +381 -0
  24. package/docs/get-started/connect-a-model.md +248 -0
  25. package/docs/get-started/create-an-instance.md +194 -0
  26. package/docs/get-started/have-your-assistant-write-a-machine.md +922 -0
  27. package/docs/get-started/install.md +126 -0
  28. package/docs/guides/add-tools-to-the-sandbox.md +295 -0
  29. package/docs/guides/answer-a-gate.md +304 -0
  30. package/docs/guides/bind-open-parts.md +223 -0
  31. package/docs/guides/cancel-a-run.md +146 -0
  32. package/docs/guides/clean-up.md +206 -0
  33. package/docs/guides/fix-a-failed-converge.md +361 -0
  34. package/docs/guides/give-the-cluster-git-credentials.md +222 -0
  35. package/docs/guides/iterate-on-a-machine-on-kind.md +190 -0
  36. package/docs/guides/let-an-agent-write-a-chart.md +226 -0
  37. package/docs/guides/look-inside-a-sandbox.md +174 -0
  38. package/docs/guides/mirror-the-kit-images.md +176 -0
  39. package/docs/guides/move-from-kind-to-a-shared-cluster.md +302 -0
  40. package/docs/guides/open-the-console.md +91 -0
  41. package/docs/guides/publish-a-machine-as-an-npm-package.md +350 -0
  42. package/docs/guides/push-and-sign-a-branch.md +486 -0
  43. package/docs/guides/run-a-chart-by-name.md +270 -0
  44. package/docs/guides/run-a-command-or-call-a-url.md +544 -0
  45. package/docs/guides/run-one-worker-per-item.md +321 -0
  46. package/docs/guides/set-an-agents-access-and-dials.md +375 -0
  47. package/docs/guides/size-a-workspace.md +204 -0
  48. package/docs/guides/start-a-run-from-a-run.md +237 -0
  49. package/docs/guides/start-a-run.md +207 -0
  50. package/docs/guides/upgrade-the-kit-version.md +240 -0
  51. package/docs/guides/use-a-hosted-provider.md +208 -0
  52. package/docs/guides/use-a-local-model.md +313 -0
  53. package/docs/guides/watch-a-run.md +326 -0
  54. package/docs/guides/write-a-workflow.md +318 -0
  55. package/docs/index.md +44 -0
  56. package/docs/llms.txt +216 -0
  57. package/docs/reference/authoring-surface/agent.md +128 -0
  58. package/docs/reference/authoring-surface/answer.md +140 -0
  59. package/docs/reference/authoring-surface/arg.md +60 -0
  60. package/docs/reference/authoring-surface/basic.md +62 -0
  61. package/docs/reference/authoring-surface/bearer.md +53 -0
  62. package/docs/reference/authoring-surface/chartschema.md +23 -0
  63. package/docs/reference/authoring-surface/chartschemafor.md +48 -0
  64. package/docs/reference/authoring-surface/command.md +191 -0
  65. package/docs/reference/authoring-surface/customize.md +64 -0
  66. package/docs/reference/authoring-surface/defineconfig.md +41 -0
  67. package/docs/reference/authoring-surface/defineevent.md +114 -0
  68. package/docs/reference/authoring-surface/dispatch.md +126 -0
  69. package/docs/reference/authoring-surface/fail.md +83 -0
  70. package/docs/reference/authoring-surface/file.md +122 -0
  71. package/docs/reference/authoring-surface/frompick.md +277 -0
  72. package/docs/reference/authoring-surface/http.md +159 -0
  73. package/docs/reference/authoring-surface/isfailure.md +58 -0
  74. package/docs/reference/authoring-surface/jr2setup.md +237 -0
  75. package/docs/reference/authoring-surface/open.md +74 -0
  76. package/docs/reference/authoring-surface/openbody.md +86 -0
  77. package/docs/reference/authoring-surface/paramsof.md +125 -0
  78. package/docs/reference/authoring-surface/pool.md +186 -0
  79. package/docs/reference/authoring-surface/push.md +112 -0
  80. package/docs/reference/authoring-surface/secret.md +118 -0
  81. package/docs/reference/authoring-surface/sentby.md +83 -0
  82. package/docs/reference/authoring-surface/source.md +51 -0
  83. package/docs/reference/authoring-surface/text.md +62 -0
  84. package/docs/reference/authoring-surface/type-agentdeclaration.md +27 -0
  85. package/docs/reference/authoring-surface/type-agentdefinition.md +80 -0
  86. package/docs/reference/authoring-surface/type-agentlogic.md +15 -0
  87. package/docs/reference/authoring-surface/type-agentslots.md +12 -0
  88. package/docs/reference/authoring-surface/type-answeraction.md +12 -0
  89. package/docs/reference/authoring-surface/type-arg.md +19 -0
  90. package/docs/reference/authoring-surface/type-argnames.md +12 -0
  91. package/docs/reference/authoring-surface/type-binding.md +62 -0
  92. package/docs/reference/authoring-surface/type-bindingof.md +12 -0
  93. package/docs/reference/authoring-surface/type-bodyacceptsdoor.md +15 -0
  94. package/docs/reference/authoring-surface/type-bodyacceptsslots.md +17 -0
  95. package/docs/reference/authoring-surface/type-ceilings.md +41 -0
  96. package/docs/reference/authoring-surface/type-chainlink.md +19 -0
  97. package/docs/reference/authoring-surface/type-chartaction.md +46 -0
  98. package/docs/reference/authoring-surface/type-chartagent.md +28 -0
  99. package/docs/reference/authoring-surface/type-chartevent.md +20 -0
  100. package/docs/reference/authoring-surface/type-chartguard.md +20 -0
  101. package/docs/reference/authoring-surface/type-chartinvoke.md +28 -0
  102. package/docs/reference/authoring-surface/type-chartjson.md +14 -0
  103. package/docs/reference/authoring-surface/type-chartstate.md +46 -0
  104. package/docs/reference/authoring-surface/type-charttext.md +40 -0
  105. package/docs/reference/authoring-surface/type-charttransition.md +22 -0
  106. package/docs/reference/authoring-surface/type-charttransitions.md +14 -0
  107. package/docs/reference/authoring-surface/type-chartvalue.md +15 -0
  108. package/docs/reference/authoring-surface/type-childat.md +12 -0
  109. package/docs/reference/authoring-surface/type-childslots.md +12 -0
  110. package/docs/reference/authoring-surface/type-commandenv.md +15 -0
  111. package/docs/reference/authoring-surface/type-commandinput.md +19 -0
  112. package/docs/reference/authoring-surface/type-commandoptions.md +78 -0
  113. package/docs/reference/authoring-surface/type-commandoutput.md +104 -0
  114. package/docs/reference/authoring-surface/type-customize.md +109 -0
  115. package/docs/reference/authoring-surface/type-dispatchdefinition.md +39 -0
  116. package/docs/reference/authoring-surface/type-dispatchinput.md +14 -0
  117. package/docs/reference/authoring-surface/type-dispatchopenparts.md +14 -0
  118. package/docs/reference/authoring-surface/type-dispatchoutput.md +65 -0
  119. package/docs/reference/authoring-surface/type-duration.md +15 -0
  120. package/docs/reference/authoring-surface/type-effectbinding.md +93 -0
  121. package/docs/reference/authoring-surface/type-effectlogic.md +12 -0
  122. package/docs/reference/authoring-surface/type-envopenparts.md +14 -0
  123. package/docs/reference/authoring-surface/type-eventaudience.md +27 -0
  124. package/docs/reference/authoring-surface/type-eventdef.md +45 -0
  125. package/docs/reference/authoring-surface/type-eventfrom.md +16 -0
  126. package/docs/reference/authoring-surface/type-eventsemantics.md +26 -0
  127. package/docs/reference/authoring-surface/type-execoutcome.md +34 -0
  128. package/docs/reference/authoring-surface/type-failslot.md +14 -0
  129. package/docs/reference/authoring-surface/type-failure.md +19 -0
  130. package/docs/reference/authoring-surface/type-failureinput.md +20 -0
  131. package/docs/reference/authoring-surface/type-filebody.md +41 -0
  132. package/docs/reference/authoring-surface/type-frominput.md +14 -0
  133. package/docs/reference/authoring-surface/type-hostinjectedinput.md +58 -0
  134. package/docs/reference/authoring-surface/type-httpdefinition.md +101 -0
  135. package/docs/reference/authoring-surface/type-httpinput.md +12 -0
  136. package/docs/reference/authoring-surface/type-httpmethod.md +14 -0
  137. package/docs/reference/authoring-surface/type-httpopenparts.md +12 -0
  138. package/docs/reference/authoring-surface/type-httpoutput.md +89 -0
  139. package/docs/reference/authoring-surface/type-httprequestparts.md +33 -0
  140. package/docs/reference/authoring-surface/type-jr2config.md +126 -0
  141. package/docs/reference/authoring-surface/type-lends.md +38 -0
  142. package/docs/reference/authoring-surface/type-logicat.md +12 -0
  143. package/docs/reference/authoring-surface/type-markedif.md +12 -0
  144. package/docs/reference/authoring-surface/type-mergedactors.md +12 -0
  145. package/docs/reference/authoring-surface/type-namedsecret.md +25 -0
  146. package/docs/reference/authoring-surface/type-noslotspec.md +26 -0
  147. package/docs/reference/authoring-surface/type-noslotworkspaceoptions.md +32 -0
  148. package/docs/reference/authoring-surface/type-openbodydeclaration.md +40 -0
  149. package/docs/reference/authoring-surface/type-openbodylogic.md +14 -0
  150. package/docs/reference/authoring-surface/type-openbodyoutput.md +42 -0
  151. package/docs/reference/authoring-surface/type-openbodyslot.md +18 -0
  152. package/docs/reference/authoring-surface/type-opencommandinput.md +17 -0
  153. package/docs/reference/authoring-surface/type-openeffect.md +12 -0
  154. package/docs/reference/authoring-surface/type-openeffectslots.md +12 -0
  155. package/docs/reference/authoring-surface/type-openof.md +12 -0
  156. package/docs/reference/authoring-surface/type-opensecret.md +23 -0
  157. package/docs/reference/authoring-surface/type-opensecretmark.md +12 -0
  158. package/docs/reference/authoring-surface/type-permissivenoslotworkspaceoptions.md +31 -0
  159. package/docs/reference/authoring-surface/type-permissiveworkspaceoptions.md +32 -0
  160. package/docs/reference/authoring-surface/type-poolmachine.md +28 -0
  161. package/docs/reference/authoring-surface/type-poolspec.md +60 -0
  162. package/docs/reference/authoring-surface/type-pushinput.md +15 -0
  163. package/docs/reference/authoring-surface/type-pushoptions.md +79 -0
  164. package/docs/reference/authoring-surface/type-pushoutput.md +124 -0
  165. package/docs/reference/authoring-surface/type-pushwireoutput.md +12 -0
  166. package/docs/reference/authoring-surface/type-quantity.md +14 -0
  167. package/docs/reference/authoring-surface/type-reached.md +12 -0
  168. package/docs/reference/authoring-surface/type-reposlot.md +70 -0
  169. package/docs/reference/authoring-surface/type-reposlotsof.md +16 -0
  170. package/docs/reference/authoring-surface/type-sandboxoptions.md +77 -0
  171. package/docs/reference/authoring-surface/type-secret.md +25 -0
  172. package/docs/reference/authoring-surface/type-sender.md +19 -0
  173. package/docs/reference/authoring-surface/type-sentbyguards.md +12 -0
  174. package/docs/reference/authoring-surface/type-size.md +85 -0
  175. package/docs/reference/authoring-surface/type-slothandles.md +32 -0
  176. package/docs/reference/authoring-surface/type-sourcenextresult.md +17 -0
  177. package/docs/reference/authoring-surface/type-sourcespec.md +44 -0
  178. package/docs/reference/authoring-surface/type-syncstate.md +12 -0
  179. package/docs/reference/authoring-surface/type-text.md +19 -0
  180. package/docs/reference/authoring-surface/type-textpart.md +24 -0
  181. package/docs/reference/authoring-surface/type-thinkinglevel.md +39 -0
  182. package/docs/reference/authoring-surface/type-toparameterizedobject.md +14 -0
  183. package/docs/reference/authoring-surface/type-toprovidedactor.md +14 -0
  184. package/docs/reference/authoring-surface/type-unknown.md +21 -0
  185. package/docs/reference/authoring-surface/type-usercontainer.md +77 -0
  186. package/docs/reference/authoring-surface/type-withoutemptyindex.md +20 -0
  187. package/docs/reference/authoring-surface/type-workflowevent.md +46 -0
  188. package/docs/reference/authoring-surface/type-workspaceaccess.md +16 -0
  189. package/docs/reference/authoring-surface/type-workspaced.md +94 -0
  190. package/docs/reference/authoring-surface/type-workspacehandles.md +23 -0
  191. package/docs/reference/authoring-surface/type-workspacemachine.md +29 -0
  192. package/docs/reference/authoring-surface/type-workspaceof.md +13 -0
  193. package/docs/reference/authoring-surface/type-workspaceoptions.md +39 -0
  194. package/docs/reference/authoring-surface/type-workspacespec.md +37 -0
  195. package/docs/reference/authoring-surface/workspace.md +259 -0
  196. package/docs/reference/authoring-surface.md +197 -0
  197. package/docs/reference/cli.md +433 -0
  198. package/docs/reference/config.md +756 -0
  199. package/docs/reference/decisions/0001-operator-owned-generic-sandbox.md +113 -0
  200. package/docs/reference/decisions/0002-duplex-streaming-actor.md +110 -0
  201. package/docs/reference/decisions/0004-worktree-provisioning-model.md +124 -0
  202. package/docs/reference/decisions/0005-sandbox-pod-composition.md +230 -0
  203. package/docs/reference/decisions/0006-agent-control-surface.md +47 -0
  204. package/docs/reference/decisions/0007-durable-machine-state.md +109 -0
  205. package/docs/reference/decisions/0008-orchestrator-as-deployed-app.md +60 -0
  206. package/docs/reference/decisions/0009-cli-and-instance-interface.md +275 -0
  207. package/docs/reference/decisions/0010-bdd-acceptance-tests.md +160 -0
  208. package/docs/reference/decisions/0011-workflow-defined-events.md +141 -0
  209. package/docs/reference/decisions/0012-workspace-wrapper-machine.md +103 -0
  210. package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +157 -0
  211. package/docs/reference/decisions/0014-observation-is-open-run-state-is-not.md +95 -0
  212. package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +265 -0
  213. package/docs/reference/decisions/0016-agent-turn-mechanics-are-internal.md +124 -0
  214. package/docs/reference/decisions/0017-pool-over-a-source-port.md +69 -0
  215. package/docs/reference/decisions/0018-instance-agents-are-definitions-jr2-assembles-the-harness.md +111 -0
  216. package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +275 -0
  217. package/docs/reference/decisions/0020-private-ca-trust-is-a-named-concept.md +60 -0
  218. package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +100 -0
  219. package/docs/reference/decisions/0022-observation-is-a-level-triggered-feed.md +103 -0
  220. package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +176 -0
  221. package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +439 -0
  222. package/docs/reference/decisions/0025-cancel-ends-a-run-stop-parks-it.md +65 -0
  223. package/docs/reference/decisions/0026-a-turn-that-is-over-is-never-prompted.md +62 -0
  224. package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +237 -0
  225. package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +89 -0
  226. package/docs/reference/decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md +154 -0
  227. package/docs/reference/decisions/0030-a-snapshot-names-the-machine-it-was-written-under.md +100 -0
  228. package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +190 -0
  229. package/docs/reference/decisions/0032-the-console-unlocks-with-the-instance-token.md +61 -0
  230. package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +127 -0
  231. package/docs/reference/decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md +57 -0
  232. package/docs/reference/decisions/0035-a-runaway-turn-is-ended-rerolled-once-then-a-fault.md +92 -0
  233. package/docs/reference/decisions/0036-a-turn-compacts-its-own-context-mid-flight.md +138 -0
  234. package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +196 -0
  235. package/docs/reference/decisions/0038-jr2-up-builds-every-image-it-deploys.md +170 -0
  236. package/docs/reference/decisions/0039-image-garbage-collects-by-reachability.md +115 -0
  237. package/docs/reference/decisions/0041-a-build-the-host-already-holds-is-not-spent-again.md +64 -0
  238. package/docs/reference/decisions/0042-ready-is-not-routable-an-admission-retries-its-connection.md +142 -0
  239. package/docs/reference/decisions/0043-the-kit-is-tested-as-installed-a-local-registry-stands-in-for-npm.md +170 -0
  240. package/docs/reference/decisions/0044-kit-images-live-at-a-canonical-home-a-self-host-mirrors-it.md +85 -0
  241. package/docs/reference/decisions/0045-the-platform-joins-the-image-address-and-the-cluster-chooses-it.md +82 -0
  242. package/docs/reference/decisions/0046-a-converge-failure-carries-its-diagnosis.md +34 -0
  243. package/docs/reference/decisions/0047-the-git-ssh-key-source-is-the-users-choice.md +47 -0
  244. package/docs/reference/decisions/0048-the-orchestrator-boots-without-its-repos.md +36 -0
  245. package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +126 -0
  246. package/docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md +73 -0
  247. package/docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md +227 -0
  248. package/docs/reference/decisions/0052-a-sandbox-lands-where-an-ordinary-pod-lands-and-the-cache-follows-it.md +81 -0
  249. package/docs/reference/decisions/0053-a-fetch-inside-the-pod-asks-the-node-cache-and-the-cache-asks-the-remote.md +111 -0
  250. package/docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md +126 -0
  251. package/docs/reference/decisions/0055-a-release-is-a-pushed-tag-and-the-publish-guard-is-the-credential.md +107 -0
  252. package/docs/reference/decisions/0056-a-global-jr2-hands-off-to-the-instances-own-and-an-instance-has-one-kit-version.md +62 -0
  253. package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +152 -0
  254. package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +106 -0
  255. package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +384 -0
  256. package/docs/reference/decisions/0060-a-workspace-states-its-size-and-jr2-reserves-all-of-it.md +111 -0
  257. package/docs/reference/decisions/0061-a-memory-kill-ends-the-agents-processes-not-the-harness.md +83 -0
  258. package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +212 -0
  259. package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +83 -0
  260. package/docs/reference/decisions/0064-a-workspace-waits-for-capacity.md +79 -0
  261. package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-a-week.md +65 -0
  262. 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
  263. package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +430 -0
  264. package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +464 -0
  265. package/docs/reference/decisions/0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md +180 -0
  266. package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +323 -0
  267. package/docs/reference/decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md +183 -0
  268. package/docs/reference/decisions/0072-the-docs-ship-inside-the-kit-and-an-instances-agents-md-indexes-them.md +144 -0
  269. package/docs/reference/decisions/0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md +122 -0
  270. package/docs/reference/decisions/0074-an-unrouted-mechanism-event-is-refused-at-load-and-faults-the-run.md +76 -0
  271. package/docs/reference/decisions.md +83 -0
  272. package/docs/reference/glossary.md +648 -0
  273. package/docs/reference/http-api.md +1348 -0
  274. package/docs/reference/machines.md +190 -0
  275. package/docs/reference/mechanism-events.md +72 -0
  276. package/docs/why.md +206 -0
  277. package/package.json +5 -3
  278. package/src/actor.ts +541 -79
  279. package/src/agent.ts +114 -38
  280. package/src/ambient.ts +2 -1
  281. package/src/assembly-checks.ts +189 -7
  282. package/src/cause.ts +234 -0
  283. package/src/chart.ts +169 -37
  284. package/src/charts.ts +34 -6
  285. package/src/config.ts +430 -178
  286. package/src/custodian.ts +74 -22
  287. package/src/customize.ts +188 -46
  288. package/src/dispatch-effect.ts +945 -0
  289. package/src/door-compat.ts +217 -0
  290. package/src/durability.ts +11 -4
  291. package/src/effect.ts +341 -44
  292. package/src/effector-wire.ts +160 -11
  293. package/src/fail.ts +209 -0
  294. package/src/harness-client.ts +541 -47
  295. package/src/http-effect.ts +495 -53
  296. package/src/http.ts +427 -16
  297. package/src/index.ts +60 -82
  298. package/src/instance.ts +68 -8
  299. package/src/internal.ts +84 -0
  300. package/src/machine-doc.ts +21 -3
  301. package/src/mechanism-routes.ts +168 -0
  302. package/src/model-keys.generated.ts +53 -0
  303. package/src/model-keys.ts +18 -58
  304. package/src/notices.ts +66 -13
  305. package/src/open-body.ts +159 -23
  306. package/src/open-effect.ts +81 -8
  307. package/src/open.ts +59 -6
  308. package/src/parts.ts +184 -13
  309. package/src/party.ts +68 -2
  310. package/src/pick.ts +463 -11
  311. package/src/pool.ts +247 -36
  312. package/src/registration.ts +83 -4
  313. package/src/repos.ts +2 -1
  314. package/src/run-host.ts +565 -31
  315. package/src/run-output.ts +38 -0
  316. package/src/sandbox-effect.ts +273 -38
  317. package/src/sandbox-kube.ts +3 -1
  318. package/src/secret-placement.ts +52 -17
  319. package/src/server.ts +3 -0
  320. package/src/setup.ts +239 -56
  321. package/src/size.ts +121 -9
  322. package/src/snapshot-store.ts +54 -10
  323. package/src/stub-harness.ts +44 -16
  324. package/src/timers.ts +14 -1
  325. package/src/vocabulary.ts +69 -14
  326. package/src/wire.ts +90 -16
  327. 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.