@north-light/crouter 0.3.172 → 0.3.174

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 (673) hide show
  1. package/bin/runtime-selector.mjs +8 -1
  2. package/dist/api/__tests__/{client.test.js → serial/client.test.js} +2 -135
  3. package/dist/api/client.d.ts +12 -21
  4. package/dist/api/client.js +22 -31
  5. package/dist/api/dto/broker-ops.d.ts +158 -0
  6. package/dist/api/dto/broker-ops.js +6 -0
  7. package/dist/api/dto/crons.d.ts +6 -0
  8. package/dist/api/dto/human.d.ts +7 -1
  9. package/dist/api/dto/inbox.d.ts +1 -71
  10. package/dist/api/dto/inbox.js +1 -9
  11. package/dist/api/dto/nodes.d.ts +6 -7
  12. package/dist/api/dto/reviews.d.ts +1 -11
  13. package/dist/api/routes.d.ts +7 -4
  14. package/dist/api/routes.js +8 -5
  15. package/dist/build-root.js +3 -3
  16. package/dist/builtin-memory/04-base-worker.md +1 -4
  17. package/dist/builtin-memory/05-kinds/advisor/00-base.md +1 -5
  18. package/dist/builtin-memory/05-kinds/advisor/01-orchestrator.md +2 -7
  19. package/dist/builtin-memory/05-kinds/design/00-base.md +1 -6
  20. package/dist/builtin-memory/05-kinds/developer/00-base.md +1 -6
  21. package/dist/builtin-memory/05-kinds/explore/00-base.md +1 -4
  22. package/dist/builtin-memory/05-kinds/explore/01-orchestrator.md +1 -2
  23. package/dist/builtin-memory/05-kinds/general/00-base.md +1 -3
  24. package/dist/builtin-memory/05-kinds/plan/00-base.md +1 -6
  25. package/dist/builtin-memory/05-kinds/plan/reviewers/00-base.md +1 -2
  26. package/dist/builtin-memory/05-kinds/plan/reviewers/architecture-fit.md +1 -3
  27. package/dist/builtin-memory/05-kinds/plan/reviewers/code-smells.md +1 -3
  28. package/dist/builtin-memory/05-kinds/plan/reviewers/pattern-consistency.md +1 -3
  29. package/dist/builtin-memory/05-kinds/plan/reviewers/requirements-coverage.md +1 -3
  30. package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +1 -4
  31. package/dist/builtin-memory/05-kinds/review/companion/00-base.md +2 -3
  32. package/dist/builtin-memory/05-kinds/spec/00-base.md +1 -2
  33. package/dist/builtin-memory/advisor/council.md +1 -6
  34. package/dist/builtin-memory/design.md +2 -7
  35. package/dist/builtin-memory/development.md +9 -14
  36. package/dist/builtin-memory/internal/marketplaces.md +2 -6
  37. package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
  38. package/dist/builtin-memory/internal/plugins.md +2 -8
  39. package/dist/builtin-memory/internal/storage-tiers.md +1 -2
  40. package/dist/builtin-memory/testing.md +41 -0
  41. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/{provider-rotation.test.ts → serial/provider-rotation.test.ts} +106 -177
  42. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/claude-plugin-commands.ts +5 -15
  43. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/frontmatter-rules/index.ts +1 -28
  44. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +43 -88
  45. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +55 -0
  46. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +60 -0
  47. package/dist/cli.js +2 -2
  48. package/dist/clients/attach/__tests__/attach-remote-readonly.test.js +37 -131
  49. package/dist/clients/attach/__tests__/chat-view-snapshot-ordering.test.js +71 -152
  50. package/dist/clients/attach/__tests__/context-message.test.js +16 -174
  51. package/dist/clients/attach/__tests__/crtr-output.test.js +36 -2069
  52. package/dist/clients/attach/__tests__/diagram.test.js +2 -2
  53. package/dist/clients/attach/__tests__/edit-diff.test.js +0 -4
  54. package/dist/clients/attach/__tests__/file-review-focus.test.js +2 -1
  55. package/dist/clients/attach/__tests__/group-activity.test.js +40 -229
  56. package/dist/clients/attach/__tests__/oauth-dialog-lifecycle.test.js +26 -76
  57. package/dist/clients/attach/__tests__/prompt-preview.test.js +4 -70
  58. package/dist/clients/attach/__tests__/ref-autocomplete.test.js +11 -125
  59. package/dist/clients/attach/__tests__/titled-editor-preview.test.js +14 -205
  60. package/dist/clients/attach/command.d.ts +2 -1
  61. package/dist/clients/attach/command.js +81 -76
  62. package/dist/clients/attach/input/capabilities.d.ts +27 -0
  63. package/dist/clients/attach/input/capabilities.js +96 -0
  64. package/dist/clients/attach/input/controller.d.ts +20 -182
  65. package/dist/clients/attach/input/controller.js +128 -537
  66. package/dist/clients/attach/input/overlay-owner.d.ts +41 -0
  67. package/dist/clients/attach/input/overlay-owner.js +199 -0
  68. package/dist/clients/attach/overlays/file-review.d.ts +0 -4
  69. package/dist/clients/attach/overlays/file-review.js +13 -5
  70. package/dist/clients/attach/overlays/graph.js +2 -10
  71. package/dist/clients/attach/render/chat-view.d.ts +4 -3
  72. package/dist/clients/attach/render/chat-view.js +39 -33
  73. package/dist/clients/attach/render/group-activity.d.ts +1 -0
  74. package/dist/clients/attach/render/group-activity.js +20 -13
  75. package/dist/clients/attach/render/group-recap.js +12 -3
  76. package/dist/clients/attach/session/connection.js +9 -15
  77. package/dist/clients/attach/session/editor-inventory.d.ts +6 -5
  78. package/dist/clients/attach/session/editor-inventory.js +7 -8
  79. package/dist/clients/attach/session/file-links.d.ts +13 -0
  80. package/dist/clients/attach/session/file-links.js +107 -0
  81. package/dist/clients/attach/session/frame.d.ts +3 -0
  82. package/dist/clients/attach/session/frame.js +10 -1
  83. package/dist/clients/attach/session/frames.js +2 -3
  84. package/dist/clients/attach/session/input-wiring.d.ts +4 -1
  85. package/dist/clients/attach/session/input-wiring.js +15 -14
  86. package/dist/clients/attach/session/mouse.d.ts +6 -1
  87. package/dist/clients/attach/session/mouse.js +8 -5
  88. package/dist/clients/attach/session/profile-files.js +18 -12
  89. package/dist/clients/attach/session/reconnect.js +1 -1
  90. package/dist/clients/attach/session/state-sync.js +1 -2
  91. package/dist/clients/attach/session/surface-host.d.ts +3 -2
  92. package/dist/clients/attach/session/surface-host.js +4 -4
  93. package/dist/clients/attach/slash/dispatch.d.ts +8 -31
  94. package/dist/clients/attach/slash/dispatch.js +34 -81
  95. package/dist/clients/attach/viewer.js +820 -860
  96. package/dist/clients/conversation/projection.d.ts +79 -0
  97. package/dist/clients/conversation/projection.js +341 -0
  98. package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +159 -0
  99. package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +151 -0
  100. package/dist/clients/inbox/controller.d.ts +0 -4
  101. package/dist/clients/inbox/controller.js +0 -2
  102. package/dist/clients/inbox/review/__tests__/review-core.test.js +4 -293
  103. package/dist/clients/inbox/review/__tests__/teardown-notice.test.js +100 -0
  104. package/dist/clients/inbox/review/comments-client.d.ts +3 -2
  105. package/dist/clients/inbox/review/comments-client.js +12 -12
  106. package/dist/clients/inbox/review/companion-pane.d.ts +30 -0
  107. package/dist/clients/inbox/review/companion-pane.js +86 -0
  108. package/dist/clients/inbox/review/document-surface.d.ts +182 -0
  109. package/dist/clients/inbox/review/{document-region.js → document-surface.js} +252 -48
  110. package/dist/clients/inbox/review/frame.js +5 -4
  111. package/dist/clients/inbox/review/launch.d.ts +8 -8
  112. package/dist/clients/inbox/review/launch.js +52 -92
  113. package/dist/clients/inbox/review/nvim.d.ts +2 -0
  114. package/dist/clients/inbox/review/nvim.js +14 -12
  115. package/dist/clients/inbox/review/review-client.d.ts +12 -8
  116. package/dist/clients/inbox/review/review-client.js +46 -35
  117. package/dist/clients/inbox/review-adapter.d.ts +3 -10
  118. package/dist/clients/inbox/review-adapter.js +27 -19
  119. package/dist/clients/inbox/surface.d.ts +0 -4
  120. package/dist/clients/inbox/surface.js +0 -2
  121. package/dist/clients/surfaces/host.d.ts +11 -3
  122. package/dist/commands/__tests__/api-client-cold-start-diagnostic.test.js +7 -34
  123. package/dist/commands/__tests__/human.test.js +11 -322
  124. package/dist/commands/api-client.d.ts +5 -5
  125. package/dist/commands/api-client.js +6 -85
  126. package/dist/commands/canvas-browse.js +3 -29
  127. package/dist/commands/canvas-config.js +1 -1
  128. package/dist/commands/canvas-use.js +3 -3
  129. package/dist/commands/canvas.js +1 -1
  130. package/dist/commands/cron.js +5 -1
  131. package/dist/commands/human/queue.d.ts +0 -4
  132. package/dist/commands/human/queue.js +7 -40
  133. package/dist/commands/memory/list.d.ts +14 -0
  134. package/dist/commands/memory/list.js +35 -31
  135. package/dist/commands/memory/read.d.ts +7 -0
  136. package/dist/commands/memory/read.js +14 -8
  137. package/dist/commands/node/bash.d.ts +1 -0
  138. package/dist/commands/node/bash.js +198 -0
  139. package/dist/commands/node/create.d.ts +23 -0
  140. package/dist/commands/node/create.js +207 -0
  141. package/dist/commands/node/inspect.d.ts +2 -0
  142. package/dist/commands/node/inspect.js +302 -0
  143. package/dist/commands/node/lifecycle.d.ts +4 -0
  144. package/dist/commands/node/lifecycle.js +415 -0
  145. package/dist/commands/node/message.d.ts +1 -0
  146. package/dist/commands/node/message.js +164 -0
  147. package/dist/commands/node/pyramid.d.ts +2 -0
  148. package/dist/commands/node/pyramid.js +79 -0
  149. package/dist/commands/node/subscription.d.ts +1 -0
  150. package/dist/commands/node/subscription.js +82 -0
  151. package/dist/commands/node/wait.d.ts +1 -0
  152. package/dist/commands/node/wait.js +159 -0
  153. package/dist/commands/node-lifecycle-revive.js +10 -57
  154. package/dist/commands/node.d.ts +0 -42
  155. package/dist/commands/node.js +11 -2065
  156. package/dist/commands/search.js +1 -1
  157. package/dist/commands/surface/node/cycle.d.ts +1 -0
  158. package/dist/commands/surface/node/cycle.js +83 -0
  159. package/dist/commands/surface/node/focus.d.ts +1 -0
  160. package/dist/commands/surface/node/focus.js +67 -0
  161. package/dist/commands/surface/node/id.d.ts +1 -0
  162. package/dist/commands/surface/node/id.js +38 -0
  163. package/dist/commands/surface/node/pin.d.ts +1 -0
  164. package/dist/commands/surface/node/pin.js +42 -0
  165. package/dist/commands/surface/node/placement.d.ts +46 -0
  166. package/dist/commands/surface/node/placement.js +124 -0
  167. package/dist/commands/surface-node.js +4 -1
  168. package/dist/commands/surface-reload.js +1 -2
  169. package/dist/commands/surface-tmux-spread.js +1 -1
  170. package/dist/commands/surface.js +5 -20
  171. package/dist/commands/sys/__tests__/setup-core.test.js +18 -271
  172. package/dist/commands/sys/config.js +6 -6
  173. package/dist/commands/sys/logs.js +1 -1
  174. package/dist/commands/sys/panels/broker-limits-panel.d.ts +17 -0
  175. package/dist/commands/sys/panels/broker-limits-panel.js +104 -0
  176. package/dist/commands/sys/panels/dependencies-panel.d.ts +14 -0
  177. package/dist/commands/sys/panels/dependencies-panel.js +80 -0
  178. package/dist/commands/sys/panels/keybindings-panel.d.ts +56 -0
  179. package/dist/commands/sys/panels/keybindings-panel.js +703 -0
  180. package/dist/commands/sys/panels/keys-panel.d.ts +16 -0
  181. package/dist/commands/sys/panels/keys-panel.js +60 -0
  182. package/dist/commands/sys/panels/models-panel.d.ts +77 -0
  183. package/dist/commands/sys/panels/models-panel.js +647 -0
  184. package/dist/commands/sys/panels/panel.d.ts +45 -0
  185. package/dist/commands/sys/panels/panel.js +137 -0
  186. package/dist/commands/sys/panels/plugins-panel.d.ts +16 -0
  187. package/dist/commands/sys/panels/plugins-panel.js +80 -0
  188. package/dist/commands/sys/panels/prefs-panel.d.ts +21 -0
  189. package/dist/commands/sys/panels/prefs-panel.js +85 -0
  190. package/dist/commands/sys/panels/provider-panel.d.ts +62 -0
  191. package/dist/commands/sys/panels/provider-panel.js +538 -0
  192. package/dist/commands/sys/settings-shell.d.ts +55 -0
  193. package/dist/commands/sys/settings-shell.js +400 -0
  194. package/dist/commands/sys/settings.d.ts +2 -0
  195. package/dist/commands/sys/settings.js +31 -0
  196. package/dist/commands/sys/setup-core.d.ts +3 -33
  197. package/dist/commands/sys/setup-core.js +27 -78
  198. package/dist/commands/sys/setup-settings-runner.d.ts +24 -0
  199. package/dist/commands/sys/setup-settings-runner.js +63 -0
  200. package/dist/commands/sys/setup-wizard.d.ts +26 -125
  201. package/dist/commands/sys/setup-wizard.js +209 -2473
  202. package/dist/commands/sys/setup.js +5 -7
  203. package/dist/commands/sys.js +3 -2
  204. package/dist/core/__tests__/broker-extension-canvas-db-boundary.test.js +50 -0
  205. package/dist/core/__tests__/broker-launch-candidates.test.js +20 -9
  206. package/dist/core/__tests__/broker-preflight.test.js +1 -1
  207. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +108 -27
  208. package/dist/core/__tests__/canvas-inbox-watcher.test.js +14 -0
  209. package/dist/core/__tests__/canvas.test.js +2 -36
  210. package/dist/core/__tests__/close.test.js +14 -3
  211. package/dist/core/__tests__/context-intro.test.js +32 -143
  212. package/dist/core/__tests__/cron-broker-capacity.test.js +22 -3
  213. package/dist/core/__tests__/daemon-boot.test.js +2 -20
  214. package/dist/core/__tests__/daemon-wedge.test.js +1 -1
  215. package/dist/core/__tests__/dead-node-policy-table.test.js +1 -18
  216. package/dist/core/__tests__/fixtures/fake-engine.d.ts +1 -1
  217. package/dist/core/__tests__/fixtures/fake-engine.js +2 -2
  218. package/dist/core/__tests__/fixtures/fake-pi-host.js +2 -2
  219. package/dist/core/__tests__/focuses.test.js +4 -98
  220. package/dist/core/__tests__/helpers/broker-clients.d.ts +0 -3
  221. package/dist/core/__tests__/helpers/broker-clients.js +0 -14
  222. package/dist/core/__tests__/helpers/harness.d.ts +0 -8
  223. package/dist/core/__tests__/helpers/harness.js +7 -87
  224. package/dist/core/__tests__/helpers/viewer-panes.js +1 -1
  225. package/dist/core/__tests__/inline-memory-refs.test.js +5 -134
  226. package/dist/core/__tests__/kickoff.test.js +1 -1
  227. package/dist/core/__tests__/memory-resolver-precedence.test.js +29 -1
  228. package/dist/core/__tests__/migration.test.js +35 -48
  229. package/dist/core/__tests__/on-read-dedup-resume.test.js +22 -12
  230. package/dist/core/__tests__/passive-subscription.test.js +26 -49
  231. package/dist/core/__tests__/preview-registry-sync.test.js +1 -2
  232. package/dist/core/__tests__/prune.test.js +94 -1
  233. package/dist/core/__tests__/review-model-floor.test.js +0 -32
  234. package/dist/core/__tests__/seam/broker-attach-multiclient.test.js +255 -0
  235. package/dist/core/__tests__/{full → seam}/broker-attach-stream.test.js +39 -7
  236. package/dist/core/__tests__/{full → seam}/broker-crash-teardown.test.js +30 -29
  237. package/dist/core/__tests__/{full → seam}/broker-pane-resolution.test.js +54 -5
  238. package/dist/core/__tests__/{full → seam}/broker-provider-retry.test.js +134 -82
  239. package/dist/core/__tests__/{full → seam}/broker-whip-interrupt.test.js +15 -4
  240. package/dist/core/__tests__/{full/idle-release-settlement.test.js → seam/dormancy-release.test.js} +69 -17
  241. package/dist/core/__tests__/{full/yield-message-transaction.test.js → seam/yield-refresh-transaction.test.js} +31 -9
  242. package/dist/core/__tests__/secrets.test.js +1 -7
  243. package/dist/core/__tests__/{broker-fork-seam.test.js → serial/broker-fork-seam.test.js} +58 -7
  244. package/dist/core/__tests__/{broker-sdk-wiring.test.js → serial/broker-sdk-wiring.test.js} +18 -162
  245. package/dist/core/__tests__/{broker-snapshot-history.test.js → serial/broker-snapshot-history.test.js} +9 -9
  246. package/dist/core/__tests__/{command-plugins.test.js → serial/command-plugins.test.js} +105 -15
  247. package/dist/core/__tests__/{deferred-no-wake.test.js → serial/deferred-no-wake.test.js} +23 -20
  248. package/dist/core/__tests__/{flagship-lifecycle.test.js → serial/flagship-lifecycle.test.js} +7 -7
  249. package/dist/core/__tests__/{host-teardown-process-group.test.js → serial/host-teardown-process-group.test.js} +6 -6
  250. package/dist/core/__tests__/{human-deliver-e2e.test.js → serial/human-deliver-e2e.test.js} +12 -12
  251. package/dist/core/__tests__/{live-mutation.test.js → serial/live-mutation.test.js} +5 -5
  252. package/dist/core/__tests__/{refresh-stall-recycle.test.js → serial/refresh-stall-recycle.test.js} +5 -14
  253. package/dist/core/__tests__/{revive.test.js → serial/revive.test.js} +15 -100
  254. package/dist/core/__tests__/{spawn-root.test.js → serial/spawn-root.test.js} +32 -15
  255. package/dist/core/__tests__/serial/subscription-delivery.test.js +126 -0
  256. package/dist/core/__tests__/serial/tmux-surface.test.js +256 -0
  257. package/dist/core/__tests__/{worktree.test.js → serial/worktree.test.js} +15 -15
  258. package/dist/core/__tests__/session-model.test.js +0 -18
  259. package/dist/core/__tests__/spawn-worktree-id-conflict.test.js +9 -0
  260. package/dist/core/__tests__/watchdog-abort-arms-retry.test.js +50 -0
  261. package/dist/core/broker-client/__tests__/transport-relay.test.js +15 -16
  262. package/dist/core/broker-client/__tests__/transport-socket.test.js +28 -0
  263. package/dist/core/broker-client/client.d.ts +2 -2
  264. package/dist/core/broker-client/client.js +4 -7
  265. package/dist/core/broker-client/transport-relay.d.ts +13 -19
  266. package/dist/core/broker-client/transport-relay.js +19 -34
  267. package/dist/core/broker-client/transport-socket.d.ts +2 -2
  268. package/dist/core/broker-client/transport-socket.js +9 -2
  269. package/dist/core/broker-client/transport.d.ts +7 -3
  270. package/dist/core/broker-client/transport.js +4 -4
  271. package/dist/core/canvas/__tests__/attention.test.js +1 -18
  272. package/dist/core/canvas/__tests__/remote-canvas-source.test.js +7 -64
  273. package/dist/core/canvas/__tests__/remote-transport.test.js +2 -3
  274. package/dist/core/canvas/__tests__/render-remote.test.js +7 -52
  275. package/dist/core/canvas/browse/app.d.ts +2 -17
  276. package/dist/core/canvas/browse/app.js +16 -88
  277. package/dist/core/canvas/browse/model.d.ts +0 -4
  278. package/dist/core/canvas/browse/model.js +1 -12
  279. package/dist/core/canvas/browse/render.js +6 -3
  280. package/dist/core/canvas/crons.d.ts +8 -5
  281. package/dist/core/canvas/crons.js +7 -6
  282. package/dist/core/canvas/extensions.d.ts +7 -3
  283. package/dist/core/canvas/extensions.js +16 -4
  284. package/dist/core/canvas/nav-model.d.ts +0 -4
  285. package/dist/core/canvas/nav-model.js +2 -14
  286. package/dist/core/canvas/node-order.d.ts +3 -0
  287. package/dist/core/canvas/node-order.js +12 -0
  288. package/dist/core/canvas/remote-canvas-source.d.ts +3 -6
  289. package/dist/core/canvas/remote-canvas-source.js +8 -15
  290. package/dist/core/command.js +11 -4
  291. package/dist/core/config.js +3 -1
  292. package/dist/core/events/envelope.js +1 -1
  293. package/dist/core/events/read.js +7 -21
  294. package/dist/core/events/source.d.ts +0 -2
  295. package/dist/core/events/source.js +0 -16
  296. package/dist/core/events/types.d.ts +1 -1
  297. package/dist/core/fault-classifier.d.ts +1 -1
  298. package/dist/core/feed/passive.d.ts +0 -11
  299. package/dist/core/feed/passive.js +1 -62
  300. package/dist/core/human/__tests__/{inbox-core.test.js → serial/inbox-core.test.js} +6 -6
  301. package/dist/core/keybindings/__tests__/resolve.test.js +1 -96
  302. package/dist/core/keybindings/background-bash.js +3 -4
  303. package/dist/core/keybindings/catalog.js +1 -1
  304. package/dist/core/memory/inline-ref-grammar.js +3 -3
  305. package/dist/core/memory-resolver.d.ts +11 -25
  306. package/dist/core/memory-resolver.js +51 -33
  307. package/dist/core/model-routes.d.ts +15 -0
  308. package/dist/core/model-routes.js +12 -0
  309. package/dist/core/pool-credential-store.js +1 -1
  310. package/dist/core/preview-registry.js +8 -9
  311. package/dist/core/profiles/__tests__/root-profile.test.js +4 -14
  312. package/dist/core/review/realize.d.ts +0 -13
  313. package/dist/core/review/realize.js +0 -40
  314. package/dist/core/review/types.d.ts +1 -1
  315. package/dist/core/runtime/__tests__/engine-route.test.js +2 -2
  316. package/dist/core/runtime/bearings.js +1 -11
  317. package/dist/core/runtime/broker/client-registry.d.ts +33 -0
  318. package/dist/core/runtime/broker/client-registry.js +164 -0
  319. package/dist/core/runtime/broker/daemon-ops.d.ts +19 -0
  320. package/dist/core/runtime/broker/daemon-ops.js +49 -0
  321. package/dist/core/runtime/broker/event-projection.d.ts +42 -0
  322. package/dist/core/runtime/broker/event-projection.js +178 -0
  323. package/dist/core/runtime/broker/fault-retry.d.ts +39 -0
  324. package/dist/core/runtime/broker/fault-retry.js +277 -0
  325. package/dist/core/runtime/broker/frame-dispatch.d.ts +238 -0
  326. package/dist/core/runtime/broker/frame-dispatch.js +1537 -0
  327. package/dist/core/runtime/broker/inbox.d.ts +22 -0
  328. package/dist/core/runtime/broker/inbox.js +167 -0
  329. package/dist/core/runtime/broker/passive.d.ts +16 -0
  330. package/dist/core/runtime/broker/passive.js +44 -0
  331. package/dist/core/runtime/broker/read-ops.d.ts +23 -0
  332. package/dist/core/runtime/broker/read-ops.js +150 -0
  333. package/dist/core/runtime/broker/rebind.d.ts +74 -0
  334. package/dist/core/runtime/broker/rebind.js +211 -0
  335. package/dist/core/runtime/broker/tool-groups.d.ts +39 -0
  336. package/dist/core/runtime/broker/tool-groups.js +244 -0
  337. package/dist/core/runtime/broker-extension-render.d.ts +11 -0
  338. package/dist/core/runtime/broker-extension-render.js +181 -0
  339. package/dist/core/runtime/broker-persona-guidance.d.ts +13 -0
  340. package/dist/core/runtime/broker-persona-guidance.js +58 -0
  341. package/dist/core/runtime/broker-protocol.d.ts +1 -1
  342. package/dist/core/runtime/broker-protocol.js +1 -1
  343. package/dist/core/runtime/broker.d.ts +5 -196
  344. package/dist/core/runtime/broker.js +276 -3059
  345. package/dist/core/runtime/canvas-extensions.js +11 -2
  346. package/dist/core/runtime/fault.js +1 -1
  347. package/dist/core/runtime/model-selection.d.ts +9 -0
  348. package/dist/core/runtime/model-selection.js +12 -0
  349. package/dist/core/runtime/model-swap.d.ts +3 -3
  350. package/dist/core/runtime/model-swap.js +33 -25
  351. package/dist/core/runtime/placement-tmux.d.ts +3 -14
  352. package/dist/core/runtime/placement-tmux.js +11 -80
  353. package/dist/core/runtime/placement.d.ts +4 -2
  354. package/dist/core/runtime/placement.js +7 -12
  355. package/dist/core/runtime/promote.d.ts +14 -1
  356. package/dist/core/runtime/promote.js +1 -1
  357. package/dist/core/runtime/reset.d.ts +1 -1
  358. package/dist/core/runtime/roadmap.js +1 -1
  359. package/dist/core/runtime/spawn.d.ts +8 -0
  360. package/dist/core/runtime/spawn.js +36 -6
  361. package/dist/core/runtime/stop-guard.d.ts +4 -3
  362. package/dist/core/runtime/stop-guard.js +36 -18
  363. package/dist/core/runtime/stop-signals.d.ts +4 -1
  364. package/dist/core/runtime/stream-watchdog.js +4 -5
  365. package/dist/core/runtime/structured-output.d.ts +15 -2
  366. package/dist/core/runtime/structured-output.js +35 -12
  367. package/dist/core/runtime/tmux-bindings.d.ts +25 -0
  368. package/dist/core/runtime/tmux-bindings.js +682 -0
  369. package/dist/core/runtime/tmux-chrome.d.ts +2 -1
  370. package/dist/core/runtime/tmux-chrome.js +3 -2
  371. package/dist/core/runtime/{tmux.d.ts → tmux-driver.d.ts} +18 -26
  372. package/dist/core/runtime/tmux-driver.js +598 -0
  373. package/dist/core/runtime/view-socket.d.ts +14 -0
  374. package/dist/core/runtime/view-socket.js +83 -0
  375. package/dist/core/session-model/session-state.d.ts +1 -2
  376. package/dist/core/session-model/session-state.js +5 -8
  377. package/dist/core/spawn.d.ts +1 -1
  378. package/dist/core/spawn.js +2 -2
  379. package/dist/core/substrate/gate.d.ts +1 -1
  380. package/dist/core/substrate/on-read-pure.d.ts +7 -0
  381. package/dist/core/substrate/on-read-pure.js +272 -0
  382. package/dist/core/termrender/termrender.js +32 -2
  383. package/dist/core/user-settings.d.ts +23 -1
  384. package/dist/core/user-settings.js +18 -1
  385. package/dist/daemon/api/__tests__/node-model-validation.test.js +134 -0
  386. package/dist/daemon/api/__tests__/review-terminal-conflict.test.js +98 -0
  387. package/dist/daemon/api/__tests__/{full → seam}/api-server.test.js +6 -48
  388. package/dist/daemon/api/__tests__/{full → seam}/b10-attach-modelauth.test.js +17 -44
  389. package/dist/daemon/api/__tests__/{full/push-parity.test.js → seam/leaf-api-parity.test.js} +61 -42
  390. package/dist/daemon/api/bridge.js +40 -81
  391. package/dist/daemon/api/handlers/broker-ops.d.ts +2 -0
  392. package/dist/daemon/api/handlers/broker-ops.js +383 -0
  393. package/dist/daemon/api/handlers/crons.js +9 -4
  394. package/dist/daemon/api/handlers/files.js +6 -7
  395. package/dist/daemon/api/handlers/inbox.js +2 -340
  396. package/dist/daemon/api/handlers/nodes.d.ts +10 -1
  397. package/dist/daemon/api/handlers/nodes.js +74 -39
  398. package/dist/daemon/api/handlers/review-comments.js +5 -1
  399. package/dist/daemon/api/handlers/reviews.js +0 -19
  400. package/dist/daemon/api/map.js +25 -4
  401. package/dist/daemon/api/router.js +2 -3
  402. package/dist/daemon/api/server.js +5 -7
  403. package/dist/daemon/cron-run.d.ts +4 -0
  404. package/dist/daemon/cron-run.js +30 -13
  405. package/dist/daemon/crtrd.d.ts +3 -58
  406. package/dist/daemon/crtrd.js +28 -1244
  407. package/dist/daemon/manage.js +0 -1
  408. package/dist/daemon/reconcilers/broker-supervision.d.ts +50 -0
  409. package/dist/daemon/reconcilers/broker-supervision.js +549 -0
  410. package/dist/daemon/reconcilers/cron-lane.d.ts +10 -0
  411. package/dist/daemon/reconcilers/cron-lane.js +32 -0
  412. package/dist/daemon/reconcilers/dormant-inbox.d.ts +18 -0
  413. package/dist/daemon/reconcilers/dormant-inbox.js +143 -0
  414. package/dist/daemon/reconcilers/storage-maintenance.d.ts +22 -0
  415. package/dist/daemon/reconcilers/storage-maintenance.js +213 -0
  416. package/dist/daemon/review/finish.js +2 -5
  417. package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +34 -53
  418. package/dist/pi-extensions/__tests__/canvas-recap.test.js +16 -113
  419. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +72 -99
  420. package/dist/pi-extensions/__tests__/canvas-stophook-context-nudge.test.js +29 -12
  421. package/dist/pi-extensions/broker-local.d.ts +20 -0
  422. package/dist/pi-extensions/broker-local.js +89 -0
  423. package/dist/pi-extensions/canvas-context-intro.d.ts +3 -3
  424. package/dist/pi-extensions/canvas-context-intro.js +22 -11
  425. package/dist/pi-extensions/canvas-doc-substrate.js +17 -34
  426. package/dist/pi-extensions/canvas-goal-capture.js +7 -12
  427. package/dist/pi-extensions/canvas-inbox-watcher.js +124 -139
  428. package/dist/pi-extensions/canvas-passive-context.d.ts +3 -3
  429. package/dist/pi-extensions/canvas-passive-context.js +22 -18
  430. package/dist/pi-extensions/canvas-recap.d.ts +2 -2
  431. package/dist/pi-extensions/canvas-recap.js +48 -21
  432. package/dist/pi-extensions/canvas-review-boundary.js +10 -4
  433. package/dist/pi-extensions/canvas-stophook.d.ts +0 -5
  434. package/dist/pi-extensions/canvas-stophook.js +149 -431
  435. package/dist/pi-extensions/canvas-structured-output.js +52 -30
  436. package/dist/pi-extensions/envelope-merge.d.ts +13 -0
  437. package/dist/pi-extensions/envelope-merge.js +29 -0
  438. package/dist/shared/working-activity.d.ts +3 -2
  439. package/dist/shared/working-activity.js +3 -2
  440. package/dist/types.d.ts +6 -5
  441. package/dist/types.js +2 -2
  442. package/docs/compat/hearth-crtr-v1.md +4 -12
  443. package/docs/compat/hearth-crtr-v2.md +3 -11
  444. package/docs/compat/hearth-crtr-v3.md +4 -12
  445. package/docs/compat/hearth-crtr-v4.md +3 -11
  446. package/docs/compat/hearth-crtr-v5.md +3 -11
  447. package/docs/public-api.md +3 -7
  448. package/package.json +7 -49
  449. package/runtime.lock.json +2766 -14533
  450. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/claude-plugin-commands.test.ts +0 -49
  451. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/memory-slash-commands.test.ts +0 -194
  452. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/statusline.test.ts +0 -68
  453. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/frontmatter-rules/test.ts +0 -119
  454. package/dist/clients/attach/__tests__/attach-chrome-remote.test.js +0 -439
  455. package/dist/clients/attach/__tests__/attach-keybindings.test.js +0 -170
  456. package/dist/clients/attach/__tests__/crtr-output-coverage.test.js +0 -57
  457. package/dist/clients/attach/__tests__/editor-frame-title.test.js +0 -19
  458. package/dist/clients/attach/__tests__/input-controller-extension-command.test.js +0 -32
  459. package/dist/clients/attach/__tests__/markdown-headings.test.js +0 -35
  460. package/dist/clients/attach/__tests__/session-identity-refresh.test.js +0 -32
  461. package/dist/clients/conversation/conversation.d.ts +0 -65
  462. package/dist/clients/conversation/conversation.js +0 -318
  463. package/dist/clients/conversation/frames.d.ts +0 -30
  464. package/dist/clients/conversation/frames.js +0 -52
  465. package/dist/clients/conversation/region.d.ts +0 -51
  466. package/dist/clients/conversation/region.js +0 -265
  467. package/dist/clients/inbox/__tests__/inbox-controller.test.js +0 -247
  468. package/dist/clients/inbox/__tests__/mount-panel.test.js +0 -776
  469. package/dist/clients/inbox/review/__tests__/remap.test.js +0 -196
  470. package/dist/clients/inbox/review/columns.d.ts +0 -22
  471. package/dist/clients/inbox/review/columns.js +0 -40
  472. package/dist/clients/inbox/review/composition.d.ts +0 -60
  473. package/dist/clients/inbox/review/composition.js +0 -278
  474. package/dist/clients/inbox/review/document-region.d.ts +0 -120
  475. package/dist/clients/surfaces/region.d.ts +0 -16
  476. package/dist/clients/web/__tests__/push-engine.test.js +0 -158
  477. package/dist/clients/web/__tests__/push-registry.test.d.ts +0 -1
  478. package/dist/clients/web/__tests__/push-registry.test.js +0 -177
  479. package/dist/clients/web/dev-server.d.ts +0 -7
  480. package/dist/clients/web/dev-server.js +0 -46
  481. package/dist/clients/web/events.d.ts +0 -19
  482. package/dist/clients/web/events.js +0 -163
  483. package/dist/clients/web/push-engine.d.ts +0 -44
  484. package/dist/clients/web/push-engine.js +0 -125
  485. package/dist/clients/web/push-registry.d.ts +0 -85
  486. package/dist/clients/web/push-registry.js +0 -373
  487. package/dist/clients/web/server.d.ts +0 -19
  488. package/dist/clients/web/server.js +0 -661
  489. package/dist/clients/web/web-client/shared/protocol.d.ts +0 -214
  490. package/dist/clients/web/web-client/shared/protocol.js +0 -8
  491. package/dist/clients/web/web-cmd.d.ts +0 -2
  492. package/dist/clients/web/web-cmd.js +0 -141
  493. package/dist/commands/__tests__/api-canvas-source.test.d.ts +0 -1
  494. package/dist/commands/__tests__/api-canvas-source.test.js +0 -37
  495. package/dist/commands/__tests__/canvas-config.test.d.ts +0 -1
  496. package/dist/commands/__tests__/canvas-config.test.js +0 -107
  497. package/dist/commands/__tests__/node-no-kickoff-flag.test.d.ts +0 -1
  498. package/dist/commands/__tests__/node-no-kickoff-flag.test.js +0 -73
  499. package/dist/commands/__tests__/node-worktree-flag.test.d.ts +0 -1
  500. package/dist/commands/__tests__/node-worktree-flag.test.js +0 -49
  501. package/dist/commands/__tests__/search-contents.test.d.ts +0 -1
  502. package/dist/commands/__tests__/search-contents.test.js +0 -52
  503. package/dist/commands/memory/__tests__/lint.test.d.ts +0 -1
  504. package/dist/commands/memory/__tests__/lint.test.js +0 -15
  505. package/dist/commands/sys/__tests__/config-broker-thresholds.test.d.ts +0 -1
  506. package/dist/commands/sys/__tests__/config-broker-thresholds.test.js +0 -95
  507. package/dist/commands/sys/__tests__/config-keybindings.test.d.ts +0 -1
  508. package/dist/commands/sys/__tests__/config-keybindings.test.js +0 -55
  509. package/dist/commands/sys/__tests__/helpers/setup-runtime.d.ts +0 -5
  510. package/dist/commands/sys/__tests__/helpers/setup-runtime.js +0 -19
  511. package/dist/commands/sys/__tests__/setup-front-door.test.d.ts +0 -1
  512. package/dist/commands/sys/__tests__/setup-front-door.test.js +0 -102
  513. package/dist/commands/sys/__tests__/setup-models.test.d.ts +0 -1
  514. package/dist/commands/sys/__tests__/setup-models.test.js +0 -205
  515. package/dist/commands/sys/__tests__/setup-providers-viewport.test.d.ts +0 -1
  516. package/dist/commands/sys/__tests__/setup-providers-viewport.test.js +0 -155
  517. package/dist/commands/sys/__tests__/sync-deps.test.d.ts +0 -1
  518. package/dist/commands/sys/__tests__/sync-deps.test.js +0 -179
  519. package/dist/commands/sys/__tests__/sync-import.test.d.ts +0 -1
  520. package/dist/commands/sys/__tests__/sync-import.test.js +0 -315
  521. package/dist/core/__tests__/base-worker-prompt.test.d.ts +0 -1
  522. package/dist/core/__tests__/base-worker-prompt.test.js +0 -57
  523. package/dist/core/__tests__/command-plugins-surfaces.test.d.ts +0 -1
  524. package/dist/core/__tests__/command-plugins-surfaces.test.js +0 -359
  525. package/dist/core/__tests__/connection-reconnect-resume.test.d.ts +0 -1
  526. package/dist/core/__tests__/connection-reconnect-resume.test.js +0 -36
  527. package/dist/core/__tests__/finalization-migration.test.d.ts +0 -1
  528. package/dist/core/__tests__/finalization-migration.test.js +0 -38
  529. package/dist/core/__tests__/fixtures/extra-extension.d.ts +0 -1
  530. package/dist/core/__tests__/fixtures/extra-extension.js +0 -3
  531. package/dist/core/__tests__/fixtures/spawn-proof-target.test.d.ts +0 -1
  532. package/dist/core/__tests__/fixtures/spawn-proof-target.test.js +0 -15
  533. package/dist/core/__tests__/fork.test.d.ts +0 -1
  534. package/dist/core/__tests__/fork.test.js +0 -108
  535. package/dist/core/__tests__/full/broker-attach-limits.test.d.ts +0 -1
  536. package/dist/core/__tests__/full/broker-attach-limits.test.js +0 -147
  537. package/dist/core/__tests__/full/broker-deliver-ack.test.d.ts +0 -1
  538. package/dist/core/__tests__/full/broker-deliver-ack.test.js +0 -73
  539. package/dist/core/__tests__/full/broker-dialogs.test.d.ts +0 -1
  540. package/dist/core/__tests__/full/broker-dialogs.test.js +0 -180
  541. package/dist/core/__tests__/full/broker-display-snapshot.test.d.ts +0 -1
  542. package/dist/core/__tests__/full/broker-display-snapshot.test.js +0 -75
  543. package/dist/core/__tests__/full/broker-dormant-wake.test.d.ts +0 -1
  544. package/dist/core/__tests__/full/broker-dormant-wake.test.js +0 -130
  545. package/dist/core/__tests__/full/broker-model-changed-broadcast.test.d.ts +0 -1
  546. package/dist/core/__tests__/full/broker-model-changed-broadcast.test.js +0 -57
  547. package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.d.ts +0 -1
  548. package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.js +0 -80
  549. package/dist/core/__tests__/full/cascade-close.test.d.ts +0 -1
  550. package/dist/core/__tests__/full/cascade-close.test.js +0 -240
  551. package/dist/core/__tests__/full/detach-focus.test.d.ts +0 -1
  552. package/dist/core/__tests__/full/detach-focus.test.js +0 -126
  553. package/dist/core/__tests__/full/idle-release-settlement.test.d.ts +0 -1
  554. package/dist/core/__tests__/full/issue-146-yield-spawn-focus.test.d.ts +0 -1
  555. package/dist/core/__tests__/full/issue-146-yield-spawn-focus.test.js +0 -107
  556. package/dist/core/__tests__/full/park-clock-release.test.d.ts +0 -1
  557. package/dist/core/__tests__/full/park-clock-release.test.js +0 -56
  558. package/dist/core/__tests__/full/yield-message-transaction.test.d.ts +0 -1
  559. package/dist/core/__tests__/memory-index-render.test.d.ts +0 -1
  560. package/dist/core/__tests__/memory-index-render.test.js +0 -60
  561. package/dist/core/__tests__/model-routes.test.d.ts +0 -1
  562. package/dist/core/__tests__/model-routes.test.js +0 -41
  563. package/dist/core/__tests__/on-read-identity.test.d.ts +0 -1
  564. package/dist/core/__tests__/on-read-identity.test.js +0 -61
  565. package/dist/core/__tests__/phase4-review-migration.test.d.ts +0 -1
  566. package/dist/core/__tests__/phase4-review-migration.test.js +0 -100
  567. package/dist/core/__tests__/prune-to-limit.test.d.ts +0 -1
  568. package/dist/core/__tests__/prune-to-limit.test.js +0 -126
  569. package/dist/core/__tests__/reset.test.d.ts +0 -1
  570. package/dist/core/__tests__/reset.test.js +0 -69
  571. package/dist/core/__tests__/spawn-no-kickoff.test.d.ts +0 -1
  572. package/dist/core/__tests__/spawn-no-kickoff.test.js +0 -115
  573. package/dist/core/__tests__/steer-note.test.d.ts +0 -1
  574. package/dist/core/__tests__/steer-note.test.js +0 -41
  575. package/dist/core/__tests__/subcommand-tier.test.d.ts +0 -1
  576. package/dist/core/__tests__/subcommand-tier.test.js +0 -99
  577. package/dist/core/__tests__/subscription-delivery.test.js +0 -211
  578. package/dist/core/__tests__/test-runner-cheap-sharding.test.d.ts +0 -1
  579. package/dist/core/__tests__/test-runner-cheap-sharding.test.js +0 -97
  580. package/dist/core/__tests__/tmux-surface.test.js +0 -484
  581. package/dist/core/__tests__/wake-bearings.test.d.ts +0 -1
  582. package/dist/core/__tests__/wake-bearings.test.js +0 -102
  583. package/dist/core/canvas/__tests__/remote-event-stream.test.d.ts +0 -1
  584. package/dist/core/canvas/__tests__/remote-event-stream.test.js +0 -146
  585. package/dist/core/canvas/__tests__/source-resolve.test.d.ts +0 -1
  586. package/dist/core/canvas/__tests__/source-resolve.test.js +0 -77
  587. package/dist/core/canvas/browse/__tests__/model.test.d.ts +0 -1
  588. package/dist/core/canvas/browse/__tests__/model.test.js +0 -290
  589. package/dist/core/canvas/browse/__tests__/rebuild-coalescer.test.d.ts +0 -1
  590. package/dist/core/canvas/browse/__tests__/rebuild-coalescer.test.js +0 -134
  591. package/dist/core/canvas/browse/__tests__/render.test.d.ts +0 -1
  592. package/dist/core/canvas/browse/__tests__/render.test.js +0 -111
  593. package/dist/core/canvas/browse/rebuild-coalescer.d.ts +0 -7
  594. package/dist/core/canvas/browse/rebuild-coalescer.js +0 -61
  595. package/dist/core/canvas/remote-event-stream.d.ts +0 -24
  596. package/dist/core/canvas/remote-event-stream.js +0 -94
  597. package/dist/core/feed/__tests__/coalesce-human.test.d.ts +0 -1
  598. package/dist/core/feed/__tests__/coalesce-human.test.js +0 -36
  599. package/dist/core/feed/__tests__/empty-node-guard.test.d.ts +0 -1
  600. package/dist/core/feed/__tests__/empty-node-guard.test.js +0 -45
  601. package/dist/core/host-exports/__tests__/export-prunes-boot-skill.test.d.ts +0 -1
  602. package/dist/core/host-exports/__tests__/export-prunes-boot-skill.test.js +0 -84
  603. package/dist/core/human/__tests__/visible-paths.test.d.ts +0 -1
  604. package/dist/core/human/__tests__/visible-paths.test.js +0 -28
  605. package/dist/core/keybindings/__tests__/inbox-affordance.test.d.ts +0 -1
  606. package/dist/core/keybindings/__tests__/inbox-affordance.test.js +0 -38
  607. package/dist/core/review/__tests__/project.test.d.ts +0 -1
  608. package/dist/core/review/__tests__/project.test.js +0 -71
  609. package/dist/core/runtime/__tests__/command-expansion.test.d.ts +0 -1
  610. package/dist/core/runtime/__tests__/command-expansion.test.js +0 -25
  611. package/dist/core/runtime/__tests__/session-visibility.test.d.ts +0 -1
  612. package/dist/core/runtime/__tests__/session-visibility.test.js +0 -119
  613. package/dist/core/runtime/__tests__/spawn-at-given-id.test.d.ts +0 -1
  614. package/dist/core/runtime/__tests__/spawn-at-given-id.test.js +0 -34
  615. package/dist/core/runtime/tmux.js +0 -1314
  616. package/dist/daemon/api/__tests__/full/push-parity.test.d.ts +0 -1
  617. package/dist/daemon/api/__tests__/full/worktree-parity.test.d.ts +0 -1
  618. package/dist/daemon/api/__tests__/full/worktree-parity.test.js +0 -160
  619. package/dist/daemon/api/__tests__/inbox.test.d.ts +0 -1
  620. package/dist/daemon/api/__tests__/inbox.test.js +0 -489
  621. package/dist/web-client/apple-touch-icon.png +0 -0
  622. package/dist/web-client/assets/fragment-mono-latin-400-normal-BYwT3kSJ.woff +0 -0
  623. package/dist/web-client/assets/fragment-mono-latin-400-normal-yxdJ5AmL.woff2 +0 -0
  624. package/dist/web-client/assets/fragment-mono-latin-ext-400-normal-BbKYyvR9.woff2 +0 -0
  625. package/dist/web-client/assets/fragment-mono-latin-ext-400-normal-CT4YFKeK.woff +0 -0
  626. package/dist/web-client/assets/index-CCPaLeW7.css +0 -2
  627. package/dist/web-client/assets/index-kyOWsnZK.js +0 -86
  628. package/dist/web-client/assets/martian-mono-cyrillic-wght-normal-B84CD5C_.woff2 +0 -0
  629. package/dist/web-client/assets/martian-mono-latin-ext-wght-normal-DlL6xMw5.woff2 +0 -0
  630. package/dist/web-client/assets/martian-mono-latin-wght-normal-5W32yIyr.woff2 +0 -0
  631. package/dist/web-client/assets/workbox-window.prod.es5-Bd17z0YL.js +0 -2
  632. package/dist/web-client/icon-192.png +0 -0
  633. package/dist/web-client/icon-512.png +0 -0
  634. package/dist/web-client/icon-maskable-512.png +0 -0
  635. package/dist/web-client/index.html +0 -17
  636. package/dist/web-client/manifest.webmanifest +0 -1
  637. package/dist/web-client/sw.js +0 -1
  638. /package/dist/api/__tests__/{client.test.d.ts → serial/client.test.d.ts} +0 -0
  639. /package/dist/clients/inbox/__tests__/{inbox-controller.test.d.ts → serial/inbox-controller.test.d.ts} +0 -0
  640. /package/dist/clients/inbox/__tests__/{mount-panel.test.d.ts → serial/mount-panel.test.d.ts} +0 -0
  641. /package/dist/clients/{attach/__tests__/attach-chrome-remote.test.d.ts → inbox/review/__tests__/teardown-notice.test.d.ts} +0 -0
  642. /package/dist/{clients/attach/__tests__/attach-keybindings.test.d.ts → core/__tests__/broker-extension-canvas-db-boundary.test.d.ts} +0 -0
  643. /package/dist/{clients/attach/__tests__/crtr-output-coverage.test.d.ts → core/__tests__/seam/broker-attach-multiclient.test.d.ts} +0 -0
  644. /package/dist/core/__tests__/{full → seam}/broker-attach-stream.test.d.ts +0 -0
  645. /package/dist/core/__tests__/{full → seam}/broker-crash-teardown.test.d.ts +0 -0
  646. /package/dist/core/__tests__/{full → seam}/broker-pane-resolution.test.d.ts +0 -0
  647. /package/dist/core/__tests__/{full → seam}/broker-provider-retry.test.d.ts +0 -0
  648. /package/dist/core/__tests__/{full → seam}/broker-whip-interrupt.test.d.ts +0 -0
  649. /package/dist/{clients/attach/__tests__/editor-frame-title.test.d.ts → core/__tests__/seam/dormancy-release.test.d.ts} +0 -0
  650. /package/dist/{clients/attach/__tests__/input-controller-extension-command.test.d.ts → core/__tests__/seam/yield-refresh-transaction.test.d.ts} +0 -0
  651. /package/dist/core/__tests__/{broker-fork-seam.test.d.ts → serial/broker-fork-seam.test.d.ts} +0 -0
  652. /package/dist/core/__tests__/{broker-sdk-wiring.test.d.ts → serial/broker-sdk-wiring.test.d.ts} +0 -0
  653. /package/dist/core/__tests__/{broker-snapshot-history.test.d.ts → serial/broker-snapshot-history.test.d.ts} +0 -0
  654. /package/dist/core/__tests__/{command-plugins.test.d.ts → serial/command-plugins.test.d.ts} +0 -0
  655. /package/dist/core/__tests__/{deferred-no-wake.test.d.ts → serial/deferred-no-wake.test.d.ts} +0 -0
  656. /package/dist/core/__tests__/{flagship-lifecycle.test.d.ts → serial/flagship-lifecycle.test.d.ts} +0 -0
  657. /package/dist/core/__tests__/{host-teardown-process-group.test.d.ts → serial/host-teardown-process-group.test.d.ts} +0 -0
  658. /package/dist/core/__tests__/{human-deliver-e2e.test.d.ts → serial/human-deliver-e2e.test.d.ts} +0 -0
  659. /package/dist/core/__tests__/{live-mutation.test.d.ts → serial/live-mutation.test.d.ts} +0 -0
  660. /package/dist/core/__tests__/{refresh-stall-recycle.test.d.ts → serial/refresh-stall-recycle.test.d.ts} +0 -0
  661. /package/dist/core/__tests__/{revive.test.d.ts → serial/revive.test.d.ts} +0 -0
  662. /package/dist/core/__tests__/{spawn-root.test.d.ts → serial/spawn-root.test.d.ts} +0 -0
  663. /package/dist/core/__tests__/{subscription-delivery.test.d.ts → serial/subscription-delivery.test.d.ts} +0 -0
  664. /package/dist/core/__tests__/{tmux-surface.test.d.ts → serial/tmux-surface.test.d.ts} +0 -0
  665. /package/dist/core/__tests__/{worktree.test.d.ts → serial/worktree.test.d.ts} +0 -0
  666. /package/dist/{clients/attach/__tests__/markdown-headings.test.d.ts → core/__tests__/watchdog-abort-arms-retry.test.d.ts} +0 -0
  667. /package/dist/{clients/attach/__tests__/session-identity-refresh.test.d.ts → core/broker-client/__tests__/transport-socket.test.d.ts} +0 -0
  668. /package/dist/core/human/__tests__/{inbox-core.test.d.ts → serial/inbox-core.test.d.ts} +0 -0
  669. /package/dist/{clients/inbox/review/__tests__/remap.test.d.ts → daemon/api/__tests__/node-model-validation.test.d.ts} +0 -0
  670. /package/dist/{clients/surfaces/region.js → daemon/api/__tests__/review-terminal-conflict.test.d.ts} +0 -0
  671. /package/dist/daemon/api/__tests__/{full → seam}/api-server.test.d.ts +0 -0
  672. /package/dist/daemon/api/__tests__/{full → seam}/b10-attach-modelauth.test.d.ts +0 -0
  673. /package/dist/{clients/web/__tests__/push-engine.test.d.ts → daemon/api/__tests__/seam/leaf-api-parity.test.d.ts} +0 -0
@@ -43,33 +43,22 @@
43
43
  // scoped by CRTR_PIDFILE in managed deployments and crtrHome()/crtrd.pid
44
44
  // otherwise. Shutdown quiesces admitted work, removes only this invocation's
45
45
  // mirror, and then releases its exact claim.
46
- import { writeFileSync, readFileSync, rmSync, mkdirSync, statSync, } from 'node:fs';
47
- import { dirname, join } from 'node:path';
48
- import { homedir } from 'node:os';
49
- import { execFile } from 'node:child_process';
46
+ import { writeFileSync, readFileSync, rmSync, mkdirSync, } from 'node:fs';
47
+ import { dirname } from 'node:path';
50
48
  import { randomBytes } from 'node:crypto';
51
- import { promisify } from 'node:util';
52
- import { crtrHome, nodeDir, jobDir, isSafeNodeId } from '../core/canvas/paths.js';
53
- import { listNodes, getRow, getNode, migrateLegacyPidIdentities, listFocuses, closeFocusRow, pruneToLimit, subscribersOf, } from '../core/canvas/index.js';
54
- import { fullName } from '../core/canvas/labels.js';
55
- import { transition } from '../core/runtime/lifecycle.js';
56
- import { isBusy, busySince } from '../core/runtime/busy.js';
57
- import { FAULT_QUIET_MS, readFault, recordFault } from '../core/runtime/fault.js';
58
- import { fanDoctrineWake } from '../core/runtime/close.js';
49
+ import { crtrHome, isSafeNodeId } from '../core/canvas/paths.js';
50
+ import { listNodes, migrateLegacyPidIdentities, } from '../core/canvas/index.js';
59
51
  import { createApiServer } from './api/server.js';
60
52
  import { reconcileUndeliveredTickets } from './human/sweep.js';
61
53
  import { sweepReviews } from './review/sweep.js';
62
- // Lean pidfile/liveness helpers now live in `pidfile.ts` (db-free, so the CLI
63
- // daemon front door can import them without reaching openDb). Imported here for
64
- // internal use and re-exported below to preserve the historical crtrd.ts
65
- // surface (tests, `commands/sys/daemon.ts`).
66
54
  import { pidfilePath } from './pidfile.js';
67
- export { readPidfile, isDaemonRunning } from './pidfile.js';
68
55
  import { rendererWarning } from '../core/runtime/package-health.js';
69
56
  import { DEFAULT_INTERVAL_MS } from './supervise-cadence.js';
70
- import { readConfig } from '../core/config.js';
71
- export { FAULT_QUIET_MS };
72
- import { captureLivenessSnapshot, capturePidCommand, captureTeardownSnapshot, isPidAlive, killProcessTreePids, recordedPidLiveness, } from '../core/canvas/pid.js';
57
+ import { BrokerSupervisionReconciler } from './reconcilers/broker-supervision.js';
58
+ import { ControllerDeathReconciler, DormantInboxReconciler } from './reconcilers/dormant-inbox.js';
59
+ import { CronLaneReconciler } from './reconcilers/cron-lane.js';
60
+ import { StorageMaintenanceReconciler } from './reconcilers/storage-maintenance.js';
61
+ import { captureLivenessSnapshot, capturePidCommand, captureTeardownSnapshot, isPidAlive, killProcessTreePids, } from '../core/canvas/pid.js';
73
62
  import { bindFleet, boundFleet } from '../core/runtime/fleet.js';
74
63
  import { bindDaemonControl, clearDaemonControl } from './control.js';
75
64
  import { spawnDaemon } from './manage.js';
@@ -77,114 +66,12 @@ import { reconcileAndMigrateBoot } from '../core/canvas/boot.js';
77
66
  import { resolveInstallId } from '../core/canvas/install-id.js';
78
67
  import { setInstallId } from '../core/runtime/nodes.js';
79
68
  import { claimDaemonOwnership, releaseDaemonOwnership } from '../core/canvas/daemon-owner.js';
80
- import { listLivePanes, tearDownNode } from '../core/runtime/placement.js';
81
- import { reviveNode } from '../core/runtime/revive.js';
82
69
  import { prewarmRecentRecipes, reapStaleSpares } from '../core/runtime/warm-pool.js';
83
- import { headlessBrokerHost } from '../core/runtime/host.js';
84
- import { reloadAuthLive } from '../core/runtime/auth-reload.js';
85
- import { DaemonFleet, retryResumeMode } from './fleet.js';
86
- export { retryResumeMode };
87
- import { appendControllerDeathNotice, readInboxSince, readCursor, } from '../core/feed/inbox.js';
70
+ import { DaemonFleet } from './fleet.js';
88
71
  import { migrateLegacyInboxIdentities } from '../core/feed/inbox-migrate.js';
89
72
  import { emitEvent } from '../core/events/emit.js';
90
73
  import { operationIdContext } from '../core/events/operation-id.js';
91
74
  import { bindDaemonEventSource } from '../core/events/source.js';
92
- import { recoverStaleCronLeases, runDueCrons } from './cron-run.js';
93
- // Perf (freeze diagnosis, 2026-07-12): every `ps`/`tmux` shell-out on the
94
- // reconcile tick used to go through `spawnSync`, which blocks the WHOLE event
95
- // loop for the life of the child process. With a large fleet of brokers (100+
96
- // observed), these synchronous spawns stacked up and froze the daemon solid —
97
- // no other tick, inbox poll, or IPC could run while one was blocked on
98
- // `posix_spawn`. `execFileAsync` (promisified `execFile`) runs the child via
99
- // libuv without blocking the main thread; every call site below that used to
100
- // `spawnSync` now awaits this instead, with identical inputs/outputs (same
101
- // `encoding`/`timeout`, same decisions made from stdout) — only the blocking
102
- // behavior changes.
103
- const execFileAsync = promisify(execFile);
104
- // Count-based auto-prune of the canvas (pruneToLimit). The canvas grows
105
- // unbounded — the TTL `crtr canvas prune` is manual, so without this a long-lived
106
- // daemon's canvas accumulates thousands of terminal nodes, making every scan +
107
- // `canvas snapshot` expensive. Keep the AUTO_PRUNE_MAX most-recently-active nodes
108
- // (live nodes are NEVER deleted, so the real floor is max(AUTO_PRUNE_MAX,
109
- // liveCount)). It's a DB sweep, not per-tick work — throttled to PRUNE_INTERVAL_MS.
110
- const AUTO_PRUNE_MAX = 2000;
111
- const PRUNE_INTERVAL_MS = 60 * 60 * 1000;
112
- // Last auto-prune wall-clock. -Infinity so the FIRST tick after start runs it
113
- // (the canvas may already be huge at boot); thereafter every PRUNE_INTERVAL_MS.
114
- // In-memory only — a daemon restart re-runs it once, which is harmless.
115
- let lastPruneAt = Number.NEGATIVE_INFINITY;
116
- // Warm-pool GC (reapStaleSpares). Unclaimed spares are hidden from every
117
- // listing, so NOTHING else collects them: the recovery sweep never resumes one
118
- // (a daemon handover therefore leaves its broker dead and its row parked
119
- // forever) and the count-based prune cannot even see it. Each carries a
120
- // ~150-300 MB resident broker, so the pool must collect its own garbage. Not
121
- // per-tick work — the sweep pid-probes each spare, so it is throttled.
122
- const SPARE_SWEEP_INTERVAL_MS = 60 * 1000;
123
- let lastSpareSweepAt = Number.NEGATIVE_INFINITY;
124
- // Idle-release/inbox retry cap (2026-07-10 diagnosis, round 2): the SAME
125
- // livelock shape as the stranded-relaunch case above, but on the SEPARATE
126
- // pass-2 path that resumes a dormant idle-released node the instant its inbox
127
- // gains an unseen entry. That path called reviveNode() unconditionally every
128
- // poll with no grace, no backoff, and no attempt cap.
129
- //
130
- // ROUND 1 keyed the retry counter on row-state residency (pid liveness),
131
- // cleared the instant the pid-alive branch observed a live engine. That
132
- // caught a broker that never boots at all — but the RECURRING incident's
133
- // actual failure is TURN-level, not launch-level: the revive succeeds (broker
134
- // launches, records a pid — clearing the round-1 counter), the turn then
135
- // faults against the provider, the engine exits with the inbox CURSOR
136
- // UNADVANCED, the node re-enters idle/idle-release, pass-2 sees the same
137
- // unseen entries again, and — because the counter was just cleared by the
138
- // transiently-alive engine — starts over at attempt 1/6 every single cycle.
139
- // crtrd.err showed exactly this: hundreds of `attempt 1/6` lines, never
140
- // `2/6`, interleaved with `fatal-fault <id> (other, pi→provider)`.
141
- //
142
- // ROUND 2 fix: key the counter on INBOX-CURSOR PROGRESS, not row-state
143
- // residency or pid liveness. `readCursor(id)` is recorded as the state's
144
- // baseline the first time a stuck delivery is observed; a transiently-alive
145
- // engine does NOT reset it (nothing clears this state on liveness — the
146
- // fleet's exit-policy jobs never touch it). Only a different exact cursor identity on
147
- // the NEXT observation — proof the wake delivered something — resets the
148
- // counter (via `cursorChanged` below); an unchanged identity is the SAME stuck
149
- // delivery and increments/backs off exactly like the stranded-relaunch path.
150
- const IDLE_RELEASE_RELAUNCH_MAX_ATTEMPTS = 6;
151
- const IDLE_RELEASE_RELAUNCH_MAX_BACKOFF_MS = 10 * 60_000; // 10 minutes
152
- // Backoff base for the idle-release retry ladder (kept at the value the old
153
- // boot-grace clock supplied, so the ladder's math is unchanged).
154
- const IDLE_RELEASE_RELAUNCH_BASE_MS = 20_000;
155
- const idleReleaseRelaunchState = new Map();
156
- function idleReleaseRelaunchBackoffMs(attempts) {
157
- return Math.min(IDLE_RELEASE_RELAUNCH_BASE_MS * 2 ** attempts, IDLE_RELEASE_RELAUNCH_MAX_BACKOFF_MS);
158
- }
159
- /** Cleared ONLY when the node row itself is gone, or once the counter gives
160
- * up and terminalizes the node — NEVER on a transiently-alive engine (that
161
- * was round 1's bug) and never on a bare idle-release status flap (the
162
- * fault-recovery cycle flaps status EVERY episode, so clearing on that would
163
- * reproduce the exact same livelock one level up). */
164
- function clearIdleReleaseRelaunchState(id) {
165
- idleReleaseRelaunchState.delete(id);
166
- }
167
- // ---------------------------------------------------------------------------
168
- // Broker-pressure thresholds bound daemon-initiated revives only. Explicit
169
- // `node lifecycle revive` and `surface node focus` calls bypass this daemon sweep.
170
- // User config is the durable host policy; environment variables are explicit
171
- // process-level overrides for managed deployments.
172
- function configuredPositiveInteger(name, fallback) {
173
- const raw = process.env[name];
174
- if (raw === undefined || raw === '')
175
- return fallback;
176
- const n = Number(raw);
177
- return Number.isSafeInteger(n) && n >= 1 ? n : fallback;
178
- }
179
- export function brokerThresholdsForDaemon() {
180
- const configured = readConfig('user').brokerThresholds;
181
- const automaticReviveCap = configuredPositiveInteger('CRTR_MAX_LIVE_BROKERS', configured.automaticReviveCap);
182
- const warning = configuredPositiveInteger('CRTR_WARN_LIVE_BROKERS', configured.warning);
183
- // Independent environment overrides can invert the configured pair. Clamp
184
- // the warning to the cap so broker pressure is always visible before revive
185
- // deferral begins.
186
- return { warning: Math.min(warning, automaticReviveCap), automaticReviveCap };
187
- }
188
75
  // A broker DECLARES its owning canvas in argv at spawn (host.ts launch:
189
76
  // `broker-cli.js --canvas-home <home> --epoch <id> <nodeId>`), so ownership is
190
77
  // an exact match on this daemon's own canvas home — no path inference. An
@@ -355,18 +242,11 @@ export async function runStartupRecoverySweep(fleet) {
355
242
  emitEvent({ level: 'error', event: 'daemon.recovery_sweep.failed', error: err });
356
243
  return;
357
244
  }
358
- const thresholds = brokerThresholdsForDaemon();
359
245
  for (const row of rows) {
360
246
  if (row.kind === 'human')
361
247
  continue; // human bridge rows — never broker-hosted
362
- if (fleet.size() >= thresholds.automaticReviveCap) {
363
- emitEvent({
364
- level: 'warn',
365
- event: 'daemon.recovery_sweep.capacity_deferred',
366
- fields: { live_count: fleet.size() },
367
- });
248
+ if (!brokerSupervision.hasStartupRecoveryCapacity(fleet))
368
249
  break;
369
- }
370
250
  await operationIdContext.fresh(async () => {
371
251
  try {
372
252
  await fleet.recoverDeadNode(row.node_id);
@@ -386,830 +266,6 @@ export async function runStartupRecoverySweep(fleet) {
386
266
  });
387
267
  }
388
268
  }
389
- // Broker-pressure warnings are surfaced at the lower threshold so a growing
390
- // census is visible before automatic revives are deferred. A canonical event
391
- // records the condition and a throttled tmux notice reaches attached clients.
392
- const OVER_CAP_WARN_THROTTLE_MS = 5 * 60_000;
393
- let lastOverCapWarnAt = Number.NEGATIVE_INFINITY;
394
- async function warnOverCapInTmux(count, warning, max) {
395
- const now = Date.now();
396
- if (now - lastOverCapWarnAt < OVER_CAP_WARN_THROTTLE_MS)
397
- return;
398
- lastOverCapWarnAt = now;
399
- emitEvent({
400
- level: 'warn',
401
- event: 'broker.capacity.warning',
402
- fields: { live_count: count, warning_threshold: warning, hard_cap: max },
403
- });
404
- const msg = `⚠ crtrd: ${count} live brokers (warning ${warning}; hard cap ${max}) — possible broker leak. ps -ax | grep broker-cli.js`;
405
- try {
406
- const clients = await execFileAsync('tmux', ['list-clients', '-F', '#{client_name}'], {
407
- encoding: 'utf8',
408
- timeout: 2000,
409
- });
410
- if (typeof clients.stdout !== 'string')
411
- return;
412
- // Fan out per-client notification — each client's primary-then-fallback
413
- // display-message sequence still runs in order, but N stale clients no
414
- // longer serialize into ~4s × N of teardown-owned latency.
415
- await Promise.allSettled(clients.stdout.split('\n').map(async (client) => {
416
- if (client === '')
417
- return;
418
- // -d 10000 holds the message for 10s (tmux ≥3.2); if this tmux
419
- // predates -d, fall back to the default display-time rather than not
420
- // warning.
421
- try {
422
- await execFileAsync('tmux', ['display-message', '-d', '10000', '-c', client, msg], {
423
- encoding: 'utf8',
424
- timeout: 2000,
425
- });
426
- }
427
- catch {
428
- try {
429
- await execFileAsync('tmux', ['display-message', '-c', client, msg], {
430
- encoding: 'utf8',
431
- timeout: 2000,
432
- });
433
- }
434
- catch {
435
- /* both display-message variants failed for this client — skip it */
436
- }
437
- }
438
- }));
439
- }
440
- catch {
441
- /* no tmux server — the canonical event above still records the condition */
442
- }
443
- }
444
- // Throttle for the canonical "at capacity" event: without it, every deferred
445
- // revive on every 2s poll would flood the daemon event stream as badly as the
446
- // livelock this cap exists to prevent.
447
- const CAPACITY_LOG_THROTTLE_MS = 30_000;
448
- let lastCapacityLogAt = Number.NEGATIVE_INFINITY;
449
- /** Capacity for a TICK-initiated launch (pass-2 inbox wakes + due crons whose
450
- * run launches or revives a node): the live count is EXACTLY the fleet size
451
- * (design D-7) — every live broker is a fleet entry, no census or row inference. Checked BEFORE
452
- * any attempt/backoff state is mutated at each call site, so a capacity-
453
- * deferred revive costs a node NOTHING against its retry budget — it's
454
- * retried plainly next poll once capacity frees up. Exit-driven 1:1 respawns
455
- * are cap-exempt by construction: the dying broker freed its slot at exit. */
456
- function hasBrokerCapacity(max) {
457
- if (boundFleet().size() < max)
458
- return true;
459
- const now = Date.now();
460
- if (now - lastCapacityLogAt >= CAPACITY_LOG_THROTTLE_MS) {
461
- lastCapacityLogAt = now;
462
- emitEvent({
463
- level: 'warn',
464
- event: 'broker.capacity.revive_deferred',
465
- fields: { live_count: boundFleet().size(), hard_cap: max },
466
- });
467
- }
468
- return false;
469
- }
470
- // §H refresh-authority grace: how long a node may sit with intent='refresh', its
471
- // turn OVER (busy marker absent) and its engine still ALIVE before the daemon
472
- // concludes the refresh stalled and force-kills the engine. The healthy window is
473
- // seconds: a sound stophook calls shutdown() the instant the refresh turn ends;
474
- // the resulting broker exit is classified as respawn-fresh. Minutes of this
475
- // live-fleet state means the engine never exited — either shutdown hung
476
- // (observed at ~200k-token contexts) OR a STALE in-process stophook (pi
477
- // extensions never reload, so a days-old resident carries an old hook that
478
- // ignores intent='refresh' at stop) never called shutdown at all. Nothing else
479
- // will emit the exit event, so the daemon is the authority: kill the engine here
480
- // and let its exit-policy job revive fresh.
481
- export const YIELD_STALL_GRACE_MS = 3 * 60_000;
482
- // After a yield-stall SIGTERM, how long the pi gets to die gracefully before
483
- // escalating to SIGKILL (a pi hung at context overflow may not run handlers).
484
- const KILL_ESCALATE_MS = 20_000;
485
- // Per-node yield-stall clocks (audit 2026-06-09, Bug 1). In-memory — a daemon
486
- // restart resets them (worst case: one extra grace).
487
- const yieldStallSince = new Map();
488
- const yieldTermAt = new Map();
489
- /** Refresh-stall decision (§H refresh-authority; audit 2026-06-09, Bug 1 — node
490
- * mq5v9hfa-74493d45 and siblings; stuck-refresh — parent mq7s5o93): a `node
491
- * yield` / refresh records intent='refresh' and relies on the in-process
492
- * stophook to call shutdown at stop, but the engine can be left ALIVE — either
493
- * it HANGS instead of exiting (very large contexts) or a STALE stophook (pi
494
- * extensions never reload) ignores intent='refresh' entirely and never calls
495
- * shutdown. Either way: engine alive, turn over, intent pending forever. The
496
- * daemon only revives DEAD engines, so without this verdict the node is
497
- * permanently stuck (no revive, no wake). Pure core, mirroring wedgeVerdict;
498
- * the kill side effects live in handleYieldStall.
499
- *
500
- * 'kill' ONLY when ALL of: engine alive, intent='refresh', the turn is over
501
- * (busy marker absent — a node may legitimately keep working for many minutes
502
- * after running `node yield` mid-turn, and a working engine must never be
503
- * killed), and the state has persisted past YIELD_STALL_GRACE_MS. */
504
- export function yieldStallVerdict(piPidAlive, intent, busy, stalledFor) {
505
- if (piPidAlive !== true)
506
- return 'leave';
507
- if (intent !== 'refresh')
508
- return 'leave';
509
- if (busy)
510
- return 'leave';
511
- if (stalledFor === null || stalledFor < YIELD_STALL_GRACE_MS)
512
- return 'pending';
513
- return 'kill';
514
- }
515
- /** Enact a refresh-stall (§H refresh-authority): clock the stalled state, then
516
- * SIGTERM the hung engine (escalating to SIGKILL if it survives
517
- * KILL_ESCALATE_MS — an engine hung at context overflow, or a stale hook that
518
- * never wired shutdown, may never run its handlers, so a graceful frame can't be
519
- * trusted; the signal escalation is what guarantees the engine actually dies).
520
- * Once the engine is dead the ORDINARY machinery finishes the job: the kill
521
- * lands as a real exit event whose policy job classifies intent='refresh' as
522
- * respawn-fresh, i.e. the fresh roadmap-revive the yield asked for. Safe
523
- * against a graceful shutdown racing us: markCleanExitDone no-ops while intent
524
- * is pending, so a dying engine can't mis-finalize the node. */
525
- function handleYieldStall(row, pid, now) {
526
- const id = row.node_id;
527
- if (row.intent !== 'refresh' || isBusy(id)) {
528
- yieldStallSince.delete(id);
529
- yieldTermAt.delete(id);
530
- return;
531
- }
532
- const since = yieldStallSince.get(id);
533
- if (since === undefined) {
534
- yieldStallSince.set(id, now);
535
- return;
536
- }
537
- if (yieldStallVerdict(true, row.intent, false, now - since) !== 'kill')
538
- return;
539
- // The tick snapshot is observation data, not signal authorization: recheck
540
- // immediately before acting so PID reuse during a long tick cannot target a
541
- // process that never belonged to this node.
542
- if (row.pi_pid_identity == null || recordedPidLiveness(pid, row.pi_pid_identity) !== 'alive')
543
- return;
544
- const termed = yieldTermAt.get(id);
545
- if (termed === undefined) {
546
- emitEvent({
547
- level: 'warn',
548
- event: 'broker.yield_stall.sigterm',
549
- ...(isSafeNodeId(id) ? { node_id: id } : {}),
550
- fields: {
551
- ...(isSafeNodeId(id) ? {} : { affected_node_id: id }),
552
- pid,
553
- elapsed_ms: now - since,
554
- grace_ms: YIELD_STALL_GRACE_MS,
555
- },
556
- });
557
- try {
558
- process.kill(pid, 'SIGTERM');
559
- }
560
- catch {
561
- /* already gone */
562
- }
563
- yieldTermAt.set(id, now);
564
- }
565
- else if (now - termed >= KILL_ESCALATE_MS) {
566
- emitEvent({
567
- level: 'warn',
568
- event: 'broker.yield_stall.sigkill',
569
- ...(isSafeNodeId(id) ? { node_id: id } : {}),
570
- fields: {
571
- ...(isSafeNodeId(id) ? {} : { affected_node_id: id }),
572
- pid,
573
- elapsed_ms: now - termed,
574
- grace_ms: KILL_ESCALATE_MS,
575
- stall_elapsed_ms: now - since,
576
- },
577
- });
578
- try {
579
- process.kill(pid, 'SIGKILL');
580
- }
581
- catch {
582
- /* already gone */
583
- }
584
- }
585
- }
586
- // ---------------------------------------------------------------------------
587
- // Wedge detection (issue #110): a broker whose pid is alive but whose engine is
588
- // frozen MID-TURN (a runaway subprocess, a hung stream) is invisible to every
589
- // other guarantee — the pid-alive branch above returns early, yield-stall only
590
- // fires on intent='refresh', and provider-fault recovery needs a completed
591
- // ERROR turn. A node in this state never pushes, so its parent waits forever
592
- // with no wake and no recorded fault marker — there's just silence.
593
- //
594
- // Detection always NOTIFIES (fans a doctrine wake + records a `daemon→node`/
595
- // 'wedged' fault so the node shows ⌘ “wedged · needs you” on canvas graph
596
- // views). Recovery
597
- // then branches on whether the broker's process TREE has a descendant:
598
- // • descendants present → a runaway SUBPROCESS is the likely cause; the
599
- // daemon does NOT touch the engine — kill the subprocess (not the node);
600
- // see `crtr memory read wedged-child-on-runaway-bash`.
601
- // • no descendants (the broker pid stands alone — e.g. the engine itself
602
- // stalled after a model_change, with nothing to kill) → the daemon
603
- // performs the ONLY remaining remediation itself: SIGTERM the broker, the
604
- // same on-demand kick `node lifecycle revive --now` does — the SIGTERM
605
- // gives Pi a bounded chance to persist an aborted turn, then the exit-policy
606
- // job either resumes that certified session with a continuation or starts
607
- // the existing dirty recovery cycle.
608
- //
609
- // Two corroborating signals, both generous, so a routine long-running bash/
610
- // build/test tool call is never mistaken for a wedge:
611
- // 1. quietForMs — ms since the busy marker's mtime last moved. canvas-
612
- // stophook re-touches that marker (`touchBusy`) on every engine-level sign
613
- // of life within a turn: a tool starting/streaming/ending, a streamed
614
- // assistant token, or the turn ending. A tool that is actively streaming
615
- // output, or a turn producing tokens, keeps this at ~0 no matter how long
616
- // the OVERALL turn runs. Only a turn with NO observed engine event at all
617
- // for WEDGE_QUIET_MS is even a candidate.
618
- // 2. cpuPercent — the broker's WHOLE PROCESS TREE (the broker pid + every
619
- // descendant), summed, sampled ONLY once a node is already a time-based
620
- // candidate (never every tick — the `ps` shell-out is cheap but pointless
621
- // on a healthy node). Sampling the tree, not just the broker pid, matters
622
- // because the broker often sits idle awaiting a CHILD subprocess (bash,
623
- // a build, a test run) that is the one actually burning CPU — sampling
624
- // the broker alone would read that as 0% and false-fire on a perfectly
625
- // healthy long-running tool call. A pi process (or any descendant)
626
- // genuinely computing shows non-trivial tree CPU and is exonerated even
627
- // past the quiet grace; a pi merely awaiting a subprocess or a stalled
628
- // I/O call — wedged OR legitimately blocked on something equally silent —
629
- // reads ~0% across the whole tree, matching the filed issue's own
630
- // evidence (pid alive, 0% CPU, session file not advancing). A failed/
631
- // unreadable CPU sample is treated as UNKNOWN, never as “low” — the
632
- // verdict stays 'pending' rather than risk a false fire on an unreadable
633
- // signal.
634
- //
635
- // ACCEPTED residual: a legitimate subprocess blocked on silent I/O (e.g. a
636
- // 20-minute download emitting no output, a long `sleep`) reads ~0% tree
637
- // CPU and is indistinguishable from a real wedge at the OS level — no CPU
638
- // signal can tell a hung child from an I/O-blocked one. This is accepted:
639
- // that case still HAS a descendant, so the daemon only notifies (never
640
- // kills) — the cost of the rare false positive is one doctrine wake, and
641
- // 20+ minutes of total silence with near-zero tree CPU is itself worth
642
- // surfacing either way. Only the no-descendants branch ever acts, and
643
- // SIGTERM preserves durable context: a settled abort resumes with a
644
- // continuation, while an unconfirmed abort takes the existing fresh cycle.
645
- // A false positive therefore costs at most one unnecessary recovery, not
646
- // lost work.
647
- // ---------------------------------------------------------------------------
648
- // Generous: routine long tool calls settle well under this; only a turn with
649
- // zero observed engine progress for 20+ minutes is even a wedge CANDIDATE.
650
- export const WEDGE_QUIET_MS = 20 * 60_000;
651
- // CPU% ceiling for the corroborating sample, taken once quietForMs already
652
- // clears the grace above. Small but nonzero — tolerates measurement noise
653
- // without accepting a process that's visibly still doing work.
654
- export const WEDGE_CPU_MAX_PERCENT = 2;
655
- /** Pure wedge decision — mirrors yieldStallVerdict: a total
656
- * function of the observed signals, no clock or process access of its own.
657
- * `busy` false — the turn already ended (however it routed) — is never a
658
- * wedge, regardless of how stale the marker looks. `quietForMs` null means
659
- * the heartbeat marker couldn't be read (treated as “not enough evidence”, not
660
- * “wedged”). `cpuPercent` null (sample failed/unavailable) or above the
661
- * ceiling — EITHER keeps the verdict at 'pending' (keep watching next tick)
662
- * rather than firing on an unconfirmed or contradicted signal. */
663
- export function wedgeVerdict(input) {
664
- if (!input.busy)
665
- return 'leave';
666
- if (input.quietForMs === null || input.quietForMs < WEDGE_QUIET_MS)
667
- return 'leave';
668
- if (input.cpuPercent === null || input.cpuPercent > WEDGE_CPU_MAX_PERCENT)
669
- return 'pending';
670
- return 'wedged';
671
- }
672
- /** Notified-episode dedup (one notice per wedge, per §Detection-not-repeat-fire):
673
- * keyed on node id → the busy-marker mtime we already notified for. The marker
674
- * only advances on genuine engine progress (a heartbeat) or a fresh turn
675
- * starting, so a stale wedge that later recovers (or a brand-new turn) always
676
- * carries a DIFFERENT mtime and is free to notify again. In-memory only, like
677
- * every other daemon dedup latch here — a restart re-arms it (worst case: one
678
- * repeat notice), which is the same tradeoff as the daemon's other in-memory dedup latches. */
679
- const wedgeNotifiedAt = new Map();
680
- /** Fatal-provider-fault notify latch (issue #94), keyed on node id → the
681
- * `fault.since` we already fanned a wake for. A fatal fault (dead OAuth grant,
682
- * bad key, protocol) has NO retry consumer, so an errored turn just leaves the
683
- * node `active` with a live-but-dormant broker and, until now, woke nobody —
684
- * the manager waiting on it hung and later scheduled wakes silently re-failed.
685
- * Deduped on `fault.since` so one episode fans exactly one wake; a later
686
- * episode (fresh `since`) or a cleared fault re-arms. In-memory like every
687
- * other latch here. */
688
- const fatalFaultNotifiedAt = new Map();
689
- /** Pure: sum CPU% across a process's entire tree (itself + every descendant).
690
- * Parses `ps -ax -o pid=,ppid=,pcpu=` output, builds a ppid→children map, and
691
- * DFS's from `rootPid` collecting root + all descendants; malformed/blank
692
- * lines are skipped. Returns `null` if `rootPid` never appears in the output
693
- * (can't measure at all — treated as UNKNOWN by wedgeVerdict, never as
694
- * “low”) or the output has no parsable rows. Exported for direct unit testing
695
- * — no process access of its own, mirroring wedgeVerdict's purity. */
696
- function parsePsTree(psOutput) {
697
- const byPid = new Map(); // pid → pcpu
698
- const childrenOf = new Map(); // ppid → child pids
699
- for (const line of psOutput.split('\n')) {
700
- const trimmed = line.trim();
701
- if (trimmed === '')
702
- continue;
703
- const parts = trimmed.split(/\s+/);
704
- if (parts.length < 3)
705
- continue;
706
- const pid = Number(parts[0]);
707
- const ppid = Number(parts[1]);
708
- const pcpu = Number(parts[2]);
709
- if (!Number.isFinite(pid) || !Number.isFinite(ppid) || !Number.isFinite(pcpu))
710
- continue;
711
- byPid.set(pid, pcpu);
712
- const siblings = childrenOf.get(ppid);
713
- if (siblings === undefined)
714
- childrenOf.set(ppid, [pid]);
715
- else
716
- siblings.push(pid);
717
- }
718
- return { byPid, childrenOf };
719
- }
720
- export function sumTreeCpu(psOutput, rootPid) {
721
- const { byPid, childrenOf } = parsePsTree(psOutput);
722
- if (!byPid.has(rootPid))
723
- return null;
724
- let total = 0;
725
- const stack = [rootPid];
726
- const seen = new Set();
727
- while (stack.length > 0) {
728
- const pid = stack.pop();
729
- if (seen.has(pid))
730
- continue; // guard against a malformed/cyclic ps snapshot
731
- seen.add(pid);
732
- total += byPid.get(pid) ?? 0;
733
- const kids = childrenOf.get(pid);
734
- if (kids !== undefined)
735
- stack.push(...kids);
736
- }
737
- return total;
738
- }
739
- /** Pure (issue #119): does `rootPid` have ANY descendant in this ps snapshot
740
- * — a runaway subprocess to kill — or does the broker stand ALONE in its own
741
- * tree (nothing to kill; the engine itself stalled)? Shares `parsePsTree`
742
- * with `sumTreeCpu` so the two facts the wedge remediation needs come from
743
- * one `ps` sample. Null when `rootPid` isn't in the sample at all (can't
744
- * tell — same "unknown, never treated as a fact" stance `sumTreeCpu` takes on
745
- * a missing root). Exported for direct unit testing. */
746
- export function treeHasDescendants(psOutput, rootPid) {
747
- const { byPid, childrenOf } = parsePsTree(psOutput);
748
- if (!byPid.has(rootPid))
749
- return null;
750
- return (childrenOf.get(rootPid) ?? []).length > 0;
751
- }
752
- /** Sample the broker's WHOLE PROCESS TREE — the broker pid plus every
753
- * descendant — via ONE `ps` shell-out, yielding both signals the wedge path
754
- * needs: the summed tree CPU% (corroborates the heartbeat silence) and
755
- * whether the tree has any descendant at all (issue #119 — decides whether
756
- * remediation is "kill the subprocess" or "kick the broker"). Null on ANY
757
- * failure (the shell-out itself failing, `ps` missing, or the root pid
758
- * absent/output unparseable) — wedgeVerdict treats a null cpuPercent exactly
759
- * like “too high”, so a broken probe can only ever suppress a false fire,
760
- * never cause one; a null sample also means handleWedgeDetection never
761
- * attempts the SIGTERM kick on unconfirmed information. */
762
- async function readTreeSample(pid) {
763
- try {
764
- const { stdout } = await execFileAsync('ps', ['-ax', '-o', 'pid=,ppid=,pcpu='], {
765
- encoding: 'utf8',
766
- timeout: 2000,
767
- });
768
- if (typeof stdout !== 'string')
769
- return null;
770
- const cpuPercent = sumTreeCpu(stdout, pid);
771
- const hasDescendants = treeHasDescendants(stdout, pid);
772
- if (cpuPercent === null || hasDescendants === null)
773
- return null;
774
- return { cpuPercent, hasDescendants };
775
- }
776
- catch {
777
- return null;
778
- }
779
- }
780
- /** Enact wedge detection for a LIVE, busy node (called from the tick's
781
- * live-fleet pass, alongside handleYieldStall — the two never
782
- * overlap in practice: yield-stall requires the turn OVER, wedge requires it
783
- * mid-flight). Reads the heartbeat + samples CPU only when already past the
784
- * time grace, fans one doctrine wake + records one `daemon→node` fault per
785
- * episode, and never touches the engine itself. */
786
- async function handleWedgeDetection(id, pid, expectedIdentity, now) {
787
- if (!isBusy(id)) {
788
- wedgeNotifiedAt.delete(id); // turn ended (however it routed) — next turn starts a fresh episode
789
- return;
790
- }
791
- const since = busySince(id);
792
- const quietForMs = since === null ? null : now - since;
793
- const sample = quietForMs !== null && quietForMs >= WEDGE_QUIET_MS ? await readTreeSample(pid) : null;
794
- const cpuPercent = sample?.cpuPercent ?? null;
795
- const verdict = wedgeVerdict({ busy: true, quietForMs, cpuPercent });
796
- if (verdict !== 'wedged')
797
- return;
798
- if (since !== null && wedgeNotifiedAt.get(id) === since)
799
- return; // already notified THIS episode
800
- // readTreeSample is intentionally not signal authorization: confirm the
801
- // launch identity again at the destructive boundary after its async probe.
802
- if (sample?.hasDescendants === false
803
- && (expectedIdentity == null || recordedPidLiveness(pid, expectedIdentity) !== 'alive'))
804
- return;
805
- if (since !== null)
806
- wedgeNotifiedAt.set(id, since);
807
- const meta = getNode(id);
808
- const name = meta !== null ? fullName(meta) : id;
809
- const minutes = Math.round((quietForMs ?? WEDGE_QUIET_MS) / 60_000);
810
- // A null sample can't reach here — wedgeVerdict only fires 'wedged' on a
811
- // confirmed near-zero cpuPercent, which requires a non-null sample — so
812
- // `sample.hasDescendants` is always defined at this point; the `?? true`
813
- // is a type-level fallback only, biased toward the SAFER branch (notify,
814
- // don't SIGTERM) if that invariant is ever violated.
815
- const hasDescendants = sample?.hasDescendants ?? true;
816
- let label;
817
- if (hasDescendants) {
818
- // A runaway subprocess is the likely cause — the daemon does not touch the
819
- // engine; the sanctioned recovery is manual (issue #110).
820
- label =
821
- `Child wedged — ${name} (${id}) is alive and mid-turn but has shown NO engine progress for ` +
822
- `${minutes}+ minutes and its process is near-idle. It will NOT recover on its own — read ` +
823
- `\`crtr memory read wedged-child-on-runaway-bash\` and kill the runaway SUBPROCESS (not this ` +
824
- `node) so its turn can resume with no lost context.`;
825
- }
826
- else {
827
- // No descendant to kill — the engine itself stalled (e.g. mid model_change)
828
- // with nothing a human could kill. The daemon performs the same kick
829
- // `node lifecycle revive --now` does on demand: SIGTERM the broker. The
830
- // exit-policy job resumes with a continuation if Pi certifies the abort;
831
- // otherwise it starts the existing dirty recovery cycle (issue #119).
832
- label =
833
- `Child wedged — ${name} (${id}) is alive and mid-turn but has shown NO engine progress for ` +
834
- `${minutes}+ minutes, its process is near-idle, AND it has no subprocess to kill (the engine ` +
835
- `itself stalled). Kicking it now: SIGTERM to the broker — the daemon will resume a cleanly ` +
836
- `aborted session with a continuation, or start a recovery cycle if the abort cannot settle.`;
837
- try {
838
- process.kill(pid, 'SIGTERM');
839
- }
840
- catch {
841
- /* already gone — its ChildProcess exit event still owns recovery */
842
- }
843
- }
844
- emitEvent({
845
- level: 'error',
846
- event: 'broker.wedge.detected',
847
- ...(isSafeNodeId(id) ? { node_id: id } : {}),
848
- fields: {
849
- ...(isSafeNodeId(id) ? {} : { affected_node_id: id }),
850
- pid,
851
- quiet_ms: quietForMs,
852
- cpu_percent: cpuPercent,
853
- has_descendants: hasDescendants,
854
- kicked: !hasDescendants,
855
- },
856
- });
857
- try {
858
- recordFault(id, {
859
- link: 'daemon→node',
860
- op: 'engine wedge',
861
- kind: 'wedged',
862
- retry: { disposition: 'manual' },
863
- message: label,
864
- });
865
- }
866
- catch {
867
- /* best-effort — the doctrine wake below is the load-bearing notice */
868
- }
869
- try {
870
- fanDoctrineWake(id, subscribersOf(id), label, {
871
- reason: 'child-wedged',
872
- child: id,
873
- quiet_ms: quietForMs,
874
- cpu_percent: cpuPercent,
875
- kicked: !hasDescendants,
876
- });
877
- }
878
- catch {
879
- /* best-effort, mirrors every other fan-out in this file */
880
- }
881
- }
882
- /** Enact a fatal-provider-fault outcome for a LIVE node (issue #94; called
883
- * from the tick's live-fleet pass). Fatal authentication and
884
- * protocol faults remain parked because re-authentication or a correction can
885
- * make the next wake viable. A context-window overflow cannot become viable
886
- * while its oversized session remains selected, so it is crashed and its broker
887
- * is torn down before the parent is notified. */
888
- function handleFatalFault(id, now) {
889
- void now;
890
- const fault = readFault(id);
891
- if (fault === null) {
892
- fatalFaultNotifiedAt.delete(id); // fault cleared — next episode re-arms
893
- return;
894
- }
895
- if (fault.retry.disposition !== 'fatal' || fault.link !== 'pi→provider')
896
- return;
897
- if (fatalFaultNotifiedAt.get(id) === fault.since)
898
- return; // already fanned THIS episode
899
- fatalFaultNotifiedAt.set(id, fault.since);
900
- const meta = getNode(id);
901
- const name = meta !== null ? fullName(meta) : id;
902
- const isAuth = fault.kind === 'auth';
903
- const firstLine = (fault.message.split('\n')[0] ?? '').slice(0, 200);
904
- const terminalOverflow = fault.kind === 'context-overflow';
905
- const label = terminalOverflow
906
- ? `Child failed — ${name} (${id}) exceeded its model context window and is now dead. ` +
907
- `Shrink the task or loaded context, or choose a larger-context model before spawning it again. Error: ${firstLine}`
908
- : `Child hit a fatal provider fault — ${name} (${id}) got a non-retryable ${fault.kind} error and will NOT recover on its own; ` +
909
- `it stays alive but every wake re-fails until the fault clears. ` +
910
- (isAuth
911
- ? `Its provider credential is dead — re-authenticate (\`/login\`, or re-auth crtr); the child recovers on its next wake once the auth reloads. `
912
- : ``) +
913
- `Error: ${firstLine}`;
914
- if (terminalOverflow) {
915
- try {
916
- transition(id, 'crash');
917
- headlessBrokerHost.teardown(id);
918
- }
919
- catch {
920
- /* the terminal notice remains useful if teardown raced the broker exit */
921
- }
922
- }
923
- emitEvent({
924
- level: 'error',
925
- event: 'fault.fatal.fanout',
926
- ...(isSafeNodeId(id) ? { node_id: id } : {}),
927
- fields: {
928
- ...(isSafeNodeId(id) ? {} : { affected_node_id: id }),
929
- kind: fault.kind,
930
- link: fault.link,
931
- terminal_overflow: terminalOverflow,
932
- },
933
- });
934
- try {
935
- fanDoctrineWake(id, subscribersOf(id), label, {
936
- reason: terminalOverflow ? 'child-context-overflow' : 'child-fault-fatal',
937
- child: id,
938
- kind: fault.kind,
939
- message: firstLine,
940
- });
941
- }
942
- catch {
943
- /* best-effort, mirrors every other fan-out in this file */
944
- }
945
- }
946
- // ---------------------------------------------------------------------------
947
- // Auth-reload fan (one /login → every live broker follows)
948
- // ---------------------------------------------------------------------------
949
- /** The credential files every broker reads: pi's shared `auth.json` plus the managed
950
- * providers' subscription pools, which own the anthropic / openai-codex credentials
951
- * outright (see `PoolCredentialStore`) — a managed login touches ONLY a pool file, so
952
- * watching auth.json alone would miss it. VENDORED from pi `config.getAuthPath()` (=
953
- * `<agentDir>/auth.json`) the same way spawn.ts vendors the sessions root: a ROOT import
954
- * of pi's `getAgentDir` would eager-load the heavy pi SDK index, so we mirror its
955
- * resolution — `PI_CODING_AGENT_DIR` env, else `~/.pi/agent`. Re-sync on a pi SDK bump
956
- * that moves auth.json. */
957
- function piCredentialPaths() {
958
- const env = process.env['PI_CODING_AGENT_DIR'];
959
- const agentDir = env !== undefined && env !== '' ? env : join(homedir(), '.pi', 'agent');
960
- return [
961
- join(agentDir, 'auth.json'),
962
- join(agentDir, 'anthropic-subscriptions.json'),
963
- join(agentDir, 'openai-codex-subscriptions.json'),
964
- ];
965
- }
966
- // Last-observed credential mtime, for the daemon's change-watch. Lazily seeded on
967
- // the first poll that sees the file (so daemon start never fans a spurious
968
- // reload), then updated on every detected change. In-memory only — a daemon
969
- // restart re-seeds from the current file, so at most one missed change spans the
970
- // restart gap (re-login during a ~1s restart), which a human just redoes.
971
- let knownAuthMtimeMs = null;
972
- /** Watch the shared auth.json mtime and, on a change, fan a live `reload_auth`
973
- * to every live broker so one `/login` propagates across the whole canvas. The
974
- * broker that performed the login already self-reloaded; re-reloading it is
975
- * harmless (authStorage.reload() is idempotent), so we don't exclude it.
976
- *
977
- * Rides the existing ~2s poll, so several rapid writes from one login collapse
978
- * to a single observed mtime change → a single fan. Each broker is reloaded
979
- * under its own try/catch (Promise.allSettled): a broker mid-shutdown or whose
980
- * socket refuses connect is logged and skipped, never aborting the others. */
981
- async function handleAuthReload(now) {
982
- // The newest mtime across the credential files: any one of them changing moves it forward,
983
- // and a file that does not exist yet simply contributes nothing.
984
- let mtimeMs = 0;
985
- for (const path of piCredentialPaths()) {
986
- try {
987
- mtimeMs = Math.max(mtimeMs, statSync(path).mtimeMs);
988
- }
989
- catch {
990
- /* not written yet — nothing to observe for this file */
991
- }
992
- }
993
- if (mtimeMs === 0)
994
- return; // never logged in — nothing to watch
995
- if (knownAuthMtimeMs === null) {
996
- knownAuthMtimeMs = mtimeMs; // seed on first sight; never fan on daemon start
997
- return;
998
- }
999
- if (mtimeMs === knownAuthMtimeMs)
1000
- return;
1001
- knownAuthMtimeMs = mtimeMs;
1002
- await operationIdContext.fresh(async () => {
1003
- try {
1004
- // Every fleet entry IS a live broker (design D-1) — no row scan, no pid
1005
- // probe. Enumerated only when auth actually changed (the rare path).
1006
- const live = Array.from(boundFleet().entries(), ([id]) => id);
1007
- if (live.length === 0)
1008
- return;
1009
- const results = await Promise.allSettled(live.map(async (id) => {
1010
- try {
1011
- await reloadAuthLive(id);
1012
- }
1013
- catch (err) {
1014
- emitEvent({
1015
- level: 'error',
1016
- event: 'auth.reload.broker_failed',
1017
- ...(isSafeNodeId(id) ? { node_id: id } : { fields: { broker_id: id } }),
1018
- error: err,
1019
- });
1020
- throw err;
1021
- }
1022
- }));
1023
- const ok = results.filter((r) => r.status === 'fulfilled').length;
1024
- emitEvent({
1025
- level: 'info',
1026
- event: 'auth.reload.completed',
1027
- outcome: ok === live.length ? 'succeeded' : 'failed',
1028
- fields: {
1029
- broker_count: live.length,
1030
- succeeded_count: ok,
1031
- failed_count: live.length - ok,
1032
- },
1033
- });
1034
- }
1035
- catch (err) {
1036
- emitEvent({ level: 'error', event: 'auth.reload.failed', error: err });
1037
- }
1038
- });
1039
- }
1040
- // retryResumeMode moved to ./fleet.js (the cycle-vs-resume arbiter inside
1041
- // classifyDeadNode); re-exported above for the historical crtrd.ts surface.
1042
- // How long a dead node's on-disk record must have been QUIET (no meta/telemetry
1043
- // write) before its leftover placement is reaped. A fresh crash keeps its pane
1044
- // around this long so a human can read the corpse; a long-dead node's orphaned
1045
- // pane is torn down on sight.
1046
- export const DEAD_REAP_GRACE_MS = 10 * 60_000;
1047
- /** ms since the node's durable record was last written (meta.json / job/
1048
- * telemetry.json mtimes — the files every live cycle touches). Infinity when
1049
- * neither exists: no disk evidence reads as ancient, not fresh. Deliberately
1050
- * NOT inbox.jsonl — children keep appending to a dead manager's inbox, which
1051
- * must not hold its corpse un-reapable forever. */
1052
- function msSinceDiskActivity(nodeId, now) {
1053
- let latest = 0;
1054
- for (const p of [join(nodeDir(nodeId), 'meta.json'), join(jobDir(nodeId), 'telemetry.json')]) {
1055
- try {
1056
- const m = statSync(p).mtimeMs;
1057
- if (m > latest)
1058
- latest = m;
1059
- }
1060
- catch {
1061
- /* absent — contributes nothing */
1062
- }
1063
- }
1064
- return latest === 0 ? Number.POSITIVE_INFINITY : now - latest;
1065
- }
1066
- /** Reap the leftover PLACEMENT of long-dead nodes (audit 2026-06-09, Bug 3 —
1067
- * node mq3l84it-40f6e8ac, a dead resident whose orphaned pane + focus row sat
1068
- * untouched for ~36h). The daemon supervises only active|idle rows, so a node
1069
- * that lands `dead` while still holding a pane is invisible to every pass and
1070
- * its residue leaks forever.
1071
- *
1072
- * Deliberate REAP-not-REVIVE decision: a dead node is NOT auto-revived. `dead`
1073
- * means crashed — auto-resurrecting a crashed RESIDENT (a human conversation)
1074
- * would pop dead chats back open unbidden, and the daemon's contract is to
1075
- * supervise live nodes, not to undo terminal states. A dead resident's wake
1076
- * sources remain the human ones (`node message send --tier critical`, focus, `canvas
1077
- * revive`), all of which still work after this reap. What we DO reclaim is the
1078
- * physical residue: kill the orphaned pane, drop the focus row, null the
1079
- * LOCATION (tearDownNode). The node row, its edges, and all on-disk state stay
1080
- * intact, so a later manual revive keeps its full graph — the leak was the
1081
- * pane/focus/presence, not the row. Grace: only after DEAD_REAP_GRACE_MS of
1082
- * disk quiet, so a just-crashed pane survives long enough to be inspected. */
1083
- function reapDeadResidue(now) {
1084
- let rows;
1085
- try {
1086
- rows = listNodes({ status: ['dead'] });
1087
- }
1088
- catch (err) {
1089
- operationIdContext.fresh(() => {
1090
- emitEvent({ level: 'error', event: 'residue.reap_scan.failed', error: err });
1091
- });
1092
- return;
1093
- }
1094
- for (const row of rows) {
1095
- if (row.pane == null && row.window == null && row.tmux_session == null)
1096
- continue;
1097
- try {
1098
- if (msSinceDiskActivity(row.node_id, now) < DEAD_REAP_GRACE_MS)
1099
- continue;
1100
- }
1101
- catch (err) {
1102
- operationIdContext.fresh(() => {
1103
- emitEvent({
1104
- level: 'error',
1105
- event: 'residue.reap.failed',
1106
- ...(isSafeNodeId(row.node_id) ? { node_id: row.node_id } : {}),
1107
- error: err,
1108
- fields: {
1109
- ...(isSafeNodeId(row.node_id) ? {} : { affected_node_id: row.node_id }),
1110
- pane: row.pane,
1111
- window: row.window,
1112
- tmux_session: row.tmux_session,
1113
- },
1114
- });
1115
- });
1116
- continue;
1117
- }
1118
- operationIdContext.fresh(() => {
1119
- try {
1120
- emitEvent({
1121
- level: 'info',
1122
- event: 'residue.reaped',
1123
- ...(isSafeNodeId(row.node_id) ? { node_id: row.node_id } : {}),
1124
- fields: {
1125
- ...(isSafeNodeId(row.node_id) ? {} : { affected_node_id: row.node_id }),
1126
- pane: row.pane,
1127
- window: row.window,
1128
- tmux_session: row.tmux_session,
1129
- },
1130
- });
1131
- tearDownNode(row.node_id);
1132
- }
1133
- catch (err) {
1134
- emitEvent({
1135
- level: 'error',
1136
- event: 'residue.reap.failed',
1137
- ...(isSafeNodeId(row.node_id) ? { node_id: row.node_id } : {}),
1138
- error: err,
1139
- fields: {
1140
- ...(isSafeNodeId(row.node_id) ? {} : { affected_node_id: row.node_id }),
1141
- pane: row.pane,
1142
- window: row.window,
1143
- tmux_session: row.tmux_session,
1144
- },
1145
- });
1146
- }
1147
- });
1148
- }
1149
- }
1150
- /** GC stale focus rows (audit 2026-06-09, Bug 5): nothing else deletes a focuses
1151
- * row when its pane dies outside the sanctioned teardown paths (a user
1152
- * kill-pane, a tmux server restart, a crashed swap), so dead-pane rows
1153
- * accumulate forever — the audit found 15 of 21 rows pointing at gone panes,
1154
- * and a stale row both misreports the node's on-screen viewer and blocks its
1155
- * node from being adopted into a fresh viewport (UNIQUE node_id). Sweep them here: one
1156
- * batched `list-panes -a` probe (never a per-row display-message), deleting
1157
- * rows whose recorded pane no longer exists. A pane-less row (a bridge/unplaced
1158
- * viewport) has nothing to verify and is left alone. On a FAILED probe (tmux
1159
- * unreachable — daemon racing a server restart) we skip the sweep entirely:
1160
- * "can't tell" must never mass-delete every viewport. */
1161
- function gcStaleFocuses() {
1162
- let rows;
1163
- try {
1164
- rows = listFocuses();
1165
- }
1166
- catch (err) {
1167
- operationIdContext.fresh(() => {
1168
- emitEvent({ level: 'error', event: 'focus.gc.failed', error: err });
1169
- });
1170
- return;
1171
- }
1172
- if (rows.length === 0)
1173
- return;
1174
- let live;
1175
- try {
1176
- live = listLivePanes();
1177
- }
1178
- catch (err) {
1179
- operationIdContext.fresh(() => {
1180
- emitEvent({ level: 'error', event: 'focus.gc.failed', error: err });
1181
- });
1182
- return;
1183
- }
1184
- if (live === null)
1185
- return; // probe failed — never GC on "can't tell"
1186
- for (const f of rows) {
1187
- if (f.pane === null || live.has(f.pane))
1188
- continue;
1189
- const removed = operationIdContext.fresh(() => {
1190
- try {
1191
- emitEvent({
1192
- level: 'info',
1193
- event: 'focus.gc.removed',
1194
- ...(isSafeNodeId(f.node_id) ? { node_id: f.node_id } : {}),
1195
- fields: {
1196
- focus_id: f.focus_id,
1197
- focus_node_id: f.node_id,
1198
- pane: f.pane,
1199
- },
1200
- });
1201
- closeFocusRow(f.focus_id);
1202
- return true;
1203
- }
1204
- catch (err) {
1205
- emitEvent({ level: 'error', event: 'focus.gc.failed', error: err });
1206
- return false;
1207
- }
1208
- });
1209
- if (!removed)
1210
- return;
1211
- }
1212
- }
1213
269
  // ---------------------------------------------------------------------------
1214
270
  // Pidfile
1215
271
  // ---------------------------------------------------------------------------
@@ -1245,25 +301,23 @@ function removeOwnedPidfileMirror(pid) {
1245
301
  throw error;
1246
302
  }
1247
303
  }
1248
- // isPidAlive now lives in canvas/pid.ts (the one shared signal-0 probe at the
1249
- // lowest layer, so canvas/, runtime/, AND daemon/ all import it down). Re-
1250
- // exported here to preserve the public surface consumed by commands/sys/daemon.ts
1251
- // and the grace-clock test.
1252
- export { isPidAlive };
1253
304
  const directTickLifecycle = {
1254
305
  acceptsDetachedWork: () => true,
1255
306
  registerDetached: (work) => {
1256
307
  void work.catch(() => { });
1257
308
  },
1258
309
  };
310
+ const brokerSupervision = new BrokerSupervisionReconciler();
311
+ const controllerDeath = new ControllerDeathReconciler();
312
+ const dormantInbox = new DormantInboxReconciler();
313
+ const cronLane = new CronLaneReconciler();
314
+ const storageMaintenance = new StorageMaintenanceReconciler();
1259
315
  /** Supervision tick over the daemon's current live set. */
1260
316
  export async function superviseTick(now = Date.now(), lifecycle = directTickLifecycle) {
1261
317
  const fleet = boundFleet();
1262
- // Auth-reload fan FIRST, independent of node supervision: a change to the
1263
- // shared auth.json propagates a live credential reload to every broker. Kept
1264
- // ahead of the listNodes guard below so a transient list error in the
1265
- // supervision passes never skips an auth fan (it does its own enumeration).
1266
- await handleAuthReload(now);
318
+ // Auth reload must precede the row-list guard: it is fleet-wide and does not
319
+ // depend on a successful canvas scan.
320
+ const thresholds = await brokerSupervision.run(now, { fleet, lifecycle });
1267
321
  let rows;
1268
322
  try {
1269
323
  rows = listNodes({ status: ['active', 'idle'] });
@@ -1272,285 +326,15 @@ export async function superviseTick(now = Date.now(), lifecycle = directTickLife
1272
326
  emitEvent({ level: 'error', event: 'canvas.node_list.failed', error: err });
1273
327
  return;
1274
328
  }
1275
- // kind:'human' rows are NOT broker-hosted agent nodes — they are bookkeeping
1276
- // rows for the `crtr human` bridge. crtrd delivers their terminal ticket result
1277
- // with pushFinal when the person responds from crouter's inbox, or when `human cancel`
1278
- // publishes a cancellation. They never launch a broker or record a `pi_session_id`, so the dead-node policy
1279
- // table would read every one as a never-booted broker (row 8) and crash it
1280
- // with surfaceBootFailure — exactly the regression where a headless
1281
- // orchestrator's `human ask` nodes all died at boot. The daemon must never
1282
- // supervise them: drop them from every row-driven pass here (the startup
1283
- // recovery sweep filters them identically).
1284
- rows = rows.filter((r) => r.kind !== 'human');
1285
- // Broker-pressure thresholds gate this tick's daemon-initiated launches; the
1286
- // live count is EXACTLY the fleet size (design D-7) — every live broker is a
1287
- // fleet entry, no census, no row inference.
1288
- const thresholds = brokerThresholdsForDaemon();
1289
- // The warning stays detached so tmux cannot hold the supervision tick. Its
1290
- // promise is registered synchronously for invocation teardown.
1291
- if (fleet.size() >= thresholds.warning && lifecycle.acceptsDetachedWork()) {
1292
- const warning = operationIdContext.fresh(() => warnOverCapInTmux(fleet.size(), thresholds.warning, thresholds.automaticReviveCap));
1293
- lifecycle.registerDetached(warning);
1294
- }
1295
- // First pass: LIVE-fleet checks. Every fleet entry IS a live broker (design
1296
- // D-1) — no pid probe, no grace clock. Dead-node recovery does NOT run here:
1297
- // a broker's real exit event already enqueued an exit-policy job on the
1298
- // serial work lane (fleet.onChildExit → classifyDeadNode), so by the time a
1299
- // tick runs, every dead node has been classified. This pass inspects only
1300
- // the conditions an exit event can never surface on a LIVE engine: a stalled
1301
- // refresh (handleYieldStall), a wedged mid-turn engine (handleWedgeDetection),
1302
- // and a parked fatal provider fault (handleFatalFault).
1303
- for (const [id, entry] of Array.from(fleet.entries())) {
1304
- await operationIdContext.fresh(async () => {
1305
- try {
1306
- const row = getRow(id);
1307
- if (row === null || (row.status !== 'active' && row.status !== 'idle'))
1308
- return;
1309
- handleYieldStall(row, entry.pid, now);
1310
- await handleWedgeDetection(id, entry.pid, row.pi_pid_identity, now);
1311
- handleFatalFault(id, now);
1312
- }
1313
- catch (err) {
1314
- // One bad node must never kill the loop.
1315
- emitEvent({
1316
- level: 'error',
1317
- event: 'broker.supervision.failed',
1318
- ...(isSafeNodeId(id) ? { node_id: id } : { fields: { affected_node_id: id } }),
1319
- error: err,
1320
- });
1321
- }
1322
- });
1323
- }
1324
- // Settle every wait whose controller has ended, independent of the waiter's
1325
- // current status. Active and focused terminal nodes own a live inbox watcher;
1326
- // idle-released nodes are revived by the second pass below for this same row.
1327
- for (const row of rows) {
1328
- const current = getRow(row.node_id);
1329
- const controllerId = current?.waiting_for;
1330
- if (controllerId === null || controllerId === undefined)
1331
- continue;
1332
- // Read-gate before the write boundary: only a dead controller warrants a
1333
- // notice, and the vast majority of live waits would otherwise open a
1334
- // write-lock transaction every tick just to no-op. settleDeadMessageWait
1335
- // re-checks liveness inside the transaction, so this gate cannot misfire.
1336
- const controller = getRow(controllerId);
1337
- if (controller !== null && (controller.status === 'active' || controller.status === 'idle'))
1338
- continue;
1339
- operationIdContext.fresh(() => {
1340
- appendControllerDeathNotice(row.node_id, controllerId);
1341
- });
1342
- }
1343
- // Second pass: revive idle-released nodes whose inbox has unseen entries.
1344
- // The in-process inbox-watcher dies with pi, so the daemon owns wake-on-message
1345
- // for dormant nodes. readCursor is the exact entry identity the watcher
1346
- // persisted before exit; later physical entries are undelivered work.
1347
- //
1348
- // Backoff + attempt cap is keyed on CURSOR IDENTITY (2026-07-10 incident,
1349
- // round 2 — see the idleReleaseRelaunchState doc above for the full failure-
1350
- // mode writeup): a node whose delivery keeps failing — whether the broker
1351
- // never boots, OR it boots and then faults its turn before changing the
1352
- // cursor — used to be relaunched here every poll (round 1) or every fault-
1353
- // recovery cycle (round 1's fix, still exploitable), forever. The counter now
1354
- // resets only when readCursor(id) differs from the prior exact identity —
1355
- // proof the wake accomplished something — never on a bare status flap or a
1356
- // transiently-alive engine.
1357
- // Capacity-checked too, so a burst of due idle-release wakes can't stampede
1358
- // past the live-broker cap either.
1359
- for (const row of rows) {
1360
- const id = row.node_id;
1361
- let r;
1362
- try {
1363
- // Re-read the ROW for fresh runtime (the first pass may have mutated it);
1364
- // no meta needed — status/intent live in the row.
1365
- r = getRow(id);
1366
- if (r === null) {
1367
- clearIdleReleaseRelaunchState(id); // node gone — nothing left to track
1368
- continue;
1369
- }
1370
- // NOT idle-release right now (e.g. mid fault-recovery, briefly active) —
1371
- // leave idleReleaseRelaunchState UNTOUCHED. Clearing here would let the
1372
- // fault-recovery cycle (idle-release → briefly alive → fault → idle-
1373
- // release again) reset the counter every episode — the exact round-2 bug.
1374
- if (r.status !== 'idle' || r.intent !== 'idle-release')
1375
- continue;
1376
- // The in-process inbox-watcher only owns delivery while the engine is LIVE.
1377
- // A released node's broker is DEAD (it freed itself while dormant), so it
1378
- // has no watcher and the daemon must wake it. A fleet entry IS the live
1379
- // engine (design D-1) — no pid probe, no identity check. Same no-clear
1380
- // rule as above — a transiently-alive engine proves nothing about a
1381
- // cursor identity change.
1382
- if (fleet.has(id))
1383
- continue;
1384
- }
1385
- catch (err) {
1386
- operationIdContext.fresh(() => {
1387
- emitEvent({
1388
- level: 'error',
1389
- event: 'inbox.poll.failed',
1390
- ...(isSafeNodeId(id) ? { node_id: id } : { fields: { affected_node_id: id } }),
1391
- error: err,
1392
- });
1393
- });
1394
- continue;
1395
- }
1396
- operationIdContext.fresh(() => {
1397
- try {
1398
- const cursorNow = readCursor(id);
1399
- const entries = readInboxSince(id, cursorNow);
1400
- // #341: deferred entries never wake an idle node — by contract they ride
1401
- // the target's next NATURAL cycle (any other wake reason), not a revive
1402
- // this pass triggers on their own account. A deferred-only unseen set is
1403
- // therefore identical to "nothing unseen" for wake-worthiness: no revive,
1404
- // and no stuck-relaunch episode to track (there is nothing this pass is
1405
- // even attempting). The entries themselves are untouched — still past the
1406
- // cursor, still on disk — so a later revive for any OTHER reason (a
1407
- // non-deferred entry landing, or an explicit revive) delivers them via the
1408
- // live watcher's normal flush, same as `entries.length === 0` below.
1409
- const wakeWorthy = entries.filter((e) => e.tier !== 'deferred');
1410
- if (wakeWorthy.length === 0) {
1411
- clearIdleReleaseRelaunchState(id); // fully delivered, or only deferred entries remain — no stuck episode to track
1412
- return;
1413
- }
1414
- const prior = idleReleaseRelaunchState.get(id);
1415
- // An exact cursor identity change since the LAST observation (or no prior
1416
- // episode at all) is the ONLY thing allowed to reset the counter.
1417
- const cursorChanged = prior === undefined || prior.cursor !== cursorNow;
1418
- const attempts = cursorChanged ? 0 : prior.attempts;
1419
- if (attempts >= IDLE_RELEASE_RELAUNCH_MAX_ATTEMPTS) {
1420
- clearIdleReleaseRelaunchState(id);
1421
- const meta = getNode(id);
1422
- emitEvent({
1423
- level: 'warn',
1424
- event: 'broker.idle_release.exhausted',
1425
- ...(isSafeNodeId(id) ? { node_id: id } : {}),
1426
- fields: {
1427
- ...(isSafeNodeId(id) ? {} : { affected_node_id: id }),
1428
- attempts,
1429
- max_attempts: IDLE_RELEASE_RELAUNCH_MAX_ATTEMPTS,
1430
- },
1431
- });
1432
- transition(id, 'crash');
1433
- if (meta !== null) {
1434
- try {
1435
- fanDoctrineWake(id, subscribersOf(id), `Child crashed — ${fullName(meta)} (${id}) failed to relaunch after ${attempts} attempts while trying to ` +
1436
- `deliver a pending inbox message (no delivery progress across any attempt), and is now dead. It stays ` +
1437
- `dead until you revive it — \`crtr node lifecycle revive ${id}\`.`, { reason: 'child-crashed', child: id });
1438
- }
1439
- catch {
1440
- /* best-effort, mirrors every other fan-out in this file */
1441
- }
1442
- }
1443
- return;
1444
- }
1445
- const lastAttempt = cursorChanged ? undefined : prior?.lastAttempt;
1446
- const backoff = idleReleaseRelaunchBackoffMs(attempts);
1447
- if (lastAttempt !== undefined && now - lastAttempt < backoff)
1448
- return; // backing off before the next attempt
1449
- if (!hasBrokerCapacity(thresholds.automaticReviveCap))
1450
- return; // deferred, not attempted — costs no retry budget
1451
- idleReleaseRelaunchState.set(id, { cursor: cursorNow, attempts: attempts + 1, lastAttempt: now });
1452
- emitEvent({
1453
- level: 'info',
1454
- event: 'broker.revive.idle_release',
1455
- ...(isSafeNodeId(id) ? { node_id: id } : {}),
1456
- attempt: attempts + 1,
1457
- fields: {
1458
- ...(isSafeNodeId(id) ? {} : { affected_node_id: id }),
1459
- attempts: attempts + 1,
1460
- max_attempts: IDLE_RELEASE_RELAUNCH_MAX_ATTEMPTS,
1461
- },
1462
- });
1463
- reviveNode(id, { resume: true });
1464
- }
1465
- catch (err) {
1466
- emitEvent({
1467
- level: 'error',
1468
- event: 'inbox.poll.failed',
1469
- ...(isSafeNodeId(id) ? { node_id: id } : { fields: { affected_node_id: id } }),
1470
- error: err,
1471
- });
1472
- }
1473
- });
1474
- }
1475
- // Third pass: recover stale cron run leases stranded by a prior daemon
1476
- // instance, then kick off every due cron. Runs stay detached from the tick
1477
- // and registered for graceful-teardown drain.
1478
- operationIdContext.fresh(() => {
1479
- try {
1480
- recoverStaleCronLeases(now, { register: (settlement) => lifecycle.registerDetached(settlement) });
1481
- }
1482
- catch (err) {
1483
- emitEvent({ level: 'error', event: 'cron.lease_recovery.failed', error: err });
1484
- }
1485
- });
1486
- if (lifecycle.acceptsDetachedWork()) {
1487
- try {
1488
- runDueCrons(now, {
1489
- register: (settlement) => lifecycle.registerDetached(settlement),
1490
- // Fleet safety: a due run that would launch or revive a node waits for
1491
- // a free slot instead of stampeding the host; it stays due meanwhile.
1492
- hasBrokerCapacity: () => hasBrokerCapacity(thresholds.automaticReviveCap),
1493
- });
1494
- }
1495
- catch (err) {
1496
- emitEvent({ level: 'error', event: 'cron.run.failed', error: err });
1497
- }
1498
- }
1499
- // Fourth pass: reap the leftover placement of long-dead nodes (Bug 3), then
1500
- // sweep focus rows whose pane died outside the sanctioned teardown paths
1501
- // (Bug 5). Reap first so the GC also catches any focus row a reaped pane
1502
- // strands; last overall so neither races a focus a same-tick revive just
1503
- // re-anchored.
1504
- reapDeadResidue(now);
1505
- gcStaleFocuses();
1506
- // Fifth pass: count-based auto-prune, throttled to once per PRUNE_INTERVAL_MS
1507
- // (NOT every 2s tick — it's a full-canvas DB sweep). Keeps the AUTO_PRUNE_MAX
1508
- // most-recently-active nodes; live nodes are never deleted (pruneToLimit's
1509
- // floor is max(AUTO_PRUNE_MAX, liveCount)). Wrapped so a sweep failure never
1510
- // crashes the tick; the tick is already serialized (serial work lane), so
1511
- // this runs sequentially within it.
1512
- if (now - lastPruneAt >= PRUNE_INTERVAL_MS) {
1513
- lastPruneAt = now;
1514
- operationIdContext.fresh(() => {
1515
- try {
1516
- const before = listNodes().length;
1517
- const { pruned } = pruneToLimit({ maxNodes: AUTO_PRUNE_MAX });
1518
- if (pruned.length > 0) {
1519
- emitEvent({
1520
- level: 'info',
1521
- event: 'canvas.auto_prune.completed',
1522
- outcome: 'succeeded',
1523
- fields: {
1524
- before_count: before,
1525
- kept_count: before - pruned.length,
1526
- pruned_count: pruned.length,
1527
- },
1528
- });
1529
- }
1530
- }
1531
- catch (err) {
1532
- emitEvent({ level: 'error', event: 'canvas.auto_prune.failed', error: err });
1533
- }
1534
- });
1535
- }
1536
- // Sixth pass: warm-pool GC. Collects spares whose broker is gone (every
1537
- // handover kills them and nothing resumes a spare) and spares nobody claimed
1538
- // inside their TTL — including the ones stranded when a runtime install
1539
- // re-keyed the pool. Throttled; never throws.
1540
- if (now - lastSpareSweepAt >= SPARE_SWEEP_INTERVAL_MS) {
1541
- lastSpareSweepAt = now;
1542
- operationIdContext.fresh(() => {
1543
- const reaped = reapStaleSpares();
1544
- if (reaped.length > 0) {
1545
- emitEvent({
1546
- level: 'info',
1547
- event: 'warm_pool.spares_reaped',
1548
- outcome: 'succeeded',
1549
- fields: { reaped_count: reaped.length },
1550
- });
1551
- }
1552
- });
1553
- }
329
+ // Human bridge rows have no broker engine and participate in none of the
330
+ // recurring row-driven reconciler passes. Startup recovery applies the same
331
+ // classification boundary.
332
+ rows = rows.filter((row) => row.kind !== 'human');
333
+ const hasBrokerCapacity = () => brokerSupervision.hasBrokerCapacity(thresholds.automaticReviveCap, fleet);
334
+ controllerDeath.run(now, { rows });
335
+ dormantInbox.run(now, { rows, fleet, hasBrokerCapacity });
336
+ cronLane.run(now, { lifecycle, hasBrokerCapacity });
337
+ storageMaintenance.run(now);
1554
338
  }
1555
339
  /** Start the supervisor loop after winning the authoritative canvas claim.
1556
340
  *