@jr2/orchestrator 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (361) 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 +478 -0
  10. package/docs/concepts/architecture.md +243 -0
  11. package/docs/concepts/charts.md +146 -0
  12. package/docs/concepts/composition.md +357 -0
  13. package/docs/concepts/converge.md +257 -0
  14. package/docs/concepts/durable-runs.md +189 -0
  15. package/docs/concepts/effects.md +658 -0
  16. package/docs/concepts/gates.md +208 -0
  17. package/docs/concepts/machines-and-workflows.md +298 -0
  18. package/docs/concepts/pools-and-sources.md +230 -0
  19. package/docs/concepts/secrets-and-trust.md +306 -0
  20. package/docs/concepts/the-menu.md +381 -0
  21. package/docs/concepts/workspaces.md +756 -0
  22. package/docs/get-started/a-model-decides.md +563 -0
  23. package/docs/get-started/an-agent-in-a-sandbox.md +383 -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 +993 -0
  27. package/docs/get-started/install.md +130 -0
  28. package/docs/guides/add-tools-to-the-sandbox.md +308 -0
  29. package/docs/guides/answer-a-gate.md +304 -0
  30. package/docs/guides/bind-open-parts.md +224 -0
  31. package/docs/guides/cancel-a-run.md +146 -0
  32. package/docs/guides/clean-up.md +206 -0
  33. package/docs/guides/cut-a-worktree.md +560 -0
  34. package/docs/guides/fix-a-failed-converge.md +361 -0
  35. package/docs/guides/give-the-cluster-git-credentials.md +222 -0
  36. package/docs/guides/iterate-on-a-machine-on-kind.md +190 -0
  37. package/docs/guides/let-an-agent-write-a-chart.md +230 -0
  38. package/docs/guides/look-inside-a-sandbox.md +177 -0
  39. package/docs/guides/mirror-the-kit-images.md +176 -0
  40. package/docs/guides/move-from-kind-to-a-shared-cluster.md +318 -0
  41. package/docs/guides/open-the-console.md +91 -0
  42. package/docs/guides/publish-a-machine-as-an-npm-package.md +350 -0
  43. package/docs/guides/push-and-sign-a-branch.md +532 -0
  44. package/docs/guides/run-a-chart-by-name.md +270 -0
  45. package/docs/guides/run-a-command-or-call-a-url.md +587 -0
  46. package/docs/guides/run-one-worker-per-item.md +322 -0
  47. package/docs/guides/set-an-agents-access-and-dials.md +420 -0
  48. package/docs/guides/size-a-workspace.md +217 -0
  49. package/docs/guides/start-a-run-from-a-run.md +237 -0
  50. package/docs/guides/start-a-run.md +207 -0
  51. package/docs/guides/upgrade-the-kit-version.md +240 -0
  52. package/docs/guides/use-a-hosted-provider.md +214 -0
  53. package/docs/guides/use-a-local-model.md +313 -0
  54. package/docs/guides/watch-a-run.md +349 -0
  55. package/docs/guides/write-a-workflow.md +318 -0
  56. package/docs/index.md +44 -0
  57. package/docs/llms.txt +237 -0
  58. package/docs/reference/authoring-surface/agent.md +151 -0
  59. package/docs/reference/authoring-surface/answer.md +156 -0
  60. package/docs/reference/authoring-surface/arg.md +82 -0
  61. package/docs/reference/authoring-surface/basic.md +62 -0
  62. package/docs/reference/authoring-surface/bearer.md +53 -0
  63. package/docs/reference/authoring-surface/branchname.md +15 -0
  64. package/docs/reference/authoring-surface/chartschema.md +23 -0
  65. package/docs/reference/authoring-surface/chartschemafor.md +48 -0
  66. package/docs/reference/authoring-surface/command.md +206 -0
  67. package/docs/reference/authoring-surface/customize.md +64 -0
  68. package/docs/reference/authoring-surface/defineconfig.md +41 -0
  69. package/docs/reference/authoring-surface/defineevent.md +114 -0
  70. package/docs/reference/authoring-surface/dispatch.md +126 -0
  71. package/docs/reference/authoring-surface/fail.md +96 -0
  72. package/docs/reference/authoring-surface/file.md +142 -0
  73. package/docs/reference/authoring-surface/frompick.md +277 -0
  74. package/docs/reference/authoring-surface/http.md +259 -0
  75. package/docs/reference/authoring-surface/isfailure.md +58 -0
  76. package/docs/reference/authoring-surface/isready.md +14 -0
  77. package/docs/reference/authoring-surface/jr2setup.md +237 -0
  78. package/docs/reference/authoring-surface/open.md +105 -0
  79. package/docs/reference/authoring-surface/openbody.md +86 -0
  80. package/docs/reference/authoring-surface/paramsof.md +143 -0
  81. package/docs/reference/authoring-surface/pool.md +186 -0
  82. package/docs/reference/authoring-surface/push.md +130 -0
  83. package/docs/reference/authoring-surface/readyof.md +21 -0
  84. package/docs/reference/authoring-surface/secret.md +139 -0
  85. package/docs/reference/authoring-surface/sentby.md +83 -0
  86. package/docs/reference/authoring-surface/source.md +51 -0
  87. package/docs/reference/authoring-surface/text.md +62 -0
  88. package/docs/reference/authoring-surface/type-agentdeclaration.md +27 -0
  89. package/docs/reference/authoring-surface/type-agentdefinition.md +80 -0
  90. package/docs/reference/authoring-surface/type-agentlogic.md +15 -0
  91. package/docs/reference/authoring-surface/type-agentslots.md +15 -0
  92. package/docs/reference/authoring-surface/type-answeraction.md +12 -0
  93. package/docs/reference/authoring-surface/type-anyframepath.md +14 -0
  94. package/docs/reference/authoring-surface/type-anyworktreepath.md +15 -0
  95. package/docs/reference/authoring-surface/type-arg.md +19 -0
  96. package/docs/reference/authoring-surface/type-argnames.md +12 -0
  97. package/docs/reference/authoring-surface/type-bindactors.md +14 -0
  98. package/docs/reference/authoring-surface/type-binding.md +61 -0
  99. package/docs/reference/authoring-surface/type-bindingof.md +12 -0
  100. package/docs/reference/authoring-surface/type-bodyacceptsdoor.md +15 -0
  101. package/docs/reference/authoring-surface/type-bodyacceptsslots.md +17 -0
  102. package/docs/reference/authoring-surface/type-ceilings.md +41 -0
  103. package/docs/reference/authoring-surface/type-chainlink.md +19 -0
  104. package/docs/reference/authoring-surface/type-chartaction.md +46 -0
  105. package/docs/reference/authoring-surface/type-chartagent.md +28 -0
  106. package/docs/reference/authoring-surface/type-chartevent.md +20 -0
  107. package/docs/reference/authoring-surface/type-chartguard.md +20 -0
  108. package/docs/reference/authoring-surface/type-chartinvoke.md +28 -0
  109. package/docs/reference/authoring-surface/type-chartjson.md +14 -0
  110. package/docs/reference/authoring-surface/type-chartstate.md +46 -0
  111. package/docs/reference/authoring-surface/type-charttext.md +40 -0
  112. package/docs/reference/authoring-surface/type-charttransition.md +22 -0
  113. package/docs/reference/authoring-surface/type-charttransitions.md +14 -0
  114. package/docs/reference/authoring-surface/type-chartvalue.md +15 -0
  115. package/docs/reference/authoring-surface/type-childat.md +12 -0
  116. package/docs/reference/authoring-surface/type-childslots.md +12 -0
  117. package/docs/reference/authoring-surface/type-commandenv.md +15 -0
  118. package/docs/reference/authoring-surface/type-commandinput.md +20 -0
  119. package/docs/reference/authoring-surface/type-commandoptions.md +56 -0
  120. package/docs/reference/authoring-surface/type-commandoutput.md +122 -0
  121. package/docs/reference/authoring-surface/type-customize.md +111 -0
  122. package/docs/reference/authoring-surface/type-dispatchdefinition.md +39 -0
  123. package/docs/reference/authoring-surface/type-dispatchinput.md +14 -0
  124. package/docs/reference/authoring-surface/type-dispatchopenparts.md +14 -0
  125. package/docs/reference/authoring-surface/type-dispatchoutput.md +72 -0
  126. package/docs/reference/authoring-surface/type-doorlessinput.md +19 -0
  127. package/docs/reference/authoring-surface/type-duration.md +15 -0
  128. package/docs/reference/authoring-surface/type-effectbinding.md +93 -0
  129. package/docs/reference/authoring-surface/type-effectlogic.md +12 -0
  130. package/docs/reference/authoring-surface/type-envopenparts.md +14 -0
  131. package/docs/reference/authoring-surface/type-eventaudience.md +27 -0
  132. package/docs/reference/authoring-surface/type-eventdef.md +45 -0
  133. package/docs/reference/authoring-surface/type-eventfrom.md +16 -0
  134. package/docs/reference/authoring-surface/type-eventsemantics.md +26 -0
  135. package/docs/reference/authoring-surface/type-execoutcome.md +34 -0
  136. package/docs/reference/authoring-surface/type-failslot.md +14 -0
  137. package/docs/reference/authoring-surface/type-failure.md +19 -0
  138. package/docs/reference/authoring-surface/type-failureinput.md +20 -0
  139. package/docs/reference/authoring-surface/type-filebody.md +37 -0
  140. package/docs/reference/authoring-surface/type-framepath.md +14 -0
  141. package/docs/reference/authoring-surface/type-frominput.md +14 -0
  142. package/docs/reference/authoring-surface/type-hostinjectedinput.md +56 -0
  143. package/docs/reference/authoring-surface/type-httpdefinition.md +104 -0
  144. package/docs/reference/authoring-surface/type-httpfileinput.md +20 -0
  145. package/docs/reference/authoring-surface/type-httpinput.md +12 -0
  146. package/docs/reference/authoring-surface/type-httpmethod.md +14 -0
  147. package/docs/reference/authoring-surface/type-httpopenparts.md +12 -0
  148. package/docs/reference/authoring-surface/type-httpoutput.md +95 -0
  149. package/docs/reference/authoring-surface/type-httprequestparts.md +33 -0
  150. package/docs/reference/authoring-surface/type-jr2config.md +126 -0
  151. package/docs/reference/authoring-surface/type-lateroutcome.md +21 -0
  152. package/docs/reference/authoring-surface/type-lends.md +38 -0
  153. package/docs/reference/authoring-surface/type-logicat.md +12 -0
  154. package/docs/reference/authoring-surface/type-markedif.md +12 -0
  155. package/docs/reference/authoring-surface/type-mergedactors.md +12 -0
  156. package/docs/reference/authoring-surface/type-namedsecret.md +25 -0
  157. package/docs/reference/authoring-surface/type-namestart.md +77 -0
  158. package/docs/reference/authoring-surface/type-noslotworkspaceoptions.md +27 -0
  159. package/docs/reference/authoring-surface/type-notworktreekeys.md +12 -0
  160. package/docs/reference/authoring-surface/type-openbodydeclaration.md +40 -0
  161. package/docs/reference/authoring-surface/type-openbodylogic.md +14 -0
  162. package/docs/reference/authoring-surface/type-openbodyoutput.md +42 -0
  163. package/docs/reference/authoring-surface/type-openbodyslot.md +18 -0
  164. package/docs/reference/authoring-surface/type-opencommandinput.md +22 -0
  165. package/docs/reference/authoring-surface/type-openeffect.md +12 -0
  166. package/docs/reference/authoring-surface/type-openeffectslots.md +12 -0
  167. package/docs/reference/authoring-surface/type-openof.md +12 -0
  168. package/docs/reference/authoring-surface/type-opensecret.md +23 -0
  169. package/docs/reference/authoring-surface/type-opensecretmark.md +12 -0
  170. package/docs/reference/authoring-surface/type-permissivenoslotworkspaceoptions.md +26 -0
  171. package/docs/reference/authoring-surface/type-permissiveworkspaceoptions.md +27 -0
  172. package/docs/reference/authoring-surface/type-poolmachine.md +28 -0
  173. package/docs/reference/authoring-surface/type-poolnextinput.md +22 -0
  174. package/docs/reference/authoring-surface/type-poolspec.md +60 -0
  175. package/docs/reference/authoring-surface/type-pushinput.md +34 -0
  176. package/docs/reference/authoring-surface/type-pushoptions.md +79 -0
  177. package/docs/reference/authoring-surface/type-pushoutput.md +149 -0
  178. package/docs/reference/authoring-surface/type-pushwireoutput.md +12 -0
  179. package/docs/reference/authoring-surface/type-quantity.md +14 -0
  180. package/docs/reference/authoring-surface/type-reached.md +12 -0
  181. package/docs/reference/authoring-surface/type-reposlot.md +70 -0
  182. package/docs/reference/authoring-surface/type-reposlotsof.md +16 -0
  183. package/docs/reference/authoring-surface/type-sandboxoptions.md +84 -0
  184. package/docs/reference/authoring-surface/type-secret.md +25 -0
  185. package/docs/reference/authoring-surface/type-sender.md +19 -0
  186. package/docs/reference/authoring-surface/type-sentbyguards.md +12 -0
  187. package/docs/reference/authoring-surface/type-size.md +85 -0
  188. package/docs/reference/authoring-surface/type-slothandles.md +24 -0
  189. package/docs/reference/authoring-surface/type-sourcenextresult.md +17 -0
  190. package/docs/reference/authoring-surface/type-sourcespec.md +44 -0
  191. package/docs/reference/authoring-surface/type-syncstate.md +12 -0
  192. package/docs/reference/authoring-surface/type-text.md +19 -0
  193. package/docs/reference/authoring-surface/type-textpart.md +24 -0
  194. package/docs/reference/authoring-surface/type-thinkinglevel.md +39 -0
  195. package/docs/reference/authoring-surface/type-toparameterizedobject.md +14 -0
  196. package/docs/reference/authoring-surface/type-toprovidedactor.md +14 -0
  197. package/docs/reference/authoring-surface/type-unknown.md +21 -0
  198. package/docs/reference/authoring-surface/type-usercontainer.md +77 -0
  199. package/docs/reference/authoring-surface/type-withoutemptyindex.md +20 -0
  200. package/docs/reference/authoring-surface/type-workflowevent.md +46 -0
  201. package/docs/reference/authoring-surface/type-workspaceaccess.md +16 -0
  202. package/docs/reference/authoring-surface/type-workspaced.md +115 -0
  203. package/docs/reference/authoring-surface/type-workspacehandles.md +22 -0
  204. package/docs/reference/authoring-surface/type-workspacemachine.md +29 -0
  205. package/docs/reference/authoring-surface/type-workspaceof.md +13 -0
  206. package/docs/reference/authoring-surface/type-workspaceoptions.md +27 -0
  207. package/docs/reference/authoring-surface/type-worktreefound.md +16 -0
  208. package/docs/reference/authoring-surface/type-worktreeinput.md +56 -0
  209. package/docs/reference/authoring-surface/type-worktreekey.md +14 -0
  210. package/docs/reference/authoring-surface/type-worktreeoptions.md +29 -0
  211. package/docs/reference/authoring-surface/type-worktreeoutput.md +45 -0
  212. package/docs/reference/authoring-surface/type-worktreepath.md +16 -0
  213. package/docs/reference/authoring-surface/type-worktreeready.md +37 -0
  214. package/docs/reference/authoring-surface/type-worktreerefused.md +21 -0
  215. package/docs/reference/authoring-surface/workspace.md +287 -0
  216. package/docs/reference/authoring-surface/worktree.md +71 -0
  217. package/docs/reference/authoring-surface.md +217 -0
  218. package/docs/reference/cli.md +433 -0
  219. package/docs/reference/config.md +765 -0
  220. package/docs/reference/decisions/0001-operator-owned-generic-sandbox.md +114 -0
  221. package/docs/reference/decisions/0002-duplex-streaming-actor.md +117 -0
  222. package/docs/reference/decisions/0004-worktree-provisioning-model.md +130 -0
  223. package/docs/reference/decisions/0005-sandbox-pod-composition.md +240 -0
  224. package/docs/reference/decisions/0006-agent-control-surface.md +50 -0
  225. package/docs/reference/decisions/0007-durable-machine-state.md +109 -0
  226. package/docs/reference/decisions/0008-orchestrator-as-deployed-app.md +60 -0
  227. package/docs/reference/decisions/0009-cli-and-instance-interface.md +275 -0
  228. package/docs/reference/decisions/0010-bdd-acceptance-tests.md +160 -0
  229. package/docs/reference/decisions/0011-workflow-defined-events.md +141 -0
  230. package/docs/reference/decisions/0012-workspace-wrapper-machine.md +103 -0
  231. package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +175 -0
  232. package/docs/reference/decisions/0014-observation-is-open-run-state-is-not.md +95 -0
  233. package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +271 -0
  234. package/docs/reference/decisions/0016-agent-turn-mechanics-are-internal.md +126 -0
  235. package/docs/reference/decisions/0017-pool-over-a-source-port.md +71 -0
  236. package/docs/reference/decisions/0018-instance-agents-are-definitions-jr2-assembles-the-harness.md +111 -0
  237. package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +287 -0
  238. package/docs/reference/decisions/0020-private-ca-trust-is-a-named-concept.md +60 -0
  239. package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +101 -0
  240. package/docs/reference/decisions/0022-observation-is-a-level-triggered-feed.md +103 -0
  241. package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +175 -0
  242. package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +436 -0
  243. package/docs/reference/decisions/0025-cancel-ends-a-run-stop-parks-it.md +65 -0
  244. package/docs/reference/decisions/0026-a-turn-that-is-over-is-never-prompted.md +62 -0
  245. package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +245 -0
  246. package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +90 -0
  247. package/docs/reference/decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md +154 -0
  248. package/docs/reference/decisions/0030-a-snapshot-names-the-machine-it-was-written-under.md +100 -0
  249. package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +193 -0
  250. package/docs/reference/decisions/0032-the-console-unlocks-with-the-instance-token.md +61 -0
  251. package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +126 -0
  252. package/docs/reference/decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md +57 -0
  253. package/docs/reference/decisions/0035-a-runaway-turn-is-ended-rerolled-once-then-a-fault.md +92 -0
  254. package/docs/reference/decisions/0036-a-turn-compacts-its-own-context-mid-flight.md +138 -0
  255. package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +196 -0
  256. package/docs/reference/decisions/0038-jr2-up-builds-every-image-it-deploys.md +170 -0
  257. package/docs/reference/decisions/0039-image-garbage-collects-by-reachability.md +115 -0
  258. package/docs/reference/decisions/0041-a-build-the-host-already-holds-is-not-spent-again.md +64 -0
  259. package/docs/reference/decisions/0042-ready-is-not-routable-an-admission-retries-its-connection.md +142 -0
  260. package/docs/reference/decisions/0043-the-kit-is-tested-as-installed-a-local-registry-stands-in-for-npm.md +170 -0
  261. package/docs/reference/decisions/0044-kit-images-live-at-a-canonical-home-a-self-host-mirrors-it.md +85 -0
  262. package/docs/reference/decisions/0045-the-platform-joins-the-image-address-and-the-cluster-chooses-it.md +82 -0
  263. package/docs/reference/decisions/0046-a-converge-failure-carries-its-diagnosis.md +34 -0
  264. package/docs/reference/decisions/0047-the-git-ssh-key-source-is-the-users-choice.md +47 -0
  265. package/docs/reference/decisions/0048-the-orchestrator-boots-without-its-repos.md +36 -0
  266. package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +126 -0
  267. package/docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md +73 -0
  268. package/docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md +238 -0
  269. package/docs/reference/decisions/0052-a-sandbox-lands-where-an-ordinary-pod-lands-and-the-cache-follows-it.md +86 -0
  270. package/docs/reference/decisions/0053-a-fetch-inside-the-pod-asks-the-node-cache-and-the-cache-asks-the-remote.md +123 -0
  271. package/docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md +126 -0
  272. package/docs/reference/decisions/0055-a-release-is-a-pushed-tag-and-the-publish-guard-is-the-credential.md +107 -0
  273. package/docs/reference/decisions/0056-a-global-jr2-hands-off-to-the-instances-own-and-an-instance-has-one-kit-version.md +62 -0
  274. package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +152 -0
  275. package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +108 -0
  276. package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +427 -0
  277. package/docs/reference/decisions/0060-a-workspace-states-its-size-and-jr2-reserves-all-of-it.md +111 -0
  278. package/docs/reference/decisions/0061-a-memory-kill-ends-the-agents-processes-not-the-harness.md +83 -0
  279. package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +239 -0
  280. package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +82 -0
  281. package/docs/reference/decisions/0064-a-workspace-waits-for-capacity.md +79 -0
  282. package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-a-week.md +65 -0
  283. package/docs/reference/decisions/0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md +552 -0
  284. package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +458 -0
  285. package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +475 -0
  286. package/docs/reference/decisions/0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md +180 -0
  287. package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +334 -0
  288. package/docs/reference/decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md +183 -0
  289. package/docs/reference/decisions/0072-the-docs-ship-inside-the-kit-and-an-instances-agents-md-indexes-them.md +144 -0
  290. package/docs/reference/decisions/0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md +122 -0
  291. package/docs/reference/decisions/0074-an-unrouted-mechanism-event-is-refused-at-load-and-faults-the-run.md +76 -0
  292. package/docs/reference/decisions/0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md +295 -0
  293. package/docs/reference/decisions/0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md +320 -0
  294. package/docs/reference/decisions/0077-jr2-requires-landlock-on-every-sandbox-node.md +79 -0
  295. package/docs/reference/decisions.md +86 -0
  296. package/docs/reference/glossary.md +687 -0
  297. package/docs/reference/http-api.md +1386 -0
  298. package/docs/reference/machines.md +189 -0
  299. package/docs/reference/mechanism-events.md +72 -0
  300. package/docs/why.md +207 -0
  301. package/package.json +5 -3
  302. package/src/actor.ts +694 -106
  303. package/src/agent.ts +114 -38
  304. package/src/ambient.ts +3 -4
  305. package/src/assembly-checks.ts +189 -7
  306. package/src/cause.ts +234 -0
  307. package/src/chart.ts +345 -46
  308. package/src/charts.ts +43 -8
  309. package/src/config.ts +440 -178
  310. package/src/custodian.lua +17 -5
  311. package/src/custodian.ts +325 -99
  312. package/src/customize.ts +199 -49
  313. package/src/dispatch-effect.ts +945 -0
  314. package/src/door-compat.ts +217 -0
  315. package/src/durability.ts +11 -4
  316. package/src/effect-checks.ts +50 -3
  317. package/src/effect.ts +641 -128
  318. package/src/effector-wire.ts +165 -13
  319. package/src/fail.ts +222 -0
  320. package/src/harness-client.ts +587 -50
  321. package/src/harness-env.ts +33 -0
  322. package/src/held-secrets.ts +125 -16
  323. package/src/holds.ts +3 -0
  324. package/src/http-effect.ts +556 -58
  325. package/src/http.ts +480 -30
  326. package/src/index.ts +74 -82
  327. package/src/instance.ts +68 -8
  328. package/src/internal.ts +85 -0
  329. package/src/kube-client.ts +6 -3
  330. package/src/machine-doc.ts +24 -7
  331. package/src/mechanism-routes.ts +168 -0
  332. package/src/model-keys.generated.ts +53 -0
  333. package/src/model-keys.ts +42 -58
  334. package/src/names.ts +19 -6
  335. package/src/notices.ts +483 -41
  336. package/src/open-body.ts +242 -40
  337. package/src/open-effect.ts +81 -8
  338. package/src/open.ts +90 -6
  339. package/src/parts.ts +213 -30
  340. package/src/party.ts +101 -2
  341. package/src/pick.ts +497 -11
  342. package/src/pool.ts +282 -50
  343. package/src/registration.ts +133 -12
  344. package/src/repo-identity.ts +7 -3
  345. package/src/repos.ts +22 -3
  346. package/src/run-host.ts +665 -45
  347. package/src/run-output.ts +38 -0
  348. package/src/sandbox-effect.ts +457 -71
  349. package/src/sandbox-kube.ts +66 -17
  350. package/src/secret-placement.ts +59 -17
  351. package/src/server.ts +3 -0
  352. package/src/setup.ts +325 -62
  353. package/src/size.ts +121 -9
  354. package/src/snapshot-store.ts +54 -10
  355. package/src/stub-harness.ts +44 -16
  356. package/src/timers.ts +14 -1
  357. package/src/tokens.ts +107 -32
  358. package/src/vocabulary.ts +88 -14
  359. package/src/wire.ts +100 -26
  360. package/src/workspace.ts +771 -238
  361. package/src/worktree.ts +249 -0
@@ -0,0 +1,83 @@
1
+ # A memory kill ends the Agent's processes, not the Harness
2
+
3
+ The Agent runs its programs (Playwright and its Chromium, `tsc`, `cargo`) through the `bash` Working tool, so they are
4
+ children of the Harness, in the Harness container's one cgroup (ADR-0005, ADR-0037). On cgroup v2 the kubelet sets
5
+ `memory.oom.group`, so when those programs pass the container's limit (ADR-0060) the kernel kills the whole container:
6
+ the Harness dies with them, its conversation with it, and the Turn faults as "conversation lost" (lab 2026-09-28; chaos
7
+ test T11). Worse, the bash tool that pi-agent-core 0.82.1 shipped reported a command killed by a signal as success
8
+ (`exitCode: code ?? 0`), so when a program was killed the Agent saw its output stop partway and read it as a pass.
9
+
10
+ **The Agent's processes** are every process in the Harness container except the Harness and its init: the tree of the
11
+ running `bash` call, anything the Agent left running from earlier calls (a dev server, a watcher), and a human's
12
+ `kubectl exec` session.
13
+
14
+ ## Decision
15
+
16
+ Layers, each a backstop for the one before:
17
+
18
+ 1. **A process killed by a signal is an error.** jr2's bash tool reads the shell's exit: a signal death, of the command
19
+ or of the shell, reports the signal and never exit 0. A Harness conformance test holds it (ADR-0027).
20
+ 2. **`tini` is PID 1 in the Harness container**, shipped on `/opt/jr2`, so killed trees leave no zombies. Not
21
+ `shareProcessNamespace` — ADR-0005 keeps it off for good.
22
+ 3. **The `bash` Working tool starts each command with `oom_score_adj=1000`**, inherited by its children. It needs no
23
+ privilege, and on a node whose owner set the kubelet's `singleProcessOOMKill: true` the kernel then picks the Agent's
24
+ process, not the Harness.
25
+ 4. **The Harness guards its own cgroup.** Every 10–20 ms it reads anon + shmem from `memory.stat` against `memory.max`
26
+ (never `memory.current`, which counts reclaimable page cache). Near the limit it kills all of the Agent's processes,
27
+ checks usage again, clears `/dev/shm`, and the running `bash` call returns
28
+ `killed: memory limit (peak X of Y); use fewer workers or a larger Size`. The Agent hears it in the same Turn and
29
+ conversation, and can retry smaller. The guard polls from the Harness's start, not only during a call. A process that
30
+ an earlier Turn left running (a dev server, a watcher) can pass the limit while no Submission runs. For example, the
31
+ workflow waits at a human's Gate. Then no call returns the text. The Harness keeps every kill for the Orchestrator to
32
+ read, and a read removes nothing. The next Agent that works in that Workspace is told (ADR-0062).
33
+ 5. **The kernel's group kill is the last backstop.** The Orchestrator reads `lastState.terminated.reason: OOMKilled` and
34
+ the fault reason starts with the fixed prefix `memory limit`, not `Turn lost` (any other restart, ADR-0021) or
35
+ "conversation lost" (a 404). The fault reaches the Machine (ADR-0016); the next Agent in that Workspace is told
36
+ through the Briefing ([ADR-0062](0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md)), never through the
37
+ Frame (ADR-0057). The kit's Machines pass the reason into their retry Turn as the example for authors. A kernel kill
38
+ while no Submission runs faults no Turn, because the restarted Harness rebuilds its conversations. The operator
39
+ publishes the container's last end on the Sandbox all the same, so before each admission into the Workspace the
40
+ Orchestrator reads it from the Sandbox watch, and a last end `OOMKilled` that no notice told yet tells the next Agent
41
+ (ADR-0062).
42
+
43
+ The guard is best-effort and says so: a fast enough spike beats any poll. The lab won 35/35 on arm64 at 50 ms, 6/6 on
44
+ amd64 at 10 ms (0.8% of a core), and only half on amd64 at 50 ms; it fired at 90–96% of the limit. Layer 5 covers what
45
+ it misses.
46
+
47
+ **The planned successor is a child cgroup for the Agent's processes** (`memory.max` below the container's, its own
48
+ `oom.group`): exact, no race, the Harness always survives (lab: 3/3 on kind). It needs a writable cgroupfs, which today
49
+ only a node owner grants (containerd ≥ 2.1 `cgroup_writable` behind a RuntimeClass, not offered on managed clusters).
50
+ KEP-5474 adds `securityContext.cgroupOptions.mountMode: Writable` so the pod asks for it itself; it is alpha, targeted
51
+ at v1.38. When it is enabled by default, jr2 sets the field on the Harness container and the child cgroup replaces layer
52
+ 4's polling.
53
+
54
+ ## Considered options
55
+
56
+ - **Accept the group kill.** Rejected as the whole answer: every memory kill becomes a Turn fault, and the Agent never
57
+ learns why, so a retry repeats it.
58
+ - **The Harness in its own container, with an exec server in the Sandbox Image's container running the Agent's
59
+ processes.** A real cgroup edge, but it reverses "Working tools execute in the Harness container" (ADR-0037) for a
60
+ problem layers 4 and the successor solve inside it.
61
+ - **Adopt the child cgroup now, used when detected.** Rejected: a path almost no cluster runs is a path nobody tests,
62
+ and the guard must be solid on its own anyway.
63
+ - **MemoryQoS `memory.high`.** Rejected: the lab saw a stall of over 9 minutes with no kill and `kubectl exec` hung.
64
+ - **earlyoom, systemd-oomd, oomd.** Rejected: they read the host's `/proc/meminfo`, need systemd, or need PSI triggers,
65
+ which a read-only cgroupfs refuses. The guard borrows their idea.
66
+ - **Kill only the running call's process group.** Rejected: `setsid` children escape it (lab), and a dev server left
67
+ from an earlier call may hold the memory.
68
+ - **Raise Node's heap for the Agent's processes** (`NODE_OPTIONS`). Rejected in ADR-0060: it turns a readable V8 abort
69
+ into a kernel kill.
70
+
71
+ ## Consequences
72
+
73
+ - The guard's threshold comes from the Harness container's limit, so it needs ADR-0060's limits first; layer 1 does not
74
+ and is fixed at once.
75
+ - The kill set includes a human's exec session open at that moment; the group kill would end it too. On the Instance
76
+ Harness that session is the only process the guard can kill, because a Menu-only Agent has no Working tools. No notice
77
+ follows: that pod holds no Workspace (ADR-0031, ADR-0062).
78
+ - A kernel kill (layer 5) while no Submission runs is told to the next Turn through the container's last end, which the
79
+ watch keeps until the container ends again. So two kernel kills before the next admission are told as one, and a
80
+ kernel kill that a later end of another kind replaces is not told. The guard fires at 90% of the limit, before the
81
+ kernel, so this window is the guard's misses alone.
82
+ - A memory fault is named in `jr2 status` and on the feed, not `Turn lost` (a restart for any other reason) or
83
+ "conversation lost" (a 404).
@@ -0,0 +1,239 @@
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 a pick's briefing
38
+ when the restatement opens that pick. The same read at a step boundary returns the conversation's pending notices, and
39
+ the Harness appends them in the same way (below).
40
+ - **The Turn part comes last, because a model obeys what it read last.** Measured on the home-lab vLLM model: with a
41
+ prompt that argued for a pick the Turn did not allow, the Allowed picks placed ahead of the prompt were obeyed 0/10
42
+ (thinking off) and 5/10 (thinking on); placed after it, 10/10 both. Wording made no difference. The author's task
43
+ still reads first, as it would with no jr2.
44
+ - **Always on.** No author can turn it off: every fact in it passes the rule above, so removing one can only make the
45
+ Agent act worse. An `instructions` that repeats a fact is harmless.
46
+ - **A notice is heard once by each member of its audience.** A member takes it at its next admission, or at a step
47
+ boundary of an open Turn: the surface read of the open Turn returns the conversation's pending notices, and the
48
+ Harness appends them, as it appends a changed Allowed-picks list
49
+ ([ADR-0029](0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md), ADR-0002). For a guard
50
+ kill that the Agent of an open Turn was not told, a member also takes it at that Turn's nudge or wake (below). A
51
+ conversation is an Instance ID ([ADR-0057](0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md)): a fresh
52
+ Turn, a runaway reroll and a burned conversation's successor each start a new one.
53
+ - **Conversation scope** ("this conversation is new; earlier context is gone") goes to that Agent's next Turn. So does
54
+ a pick's answer that never reached its call
55
+ ([ADR-0024](0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md)): the route left the Agent's state, and
56
+ the next Turn of the conversation the picking Turn continued hears "the workflow left the state of your last turn
57
+ while your pick was outstanding", with the answer — or, when something other than the route left the state while the
58
+ pick's work ran, "your pick did not finish: the workflow left the state that asked for your last turn while it ran,
59
+ and that stopped it" (`stopped`), with an earlier link's reply when one came. No pick-answer notice says the work
60
+ ran to its end. A fresh picking Turn has no such conversation, and its answer is no notice.
61
+ - **Workspace scope** ("the Sandbox's processes were killed at its memory limit during `coder`'s Turn; `/dev/shm` was
62
+ cleared. Anything that was running in the Sandbox at that time is gone.") goes to an **audience** that the ledger
63
+ fixes when it raises the notice. The rule is: an Agent hears a kill if it was there for it.
64
+ 1. Each conversation admitted in that Workspace before the kill that can still take a Turn: the conversation of an
65
+ open Turn, which hears it at its next step boundary, or a continued conversation, which hears it at its next
66
+ Turn. A fresh Turn that already ended is not in the audience.
67
+ 2. When the kill cut a specific Turn — a kill read on that Turn's stream names it — that Agent's next Turn, fresh or
68
+ continued: the retry of the broken work.
69
+ 3. Minus each conversation whose own `bash` answer returned the kill's verdict (`told`, below). That answer told it
70
+ already.
71
+
72
+ Each member hears the notice once. The notice retires when every member has heard it. A conversation that starts
73
+ after the kill does not hear it, except the retry of (2). So a Pool of coders in one Workspace
74
+ ([ADR-0075](0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md)) all hear a kill that cut their work.
75
+ Only a kill read on a stream while its wait is live names the Turn whose Submission ran. Both forms end with the
76
+ whole Sandbox, because every process in it died, a shared Workspace's too, and in the past tense, because the Agent
77
+ may hear it after processes started again. A kill read from the pod's list names no Turn and says nothing of when or
78
+ whose: "the Sandbox's processes were killed at its memory limit (peak X of Y); /dev/shm was cleared. Anything that
79
+ was running in the Sandbox at that time is gone." The list cannot say whose Turn a kill ended: the kill may land
80
+ after a state exits, or end an attach, a sync or a command. A kernel kill read from the Sandbox's last end (below)
81
+ says the same, with the limit and no peak: "the Sandbox's processes were killed at its memory limit of Y; …". A
82
+ Menu-only Agent outside a Workspace receives conversation notices only.
83
+
84
+ - A notice never replaces a fault. The Machine still receives `agent.fault` and decides (ADR-0016); the notice makes
85
+ sure the next Agent is not working blind.
86
+
87
+ - **The Orchestrator is the only keeper of notices**: pending notices are plain data in the run's persisted state
88
+ (ADR-0007). It sees kernel kills and fresh conversations itself. A kernel kill (ADR-0061, layer 5) has two paths,
89
+ under one source, the Workspace and the time the Harness container ended. A Turn that the kill ended faults on it, and
90
+ the fault raises the notice with the Turn's Agent. A kill while no Turn runs faults nothing, so before each admission
91
+ into a Workspace the Agent actor also reads the container's last end from the Sandbox watch, which the operator
92
+ publishes: a last end `OOMKilled` raises the notice, which names no Turn. The ledger's `seen` makes the two paths one
93
+ notice. An actor whose own watch saw the restart reads the last end only when it re-admits the Turn after the cut.
94
+ When the Turn faults, the fault's notice names the Turn's Agent. A watch that has not listed yet says nothing, and the
95
+ next admission reads again: the last end stays until the container ends again. A nudge and a wake take no kernel kill;
96
+ 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
97
+ guard kill. The Harness reports a guard kill (ADR-0061) on two paths, under one id that the Harness gives the kill:
98
+ - **On a stream.** While a Submission runs, the kill is an event on that conversation's updates stream. The Agent
99
+ actor reads a stream while it waits on the Submission. It stops when the Agent's state exits, before its abort
100
+ reaches the Harness. So a stream does not always reach a reader: a kill in that window is on a stream that no one
101
+ reads. A stopped actor raises nothing from its stream.
102
+ - **In the pod's list.** The Harness keeps every kill, with `told`: the conversations whose `bash` call returned the
103
+ kill's verdict to the model. A conversation goes into it when the answer returns, not when the guard fires: a call
104
+ cut short (the container's end, a restart) told no one. A model that writes text, or waits, is not told. The Harness
105
+ keeps the list in a file beside its conversations and answers it on `GET /memory-kills`. A kill while no Submission
106
+ runs is on this path alone: for example, a dev server killed while the workflow waits at a Gate. Before each
107
+ admission into a Workspace, the Agent actor reads the list. The actor sets the read's limit of 5 seconds, and it is
108
+ the only limit. A read that never left the Orchestrator, or a draining Harness's 503, is sent again in that time. A
109
+ read that fails or times out is none, and the next admission reads again: the list keeps the kill. The Orchestrator
110
+ 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
111
+ failure again only when a read fails after a read that worked. The file is beside the Harness's conversations, which
112
+ no confined child can reach
113
+ ([ADR-0076](0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md)). The bounds stay as a
114
+ second layer: a `peak` or a `limit` that is not a size in the guard's form, or is long, is left out and logged, on
115
+ the Harness's read of the file and on the Orchestrator's read of the list and of a stream. The kill is kept, so a
116
+ real kill is never lost, and its notice says no sizes: "the Sandbox's processes were killed at its memory limit;
117
+ /dev/shm was cleared. …". Only a kill with no id or no time, or one outside the bounds below, is dropped. A stream
118
+ event from an older Harness has no id; its source is then its place on the stream, exactly as the Orchestrator
119
+ raised it before.
120
+
121
+ Both paths raise the notice with the same source, the Workspace and the kill's id. The ledger keeps every source it
122
+ raised (`seen`) in the run's persisted state. So a kill read on both paths, or on two streams, is one notice. Each
123
+ read of the list raises one notice at most: the newest kill's, unless it is raised already. The read marks the older
124
+ 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
125
+ stream are not folded: each one with different sizes gives its own notice. Two notices with the same words keep two
126
+ entries in the ledger, and an admission that takes both says them once. The run is saved when a raise changes the
127
+ ledger, and only then. A read removes nothing, so a crash between the read and the save that keeps it loses no kill. A
128
+ restart of the Orchestrator raises no kill twice. A restart of the Harness container keeps the file, which is on the
129
+ pod's emptyDir with its conversations (ADR-0031). A lost pod takes the file with it, but then the Workspace is lost
130
+ (ADR-0021). The list keeps the newest 16 kills, each with at most 32 conversations in `told`, and the Orchestrator
131
+ reads at most 256 KiB of it. The two sides hold the same bounds: an `id` and an `at` of at most 64, an `agent` and an
132
+ `instanceId` of at most 128, and an `at` that parses as a time. A kill outside them is dropped, and a `told` entry
133
+ outside them is left out. The Harness counts the bytes of the JSON text, and the Orchestrator counts characters, which
134
+ are never more, so the Harness keeps no kill that the Orchestrator drops. The Harness reads at most the last 256 KiB
135
+ of the file. With these bounds the largest list is 149,259 bytes. When more come between two admissions, the oldest is
136
+ lost, and a newer kill tells the same thing. When more than 16 come in one Turn, the Turn's mark leaves the list too,
137
+ and every kill in the list then counts as after the mark; a nudge or a wake of that Turn may then carry a kill from
138
+ before it. The Instance Harness keeps no kill, because it holds no Workspace.
139
+
140
+ A notice counts as delivered when its admission is ledgered, so a restart before that point delivers it again and a
141
+ restart after it does not. A notice at a step boundary has no admission. The surface read of the open Turn reserves it
142
+ for that member, and it counts as delivered, and heard, when the Harness acknowledges the append. When the Turn ends
143
+ with no acknowledgment, the reservation is released, and the notice is pending again for that member. A reservation
144
+ lives in the Orchestrator's memory only, so a restart forgets it: the notice is pending again, and the next read or
145
+ admission carries it. So a conversation can hear a notice twice after a crash, as for an admission. Delivered is not
146
+ yet heard: if the Orchestrator ends the Turn before the Harness reads its surface, the Harness never prompts it
147
+ (ADR-0026), so the Orchestrator raises that admission's notices again. This holds for a nudge and a wake too: each one
148
+ must have its surface read again before its notices count as heard. An admission that fails leaves the earlier
149
+ admission's notices heard. A nudge and a wake re-prompt the Turn within itself. Of the notices, they take only the
150
+ guard kills of the list that come after the newest kill whose `told` names their conversation. That `bash` answer told
151
+ the Agent that all its processes died, so it covers every kill up to it. They also take no kill from before the Turn
152
+ started. Such a kill would tell the Agent that processes it started in this Turn died. The Turn's first read of the
153
+ list sets its mark: the id of the newest kill it saw, or "none". The mark is ledgered with every admission of the
154
+ Turn, and a restore reads it back, so a nudge or a wake takes only kills after it. Only when the first read failed is
155
+ the Turn's start the mark, ledgered the same way, with one second allowed for a pod clock that runs behind. A read
156
+ that failed is "could not tell", never an empty list, so it sets no mark. The kills they do not take stay pending for
157
+ the next Turn. The re-admission after a Harness restart cut the Turn continues the same Turn, so it takes every notice
158
+ in scope except the guard kills that this rule leaves out. When its read could not tell, it takes no guard kill at
159
+ all, as a nudge then takes none: it cannot know which kills its own `bash` reported. The notices that the Turn's
160
+ admissions carried since the Harness last read the Turn's surface are pending again first, each with the source it was
161
+ raised with, and this re-admission carries them whatever else its scope leaves out. A notice with no source merges
162
+ only into a pending one with no source, so an equal notice of another kill never takes its place. When such an unread
163
+ admission carried the Turn's task, the task follows the re-admit text. When the rebuilt Harness lost the conversation,
164
+ nothing of the Turn is left: every notice that the Turn's admissions carried is pending again whatever the surface
165
+ reads said, the re-admission takes every notice in scope, as a Turn's admission does, and the Turn's task follows the
166
+ re-admit text. It says once that the conversation is new. A restore drops the prompt from a Turn's input, so the task
167
+ rides the ledgered admission too; a lost conversation with no task to send faults the Turn instead. Whether the
168
+ Harness still holds the conversation is asked again for up to 30 seconds while the new container does not listen yet
169
+ or drains, and only then is the answer taken as "held". The continue path asks the same way, so a continued Turn after
170
+ a restart can wait up to 30 seconds before its admission. A Harness in CrashLoopBackOff can take longer than that: the
171
+ answer is then "held", and it can be wrong. So every admission to a conversation that the Harness was taken to keep
172
+ says so with `kept: true`: the re-admission after a cut, a continued Turn, and a nudge or a wake. A Harness that keeps
173
+ no such conversation refuses it with 404 and `kept: false` in the body, and seats none. The Orchestrator takes only
174
+ that 404 as the refusal; another 404 is a failed admission. Without the refusal the Harness would seat a new
175
+ conversation, and a model on an empty context would read "continue your task" with no task. On the refusal the Turn
176
+ takes the path of a lost conversation. The re-admission is admitted as a Turn, with the task after the re-admit text
177
+ and the notice that the conversation is new, or faults when it has no task to send. A continued Turn is told that its
178
+ conversation is new, and is admitted again. A nudge or a wake is admitted again as a Turn: its prompt is the task
179
+ alone, because nothing was cut, with the notice that the conversation is new and every notice in scope. Every notice
180
+ that the Turn's admissions carried is pending again first, as for a lost conversation after a cut. With no task to
181
+ send, the Turn faults. The Instance Harness's restart is on no watch, so this refusal is the only way a nudge or a
182
+ wake learns of it. What the Turn's admissions carried, and whether the Harness read their surface, is kept in the
183
+ Orchestrator's memory alone: after a restore of the Orchestrator before a surface read, nothing is raised again. A
184
+ fresh Turn's "this conversation is new", which rides an admission alone, is carried again by the next re-admission
185
+ when the Harness has not read the one before.
186
+
187
+ A conversation whose own `bash` call answered a kill is not in that kill's audience, so it does not hear the kill
188
+ again as a notice, at its next Turn or at a step boundary.
189
+
190
+ - **The Harness writes every word.** The admit body carries typed notices
191
+ (`notices: [{ kind: "memory-limit", scope, agent?, limit?, peak? } | { kind: "conversation-new", reason } | { kind: "pick-answer", pick, cut, answer?, stopped? } …]`,
192
+ where `peak` comes only with `limit`), and the Harness renders both parts. An outstanding pick's answer is data too —
193
+ the last reply since the pick, a party's event with its payload or a child's output (ADR-0024) — and its words ("Its
194
+ answer: `approve` from Gate `review`, with {…}", "No reply came back for it.") are the Harness's, in the receipt and
195
+ in the notice alike; "it moved the workflow, and nothing ran" is a guard straight back's receipt alone. The standing
196
+ part needs no wire: the Harness already holds its limits (cgroup and Downward API env), the definition's `workspace`
197
+ access, and the Menu. The wording lives in one place, and Harness conformance checks it (ADR-0027).
198
+
199
+ ## Considered options
200
+
201
+ - **Put the facts in the Frame.** Rejected: the Frame is the Machine state's (ADR-0057), and a kit that writes into it
202
+ makes the author's prompt say things the author did not write.
203
+ - **Leave it to authors' `instructions`.** Rejected: a definition is written before any run, so it cannot know a Size a
204
+ composer retuned, a working directory, or what happened to the last Turn — and packaged Agents would each word it
205
+ differently or not at all.
206
+ - **An author switch** (`agent({ briefing: false })`, or notices only). Rejected: under the rule, each fact is one a
207
+ correct Agent needs; the switch only lets the ADR-0057 failure back in.
208
+ - **Everything in the system prompt.** Rejected: per-Turn content there breaks the prompt cache on every Turn.
209
+ - **Finished text from the Orchestrator.** Rejected: wording split across two processes, and untestable where the turn
210
+ loop is tested.
211
+ - **Report a guard kill on the stream alone.** Rejected: a kill while no Submission runs, or after the wait stopped,
212
+ reaches no reader. A dev server killed while the workflow waits at a Gate is then never told to the next Turn, which
213
+ works blind.
214
+ - **Put that kill on every conversation's stream.** Rejected: the next Admission reads its conversation's stream from
215
+ its own offset, which is after the event.
216
+ - **Keep in the list only the kills that no stream carried.** Rejected: a kill on a stream whose wait stopped is then in
217
+ no place that is read. And two streams that carry one kill would give two notices.
218
+ - **The Harness pushes the kill to the Orchestrator.** Rejected: the Harness container is the Agent's, and ADR-0013
219
+ closes a route from it to the Orchestrator. A push also needs a retry and an acknowledgement, and an idempotent read
220
+ does not.
221
+ - **Read the kill on the Sandbox watch.** Rejected: the operator publishes what the kubelet says about the container,
222
+ and a guard kill does not end the container. The Harness has no Kubernetes credentials to publish it.
223
+ - **The Harness words the kill into the next Turn itself.** Rejected: a second keeper of notices, outside the run's
224
+ persisted state, which cannot know when an admission is ledgered or heard.
225
+
226
+ ## Consequences
227
+
228
+ - ADR-0060's CPU fact and ADR-0061's memory-limit notice are the Briefing's first users.
229
+ - Each admission into a Workspace costs one more request to its Harness, `GET /memory-kills`. The first read from a
230
+ Harness that fails is said in one line of the Orchestrator's log.
231
+ - The no-signal nudge stays a re-prompt within a Turn (ADR-0016); it is not a notice.
232
+ - The Briefing is how a Turn learns its Allowed picks: the tools block is the whole Menu, fixed for a conversation so it
233
+ does not break the cache
234
+ ([ADR-0029](0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md)).
235
+ - A Workspace notice reaches each conversation that was there for the kill, and the retry of the Turn that it cut; an
236
+ open Turn hears it at its next step boundary, not only at its next admission (ADR-0002,
237
+ [ADR-0029](0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md)). So a memory kill reaches
238
+ every coder of a Pool that shares one Workspace (ADR-0075).
239
+ - GLOSSARY.md gains **Briefing**.
@@ -0,0 +1,82 @@
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 — on `POST /attach { slots }`, authenticated like every Harness call (ADR-0058). No stream per
56
+ Workspace crosses the API server, and **`pods/exec` leaves the Orchestrator's RBAC**. It also binds work late: a pod
57
+ already running receives its Repos, the shape standby Sandboxes and Agent Substrate need.
58
+
59
+ ## Considered options
60
+
61
+ - **`@kubernetes/client-node`.** Informers built in, but a large dependency tree and generated models for every kind jr2
62
+ never touches, around kubeconfig handling in-cluster code does not need. A contained swap if jr2 ever needs exec or
63
+ many more kinds: every call stays behind the Sandbox and Repo ports.
64
+ - **`node:https` with its own agent.** Trust scoped to this client alone, but transport code built-in `fetch` makes
65
+ unnecessary; the Orchestrator sets `NODE_EXTRA_CA_CERTS` for nothing else.
66
+ - **Keep `kubectl`, add `kubectl get -w`.** Fixes detection, not the process per write, and adds a long-lived child per
67
+ watch.
68
+ - **Watch Pods as well.** Two watches, and the Orchestrator interpreting pod internals the operator already reads.
69
+ - **A jr2 WebSocket API.** Nothing to add: a Kubernetes watch is already a pushed stream of events.
70
+ - **Keep the attach as exec through the new client.** The exec protocol is the hardest part of any Kubernetes client, it
71
+ keeps one stream per Workspace through the API server, and it keeps `pods/exec`.
72
+ - **Attach from an init container.** Declarative, but it fixes the Repos at pod creation, which rules out late binding,
73
+ and it must wait on a cache that exists only after scheduling.
74
+
75
+ ## Consequences
76
+
77
+ - ADR-0021 is rewritten in place: the watch answers, the Lease only asserts. ADR-0012, ADR-0004 and ADR-0053 now read
78
+ the watch and the Harness attach. ADR-0001: the operator publishes pod facts on the Sandbox's status, and writes
79
+ status only on change — every write is now a watch event at the Orchestrator.
80
+ - The Harness gains a route that is not about a conversation. It is jr2's own work in the container ADR-0005 gives it.
81
+ - The CLI keeps `kubectl` on the user's machine (`jr2 up`, `jr2 ssh`, ADR-0009); only the Orchestrator stops using it.
82
+ - 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.