@north-light/crouter 0.3.197 → 0.3.199

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 (339) hide show
  1. package/dist/api/client.d.ts +19 -19
  2. package/dist/api/client.js +29 -37
  3. package/dist/api/dto/broker-ops.d.ts +3 -1
  4. package/dist/api/dto/broker.d.ts +13 -2
  5. package/dist/api/dto/broker.js +4 -4
  6. package/dist/api/dto/human.d.ts +6 -5
  7. package/dist/api/dto/inbox.d.ts +126 -33
  8. package/dist/api/dto/nodes.d.ts +8 -2
  9. package/dist/api/dto/profiles.d.ts +23 -0
  10. package/dist/api/dto/profiles.js +2 -2
  11. package/dist/api/dto/worktree.d.ts +3 -5
  12. package/dist/api/routes.d.ts +3 -5
  13. package/dist/api/routes.js +4 -6
  14. package/dist/builtin-memory/00-runtime-base.md +8 -8
  15. package/dist/builtin-memory/01-spine/01-no-manager.md +1 -1
  16. package/dist/builtin-memory/02-lifecycle/00-terminal.md +3 -3
  17. package/dist/builtin-memory/04-orchestration-kernel.md +6 -6
  18. package/dist/builtin-memory/05-kinds/design/00-base.md +1 -1
  19. package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +2 -2
  20. package/dist/builtin-memory/05-kinds/review/00-base.md +2 -2
  21. package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +1 -1
  22. package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +1 -1
  23. package/dist/builtin-memory/insights/capture.md +2 -2
  24. package/dist/builtin-memory/insights/init.md +3 -3
  25. package/dist/builtin-memory/internal/INDEX.md +1 -1
  26. package/dist/builtin-memory/internal/memory-loading.md +1 -1
  27. package/dist/builtin-memory/internal/nodes-and-canvas.md +3 -3
  28. package/dist/builtin-memory/internal/plugins.md +59 -2
  29. package/dist/builtin-memory/internal/storage-tiers.md +3 -1
  30. package/dist/builtin-memory/spec/roadmap.md +2 -2
  31. package/dist/builtin-memory/testing.md +1 -1
  32. package/dist/builtin-memory/wedged-child-on-runaway-bash.md +1 -1
  33. package/dist/clients/attach/chrome/bash-jobs.js +3 -3
  34. package/dist/clients/attach/render/chat-view.d.ts +29 -9
  35. package/dist/clients/attach/render/chat-view.js +64 -35
  36. package/dist/clients/attach/render/markdown-source.js +0 -6
  37. package/dist/clients/attach/render/page-block.d.ts +34 -0
  38. package/dist/clients/attach/render/page-block.js +226 -0
  39. package/dist/clients/attach/render/tool-calls.js +16 -2
  40. package/dist/clients/attach/session/whip.d.ts +2 -0
  41. package/dist/clients/attach/session/whip.js +2 -0
  42. package/dist/clients/attach/viewer.js +1419 -982
  43. package/dist/clients/conversation/projection.js +18 -3
  44. package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +9 -6
  45. package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +13 -14
  46. package/dist/clients/inbox/controller.js +10 -16
  47. package/dist/clients/inbox/page-adapter.d.ts +3 -4
  48. package/dist/clients/inbox/page-adapter.js +3 -4
  49. package/dist/clients/inbox/resolve.d.ts +1 -1
  50. package/dist/clients/inbox/resolve.js +3 -3
  51. package/dist/clients/inbox/review/document-surface.js +3 -1
  52. package/dist/clients/inbox/review/state.d.ts +2 -1
  53. package/dist/clients/inbox/review/state.js +5 -16
  54. package/dist/clients/inbox/tui/input.js +37 -19
  55. package/dist/clients/inbox/tui/panel.js +5 -31
  56. package/dist/clients/inbox/tui/render.d.ts +11 -0
  57. package/dist/clients/inbox/tui/render.js +101 -36
  58. package/dist/clients/inbox/tui/slots.d.ts +4 -4
  59. package/dist/clients/inbox/tui/slots.js +19 -44
  60. package/dist/clients/inbox/tui/types.d.ts +6 -10
  61. package/dist/clients/inbox/tui.js +2 -2
  62. package/dist/commands/__tests__/human.test.js +118 -24
  63. package/dist/commands/__tests__/node-message.test.js +14 -0
  64. package/dist/commands/attention.js +4 -4
  65. package/dist/commands/canvas-browse.js +1 -1
  66. package/dist/commands/canvas-snapshot.js +1 -1
  67. package/dist/commands/canvas.js +1 -1
  68. package/dist/commands/dashboard.js +1 -1
  69. package/dist/commands/human/{doc.d.ts → components.d.ts} +1 -1
  70. package/dist/commands/human/components.js +77 -0
  71. package/dist/commands/human/feedback.d.ts +2 -0
  72. package/dist/commands/human/feedback.js +108 -0
  73. package/dist/commands/human/prompts.d.ts +17 -8
  74. package/dist/commands/human/prompts.js +217 -226
  75. package/dist/commands/human/queue.js +57 -88
  76. package/dist/commands/human/shared.d.ts +4 -15
  77. package/dist/commands/human/shared.js +19 -61
  78. package/dist/commands/human.js +32 -19
  79. package/dist/commands/memory/origin.js +2 -2
  80. package/dist/commands/node/bash.js +4 -4
  81. package/dist/commands/node/inspect.js +3 -3
  82. package/dist/commands/node/lifecycle.js +1 -1
  83. package/dist/commands/node/message.js +2 -2
  84. package/dist/commands/node-worktree.js +8 -8
  85. package/dist/commands/pkg/browse.js +1 -1
  86. package/dist/commands/pkg/plugin-manage.js +129 -10
  87. package/dist/commands/profile/delete.js +41 -15
  88. package/dist/commands/profile.js +1 -1
  89. package/dist/commands/push.js +1 -1
  90. package/dist/commands/surface/node/focus.js +29 -14
  91. package/dist/commands/surface-edit.js +3 -3
  92. package/dist/commands/surface-inbox.d.ts +2 -0
  93. package/dist/commands/{human/inbox.js → surface-inbox.js} +14 -18
  94. package/dist/commands/surface.js +2 -1
  95. package/dist/commands/sys/doctor.js +60 -3
  96. package/dist/commands/sys/feedback.js +79 -25
  97. package/dist/commands/sys/settings.js +1 -1
  98. package/dist/core/__tests__/cron-broker-capacity.test.js +1 -1
  99. package/dist/core/__tests__/daemon-boot.test.js +7 -4
  100. package/dist/core/__tests__/dead-node-policy-table.test.js +20 -36
  101. package/dist/core/__tests__/extension-abort.test.js +5 -2
  102. package/dist/core/__tests__/fixtures/fake-engine.d.ts +13 -2
  103. package/dist/core/__tests__/fixtures/fake-engine.js +47 -3
  104. package/dist/core/__tests__/human-cancel-guard.test.js +31 -10
  105. package/dist/core/__tests__/human-deliver.test.js +41 -45
  106. package/dist/core/__tests__/human-node-not-supervised.test.js +1 -1
  107. package/dist/core/__tests__/plugin-page-components.test.js +157 -0
  108. package/dist/core/__tests__/seam/broker-crash-teardown.test.js +13 -11
  109. package/dist/core/__tests__/seam/dormancy-release.test.js +103 -4
  110. package/dist/core/__tests__/serial/flagship-lifecycle.test.js +2 -2
  111. package/dist/core/__tests__/serial/human-deliver-e2e.test.js +12 -16
  112. package/dist/core/__tests__/serial/live-mutation.test.js +1 -1
  113. package/dist/core/__tests__/serial/tmux-surface.test.js +4 -2
  114. package/dist/core/__tests__/serial/worktree.test.js +24 -198
  115. package/dist/core/__tests__/session-cycles.test.js +22 -0
  116. package/dist/core/__tests__/stop-guard.test.js +4 -4
  117. package/dist/core/__tests__/watchdog-abort-arms-retry.test.js +19 -4
  118. package/dist/core/bash-jobs.d.ts +6 -0
  119. package/dist/core/bash-jobs.js +10 -0
  120. package/dist/core/canvas/__tests__/attention.test.js +8 -5
  121. package/dist/core/canvas/__tests__/render-remote.test.js +5 -5
  122. package/dist/core/canvas/attention.js +8 -6
  123. package/dist/core/canvas/browse/app.d.ts +1 -1
  124. package/dist/core/canvas/browse/app.js +21 -3
  125. package/dist/core/canvas/browse/model.d.ts +8 -2
  126. package/dist/core/canvas/browse/model.js +11 -4
  127. package/dist/core/canvas/browse/render.d.ts +4 -0
  128. package/dist/core/canvas/browse/render.js +5 -1
  129. package/dist/core/canvas/canvas.d.ts +33 -0
  130. package/dist/core/canvas/canvas.js +92 -0
  131. package/dist/core/canvas/crons.d.ts +9 -0
  132. package/dist/core/canvas/crons.js +51 -0
  133. package/dist/core/canvas/node-order.d.ts +22 -1
  134. package/dist/core/canvas/node-order.js +26 -1
  135. package/dist/core/canvas/render-source.d.ts +5 -0
  136. package/dist/core/canvas/render-source.js +1 -0
  137. package/dist/core/canvas/types.d.ts +9 -3
  138. package/dist/core/command-plugins/bundle.d.ts +6 -1
  139. package/dist/core/command-plugins/bundle.js +26 -13
  140. package/dist/core/command.js +8 -12
  141. package/dist/core/config.d.ts +47 -1
  142. package/dist/core/config.js +138 -4
  143. package/dist/core/help.d.ts +3 -0
  144. package/dist/core/help.js +1 -1
  145. package/dist/core/human/__tests__/page-catalog.test.d.ts +1 -0
  146. package/dist/core/human/__tests__/page-catalog.test.js +20 -0
  147. package/dist/core/human/__tests__/page-tickets.test.js +68 -34
  148. package/dist/core/human/__tests__/page.test.js +60 -119
  149. package/dist/core/human/__tests__/serial/inbox-core.test.js +6 -16
  150. package/dist/core/human/answer-text.d.ts +3 -0
  151. package/dist/core/human/answer-text.js +98 -0
  152. package/dist/core/human/answer.d.ts +53 -0
  153. package/dist/core/human/answer.js +119 -0
  154. package/dist/core/human/component-docs.d.ts +18 -0
  155. package/dist/core/human/component-docs.js +434 -0
  156. package/dist/core/human/convention.d.ts +17 -3
  157. package/dist/core/human/convention.js +36 -5
  158. package/dist/core/human/feedback-companion.d.ts +20 -0
  159. package/dist/core/human/feedback-companion.js +98 -0
  160. package/dist/core/human/feedback.d.ts +65 -0
  161. package/dist/core/human/feedback.js +116 -0
  162. package/dist/core/human/page-catalog.d.ts +56 -5
  163. package/dist/core/human/page-catalog.js +151 -22
  164. package/dist/core/human/page-errors.d.ts +4 -0
  165. package/dist/core/human/page-errors.js +7 -0
  166. package/dist/core/human/page-eval.d.ts +20 -0
  167. package/dist/core/human/page-eval.js +123 -0
  168. package/dist/core/human/page-schema.d.ts +48 -116
  169. package/dist/core/human/page-schema.js +100 -72
  170. package/dist/core/human/page-synth.d.ts +2 -4
  171. package/dist/core/human/page-synth.js +20 -7
  172. package/dist/core/human/page.d.ts +13 -21
  173. package/dist/core/human/page.js +147 -391
  174. package/dist/core/human/review-schema.d.ts +1 -0
  175. package/dist/core/human/review-schema.js +1 -1
  176. package/dist/core/human/root.d.ts +6 -2
  177. package/dist/core/human/root.js +12 -4
  178. package/dist/core/human/scan.d.ts +3 -3
  179. package/dist/core/human/scan.js +21 -21
  180. package/dist/core/human/summary.d.ts +2 -6
  181. package/dist/core/human/summary.js +3 -59
  182. package/dist/core/human/tickets.d.ts +22 -15
  183. package/dist/core/human/tickets.js +90 -28
  184. package/dist/core/human/types.d.ts +14 -2
  185. package/dist/core/keybindings/catalog.d.ts +2 -2
  186. package/dist/core/keybindings/catalog.js +1 -0
  187. package/dist/core/preview-registry.js +8 -10
  188. package/dist/core/profiles/default-binding.d.ts +4 -0
  189. package/dist/core/profiles/default-binding.js +24 -0
  190. package/dist/core/profiles/deletion-reservation.d.ts +8 -0
  191. package/dist/core/profiles/deletion-reservation.js +38 -0
  192. package/dist/core/profiles/manifest.d.ts +3 -0
  193. package/dist/core/profiles/manifest.js +12 -0
  194. package/dist/core/profiles/select.js +2 -2
  195. package/dist/core/review/realize.js +12 -4
  196. package/dist/core/review/store.d.ts +6 -0
  197. package/dist/core/review/store.js +31 -0
  198. package/dist/core/runtime/bearings.d.ts +0 -4
  199. package/dist/core/runtime/bearings.js +4 -62
  200. package/dist/core/runtime/bin-contributions.d.ts +44 -0
  201. package/dist/core/runtime/bin-contributions.js +186 -0
  202. package/dist/core/runtime/boot-root.js +6 -1
  203. package/dist/core/runtime/broker/client-registry.d.ts +4 -0
  204. package/dist/core/runtime/broker/client-registry.js +13 -1
  205. package/dist/core/runtime/broker/extension-abort.d.ts +1 -1
  206. package/dist/core/runtime/broker/extension-abort.js +4 -2
  207. package/dist/core/runtime/broker/fault-retry.js +4 -0
  208. package/dist/core/runtime/broker/frame-dispatch.d.ts +3 -1
  209. package/dist/core/runtime/broker/frame-dispatch.js +5 -6
  210. package/dist/core/runtime/broker/read-ops.d.ts +1 -1
  211. package/dist/core/runtime/broker/read-ops.js +2 -2
  212. package/dist/core/runtime/broker/rebind.d.ts +1 -0
  213. package/dist/core/runtime/broker/rebind.js +6 -1
  214. package/dist/core/runtime/broker-extension-render.js +19 -0
  215. package/dist/core/runtime/broker-protocol.d.ts +14 -0
  216. package/dist/core/runtime/broker.d.ts +1 -1
  217. package/dist/core/runtime/broker.js +25 -12
  218. package/dist/core/runtime/busy.d.ts +4 -3
  219. package/dist/core/runtime/busy.js +4 -3
  220. package/dist/core/runtime/front-door-env.d.ts +5 -0
  221. package/dist/core/runtime/front-door-env.js +16 -0
  222. package/dist/core/runtime/front-door.d.ts +1 -5
  223. package/dist/core/runtime/front-door.js +2 -5
  224. package/dist/core/runtime/host.d.ts +1 -1
  225. package/dist/core/runtime/invocation.d.ts +6 -0
  226. package/dist/core/runtime/invocation.js +10 -0
  227. package/dist/core/runtime/kickoff.js +34 -0
  228. package/dist/core/runtime/launch.d.ts +2 -6
  229. package/dist/core/runtime/node-read.js +4 -1
  230. package/dist/core/runtime/nodes.js +2 -0
  231. package/dist/core/runtime/promote.d.ts +1 -0
  232. package/dist/core/runtime/promote.js +3 -1
  233. package/dist/core/runtime/session-cycles.d.ts +6 -0
  234. package/dist/core/runtime/session-cycles.js +15 -1
  235. package/dist/core/runtime/session-visibility.d.ts +17 -8
  236. package/dist/core/runtime/session-visibility.js +19 -10
  237. package/dist/core/runtime/spawn-env.d.ts +9 -1
  238. package/dist/core/runtime/spawn-env.js +62 -2
  239. package/dist/core/runtime/spawn.js +4 -4
  240. package/dist/core/runtime/stop-guard.d.ts +2 -2
  241. package/dist/core/runtime/stop-guard.js +2 -2
  242. package/dist/core/runtime/stop-signals.d.ts +1 -1
  243. package/dist/core/runtime/stop-signals.js +2 -2
  244. package/dist/core/runtime/tmux-bindings.js +1 -1
  245. package/dist/core/session-model/session-state.d.ts +4 -3
  246. package/dist/core/session-model/session-state.js +14 -1
  247. package/dist/core/user-settings.d.ts +36 -2
  248. package/dist/core/user-settings.js +56 -14
  249. package/dist/core/worktree.d.ts +15 -45
  250. package/dist/core/worktree.js +59 -261
  251. package/dist/daemon/api/__tests__/seam/profile-delete.test.d.ts +1 -0
  252. package/dist/daemon/api/__tests__/seam/profile-delete.test.js +386 -0
  253. package/dist/daemon/api/handlers/bash-jobs.js +1 -1
  254. package/dist/daemon/api/handlers/broker-ops.js +9 -5
  255. package/dist/daemon/api/handlers/feedback-comments.d.ts +2 -0
  256. package/dist/daemon/api/handlers/feedback-comments.js +207 -0
  257. package/dist/daemon/api/handlers/human.js +24 -20
  258. package/dist/daemon/api/handlers/inbox.d.ts +10 -0
  259. package/dist/daemon/api/handlers/inbox.js +48 -109
  260. package/dist/daemon/api/handlers/profiles.js +11 -29
  261. package/dist/daemon/api/map.d.ts +6 -1
  262. package/dist/daemon/api/map.js +23 -0
  263. package/dist/daemon/api/server.js +3 -1
  264. package/dist/daemon/companion-retire.d.ts +2 -0
  265. package/dist/daemon/companion-retire.js +33 -0
  266. package/dist/daemon/cron-run.js +29 -17
  267. package/dist/daemon/crtrd.js +1 -1
  268. package/dist/daemon/fleet.d.ts +22 -10
  269. package/dist/daemon/fleet.js +43 -24
  270. package/dist/daemon/human/finish.d.ts +12 -6
  271. package/dist/daemon/human/finish.js +134 -54
  272. package/dist/daemon/human/sweep.js +9 -3
  273. package/dist/daemon/profile-delete.d.ts +7 -0
  274. package/dist/daemon/profile-delete.js +306 -0
  275. package/dist/daemon/reconcilers/broker-supervision.js +14 -9
  276. package/dist/daemon/reconcilers/dormant-inbox.js +11 -6
  277. package/dist/daemon/reconcilers/live-obligation.d.ts +21 -1
  278. package/dist/daemon/reconcilers/live-obligation.js +30 -2
  279. package/dist/daemon/reconcilers/storage-maintenance.d.ts +5 -0
  280. package/dist/daemon/reconcilers/storage-maintenance.js +28 -1
  281. package/dist/daemon/review/deliver.js +3 -3
  282. package/dist/daemon/review/finish.d.ts +0 -2
  283. package/dist/daemon/review/finish.js +1 -29
  284. package/dist/daemon/review/sweep.js +12 -1
  285. package/dist/pi-extensions/__tests__/canvas-bash-valve.test.js +56 -1
  286. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +4 -4
  287. package/dist/pi-extensions/canvas-bash-valve.d.ts +9 -1
  288. package/dist/pi-extensions/canvas-bash-valve.js +31 -13
  289. package/dist/pi-extensions/canvas-inbox-watcher.js +1 -1
  290. package/dist/pi-extensions/canvas-review-boundary.d.ts +4 -0
  291. package/dist/pi-extensions/canvas-review-boundary.js +18 -9
  292. package/dist/pi-extensions/canvas-stophook.js +4 -4
  293. package/dist/shared/generated-context.js +2 -2
  294. package/dist/types.d.ts +61 -5
  295. package/dist/types.js +8 -2
  296. package/package.json +4 -4
  297. package/runtime.lock.json +5 -33
  298. package/scripts/install-runtime.mjs +19 -24
  299. package/dist/builtin-memory/init.md +0 -38
  300. package/dist/builtin-memory/plan.md +0 -19
  301. package/dist/builtin-memory/spec.md +0 -19
  302. package/dist/clients/attach/render/html-markdown.d.ts +0 -13
  303. package/dist/clients/attach/render/html-markdown.js +0 -206
  304. package/dist/commands/human/doc.js +0 -144
  305. package/dist/commands/human/inbox.d.ts +0 -2
  306. package/dist/core/human/__tests__/page-render.test.js +0 -49
  307. package/dist/core/human/markdown-html.d.ts +0 -8
  308. package/dist/core/human/markdown-html.js +0 -581
  309. package/dist/core/human/page-render.d.ts +0 -3
  310. package/dist/core/human/page-render.js +0 -131
  311. package/dist/pages/bundle.css +0 -1
  312. package/dist/pages/bundle.js +0 -1347
  313. package/dist/pages/comments.d.ts +0 -43
  314. package/dist/pages/comments.js +0 -125
  315. package/dist/pages/elements/cards.d.ts +0 -17
  316. package/dist/pages/elements/cards.js +0 -802
  317. package/dist/pages/elements/chart.js +0 -648
  318. package/dist/pages/elements/options.d.ts +0 -21
  319. package/dist/pages/elements/options.js +0 -629
  320. package/dist/pages/elements/pages.d.ts +0 -24
  321. package/dist/pages/elements/pages.js +0 -355
  322. package/dist/pages/elements/slot.d.ts +0 -21
  323. package/dist/pages/elements/slot.js +0 -105
  324. package/dist/pages/elements/table.d.ts +0 -19
  325. package/dist/pages/elements/table.js +0 -935
  326. package/dist/pages/elements/text.d.ts +0 -25
  327. package/dist/pages/elements/text.js +0 -968
  328. package/dist/pages/entry.d.ts +0 -8
  329. package/dist/pages/entry.js +0 -8
  330. package/dist/pages/host.d.ts +0 -158
  331. package/dist/pages/host.js +0 -182
  332. package/dist/pages/register.d.ts +0 -2
  333. package/dist/pages/register.js +0 -3
  334. package/dist/pages/slot-config.d.ts +0 -36
  335. package/dist/pages/slot-config.js +0 -50
  336. package/dist/pages/types.d.ts +0 -98
  337. package/dist/pages/types.js +0 -23
  338. /package/dist/{core/human/__tests__/page-render.test.d.ts → commands/__tests__/node-message.test.d.ts} +0 -0
  339. /package/dist/{pages/elements/chart.d.ts → core/__tests__/plugin-page-components.test.d.ts} +0 -0
@@ -14,7 +14,7 @@ import { clearFault, readFault } from '../fault.js';
14
14
  import { isManagedProvider, readSubscriptionPool, } from '../../subscription-state.js';
15
15
  import { SessionListCache } from '../session-list-cache.js';
16
16
  import { sessionListCachePath } from '../../canvas/paths.js';
17
- import { REVIEW_BOUNDARY_CUSTOM_TYPE } from '../session-visibility.js';
17
+ import { isReviewBoundary } from '../session-visibility.js';
18
18
  import { isUnknownModel, parseModelSpec } from '../model-selection.js';
19
19
  import { buildCommandList, buildGetTreeData, buildListMemoryRefsData, buildListModelsData, buildScopedModelsData, buildSessionsData, buildSettingsData, modelKey, } from './read-ops.js';
20
20
  export const REVIEW_BOUNDARY_NAVIGATION_CODE = 'review_boundary_navigation';
@@ -279,7 +279,7 @@ export function stripHiddenEntriesFromJsonlExport(filePath) {
279
279
  }
280
280
  /** Owns frame-local input state while dispatching through explicit broker collaborators. */
281
281
  export function createFrameDispatchContext(deps) {
282
- const { nodeId, cfg, registry, toolGroups, projection, rebind, reviewCompanion, pendingDialogs, sendWelcome, replayExtraPendingDialogsTo, reWelcomeAll, disposeAndExit, persistModelChoice, broadcastModelChanged, registryOf, formatModelSpec, formatExactModelSpec, resolveLaunchModel, } = deps;
282
+ const { nodeId, cfg, registry, toolGroups, projection, rebind, boundaryReviewId, pendingDialogs, sendWelcome, replayExtraPendingDialogsTo, reWelcomeAll, disposeAndExit, persistModelChoice, broadcastModelChanged, registryOf, formatModelSpec, formatExactModelSpec, resolveLaunchModel, } = deps;
283
283
  const currentSession = () => rebind.session();
284
284
  const currentServices = () => rebind.services();
285
285
  let kickoffPending;
@@ -657,11 +657,10 @@ export function createFrameDispatchContext(deps) {
657
657
  // pass no id (unchanged behavior).
658
658
  ...(id !== undefined ? { id } : {}),
659
659
  });
660
- const isPreBoundaryReviewEntry = (entryId) => reviewCompanion &&
660
+ const isPreBoundaryReviewEntry = (entryId) => boundaryReviewId !== undefined &&
661
661
  !currentSession()
662
662
  .sessionManager.getBranch(entryId)
663
- .some((entry) => entry.type === 'custom_message' &&
664
- entry.customType === REVIEW_BOUNDARY_CUSTOM_TYPE);
663
+ .some((entry) => isReviewBoundary(entry, boundaryReviewId));
665
664
  const reviewBoundaryRefusalTo = (client, targetId) => {
666
665
  const refusal = new ReviewBoundaryNavigationError(targetId);
667
666
  registry.sendFrame(client, {
@@ -1112,7 +1111,7 @@ export function createFrameDispatchContext(deps) {
1112
1111
  }
1113
1112
  case 'get_tree': {
1114
1113
  try {
1115
- registry.sendFrame(client, buildGetTreeData(frame.id, currentSession(), reviewCompanion));
1114
+ registry.sendFrame(client, buildGetTreeData(frame.id, currentSession(), boundaryReviewId));
1116
1115
  }
1117
1116
  catch (err) {
1118
1117
  engineErrorTo(client, frame.id)(err);
@@ -18,6 +18,6 @@ export declare function buildCommandList(session: BrokerSession): CommandDisclos
18
18
  export declare function buildListMemoryRefsData(id: string, refs: RefMeta[], engineLeadingCommandNames: string[]): ListMemoryRefsData;
19
19
  export declare function buildListModelsData(id: string, session: BrokerSession): ListModelsData;
20
20
  export declare function buildScopedModelsData(id: string, session: BrokerSession): ListScopedModelsData;
21
- export declare function buildGetTreeData(id: string, session: BrokerSession, reviewCompanion: boolean): GetTreeData;
21
+ export declare function buildGetTreeData(id: string, session: BrokerSession, boundaryReviewId: string | undefined): GetTreeData;
22
22
  export declare function buildSettingsData(id: string, session: BrokerSession): GetSettingsData;
23
23
  export declare function buildSessionsData(id: string, scope: 'cwd' | 'all', session: BrokerSession, cache: SessionListCache): Promise<ListSessionsData>;
@@ -67,9 +67,9 @@ export function buildScopedModelsData(id, session) {
67
67
  enabledModelIds: session.settingsManager.getEnabledModels() ?? null,
68
68
  };
69
69
  }
70
- export function buildGetTreeData(id, session, reviewCompanion) {
70
+ export function buildGetTreeData(id, session, boundaryReviewId) {
71
71
  const manager = session.sessionManager;
72
- const tree = reviewCompanion ? boundaryRootedTree(manager.getTree()) : manager.getTree();
72
+ const tree = boundaryReviewId !== undefined ? boundaryRootedTree(manager.getTree(), boundaryReviewId) : manager.getTree();
73
73
  const visibleEntryIds = new Set();
74
74
  const collectEntryIds = (nodes) => {
75
75
  for (const node of nodes) {
@@ -17,6 +17,7 @@ export interface SubscriptionGeneration {
17
17
  messages?: unknown[];
18
18
  } | null;
19
19
  stagedWatchdogAbort: boolean;
20
+ stagedRefreshAbort: boolean;
20
21
  stagedOverflowFailure: {
21
22
  errorMessage: string;
22
23
  } | null;
@@ -56,7 +56,11 @@ export class RebindDriver {
56
56
  await candidateSession.bindExtensions({
57
57
  uiContext: this.deps.uiContext(),
58
58
  mode: 'rpc',
59
- abortHandler: () => abortSessionFromExtension(candidateSession),
59
+ abortHandler: () => abortSessionFromExtension(candidateSession, () => {
60
+ const generation = this.installed;
61
+ if (generation?.session === candidateSession)
62
+ generation.stagedRefreshAbort = true;
63
+ }),
60
64
  shutdownHandler: () => this.requestShutdown(),
61
65
  });
62
66
  if (ticket !== this.latestRebindRequest)
@@ -92,6 +96,7 @@ export class RebindDriver {
92
96
  fallback: { blocked: false },
93
97
  stagedProviderAgentEnd: null,
94
98
  stagedWatchdogAbort: false,
99
+ stagedRefreshAbort: false,
95
100
  stagedOverflowFailure: null,
96
101
  executingTools: new Set(),
97
102
  };
@@ -114,10 +114,29 @@ function worktreeNote(node) {
114
114
  return '';
115
115
  return `Branch: \`${worktree.branch}\`\nWorktree: \`${worktree.path}\`\nBase: \`${worktree.base_ref}\` @ \`${worktree.base_sha}\`\nCommit work here. Land it with \`crtr node worktree close\` before finishing. Auto-drop applies only when the worktree is clean (no uncommitted changes) and nothing remains to land (zero commits ahead of the base, or its commits are already contained in the base); unlanded commits (commits not already contained in the base) or uncommitted changes require an explicit close/landing decision.`;
116
116
  }
117
+ /** The companion note — rendered ONLY when a node carries its immutable
118
+ * companion binding. Names the target (the document under review, or the page
119
+ * the feedback is about), and for a page-feedback companion carries the whole
120
+ * behavioral contract — the companion is an ordinary fork of the sending node,
121
+ * so this note is the only place it learns the role. */
117
122
  function reviewNote(node) {
118
123
  const review = node.review_binding;
119
124
  if (review === null || review === undefined)
120
125
  return '';
126
+ if (review.kind === 'page_feedback') {
127
+ const ticketId = review.review_id;
128
+ const targetFile = review.target_file;
129
+ return (`You exist for one page's feedback conversation — ticket \`${ticketId}\`, page source \`${targetFile}\`. ` +
130
+ `Think with the person about this one page and answer in a few plain sentences from the context you already carry. ` +
131
+ `Revise the page in place with \`crtr human send --replace ${ticketId} --page ${targetFile}\` — it swaps in on their screen immediately — and briefly say what changed; a silent swap leaves them guessing. ` +
132
+ `After dealing with an open feedback item — by revising the page or answering it here — resolve it with \`crtr human feedback resolve <comment_id>\`; ` +
133
+ `\`crtr human feedback list ${ticketId}\` shows each item's id and status. ` +
134
+ `Never author feedback or resolve an item before dealing with it — a sent comment is immutable, and resolution is your report that the work is done. ` +
135
+ `Asked to act beyond this page, give your advice and point the person at submitting it: the submitted answer is what reaches the task's owner, while work done here is invisible to it and ends when the ticket settles. ` +
136
+ `When a question needs real exploration, delegate it to a child node and keep talking while it runs. ` +
137
+ `The inherited conversation belongs to the origin node, not you — its task stays with it; carry only the context needed for this page. ` +
138
+ `The person's visible transcript begins at the boundary marker above; everything earlier is context they cannot see — never treat it as shared ground; when they ask about it, answer plainly in your own words rather than replaying it.`);
139
+ }
121
140
  return `You exist for one human review of \`${review.target_file}\`. The inherited conversation belongs to the origin node, not you — carry only the context needed for this review. The person's visible transcript begins at the boundary marker above; everything earlier is context they cannot see.`;
122
141
  }
123
142
  export function buildContextBearingsFromState(state, projectContext, situational = '', seen) {
@@ -46,6 +46,20 @@ export interface BrokerSnapshot {
46
46
  }>;
47
47
  title: string | undefined;
48
48
  };
49
+ /** The engine's UNRUN queue — the steer/follow-up texts a writable client
50
+ * sent mid-turn that have not started yet (pi's `_steeringMessages` +
51
+ * `_followUpMessages`, read non-destructively). `state.pendingMessageCount`
52
+ * is the same queue's SIZE; without the texts a (re)attaching viewer can say
53
+ * how many messages are waiting but not which, so it cannot render the
54
+ * queued rows it renders from `queue_update` while attached — the welcome
55
+ * would no longer fully determine viewer state.
56
+ *
57
+ * Ingest defensively: a broker pinned to a pre-`queued` runtime generation is
58
+ * still live and sends `welcome` without it. */
59
+ queued?: {
60
+ steering: string[];
61
+ followUp: string[];
62
+ };
49
63
  /** Broker-owned summaries keyed by the first tool-call id in each group.
50
64
  * Structured objects with bullets and filesDeleted.
51
65
  * Optional so snapshots from an older live runtime remain readable. */
@@ -29,7 +29,7 @@ interface PendingDialog {
29
29
  * bash children. No-op before the session is built or after a clean dispose. */
30
30
  export declare function disposeActiveSession(): void;
31
31
  export declare function runBroker(nodeId: string, startupAt?: bigint, startupTs?: string): Promise<void>;
32
- export declare function snapshotMessages(session: CreateAgentSessionResult['session'], reviewCompanion?: boolean): BrokerSnapshot['messages'];
32
+ export declare function snapshotMessages(session: CreateAgentSessionResult['session'], boundaryReviewId?: string): BrokerSnapshot['messages'];
33
33
  export declare function buildBrokerSession(engine: BrokerEngine, cfg: BrokerSdkConfig): Promise<{
34
34
  session: CreateAgentSessionResult['session'];
35
35
  services: AgentSessionServices;
@@ -120,7 +120,8 @@ export function disposeActiveSession() {
120
120
  // ---------------------------------------------------------------------------
121
121
  export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), startupTs = new Date().toISOString()) {
122
122
  // A clean-abort certificate belongs only to the dead broker that wrote it.
123
- // Fleet classification consumes it before launching this successor.
123
+ // Fleet classification consumes it as proof that the dead broker had a turn
124
+ // in flight even though Pi's settled abort already cleared the busy marker.
124
125
  clearCleanAbort(nodeId);
125
126
  // The canonical broker event source (job/log.jsonl) is bound before any other
126
127
  // work in this function — no path/env/state setup may precede it, and the
@@ -215,8 +216,11 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
215
216
  let { session, services, resuming, runtime } = await buildBrokerSession(engine, cfg);
216
217
  // Review identity is immutable for the broker's lifetime. Snapshot producers
217
218
  // close over this one construction-time classification rather than re-reading
218
- // canvas metadata for every welcome or session replacement.
219
- const reviewCompanion = (await brokerExtensionState(nodeId)).node.review_binding != null;
219
+ // canvas metadata for every welcome or session replacement. The BOUND review
220
+ // id (not a mere boolean) is what selects this companion's own boundary
221
+ // marker — a fork of another companion inherits that companion's earlier
222
+ // marker, and slicing at any marker would reveal its transcript.
223
+ const boundaryReviewId = (await brokerExtensionState(nodeId)).node.review_binding?.review_id;
220
224
  // M3: name the boot session as the process-fatal disposal target immediately —
221
225
  // enqueueRebind reassigns this on every later replacement, but without this
222
226
  // initial assignment `activeSession` stays null (and disposeActiveSession() a
@@ -354,7 +358,7 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
354
358
  const buildSnapshot = () => {
355
359
  const liveSession = rebind.session();
356
360
  return ({
357
- messages: snapshotMessages(liveSession, reviewCompanion),
361
+ messages: snapshotMessages(liveSession, boundaryReviewId),
358
362
  stats: liveSession.getSessionStats(),
359
363
  // pi's `get_state` (rpc-mode.js), field for field, from the same getters.
360
364
  // The ONE divergence: a model-less session (undefined OR the SDK
@@ -381,6 +385,14 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
381
385
  },
382
386
  toolGroupSummaries: toolGroups.summaries(),
383
387
  workingActivity: projection.currentWorkingActivity(),
388
+ // The UNRUN queue's texts, read non-destructively (`clearQueue()` would
389
+ // consume them). Same two arrays a live `queue_update` carries, so a client
390
+ // that attaches mid-queue lands on the state its next `queue_update` would
391
+ // have produced.
392
+ queued: {
393
+ steering: [...liveSession.getSteeringMessages()],
394
+ followUp: [...liveSession.getFollowUpMessages()],
395
+ },
384
396
  });
385
397
  };
386
398
  // Send a client its catch-up snapshot. welcome.pending_dialog carries a single
@@ -508,7 +520,7 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
508
520
  rebind.setRebindSession();
509
521
  await rebind.enqueueRebind(session, services);
510
522
  const frameDispatch = createFrameDispatchContext({
511
- nodeId, cfg, registry, toolGroups, projection, rebind, reviewCompanion,
523
+ nodeId, cfg, registry, toolGroups, projection, rebind, boundaryReviewId,
512
524
  pendingDialogs, sendWelcome, replayExtraPendingDialogsTo, reWelcomeAll, disposeAndExit,
513
525
  persistModelChoice, broadcastModelChanged, registryOf, formatModelSpec,
514
526
  formatExactModelSpec, resolveLaunchModel,
@@ -594,9 +606,10 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
594
606
  // A daemon fleet bounce gives a busy Pi a short, bounded chance to persist
595
607
  // its aborted turn before teardown. `session.abort()` resolves only after Pi
596
608
  // reaches idle (and thus after its message_end persistence); only then may we
597
- // leave the durable clean-abort certificate that changes recovery to resume.
598
- // A timeout/rejection deliberately leaves no certificate, preserving dirty
599
- // crash → recovery-cycle behavior and the fleet's shared SIGKILL deadline.
609
+ // leave the durable clean-abort certificate that preserves the otherwise
610
+ // cleared evidence that this broker had a turn in flight. A timeout/rejection
611
+ // deliberately leaves no certificate: the surviving busy marker carries the
612
+ // same interrupted-turn fact through the fleet's shared SIGKILL deadline.
600
613
  const ABORT_BEFORE_EXIT_MS = 2_500;
601
614
  let signalExitInFlight = false;
602
615
  const abortAndExit = async (reason) => {
@@ -633,8 +646,8 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
633
646
  }
634
647
  else if (frameDispatch.pendingPromptDispatches() > 0 || rebind.isAgentRunActive()) {
635
648
  // The engine never reached an abortable persisted turn in the budget.
636
- // Preserve the existing dirty-death recovery cycle rather than certifying
637
- // a session that may not contain the prompt it was preparing.
649
+ // Preserve the dirty interrupted-turn witness so recovery resumes the
650
+ // saved transcript and explicitly re-drives the stopped work.
638
651
  markBusy(nodeId);
639
652
  }
640
653
  }
@@ -671,13 +684,13 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
671
684
  // conversation instead of just the post-yield window; single-cycle sessions and
672
685
  // the fake-engine fixture (no tree accessors) fall through to plain
673
686
  // `buildSessionContext().messages` unchanged.
674
- export function snapshotMessages(session, reviewCompanion = false) {
687
+ export function snapshotMessages(session, boundaryReviewId) {
675
688
  // Cast: the real SDK's `SessionEntry.data` is `unknown` (untyped custom-entry
676
689
  // payload), narrower than `CycleSessionManagerLike`'s structural read of the
677
690
  // `crtr-cycle` marker's `{cycle, fromLeaf}` shape — the fields we read off it
678
691
  // are ones ONLY this module's own `appendCustomEntry` call (above) ever writes.
679
692
  const messages = cycleAwareMessages(session.sessionManager);
680
- return visibleMessages(messages, { reviewCompanion });
693
+ return visibleMessages(messages, { boundaryReviewId });
681
694
  }
682
695
  // ---------------------------------------------------------------------------
683
696
  // resolveLaunchModel — the SOLE launch model-selection policy, module-scoped
@@ -4,12 +4,13 @@ export declare function markBusy(nodeId: string): void;
4
4
  /** Clear the mid-run marker after Pi settles, however it routed. Best-effort. */
5
5
  export declare function clearBusy(nodeId: string): void;
6
6
  /** Certify that this broker aborted a busy turn and Pi persisted its settled
7
- * aborted entry before teardown. The daemon consumes this durable certificate
8
- * once to resume with a continuation; a dirty death never writes it. */
7
+ * aborted entry before teardown. Settling clears the busy marker, so the daemon
8
+ * consumes this durable certificate as the surviving interrupted-turn witness
9
+ * and resumes with a continuation. A dirty death instead leaves `busy` behind. */
9
10
  export declare function markCleanAbort(nodeId: string): void;
10
11
  /** Remove a consumed clean-abort certificate as a broker boots. */
11
12
  export declare function clearCleanAbort(nodeId: string): void;
12
- /** Whether the dead broker left a persisted clean-abort certificate. */
13
+ /** Whether the dead broker left a persisted clean-abort turn witness. */
13
14
  export declare function hasCleanAbort(nodeId: string): boolean;
14
15
  /** Is the node currently inside a turn? AND this with `pidAlive` at the call
15
16
  * site — a stale marker from a crashed pi is harmless because the dead pid
@@ -44,8 +44,9 @@ export function clearBusy(nodeId) {
44
44
  }
45
45
  }
46
46
  /** Certify that this broker aborted a busy turn and Pi persisted its settled
47
- * aborted entry before teardown. The daemon consumes this durable certificate
48
- * once to resume with a continuation; a dirty death never writes it. */
47
+ * aborted entry before teardown. Settling clears the busy marker, so the daemon
48
+ * consumes this durable certificate as the surviving interrupted-turn witness
49
+ * and resumes with a continuation. A dirty death instead leaves `busy` behind. */
49
50
  export function markCleanAbort(nodeId) {
50
51
  try {
51
52
  mkdirSync(jobDir(nodeId), { recursive: true });
@@ -64,7 +65,7 @@ export function clearCleanAbort(nodeId) {
64
65
  /* best-effort */
65
66
  }
66
67
  }
67
- /** Whether the dead broker left a persisted clean-abort certificate. */
68
+ /** Whether the dead broker left a persisted clean-abort turn witness. */
68
69
  export function hasCleanAbort(nodeId) {
69
70
  return existsSync(cleanAbortPath(nodeId));
70
71
  }
@@ -0,0 +1,5 @@
1
+ /** Env marker set on every pi the front door boots. Its presence means we are
2
+ * already inside a front-door-booted root, so a nested front-door launch must
3
+ * be refused — otherwise a removed/renamed subcommand that a child pi re-runs
4
+ * (e.g. `crtr node -h`) fork-bombs pi until the machine must be rebooted. */
5
+ export declare const FRONT_DOOR_ENV = "CRTR_FRONT_DOOR";
@@ -0,0 +1,16 @@
1
+ // The front-door recursion-guard env name, alone in a dependency-free module.
2
+ //
3
+ // `front-door.ts` owns the boot path and reaches the canvas through the
4
+ // dynamic imports inside `maybeBootRoot`; every module that only needs the
5
+ // MARKER NAME must not inherit that reach. The shipped broker extensions are
6
+ // gated on their static import graph, which cannot tell a lazily-imported boot
7
+ // dependency from an eagerly-loaded one, so importing the constant from
8
+ // `front-door.js` reads as an extension reaching canvas.db.
9
+ //
10
+ // `front-door.ts` re-exports this, so `FRONT_DOOR_ENV` stays importable from
11
+ // either module and every pi-booting path keeps setting the same marker.
12
+ /** Env marker set on every pi the front door boots. Its presence means we are
13
+ * already inside a front-door-booted root, so a nested front-door launch must
14
+ * be refused — otherwise a removed/renamed subcommand that a child pi re-runs
15
+ * (e.g. `crtr node -h`) fork-bombs pi until the machine must be rebooted. */
16
+ export const FRONT_DOOR_ENV = 'CRTR_FRONT_DOOR';
@@ -1,8 +1,4 @@
1
- /** Env marker set on every pi the front door boots. Its presence means we are
2
- * already inside a front-door-booted root, so a nested front-door launch must
3
- * be refused — otherwise a removed/renamed subcommand that a child pi re-runs
4
- * (e.g. `crtr node -h`) fork-bombs pi until the machine must be rebooted. */
5
- export declare const FRONT_DOOR_ENV = "CRTR_FRONT_DOOR";
1
+ export { FRONT_DOOR_ENV } from './front-door-env.js';
6
2
  /** If this invocation is a front-door (root) launch, boot it and never return.
7
3
  * Returns false when it's a recognized subcommand / help / unknown token (let
8
4
  * the dispatcher handle it — for unknown tokens it errors cleanly).
@@ -21,6 +21,7 @@
21
21
  import { existsSync, statSync } from 'node:fs';
22
22
  import { resolve as resolvePath } from 'node:path';
23
23
  import { SUBTREE_NAMES } from '../../build-root.js';
24
+ import { FRONT_DOOR_ENV } from './front-door-env.js';
24
25
  // NOTE: the boot-path deps — ./boot-root.js (bootRoot), ./resume-root.js (resumeRoot),
25
26
  // ./package-health.js (missingPackageWarning + piAgentDir) — are DYNAMICALLY
26
27
  // imported inside maybeBootRoot, NOT statically here. `cli.ts` calls
@@ -89,11 +90,7 @@ function parseRootArgs(tokens) {
89
90
  const prompt = positionals.length > 0 ? positionals.join(' ') : undefined;
90
91
  return { cwd, prompt, name, kind, profile, pickProfile, resume };
91
92
  }
92
- /** Env marker set on every pi the front door boots. Its presence means we are
93
- * already inside a front-door-booted root, so a nested front-door launch must
94
- * be refused — otherwise a removed/renamed subcommand that a child pi re-runs
95
- * (e.g. `crtr node -h`) fork-bombs pi until the machine must be rebooted. */
96
- export const FRONT_DOOR_ENV = 'CRTR_FRONT_DOOR';
93
+ export { FRONT_DOOR_ENV } from './front-door-env.js';
97
94
  /** If this invocation is a front-door (root) launch, boot it and never return.
98
95
  * Returns false when it's a recognized subcommand / help / unknown token (let
99
96
  * the dispatcher handle it — for unknown tokens it errors cleanly).
@@ -1,5 +1,5 @@
1
1
  import { type NodeRow } from '../canvas/index.js';
2
- import type { PiInvocation } from './launch.js';
2
+ import type { PiInvocation } from './invocation.js';
3
3
  /** View-side launch hints the broker host needs beyond the PiInvocation. The
4
4
  * broker uses `cwd` + `resuming`; `name` is carried for the node's display. */
5
5
  export interface LaunchPlacement {
@@ -0,0 +1,6 @@
1
+ export interface PiInvocation {
2
+ /** argv after the `pi` binary. */
3
+ argv: string[];
4
+ /** env to merge into the process. */
5
+ env: Record<string, string>;
6
+ }
@@ -0,0 +1,10 @@
1
+ // The broker-launch recipe shape, alone in a dependency-free module.
2
+ //
3
+ // `launch.ts` builds a PiInvocation and therefore reaches canvas state; the
4
+ // modules that only need to READ the recipe shape (spawn-env, host) must not.
5
+ // A type-only import is erased at compile time, but the shipped broker
6
+ // extensions are gated on their STATIC import graph — the gate cannot tell an
7
+ // erased edge from a live one, so an extension that reaches spawn-env would
8
+ // read as reaching canvas.db through launch.ts. Keeping the interface here
9
+ // makes the graph say what the runtime already does.
10
+ export {};
@@ -20,6 +20,7 @@
20
20
  import { existsSync, readFileSync, writeFileSync, rmSync, mkdirSync, readdirSync, } from 'node:fs';
21
21
  import { join } from 'node:path';
22
22
  import { contextDir, reportsDir, getNode, subscriptionsOf, subscribersOf, } from '../canvas/index.js';
23
+ import { activeBackgroundBashJobs, bashJobCommandLine, formatBashElapsed } from '../bash-jobs.js';
23
24
  import { readRoadmap, roadmapPath } from './roadmap.js';
24
25
  import { buildWakeBearings } from './bearings.js';
25
26
  import { personaDrift, commitPersonaAck } from './persona.js';
@@ -205,6 +206,33 @@ function feedBlock(nodeId, unreadDigest) {
205
206
  }
206
207
  return `<feed>\n${lines.join('\n')}\n</feed>`;
207
208
  }
209
+ /** One-line label for a job: its purpose when the caller gave one, else its real
210
+ * command line, clipped so a long pipeline stays scannable. */
211
+ function jobLabel(purpose, command) {
212
+ const raw = (purpose ?? bashJobCommandLine(command)).trim() || '(no command)';
213
+ return raw.length > 120 ? `${raw.slice(0, 117)}…` : raw;
214
+ }
215
+ /** Live file-backed background bash jobs this node owns. A fresh revive has no
216
+ * memory of handing anything off, so without this block the node cannot tell a
217
+ * command it started is still running: it can neither read the log nor decide
218
+ * whether to wait, and may relaunch the same work. Returns null — and the
219
+ * kickoff omits the block entirely — when nothing is live. */
220
+ function backgroundJobsBlock(nodeId) {
221
+ const jobs = activeBackgroundBashJobs(contextDir(nodeId));
222
+ if (jobs.length === 0)
223
+ return null;
224
+ const now = Date.now();
225
+ const one = jobs.length === 1;
226
+ const lines = [
227
+ `${jobs.length} background bash job${one ? '' : 's'} you started ${one ? 'is' : 'are'} STILL RUNNING, detached from any turn and unaffected by your refresh. ` +
228
+ `Each one sends you a message when it exits — that wake is automatic, so do not poll it and do not relaunch the same command.`,
229
+ ];
230
+ for (const job of jobs) {
231
+ lines.push(` - ${job.jobId} — ${jobLabel(job.purpose, job.command)} · running ${formatBashElapsed(now - job.startedAtMs)} · log ${job.logPath}`);
232
+ }
233
+ lines.push('', 'Read output so far: `tail -n 50 <log>` (or `crtr node bash list --tail 20`). Abandon one: `crtr node bash kill <job-id>`.');
234
+ return `<background-jobs>\n${lines.join('\n')}\n</background-jobs>`;
235
+ }
208
236
  // ---------------------------------------------------------------------------
209
237
  // buildReviveKickoff — assemble the full fresh-revive first message.
210
238
  // ---------------------------------------------------------------------------
@@ -257,6 +285,12 @@ export function buildReviveKickoff(meta, bearings, wakeReason) {
257
285
  // revive (a new pi process boot) and re-injects buildContextIntro(), which
258
286
  // already carries the sidecar as a hidden sibling of <crtr-bearings>; that seam
259
287
  // covers this path, so duplicating it into the visible prompt would leak it.
288
+ // Live background bash jobs sit beside the context-dir listing: both describe
289
+ // this node's OWN in-flight state, and both precede <feed>, which is about the
290
+ // other nodes it awaits. Absent when nothing is running.
291
+ const jobsBlock = backgroundJobsBlock(nodeId);
292
+ if (jobsBlock !== null)
293
+ parts.push(jobsBlock);
260
294
  parts.push(feedBlock(nodeId, bearings.unreadDigest));
261
295
  parts.push(bearings.yieldMsg !== null
262
296
  ? `<yield-message>\n${bearings.yieldMsg.trim()}\n</yield-message>`
@@ -1,3 +1,4 @@
1
+ import type { PiInvocation } from './invocation.js';
1
2
  export { CANVAS_EXTENSIONS, CANVAS_STOPHOOK_PATH, CANVAS_INBOX_WATCHER_PATH, CANVAS_RECAP_PATH, CANVAS_GOAL_CAPTURE_PATH, CANVAS_PASSIVE_CONTEXT_PATH, CANVAS_CONTEXT_INTRO_PATH, CANVAS_TOOL_GUIDE_PATH, CANVAS_DOC_SUBSTRATE_PATH, CANVAS_STRUCTURED_OUTPUT_PATH, CANVAS_BASH_VALVE_PATH, } from './canvas-extensions.js';
2
3
  import type { NodeMeta, LaunchSpec, Mode, Lifecycle } from '../canvas/index.js';
3
4
  import type { ModelProvider, ModelStrength, ScopeConfig } from '../../types.js';
@@ -104,12 +105,7 @@ export declare function buildLaunchSpec(kind: string, mode: Mode, opts: {
104
105
  launch: LaunchSpec;
105
106
  lifecycle: 'terminal' | 'resident';
106
107
  };
107
- export interface PiInvocation {
108
- /** argv after the `pi` binary. */
109
- argv: string[];
110
- /** env to merge into the process. */
111
- env: Record<string, string>;
112
- }
108
+ export type { PiInvocation } from './invocation.js';
113
109
  /** Construct the pi invocation for a node.
114
110
  * - fresh start: pass `prompt` (the node's first user message), no resume.
115
111
  * - fork start: pass `forkFrom` (absolute .jsonl path or partial uuid) to `--fork`
@@ -152,7 +152,7 @@ export async function readNodeSnapshot(nodeId) {
152
152
  copyFileSync(sessionFile, copy);
153
153
  const manager = SessionManager.open(copy);
154
154
  const context = manager.buildSessionContext();
155
- const messages = visibleMessages(cycleAwareMessages(manager), { reviewCompanion: node.review_binding != null });
155
+ const messages = visibleMessages(cycleAwareMessages(manager), { boundaryReviewId: node.review_binding?.review_id });
156
156
  const sessionId = manager.getSessionId();
157
157
  const sessionName = manager.getSessionName();
158
158
  const snapshot = {
@@ -179,6 +179,9 @@ export async function readNodeSnapshot(nodeId) {
179
179
  // process state (last setStatus/setWidget/setTitle) and is not persisted,
180
180
  // so an offline read has none.
181
181
  display: { statuses: {}, widgets: {}, title: undefined },
182
+ // The unrun queue lives in the engine's memory; a dormant node has no
183
+ // engine, so nothing is waiting to run.
184
+ queued: { steering: [], followUp: [] },
182
185
  // Sidecar summaries survive broker exit so a dormant transcript receives
183
186
  // the same completed-group data as a live welcome snapshot. Malformed or
184
187
  // absent sidecars are deliberately ignored: tool calls remain visible.
@@ -19,6 +19,7 @@ import { memoryDir } from './memory.js';
19
19
  import { resolveInstallId } from '../canvas/install-id.js';
20
20
  import { canvasDbPath } from '../canvas/paths.js';
21
21
  import { usage } from '../errors.js';
22
+ import { assertProfileAvailableForBirth } from '../profiles/deletion-reservation.js';
22
23
  // crtrd sets this once at boot (setInstallId) and it wins unconditionally —
23
24
  // a daemon process only ever serves one canvas home for its whole lifetime,
24
25
  // so no keying is needed there. Every OTHER in-process caller (tests,
@@ -259,6 +260,7 @@ export function spawnNode(opts) {
259
260
  if (parent !== null && getNode(parent) === null) {
260
261
  throw new Error(`cannot spawn under unknown parent node: ${parent}`);
261
262
  }
263
+ assertProfileAvailableForBirth(meta.profile_id ?? null);
262
264
  createNode(meta);
263
265
  // Create the node-local memory directory so substrate docs can be written
264
266
  // directly into it without a separate mkdir. Skip for ephemeral human-bridge
@@ -19,6 +19,7 @@ export declare function reshapeNode(nodeId: string, opts?: {
19
19
  model?: string;
20
20
  mode?: Mode;
21
21
  lifecycle?: Lifecycle;
22
+ profileId?: string | null;
22
23
  }, beforePersist?: () => void): NodeMeta;
23
24
  /** Promote a node to an orchestrator (mode→orchestrator), optionally
24
25
  * specializing its kind (e.g. a `general` worker becoming a
@@ -42,6 +42,7 @@ export function reshapeNode(nodeId, opts = {}, beforePersist) {
42
42
  const targetModel = opts.model ?? node.model_override ?? undefined;
43
43
  const targetMode = opts.mode ?? node.mode;
44
44
  const targetLifecycle = opts.lifecycle ?? node.lifecycle;
45
+ const targetProfileId = opts.profileId === undefined ? node.profile_id : opts.profileId;
45
46
  // Rewrite the launch spec to the target kind/mode's persona so the *next*
46
47
  // revive comes back as this exact shape (polymorph stage 2). nodeEnv reads
47
48
  // meta.{kind,mode}, so CRTR_KIND/CRTR_MODE flip immediately for the live
@@ -66,7 +67,7 @@ export function reshapeNode(nodeId, opts = {}, beforePersist) {
66
67
  // Resolve kind/ladder config against the NODE's own cwd + profile, not
67
68
  // this process's ambient scope.
68
69
  cwd: node.cwd,
69
- profileId: node.profile_id,
70
+ profileId: targetProfileId,
70
71
  });
71
72
  // Promotion seeds after its target launch has been built but before the
72
73
  // mutation is persisted, preserving the original promote() failure order.
@@ -81,6 +82,7 @@ export function reshapeNode(nodeId, opts = {}, beforePersist) {
81
82
  kind: targetKind,
82
83
  mode: targetMode,
83
84
  lifecycle: targetLifecycle,
85
+ profile_id: targetProfileId,
84
86
  launch,
85
87
  ...(carriesDefaultHandle ? { name: targetKind } : {}),
86
88
  // Persist a newly-chosen tier so it is durable across future revives; omit
@@ -20,6 +20,12 @@ export interface CycleSessionManagerLike {
20
20
  * path root-first. */
21
21
  getBranch?(fromId?: string): SessionEntry[];
22
22
  }
23
+ /** True when an assistant turn ended because its own abort fired — an ESC, a
24
+ * critical-tier interrupt, or the self-abort a refresh-yield performs. */
25
+ export declare function endedByAbort(message: {
26
+ stopReason?: string;
27
+ errorMessage?: string;
28
+ }): boolean;
23
29
  /** A refresh-yield deliberately aborts its own turn before the next cycle is
24
30
  * rooted. Preserve that assistant message (including tool calls and prose), but
25
31
  * omit the failure annotation: the cycle divider is the truthful visible seam. */
@@ -32,11 +32,25 @@ function cycleDivider(cycle, timestamp) {
32
32
  timestamp: new Date(timestamp).getTime(),
33
33
  };
34
34
  }
35
+ /** Text a provider attaches when the request's own AbortSignal fired. Pi's
36
+ * providers stamp `stopReason: 'aborted'` when they observe the signal
37
+ * themselves, but an abort that lands while the next request is starting
38
+ * reaches their catch as a bare AbortError instead, and is recorded as
39
+ * `stopReason: 'error'` carrying this text. Both spellings are the same healthy
40
+ * event — a turn that stopped because something asked it to stop. */
41
+ const ABORT_ERROR_TEXT = /\b(?:request|operation) was aborted\b|\bthe user aborted\b/i;
42
+ /** True when an assistant turn ended because its own abort fired — an ESC, a
43
+ * critical-tier interrupt, or the self-abort a refresh-yield performs. */
44
+ export function endedByAbort(message) {
45
+ if (message.stopReason === 'aborted')
46
+ return true;
47
+ return message.stopReason === 'error' && ABORT_ERROR_TEXT.test(message.errorMessage ?? '');
48
+ }
35
49
  /** A refresh-yield deliberately aborts its own turn before the next cycle is
36
50
  * rooted. Preserve that assistant message (including tool calls and prose), but
37
51
  * omit the failure annotation: the cycle divider is the truthful visible seam. */
38
52
  export function withoutYieldAbort(message) {
39
- if (message.role !== 'assistant' || message.stopReason !== 'aborted')
53
+ if (message.role !== 'assistant' || !endedByAbort(message))
40
54
  return message;
41
55
  const { stopReason: _stopReason, errorMessage: _errorMessage, ...visible } = message;
42
56
  return visible;
@@ -12,18 +12,23 @@ export declare function isReviewBoundary(entry: unknown, reviewId?: string): boo
12
12
  /**
13
13
  * Project snapshot messages to the visible window for a review companion.
14
14
  *
15
+ * A companion passes its binding's review id so the slice starts at ITS OWN
16
+ * boundary marker: a node forked from another companion inherits that
17
+ * companion's earlier marker, and slicing at the first marker of any review
18
+ * would reveal the origin companion's transcript.
19
+ *
15
20
  * Contract (in evaluation order):
16
- * 1. Marker present → slice from the **first** occurrence (marker included) to
17
- * the end. Everything before the marker is inherited context, hidden from
18
- * the person.
19
- * 2. Not a review companion → return input unchanged (same reference). Ordinary
20
- * nodes are not filtered.
21
- * 3. No marker and review companion → a leading compaction summary is replaced
21
+ * 1. This binding's marker present (any marker when the node has no binding) →
22
+ * slice from the **first** occurrence (marker included) to the end.
23
+ * Everything before the marker is inherited context, hidden from the person.
24
+ * 2. Not a companion (`boundaryReviewId` undefined) → return input unchanged
25
+ * (same reference). Ordinary nodes are not filtered.
26
+ * 3. Bound but no matching marker → a leading compaction summary is replaced
22
27
  * with a synthetic boundary while its retained tail remains visible. Every
23
28
  * other markerless shape fails closed with the synthetic boundary alone.
24
29
  */
25
30
  export declare function visibleMessages(messages: BrokerSnapshot['messages'], opts: {
26
- reviewCompanion: boolean;
31
+ boundaryReviewId: string | undefined;
27
32
  }): BrokerSnapshot['messages'];
28
33
  /**
29
34
  * Project a session tree to the review companion's visible transcript boundary.
@@ -33,5 +38,9 @@ export declare function visibleMessages(messages: BrokerSnapshot['messages'], op
33
38
  * boundary is crossed, its complete descendant subtree remains navigable. An
34
39
  * absent boundary produces an empty tree, so picker previews never disclose an
35
40
  * inherited entry before a mutation guard can reject its selection.
41
+ *
42
+ * `boundaryReviewId` selects only this binding's marker as a root, so a
43
+ * companion forked from another companion never navigates into the inherited
44
+ * subtree rooted at the origin's earlier marker.
36
45
  */
37
- export declare function boundaryRootedTree(tree: readonly WireSessionTreeNode[]): WireSessionTreeNode[];
46
+ export declare function boundaryRootedTree(tree: readonly WireSessionTreeNode[], boundaryReviewId?: string): WireSessionTreeNode[];