@jr2/orchestrator 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (350) hide show
  1. package/console/components/app.ts +55 -15
  2. package/console/components/bottom-drawer.ts +259 -0
  3. package/console/components/drawer.ts +14 -6
  4. package/console/components/fleet.ts +36 -13
  5. package/console/components/machine-pane.ts +61 -5
  6. package/console/components/nav.ts +54 -8
  7. package/console/conversation.ts +161 -0
  8. package/console/login.ts +123 -0
  9. package/console/main.ts +600 -60
  10. package/console/store.ts +611 -64
  11. package/console/style.css +231 -0
  12. package/docs/agents-block.md +2 -2
  13. package/docs/concepts/agents-and-turns.md +142 -29
  14. package/docs/concepts/architecture.md +60 -34
  15. package/docs/concepts/callers-and-hooks.md +205 -0
  16. package/docs/concepts/charts.md +13 -8
  17. package/docs/concepts/composition.md +10 -1
  18. package/docs/concepts/converge.md +7 -2
  19. package/docs/concepts/durable-runs.md +43 -16
  20. package/docs/concepts/effects.md +133 -50
  21. package/docs/concepts/gates.md +95 -25
  22. package/docs/concepts/machines-and-workflows.md +14 -4
  23. package/docs/concepts/pools-and-sources.md +193 -17
  24. package/docs/concepts/secrets-and-trust.md +186 -62
  25. package/docs/concepts/the-menu.md +14 -0
  26. package/docs/concepts/workspaces.md +135 -56
  27. package/docs/get-started/a-model-decides.md +14 -13
  28. package/docs/get-started/an-agent-in-a-sandbox.md +46 -35
  29. package/docs/get-started/connect-a-model.md +1 -1
  30. package/docs/get-started/create-an-instance.md +17 -14
  31. package/docs/get-started/have-your-assistant-write-a-machine.md +111 -40
  32. package/docs/get-started/install.md +4 -0
  33. package/docs/guides/add-tools-to-the-sandbox.md +72 -15
  34. package/docs/guides/answer-a-gate-from-a-forge.md +304 -0
  35. package/docs/guides/answer-a-gate.md +74 -37
  36. package/docs/guides/bind-open-parts.md +29 -12
  37. package/docs/guides/call-an-mcp-tool-from-a-machine.md +572 -0
  38. package/docs/guides/cancel-a-run.md +26 -16
  39. package/docs/guides/cut-a-worktree.md +560 -0
  40. package/docs/guides/fix-a-failed-converge.md +19 -7
  41. package/docs/guides/give-a-caller-a-scope.md +203 -0
  42. package/docs/guides/give-an-agent-mcp-tools.md +422 -0
  43. package/docs/guides/iterate-on-a-machine-on-kind.md +10 -9
  44. package/docs/guides/let-an-agent-write-a-chart.md +14 -7
  45. package/docs/guides/look-inside-a-sandbox.md +115 -42
  46. package/docs/guides/move-from-kind-to-a-shared-cluster.md +16 -0
  47. package/docs/guides/open-the-console.md +63 -14
  48. package/docs/guides/push-and-sign-a-branch.md +67 -21
  49. package/docs/guides/read-a-conversation.md +225 -0
  50. package/docs/guides/run-a-chart-by-name.md +17 -16
  51. package/docs/guides/run-a-command-or-call-a-url.md +84 -41
  52. package/docs/guides/run-one-worker-per-item.md +34 -27
  53. package/docs/guides/set-an-agents-access-and-dials.md +69 -22
  54. package/docs/guides/size-a-workspace.md +19 -6
  55. package/docs/guides/start-a-run-for-each-alert.md +290 -0
  56. package/docs/guides/start-a-run-from-a-run.md +18 -16
  57. package/docs/guides/start-a-run.md +24 -18
  58. package/docs/guides/steer-an-agent.md +332 -0
  59. package/docs/guides/use-a-hosted-provider.md +7 -1
  60. package/docs/guides/watch-a-run.md +146 -117
  61. package/docs/guides/write-a-hook-kind.md +238 -0
  62. package/docs/guides/write-a-tool-as-an-mcp-server.md +351 -0
  63. package/docs/guides/write-a-workflow.md +18 -8
  64. package/docs/llms.txt +110 -23
  65. package/docs/reference/authoring-surface/agent.md +35 -12
  66. package/docs/reference/authoring-surface/answer.md +20 -4
  67. package/docs/reference/authoring-surface/arg.md +28 -6
  68. package/docs/reference/authoring-surface/basic.md +1 -1
  69. package/docs/reference/authoring-surface/bearer.md +1 -1
  70. package/docs/reference/authoring-surface/branchname.md +15 -0
  71. package/docs/reference/authoring-surface/carry.md +23 -0
  72. package/docs/reference/authoring-surface/chartschema.md +1 -1
  73. package/docs/reference/authoring-surface/chartschemafor.md +1 -1
  74. package/docs/reference/authoring-surface/command.md +37 -22
  75. package/docs/reference/authoring-surface/customize.md +4 -1
  76. package/docs/reference/authoring-surface/defineconfig.md +1 -1
  77. package/docs/reference/authoring-surface/defineevent.md +1 -1
  78. package/docs/reference/authoring-surface/dispatch.md +5 -4
  79. package/docs/reference/authoring-surface/fail.md +18 -5
  80. package/docs/reference/authoring-surface/file.md +37 -17
  81. package/docs/reference/authoring-surface/frompick.md +1 -1
  82. package/docs/reference/authoring-surface/gate.md +118 -0
  83. package/docs/reference/authoring-surface/group-agents.md +15 -0
  84. package/docs/reference/authoring-surface/group-charts.md +16 -0
  85. package/docs/reference/authoring-surface/group-composition.md +25 -0
  86. package/docs/reference/authoring-surface/group-config.md +17 -0
  87. package/docs/reference/authoring-surface/group-effects.md +48 -0
  88. package/docs/reference/authoring-surface/group-failure.md +13 -0
  89. package/docs/reference/authoring-surface/group-gates.md +16 -0
  90. package/docs/reference/authoring-surface/group-machine-and-events.md +15 -0
  91. package/docs/reference/authoring-surface/group-steering.md +12 -0
  92. package/docs/reference/authoring-surface/group-types-these-signatures-name.md +137 -0
  93. package/docs/reference/authoring-surface/group-workspaces-and-repos.md +15 -0
  94. package/docs/reference/authoring-surface/http.md +109 -9
  95. package/docs/reference/authoring-surface/isfailure.md +1 -1
  96. package/docs/reference/authoring-surface/isready.md +14 -0
  97. package/docs/reference/authoring-surface/jr2setup.md +14 -4
  98. package/docs/reference/authoring-surface/mcp.md +107 -0
  99. package/docs/reference/authoring-surface/open.md +41 -8
  100. package/docs/reference/authoring-surface/openbody.md +1 -1
  101. package/docs/reference/authoring-surface/paramsof.md +23 -5
  102. package/docs/reference/authoring-surface/pool.md +79 -11
  103. package/docs/reference/authoring-surface/push.md +31 -13
  104. package/docs/reference/authoring-surface/readyof.md +21 -0
  105. package/docs/reference/authoring-surface/reread.md +19 -0
  106. package/docs/reference/authoring-surface/secret.md +26 -5
  107. package/docs/reference/authoring-surface/sentby.md +1 -1
  108. package/docs/reference/authoring-surface/skip.md +22 -0
  109. package/docs/reference/authoring-surface/source.md +59 -4
  110. package/docs/reference/authoring-surface/steer.md +93 -0
  111. package/docs/reference/authoring-surface/text.md +1 -1
  112. package/docs/reference/authoring-surface/type-agentdefinition.md +16 -3
  113. package/docs/reference/authoring-surface/type-agentmcpserver.md +54 -0
  114. package/docs/reference/authoring-surface/type-agentslots.md +4 -1
  115. package/docs/reference/authoring-surface/type-anyframepath.md +14 -0
  116. package/docs/reference/authoring-surface/type-anyworktreepath.md +15 -0
  117. package/docs/reference/authoring-surface/type-bindactors.md +14 -0
  118. package/docs/reference/authoring-surface/type-binding.md +6 -7
  119. package/docs/reference/authoring-surface/type-caller.md +40 -0
  120. package/docs/reference/authoring-surface/type-callerrecord.md +32 -0
  121. package/docs/reference/authoring-surface/type-carry.md +19 -0
  122. package/docs/reference/authoring-surface/type-chainlink.md +3 -3
  123. package/docs/reference/authoring-surface/type-commandinput.md +4 -3
  124. package/docs/reference/authoring-surface/type-commandoptions.md +0 -22
  125. package/docs/reference/authoring-surface/type-commandoutput.md +23 -5
  126. package/docs/reference/authoring-surface/type-customize.md +20 -3
  127. package/docs/reference/authoring-surface/type-delivered.md +17 -0
  128. package/docs/reference/authoring-surface/type-deliveredevent.md +18 -0
  129. package/docs/reference/authoring-surface/type-dispatchdefinition.md +4 -3
  130. package/docs/reference/authoring-surface/type-dispatchoutput.md +9 -2
  131. package/docs/reference/authoring-surface/type-doorlessinput.md +19 -0
  132. package/docs/reference/authoring-surface/type-effectbinding.md +12 -2
  133. package/docs/reference/authoring-surface/type-filebody.md +4 -8
  134. package/docs/reference/authoring-surface/type-framepath.md +14 -0
  135. package/docs/reference/authoring-surface/type-gatebinding.md +15 -0
  136. package/docs/reference/authoring-surface/type-gatehooks.md +12 -0
  137. package/docs/reference/authoring-surface/type-gatelook.md +37 -0
  138. package/docs/reference/authoring-surface/type-gateoutcome.md +32 -0
  139. package/docs/reference/authoring-surface/type-gatereply.md +19 -0
  140. package/docs/reference/authoring-surface/type-gateslot.md +21 -0
  141. package/docs/reference/authoring-surface/type-gateslotinput.md +13 -0
  142. package/docs/reference/authoring-surface/type-gateslotparts.md +18 -0
  143. package/docs/reference/authoring-surface/type-gateslots.md +12 -0
  144. package/docs/reference/authoring-surface/type-gatetypesat.md +13 -0
  145. package/docs/reference/authoring-surface/type-hostinjectedinput.md +3 -5
  146. package/docs/reference/authoring-surface/type-httpdefinition.md +6 -3
  147. package/docs/reference/authoring-surface/type-httpfileinput.md +20 -0
  148. package/docs/reference/authoring-surface/type-httpoutput.md +8 -2
  149. package/docs/reference/authoring-surface/type-jr2config.md +37 -2
  150. package/docs/reference/authoring-surface/type-lateroutcome.md +21 -0
  151. package/docs/reference/authoring-surface/type-lends.md +5 -1
  152. package/docs/reference/authoring-surface/type-lookoutput.md +14 -0
  153. package/docs/reference/authoring-surface/type-mcpcalloutput.md +78 -0
  154. package/docs/reference/authoring-surface/type-mcpnotreturned.md +42 -0
  155. package/docs/reference/authoring-surface/type-mcpoptions.md +47 -0
  156. package/docs/reference/authoring-surface/type-mcpoutput.md +50 -0
  157. package/docs/reference/authoring-surface/type-mcpreadoutput.md +53 -0
  158. package/docs/reference/authoring-surface/type-mcpremoteserver.md +50 -0
  159. package/docs/reference/authoring-surface/type-mcpserver.md +41 -0
  160. package/docs/reference/authoring-surface/type-mcpstdioserver.md +42 -0
  161. package/docs/reference/authoring-surface/type-namedsecret.md +4 -4
  162. package/docs/reference/authoring-surface/type-namestart.md +77 -0
  163. package/docs/reference/authoring-surface/type-noslotworkspaceoptions.md +1 -6
  164. package/docs/reference/authoring-surface/type-notworktreekeys.md +12 -0
  165. package/docs/reference/authoring-surface/type-openbodydeclaration.md +1 -1
  166. package/docs/reference/authoring-surface/type-openbodyoutput.md +1 -1
  167. package/docs/reference/authoring-surface/type-opencommandinput.md +7 -2
  168. package/docs/reference/authoring-surface/type-permissivenoslotworkspaceoptions.md +1 -6
  169. package/docs/reference/authoring-surface/type-permissiveworkspaceoptions.md +5 -10
  170. package/docs/reference/authoring-surface/type-poolnextinput.md +22 -0
  171. package/docs/reference/authoring-surface/type-poolspec.md +11 -6
  172. package/docs/reference/authoring-surface/type-pushinput.md +22 -3
  173. package/docs/reference/authoring-surface/type-pushoutput.md +33 -8
  174. package/docs/reference/authoring-surface/type-pushsourcespec.md +38 -0
  175. package/docs/reference/authoring-surface/type-readsourcespec.md +47 -0
  176. package/docs/reference/authoring-surface/type-reposlot.md +6 -6
  177. package/docs/reference/authoring-surface/type-reread.md +18 -0
  178. package/docs/reference/authoring-surface/type-runsconfig.md +41 -0
  179. package/docs/reference/authoring-surface/type-sandboxoptions.md +11 -4
  180. package/docs/reference/authoring-surface/type-sender.md +26 -1
  181. package/docs/reference/authoring-surface/type-sendercallerfield.md +14 -0
  182. package/docs/reference/authoring-surface/type-size.md +1 -1
  183. package/docs/reference/authoring-surface/type-skip.md +19 -0
  184. package/docs/reference/authoring-surface/type-slothandles.md +5 -13
  185. package/docs/reference/authoring-surface/type-sourcehooks.md +23 -0
  186. package/docs/reference/authoring-surface/type-sourcein.md +25 -0
  187. package/docs/reference/authoring-surface/type-sourcereply.md +18 -0
  188. package/docs/reference/authoring-surface/type-sourcespec.md +26 -17
  189. package/docs/reference/authoring-surface/type-sourceverdict.md +49 -0
  190. package/docs/reference/authoring-surface/type-standingpoolmachine.md +12 -0
  191. package/docs/reference/authoring-surface/type-standingpoolspec.md +51 -0
  192. package/docs/reference/authoring-surface/type-steerargs.md +20 -0
  193. package/docs/reference/authoring-surface/type-thinkinglevel.md +1 -1
  194. package/docs/reference/authoring-surface/type-usercontainer.md +1 -1
  195. package/docs/reference/authoring-surface/type-workspaced.md +30 -9
  196. package/docs/reference/authoring-surface/type-workspacehandles.md +5 -6
  197. package/docs/reference/authoring-surface/type-workspaceoptions.md +1 -13
  198. package/docs/reference/authoring-surface/type-worktreefound.md +16 -0
  199. package/docs/reference/authoring-surface/type-worktreeinput.md +56 -0
  200. package/docs/reference/authoring-surface/type-worktreekey.md +14 -0
  201. package/docs/reference/authoring-surface/type-worktreeoptions.md +29 -0
  202. package/docs/reference/authoring-surface/type-worktreeoutput.md +45 -0
  203. package/docs/reference/authoring-surface/type-worktreepath.md +16 -0
  204. package/docs/reference/authoring-surface/type-worktreeready.md +37 -0
  205. package/docs/reference/authoring-surface/type-worktreerefused.md +21 -0
  206. package/docs/reference/authoring-surface/workspace.md +66 -38
  207. package/docs/reference/authoring-surface/worktree.md +71 -0
  208. package/docs/reference/authoring-surface.md +88 -147
  209. package/docs/reference/cli.md +413 -74
  210. package/docs/reference/config.md +248 -6
  211. package/docs/reference/decisions/0001-operator-owned-generic-sandbox.md +10 -9
  212. package/docs/reference/decisions/0002-duplex-streaming-actor.md +81 -14
  213. package/docs/reference/decisions/0004-worktree-provisioning-model.md +27 -21
  214. package/docs/reference/decisions/0005-sandbox-pod-composition.md +39 -25
  215. package/docs/reference/decisions/0006-agent-control-surface.md +6 -3
  216. package/docs/reference/decisions/0007-durable-machine-state.md +1 -1
  217. package/docs/reference/decisions/0009-cli-and-instance-interface.md +150 -49
  218. package/docs/reference/decisions/0010-bdd-acceptance-tests.md +5 -3
  219. package/docs/reference/decisions/0011-workflow-defined-events.md +256 -12
  220. package/docs/reference/decisions/0012-workspace-wrapper-machine.md +39 -32
  221. package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +54 -30
  222. package/docs/reference/decisions/0014-observation-is-open-run-state-is-not.md +53 -27
  223. package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +26 -16
  224. package/docs/reference/decisions/0016-agent-turn-mechanics-are-internal.md +9 -7
  225. package/docs/reference/decisions/0017-pool-over-a-source-port.md +226 -10
  226. package/docs/reference/decisions/0018-instance-agents-are-definitions-jr2-assembles-the-harness.md +15 -7
  227. package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +53 -27
  228. package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +16 -2
  229. package/docs/reference/decisions/0022-observation-is-a-level-triggered-feed.md +4 -4
  230. package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +124 -43
  231. package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +36 -38
  232. package/docs/reference/decisions/0025-cancel-ends-a-run-stop-parks-it.md +10 -5
  233. package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +16 -7
  234. package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +36 -19
  235. package/docs/reference/decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md +18 -10
  236. package/docs/reference/decisions/0030-a-snapshot-names-the-machine-it-was-written-under.md +3 -2
  237. package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +23 -15
  238. package/docs/reference/decisions/0032-the-console-unlocks-with-a-callers-credential.md +101 -0
  239. package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +22 -22
  240. package/docs/reference/decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md +6 -6
  241. package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +13 -12
  242. package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +23 -23
  243. package/docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md +6 -1
  244. package/docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md +57 -46
  245. package/docs/reference/decisions/0052-a-sandbox-lands-where-an-ordinary-pod-lands-and-the-cache-follows-it.md +5 -0
  246. package/docs/reference/decisions/0053-a-fetch-inside-the-pod-asks-the-node-cache-and-the-cache-asks-the-remote.md +20 -8
  247. package/docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md +55 -40
  248. package/docs/reference/decisions/0055-a-release-is-a-pushed-tag-and-the-publish-guard-is-the-credential.md +14 -13
  249. package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +31 -24
  250. package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +18 -10
  251. package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +137 -86
  252. package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +165 -91
  253. package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +7 -7
  254. package/docs/reference/decisions/0064-a-workspace-waits-for-capacity.md +2 -2
  255. package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-kept-for-a-set-time.md +109 -0
  256. package/docs/reference/decisions/0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md +53 -23
  257. package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +221 -153
  258. package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +161 -96
  259. package/docs/reference/decisions/0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md +32 -9
  260. package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +48 -33
  261. package/docs/reference/decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md +56 -30
  262. package/docs/reference/decisions/0072-the-docs-ship-inside-the-kit-and-an-instances-agents-md-indexes-them.md +35 -25
  263. package/docs/reference/decisions/0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md +1 -1
  264. package/docs/reference/decisions/0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md +296 -0
  265. package/docs/reference/decisions/0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md +347 -0
  266. package/docs/reference/decisions/0077-jr2-requires-landlock-on-every-sandbox-node.md +79 -0
  267. package/docs/reference/decisions/0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md +285 -0
  268. package/docs/reference/decisions/0079-an-agent-holds-mcp-tools-a-machine-calls-them-as-an-effect-and-the-machine-is-the-approval.md +198 -0
  269. package/docs/reference/decisions.md +9 -4
  270. package/docs/reference/glossary.md +138 -45
  271. package/docs/reference/hooks.md +1077 -0
  272. package/docs/reference/http-api.md +988 -123
  273. package/docs/reference/machines.md +0 -1
  274. package/docs/why.md +7 -5
  275. package/package.json +10 -3
  276. package/src/actor.ts +194 -42
  277. package/src/agent.ts +56 -6
  278. package/src/ambient.ts +3 -5
  279. package/src/assembly-checks.ts +530 -4
  280. package/src/callers.ts +245 -0
  281. package/src/chart.ts +238 -26
  282. package/src/charts.ts +9 -2
  283. package/src/config.ts +566 -7
  284. package/src/conversations.ts +453 -0
  285. package/src/custodian.lua +17 -5
  286. package/src/custodian.ts +257 -77
  287. package/src/customize.ts +154 -7
  288. package/src/dispatch-effect.ts +142 -26
  289. package/src/durability.ts +2 -0
  290. package/src/duration.ts +20 -0
  291. package/src/effect-checks.ts +52 -4
  292. package/src/effect.ts +401 -132
  293. package/src/effector-wire.ts +278 -30
  294. package/src/fail.ts +17 -4
  295. package/src/gate-brand.ts +16 -0
  296. package/src/gate.ts +634 -48
  297. package/src/harness-client.ts +84 -35
  298. package/src/harness-env.ts +33 -0
  299. package/src/held-secrets.ts +197 -39
  300. package/src/holds.ts +5 -2
  301. package/src/hook-kinds/discord.ts +92 -0
  302. package/src/hook-kinds/door.ts +107 -0
  303. package/src/hook-kinds/github.ts +89 -0
  304. package/src/hook-kinds/gitlab.ts +72 -0
  305. package/src/hook-kinds/hook.ts +7 -0
  306. package/src/hook-kinds/index.ts +465 -0
  307. package/src/hook-kinds/slack.ts +151 -0
  308. package/src/hook-kinds/webhook.ts +113 -0
  309. package/src/hooks.ts +506 -0
  310. package/src/http-effect.ts +164 -70
  311. package/src/http.ts +933 -99
  312. package/src/index.ts +30 -5
  313. package/src/instance.ts +196 -26
  314. package/src/internal.ts +23 -3
  315. package/src/kube-client.ts +6 -3
  316. package/src/machine-doc.ts +29 -5
  317. package/src/mcp-effect.ts +424 -0
  318. package/src/model-keys.ts +24 -0
  319. package/src/mount-digest.ts +41 -7
  320. package/src/names.ts +82 -7
  321. package/src/notices.ts +576 -59
  322. package/src/open-body.ts +110 -30
  323. package/src/open-effect.ts +17 -4
  324. package/src/open.ts +40 -7
  325. package/src/parts.ts +125 -35
  326. package/src/party.ts +35 -1
  327. package/src/pick.ts +43 -9
  328. package/src/pool.ts +1172 -172
  329. package/src/registration.ts +249 -17
  330. package/src/repo-identity.ts +7 -3
  331. package/src/repos.ts +20 -2
  332. package/src/run-host.ts +1614 -190
  333. package/src/run-list.ts +191 -0
  334. package/src/sandbox-effect.ts +217 -66
  335. package/src/sandbox-kube.ts +116 -21
  336. package/src/secret-placement.ts +248 -35
  337. package/src/server.ts +53 -4
  338. package/src/setup.ts +158 -38
  339. package/src/snapshot-store.ts +565 -48
  340. package/src/steer.ts +111 -0
  341. package/src/stub-harness.ts +6 -2
  342. package/src/tokens.ts +362 -46
  343. package/src/vocabulary.ts +23 -4
  344. package/src/wire.ts +27 -10
  345. package/src/workspace.ts +314 -259
  346. package/src/worktree.ts +249 -0
  347. package/docs/reference/authoring-surface/type-noslotspec.md +0 -26
  348. package/docs/reference/authoring-surface/type-workspacespec.md +0 -37
  349. package/docs/reference/decisions/0032-the-console-unlocks-with-the-instance-token.md +0 -61
  350. package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-a-week.md +0 -65
package/src/notices.ts CHANGED
@@ -1,66 +1,267 @@
1
1
  // The Orchestrator's notices (ADR-0062): what happened that the next Turn cannot find out for
2
2
  // itself — a memory kill in its Workspace, a conversation that is new — kept as plain data in the
3
3
  // run's persisted state (ADR-0007), beside the admission and epoch ledgers, and handed to the next
4
- // admission in scope. The Harness writes every word; this is only the keeping.
4
+ // admission in scope, or to an open Turn at a step boundary. The Harness writes every word; this is
5
+ // only the keeping.
5
6
  //
6
- // Delivered ONCE, and "delivered" means LEDGERED: a notice leaves the pending set in the same save
7
- // that records the admission it rode. A restart before that save finds it still pending and the
8
- // restored Turn's admission carries it again; a restart after it does not. Between `take` and that
9
- // save the notice is RESERVED, in memory only, so a second admission in the same scope cannot carry
10
- // it too — and a restart forgets reservations along with everything else in memory.
11
-
12
- import type { Notice } from "./wire.ts";
13
-
14
- /** Where an admission may hear notices: its continued conversation's base Instance ID (ADR-0057)
15
- * and its Workspace's Sandbox name. Either may be absent — a fresh Turn has no conversation to be
16
- * told about, and a Menu-only Agent works in no Workspace. `only`, when set, narrows the take to
17
- * the notices raised by these sources: what a nudge or a wake carries (ADR-0062). `except` leaves
18
- * out the notices raised by these: what a re-admission of the same Turn was told already;
7
+ // A conversation notice reaches one conversation, so it leaves the pending set when it is heard. A
8
+ // Workspace notice goes to an AUDIENCE that the ledger fixes when it raises it — "an Agent hears a
9
+ // kill if it was there for it":
10
+ //
11
+ // 1. each conversation admitted in that Workspace before the kill that can still take a Turn: an
12
+ // open Turn's, or a continued conversation's (`members`);
13
+ // 2. when the kill cut specific Turns, each one's Agent's NEXT Turn, fresh or continued (`retry`:
14
+ // one kill can cut several Turns, and each Agent retries its own work);
15
+ // 3. minus each conversation whose own `bash` answer returned the kill's verdict (`told`). A
16
+ // retry in a told conversation — the next Turn of a continued one — already has the verdict
17
+ // in its context: it is the retry, and hears nothing.
18
+ //
19
+ // Each member hears it once (`heardBy`), and it RETIRES when every member and every retry has
20
+ // heard it. A conversation that starts after the kill, other than a retry, does not hear it. A
21
+ // fresh Turn that ends leaves the audience, because it can take no Turn again. "Before the kill"
22
+ // allows for the clocks (`AUDIENCE_SKEW_MS`): a kernel kill's time is the kubelet's, to the second.
23
+ //
24
+ // "Heard" means LEDGERED: a notice is heard by a conversation in the same save that records the
25
+ // admission it rode there, or the Harness's acknowledgment of a step boundary that appended it.
26
+ // Between `take` and that save the notice is RESERVED for the taking conversation, in memory only, so
27
+ // a second read in the same conversation cannot carry it too — and a restart forgets reservations
28
+ // along with everything else in memory. jr2 keeps no "handled" state: whether an Agent acted on a
29
+ // notice is not the ledger's concern.
30
+ //
31
+ // The same ledger keeps Steering messages (ADR-0002): one transport, two kinds of body. Each entry is
32
+ // an ENVELOPE — the address, the source, the audience, what is reserved and heard — and a BODY: a
33
+ // Notice, or a Steering message with its author. The address is a tagged value: the body's `scope`
34
+ // names its kind and `to` its value — `conversation` (a continued conversation's base id),
35
+ // `workspace` (a Sandbox name), or `slot` (an Agent slot of one Machine instance, ADR-0076). A
36
+ // message to a slot has one taker, like a conversation notice: the slot's open Turn at a step
37
+ // boundary, or else its next Turn's admission, whose conversation does not exist yet when it is
38
+ // sent. It RETIRES, recorded "not delivered", when its Machine instance ends (`retire`). The ledger
39
+ // also keeps the record of each message (`steering`): its author and its outcome.
40
+
41
+ import type { Notice, SteeringAuthor, SteeringMessage } from "./wire.ts";
42
+
43
+ /** One body of the ledger: a Notice (a fact with no author) or a Steering message (an author's
44
+ * text). The body's `scope` is the kind of its address. */
45
+ export type LedgerBody = Notice | SteeringMessage;
46
+
47
+ /** What became of one Steering message (ADR-0002): `held` until a Turn of its slot hears it,
48
+ * `heard` once one did, `not-delivered` when its Machine instance ended first. */
49
+ export type SteeringOutcome = "held" | "heard" | "not-delivered";
50
+
51
+ /** The run's record of one Steering message: the slot it went to, its author, its text, when it was
52
+ * sent, and what became of it. */
53
+ export type SteeringRecord = {
54
+ id: string;
55
+ agent: string;
56
+ author: SteeringAuthor;
57
+ text: string;
58
+ at: string;
59
+ outcome: SteeringOutcome;
60
+ };
61
+
62
+ /** How many Steering messages a run's record keeps: the newest, and every held one. A message is at
63
+ * most 16 KiB, so the record stays under about 1.6 MiB of a run's blob. */
64
+ export const STEERING_RECORD_MAX = 100;
65
+
66
+ /** The Machine path of a slot (`<machine path>/<agent>`): what ends with its Machine instance. */
67
+ export const slotMachine = (slot: string): string => slot.slice(0, Math.max(0, slot.lastIndexOf("/")));
68
+
69
+ const isSteering = (body: LedgerBody): body is SteeringMessage => body.kind === "steering";
70
+
71
+ /** Whether a record is the record of this message to this slot: a message that an unheard
72
+ * admission carried is put back as a new entry, and its record stays the same. */
73
+ const recordOf = (r: SteeringRecord, body: SteeringMessage, slot: string): boolean =>
74
+ r.agent === slot && r.at === body.at && r.text === body.text;
75
+
76
+ /** Where an admission, or a step boundary, may hear notices: its continued conversation's base
77
+ * Instance ID (ADR-0057) and its Workspace's Sandbox name. Either may be absent — a fresh Turn has no
78
+ * conversation to be told about, and a Menu-only Agent works in no Workspace. `taker` is the
79
+ * conversation that takes a Workspace notice — the Instance ID the admission rides, which a fresh
80
+ * Turn has too, as a new conversation; absent, `conversation` is the taker, and a scope with neither
81
+ * takes no Workspace notice. `slot` is the taker's Agent (ADR-0076), and `turn` says this admission
82
+ * starts a Turn: together they let a cut Turn's retry hear its kill. `only`, when set, narrows the
83
+ * take to the notices raised by these sources: what a nudge or a wake carries (ADR-0062). `except`
84
+ * leaves out the notices raised by these: what a re-admission of the same Turn was told already;
19
85
  * `exceptPrefix` every notice whose source starts so — all guard kills, when that is not known. */
20
86
  export type NoticeScope = {
21
87
  conversation?: string;
22
88
  workspace?: string;
89
+ taker?: string;
90
+ slot?: string;
91
+ turn?: boolean;
23
92
  only?: readonly string[];
24
93
  except?: readonly string[];
25
94
  exceptPrefix?: string;
26
95
  };
27
96
 
97
+ /** A cut Turn's retry (D4 rule 2): the next Turn of `slot` — in any conversation, the cut Turn's
98
+ * own `cut` included when it continues. `retried`: that Turn took it. */
99
+ export type NoticeRetry = { slot: string; cut?: string; retried?: true };
100
+
101
+ /** How far a member's first admission may come after a kill's time and still count as before it:
102
+ * a kernel kill's time is the kubelet's `finishedAt`, which has whole seconds, and a guard kill's is
103
+ * read on a stream; a member's is this process's clock. Two seconds holds the truncation and a
104
+ * second of skew between the clocks. Wider is a notice to a conversation that started just after
105
+ * the kill; narrower is a member that never hears it. */
106
+ export const AUDIENCE_SKEW_MS = 2_000;
107
+
28
108
  /** One pending notice: the notice, and the conversation or Workspace it goes to. `source` names
29
109
  * the event that raised it, so the same event raised twice — two Turns lost to one kernel kill, a
30
- * stream read replayed after a restore — is one notice. */
31
- export type PendingNotice = { id: string; notice: Notice; to: string; source?: string };
110
+ * stream read replayed after a restore — is one notice. On a Workspace notice: `audience`, the
111
+ * conversations fixed at the raise; `retry`, each cut Turn's next Turn; `heardBy`, the
112
+ * conversations it was delivered to, which do not take it again; `told`, the conversations whose
113
+ * own `bash` answer told them. */
114
+ export type PendingNotice = {
115
+ id: string;
116
+ /** The body: a Notice or a Steering message. */
117
+ notice: LedgerBody;
118
+ to: string;
119
+ source?: string;
120
+ audience?: string[];
121
+ retry?: NoticeRetry[];
122
+ heardBy?: string[];
123
+ told?: string[];
124
+ };
125
+
126
+ /** One conversation admitted in a Workspace (ADR-0062): who can be in a Workspace notice's audience.
127
+ * `open`: a Turn of it is live. `continued`: it continues across Turns, so it can take another Turn
128
+ * after this one ends. `since`: its first admission there, which a kill must come after. */
129
+ export type NoticeMember = { taker: string; slot: string; continued: boolean; open: boolean; since: string };
32
130
 
33
131
  /** The persisted form (`RunBlob.notices`). `seen` is every source ever raised, so a delivered
34
- * notice is not raised again by a late or replayed witness of the same event. */
35
- export type NoticeLedgerState = { pending: PendingNotice[]; seen: string[] };
132
+ * notice is not raised again by a late or replayed witness of the same event. `members`: by
133
+ * Workspace, the conversations admitted there that can still take a Turn. */
134
+ export type NoticeLedgerState = {
135
+ pending: PendingNotice[];
136
+ seen: string[];
137
+ /** Each retry ever recorded, by source and slot: a replayed raise records it once. */
138
+ retriesSeen?: string[];
139
+ members?: Record<string, NoticeMember[]>;
140
+ /** The record of the run's Steering messages, oldest first (ADR-0002). */
141
+ steering?: SteeringRecord[];
142
+ };
36
143
 
37
144
  /** What one admission takes: the notices it carries, and their ids for the ledger write. */
38
145
  export type TakenNotices = {
39
146
  ids: string[];
40
- notices: Notice[];
41
- /** Every notice taken, with the source it was raised with — what `restore` puts back when no
42
- * model heard the admission. */
43
- raised?: Array<{ notice: Notice; source?: string }>;
147
+ notices: LedgerBody[];
148
+ /** Every notice taken, in the order of `ids`, with the source it was raised with — what `restore`
149
+ * puts back when no model heard the admission. */
150
+ raised?: Array<{ notice: LedgerBody; source?: string }>;
44
151
  };
45
152
 
153
+ /** What a Workspace notice's raise knows of its kill: the cut Turn's retry, the conversations its
154
+ * `bash` answer told, and when it happened — a conversation first admitted after it was not there. */
155
+ export type RaiseOptions = { retry?: NoticeRetry; told?: readonly string[]; at?: string };
156
+
157
+ /** A reservation: a conversation notice's id alone (it has one taker), a Workspace notice's id
158
+ * with the conversation that took it (each conversation takes it once). */
159
+ const reservation = (p: PendingNotice, taker: string | undefined): string =>
160
+ p.notice.scope === "workspace" ? `${p.id}\u0000${taker ?? ""}` : p.id;
161
+
162
+ /** The mark of a retry recorded for a source: a replayed raise records it once. */
163
+ const retryMark = (source: string, slot: string): string => `${source}\u0000retry\u0000${slot}`;
164
+
165
+ const copyRetry = (r: readonly NoticeRetry[] | undefined) =>
166
+ r === undefined ? {} : { retry: r.map((x) => ({ ...x })) };
167
+
46
168
  export class NoticeLedger {
47
169
  readonly #pending: PendingNotice[];
48
170
  readonly #seen: Set<string>;
171
+ readonly #retriesSeen: Set<string>;
172
+ readonly #members: Map<string, NoticeMember[]>;
49
173
  readonly #reserved = new Set<string>();
174
+ /** The reservations a cut Turn's retry holds, by the slot it retries: delivering one marks that
175
+ * retry `retried`. */
176
+ readonly #asRetry = new Map<string, string>();
50
177
  readonly #newId: () => string;
178
+ readonly #steering: SteeringRecord[];
51
179
 
52
180
  constructor(state: NoticeLedgerState | undefined, newId: () => string) {
53
- this.#pending = [...(state?.pending ?? [])];
181
+ this.#pending = (state?.pending ?? []).map((p) => ({
182
+ ...p,
183
+ ...(p.audience === undefined ? {} : { audience: [...p.audience] }),
184
+ ...copyRetry(p.retry),
185
+ ...(p.heardBy === undefined ? {} : { heardBy: [...p.heardBy] }),
186
+ ...(p.told === undefined ? {} : { told: [...p.told] }),
187
+ }));
54
188
  this.#seen = new Set(state?.seen ?? []);
189
+ this.#retriesSeen = new Set(state?.retriesSeen ?? []);
190
+ this.#members = new Map(Object.entries(state?.members ?? {}).map(([ws, ms]) => [ws, ms.map((m) => ({ ...m }))]));
55
191
  this.#newId = newId;
192
+ this.#steering = (state?.steering ?? []).map((r) => ({ ...r }));
193
+ }
194
+
195
+ /** Hold a Steering message for `slot` (ADR-0002): its open Turn hears it at a step boundary, or
196
+ * else its next Turn's admission carries it. Recorded `held`. Answers its id. */
197
+ steer(message: SteeringMessage, slot: string): string {
198
+ const id = this.#newId();
199
+ this.#pending.push({ id, notice: message, to: slot });
200
+ this.#steering.push({
201
+ id,
202
+ agent: slot,
203
+ author: message.author,
204
+ text: message.text,
205
+ at: message.at,
206
+ outcome: "held",
207
+ });
208
+ this.#trimRecord();
209
+ return id;
210
+ }
211
+
212
+ /** The Machine instance at `machine` (a slot's Machine path) ended (ADR-0002): every message held
213
+ * for a slot of it, or of a Machine below it, retires, recorded `not-delivered`. With no
214
+ * `machine`, the run ended, and every held message retires. Answers whether anything changed. */
215
+ retire(machine?: string): boolean {
216
+ let changed = false;
217
+ for (const p of [...this.#pending]) {
218
+ if (!isSteering(p.notice)) continue;
219
+ const at = slotMachine(p.to);
220
+ if (machine !== undefined && at !== machine && !at.startsWith(`${machine}.`)) continue;
221
+ this.#pending.splice(this.#pending.indexOf(p), 1);
222
+ this.#reserved.delete(p.id);
223
+ this.#outcome(p.notice, p.to, "not-delivered");
224
+ changed = true;
225
+ }
226
+ return changed;
227
+ }
228
+
229
+ /** The run's record of its Steering messages, oldest first. */
230
+ steeringRecord(): SteeringRecord[] {
231
+ return this.#steering.map((r) => ({ ...r }));
232
+ }
233
+
234
+ /** The outcome of a message, on its record. */
235
+ #outcome(body: SteeringMessage, slot: string, outcome: SteeringOutcome): void {
236
+ const record = this.#steering.find((r) => recordOf(r, body, slot));
237
+ if (record !== undefined) record.outcome = outcome;
238
+ }
239
+
240
+ /** Whether a message's record says that it retired: one put back after its Machine ended stays
241
+ * retired. */
242
+ #retired(body: SteeringMessage, slot: string): boolean {
243
+ return this.#steering.some((r) => r.outcome === "not-delivered" && recordOf(r, body, slot));
244
+ }
245
+
246
+ /** Keep the newest {@link STEERING_RECORD_MAX} records, and every held one. */
247
+ #trimRecord(): void {
248
+ let over = this.#steering.length - STEERING_RECORD_MAX;
249
+ for (let i = 0; over > 0 && i < this.#steering.length; ) {
250
+ if (this.#steering[i]!.outcome === "held") {
251
+ i++;
252
+ continue;
253
+ }
254
+ this.#steering.splice(i, 1);
255
+ over--;
256
+ }
56
257
  }
57
258
 
58
259
  /** Raise a notice for `to` (a conversation's base id, or a Sandbox name — the notice's own
59
260
  * `scope` says which). `older` are the sources of earlier events that this one tells the same as —
60
261
  * a burst of guard kills, read at once: they are marked raised and give no notice of their own.
61
- * Answers whether anything changed — a notice pending, or a source marked — so the caller saves
62
- * only then. */
63
- raise(notice: Notice, to: string, source?: string, older: readonly string[] = []): boolean {
262
+ * A Workspace notice's audience is fixed here, from `opts`. Answers whether anything changed — a
263
+ * notice pending, or a source marked — so the caller saves only then. */
264
+ raise(notice: Notice, to: string, source?: string, older: readonly string[] = [], opts: RaiseOptions = {}): boolean {
64
265
  let changed = false;
65
266
  for (const seen of older) {
66
267
  if (this.#seen.has(seen)) continue;
@@ -68,33 +269,73 @@ export class NoticeLedger {
68
269
  changed = true;
69
270
  }
70
271
  if (source !== undefined) {
71
- if (this.#seen.has(source)) return changed;
272
+ // A second witness of one kill: a Turn it cut that the first did not name still retries.
273
+ if (this.#seen.has(source)) return this.#addRetry(notice, to, source, opts.retry) || changed;
72
274
  this.#seen.add(source);
73
- } else {
74
- // A notice with no source is one notice with a pending twin that has none either. A notice
75
- // WITH a source keeps its own entry, so a take by source (`only`, `except`) finds it; equal
76
- // ones are one notice in the admission (`take`).
77
- const same = JSON.stringify(notice);
78
- if (this.#pending.some((p) => p.source === undefined && p.to === to && JSON.stringify(p.notice) === same)) {
79
- return changed;
275
+ if (opts.retry !== undefined) this.#retriesSeen.add(retryMark(source, opts.retry.slot));
276
+ changed = true;
277
+ }
278
+ const workspace = notice.scope === "workspace";
279
+ const audience = workspace ? this.#audienceOf(to, opts) : undefined;
280
+ if (source === undefined) {
281
+ // A notice with no source is one notice with a pending twin that has none either — while no
282
+ // conversation has heard or reserved that twin: a later event is news to each conversation
283
+ // the first one reached. A notice WITH a source keeps its own entry, so a take by source
284
+ // (`only`, `except`) finds it; equal ones are one notice in the admission (`take`).
285
+ const twin = this.#twin(notice, to);
286
+ if (twin !== undefined) {
287
+ if (audience === undefined) return changed;
288
+ const before = twin.audience?.length ?? 0;
289
+ twin.audience = [...new Set([...(twin.audience ?? []), ...audience])];
290
+ const retried = opts.retry !== undefined && !(twin.retry ?? []).some((r) => r.slot === opts.retry!.slot);
291
+ if (retried) twin.retry = [...(twin.retry ?? []), { ...opts.retry! }];
292
+ return changed || twin.audience.length !== before || retried;
80
293
  }
81
294
  }
82
- this.#pending.push({ id: this.#newId(), notice, to, ...(source === undefined ? {} : { source }) });
295
+ // A Workspace notice no one was there for, and that no Turn retries, tells no one.
296
+ if (audience !== undefined && audience.length === 0 && opts.retry === undefined) return changed;
297
+ this.#pending.push({
298
+ id: this.#newId(),
299
+ notice,
300
+ to,
301
+ ...(source === undefined ? {} : { source }),
302
+ ...(audience === undefined ? {} : { audience }),
303
+ ...(opts.retry === undefined ? {} : { retry: [{ ...opts.retry }] }),
304
+ ...(workspace && opts.told !== undefined && opts.told.length > 0 ? { told: [...opts.told] } : {}),
305
+ });
83
306
  return true;
84
307
  }
85
308
 
86
- /** Reserve every unreserved notice in this admission's scope, in the order raised. Equal notices —
87
- * two kills that tell the same thing — are said once, and every id is reserved. */
309
+ /** A raise of a seen source that names a cut Turn: its retry joins the notice, or is pending on a
310
+ * notice of its own when the first witness gave none or every member has heard it. Once for each
311
+ * slot and source, so a replayed read adds nothing. */
312
+ #addRetry(notice: Notice, to: string, source: string, retry: NoticeRetry | undefined): boolean {
313
+ if (retry === undefined || notice.scope !== "workspace") return false;
314
+ const mark = retryMark(source, retry.slot);
315
+ if (this.#retriesSeen.has(mark)) return false;
316
+ this.#retriesSeen.add(mark);
317
+ const pending = this.#pending.find((p) => p.source === source && p.to === to);
318
+ if (pending !== undefined) {
319
+ pending.retry = [...(pending.retry ?? []), { ...retry }];
320
+ return true;
321
+ }
322
+ this.#pending.push({ id: this.#newId(), notice, to, source, audience: [], retry: [{ ...retry }] });
323
+ return true;
324
+ }
325
+
326
+ /** Reserve every notice in this scope that this conversation may hear and has neither heard nor
327
+ * reserved, in the order raised. Equal notices — two kills that tell the same thing — are said
328
+ * once, and every id is reserved. */
88
329
  take(scope: NoticeScope): TakenNotices {
89
- const taken = this.#pending.filter(
90
- (p) =>
91
- !this.#reserved.has(p.id) &&
92
- (scope.only === undefined || (p.source !== undefined && scope.only.includes(p.source))) &&
93
- (scope.except === undefined || p.source === undefined || !scope.except.includes(p.source)) &&
94
- (scope.exceptPrefix === undefined || p.source === undefined || !p.source.startsWith(scope.exceptPrefix)) &&
95
- (p.notice.scope === "conversation" ? p.to === scope.conversation : p.to === scope.workspace),
96
- );
97
- for (const p of taken) this.#reserved.add(p.id);
330
+ const taker = scope.taker ?? scope.conversation;
331
+ this.#retryTold(scope, taker);
332
+ const taken = this.#eligible(scope, taker);
333
+ for (const p of taken) {
334
+ const key = reservation(p, taker);
335
+ this.#reserved.add(key);
336
+ const retry = this.#retryFor(p, scope, taker);
337
+ if (retry !== undefined) this.#asRetry.set(key, retry.slot);
338
+ }
98
339
  const said = new Set<string>();
99
340
  const notices = taken.flatMap((p) => {
100
341
  const key = JSON.stringify(p.notice);
@@ -109,33 +350,309 @@ export class NoticeLedger {
109
350
  };
110
351
  }
111
352
 
353
+ /** What {@link take} would reserve in this scope, reserving nothing: a step boundary asks first
354
+ * whether a guard kill is among them, and so whether it must learn who was told. */
355
+ peek(scope: NoticeScope): Array<{ id: string; notice: LedgerBody; source?: string }> {
356
+ return this.#eligible(scope, scope.taker ?? scope.conversation).map((p) => ({
357
+ id: p.id,
358
+ notice: p.notice,
359
+ ...(p.source === undefined ? {} : { source: p.source }),
360
+ }));
361
+ }
362
+
112
363
  /** Put back a notice an admission carried that no model heard, with the source it was raised
113
- * with — `seen` already holds it, so a raise would drop it. Nothing changes when a notice with
114
- * that source is pending already. Answers whether anything changed. */
115
- restore(notice: Notice, to: string, source?: string): boolean {
364
+ * with — `seen` already holds it, so a raise would drop it. A Workspace notice is still pending
365
+ * for the other conversations: it is pending again for `taker`, the conversation that did not hear
366
+ * it, which is a member of its audience again. Nothing changes when the notice is pending for that
367
+ * conversation already. Answers whether anything changed. */
368
+ restore(notice: LedgerBody, to: string, source?: string, taker?: string): boolean {
369
+ // A Steering message has one taker: pending again for its slot, unless its Machine instance
370
+ // ended while the admission that carried it was unheard.
371
+ if (isSteering(notice)) {
372
+ const same = (p: PendingNotice) =>
373
+ p.to === to && isSteering(p.notice) && p.notice.at === notice.at && p.notice.text === notice.text;
374
+ if (this.#retired(notice, to) || this.#pending.some(same)) return false;
375
+ this.#pending.push({ id: this.#newId(), notice, to });
376
+ this.#outcome(notice, to, "held");
377
+ return true;
378
+ }
379
+ const pending =
380
+ source === undefined
381
+ ? notice.scope === "workspace" && taker !== undefined
382
+ ? this.#equal(notice, to, taker)
383
+ : this.#twin(notice, to)
384
+ : this.#pending.find((p) => p.source === source && p.to === to);
385
+ if (pending !== undefined) {
386
+ if (pending.notice.scope !== "workspace" || taker === undefined) return false;
387
+ const heardBy = pending.heardBy ?? [];
388
+ const inAudience = pending.audience === undefined || pending.audience.includes(taker);
389
+ if (!heardBy.includes(taker) && inAudience) return false;
390
+ pending.heardBy = heardBy.filter((c) => c !== taker);
391
+ if (!inAudience) pending.audience = [...(pending.audience ?? []), taker];
392
+ return true;
393
+ }
394
+ if (source !== undefined) this.#seen.add(source);
395
+ if (notice.scope === "workspace") {
396
+ // Retired, or never pending for anyone else: pending again for the one that did not hear it.
397
+ if (taker === undefined) return false;
398
+ this.#pending.push({
399
+ id: this.#newId(),
400
+ notice,
401
+ to,
402
+ ...(source === undefined ? {} : { source }),
403
+ audience: [taker],
404
+ });
405
+ return true;
406
+ }
116
407
  if (source === undefined) return this.raise(notice, to);
117
- if (this.#pending.some((p) => p.source === source && p.to === to)) return false;
118
- this.#seen.add(source);
119
408
  this.#pending.push({ id: this.#newId(), notice, to, source });
120
409
  return true;
121
410
  }
122
411
 
123
- /** The admission that carried them was ledgered: they are delivered. */
124
- delivered(ids: readonly string[]): void {
412
+ /** The admission that carried them, in `taker`'s conversation, was ledgered — or the Harness
413
+ * acknowledged the step boundary that appended them: they are heard there. A conversation notice
414
+ * leaves the pending set; a Workspace notice stays for the rest of its audience, and `taker` does
415
+ * not hear it again. It retires once every member has heard it. */
416
+ delivered(ids: readonly string[], taker?: string): void {
125
417
  for (const id of ids) {
126
- this.#reserved.delete(id);
127
418
  const at = this.#pending.findIndex((p) => p.id === id);
128
- if (at !== -1) this.#pending.splice(at, 1);
419
+ const p = this.#pending[at];
420
+ if (p === undefined) {
421
+ this.#reserved.delete(id);
422
+ continue;
423
+ }
424
+ const key = reservation(p, taker);
425
+ this.#reserved.delete(key);
426
+ if (p.notice.scope !== "workspace") {
427
+ this.#pending.splice(at, 1);
428
+ if (isSteering(p.notice)) this.#outcome(p.notice, p.to, "heard");
429
+ continue;
430
+ }
431
+ const slot = this.#asRetry.get(key);
432
+ if (slot !== undefined) {
433
+ this.#asRetry.delete(key);
434
+ for (const r of p.retry ?? []) if (r.slot === slot) r.retried = true;
435
+ }
436
+ if (taker !== undefined && !(p.heardBy ?? []).includes(taker)) p.heardBy = [...(p.heardBy ?? []), taker];
437
+ this.#retireIfHeard(p);
129
438
  }
130
439
  }
131
440
 
132
- /** The admission that carried them failed: they are pending again, for the next one. */
133
- release(ids: readonly string[]): void {
134
- for (const id of ids) this.#reserved.delete(id);
441
+ /** The Harness acknowledged the step boundary that appended these (ADR-0062): each one that
442
+ * `taker` holds a reservation on is heard there. Answers how many were. */
443
+ acknowledge(ids: readonly string[], taker: string): number {
444
+ const mine = ids.filter((id) => {
445
+ const p = this.#pending.find((q) => q.id === id);
446
+ return p !== undefined && this.#reserved.has(reservation(p, taker));
447
+ });
448
+ this.delivered(mine, taker);
449
+ return mine.length;
450
+ }
451
+
452
+ /** The admission that carried them, in `taker`'s conversation, failed — or the Turn ended with a
453
+ * step boundary's notices unacknowledged: they are pending again there, for the next one. */
454
+ release(ids: readonly string[], taker?: string): void {
455
+ for (const id of ids) {
456
+ const p = this.#pending.find((q) => q.id === id);
457
+ const key = p === undefined ? id : reservation(p, taker);
458
+ this.#reserved.delete(key);
459
+ this.#asRetry.delete(key);
460
+ }
461
+ }
462
+
463
+ /** A conversation was admitted in `workspace` (ADR-0062): it is there for every kill from now on,
464
+ * while it can take a Turn. Answers whether anything changed. */
465
+ admitted(workspace: string, member: Omit<NoticeMember, "open">): boolean {
466
+ const members = this.#members.get(workspace) ?? [];
467
+ const known = members.find((m) => m.taker === member.taker);
468
+ if (known !== undefined) {
469
+ if (known.open && known.continued === member.continued) return false;
470
+ known.open = true;
471
+ known.continued = member.continued;
472
+ return true;
473
+ }
474
+ members.push({ ...member, open: true });
475
+ this.#members.set(workspace, members);
476
+ return true;
477
+ }
478
+
479
+ /** A Turn of these conversations in `workspace` ended. A continued one can take another Turn and
480
+ * stays a member; a fresh one cannot, so it leaves every audience it has not heard yet — which can
481
+ * retire a notice. Answers whether anything changed. */
482
+ ended(workspace: string, takers: readonly string[]): boolean {
483
+ let changed = false;
484
+ const members = this.#members.get(workspace) ?? [];
485
+ for (const taker of takers) {
486
+ const at = members.findIndex((m) => m.taker === taker);
487
+ const m = members[at];
488
+ if (m === undefined) continue;
489
+ changed = true;
490
+ if (m.continued) {
491
+ m.open = false;
492
+ continue;
493
+ }
494
+ members.splice(at, 1);
495
+ this.#leave(taker, workspace);
496
+ }
497
+ if (members.length === 0) this.#members.delete(workspace);
498
+ return changed;
499
+ }
500
+
501
+ /** A conversation that can take no Turn again — a continued one jr2 burned (ADR-0057): it leaves
502
+ * every Workspace and every audience. Answers whether anything changed. */
503
+ forget(taker: string): boolean {
504
+ let changed = false;
505
+ for (const [workspace, members] of this.#members) {
506
+ const at = members.findIndex((m) => m.taker === taker);
507
+ if (at < 0) continue;
508
+ members.splice(at, 1);
509
+ if (members.length === 0) this.#members.delete(workspace);
510
+ this.#leave(taker, workspace);
511
+ changed = true;
512
+ }
513
+ return changed;
514
+ }
515
+
516
+ /** The conversations whose own `bash` answer returned the verdict of the kill `source` raised
517
+ * (D4 rule 3): that answer told them, so they leave its audience. Answers whether anything
518
+ * changed. */
519
+ told(source: string, takers: readonly string[]): boolean {
520
+ if (takers.length === 0) return false;
521
+ const p = this.#pending.find((q) => q.source === source);
522
+ if (p === undefined || p.audience === undefined) return false;
523
+ const before = p.audience.length;
524
+ const toldBefore = p.told?.length ?? 0;
525
+ p.told = [...new Set([...(p.told ?? []), ...takers])];
526
+ p.audience = p.audience.filter((t) => !takers.includes(t) || (p.heardBy ?? []).includes(t));
527
+ if (p.audience.length === before && p.told.length === toldBefore) return false;
528
+ this.#retireIfHeard(p);
529
+ return true;
135
530
  }
136
531
 
137
532
  /** The persisted form — reserved notices included, because reserved is not delivered. */
138
533
  state(): NoticeLedgerState {
139
- return { pending: [...this.#pending], seen: [...this.#seen] };
534
+ return {
535
+ pending: this.#pending.map((p) => ({
536
+ ...p,
537
+ ...(p.audience === undefined ? {} : { audience: [...p.audience] }),
538
+ ...copyRetry(p.retry),
539
+ ...(p.heardBy === undefined ? {} : { heardBy: [...p.heardBy] }),
540
+ ...(p.told === undefined ? {} : { told: [...p.told] }),
541
+ })),
542
+ seen: [...this.#seen],
543
+ ...(this.#retriesSeen.size === 0 ? {} : { retriesSeen: [...this.#retriesSeen] }),
544
+ ...(this.#members.size === 0
545
+ ? {}
546
+ : { members: Object.fromEntries([...this.#members].map(([ws, ms]) => [ws, ms.map((m) => ({ ...m }))])) }),
547
+ ...(this.#steering.length === 0 ? {} : { steering: this.#steering.map((r) => ({ ...r })) }),
548
+ };
549
+ }
550
+
551
+ /** The notices this scope may take for `taker`, unreserved and unheard, in the order raised. */
552
+ #eligible(scope: NoticeScope, taker: string | undefined): PendingNotice[] {
553
+ return this.#pending.filter(
554
+ (p) =>
555
+ !this.#reserved.has(reservation(p, taker)) &&
556
+ (scope.only === undefined || (p.source !== undefined && scope.only.includes(p.source))) &&
557
+ (scope.except === undefined || p.source === undefined || !scope.except.includes(p.source)) &&
558
+ (scope.exceptPrefix === undefined || p.source === undefined || !p.source.startsWith(scope.exceptPrefix)) &&
559
+ (p.notice.scope === "conversation"
560
+ ? p.to === scope.conversation
561
+ : p.notice.scope === "slot"
562
+ ? p.to === scope.slot
563
+ : taker !== undefined &&
564
+ p.to === scope.workspace &&
565
+ !(p.heardBy ?? []).includes(taker) &&
566
+ (p.audience === undefined ||
567
+ p.audience.includes(taker) ||
568
+ this.#retryFor(p, scope, taker) !== undefined)),
569
+ );
570
+ }
571
+
572
+ /** The cut Turn's retry that this take is (D4 rule 2), or undefined: a Turn's admission of the cut
573
+ * Agent's slot, in any conversation that its own `bash` answer did not tell — the cut Turn's own,
574
+ * when it continues, too. */
575
+ #retryFor(p: PendingNotice, scope: NoticeScope, taker: string | undefined): NoticeRetry | undefined {
576
+ if (scope.turn !== true || taker === undefined || (p.told ?? []).includes(taker)) return undefined;
577
+ return (p.retry ?? []).find((r) => r.retried !== true && r.slot === scope.slot);
578
+ }
579
+
580
+ /** A Turn of a retried slot in a conversation that its `bash` answer told: the next Turn of a
581
+ * continued conversation, which has the verdict in its context. It is the retry, and hears
582
+ * nothing more — so the notice can retire. */
583
+ #retryTold(scope: NoticeScope, taker: string | undefined): void {
584
+ if (scope.turn !== true || taker === undefined || scope.workspace === undefined) return;
585
+ for (const p of [...this.#pending]) {
586
+ if (p.to !== scope.workspace || !(p.told ?? []).includes(taker)) continue;
587
+ let changed = false;
588
+ for (const r of p.retry ?? []) {
589
+ if (r.retried === true || r.slot !== scope.slot) continue;
590
+ r.retried = true;
591
+ changed = true;
592
+ }
593
+ if (changed) this.#retireIfHeard(p);
594
+ }
595
+ }
596
+
597
+ /** Who of a Workspace's members is there for a kill now: each that can take a Turn, first
598
+ * admitted before the kill, and not told by its own `bash` answer. */
599
+ #audienceOf(workspace: string, opts: RaiseOptions): string[] {
600
+ const at = opts.at === undefined ? undefined : Date.parse(opts.at);
601
+ return (this.#members.get(workspace) ?? [])
602
+ .filter((m) => m.open || m.continued)
603
+ .filter((m) => at === undefined || Number.isNaN(at) || Date.parse(m.since) <= at + AUDIENCE_SKEW_MS)
604
+ .map((m) => m.taker)
605
+ .filter((t) => !(opts.told ?? []).includes(t));
606
+ }
607
+
608
+ /** `taker` can hear no more in `workspace`: it leaves each audience there it has not heard. */
609
+ #leave(taker: string, workspace: string): void {
610
+ for (const p of [...this.#pending]) {
611
+ if (p.notice.scope !== "workspace" || p.to !== workspace || p.audience === undefined) continue;
612
+ if ((p.heardBy ?? []).includes(taker) || !p.audience.includes(taker)) continue;
613
+ p.audience = p.audience.filter((t) => t !== taker);
614
+ this.#retireIfHeard(p);
615
+ }
616
+ }
617
+
618
+ /** A Workspace notice every member has heard, and every retry has, is done. */
619
+ #retireIfHeard(p: PendingNotice): void {
620
+ if (p.notice.scope !== "workspace" || p.audience === undefined) return;
621
+ const heard = p.heardBy ?? [];
622
+ if (!p.audience.every((t) => heard.includes(t))) return;
623
+ if ((p.retry ?? []).some((r) => r.retried !== true)) return;
624
+ const at = this.#pending.indexOf(p);
625
+ if (at >= 0) this.#pending.splice(at, 1);
626
+ }
627
+
628
+ /** The pending notice with no source that tells the same as `notice`, to the same place, and
629
+ * that a new event of the same words can fold into: a conversation notice always, a Workspace
630
+ * notice only while no conversation has heard or reserved it. */
631
+ #twin(notice: Notice, to: string): PendingNotice | undefined {
632
+ return this.#sameAs(notice, to).find(
633
+ (p) => p.notice.scope !== "workspace" || ((p.heardBy ?? []).length === 0 && !this.#reservedByAny(p)),
634
+ );
635
+ }
636
+
637
+ /** The pending Workspace notice with no source that tells the same as `notice` and that `taker`
638
+ * restores: one still pending for it — then nothing changes — or else the last one it heard. */
639
+ #equal(notice: Notice, to: string, taker: string): PendingNotice | undefined {
640
+ const same = this.#sameAs(notice, to);
641
+ return (
642
+ same.find(
643
+ (p) => !(p.heardBy ?? []).includes(taker) && (p.audience === undefined || p.audience.includes(taker)),
644
+ ) ?? same.at(-1)
645
+ );
646
+ }
647
+
648
+ #sameAs(notice: Notice, to: string): PendingNotice[] {
649
+ const same = JSON.stringify(notice);
650
+ return this.#pending.filter((p) => p.source === undefined && p.to === to && JSON.stringify(p.notice) === same);
651
+ }
652
+
653
+ /** Whether any conversation holds a reservation on this notice. */
654
+ #reservedByAny(p: PendingNotice): boolean {
655
+ for (const key of this.#reserved) if (key === p.id || key.startsWith(`${p.id}\u0000`)) return true;
656
+ return false;
140
657
  }
141
658
  }