@north-light/crouter 0.3.312 → 0.3.313

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 (495) hide show
  1. package/dist/api/client.d.ts +27 -30
  2. package/dist/api/client.js +80 -67
  3. package/dist/api/dto/broker-ops.d.ts +10 -20
  4. package/dist/api/dto/canvas.d.ts +1 -10
  5. package/dist/api/dto/health.d.ts +113 -0
  6. package/dist/api/dto/human-requests.d.ts +14 -3
  7. package/dist/api/dto/human.d.ts +2 -48
  8. package/dist/api/dto/human.js +3 -2
  9. package/dist/api/dto/inbox.d.ts +14 -0
  10. package/dist/api/dto/mail.d.ts +43 -0
  11. package/dist/api/dto/nodes.d.ts +11 -8
  12. package/dist/api/dto/recovery.d.ts +96 -0
  13. package/dist/api/dto/reviews.d.ts +2 -2
  14. package/dist/api/routes.d.ts +9 -6
  15. package/dist/api/routes.js +11 -11
  16. package/dist/build-root.d.ts +4 -0
  17. package/dist/build-root.js +11 -1
  18. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/integration/provider-rotation.test.ts +35 -47
  19. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.d.ts +1 -1
  20. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +12 -5
  21. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +12 -5
  22. package/dist/cli.js +10 -1
  23. package/dist/clients/__tests__/helpers/human-daemon.d.ts +11 -0
  24. package/dist/clients/__tests__/helpers/human-daemon.js +60 -0
  25. package/dist/clients/attach/chrome/canvas-panels.js +1 -1
  26. package/dist/clients/attach/chrome/inbox-strip.d.ts +3 -3
  27. package/dist/clients/attach/chrome/inbox-strip.js +17 -9
  28. package/dist/clients/attach/chrome/ticket-panel.d.ts +2 -2
  29. package/dist/clients/attach/chrome/ticket-panel.js +77 -46
  30. package/dist/clients/attach/overlays/graph.js +6 -9
  31. package/dist/clients/attach/render/chat-view.d.ts +5 -4
  32. package/dist/clients/attach/render/chat-view.js +22 -13
  33. package/dist/clients/attach/render/page-block.d.ts +11 -7
  34. package/dist/clients/attach/render/page-block.js +64 -55
  35. package/dist/clients/attach/viewer.js +746 -735
  36. package/dist/clients/inbox/__tests__/integration/inbox-controller.test.js +56 -56
  37. package/dist/clients/inbox/__tests__/integration/stale-row-activation.test.js +45 -32
  38. package/dist/clients/inbox/controller.d.ts +42 -14
  39. package/dist/clients/inbox/controller.js +240 -124
  40. package/dist/clients/inbox/dto-adapt.d.ts +28 -0
  41. package/dist/clients/inbox/dto-adapt.js +93 -0
  42. package/dist/clients/inbox/page-adapter.d.ts +5 -4
  43. package/dist/clients/inbox/page-adapter.js +12 -12
  44. package/dist/clients/inbox/resolve.d.ts +10 -5
  45. package/dist/clients/inbox/resolve.js +13 -12
  46. package/dist/clients/inbox/review/review-client.d.ts +0 -1
  47. package/dist/clients/inbox/review/review-client.js +0 -4
  48. package/dist/clients/inbox/review-adapter.d.ts +2 -1
  49. package/dist/clients/inbox/review-adapter.js +3 -5
  50. package/dist/clients/inbox/surface.d.ts +2 -2
  51. package/dist/clients/inbox/surface.js +19 -8
  52. package/dist/clients/inbox/tui.d.ts +3 -3
  53. package/dist/commands/__tests__/human.test.js +31 -15
  54. package/dist/commands/__tests__/integration/dashboard-snapshot.test.js +36 -11
  55. package/dist/commands/{sys/__tests__ → __tests__/integration}/migrate.test.js +142 -36
  56. package/dist/commands/__tests__/revive-now-gate.test.js +2 -0
  57. package/dist/commands/api-client.d.ts +5 -4
  58. package/dist/commands/api-client.js +13 -7
  59. package/dist/commands/canvas-history/shared.js +1 -1
  60. package/dist/commands/canvas-history.js +1 -1
  61. package/dist/commands/canvas-roster.js +1 -1
  62. package/dist/commands/canvas.js +1 -2
  63. package/dist/commands/human/feedback.js +20 -17
  64. package/dist/commands/human/prompts.js +45 -36
  65. package/dist/commands/human/queue.js +73 -123
  66. package/dist/commands/human/review.js +6 -16
  67. package/dist/commands/memory/__tests__/command-selector-and-mutation-guards.test.js +2 -0
  68. package/dist/commands/memory/__tests__/memory-list-and-lint-ignore.test.js +2 -0
  69. package/dist/commands/memory/__tests__/repository-root-lint.test.js +2 -0
  70. package/dist/commands/node-inspect-artifacts.js +1 -1
  71. package/dist/commands/node-lifecycle-revive.d.ts +2 -2
  72. package/dist/commands/node-lifecycle-revive.js +9 -19
  73. package/dist/commands/pkg/plugin-manage.js +0 -1
  74. package/dist/commands/profile/show.js +4 -12
  75. package/dist/commands/surface-tmux-spread.js +1 -5
  76. package/dist/commands/sys/__tests__/config-model-ladders.test.js +23 -30
  77. package/dist/commands/sys/__tests__/sync-project-guidance.test.js +37 -11
  78. package/dist/commands/sys/branch.d.ts +2 -0
  79. package/dist/commands/sys/branch.js +19 -0
  80. package/dist/commands/sys/config.js +106 -33
  81. package/dist/commands/sys/daemon.d.ts +2 -0
  82. package/dist/commands/sys/daemon.js +130 -7
  83. package/dist/commands/sys/doctor.js +5 -15
  84. package/dist/commands/sys/migrate.js +21 -92
  85. package/dist/commands/sys/setup-settings-runner.js +4 -2
  86. package/dist/commands/sys.js +2 -15
  87. package/dist/core/__tests__/boot-fault-attempt.test.js +23 -1
  88. package/dist/core/__tests__/boot.test.js +32 -25
  89. package/dist/core/__tests__/broker-launch-candidates.test.js +49 -0
  90. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +149 -176
  91. package/dist/core/__tests__/canvas-inbox-watcher-naming.test.js +59 -10
  92. package/dist/core/__tests__/canvas-inbox-watcher.test.js +76 -20
  93. package/dist/core/__tests__/canvas.test.js +18 -47
  94. package/dist/core/__tests__/child-death-wake.test.js +29 -7
  95. package/dist/core/__tests__/cli-help-contract.test.js +2 -0
  96. package/dist/core/__tests__/close.test.js +29 -10
  97. package/dist/core/__tests__/collectible.test.js +2 -1
  98. package/dist/core/__tests__/config-change-delta.test.js +2 -0
  99. package/dist/core/__tests__/context-intro.test.js +13 -1
  100. package/dist/core/__tests__/cron-armed-context-failure.test.js +17 -12
  101. package/dist/core/__tests__/cron-held-settlement.test.js +21 -3
  102. package/dist/core/__tests__/cron-node-sink-parked-root.test.js +29 -10
  103. package/dist/core/__tests__/cron-self-cancel-delivery.test.js +14 -2
  104. package/dist/core/__tests__/cron-timeout-descendants.test.js +13 -1
  105. package/dist/core/__tests__/daemon-boot.test.js +51 -14
  106. package/dist/core/__tests__/fixtures/fake-engine.d.ts +1 -1
  107. package/dist/core/__tests__/fixtures/fake-engine.js +8 -3
  108. package/dist/core/__tests__/focuses.test.js +10 -5
  109. package/dist/core/__tests__/helpers/harness.js +50 -18
  110. package/dist/core/__tests__/history-inbox.test.js +12 -16
  111. package/dist/core/__tests__/human-action-delivery.test.js +48 -28
  112. package/dist/core/__tests__/human-actions.test.js +7 -3
  113. package/dist/core/__tests__/inline-memory-refs.test.js +7 -2
  114. package/dist/core/__tests__/integration/broker-fork-seam.test.js +37 -10
  115. package/dist/core/__tests__/integration/broker-sdk-wiring.test.js +42 -0
  116. package/dist/core/__tests__/integration/broker-snapshot-history.test.js +42 -29
  117. package/dist/core/__tests__/integration/broker-wedge-supervision.test.js +36 -35
  118. package/dist/core/__tests__/integration/command-plugins.test.js +15 -6
  119. package/dist/core/__tests__/integration/deferred-no-wake.test.js +113 -92
  120. package/dist/core/__tests__/integration/host-teardown-process-group.test.js +9 -5
  121. package/dist/core/__tests__/integration/lifecycle-hooks.test.js +16 -3
  122. package/dist/core/__tests__/integration/plugin-revalidate.test.js +4 -0
  123. package/dist/core/__tests__/integration/profile-pause.test.js +4 -4
  124. package/dist/core/__tests__/integration/refresh-stall-recycle.test.js +9 -12
  125. package/dist/core/__tests__/integration/revive.test.js +67 -21
  126. package/dist/core/__tests__/integration/spawn-root.test.js +51 -8
  127. package/dist/core/__tests__/integration/subscription-delivery.test.js +33 -21
  128. package/dist/core/__tests__/integration/worktree-land.test.js +5 -2
  129. package/dist/core/__tests__/integration/worktree-reap.test.js +15 -1
  130. package/dist/core/__tests__/kickoff.test.js +36 -39
  131. package/dist/core/__tests__/lifecycle-terminal-reasons.test.js +2 -1
  132. package/dist/core/__tests__/lifecycle.test.js +2 -1
  133. package/dist/core/__tests__/live-lifecycle-prompt.test.js +13 -1
  134. package/dist/core/__tests__/memory-resolver-precedence.test.js +2 -0
  135. package/dist/core/__tests__/migration.test.js +145 -31
  136. package/dist/core/__tests__/model-pin-durability.test.js +4 -1
  137. package/dist/core/__tests__/model-routes-config.test.js +13 -5
  138. package/dist/core/__tests__/node-outcome.test.js +93 -26
  139. package/dist/core/__tests__/on-read-crouter-home-fence.test.js +2 -1
  140. package/dist/core/__tests__/on-read-dedup-resume.test.js +2 -1
  141. package/dist/core/__tests__/on-read-nested-store.test.js +2 -1
  142. package/dist/core/__tests__/passive-subscription.test.js +50 -157
  143. package/dist/core/__tests__/plugin-kinds.test.js +6 -4
  144. package/dist/core/__tests__/plugin-link-symlinked-scope-root.test.js +6 -1
  145. package/dist/core/__tests__/plugin-page-components.test.js +6 -4
  146. package/dist/core/__tests__/preview-registry-sync.test.js +2 -0
  147. package/dist/core/__tests__/profile-project-memory-delivery.test.js +4 -1
  148. package/dist/core/__tests__/prune.test.js +11 -5
  149. package/dist/core/__tests__/push-final-guard.test.js +14 -7
  150. package/dist/core/__tests__/relaunch-root.test.js +30 -2
  151. package/dist/core/__tests__/remote-canvas-target.test.js +7 -1
  152. package/dist/core/__tests__/repository-root-identity.test.js +4 -0
  153. package/dist/core/__tests__/review-model-floor.test.js +6 -3
  154. package/dist/core/__tests__/revive-all.test.js +13 -3
  155. package/dist/core/__tests__/revive-capacity.test.js +26 -4
  156. package/dist/core/__tests__/revive-parked-fresh.test.js +31 -8
  157. package/dist/core/__tests__/seam/broker-attach-stream.test.js +1 -1
  158. package/dist/core/__tests__/seam/broker-cap-freeze.test.js +4 -3
  159. package/dist/core/__tests__/seam/broker-crash-teardown.test.js +11 -14
  160. package/dist/core/__tests__/seam/broker-provider-retry.test.js +85 -71
  161. package/dist/core/__tests__/seam/dormancy-release.test.js +33 -19
  162. package/dist/core/__tests__/seam/held-deferred-human-prompt.test.js +7 -5
  163. package/dist/core/__tests__/seam/inbox-reference-guidance.test.js +20 -6
  164. package/dist/core/__tests__/seam/node-outcome-delivery.test.js +27 -3
  165. package/dist/core/__tests__/spawn-worktree-id-conflict.test.js +2 -1
  166. package/dist/core/__tests__/stop-guard.test.js +2 -1
  167. package/dist/core/__tests__/subscription-pool-credential-guard.test.js +4 -3
  168. package/dist/core/__tests__/warm-claim-preference-snapshot.test.js +18 -4
  169. package/dist/core/__tests__/yield-ensures-daemon.test.js +2 -1
  170. package/dist/core/bootstrap.js +2 -0
  171. package/dist/core/broker-client/__tests__/transport-socket.test.js +51 -6
  172. package/dist/core/broker-client/transport-socket.d.ts +1 -0
  173. package/dist/core/broker-client/transport-socket.js +13 -5
  174. package/dist/core/canvas/__tests__/attention.test.js +29 -35
  175. package/dist/core/canvas/__tests__/history-transcript.test.js +6 -6
  176. package/dist/core/canvas/__tests__/render-remote.test.js +25 -9
  177. package/dist/core/canvas/attention.d.ts +2 -5
  178. package/dist/core/canvas/attention.js +15 -64
  179. package/dist/core/canvas/boot.d.ts +6 -7
  180. package/dist/core/canvas/boot.js +5 -9
  181. package/dist/core/canvas/canvas.d.ts +13 -109
  182. package/dist/core/canvas/canvas.js +140 -401
  183. package/dist/core/canvas/crons.d.ts +2 -0
  184. package/dist/core/canvas/crons.js +16 -15
  185. package/dist/core/canvas/daemon-owner.js +10 -0
  186. package/dist/core/canvas/db.d.ts +2 -2
  187. package/dist/core/canvas/db.js +2 -18
  188. package/dist/core/canvas/deliveries.d.ts +104 -0
  189. package/dist/core/canvas/deliveries.js +354 -0
  190. package/dist/core/canvas/executions.d.ts +29 -0
  191. package/dist/core/canvas/executions.js +68 -0
  192. package/dist/core/canvas/history.js +35 -35
  193. package/dist/core/canvas/index.d.ts +3 -1
  194. package/dist/core/canvas/index.js +3 -1
  195. package/dist/core/canvas/migrations.js +20 -12
  196. package/dist/core/canvas/nav-model.js +1 -3
  197. package/dist/core/canvas/nav-render.d.ts +4 -4
  198. package/dist/core/canvas/nav-render.js +11 -21
  199. package/dist/core/canvas/node-sql-migration.d.ts +3 -0
  200. package/dist/core/canvas/node-sql-migration.js +261 -0
  201. package/dist/core/canvas/paths.d.ts +2 -16
  202. package/dist/core/canvas/paths.js +2 -31
  203. package/dist/core/canvas/recovery.d.ts +76 -0
  204. package/dist/core/canvas/recovery.js +588 -0
  205. package/dist/core/canvas/remote-canvas-source.js +4 -0
  206. package/dist/core/canvas/render-source.d.ts +16 -27
  207. package/dist/core/canvas/render-source.js +16 -45
  208. package/dist/core/canvas/sessions.d.ts +33 -0
  209. package/dist/core/canvas/sessions.js +78 -0
  210. package/dist/core/canvas/types.d.ts +33 -33
  211. package/dist/core/canvas/types.js +2 -5
  212. package/dist/core/canvas/waits.d.ts +18 -0
  213. package/dist/core/canvas/waits.js +70 -0
  214. package/dist/core/canvas/worktrees.d.ts +40 -0
  215. package/dist/core/canvas/worktrees.js +93 -0
  216. package/dist/core/config.d.ts +17 -12
  217. package/dist/core/config.js +106 -155
  218. package/dist/core/feed/__tests__/inbox-migrate.test.js +27 -31
  219. package/dist/core/feed/feed.d.ts +0 -6
  220. package/dist/core/feed/feed.js +68 -56
  221. package/dist/core/feed/inbox.d.ts +7 -81
  222. package/dist/core/feed/inbox.js +46 -378
  223. package/dist/core/feed/messages.d.ts +58 -0
  224. package/dist/core/feed/messages.js +217 -0
  225. package/dist/core/fs-utils.d.ts +4 -0
  226. package/dist/core/fs-utils.js +21 -1
  227. package/dist/core/human/answer.d.ts +1 -1
  228. package/dist/core/human/answer.js +1 -1
  229. package/dist/core/human/claim.d.ts +1 -1
  230. package/dist/core/human/claim.js +2 -4
  231. package/dist/core/human/component-docs.js +1 -1
  232. package/dist/core/human/feedback-companion.js +3 -2
  233. package/dist/core/human/feedback.d.ts +5 -8
  234. package/dist/core/human/feedback.js +57 -85
  235. package/dist/core/human/page-authoring.d.ts +30 -0
  236. package/dist/core/human/page-authoring.js +19 -0
  237. package/dist/core/human/requests.d.ts +136 -0
  238. package/dist/core/human/requests.js +515 -0
  239. package/dist/core/human/root.d.ts +0 -7
  240. package/dist/core/human/root.js +0 -17
  241. package/dist/core/human/scan.d.ts +2 -7
  242. package/dist/core/human/scan.js +48 -132
  243. package/dist/core/human/summary.d.ts +1 -1
  244. package/dist/core/human/summary.js +1 -1
  245. package/dist/core/human/tickets.d.ts +23 -83
  246. package/dist/core/human/tickets.js +92 -243
  247. package/dist/core/human/types.d.ts +5 -1
  248. package/dist/core/installed-plugins.js +8 -3
  249. package/dist/core/keybindings/__tests__/resolve.test.js +19 -17
  250. package/dist/core/locked-file.d.ts +6 -3
  251. package/dist/core/locked-file.js +36 -9
  252. package/dist/core/memory/extensions.js +2 -0
  253. package/dist/core/preview-registry.js +0 -1
  254. package/dist/core/profiles/__tests__/env-store.test.js +2 -0
  255. package/dist/core/profiles/default-binding.d.ts +0 -4
  256. package/dist/core/profiles/default-binding.js +12 -58
  257. package/dist/core/profiles/env-store.js +8 -3
  258. package/dist/core/profiles/manifest.d.ts +2 -37
  259. package/dist/core/profiles/manifest.js +35 -161
  260. package/dist/core/profiles/select.js +3 -0
  261. package/dist/core/review/__tests__/capture-origin.test.js +13 -2
  262. package/dist/core/review/__tests__/comments.test.js +28 -10
  263. package/dist/core/review/__tests__/remap.test.js +28 -10
  264. package/dist/core/review/__tests__/stage-identity.test.js +17 -7
  265. package/dist/core/review/birth.d.ts +1 -1
  266. package/dist/core/review/birth.js +34 -5
  267. package/dist/core/review/comments.js +1 -1
  268. package/dist/core/review/document.js +1 -1
  269. package/dist/core/review/project.d.ts +5 -1
  270. package/dist/core/review/project.js +14 -36
  271. package/dist/core/review/realize.d.ts +3 -6
  272. package/dist/core/review/realize.js +51 -168
  273. package/dist/core/review/remap.js +1 -1
  274. package/dist/core/review/stage.d.ts +3 -2
  275. package/dist/core/review/stage.js +2 -3
  276. package/dist/core/review/store.d.ts +3 -21
  277. package/dist/core/review/store.js +140 -186
  278. package/dist/core/review/types.d.ts +0 -1
  279. package/dist/core/runtime/__tests__/install-id-cache-scoping.test.js +9 -5
  280. package/dist/core/runtime/__tests__/spawn-env.test.js +8 -9
  281. package/dist/core/runtime/broker/auth-reload.js +9 -10
  282. package/dist/core/runtime/broker/daemon-ops.d.ts +25 -8
  283. package/dist/core/runtime/broker/daemon-ops.js +42 -9
  284. package/dist/core/runtime/broker/extension-abort.d.ts +3 -0
  285. package/dist/core/runtime/broker/extension-abort.js +11 -0
  286. package/dist/core/runtime/broker/fault-retry.d.ts +15 -6
  287. package/dist/core/runtime/broker/fault-retry.js +142 -95
  288. package/dist/core/runtime/broker/inbox.d.ts +4 -32
  289. package/dist/core/runtime/broker/inbox.js +4 -92
  290. package/dist/core/runtime/broker/rebind.d.ts +1 -0
  291. package/dist/core/runtime/broker/rebind.js +21 -12
  292. package/dist/core/runtime/broker-persona-guidance.d.ts +1 -1
  293. package/dist/core/runtime/broker-persona-guidance.js +2 -13
  294. package/dist/core/runtime/broker.js +28 -22
  295. package/dist/core/runtime/close.d.ts +3 -9
  296. package/dist/core/runtime/close.js +16 -27
  297. package/dist/core/runtime/fault.d.ts +3 -93
  298. package/dist/core/runtime/fault.js +9 -419
  299. package/dist/core/runtime/kickoff.d.ts +3 -6
  300. package/dist/core/runtime/kickoff.js +12 -18
  301. package/dist/core/runtime/launch.js +1 -1
  302. package/dist/core/runtime/lifecycle.js +40 -7
  303. package/dist/core/runtime/placement.js +2 -2
  304. package/dist/core/runtime/recycle.js +8 -2
  305. package/dist/core/runtime/reset.js +22 -6
  306. package/dist/core/runtime/revive.js +226 -218
  307. package/dist/core/runtime/spawn.js +14 -3
  308. package/dist/core/scoped-state/db.d.ts +7 -0
  309. package/dist/core/scoped-state/db.js +92 -0
  310. package/dist/core/scoped-state/migrate.d.ts +11 -0
  311. package/dist/core/scoped-state/migrate.js +198 -0
  312. package/dist/core/scoped-state/paths.d.ts +9 -0
  313. package/dist/core/scoped-state/paths.js +27 -0
  314. package/dist/core/scoped-state/profiles.d.ts +12 -0
  315. package/dist/core/scoped-state/profiles.js +62 -0
  316. package/dist/core/scoped-state/providers.d.ts +16 -0
  317. package/dist/core/scoped-state/providers.js +8 -0
  318. package/dist/core/scoped-state/schema.d.ts +3 -0
  319. package/dist/core/scoped-state/schema.js +26 -0
  320. package/dist/core/scoped-state/settings.d.ts +28 -0
  321. package/dist/core/scoped-state/settings.js +82 -0
  322. package/dist/core/subscription-state.d.ts +29 -48
  323. package/dist/core/subscription-state.js +176 -712
  324. package/dist/core/worktree-close.js +4 -4
  325. package/dist/core/worktree-mutation-async.js +2 -2
  326. package/dist/core/worktree-sweep.js +11 -16
  327. package/dist/core/worktree.js +6 -12
  328. package/dist/daemon/__tests__/autostart-storm.test.js +1 -39
  329. package/dist/daemon/__tests__/helpers/source-daemon.d.ts +2 -2
  330. package/dist/daemon/__tests__/helpers/source-daemon.js +7 -6
  331. package/dist/daemon/__tests__/integration/migration-startup.test.js +59 -26
  332. package/dist/daemon/__tests__/node-deadline-lane.test.js +2 -1
  333. package/dist/daemon/__tests__/node-outcome-birth-invariants.test.js +39 -15
  334. package/dist/daemon/__tests__/node-outcome-delivery.test.js +39 -10
  335. package/dist/daemon/__tests__/reconciler-signature.test.js +2 -2
  336. package/dist/daemon/__tests__/subtree-filter-freshness.test.js +2 -2
  337. package/dist/daemon/api/__tests__/broker-settle-park.test.js +17 -12
  338. package/dist/daemon/api/__tests__/canvas-snapshot-fields.test.js +27 -5
  339. package/dist/daemon/api/__tests__/model-config.test.js +8 -8
  340. package/dist/daemon/api/__tests__/node-create-description.test.js +22 -5
  341. package/dist/daemon/api/__tests__/node-model-validation.test.js +21 -1
  342. package/dist/daemon/api/__tests__/nodes-activity-query.test.js +10 -12
  343. package/dist/daemon/api/__tests__/profile-launch-gates.test.js +4 -1
  344. package/dist/daemon/api/__tests__/profile-metadata-route.test.js +4 -1
  345. package/dist/daemon/api/__tests__/reopen-delivery.test.js +26 -8
  346. package/dist/daemon/api/__tests__/seam/api-server.test.js +24 -32
  347. package/dist/daemon/api/__tests__/seam/b10-attach-modelauth.test.js +4 -1
  348. package/dist/daemon/api/__tests__/seam/leaf-api-parity.test.js +5 -4
  349. package/dist/daemon/api/__tests__/seam/profile-delete.test.js +27 -41
  350. package/dist/daemon/api/__tests__/submit-result.test.js +3 -2
  351. package/dist/daemon/api/bridge.js +1 -1
  352. package/dist/daemon/api/handlers/bash-jobs.js +6 -7
  353. package/dist/daemon/api/handlers/broker-ops.js +47 -84
  354. package/dist/daemon/api/handlers/broker-recovery.d.ts +4 -0
  355. package/dist/daemon/api/handlers/broker-recovery.js +251 -0
  356. package/dist/daemon/api/handlers/canvas.js +2 -44
  357. package/dist/daemon/api/handlers/daemon.js +41 -0
  358. package/dist/daemon/api/handlers/feedback-comments.js +2 -2
  359. package/dist/daemon/api/handlers/health.js +10 -5
  360. package/dist/daemon/api/handlers/human-requests.js +168 -188
  361. package/dist/daemon/api/handlers/human.js +3 -151
  362. package/dist/daemon/api/handlers/inbox-page.d.ts +4 -0
  363. package/dist/daemon/api/handlers/inbox-page.js +89 -0
  364. package/dist/daemon/api/handlers/inbox.js +21 -120
  365. package/dist/daemon/api/handlers/messages.d.ts +1 -0
  366. package/dist/daemon/api/handlers/messages.js +73 -15
  367. package/dist/daemon/api/handlers/node-outcomes.js +58 -13
  368. package/dist/daemon/api/handlers/nodes.js +7 -13
  369. package/dist/daemon/api/handlers/reviews.js +4 -29
  370. package/dist/daemon/api/map.d.ts +25 -3
  371. package/dist/daemon/api/map.js +84 -14
  372. package/dist/daemon/api/server.js +68 -9
  373. package/dist/daemon/control.d.ts +18 -0
  374. package/dist/daemon/cron/disposition.d.ts +2 -0
  375. package/dist/daemon/cron/disposition.js +8 -5
  376. package/dist/daemon/cron/settle.js +37 -13
  377. package/dist/daemon/cron/sinks.d.ts +18 -1
  378. package/dist/daemon/cron/sinks.js +47 -93
  379. package/dist/daemon/crtrd-cli.js +4 -19
  380. package/dist/daemon/crtrd.d.ts +2 -0
  381. package/dist/daemon/crtrd.js +206 -81
  382. package/dist/daemon/fleet.js +5 -6
  383. package/dist/daemon/human/finish.d.ts +12 -24
  384. package/dist/daemon/human/finish.js +88 -285
  385. package/dist/daemon/human/settle.d.ts +40 -0
  386. package/dist/daemon/human/settle.js +89 -0
  387. package/dist/daemon/human/sweep.d.ts +20 -5
  388. package/dist/daemon/human/sweep.js +56 -60
  389. package/dist/daemon/manage.d.ts +21 -16
  390. package/dist/daemon/manage.js +85 -51
  391. package/dist/daemon/messaging/node-message.d.ts +1 -1
  392. package/dist/daemon/messaging/node-message.js +9 -12
  393. package/dist/daemon/park-activity.js +3 -3
  394. package/dist/daemon/reconcilers/bash-deadline.js +6 -7
  395. package/dist/daemon/reconcilers/broker-supervision.d.ts +1 -1
  396. package/dist/daemon/reconcilers/broker-supervision.js +41 -19
  397. package/dist/daemon/reconcilers/controller-death.js +6 -18
  398. package/dist/daemon/reconcilers/delivery-lane.d.ts +9 -0
  399. package/dist/daemon/reconcilers/delivery-lane.js +130 -0
  400. package/dist/daemon/reconcilers/live-obligation.js +3 -3
  401. package/dist/daemon/reconcilers/node-deadline.js +1 -1
  402. package/dist/daemon/reconcilers/node-lifecycle/freeze-lane.d.ts +1 -1
  403. package/dist/daemon/reconcilers/node-lifecycle/freeze-lane.js +10 -12
  404. package/dist/daemon/reconcilers/node-lifecycle/tick.js +5 -5
  405. package/dist/daemon/reconcilers/node-lifecycle/wants-execution.js +1 -1
  406. package/dist/daemon/reconcilers/pending-review-submit.js +3 -3
  407. package/dist/daemon/reconcilers/storage-maintenance.d.ts +2 -8
  408. package/dist/daemon/reconcilers/storage-maintenance.js +13 -43
  409. package/dist/daemon/review/companion.js +5 -11
  410. package/dist/daemon/review/deliver.d.ts +1 -5
  411. package/dist/daemon/review/deliver.js +3 -52
  412. package/dist/daemon/review/finish.d.ts +2 -27
  413. package/dist/daemon/review/finish.js +79 -263
  414. package/dist/daemon/review/sweep.d.ts +3 -10
  415. package/dist/daemon/review/sweep.js +13 -131
  416. package/dist/daemon/startup-policy.d.ts +0 -5
  417. package/dist/daemon/startup-policy.js +0 -5
  418. package/dist/index.d.ts +2 -1
  419. package/dist/index.js +1 -1
  420. package/dist/migrations/__tests__/activation-concurrency.test.js +3 -2
  421. package/dist/migrations/__tests__/activation.test.js +9 -6
  422. package/dist/migrations/activation.d.ts +3 -2
  423. package/dist/migrations/activation.js +2 -4
  424. package/dist/migrations/corpus.d.ts +2 -5
  425. package/dist/migrations/corpus.js +9 -20
  426. package/dist/migrations/runner.d.ts +3 -1
  427. package/dist/migrations/runner.js +6 -2
  428. package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +21 -89
  429. package/dist/pi-extensions/__tests__/canvas-goal-capture-envelope.test.js +4 -2
  430. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +51 -33
  431. package/dist/pi-extensions/__tests__/canvas-stophook-context-nudge.test.js +16 -2
  432. package/dist/pi-extensions/__tests__/pre-command-gate.test.js +13 -1
  433. package/dist/pi-extensions/canvas-inbox-watcher.js +98 -244
  434. package/dist/pi-extensions/canvas-passive-context.d.ts +4 -8
  435. package/dist/pi-extensions/canvas-passive-context.js +85 -37
  436. package/dist/pi-extensions/canvas-stophook.js +47 -31
  437. package/dist/shared/env.d.ts +1 -0
  438. package/dist/shared/env.js +3 -0
  439. package/package.json +1 -1
  440. package/runtime.lock.json +5 -5
  441. package/dist/clients/attach/__tests__/page-block.test.js +0 -54
  442. package/dist/clients/inbox/review-visibility.d.ts +0 -5
  443. package/dist/clients/inbox/review-visibility.js +0 -19
  444. package/dist/commands/canvas-rebuild-index.d.ts +0 -2
  445. package/dist/commands/canvas-rebuild-index.js +0 -46
  446. package/dist/core/__tests__/human-cancel-guard.test.js +0 -110
  447. package/dist/core/__tests__/human-deliver.test.d.ts +0 -1
  448. package/dist/core/__tests__/human-deliver.test.js +0 -256
  449. package/dist/core/__tests__/human-node-not-supervised.test.d.ts +0 -1
  450. package/dist/core/__tests__/human-node-not-supervised.test.js +0 -89
  451. package/dist/core/__tests__/integration/human-deliver-e2e.test.d.ts +0 -1
  452. package/dist/core/__tests__/integration/human-deliver-e2e.test.js +0 -131
  453. package/dist/core/__tests__/phase4-review-store.test.d.ts +0 -1
  454. package/dist/core/__tests__/phase4-review-store.test.js +0 -144
  455. package/dist/core/__tests__/watchdog-abort-arms-retry.test.d.ts +0 -1
  456. package/dist/core/__tests__/watchdog-abort-arms-retry.test.js +0 -110
  457. package/dist/core/canvas/human-deliveries.d.ts +0 -39
  458. package/dist/core/canvas/human-deliveries.js +0 -75
  459. package/dist/core/canvas/node-outcome-deliveries.d.ts +0 -64
  460. package/dist/core/canvas/node-outcome-deliveries.js +0 -159
  461. package/dist/core/feed/passive.d.ts +0 -3
  462. package/dist/core/feed/passive.js +0 -53
  463. package/dist/core/human/__tests__/inbox-cancel-worker.d.ts +0 -1
  464. package/dist/core/human/__tests__/inbox-cancel-worker.js +0 -7
  465. package/dist/core/human/__tests__/inbox-claim-worker.d.ts +0 -1
  466. package/dist/core/human/__tests__/inbox-claim-worker.js +0 -17
  467. package/dist/core/human/__tests__/inbox-stale-claim-worker.d.ts +0 -1
  468. package/dist/core/human/__tests__/inbox-stale-claim-worker.js +0 -8
  469. package/dist/core/human/__tests__/integration/inbox-core.test.d.ts +0 -1
  470. package/dist/core/human/__tests__/integration/inbox-core.test.js +0 -145
  471. package/dist/core/human/__tests__/page-tickets.test.d.ts +0 -1
  472. package/dist/core/human/__tests__/page-tickets.test.js +0 -161
  473. package/dist/core/review/ticket-filter.d.ts +0 -17
  474. package/dist/core/review/ticket-filter.js +0 -21
  475. package/dist/core/runtime/broker/passive.d.ts +0 -17
  476. package/dist/core/runtime/broker/passive.js +0 -44
  477. package/dist/core/runtime/busy.d.ts +0 -46
  478. package/dist/core/runtime/busy.js +0 -150
  479. package/dist/daemon/__tests__/startup-block-marker.test.d.ts +0 -1
  480. package/dist/daemon/__tests__/startup-block-marker.test.js +0 -86
  481. package/dist/daemon/api/__tests__/review-terminal-conflict.test.d.ts +0 -1
  482. package/dist/daemon/api/__tests__/review-terminal-conflict.test.js +0 -98
  483. package/dist/daemon/human/deliver-action.d.ts +0 -3
  484. package/dist/daemon/human/deliver-action.js +0 -9
  485. package/dist/daemon/node-outcome/deliver-outcome.d.ts +0 -3
  486. package/dist/daemon/node-outcome/deliver-outcome.js +0 -9
  487. package/dist/daemon/reconcilers/human-delivery-lane.d.ts +0 -10
  488. package/dist/daemon/reconcilers/human-delivery-lane.js +0 -41
  489. package/dist/daemon/reconcilers/node-outcome-delivery-lane.d.ts +0 -10
  490. package/dist/daemon/reconcilers/node-outcome-delivery-lane.js +0 -41
  491. package/dist/daemon/startup-block-marker.d.ts +0 -25
  492. package/dist/daemon/startup-block-marker.js +0 -99
  493. /package/dist/{clients/attach/__tests__/page-block.test.d.ts → api/dto/mail.js} +0 -0
  494. /package/dist/{commands/sys/__tests__/migrate.test.d.ts → api/dto/recovery.js} +0 -0
  495. /package/dist/{core/__tests__/human-cancel-guard.test.d.ts → commands/__tests__/integration/migrate.test.d.ts} +0 -0
@@ -17,7 +17,7 @@
17
17
  // message) — falling back to a short note when it never reported.
18
18
  import { readdirSync, readFileSync, statSync } from 'node:fs';
19
19
  import { join } from 'node:path';
20
- import { getNode, setPresence, setFocusOccupant, fullName, recordLaunch, recordPid } from '../canvas/index.js';
20
+ import { abortExecution, beginExecution, getNode, setPresence, setFocusOccupant, fullName, recordExecutionPid, recordLaunch } from '../canvas/index.js';
21
21
  import { reportsDir } from '../canvas/paths.js';
22
22
  import { pushFinal } from '../feed/feed.js';
23
23
  import { spawnNode, rootOfSpine } from './nodes.js';
@@ -25,6 +25,7 @@ import { buildLaunchSpecAsync, buildPiArgv } from './launch.js';
25
25
  import { focusOf, respawnPaneSync, setPaneOption } from './placement.js';
26
26
  import { waitForBrokerViewSocket, viewerSplitEnv } from './placement-tmux.js';
27
27
  import { headlessBrokerHost } from './host.js';
28
+ import { boundFleet } from './fleet.js';
28
29
  import { transition } from './lifecycle.js';
29
30
  import { ensureDaemon } from '../../daemon/manage.js';
30
31
  import { invokeBirthLifecycleHooks } from './spawn.js';
@@ -137,17 +138,22 @@ export async function recycleNode(nodeId, callerPane) {
137
138
  * broker. This direct birth path writes the same launch/pid coordinates as
138
139
  * reviveNode, and a failed readiness probe terminalizes and tears it down. */
139
140
  async function launchRecycledBroker(fresh, inv) {
141
+ const executionId = beginExecution(fresh.node_id, { daemonEpoch: boundFleet().epoch() });
142
+ inv.env = { ...inv.env, CRTR_EXECUTION_ID: executionId };
140
143
  let placed;
141
144
  try {
142
145
  placed = headlessBrokerHost.launch(fresh.node_id, inv, { cwd: fresh.cwd, name: fullName(fresh), resuming: false });
143
146
  if (placed.pid === null)
144
147
  throw new Error('broker host returned no pid');
145
148
  recordLaunch(fresh.node_id, new Date().toISOString());
146
- recordPid(fresh.node_id, placed.pid);
149
+ if (recordExecutionPid(executionId, fresh.node_id, placed.pid) === 'shutdown') {
150
+ throw new Error('broker execution was superseded');
151
+ }
147
152
  if (await waitForBrokerViewSocket(fresh.node_id, placed.exited))
148
153
  return true;
149
154
  }
150
155
  catch {
156
+ abortExecution(executionId, { reason: 'launch_failed' });
151
157
  // The replacement never became usable; the finished root's broker remains
152
158
  // intact and this half-born row cannot remain active without a fleet handle.
153
159
  }
@@ -20,9 +20,10 @@
20
20
  // reserved for finish).
21
21
  //
22
22
  // Best-effort throughout: a tmux/fs failure on one node never aborts the reap.
23
- import { getNode, updateNode, recordLaunch, recordPid, fullName, closeFocusRow, view, } from '../canvas/index.js';
23
+ import { getNode, beginExecution, recordExecutionPid, abortExecution, bindSession, recordLaunch, fullName, liveExecutionForNode, closeFocusRow, view, } from '../canvas/index.js';
24
24
  import { transition } from './lifecycle.js';
25
25
  import { headlessBrokerHost, requestBrokerTeardown } from './host.js';
26
+ import { boundFleet } from './fleet.js';
26
27
  import { tearDownNode, focusOf, registerViewerFocus, respawnPaneSync, windowOfPane, renameWindow, } from './placement.js';
27
28
  import { viewerSplitEnv, waitForBrokerViewSocket } from './placement-tmux.js';
28
29
  import { buildLaunchSpecAsync, buildPiArgv } from './launch.js';
@@ -116,6 +117,8 @@ export async function relaunchRoot(oldId, deps = {}) {
116
117
  ...inv.env,
117
118
  CRTR_SUBTREE: rootOfSpine(newMeta.node_id),
118
119
  };
120
+ const executionId = beginExecution(newMeta.node_id, { daemonEpoch: boundFleet().epoch() });
121
+ inv.env = { ...inv.env, CRTR_EXECUTION_ID: executionId };
119
122
  let placed;
120
123
  try {
121
124
  placed = launchBroker(newMeta.node_id, inv, {
@@ -125,10 +128,12 @@ export async function relaunchRoot(oldId, deps = {}) {
125
128
  });
126
129
  }
127
130
  catch (error) {
131
+ abortExecution(executionId, { reason: 'launch_failed' });
128
132
  transition(newMeta.node_id, 'crash');
129
133
  throw error;
130
134
  }
131
135
  if (placed.pid == null) {
136
+ abortExecution(executionId, { reason: 'launch_failed' });
132
137
  // The new broker never started — crash the half-born node and BAIL. The old
133
138
  // root is still live and untouched (nothing below this point has run).
134
139
  transition(newMeta.node_id, 'crash');
@@ -138,7 +143,15 @@ export async function relaunchRoot(oldId, deps = {}) {
138
143
  // real exit, while the row gets the matching launch and pid coordinates before
139
144
  // readiness can observe an early exit.
140
145
  recordLaunch(newMeta.node_id, new Date().toISOString());
141
- recordPid(newMeta.node_id, placed.pid);
146
+ if (recordExecutionPid(executionId, newMeta.node_id, placed.pid) === 'shutdown') {
147
+ abortExecution(executionId, { reason: 'superseded' });
148
+ try {
149
+ teardownBroker(newMeta.node_id);
150
+ }
151
+ catch { /* best-effort */ }
152
+ transition(newMeta.node_id, 'crash');
153
+ return null;
154
+ }
142
155
  let ready = false;
143
156
  try {
144
157
  ready = await waitForViewSocket(newMeta.node_id, placed.exited);
@@ -191,10 +204,13 @@ export function resetRoot(nodeId, newSessionId, newSessionFile) {
191
204
  if (meta === null || meta.parent == null)
192
205
  return empty; // unknown or a root
193
206
  if (newSessionId !== undefined) {
194
- updateNode(nodeId, {
195
- pi_session_id: newSessionId,
196
- ...(newSessionFile !== undefined ? { pi_session_file: newSessionFile } : {}),
197
- });
207
+ const execution = liveExecutionForNode(nodeId);
208
+ if (execution !== null)
209
+ bindSession(nodeId, {
210
+ executionId: execution.executionId,
211
+ sessionId: newSessionId,
212
+ transcriptPath: newSessionFile ?? null,
213
+ });
198
214
  }
199
215
  return empty;
200
216
  }
@@ -24,7 +24,7 @@
24
24
  // reviveNode remains the ONLY sanctioned launcher of the node engine.
25
25
  import { existsSync, readFileSync } from 'node:fs';
26
26
  import { isAbsolute } from 'node:path';
27
- import { findNodeBySessionFile, getNode, updateNode, clearPid, recordLaunch, recordPid, setFrozen, clearFrozen, fullName, subscribersOf, } from '../canvas/index.js';
27
+ import { findNodeBySessionFile, getNode, updateNode, beginExecution, recordExecutionPid, abortExecution, endLiveExecution, unbindSession, recordLaunch, setFrozen, clearFrozen, fullName, subscribersOf, } from '../canvas/index.js';
28
28
  import { emitEvent } from '../events/emit.js';
29
29
  import { cancelCronsOnWake } from '../canvas/crons.js';
30
30
  import { isParked, transition } from './lifecycle.js';
@@ -36,8 +36,7 @@ import { readRoadmap } from './roadmap.js';
36
36
  import { encodeKickoffOrigin, KICKOFF_ORIGIN_ENV } from './stamp/protocol.js';
37
37
  import { headlessBrokerHost } from './host.js';
38
38
  import { boundFleet, brokerCapReached, brokerThresholdsForDaemon } from './fleet.js';
39
- import { clearFault, beginBootFaultAttempt } from './fault.js';
40
- import { clearTransportPoisonVerdict } from './busy.js';
39
+ import { beginBootFaultAttempt, clearFault } from '../canvas/recovery.js';
41
40
  import { clearContextExposureState, initializeForkContextExposure, } from '../substrate/injected-store.js';
42
41
  import { rootOfSpine } from './nodes.js';
43
42
  import { isReviewCompanionBound } from '../review/companion.js';
@@ -228,7 +227,7 @@ async function decideRevive(nodeId, opts, wakeParked) {
228
227
  // every status surface. Clearing it is what makes `isFrozenRow` true and
229
228
  // what tells the tick this row has never launched OR has already been
230
229
  // written off, so the freeze lane owes it a launch rather than a verdict.
231
- clearPid(nodeId);
230
+ endLiveExecution(nodeId, { reason: 'frozen' });
232
231
  emitEvent({
233
232
  level: 'info',
234
233
  event: 'broker.frozen',
@@ -289,7 +288,7 @@ async function launchRevive(nodeId, meta, opts) {
289
288
  // immutable fork instead of resuming an inherited transcript.
290
289
  const reviewCompanionUnbound = meta.review_binding != null && !isReviewCompanionBound(nodeId);
291
290
  if (reviewCompanionUnbound && (meta.pi_session_id != null || meta.pi_session_file != null)) {
292
- updateNode(nodeId, { pi_session_id: null, pi_session_file: null });
291
+ unbindSession(nodeId);
293
292
  meta.pi_session_id = null;
294
293
  meta.pi_session_file = null;
295
294
  }
@@ -312,225 +311,234 @@ async function launchRevive(nodeId, meta, opts) {
312
311
  next: `Restore ${forkSourceFile}, or update the node's fork source, then retry \`crtr node lifecycle revive ${nodeId}\`.`,
313
312
  });
314
313
  }
315
- // Every (re)launch is a new cycle — bump the counter so CRTR_CYCLES and the
316
- // footer stay in sync. Mutate the in-memory meta too so buildPiArgv below
317
- // launches with the incremented count.
318
- meta.cycles = (meta.cycles ?? 0) + 1;
319
- updateNode(nodeId, { cycles: meta.cycles });
320
- // Ordinary nodes choose resume/cycle/fresh from their recorded session.
321
- // Persisted pending-refresh state (durable across a pre-session_start
322
- // crash-retry) outranks a caller's strict-resume request. A parked node now
323
- // resumes its transcript too: its isolated parking turn never entered the
324
- // main session, so wake mail continues one intact conversation without a
325
- // fresh-cycle handoff. resumeArgs then selects only an existing absolute
326
- // session path: strict resume continues its leaf, while refresh/cycle mode
327
- // roots a new one.
328
- const refreshPending = meta.intent === 'refresh' || meta.cycle_pending === true;
329
- const effectiveResume = refreshPending ? false : opts.resume;
330
- const resume = forkBirthPending ? {} : resumableSessionArgs(nodeId, meta, effectiveResume);
331
- // hasSessionPath: there is a real `.jsonl` to hand pi via `--session`, whether
332
- // as a true resume OR a cycling refresh-yield (resumeArgs sets `newCycle` in
333
- // the latter case). cycling and resuming are mutually exclusive; `resuming`
334
- // keeps the STRICT true-resume-only meaning `ReviveResult.resumed` and the
335
- // launch-placement hint below have always carried.
336
- const hasSessionPath = resume.resumeSessionPath !== undefined;
337
- const cycling = hasSessionPath && resume.newCycle === true;
338
- const resuming = hasSessionPath && !cycling;
339
- // Never conversed and nothing on disk to rebuild from — relaunch as a birth
340
- // (see isUnstartedBirth). A clock-driven wake must keep its <crtr-wake>
341
- // kickoff — the schedule IS the instruction to take a turn, so it never
342
- // degrades to a birth replay. This one decision selects the kickoff below
343
- // and is what the node:start hook payload reports as `isBirth`: a hook is
344
- // told "birth" exactly when the node boots as one.
345
- const birthReplay = !hasSessionPath
346
- && (opts.wakeReason === undefined || opts.wakeReason === 'runtime-restart-abort')
347
- && isUnstartedBirth(nodeId);
348
- const isBirth = forkBirthPending || birthReplay;
349
- // A cycling launch is provisional until session_start confirms it. Once
350
- // true, this marker is monotonic across failed retries; only that confirmed
351
- // boot may clear it.
352
- const cyclePending = meta.cycle_pending === true || cycling;
353
- updateNode(nodeId, { cycle_pending: cyclePending });
354
- meta.cycle_pending = cyclePending;
355
- // A truly fresh launch — no `.jsonl` to resume OR cycle from —
356
- // starts a NEW pi session, so the old session identity is dead. Clear it from meta NOW,
357
- // before launch, so the recorded state matches what we're about to do. This
358
- // is load-bearing for exit-policy classification (classifyDeadNode row 8):
359
- // if this fresh broker dies BEFORE session_start (recording no new session),
360
- // the exit-policy job reads pi_session_id=NULL → boot-failure, the correct
361
- // terminal outcome (transition('crash') + surfaceBootFailure) — a leftover
362
- // stale id would instead be misread as a resumable session and retried. A
363
- // cycling refresh keeps BOTH ids — the `.jsonl` and its recorded session id
364
- // are exactly what's being reused — so a cycling broker that dies
365
- // pre-session_start classifies as a retryable respawn, same class as a
366
- // resume revive.
367
- if (!forkBirthPending && !hasSessionPath && (meta.pi_session_id != null || meta.pi_session_file != null)) {
368
- updateNode(nodeId, { pi_session_id: null, pi_session_file: null });
369
- meta.pi_session_id = null;
370
- meta.pi_session_file = null;
371
- }
372
- // A true resume needs no kickoff — the saved conversation just continues. A
373
- // fresh revive OR a cycling refresh-yield both get one so the (re-rooted)
374
- // window re-reads its roadmap and continues: drainBearings captures the
375
- // yield note and consumes the feed/persona acknowledgements; the builder is
376
- // then pure. `resume` carries `resumeSessionPath`/`newCycle` through
377
- // unchanged (empty for true-fresh, both set for cycling) so buildPiArgv emits
378
- // the right argv either way.
379
- let inv;
380
- // The provenance of whatever kickoff this branch composes, handed to the
381
- // broker process through inv.env below. A promptless launch announces
382
- // nothing — there is no message to stamp.
383
- let kickoffOrigin = null;
384
- const birthOrigin = () => ({
385
- type: 'runtime',
386
- kind: 'kickoff',
387
- ...(meta.spawned_by != null && meta.spawned_by !== '' ? { spawnedBy: meta.spawned_by } : {}),
388
- });
389
- if (forkBirthPending) {
390
- const goal = readGoal(nodeId);
391
- inv = buildPiArgv(meta, {
392
- forkFrom: forkSourceFile,
393
- ...(goal != null && goal !== '' ? { prompt: goal } : {}),
394
- });
395
- if (goal != null && goal !== '')
396
- kickoffOrigin = birthOrigin();
397
- initializeForkContextExposure(nodeId, findNodeBySessionFile(forkSourceFile)?.node_id ?? null);
398
- }
399
- else if (resuming) {
400
- inv = buildPiArgv(meta, {
401
- ...resume,
402
- ...(opts.wakeReason === 'runtime-restart-abort' ? { prompt: RUNTIME_RESTART_CONTINUATION } : {}),
403
- });
404
- if (opts.wakeReason === 'runtime-restart-abort') {
405
- kickoffOrigin = { type: 'runtime', kind: 'restart-continuation' };
406
- }
407
- }
408
- else if (birthReplay) {
409
- // A node with a spawn mandate replays it; a bare interactive node boots
410
- // promptless and takes no turn at all.
411
- const goal = readGoal(nodeId)?.trim();
412
- inv = buildPiArgv(meta, goal !== undefined && goal !== '' ? { prompt: goal } : {});
413
- if (goal !== undefined && goal !== '')
414
- kickoffOrigin = birthOrigin();
415
- clearContextExposureState(nodeId);
416
- }
417
- else {
418
- // Fresh or cycling: drain the one-shot bearings, then build the kickoff.
419
- // When a scheduled bare self-alarm drove this revive, opts.wakeReason
420
- // carries the wake provenance so the kickoff leads with a <crtr-wake> block
421
- // ("a timer woke you"); every other reviveNode caller passes nothing → no
422
- // block.
423
- const bearings = drainBearings(meta);
424
- inv = buildPiArgv(meta, {
425
- ...resume,
426
- prompt: buildReviveKickoff(meta, bearings, opts.wakeReason === 'runtime-restart-abort' ? undefined : opts.wakeReason),
427
- });
428
- kickoffOrigin = opts.wakeReason !== undefined && opts.wakeReason !== 'runtime-restart-abort'
429
- ? { type: 'runtime', kind: 'wake', wake: opts.wakeReason }
430
- : { type: 'runtime', kind: 'revive', cycle: meta.cycles ?? 0 };
431
- clearContextExposureState(nodeId);
432
- }
433
- // Every lifecycle launch emits the authoritative spine root rather than
434
- // inheriting whichever node happened to invoke reviveNode.
435
- inv.env = { ...inv.env, CRTR_SUBTREE: rootOfSpine(nodeId) };
436
- if (kickoffOrigin !== null)
437
- inv.env[KICKOFF_ORIGIN_ENV] = encodeKickoffOrigin(kickoffOrigin);
438
- // The broker host launches the detached engine. reviveNode keeps
439
- // transition+clearPid around it (the crash-safety ordering, unchanged). It
440
- // opens NO viewer — engine-only; existing viewers reconnect, and a viewer-less
441
- // node is brought on screen by the next `focus`.
442
- transition(nodeId, meta.intent === 'refresh' ? 'refresh-launch' : 'revive');
443
- // Clear any active fault the instant this node is (re)launched — don't wait
444
- // for the fresh agent_start. A revive (auto or on-demand) is the recovery
445
- // this marker advertised. Snapshot the boot-fault attempt in the SAME
446
- // breath (see beginBootFaultAttempt): its own verification, not this call's
447
- // unchecked result, is what scopes a later diagnostic read to this attempt.
448
- clearFault(nodeId, { preserveProviderRetryEpisode: true });
449
- const bootFault = beginBootFaultAttempt(nodeId);
450
- // A cron armed --cancel-on-wake belongs to the dormancy being left, so a WAKE
451
- // of its anchor deletes it — an inbox delivery, an attach, an explicit
452
- // revive. A recovery relaunch is not a wake: the broker instance died or was
453
- // torn down and the daemon is putting the same waiting node back on its feet,
454
- // with nothing external having arrived. The wait the deadline bounds is still
455
- // open, so the deadline survives — the same rule lifecycle.ts applies to the
456
- // `crash` transition (a deadline MUST survive instance death).
457
- if (opts.recovery !== true)
458
- cancelCronsOnWake(nodeId);
459
- ensureNodeDirs(nodeId);
460
- clearTransportPoisonVerdict(nodeId);
461
- const lifecycleHooks = discoverLifecycleHookRegistry(meta.cwd, meta.profile_id).plans.get('node:start') ?? [];
462
- await invokeLifecycleHooks(lifecycleHooks, {
463
- node: {
464
- id: nodeId,
465
- name: meta.name,
466
- kind: meta.kind,
467
- mode: meta.mode,
468
- lifecycle: meta.lifecycle,
469
- cwd: meta.cwd,
470
- nodeDir: nodeDir(nodeId),
471
- contextDir: contextDir(nodeId),
472
- jobDir: jobDir(nodeId),
473
- reportsDir: reportsDir(nodeId),
474
- },
475
- runtime: {
476
- isBirth,
477
- canvasHome: crtrHome(),
478
- profile: meta.profile_id ?? null,
479
- },
480
- });
481
- let launched;
314
+ const executionId = beginExecution(nodeId, { daemonEpoch: boundFleet().epoch() });
482
315
  try {
483
- launched = headlessBrokerHost.launch(nodeId, inv, {
484
- cwd: meta.cwd,
485
- name: fullName(meta),
486
- resuming,
316
+ // Every (re)launch is a new cycle — bump the counter so CRTR_CYCLES and the
317
+ // footer stay in sync. Mutate the in-memory meta too so buildPiArgv below
318
+ // launches with the incremented count.
319
+ meta.cycles = (meta.cycles ?? 0) + 1;
320
+ updateNode(nodeId, { cycles: meta.cycles });
321
+ // Ordinary nodes choose resume/cycle/fresh from their recorded session.
322
+ // Persisted pending-refresh state (durable across a pre-session_start
323
+ // crash-retry) outranks a caller's strict-resume request. A parked node now
324
+ // resumes its transcript too: its isolated parking turn never entered the
325
+ // main session, so wake mail continues one intact conversation without a
326
+ // fresh-cycle handoff. resumeArgs then selects only an existing absolute
327
+ // session path: strict resume continues its leaf, while refresh/cycle mode
328
+ // roots a new one.
329
+ const refreshPending = meta.intent === 'refresh' || meta.cycle_pending === true;
330
+ const effectiveResume = refreshPending ? false : opts.resume;
331
+ const resume = forkBirthPending ? {} : resumableSessionArgs(nodeId, meta, effectiveResume);
332
+ // hasSessionPath: there is a real `.jsonl` to hand pi via `--session`, whether
333
+ // as a true resume OR a cycling refresh-yield (resumeArgs sets `newCycle` in
334
+ // the latter case). cycling and resuming are mutually exclusive; `resuming`
335
+ // keeps the STRICT true-resume-only meaning `ReviveResult.resumed` and the
336
+ // launch-placement hint below have always carried.
337
+ const hasSessionPath = resume.resumeSessionPath !== undefined;
338
+ const cycling = hasSessionPath && resume.newCycle === true;
339
+ const resuming = hasSessionPath && !cycling;
340
+ // Never conversed and nothing on disk to rebuild from — relaunch as a birth
341
+ // (see isUnstartedBirth). A clock-driven wake must keep its <crtr-wake>
342
+ // kickoff — the schedule IS the instruction to take a turn, so it never
343
+ // degrades to a birth replay. This one decision selects the kickoff below
344
+ // and is what the node:start hook payload reports as `isBirth`: a hook is
345
+ // told "birth" exactly when the node boots as one.
346
+ const birthReplay = !hasSessionPath
347
+ && (opts.wakeReason === undefined || opts.wakeReason === 'runtime-restart-abort')
348
+ && isUnstartedBirth(nodeId);
349
+ const isBirth = forkBirthPending || birthReplay;
350
+ // A cycling launch is provisional until session_start confirms it. Once
351
+ // true, this marker is monotonic across failed retries; only that confirmed
352
+ // boot may clear it.
353
+ const cyclePending = meta.cycle_pending === true || cycling;
354
+ updateNode(nodeId, { cycle_pending: cyclePending });
355
+ meta.cycle_pending = cyclePending;
356
+ // A truly fresh launch — no `.jsonl` to resume OR cycle from —
357
+ // starts a NEW pi session, so the old session identity is dead. Clear it from meta NOW,
358
+ // before launch, so the recorded state matches what we're about to do. This
359
+ // is load-bearing for exit-policy classification (classifyDeadNode row 8):
360
+ // if this fresh broker dies BEFORE session_start (recording no new session),
361
+ // the exit-policy job reads pi_session_id=NULL → boot-failure, the correct
362
+ // terminal outcome (transition('crash') + surfaceBootFailure) — a leftover
363
+ // stale id would instead be misread as a resumable session and retried. A
364
+ // cycling refresh keeps BOTH ids — the `.jsonl` and its recorded session id
365
+ // are exactly what's being reused — so a cycling broker that dies
366
+ // pre-session_start classifies as a retryable respawn, same class as a
367
+ // resume revive.
368
+ if (!forkBirthPending && !hasSessionPath && (meta.pi_session_id != null || meta.pi_session_file != null)) {
369
+ unbindSession(nodeId);
370
+ meta.pi_session_id = null;
371
+ meta.pi_session_file = null;
372
+ }
373
+ // A true resume needs no kickoff — the saved conversation just continues. A
374
+ // fresh revive OR a cycling refresh-yield both get one so the (re-rooted)
375
+ // window re-reads its roadmap and continues: drainBearings captures the
376
+ // yield note and consumes the feed/persona acknowledgements; the builder is
377
+ // then pure. `resume` carries `resumeSessionPath`/`newCycle` through
378
+ // unchanged (empty for true-fresh, both set for cycling) so buildPiArgv emits
379
+ // the right argv either way.
380
+ let inv;
381
+ // The provenance of whatever kickoff this branch composes, handed to the
382
+ // broker process through inv.env below. A promptless launch announces
383
+ // nothing — there is no message to stamp.
384
+ let kickoffOrigin = null;
385
+ const birthOrigin = () => ({
386
+ type: 'runtime',
387
+ kind: 'kickoff',
388
+ ...(meta.spawned_by != null && meta.spawned_by !== '' ? { spawnedBy: meta.spawned_by } : {}),
487
389
  });
488
- if (launched.pid === null) {
489
- throw new Error(`broker host returned no pid for ${nodeId}`);
390
+ if (forkBirthPending) {
391
+ const goal = readGoal(nodeId);
392
+ inv = buildPiArgv(meta, {
393
+ forkFrom: forkSourceFile,
394
+ ...(goal != null && goal !== '' ? { prompt: goal } : {}),
395
+ });
396
+ if (goal != null && goal !== '')
397
+ kickoffOrigin = birthOrigin();
398
+ initializeForkContextExposure(nodeId, findNodeBySessionFile(forkSourceFile)?.node_id ?? null);
490
399
  }
491
- }
492
- catch (error) {
400
+ else if (resuming) {
401
+ inv = buildPiArgv(meta, {
402
+ ...resume,
403
+ ...(opts.wakeReason === 'runtime-restart-abort' ? { prompt: RUNTIME_RESTART_CONTINUATION } : {}),
404
+ });
405
+ if (opts.wakeReason === 'runtime-restart-abort') {
406
+ kickoffOrigin = { type: 'runtime', kind: 'restart-continuation' };
407
+ }
408
+ }
409
+ else if (birthReplay) {
410
+ // A node with a spawn mandate replays it; a bare interactive node boots
411
+ // promptless and takes no turn at all.
412
+ const goal = readGoal(nodeId)?.trim();
413
+ inv = buildPiArgv(meta, goal !== undefined && goal !== '' ? { prompt: goal } : {});
414
+ if (goal !== undefined && goal !== '')
415
+ kickoffOrigin = birthOrigin();
416
+ clearContextExposureState(nodeId);
417
+ }
418
+ else {
419
+ // Fresh or cycling: drain the one-shot bearings, then build the kickoff.
420
+ // When a scheduled bare self-alarm drove this revive, opts.wakeReason
421
+ // carries the wake provenance so the kickoff leads with a <crtr-wake> block
422
+ // ("a timer woke you"); every other reviveNode caller passes nothing → no
423
+ // block.
424
+ const bearings = drainBearings(meta, executionId);
425
+ inv = buildPiArgv(meta, {
426
+ ...resume,
427
+ prompt: buildReviveKickoff(meta, bearings, opts.wakeReason === 'runtime-restart-abort' ? undefined : opts.wakeReason),
428
+ });
429
+ kickoffOrigin = opts.wakeReason !== undefined && opts.wakeReason !== 'runtime-restart-abort'
430
+ ? { type: 'runtime', kind: 'wake', wake: opts.wakeReason }
431
+ : { type: 'runtime', kind: 'revive', cycle: meta.cycles ?? 0 };
432
+ clearContextExposureState(nodeId);
433
+ }
434
+ // Every lifecycle launch emits the authoritative spine root rather than
435
+ // inheriting whichever node happened to invoke reviveNode.
436
+ inv.env = { ...inv.env, CRTR_SUBTREE: rootOfSpine(nodeId) };
437
+ if (kickoffOrigin !== null)
438
+ inv.env[KICKOFF_ORIGIN_ENV] = encodeKickoffOrigin(kickoffOrigin);
439
+ // The broker host launches the detached engine. reviveNode keeps
440
+ // transition+clearPid around it (the crash-safety ordering, unchanged). It
441
+ // opens NO viewer — engine-only; existing viewers reconnect, and a viewer-less
442
+ // node is brought on screen by the next `focus`.
443
+ transition(nodeId, meta.intent === 'refresh' ? 'refresh-launch' : 'revive');
444
+ // Clear any active fault the instant this node is (re)launched — don't wait
445
+ // for the fresh agent_start. A revive (auto or on-demand) is the recovery
446
+ // this marker advertised. Snapshot the boot-fault attempt in the SAME
447
+ // breath (see beginBootFaultAttempt): its own verification, not this call's
448
+ // unchecked result, is what scopes a later diagnostic read to this attempt.
449
+ clearFault(nodeId, { preserveEpisode: true });
450
+ const bootFault = beginBootFaultAttempt(nodeId);
451
+ // A cron armed --cancel-on-wake belongs to the dormancy being left, so a WAKE
452
+ // of its anchor deletes it — an inbox delivery, an attach, an explicit
453
+ // revive. A recovery relaunch is not a wake: the broker instance died or was
454
+ // torn down and the daemon is putting the same waiting node back on its feet,
455
+ // with nothing external having arrived. The wait the deadline bounds is still
456
+ // open, so the deadline survives — the same rule lifecycle.ts applies to the
457
+ // `crash` transition (a deadline MUST survive instance death).
458
+ if (opts.recovery !== true)
459
+ cancelCronsOnWake(nodeId);
460
+ ensureNodeDirs(nodeId);
461
+ const lifecycleHooks = discoverLifecycleHookRegistry(meta.cwd, meta.profile_id).plans.get('node:start') ?? [];
462
+ await invokeLifecycleHooks(lifecycleHooks, {
463
+ node: {
464
+ id: nodeId,
465
+ name: meta.name,
466
+ kind: meta.kind,
467
+ mode: meta.mode,
468
+ lifecycle: meta.lifecycle,
469
+ cwd: meta.cwd,
470
+ nodeDir: nodeDir(nodeId),
471
+ contextDir: contextDir(nodeId),
472
+ jobDir: jobDir(nodeId),
473
+ reportsDir: reportsDir(nodeId),
474
+ },
475
+ runtime: {
476
+ isBirth,
477
+ canvasHome: crtrHome(),
478
+ profile: meta.profile_id ?? null,
479
+ },
480
+ });
481
+ inv.env = { ...inv.env, CRTR_EXECUTION_ID: executionId };
482
+ let launched;
493
483
  try {
494
- transition(nodeId, 'crash', {
495
- reason: 'launch_failed',
496
- outcome: {
497
- detail: {
498
- schema: 'crtr.node-outcome-detail/v1',
499
- message: error instanceof Error ? error.message : String(error),
500
- },
501
- },
484
+ launched = headlessBrokerHost.launch(nodeId, inv, {
485
+ cwd: meta.cwd,
486
+ name: fullName(meta),
487
+ resuming,
502
488
  });
503
- // Doctrine wake: a launch-refusal here marks the node dead with NOTHING
504
- // else fanning that out — the daemon's own auto-revive is the most common
505
- // caller (superviseTick, best-effort try/catch, stderr-only on failure), so
506
- // without this a manager waiting dormant on this child hangs forever,
507
- // never told it will not come back on its own. Mirrors closeNode's step-4
508
- // fan-out (same subscribers table, same active/passive split).
509
- fanDoctrineWake(nodeId, subscribersOf(nodeId), `Child crashed — ${fullName(meta)} (${nodeId}) failed to relaunch and is now dead: ${error.message}\n\n` +
510
- `It stays dead until you revive it — \`crtr node lifecycle revive ${nodeId}\`.`, { reason: 'child-crashed', child: nodeId });
489
+ if (launched.pid === null) {
490
+ throw new Error(`broker host returned no pid for ${nodeId}`);
491
+ }
492
+ }
493
+ catch (error) {
494
+ try {
495
+ transition(nodeId, 'crash', {
496
+ reason: 'launch_failed',
497
+ outcome: {
498
+ detail: {
499
+ schema: 'crtr.node-outcome-detail/v1',
500
+ message: error instanceof Error ? error.message : String(error),
501
+ },
502
+ },
503
+ });
504
+ // Doctrine wake: a launch-refusal here marks the node dead with NOTHING
505
+ // else fanning that out — the daemon's own auto-revive is the most common
506
+ // caller (superviseTick, best-effort try/catch, stderr-only on failure), so
507
+ // without this a manager waiting dormant on this child hangs forever,
508
+ // never told it will not come back on its own. Mirrors closeNode's step-4
509
+ // fan-out (same subscribers table, same active/passive split).
510
+ fanDoctrineWake(nodeId, subscribersOf(nodeId), `Child crashed — ${fullName(meta)} (${nodeId}) failed to relaunch and is now dead: ${error.message}\n\n` +
511
+ `It stays dead until you revive it — \`crtr node lifecycle revive ${nodeId}\`.`, { reason: 'child-crashed', child: nodeId });
512
+ }
513
+ catch {
514
+ /* best-effort cleanup */
515
+ }
516
+ abortExecution(executionId, { reason: 'launch_failed' });
517
+ throw new Error(`[broker] refusing to revive ${nodeId}: ${error.message}`);
511
518
  }
512
- catch {
513
- /* best-effort cleanup */
519
+ recordLaunch(nodeId, new Date().toISOString());
520
+ // The execution row owns the broker pid. A stale broker that reaches the
521
+ // daemon after its execution was superseded is stopped rather than claiming
522
+ // the current node process slot.
523
+ if (recordExecutionPid(executionId, nodeId, launched.pid) === 'shutdown') {
524
+ abortExecution(executionId, { reason: 'superseded' });
525
+ headlessBrokerHost.teardown(nodeId);
526
+ throw new Error(`[broker] refusing to revive ${nodeId}: execution was superseded during launch`);
514
527
  }
515
- clearPid(nodeId);
516
- throw new Error(`[broker] refusing to revive ${nodeId}: ${error.message}`);
528
+ // A launch is the definition of not-frozen, and every launcher passes
529
+ // through here — so this is the one place the wait mark is retired.
530
+ clearFrozen(nodeId);
531
+ return {
532
+ window: null,
533
+ session: meta.tmux_session ?? null,
534
+ resumed: resuming,
535
+ launch: launched,
536
+ faultExplanation: bootFault.explanation,
537
+ outcome: 'launched',
538
+ };
539
+ }
540
+ catch (error) {
541
+ abortExecution(executionId, { reason: 'launch_failed' });
542
+ throw error;
517
543
  }
518
- recordLaunch(nodeId, new Date().toISOString());
519
- // Record the real broker pid. The pid is METADATA now (design D-13) — the
520
- // liveness authority is the fleet entry registered synchronously inside
521
- // `launch` above — but session_start re-records it after extension bind,
522
- // and status surfaces (canvas views, the daemon's identity rechecks at
523
- // signal boundaries) still read it.
524
- recordPid(nodeId, launched.pid);
525
- // A launch is the definition of not-frozen, and every launcher passes
526
- // through here — so this is the one place the wait mark is retired.
527
- clearFrozen(nodeId);
528
- return {
529
- window: null,
530
- session: meta.tmux_session ?? null,
531
- resumed: resuming,
532
- launch: launched,
533
- faultExplanation: bootFault.explanation,
534
- outcome: 'launched',
535
- };
536
544
  }
@@ -22,13 +22,13 @@ import { appendSituationalContext, formatSituationalProse } from './situational-
22
22
  import { hasRoadmap, seedRoadmap } from './roadmap.js';
23
23
  import { buildWakeBearings } from './bearings.js';
24
24
  import { encodeKickoffOrigin, KICKOFF_ORIGIN_ENV } from './stamp/protocol.js';
25
- import { canonicalSessionFile, contextDir, crtrHome, findNodeBySessionFile, getNode, fullName, jobDir, nodeDir, recordPid, reportsDir, setFrozen } from '../canvas/index.js';
25
+ import { beginExecution, canonicalSessionFile, contextDir, crtrHome, findNodeBySessionFile, getNode, fullName, jobDir, nodeDir, recordExecutionPid, abortExecution, reportsDir, setFrozen } from '../canvas/index.js';
26
26
  import { boundFleet, brokerThresholdsForDaemon } from './fleet.js';
27
27
  import { emitEvent } from '../events/emit.js';
28
28
  import { openViewerWindow, focusOf, windowOfPane, } from './placement.js';
29
29
  import { waitForBrokerViewSocket } from './placement-tmux.js';
30
30
  import { transition } from './lifecycle.js';
31
- import { beginBootFaultAttempt } from './fault.js';
31
+ import { beginBootFaultAttempt } from '../canvas/recovery.js';
32
32
  import { piSessionsRoot } from './pi-vendored.js';
33
33
  import { headlessBrokerHost } from './host.js';
34
34
  import { ensureDaemon } from '../../daemon/manage.js';
@@ -390,6 +390,8 @@ export async function spawnChildPrepared(opts, beforeBrokerLaunch, afterNodeCrea
390
390
  return { node: getNode(meta.node_id), frozen: true, window: null, session: null };
391
391
  }
392
392
  const bootFault = beginBootFaultAttempt(meta.node_id);
393
+ const executionId = beginExecution(meta.node_id, { daemonEpoch: fleet.epoch() });
394
+ inv.env = { ...inv.env, CRTR_EXECUTION_ID: executionId };
393
395
  // Broker is the only host: launch the detached broker ENGINE. A managed child
394
396
  // gets NO viewer — spawning it returns once the engine is launched and a human
395
397
  // opens a viewer on demand via `crtr surface node focus` / surface attach. A --root gets
@@ -402,6 +404,10 @@ export async function spawnChildPrepared(opts, beforeBrokerLaunch, afterNodeCrea
402
404
  resuming: false,
403
405
  });
404
406
  }
407
+ catch (error) {
408
+ abortExecution(executionId, { reason: 'launch_failed' });
409
+ throw error;
410
+ }
405
411
  finally {
406
412
  fleet.releaseReservation(meta.node_id);
407
413
  }
@@ -411,10 +417,15 @@ export async function spawnChildPrepared(opts, beforeBrokerLaunch, afterNodeCrea
411
417
  // spawnNode). Mirrors the old window===null crash.
412
418
  if (placed.pid === null) {
413
419
  const error = new Error(`broker host returned no pid for ${meta.node_id}`);
420
+ abortExecution(executionId, { reason: 'launch_failed' });
414
421
  transition(meta.node_id, 'crash', { reason: 'launch_failed', outcome: launchFailureOutcome(error) });
415
422
  throw brokerLaunchFailed(`failed to launch the broker engine for ${meta.node_id} (${meta.name}) — the node was not started.`, `Inspect canonical event diagnostics with \`crtr sys logs --node ${meta.node_id}\`, then verify the pi engine can start in this environment.`);
416
423
  }
417
- recordPid(meta.node_id, placed.pid);
424
+ if (recordExecutionPid(executionId, meta.node_id, placed.pid) === 'shutdown') {
425
+ abortExecution(executionId, { reason: 'superseded' });
426
+ headlessBrokerHost.teardown(meta.node_id);
427
+ throw brokerLaunchFailed(`broker execution was superseded for ${meta.node_id}`, 'Retry spawning the node.');
428
+ }
418
429
  // A root created through `node new --root` must accept viewers before the
419
430
  // command returns because its caller may attach immediately. Only that root
420
431
  // waits — a managed child (and every cron-launched