@north-light/crouter 0.3.154

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 (1643) hide show
  1. package/README.md +48 -0
  2. package/bin/crouter +4 -0
  3. package/bin/crtr +4 -0
  4. package/bin/crtrd +4 -0
  5. package/bin/runtime-generation-manifest.mjs +263 -0
  6. package/bin/runtime-selector.mjs +378 -0
  7. package/dist/api/__tests__/client.test.d.ts +1 -0
  8. package/dist/api/__tests__/client.test.js +274 -0
  9. package/dist/api/client.d.ts +249 -0
  10. package/dist/api/client.js +616 -0
  11. package/dist/api/dto/attach.d.ts +16 -0
  12. package/dist/api/dto/attach.js +13 -0
  13. package/dist/api/dto/broker.d.ts +45 -0
  14. package/dist/api/dto/broker.js +20 -0
  15. package/dist/api/dto/canvas.d.ts +253 -0
  16. package/dist/api/dto/canvas.js +2 -0
  17. package/dist/api/dto/common.d.ts +27 -0
  18. package/dist/api/dto/common.js +15 -0
  19. package/dist/api/dto/config.d.ts +19 -0
  20. package/dist/api/dto/config.js +3 -0
  21. package/dist/api/dto/crons.d.ts +124 -0
  22. package/dist/api/dto/crons.js +10 -0
  23. package/dist/api/dto/files.d.ts +11 -0
  24. package/dist/api/dto/files.js +7 -0
  25. package/dist/api/dto/focus.d.ts +24 -0
  26. package/dist/api/dto/focus.js +10 -0
  27. package/dist/api/dto/health.d.ts +41 -0
  28. package/dist/api/dto/health.js +2 -0
  29. package/dist/api/dto/human.d.ts +57 -0
  30. package/dist/api/dto/human.js +4 -0
  31. package/dist/api/dto/inbox.d.ts +105 -0
  32. package/dist/api/dto/inbox.js +10 -0
  33. package/dist/api/dto/lifecycle.d.ts +79 -0
  34. package/dist/api/dto/lifecycle.js +3 -0
  35. package/dist/api/dto/messages.d.ts +55 -0
  36. package/dist/api/dto/messages.js +2 -0
  37. package/dist/api/dto/modelauth.d.ts +41 -0
  38. package/dist/api/dto/modelauth.js +3 -0
  39. package/dist/api/dto/nodes.d.ts +209 -0
  40. package/dist/api/dto/nodes.js +3 -0
  41. package/dist/api/dto/profiles.d.ts +14 -0
  42. package/dist/api/dto/profiles.js +3 -0
  43. package/dist/api/dto/reports.d.ts +41 -0
  44. package/dist/api/dto/reports.js +2 -0
  45. package/dist/api/dto/subscriptions.d.ts +14 -0
  46. package/dist/api/dto/subscriptions.js +2 -0
  47. package/dist/api/dto/worktree.d.ts +19 -0
  48. package/dist/api/dto/worktree.js +6 -0
  49. package/dist/api/errors.d.ts +19 -0
  50. package/dist/api/errors.js +30 -0
  51. package/dist/api/index.d.ts +24 -0
  52. package/dist/api/index.js +25 -0
  53. package/dist/api/routes.d.ts +64 -0
  54. package/dist/api/routes.js +92 -0
  55. package/dist/build-root.d.ts +43 -0
  56. package/dist/build-root.js +96 -0
  57. package/dist/builtin-memory/00-runtime-base.md +51 -0
  58. package/dist/builtin-memory/01-spine/00-has-manager.md +23 -0
  59. package/dist/builtin-memory/01-spine/01-no-manager.md +10 -0
  60. package/dist/builtin-memory/02-lifecycle/00-terminal.md +19 -0
  61. package/dist/builtin-memory/02-lifecycle/01-resident.md +10 -0
  62. package/dist/builtin-memory/04-base-worker.md +18 -0
  63. package/dist/builtin-memory/04-orchestration-kernel.md +109 -0
  64. package/dist/builtin-memory/05-kinds/advisor/00-base.md +19 -0
  65. package/dist/builtin-memory/05-kinds/advisor/01-orchestrator.md +26 -0
  66. package/dist/builtin-memory/05-kinds/design/00-base.md +20 -0
  67. package/dist/builtin-memory/05-kinds/design/01-orchestrator.md +13 -0
  68. package/dist/builtin-memory/05-kinds/developer/00-base.md +22 -0
  69. package/dist/builtin-memory/05-kinds/developer/01-orchestrator.md +21 -0
  70. package/dist/builtin-memory/05-kinds/explore/00-base.md +20 -0
  71. package/dist/builtin-memory/05-kinds/explore/01-orchestrator.md +16 -0
  72. package/dist/builtin-memory/05-kinds/general/00-base.md +15 -0
  73. package/dist/builtin-memory/05-kinds/general/01-orchestrator.md +11 -0
  74. package/dist/builtin-memory/05-kinds/plan/00-base.md +20 -0
  75. package/dist/builtin-memory/05-kinds/plan/01-orchestrator.md +17 -0
  76. package/dist/builtin-memory/05-kinds/plan/reviewers/architecture-fit.md +17 -0
  77. package/dist/builtin-memory/05-kinds/plan/reviewers/code-smells.md +17 -0
  78. package/dist/builtin-memory/05-kinds/plan/reviewers/pattern-consistency.md +19 -0
  79. package/dist/builtin-memory/05-kinds/plan/reviewers/requirements-coverage.md +17 -0
  80. package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +20 -0
  81. package/dist/builtin-memory/05-kinds/product/00-base.md +25 -0
  82. package/dist/builtin-memory/05-kinds/product/01-orchestrator.md +15 -0
  83. package/dist/builtin-memory/05-kinds/product/teardown.md +15 -0
  84. package/dist/builtin-memory/05-kinds/review/00-base.md +19 -0
  85. package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +17 -0
  86. package/dist/builtin-memory/05-kinds/spec/00-base.md +20 -0
  87. package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +15 -0
  88. package/dist/builtin-memory/05-kinds/spec/requirements.md +13 -0
  89. package/dist/builtin-memory/design.md +62 -0
  90. package/dist/builtin-memory/development.md +120 -0
  91. package/dist/builtin-memory/internal/INDEX.md +26 -0
  92. package/dist/builtin-memory/internal/agent-shaping.md +77 -0
  93. package/dist/builtin-memory/internal/examples/INDEX.md +15 -0
  94. package/dist/builtin-memory/internal/examples/imessage-assistant.md +100 -0
  95. package/dist/builtin-memory/internal/marketplaces.md +164 -0
  96. package/dist/builtin-memory/internal/nodes-and-canvas.md +51 -0
  97. package/dist/builtin-memory/internal/plugins.md +249 -0
  98. package/dist/builtin-memory/internal/storage-tiers.md +41 -0
  99. package/dist/builtin-memory/planning.md +43 -0
  100. package/dist/builtin-memory/product.md +80 -0
  101. package/dist/builtin-memory/spec.md +98 -0
  102. package/dist/builtin-memory/wedged-child-on-runaway-bash.md +34 -0
  103. package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +47 -0
  104. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/claude-plugin-commands.test.ts +49 -0
  105. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/crtr-commands.test.ts +93 -0
  106. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/memory-slash-commands.test.ts +194 -0
  107. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/provider-rotation.test.ts +2632 -0
  108. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/statusline.test.ts +68 -0
  109. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/claude-plugin-commands.ts +492 -0
  110. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/crouter-help.ts +95 -0
  111. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/crtr-commands/filters.json +7 -0
  112. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/crtr-commands/index.ts +191 -0
  113. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/frontmatter-rules/index.ts +448 -0
  114. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/frontmatter-rules/test.ts +119 -0
  115. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +195 -0
  116. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +1232 -0
  117. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/statusline.ts +255 -0
  118. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/strip-skills-docs.ts +47 -0
  119. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/sysprompt-window.ts +19 -0
  120. package/dist/builtin-pi-packages/pi-crtr-extensions/lib/package.json +3 -0
  121. package/dist/builtin-pi-packages/pi-crtr-extensions/lib/subscription-state.js +1 -0
  122. package/dist/builtin-pi-packages/pi-crtr-extensions/lib/subscription-state.ts +2 -0
  123. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/LICENSE +13 -0
  124. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/README.md +172 -0
  125. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/bin.mjs +11 -0
  126. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/compose/compose-collection.js +88 -0
  127. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/compose/compose-doc.js +43 -0
  128. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/compose/compose-node.js +109 -0
  129. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/compose/compose-scalar.js +86 -0
  130. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/compose/composer.js +219 -0
  131. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/compose/resolve-block-map.js +115 -0
  132. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/compose/resolve-block-scalar.js +198 -0
  133. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/compose/resolve-block-seq.js +49 -0
  134. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/compose/resolve-end.js +37 -0
  135. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/compose/resolve-flow-collection.js +207 -0
  136. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/compose/resolve-flow-scalar.js +225 -0
  137. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/compose/resolve-props.js +146 -0
  138. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/compose/util-contains-newline.js +34 -0
  139. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/compose/util-empty-scalar-position.js +26 -0
  140. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/compose/util-flow-indent-check.js +15 -0
  141. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/compose/util-map-includes.js +13 -0
  142. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/doc/Document.js +335 -0
  143. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/doc/anchors.js +71 -0
  144. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/doc/applyReviver.js +55 -0
  145. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/doc/createNode.js +88 -0
  146. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/doc/directives.js +176 -0
  147. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/errors.js +57 -0
  148. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/index.js +17 -0
  149. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/log.js +11 -0
  150. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/nodes/Alias.js +116 -0
  151. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/nodes/Collection.js +147 -0
  152. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/nodes/Node.js +38 -0
  153. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/nodes/Pair.js +36 -0
  154. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/nodes/Scalar.js +24 -0
  155. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/nodes/YAMLMap.js +144 -0
  156. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/nodes/YAMLSeq.js +113 -0
  157. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/nodes/addPairToJSMap.js +63 -0
  158. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/nodes/identity.js +36 -0
  159. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/nodes/toJS.js +37 -0
  160. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/parse/cst-scalar.js +214 -0
  161. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/parse/cst-stringify.js +61 -0
  162. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/parse/cst-visit.js +97 -0
  163. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/parse/cst.js +98 -0
  164. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/parse/lexer.js +721 -0
  165. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/parse/line-counter.js +39 -0
  166. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/parse/parser.js +975 -0
  167. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/public-api.js +102 -0
  168. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/Schema.js +37 -0
  169. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/common/map.js +17 -0
  170. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/common/null.js +15 -0
  171. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/common/seq.js +17 -0
  172. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/common/string.js +14 -0
  173. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/core/bool.js +19 -0
  174. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/core/float.js +43 -0
  175. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/core/int.js +38 -0
  176. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/core/schema.js +23 -0
  177. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/json/schema.js +62 -0
  178. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/tags.js +96 -0
  179. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/yaml-1.1/binary.js +58 -0
  180. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/yaml-1.1/bool.js +26 -0
  181. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/yaml-1.1/float.js +46 -0
  182. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/yaml-1.1/int.js +71 -0
  183. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/yaml-1.1/merge.js +67 -0
  184. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/yaml-1.1/omap.js +74 -0
  185. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/yaml-1.1/pairs.js +78 -0
  186. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/yaml-1.1/schema.js +39 -0
  187. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/yaml-1.1/set.js +93 -0
  188. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/yaml-1.1/timestamp.js +101 -0
  189. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/stringify/foldFlowLines.js +146 -0
  190. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/stringify/stringify.js +129 -0
  191. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/stringify/stringifyCollection.js +153 -0
  192. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/stringify/stringifyComment.js +20 -0
  193. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/stringify/stringifyDocument.js +85 -0
  194. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/stringify/stringifyNumber.js +25 -0
  195. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/stringify/stringifyPair.js +150 -0
  196. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/stringify/stringifyString.js +336 -0
  197. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/util.js +11 -0
  198. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/visit.js +233 -0
  199. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/index.js +5 -0
  200. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/package.json +3 -0
  201. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/cli.d.ts +8 -0
  202. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/cli.mjs +201 -0
  203. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/compose-collection.d.ts +11 -0
  204. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/compose-collection.js +90 -0
  205. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/compose-doc.d.ts +7 -0
  206. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/compose-doc.js +45 -0
  207. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/compose-node.d.ts +29 -0
  208. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/compose-node.js +112 -0
  209. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/compose-scalar.d.ts +5 -0
  210. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/compose-scalar.js +88 -0
  211. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/composer.d.ts +63 -0
  212. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/composer.js +224 -0
  213. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/resolve-block-map.d.ts +6 -0
  214. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/resolve-block-map.js +117 -0
  215. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/resolve-block-scalar.d.ts +11 -0
  216. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/resolve-block-scalar.js +200 -0
  217. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/resolve-block-seq.d.ts +6 -0
  218. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/resolve-block-seq.js +51 -0
  219. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/resolve-end.d.ts +6 -0
  220. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/resolve-end.js +39 -0
  221. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/resolve-flow-collection.d.ts +7 -0
  222. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/resolve-flow-collection.js +209 -0
  223. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/resolve-flow-scalar.d.ts +10 -0
  224. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/resolve-flow-scalar.js +227 -0
  225. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/resolve-props.d.ts +23 -0
  226. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/resolve-props.js +148 -0
  227. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/util-contains-newline.d.ts +2 -0
  228. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/util-contains-newline.js +36 -0
  229. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/util-empty-scalar-position.d.ts +2 -0
  230. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/util-empty-scalar-position.js +28 -0
  231. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/util-flow-indent-check.d.ts +3 -0
  232. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/util-flow-indent-check.js +17 -0
  233. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/util-map-includes.d.ts +4 -0
  234. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/util-map-includes.js +15 -0
  235. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/doc/Document.d.ts +141 -0
  236. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/doc/Document.js +337 -0
  237. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/doc/anchors.d.ts +24 -0
  238. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/doc/anchors.js +76 -0
  239. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/doc/applyReviver.d.ts +9 -0
  240. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/doc/applyReviver.js +57 -0
  241. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/doc/createNode.d.ts +17 -0
  242. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/doc/createNode.js +90 -0
  243. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/doc/directives.d.ts +49 -0
  244. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/doc/directives.js +178 -0
  245. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/errors.d.ts +21 -0
  246. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/errors.js +62 -0
  247. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/index.d.ts +25 -0
  248. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/index.js +50 -0
  249. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/log.d.ts +3 -0
  250. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/log.js +19 -0
  251. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/Alias.d.ts +29 -0
  252. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/Alias.js +118 -0
  253. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/Collection.d.ts +73 -0
  254. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/Collection.js +151 -0
  255. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/Node.d.ts +53 -0
  256. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/Node.js +40 -0
  257. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/Pair.d.ts +22 -0
  258. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/Pair.js +39 -0
  259. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/Scalar.d.ts +47 -0
  260. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/Scalar.js +27 -0
  261. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/YAMLMap.d.ts +53 -0
  262. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/YAMLMap.js +147 -0
  263. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/YAMLSeq.d.ts +60 -0
  264. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/YAMLSeq.js +115 -0
  265. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/addPairToJSMap.d.ts +4 -0
  266. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/addPairToJSMap.js +65 -0
  267. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/identity.d.ts +23 -0
  268. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/identity.js +53 -0
  269. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/toJS.d.ts +29 -0
  270. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/toJS.js +39 -0
  271. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/options.d.ts +350 -0
  272. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/parse/cst-scalar.d.ts +64 -0
  273. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/parse/cst-scalar.js +218 -0
  274. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/parse/cst-stringify.d.ts +8 -0
  275. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/parse/cst-stringify.js +63 -0
  276. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/parse/cst-visit.d.ts +39 -0
  277. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/parse/cst-visit.js +99 -0
  278. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/parse/cst.d.ts +109 -0
  279. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/parse/cst.js +112 -0
  280. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/parse/lexer.d.ts +87 -0
  281. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/parse/lexer.js +723 -0
  282. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/parse/line-counter.d.ts +22 -0
  283. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/parse/line-counter.js +41 -0
  284. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/parse/parser.d.ts +84 -0
  285. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/parse/parser.js +980 -0
  286. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/public-api.d.ts +44 -0
  287. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/public-api.js +107 -0
  288. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/Schema.d.ts +17 -0
  289. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/Schema.js +39 -0
  290. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/common/map.d.ts +2 -0
  291. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/common/map.js +19 -0
  292. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/common/null.d.ts +4 -0
  293. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/common/null.js +17 -0
  294. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/common/seq.d.ts +2 -0
  295. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/common/seq.js +19 -0
  296. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/common/string.d.ts +2 -0
  297. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/common/string.js +16 -0
  298. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/core/bool.d.ts +4 -0
  299. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/core/bool.js +21 -0
  300. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/core/float.d.ts +4 -0
  301. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/core/float.js +47 -0
  302. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/core/int.d.ts +4 -0
  303. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/core/int.js +42 -0
  304. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/core/schema.d.ts +1 -0
  305. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/core/schema.js +25 -0
  306. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/json/schema.d.ts +2 -0
  307. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/json/schema.js +64 -0
  308. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/json-schema.d.ts +69 -0
  309. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/tags.d.ts +48 -0
  310. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/tags.js +99 -0
  311. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/types.d.ts +92 -0
  312. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/binary.d.ts +2 -0
  313. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/binary.js +70 -0
  314. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/bool.d.ts +7 -0
  315. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/bool.js +29 -0
  316. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/float.d.ts +4 -0
  317. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/float.js +50 -0
  318. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/int.d.ts +5 -0
  319. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/int.js +76 -0
  320. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/merge.d.ts +9 -0
  321. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/merge.js +71 -0
  322. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/omap.d.ts +22 -0
  323. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/omap.js +77 -0
  324. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/pairs.d.ts +10 -0
  325. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/pairs.js +82 -0
  326. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/schema.d.ts +1 -0
  327. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/schema.js +41 -0
  328. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/set.d.ts +28 -0
  329. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/set.js +96 -0
  330. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/timestamp.d.ts +6 -0
  331. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/timestamp.js +105 -0
  332. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/stringify/foldFlowLines.d.ts +34 -0
  333. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/stringify/foldFlowLines.js +151 -0
  334. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/stringify/stringify.d.ts +21 -0
  335. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/stringify/stringify.js +132 -0
  336. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/stringify/stringifyCollection.d.ts +17 -0
  337. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/stringify/stringifyCollection.js +155 -0
  338. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/stringify/stringifyComment.d.ts +10 -0
  339. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/stringify/stringifyComment.js +24 -0
  340. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/stringify/stringifyDocument.d.ts +4 -0
  341. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/stringify/stringifyDocument.js +87 -0
  342. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/stringify/stringifyNumber.d.ts +2 -0
  343. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/stringify/stringifyNumber.js +27 -0
  344. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/stringify/stringifyPair.d.ts +3 -0
  345. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/stringify/stringifyPair.js +152 -0
  346. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/stringify/stringifyString.d.ts +9 -0
  347. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/stringify/stringifyString.js +338 -0
  348. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/test-events.d.ts +4 -0
  349. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/test-events.js +134 -0
  350. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/util.d.ts +16 -0
  351. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/util.js +28 -0
  352. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/visit.d.ts +102 -0
  353. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/visit.js +236 -0
  354. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/package.json +97 -0
  355. package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/util.js +2 -0
  356. package/dist/builtin-pi-packages/pi-crtr-extensions/package-lock.json +3385 -0
  357. package/dist/builtin-pi-packages/pi-crtr-extensions/package.json +25 -0
  358. package/dist/builtin-pi-packages/pi-crtr-extensions/tsconfig.json +10 -0
  359. package/dist/builtin-pi-packages/pi-mode-switch/README.md +36 -0
  360. package/dist/builtin-pi-packages/pi-mode-switch/bin/mode +36 -0
  361. package/dist/builtin-pi-packages/pi-mode-switch/extensions/index.ts +449 -0
  362. package/dist/builtin-pi-packages/pi-mode-switch/package.json +14 -0
  363. package/dist/builtin-pi-packages/pi-mode-switch/tsconfig.json +9 -0
  364. package/dist/builtin-views/_lib/states.mjs +161 -0
  365. package/dist/builtin-views/canvas/core.mjs +657 -0
  366. package/dist/builtin-views/canvas/text.mjs +58 -0
  367. package/dist/builtin-views/canvas/tui.mjs +168 -0
  368. package/dist/builtin-views/canvas/web.jsx +121 -0
  369. package/dist/builtin-views/chat/core.mjs +684 -0
  370. package/dist/builtin-views/chat/text.mjs +101 -0
  371. package/dist/builtin-views/chat/tui.mjs +360 -0
  372. package/dist/builtin-views/chat/web.jsx +362 -0
  373. package/dist/builtin-views/git-pr/core.mjs +673 -0
  374. package/dist/builtin-views/git-pr/text.mjs +84 -0
  375. package/dist/builtin-views/git-pr/tui.mjs +301 -0
  376. package/dist/builtin-views/git-pr/web.jsx +216 -0
  377. package/dist/builtin-views/inbox/_lib/render.mjs +175 -0
  378. package/dist/builtin-views/inbox/core.mjs +1273 -0
  379. package/dist/builtin-views/inbox/text.mjs +73 -0
  380. package/dist/builtin-views/inbox/tui.mjs +314 -0
  381. package/dist/builtin-views/inbox/web.jsx +188 -0
  382. package/dist/builtin-views/linkedin/core.mjs +906 -0
  383. package/dist/builtin-views/linkedin/text.mjs +69 -0
  384. package/dist/builtin-views/linkedin/tui.mjs +413 -0
  385. package/dist/builtin-views/linkedin/web.jsx +206 -0
  386. package/dist/builtin-views/prompt-review/core.mjs +735 -0
  387. package/dist/builtin-views/prompt-review/text.mjs +15 -0
  388. package/dist/builtin-views/prompt-review/tui.mjs +196 -0
  389. package/dist/builtin-views/prompt-review/web.jsx +484 -0
  390. package/dist/builtin-views/workspace-sidebar/__tests__/core.test.d.ts +1 -0
  391. package/dist/builtin-views/workspace-sidebar/__tests__/core.test.js +50 -0
  392. package/dist/builtin-views/workspace-sidebar/__tests__/core.test.ts +53 -0
  393. package/dist/builtin-views/workspace-sidebar/core.mjs +670 -0
  394. package/dist/builtin-views/workspace-sidebar/text.mjs +53 -0
  395. package/dist/builtin-views/workspace-sidebar/tui.mjs +141 -0
  396. package/dist/builtin-views/workspace-sidebar/web.jsx +109 -0
  397. package/dist/cli.d.ts +2 -0
  398. package/dist/cli.js +86 -0
  399. package/dist/clients/attach/__tests__/attach-chrome-remote.test.d.ts +1 -0
  400. package/dist/clients/attach/__tests__/attach-chrome-remote.test.js +467 -0
  401. package/dist/clients/attach/__tests__/attach-keybindings.test.d.ts +1 -0
  402. package/dist/clients/attach/__tests__/attach-keybindings.test.js +141 -0
  403. package/dist/clients/attach/__tests__/attach-remote-readonly.test.d.ts +1 -0
  404. package/dist/clients/attach/__tests__/attach-remote-readonly.test.js +191 -0
  405. package/dist/clients/attach/__tests__/chat-view-snapshot-ordering.test.d.ts +1 -0
  406. package/dist/clients/attach/__tests__/chat-view-snapshot-ordering.test.js +236 -0
  407. package/dist/clients/attach/__tests__/context-message.test.d.ts +1 -0
  408. package/dist/clients/attach/__tests__/context-message.test.js +46 -0
  409. package/dist/clients/attach/__tests__/crtr-output-coverage.test.d.ts +1 -0
  410. package/dist/clients/attach/__tests__/crtr-output-coverage.test.js +55 -0
  411. package/dist/clients/attach/__tests__/crtr-output.test.d.ts +1 -0
  412. package/dist/clients/attach/__tests__/crtr-output.test.js +2103 -0
  413. package/dist/clients/attach/__tests__/diagram.test.d.ts +1 -0
  414. package/dist/clients/attach/__tests__/diagram.test.js +42 -0
  415. package/dist/clients/attach/__tests__/markdown-headings.test.d.ts +1 -0
  416. package/dist/clients/attach/__tests__/markdown-headings.test.js +27 -0
  417. package/dist/clients/attach/__tests__/oauth-dialog-lifecycle.test.d.ts +1 -0
  418. package/dist/clients/attach/__tests__/oauth-dialog-lifecycle.test.js +133 -0
  419. package/dist/clients/attach/__tests__/prompt-preview.test.d.ts +1 -0
  420. package/dist/clients/attach/__tests__/prompt-preview.test.js +87 -0
  421. package/dist/clients/attach/__tests__/ref-autocomplete.test.d.ts +1 -0
  422. package/dist/clients/attach/__tests__/ref-autocomplete.test.js +176 -0
  423. package/dist/clients/attach/__tests__/titled-editor-preview.test.d.ts +1 -0
  424. package/dist/clients/attach/__tests__/titled-editor-preview.test.js +271 -0
  425. package/dist/clients/attach/__tests__/visible-paths.test.d.ts +1 -0
  426. package/dist/clients/attach/__tests__/visible-paths.test.js +19 -0
  427. package/dist/clients/attach/chrome/bash-jobs.d.ts +9 -0
  428. package/dist/clients/attach/chrome/bash-jobs.js +43 -0
  429. package/dist/clients/attach/chrome/canvas-panels.d.ts +36 -0
  430. package/dist/clients/attach/chrome/canvas-panels.js +172 -0
  431. package/dist/clients/attach/chrome/completion-bell.d.ts +10 -0
  432. package/dist/clients/attach/chrome/completion-bell.js +89 -0
  433. package/dist/clients/attach/chrome/cron-wait.d.ts +3 -0
  434. package/dist/clients/attach/chrome/cron-wait.js +34 -0
  435. package/dist/clients/attach/chrome/git-info.d.ts +20 -0
  436. package/dist/clients/attach/chrome/git-info.js +44 -0
  437. package/dist/clients/attach/chrome/roster.d.ts +26 -0
  438. package/dist/clients/attach/chrome/roster.js +233 -0
  439. package/dist/clients/attach/chrome/status-line.d.ts +35 -0
  440. package/dist/clients/attach/chrome/status-line.js +133 -0
  441. package/dist/clients/attach/chrome/widgets.d.ts +9 -0
  442. package/dist/clients/attach/chrome/widgets.js +61 -0
  443. package/dist/clients/attach/command.d.ts +2 -0
  444. package/dist/clients/attach/command.js +96 -0
  445. package/dist/clients/attach/config.d.ts +91 -0
  446. package/dist/clients/attach/config.js +271 -0
  447. package/dist/clients/attach/input/clipboard-image.d.ts +55 -0
  448. package/dist/clients/attach/input/clipboard-image.js +237 -0
  449. package/dist/clients/attach/input/controller.d.ts +210 -0
  450. package/dist/clients/attach/input/controller.js +689 -0
  451. package/dist/clients/attach/input/ref-autocomplete.d.ts +75 -0
  452. package/dist/clients/attach/input/ref-autocomplete.js +135 -0
  453. package/dist/clients/attach/input/titled-editor.d.ts +209 -0
  454. package/dist/clients/attach/input/titled-editor.js +801 -0
  455. package/dist/clients/attach/overlays/auth.d.ts +13 -0
  456. package/dist/clients/attach/overlays/auth.js +245 -0
  457. package/dist/clients/attach/overlays/dialogs.d.ts +27 -0
  458. package/dist/clients/attach/overlays/dialogs.js +102 -0
  459. package/dist/clients/attach/overlays/file-review.d.ts +9 -0
  460. package/dist/clients/attach/overlays/file-review.js +181 -0
  461. package/dist/clients/attach/overlays/graph.d.ts +76 -0
  462. package/dist/clients/attach/overlays/graph.js +657 -0
  463. package/dist/clients/attach/overlays/mcp.d.ts +32 -0
  464. package/dist/clients/attach/overlays/mcp.js +310 -0
  465. package/dist/clients/attach/overlays/pickers.d.ts +59 -0
  466. package/dist/clients/attach/overlays/pickers.js +170 -0
  467. package/dist/clients/attach/render/chat-view.d.ts +185 -0
  468. package/dist/clients/attach/render/chat-view.js +946 -0
  469. package/dist/clients/attach/render/context-message.d.ts +14 -0
  470. package/dist/clients/attach/render/context-message.js +81 -0
  471. package/dist/clients/attach/render/crtr-output.d.ts +48 -0
  472. package/dist/clients/attach/render/crtr-output.js +694 -0
  473. package/dist/clients/attach/render/diagram.d.ts +14 -0
  474. package/dist/clients/attach/render/diagram.js +135 -0
  475. package/dist/clients/attach/render/edit-diff.d.ts +6 -0
  476. package/dist/clients/attach/render/edit-diff.js +263 -0
  477. package/dist/clients/attach/render/frozen-history.d.ts +38 -0
  478. package/dist/clients/attach/render/frozen-history.js +100 -0
  479. package/dist/clients/attach/render/markdown-headings.d.ts +11 -0
  480. package/dist/clients/attach/render/markdown-headings.js +82 -0
  481. package/dist/clients/attach/session/banner.d.ts +9 -0
  482. package/dist/clients/attach/session/banner.js +29 -0
  483. package/dist/clients/attach/session/bindings.d.ts +12 -0
  484. package/dist/clients/attach/session/bindings.js +52 -0
  485. package/dist/clients/attach/session/chrome-refresh.d.ts +34 -0
  486. package/dist/clients/attach/session/chrome-refresh.js +93 -0
  487. package/dist/clients/attach/session/connection.d.ts +44 -0
  488. package/dist/clients/attach/session/connection.js +178 -0
  489. package/dist/clients/attach/session/context.d.ts +54 -0
  490. package/dist/clients/attach/session/context.js +16 -0
  491. package/dist/clients/attach/session/editor-frame.d.ts +43 -0
  492. package/dist/clients/attach/session/editor-frame.js +136 -0
  493. package/dist/clients/attach/session/editor-inventory.d.ts +59 -0
  494. package/dist/clients/attach/session/editor-inventory.js +161 -0
  495. package/dist/clients/attach/session/focus-events.d.ts +4 -0
  496. package/dist/clients/attach/session/focus-events.js +12 -0
  497. package/dist/clients/attach/session/frames.d.ts +43 -0
  498. package/dist/clients/attach/session/frames.js +170 -0
  499. package/dist/clients/attach/session/identity.d.ts +16 -0
  500. package/dist/clients/attach/session/identity.js +25 -0
  501. package/dist/clients/attach/session/input-wiring.d.ts +31 -0
  502. package/dist/clients/attach/session/input-wiring.js +128 -0
  503. package/dist/clients/attach/session/keys.d.ts +22 -0
  504. package/dist/clients/attach/session/keys.js +60 -0
  505. package/dist/clients/attach/session/layout.d.ts +20 -0
  506. package/dist/clients/attach/session/layout.js +56 -0
  507. package/dist/clients/attach/session/mode.d.ts +25 -0
  508. package/dist/clients/attach/session/mode.js +97 -0
  509. package/dist/clients/attach/session/pane-tag.d.ts +15 -0
  510. package/dist/clients/attach/session/pane-tag.js +43 -0
  511. package/dist/clients/attach/session/pi-assets.d.ts +1 -0
  512. package/dist/clients/attach/session/pi-assets.js +24 -0
  513. package/dist/clients/attach/session/pi-oauth-flows.d.ts +1 -0
  514. package/dist/clients/attach/session/pi-oauth-flows.js +21 -0
  515. package/dist/clients/attach/session/reconnect.d.ts +37 -0
  516. package/dist/clients/attach/session/reconnect.js +127 -0
  517. package/dist/clients/attach/session/state-sync.d.ts +38 -0
  518. package/dist/clients/attach/session/state-sync.js +68 -0
  519. package/dist/clients/attach/session/teardown.d.ts +30 -0
  520. package/dist/clients/attach/session/teardown.js +96 -0
  521. package/dist/clients/attach/slash/dispatch.d.ts +124 -0
  522. package/dist/clients/attach/slash/dispatch.js +696 -0
  523. package/dist/clients/attach/slash/prompt-preview.d.ts +30 -0
  524. package/dist/clients/attach/slash/prompt-preview.js +121 -0
  525. package/dist/clients/attach/viewer.d.ts +8 -0
  526. package/dist/clients/attach/viewer.js +1559 -0
  527. package/dist/clients/attach/visible-paths.d.ts +9 -0
  528. package/dist/clients/attach/visible-paths.js +76 -0
  529. package/dist/clients/web/__tests__/push-engine.test.d.ts +1 -0
  530. package/dist/clients/web/__tests__/push-engine.test.js +302 -0
  531. package/dist/clients/web/__tests__/push-registry.test.d.ts +1 -0
  532. package/dist/clients/web/__tests__/push-registry.test.js +177 -0
  533. package/dist/clients/web/dev-server.d.ts +7 -0
  534. package/dist/clients/web/dev-server.js +60 -0
  535. package/dist/clients/web/events.d.ts +19 -0
  536. package/dist/clients/web/events.js +165 -0
  537. package/dist/clients/web/push-engine.d.ts +108 -0
  538. package/dist/clients/web/push-engine.js +256 -0
  539. package/dist/clients/web/push-registry.d.ts +89 -0
  540. package/dist/clients/web/push-registry.js +378 -0
  541. package/dist/clients/web/server.d.ts +19 -0
  542. package/dist/clients/web/server.js +693 -0
  543. package/dist/clients/web/web-client/shared/protocol.d.ts +322 -0
  544. package/dist/clients/web/web-client/shared/protocol.js +8 -0
  545. package/dist/clients/web/web-cmd.d.ts +2 -0
  546. package/dist/clients/web/web-cmd.js +141 -0
  547. package/dist/commands/__tests__/api-client-cold-start-diagnostic.test.d.ts +1 -0
  548. package/dist/commands/__tests__/api-client-cold-start-diagnostic.test.js +96 -0
  549. package/dist/commands/__tests__/canvas-config.test.d.ts +1 -0
  550. package/dist/commands/__tests__/canvas-config.test.js +107 -0
  551. package/dist/commands/__tests__/human.test.d.ts +1 -0
  552. package/dist/commands/__tests__/human.test.js +344 -0
  553. package/dist/commands/__tests__/node-no-kickoff-flag.test.d.ts +1 -0
  554. package/dist/commands/__tests__/node-no-kickoff-flag.test.js +73 -0
  555. package/dist/commands/__tests__/node-worktree-flag.test.d.ts +1 -0
  556. package/dist/commands/__tests__/node-worktree-flag.test.js +49 -0
  557. package/dist/commands/__tests__/revive-now-gate.test.d.ts +1 -0
  558. package/dist/commands/__tests__/revive-now-gate.test.js +60 -0
  559. package/dist/commands/api-client.d.ts +100 -0
  560. package/dist/commands/api-client.js +377 -0
  561. package/dist/commands/attention.d.ts +2 -0
  562. package/dist/commands/attention.js +223 -0
  563. package/dist/commands/canvas-browse.d.ts +2 -0
  564. package/dist/commands/canvas-browse.js +95 -0
  565. package/dist/commands/canvas-config.d.ts +2 -0
  566. package/dist/commands/canvas-config.js +210 -0
  567. package/dist/commands/canvas-history/grep.d.ts +1 -0
  568. package/dist/commands/canvas-history/grep.js +104 -0
  569. package/dist/commands/canvas-history/read.d.ts +1 -0
  570. package/dist/commands/canvas-history/read.js +48 -0
  571. package/dist/commands/canvas-history/search.d.ts +1 -0
  572. package/dist/commands/canvas-history/search.js +118 -0
  573. package/dist/commands/canvas-history/shared.d.ts +27 -0
  574. package/dist/commands/canvas-history/shared.js +76 -0
  575. package/dist/commands/canvas-history.d.ts +2 -0
  576. package/dist/commands/canvas-history.js +30 -0
  577. package/dist/commands/canvas-issue.d.ts +2 -0
  578. package/dist/commands/canvas-issue.js +149 -0
  579. package/dist/commands/canvas-prune.d.ts +2 -0
  580. package/dist/commands/canvas-prune.js +108 -0
  581. package/dist/commands/canvas-rebuild-index.d.ts +2 -0
  582. package/dist/commands/canvas-rebuild-index.js +46 -0
  583. package/dist/commands/canvas-roster.d.ts +2 -0
  584. package/dist/commands/canvas-roster.js +62 -0
  585. package/dist/commands/canvas-snapshot.d.ts +2 -0
  586. package/dist/commands/canvas-snapshot.js +34 -0
  587. package/dist/commands/canvas-use.d.ts +1 -0
  588. package/dist/commands/canvas-use.js +63 -0
  589. package/dist/commands/canvas.d.ts +3 -0
  590. package/dist/commands/canvas.js +50 -0
  591. package/dist/commands/chord.d.ts +2 -0
  592. package/dist/commands/chord.js +242 -0
  593. package/dist/commands/cron.d.ts +2 -0
  594. package/dist/commands/cron.js +524 -0
  595. package/dist/commands/dashboard.d.ts +2 -0
  596. package/dist/commands/dashboard.js +80 -0
  597. package/dist/commands/human/prompts.d.ts +8 -0
  598. package/dist/commands/human/prompts.js +427 -0
  599. package/dist/commands/human/queue.d.ts +12 -0
  600. package/dist/commands/human/queue.js +531 -0
  601. package/dist/commands/human/shared.d.ts +80 -0
  602. package/dist/commands/human/shared.js +194 -0
  603. package/dist/commands/human.d.ts +2 -0
  604. package/dist/commands/human.js +44 -0
  605. package/dist/commands/memory/delete.d.ts +1 -0
  606. package/dist/commands/memory/delete.js +77 -0
  607. package/dist/commands/memory/find.d.ts +1 -0
  608. package/dist/commands/memory/find.js +192 -0
  609. package/dist/commands/memory/lint.d.ts +9 -0
  610. package/dist/commands/memory/lint.js +217 -0
  611. package/dist/commands/memory/list.d.ts +1 -0
  612. package/dist/commands/memory/list.js +84 -0
  613. package/dist/commands/memory/origin.d.ts +1 -0
  614. package/dist/commands/memory/origin.js +100 -0
  615. package/dist/commands/memory/read.d.ts +1 -0
  616. package/dist/commands/memory/read.js +72 -0
  617. package/dist/commands/memory/shared.d.ts +54 -0
  618. package/dist/commands/memory/shared.js +225 -0
  619. package/dist/commands/memory/write.d.ts +1 -0
  620. package/dist/commands/memory/write.js +142 -0
  621. package/dist/commands/memory.d.ts +2 -0
  622. package/dist/commands/memory.js +26 -0
  623. package/dist/commands/node-context.d.ts +2 -0
  624. package/dist/commands/node-context.js +157 -0
  625. package/dist/commands/node-inspect-artifacts.d.ts +1 -0
  626. package/dist/commands/node-inspect-artifacts.js +109 -0
  627. package/dist/commands/node-lifecycle-revive.d.ts +12 -0
  628. package/dist/commands/node-lifecycle-revive.js +287 -0
  629. package/dist/commands/node-snapshot.d.ts +2 -0
  630. package/dist/commands/node-snapshot.js +37 -0
  631. package/dist/commands/node-transcript.d.ts +2 -0
  632. package/dist/commands/node-transcript.js +62 -0
  633. package/dist/commands/node-worktree.d.ts +2 -0
  634. package/dist/commands/node-worktree.js +102 -0
  635. package/dist/commands/node.d.ts +44 -0
  636. package/dist/commands/node.js +1943 -0
  637. package/dist/commands/pkg/cli-inspect.d.ts +17 -0
  638. package/dist/commands/pkg/cli-inspect.js +190 -0
  639. package/dist/commands/pkg/cli-manage.d.ts +3 -0
  640. package/dist/commands/pkg/cli-manage.js +206 -0
  641. package/dist/commands/pkg/cli.d.ts +1 -0
  642. package/dist/commands/pkg/cli.js +14 -0
  643. package/dist/commands/pkg/market-inspect.d.ts +2 -0
  644. package/dist/commands/pkg/market-inspect.js +141 -0
  645. package/dist/commands/pkg/market-manage.d.ts +3 -0
  646. package/dist/commands/pkg/market-manage.js +224 -0
  647. package/dist/commands/pkg/market.d.ts +1 -0
  648. package/dist/commands/pkg/market.js +14 -0
  649. package/dist/commands/pkg/plugin-inspect.d.ts +2 -0
  650. package/dist/commands/pkg/plugin-inspect.js +150 -0
  651. package/dist/commands/pkg/plugin-manage.d.ts +19 -0
  652. package/dist/commands/pkg/plugin-manage.js +398 -0
  653. package/dist/commands/pkg/plugin.d.ts +1 -0
  654. package/dist/commands/pkg/plugin.js +14 -0
  655. package/dist/commands/pkg/shared.d.ts +5 -0
  656. package/dist/commands/pkg/shared.js +61 -0
  657. package/dist/commands/pkg.d.ts +2 -0
  658. package/dist/commands/pkg.js +24 -0
  659. package/dist/commands/profile/default.d.ts +2 -0
  660. package/dist/commands/profile/default.js +143 -0
  661. package/dist/commands/profile/delete.d.ts +1 -0
  662. package/dist/commands/profile/delete.js +39 -0
  663. package/dist/commands/profile/env.d.ts +2 -0
  664. package/dist/commands/profile/env.js +194 -0
  665. package/dist/commands/profile/list.d.ts +1 -0
  666. package/dist/commands/profile/list.js +35 -0
  667. package/dist/commands/profile/new.d.ts +1 -0
  668. package/dist/commands/profile/new.js +75 -0
  669. package/dist/commands/profile/project.d.ts +2 -0
  670. package/dist/commands/profile/project.js +97 -0
  671. package/dist/commands/profile/rename.d.ts +1 -0
  672. package/dist/commands/profile/rename.js +42 -0
  673. package/dist/commands/profile/show.d.ts +1 -0
  674. package/dist/commands/profile/show.js +50 -0
  675. package/dist/commands/profile.d.ts +2 -0
  676. package/dist/commands/profile.js +37 -0
  677. package/dist/commands/push.d.ts +2 -0
  678. package/dist/commands/push.js +166 -0
  679. package/dist/commands/revive.d.ts +2 -0
  680. package/dist/commands/revive.js +174 -0
  681. package/dist/commands/search/answer.d.ts +1 -0
  682. package/dist/commands/search/answer.js +50 -0
  683. package/dist/commands/search/contents.d.ts +1 -0
  684. package/dist/commands/search/contents.js +96 -0
  685. package/dist/commands/search/exa.d.ts +53 -0
  686. package/dist/commands/search/exa.js +157 -0
  687. package/dist/commands/search/puremd.d.ts +11 -0
  688. package/dist/commands/search/puremd.js +53 -0
  689. package/dist/commands/search/web.d.ts +1 -0
  690. package/dist/commands/search/web.js +65 -0
  691. package/dist/commands/search.d.ts +2 -0
  692. package/dist/commands/search.js +24 -0
  693. package/dist/commands/surface-edit.d.ts +2 -0
  694. package/dist/commands/surface-edit.js +159 -0
  695. package/dist/commands/surface-inspect.d.ts +2 -0
  696. package/dist/commands/surface-inspect.js +38 -0
  697. package/dist/commands/surface-node.d.ts +2 -0
  698. package/dist/commands/surface-node.js +16 -0
  699. package/dist/commands/surface-reload.d.ts +2 -0
  700. package/dist/commands/surface-reload.js +127 -0
  701. package/dist/commands/surface-tmux-spread.d.ts +2 -0
  702. package/dist/commands/surface-tmux-spread.js +220 -0
  703. package/dist/commands/surface.d.ts +2 -0
  704. package/dist/commands/surface.js +48 -0
  705. package/dist/commands/sys/__tests__/config-broker-thresholds.test.d.ts +1 -0
  706. package/dist/commands/sys/__tests__/config-broker-thresholds.test.js +95 -0
  707. package/dist/commands/sys/__tests__/config-keybindings.test.d.ts +1 -0
  708. package/dist/commands/sys/__tests__/config-keybindings.test.js +55 -0
  709. package/dist/commands/sys/__tests__/config-model-ladders.test.d.ts +1 -0
  710. package/dist/commands/sys/__tests__/config-model-ladders.test.js +121 -0
  711. package/dist/commands/sys/__tests__/helpers/setup-runtime.d.ts +5 -0
  712. package/dist/commands/sys/__tests__/helpers/setup-runtime.js +19 -0
  713. package/dist/commands/sys/__tests__/setup-core.test.d.ts +1 -0
  714. package/dist/commands/sys/__tests__/setup-core.test.js +291 -0
  715. package/dist/commands/sys/__tests__/setup-front-door.test.d.ts +1 -0
  716. package/dist/commands/sys/__tests__/setup-front-door.test.js +137 -0
  717. package/dist/commands/sys/__tests__/setup-models.test.d.ts +1 -0
  718. package/dist/commands/sys/__tests__/setup-models.test.js +205 -0
  719. package/dist/commands/sys/__tests__/setup-providers-viewport.test.d.ts +1 -0
  720. package/dist/commands/sys/__tests__/setup-providers-viewport.test.js +155 -0
  721. package/dist/commands/sys/__tests__/sync-deps.test.d.ts +1 -0
  722. package/dist/commands/sys/__tests__/sync-deps.test.js +188 -0
  723. package/dist/commands/sys/__tests__/sync-import.test.d.ts +1 -0
  724. package/dist/commands/sys/__tests__/sync-import.test.js +314 -0
  725. package/dist/commands/sys/config.d.ts +1 -0
  726. package/dist/commands/sys/config.js +292 -0
  727. package/dist/commands/sys/daemon.d.ts +2 -0
  728. package/dist/commands/sys/daemon.js +188 -0
  729. package/dist/commands/sys/doctor.d.ts +1 -0
  730. package/dist/commands/sys/doctor.js +528 -0
  731. package/dist/commands/sys/feedback.d.ts +1 -0
  732. package/dist/commands/sys/feedback.js +174 -0
  733. package/dist/commands/sys/logs.d.ts +1 -0
  734. package/dist/commands/sys/logs.js +127 -0
  735. package/dist/commands/sys/prompt-review.d.ts +1 -0
  736. package/dist/commands/sys/prompt-review.js +291 -0
  737. package/dist/commands/sys/promptstudio.d.ts +2 -0
  738. package/dist/commands/sys/promptstudio.js +65 -0
  739. package/dist/commands/sys/setup-core.d.ts +138 -0
  740. package/dist/commands/sys/setup-core.js +582 -0
  741. package/dist/commands/sys/setup-wizard.d.ts +130 -0
  742. package/dist/commands/sys/setup-wizard.js +2310 -0
  743. package/dist/commands/sys/setup.d.ts +6 -0
  744. package/dist/commands/sys/setup.js +36 -0
  745. package/dist/commands/sys/shared.d.ts +51 -0
  746. package/dist/commands/sys/shared.js +81 -0
  747. package/dist/commands/sys/support/prepare.d.ts +13 -0
  748. package/dist/commands/sys/support/prepare.js +235 -0
  749. package/dist/commands/sys/support/submit.d.ts +19 -0
  750. package/dist/commands/sys/support/submit.js +525 -0
  751. package/dist/commands/sys/support.d.ts +2 -0
  752. package/dist/commands/sys/support.js +18 -0
  753. package/dist/commands/sys/sync-deps.d.ts +19 -0
  754. package/dist/commands/sys/sync-deps.js +508 -0
  755. package/dist/commands/sys/sync-project-guidance.d.ts +1 -0
  756. package/dist/commands/sys/sync-project-guidance.js +345 -0
  757. package/dist/commands/sys/sync-shared.d.ts +7 -0
  758. package/dist/commands/sys/sync-shared.js +49 -0
  759. package/dist/commands/sys/sync-skills.d.ts +1 -0
  760. package/dist/commands/sys/sync-skills.js +306 -0
  761. package/dist/commands/sys/sync.d.ts +2 -0
  762. package/dist/commands/sys/sync.js +18 -0
  763. package/dist/commands/sys/sysprompt.d.ts +1 -0
  764. package/dist/commands/sys/sysprompt.js +97 -0
  765. package/dist/commands/sys/update.d.ts +2 -0
  766. package/dist/commands/sys/update.js +139 -0
  767. package/dist/commands/sys.d.ts +2 -0
  768. package/dist/commands/sys.js +30 -0
  769. package/dist/commands/view-cycle.d.ts +2 -0
  770. package/dist/commands/view-cycle.js +130 -0
  771. package/dist/commands/view-list.d.ts +2 -0
  772. package/dist/commands/view-list.js +66 -0
  773. package/dist/commands/view-new.d.ts +2 -0
  774. package/dist/commands/view-new.js +74 -0
  775. package/dist/commands/view-pick.d.ts +6 -0
  776. package/dist/commands/view-pick.js +129 -0
  777. package/dist/commands/view-run.d.ts +2 -0
  778. package/dist/commands/view-run.js +214 -0
  779. package/dist/commands/view.d.ts +2 -0
  780. package/dist/commands/view.js +31 -0
  781. package/dist/commands/workspace.d.ts +2 -0
  782. package/dist/commands/workspace.js +165 -0
  783. package/dist/core/__tests__/base-worker-prompt.test.d.ts +1 -0
  784. package/dist/core/__tests__/base-worker-prompt.test.js +60 -0
  785. package/dist/core/__tests__/boot-fault-attempt.test.d.ts +1 -0
  786. package/dist/core/__tests__/boot-fault-attempt.test.js +83 -0
  787. package/dist/core/__tests__/boot.test.d.ts +1 -0
  788. package/dist/core/__tests__/boot.test.js +275 -0
  789. package/dist/core/__tests__/branded-host-concurrency.test.d.ts +1 -0
  790. package/dist/core/__tests__/branded-host-concurrency.test.js +72 -0
  791. package/dist/core/__tests__/broker-fork-seam.test.d.ts +1 -0
  792. package/dist/core/__tests__/broker-fork-seam.test.js +118 -0
  793. package/dist/core/__tests__/broker-launch-candidates.test.d.ts +1 -0
  794. package/dist/core/__tests__/broker-launch-candidates.test.js +241 -0
  795. package/dist/core/__tests__/broker-preflight.test.d.ts +1 -0
  796. package/dist/core/__tests__/broker-preflight.test.js +95 -0
  797. package/dist/core/__tests__/broker-sdk-wiring.test.d.ts +1 -0
  798. package/dist/core/__tests__/broker-sdk-wiring.test.js +732 -0
  799. package/dist/core/__tests__/broker-snapshot-history.test.d.ts +1 -0
  800. package/dist/core/__tests__/broker-snapshot-history.test.js +213 -0
  801. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.d.ts +1 -0
  802. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +502 -0
  803. package/dist/core/__tests__/canvas-inbox-watcher.test.d.ts +1 -0
  804. package/dist/core/__tests__/canvas-inbox-watcher.test.js +255 -0
  805. package/dist/core/__tests__/canvas.test.d.ts +1 -0
  806. package/dist/core/__tests__/canvas.test.js +201 -0
  807. package/dist/core/__tests__/chat-view-reconnect.test.d.ts +1 -0
  808. package/dist/core/__tests__/chat-view-reconnect.test.js +105 -0
  809. package/dist/core/__tests__/child-death-wake.test.d.ts +1 -0
  810. package/dist/core/__tests__/child-death-wake.test.js +161 -0
  811. package/dist/core/__tests__/close.test.d.ts +1 -0
  812. package/dist/core/__tests__/close.test.js +254 -0
  813. package/dist/core/__tests__/command-plugins-surfaces.test.d.ts +1 -0
  814. package/dist/core/__tests__/command-plugins-surfaces.test.js +325 -0
  815. package/dist/core/__tests__/command-plugins.test.d.ts +1 -0
  816. package/dist/core/__tests__/command-plugins.test.js +448 -0
  817. package/dist/core/__tests__/connection-reconnect-resume.test.d.ts +1 -0
  818. package/dist/core/__tests__/connection-reconnect-resume.test.js +36 -0
  819. package/dist/core/__tests__/context-intro.test.d.ts +1 -0
  820. package/dist/core/__tests__/context-intro.test.js +256 -0
  821. package/dist/core/__tests__/cron-broker-capacity.test.d.ts +1 -0
  822. package/dist/core/__tests__/cron-broker-capacity.test.js +100 -0
  823. package/dist/core/__tests__/daemon-boot.test.d.ts +1 -0
  824. package/dist/core/__tests__/daemon-boot.test.js +374 -0
  825. package/dist/core/__tests__/daemon-wedge.test.d.ts +1 -0
  826. package/dist/core/__tests__/daemon-wedge.test.js +104 -0
  827. package/dist/core/__tests__/dead-node-policy-table.test.d.ts +1 -0
  828. package/dist/core/__tests__/dead-node-policy-table.test.js +103 -0
  829. package/dist/core/__tests__/deferred-no-wake.test.d.ts +1 -0
  830. package/dist/core/__tests__/deferred-no-wake.test.js +514 -0
  831. package/dist/core/__tests__/events-contract.test.d.ts +1 -0
  832. package/dist/core/__tests__/events-contract.test.js +76 -0
  833. package/dist/core/__tests__/fault-classifier.test.d.ts +1 -0
  834. package/dist/core/__tests__/fault-classifier.test.js +71 -0
  835. package/dist/core/__tests__/finalization-migration.test.d.ts +1 -0
  836. package/dist/core/__tests__/finalization-migration.test.js +38 -0
  837. package/dist/core/__tests__/fixtures/api-server-host.d.ts +1 -0
  838. package/dist/core/__tests__/fixtures/api-server-host.js +47 -0
  839. package/dist/core/__tests__/fixtures/c3-custom-provider-ext.d.ts +2 -0
  840. package/dist/core/__tests__/fixtures/c3-custom-provider-ext.js +34 -0
  841. package/dist/core/__tests__/fixtures/c5-command-boundary-ext.d.ts +2 -0
  842. package/dist/core/__tests__/fixtures/c5-command-boundary-ext.js +8 -0
  843. package/dist/core/__tests__/fixtures/extra-extension.d.ts +1 -0
  844. package/dist/core/__tests__/fixtures/extra-extension.js +3 -0
  845. package/dist/core/__tests__/fixtures/fake-engine.d.ts +310 -0
  846. package/dist/core/__tests__/fixtures/fake-engine.js +1210 -0
  847. package/dist/core/__tests__/fixtures/fake-pi-host.d.ts +2 -0
  848. package/dist/core/__tests__/fixtures/fake-pi-host.js +311 -0
  849. package/dist/core/__tests__/fixtures/metadata-migration-race.d.ts +1 -0
  850. package/dist/core/__tests__/fixtures/metadata-migration-race.js +49 -0
  851. package/dist/core/__tests__/fixtures/spawn-proof-target.test.d.ts +1 -0
  852. package/dist/core/__tests__/fixtures/spawn-proof-target.test.js +15 -0
  853. package/dist/core/__tests__/flagship-lifecycle.test.d.ts +1 -0
  854. package/dist/core/__tests__/flagship-lifecycle.test.js +303 -0
  855. package/dist/core/__tests__/focuses.test.d.ts +1 -0
  856. package/dist/core/__tests__/focuses.test.js +208 -0
  857. package/dist/core/__tests__/fork.test.d.ts +1 -0
  858. package/dist/core/__tests__/fork.test.js +108 -0
  859. package/dist/core/__tests__/full/broker-attach-limits.test.d.ts +1 -0
  860. package/dist/core/__tests__/full/broker-attach-limits.test.js +147 -0
  861. package/dist/core/__tests__/full/broker-attach-stream.test.d.ts +1 -0
  862. package/dist/core/__tests__/full/broker-attach-stream.test.js +133 -0
  863. package/dist/core/__tests__/full/broker-crash-teardown.test.d.ts +1 -0
  864. package/dist/core/__tests__/full/broker-crash-teardown.test.js +122 -0
  865. package/dist/core/__tests__/full/broker-deliver-ack.test.d.ts +1 -0
  866. package/dist/core/__tests__/full/broker-deliver-ack.test.js +73 -0
  867. package/dist/core/__tests__/full/broker-dialogs.test.d.ts +1 -0
  868. package/dist/core/__tests__/full/broker-dialogs.test.js +180 -0
  869. package/dist/core/__tests__/full/broker-display-snapshot.test.d.ts +1 -0
  870. package/dist/core/__tests__/full/broker-display-snapshot.test.js +75 -0
  871. package/dist/core/__tests__/full/broker-dormant-wake.test.d.ts +1 -0
  872. package/dist/core/__tests__/full/broker-dormant-wake.test.js +130 -0
  873. package/dist/core/__tests__/full/broker-model-changed-broadcast.test.d.ts +1 -0
  874. package/dist/core/__tests__/full/broker-model-changed-broadcast.test.js +57 -0
  875. package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.d.ts +1 -0
  876. package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.js +80 -0
  877. package/dist/core/__tests__/full/broker-pane-resolution.test.d.ts +1 -0
  878. package/dist/core/__tests__/full/broker-pane-resolution.test.js +155 -0
  879. package/dist/core/__tests__/full/broker-provider-retry.test.d.ts +1 -0
  880. package/dist/core/__tests__/full/broker-provider-retry.test.js +387 -0
  881. package/dist/core/__tests__/full/cascade-close.test.d.ts +1 -0
  882. package/dist/core/__tests__/full/cascade-close.test.js +240 -0
  883. package/dist/core/__tests__/full/consult-startup-failure.test.d.ts +1 -0
  884. package/dist/core/__tests__/full/consult-startup-failure.test.js +124 -0
  885. package/dist/core/__tests__/full/detach-focus.test.d.ts +1 -0
  886. package/dist/core/__tests__/full/detach-focus.test.js +126 -0
  887. package/dist/core/__tests__/full/idle-release-settlement.test.d.ts +1 -0
  888. package/dist/core/__tests__/full/idle-release-settlement.test.js +108 -0
  889. package/dist/core/__tests__/full/issue-146-yield-spawn-focus.test.d.ts +1 -0
  890. package/dist/core/__tests__/full/issue-146-yield-spawn-focus.test.js +107 -0
  891. package/dist/core/__tests__/full/park-clock-release.test.d.ts +1 -0
  892. package/dist/core/__tests__/full/park-clock-release.test.js +56 -0
  893. package/dist/core/__tests__/full/yield-message-transaction.test.d.ts +1 -0
  894. package/dist/core/__tests__/full/yield-message-transaction.test.js +70 -0
  895. package/dist/core/__tests__/helpers/broker-clients.d.ts +43 -0
  896. package/dist/core/__tests__/helpers/broker-clients.js +182 -0
  897. package/dist/core/__tests__/helpers/fleet.d.ts +12 -0
  898. package/dist/core/__tests__/helpers/fleet.js +45 -0
  899. package/dist/core/__tests__/helpers/harness.d.ts +128 -0
  900. package/dist/core/__tests__/helpers/harness.js +867 -0
  901. package/dist/core/__tests__/helpers/viewer-panes.d.ts +12 -0
  902. package/dist/core/__tests__/helpers/viewer-panes.js +133 -0
  903. package/dist/core/__tests__/history-inbox.test.d.ts +1 -0
  904. package/dist/core/__tests__/history-inbox.test.js +105 -0
  905. package/dist/core/__tests__/host-teardown-process-group.test.d.ts +1 -0
  906. package/dist/core/__tests__/host-teardown-process-group.test.js +476 -0
  907. package/dist/core/__tests__/human-cancel-guard.test.d.ts +1 -0
  908. package/dist/core/__tests__/human-cancel-guard.test.js +71 -0
  909. package/dist/core/__tests__/human-deliver-e2e.test.d.ts +1 -0
  910. package/dist/core/__tests__/human-deliver-e2e.test.js +123 -0
  911. package/dist/core/__tests__/human-deliver.test.d.ts +1 -0
  912. package/dist/core/__tests__/human-deliver.test.js +399 -0
  913. package/dist/core/__tests__/human-node-not-supervised.test.d.ts +1 -0
  914. package/dist/core/__tests__/human-node-not-supervised.test.js +94 -0
  915. package/dist/core/__tests__/inline-memory-refs.test.d.ts +1 -0
  916. package/dist/core/__tests__/inline-memory-refs.test.js +320 -0
  917. package/dist/core/__tests__/kickoff.test.d.ts +1 -0
  918. package/dist/core/__tests__/kickoff.test.js +148 -0
  919. package/dist/core/__tests__/lifecycle.test.d.ts +1 -0
  920. package/dist/core/__tests__/lifecycle.test.js +165 -0
  921. package/dist/core/__tests__/live-lifecycle-prompt.test.d.ts +1 -0
  922. package/dist/core/__tests__/live-lifecycle-prompt.test.js +51 -0
  923. package/dist/core/__tests__/live-mutation-verbs.test.d.ts +1 -0
  924. package/dist/core/__tests__/live-mutation-verbs.test.js +195 -0
  925. package/dist/core/__tests__/live-mutation.test.d.ts +1 -0
  926. package/dist/core/__tests__/live-mutation.test.js +197 -0
  927. package/dist/core/__tests__/memory-index-render.test.d.ts +1 -0
  928. package/dist/core/__tests__/memory-index-render.test.js +60 -0
  929. package/dist/core/__tests__/memory-resolver-precedence.test.d.ts +1 -0
  930. package/dist/core/__tests__/memory-resolver-precedence.test.js +169 -0
  931. package/dist/core/__tests__/migration.test.d.ts +1 -0
  932. package/dist/core/__tests__/migration.test.js +717 -0
  933. package/dist/core/__tests__/model-pin-durability.test.d.ts +1 -0
  934. package/dist/core/__tests__/model-pin-durability.test.js +124 -0
  935. package/dist/core/__tests__/on-read-crouter-home-fence.test.d.ts +1 -0
  936. package/dist/core/__tests__/on-read-crouter-home-fence.test.js +67 -0
  937. package/dist/core/__tests__/on-read-dedup-resume.test.d.ts +1 -0
  938. package/dist/core/__tests__/on-read-dedup-resume.test.js +121 -0
  939. package/dist/core/__tests__/on-read-identity.test.d.ts +1 -0
  940. package/dist/core/__tests__/on-read-identity.test.js +68 -0
  941. package/dist/core/__tests__/parse-argv-stdin-secret.test.d.ts +1 -0
  942. package/dist/core/__tests__/parse-argv-stdin-secret.test.js +26 -0
  943. package/dist/core/__tests__/passive-subscription.test.d.ts +1 -0
  944. package/dist/core/__tests__/passive-subscription.test.js +212 -0
  945. package/dist/core/__tests__/pid-identity-match.test.d.ts +1 -0
  946. package/dist/core/__tests__/pid-identity-match.test.js +145 -0
  947. package/dist/core/__tests__/pid-zombie.test.d.ts +1 -0
  948. package/dist/core/__tests__/pid-zombie.test.js +42 -0
  949. package/dist/core/__tests__/preview-registry-sync.test.d.ts +1 -0
  950. package/dist/core/__tests__/preview-registry-sync.test.js +65 -0
  951. package/dist/core/__tests__/prune-to-limit.test.d.ts +1 -0
  952. package/dist/core/__tests__/prune-to-limit.test.js +126 -0
  953. package/dist/core/__tests__/prune.test.d.ts +1 -0
  954. package/dist/core/__tests__/prune.test.js +135 -0
  955. package/dist/core/__tests__/push-final-guard.test.d.ts +1 -0
  956. package/dist/core/__tests__/push-final-guard.test.js +184 -0
  957. package/dist/core/__tests__/refresh-stall-recycle.test.d.ts +1 -0
  958. package/dist/core/__tests__/refresh-stall-recycle.test.js +101 -0
  959. package/dist/core/__tests__/relaunch-root.test.d.ts +1 -0
  960. package/dist/core/__tests__/relaunch-root.test.js +114 -0
  961. package/dist/core/__tests__/remote-canvas-target.test.d.ts +1 -0
  962. package/dist/core/__tests__/remote-canvas-target.test.js +87 -0
  963. package/dist/core/__tests__/reset.test.d.ts +1 -0
  964. package/dist/core/__tests__/reset.test.js +69 -0
  965. package/dist/core/__tests__/respawn-throttle.test.d.ts +1 -0
  966. package/dist/core/__tests__/respawn-throttle.test.js +154 -0
  967. package/dist/core/__tests__/review-model-floor.test.d.ts +1 -0
  968. package/dist/core/__tests__/review-model-floor.test.js +104 -0
  969. package/dist/core/__tests__/revive-all.test.d.ts +1 -0
  970. package/dist/core/__tests__/revive-all.test.js +147 -0
  971. package/dist/core/__tests__/revive.test.d.ts +1 -0
  972. package/dist/core/__tests__/revive.test.js +575 -0
  973. package/dist/core/__tests__/scope-crouter-home-fence.test.d.ts +1 -0
  974. package/dist/core/__tests__/scope-crouter-home-fence.test.js +55 -0
  975. package/dist/core/__tests__/secrets.test.d.ts +1 -0
  976. package/dist/core/__tests__/secrets.test.js +55 -0
  977. package/dist/core/__tests__/session-branch.test.d.ts +1 -0
  978. package/dist/core/__tests__/session-branch.test.js +61 -0
  979. package/dist/core/__tests__/session-cycles.test.d.ts +1 -0
  980. package/dist/core/__tests__/session-cycles.test.js +77 -0
  981. package/dist/core/__tests__/session-model.test.d.ts +1 -0
  982. package/dist/core/__tests__/session-model.test.js +129 -0
  983. package/dist/core/__tests__/spawn-no-kickoff.test.d.ts +1 -0
  984. package/dist/core/__tests__/spawn-no-kickoff.test.js +119 -0
  985. package/dist/core/__tests__/spawn-root.test.d.ts +1 -0
  986. package/dist/core/__tests__/spawn-root.test.js +320 -0
  987. package/dist/core/__tests__/spawn-worktree-id-conflict.test.d.ts +1 -0
  988. package/dist/core/__tests__/spawn-worktree-id-conflict.test.js +59 -0
  989. package/dist/core/__tests__/stdin-body-no-discard.test.d.ts +1 -0
  990. package/dist/core/__tests__/stdin-body-no-discard.test.js +33 -0
  991. package/dist/core/__tests__/stdout-errors.test.d.ts +1 -0
  992. package/dist/core/__tests__/stdout-errors.test.js +16 -0
  993. package/dist/core/__tests__/steer-note.test.d.ts +1 -0
  994. package/dist/core/__tests__/steer-note.test.js +41 -0
  995. package/dist/core/__tests__/stop-guard.test.d.ts +1 -0
  996. package/dist/core/__tests__/stop-guard.test.js +157 -0
  997. package/dist/core/__tests__/stream-watchdog.test.d.ts +1 -0
  998. package/dist/core/__tests__/stream-watchdog.test.js +154 -0
  999. package/dist/core/__tests__/subcommand-tier.test.d.ts +1 -0
  1000. package/dist/core/__tests__/subcommand-tier.test.js +99 -0
  1001. package/dist/core/__tests__/subscription-delivery.test.d.ts +1 -0
  1002. package/dist/core/__tests__/subscription-delivery.test.js +211 -0
  1003. package/dist/core/__tests__/subscription-pool-credential-guard.test.d.ts +1 -0
  1004. package/dist/core/__tests__/subscription-pool-credential-guard.test.js +94 -0
  1005. package/dist/core/__tests__/test-runner-cheap-sharding.test.d.ts +1 -0
  1006. package/dist/core/__tests__/test-runner-cheap-sharding.test.js +97 -0
  1007. package/dist/core/__tests__/tmux-surface.test.d.ts +1 -0
  1008. package/dist/core/__tests__/tmux-surface.test.js +470 -0
  1009. package/dist/core/__tests__/wake-bearings.test.d.ts +1 -0
  1010. package/dist/core/__tests__/wake-bearings.test.js +102 -0
  1011. package/dist/core/__tests__/worktree.test.d.ts +1 -0
  1012. package/dist/core/__tests__/worktree.test.js +876 -0
  1013. package/dist/core/__tests__/yield-ensures-daemon.test.d.ts +1 -0
  1014. package/dist/core/__tests__/yield-ensures-daemon.test.js +54 -0
  1015. package/dist/core/auth-file.d.ts +40 -0
  1016. package/dist/core/auth-file.js +177 -0
  1017. package/dist/core/auth-interaction.d.ts +19 -0
  1018. package/dist/core/auth-interaction.js +94 -0
  1019. package/dist/core/auto-update.d.ts +1 -0
  1020. package/dist/core/auto-update.js +93 -0
  1021. package/dist/core/bash-jobs.d.ts +55 -0
  1022. package/dist/core/bash-jobs.js +150 -0
  1023. package/dist/core/bootstrap.d.ts +5 -0
  1024. package/dist/core/bootstrap.js +98 -0
  1025. package/dist/core/broker-client/__tests__/transport-relay.test.d.ts +1 -0
  1026. package/dist/core/broker-client/__tests__/transport-relay.test.js +213 -0
  1027. package/dist/core/broker-client/client.d.ts +97 -0
  1028. package/dist/core/broker-client/client.js +189 -0
  1029. package/dist/core/broker-client/index.d.ts +1 -0
  1030. package/dist/core/broker-client/index.js +13 -0
  1031. package/dist/core/broker-client/transport-relay.d.ts +51 -0
  1032. package/dist/core/broker-client/transport-relay.js +169 -0
  1033. package/dist/core/broker-client/transport-socket.d.ts +29 -0
  1034. package/dist/core/broker-client/transport-socket.js +164 -0
  1035. package/dist/core/broker-client/transport.d.ts +32 -0
  1036. package/dist/core/broker-client/transport.js +26 -0
  1037. package/dist/core/canvas/__tests__/attention.test.d.ts +1 -0
  1038. package/dist/core/canvas/__tests__/attention.test.js +97 -0
  1039. package/dist/core/canvas/__tests__/human-work-outbox.test.d.ts +1 -0
  1040. package/dist/core/canvas/__tests__/human-work-outbox.test.js +123 -0
  1041. package/dist/core/canvas/__tests__/remote-canvas-source.test.d.ts +1 -0
  1042. package/dist/core/canvas/__tests__/remote-canvas-source.test.js +372 -0
  1043. package/dist/core/canvas/__tests__/remote-event-stream.test.d.ts +1 -0
  1044. package/dist/core/canvas/__tests__/remote-event-stream.test.js +146 -0
  1045. package/dist/core/canvas/__tests__/render-remote.test.d.ts +1 -0
  1046. package/dist/core/canvas/__tests__/render-remote.test.js +241 -0
  1047. package/dist/core/canvas/__tests__/source-resolve.test.d.ts +1 -0
  1048. package/dist/core/canvas/__tests__/source-resolve.test.js +77 -0
  1049. package/dist/core/canvas/attention.d.ts +19 -0
  1050. package/dist/core/canvas/attention.js +87 -0
  1051. package/dist/core/canvas/boot-id.d.ts +6 -0
  1052. package/dist/core/canvas/boot-id.js +26 -0
  1053. package/dist/core/canvas/boot.d.ts +93 -0
  1054. package/dist/core/canvas/boot.js +190 -0
  1055. package/dist/core/canvas/browse/__tests__/model.test.d.ts +1 -0
  1056. package/dist/core/canvas/browse/__tests__/model.test.js +290 -0
  1057. package/dist/core/canvas/browse/__tests__/rebuild-coalescer.test.d.ts +1 -0
  1058. package/dist/core/canvas/browse/__tests__/rebuild-coalescer.test.js +134 -0
  1059. package/dist/core/canvas/browse/__tests__/render.test.d.ts +1 -0
  1060. package/dist/core/canvas/browse/__tests__/render.test.js +111 -0
  1061. package/dist/core/canvas/browse/app.d.ts +41 -0
  1062. package/dist/core/canvas/browse/app.js +916 -0
  1063. package/dist/core/canvas/browse/model.d.ts +169 -0
  1064. package/dist/core/canvas/browse/model.js +485 -0
  1065. package/dist/core/canvas/browse/pins.d.ts +4 -0
  1066. package/dist/core/canvas/browse/pins.js +29 -0
  1067. package/dist/core/canvas/browse/rebuild-coalescer.d.ts +7 -0
  1068. package/dist/core/canvas/browse/rebuild-coalescer.js +61 -0
  1069. package/dist/core/canvas/browse/render.d.ts +59 -0
  1070. package/dist/core/canvas/browse/render.js +550 -0
  1071. package/dist/core/canvas/canvas.d.ts +358 -0
  1072. package/dist/core/canvas/canvas.js +897 -0
  1073. package/dist/core/canvas/crons.d.ts +222 -0
  1074. package/dist/core/canvas/crons.js +367 -0
  1075. package/dist/core/canvas/daemon-owner.d.ts +67 -0
  1076. package/dist/core/canvas/daemon-owner.js +141 -0
  1077. package/dist/core/canvas/db.d.ts +30 -0
  1078. package/dist/core/canvas/db.js +1167 -0
  1079. package/dist/core/canvas/extensions.d.ts +5 -0
  1080. package/dist/core/canvas/extensions.js +39 -0
  1081. package/dist/core/canvas/focuses.d.ts +22 -0
  1082. package/dist/core/canvas/focuses.js +81 -0
  1083. package/dist/core/canvas/history.d.ts +91 -0
  1084. package/dist/core/canvas/history.js +490 -0
  1085. package/dist/core/canvas/human-work-outbox.d.ts +73 -0
  1086. package/dist/core/canvas/human-work-outbox.js +261 -0
  1087. package/dist/core/canvas/index.d.ts +11 -0
  1088. package/dist/core/canvas/index.js +13 -0
  1089. package/dist/core/canvas/install-id.d.ts +9 -0
  1090. package/dist/core/canvas/install-id.js +84 -0
  1091. package/dist/core/canvas/labels.d.ts +28 -0
  1092. package/dist/core/canvas/labels.js +38 -0
  1093. package/dist/core/canvas/meta-file.d.ts +20 -0
  1094. package/dist/core/canvas/meta-file.js +98 -0
  1095. package/dist/core/canvas/nav-model.d.ts +55 -0
  1096. package/dist/core/canvas/nav-model.js +284 -0
  1097. package/dist/core/canvas/nav-render.d.ts +124 -0
  1098. package/dist/core/canvas/nav-render.js +266 -0
  1099. package/dist/core/canvas/paths.d.ts +81 -0
  1100. package/dist/core/canvas/paths.js +164 -0
  1101. package/dist/core/canvas/pid.d.ts +335 -0
  1102. package/dist/core/canvas/pid.js +636 -0
  1103. package/dist/core/canvas/remote-canvas-source.d.ts +62 -0
  1104. package/dist/core/canvas/remote-canvas-source.js +231 -0
  1105. package/dist/core/canvas/remote-event-stream.d.ts +24 -0
  1106. package/dist/core/canvas/remote-event-stream.js +94 -0
  1107. package/dist/core/canvas/render-source.d.ts +132 -0
  1108. package/dist/core/canvas/render-source.js +457 -0
  1109. package/dist/core/canvas/render.d.ts +47 -0
  1110. package/dist/core/canvas/render.js +237 -0
  1111. package/dist/core/canvas/source.d.ts +35 -0
  1112. package/dist/core/canvas/source.js +44 -0
  1113. package/dist/core/canvas/status-glyph.d.ts +42 -0
  1114. package/dist/core/canvas/status-glyph.js +116 -0
  1115. package/dist/core/canvas/telemetry.d.ts +14 -0
  1116. package/dist/core/canvas/telemetry.js +35 -0
  1117. package/dist/core/canvas/types.d.ts +302 -0
  1118. package/dist/core/canvas/types.js +8 -0
  1119. package/dist/core/clipboard-text.d.ts +3 -0
  1120. package/dist/core/clipboard-text.js +39 -0
  1121. package/dist/core/command-manifests/registry.d.ts +43 -0
  1122. package/dist/core/command-manifests/registry.js +103 -0
  1123. package/dist/core/command-manifests/schema.d.ts +87 -0
  1124. package/dist/core/command-manifests/schema.js +849 -0
  1125. package/dist/core/command-plugins/adapter.d.ts +22 -0
  1126. package/dist/core/command-plugins/adapter.js +160 -0
  1127. package/dist/core/command-plugins/compose.d.ts +11 -0
  1128. package/dist/core/command-plugins/compose.js +88 -0
  1129. package/dist/core/command-plugins/discovery.d.ts +74 -0
  1130. package/dist/core/command-plugins/discovery.js +291 -0
  1131. package/dist/core/command.d.ts +175 -0
  1132. package/dist/core/command.js +636 -0
  1133. package/dist/core/config.d.ts +125 -0
  1134. package/dist/core/config.js +620 -0
  1135. package/dist/core/configured-clis/cache.d.ts +16 -0
  1136. package/dist/core/configured-clis/cache.js +57 -0
  1137. package/dist/core/configured-clis/compose.d.ts +14 -0
  1138. package/dist/core/configured-clis/compose.js +60 -0
  1139. package/dist/core/configured-clis/discovery.d.ts +47 -0
  1140. package/dist/core/configured-clis/discovery.js +173 -0
  1141. package/dist/core/configured-clis/fetch.d.ts +51 -0
  1142. package/dist/core/configured-clis/fetch.js +156 -0
  1143. package/dist/core/configured-clis/invoker.d.ts +36 -0
  1144. package/dist/core/configured-clis/invoker.js +547 -0
  1145. package/dist/core/configured-clis/manifest.d.ts +24 -0
  1146. package/dist/core/configured-clis/manifest.js +284 -0
  1147. package/dist/core/configured-clis/registration.d.ts +40 -0
  1148. package/dist/core/configured-clis/registration.js +201 -0
  1149. package/dist/core/errors.d.ts +27 -0
  1150. package/dist/core/errors.js +48 -0
  1151. package/dist/core/events/emit.d.ts +4 -0
  1152. package/dist/core/events/emit.js +17 -0
  1153. package/dist/core/events/envelope.d.ts +2 -0
  1154. package/dist/core/events/envelope.js +84 -0
  1155. package/dist/core/events/errors.d.ts +4 -0
  1156. package/dist/core/events/errors.js +69 -0
  1157. package/dist/core/events/index.d.ts +5 -0
  1158. package/dist/core/events/index.js +4 -0
  1159. package/dist/core/events/operation-id.d.ts +4 -0
  1160. package/dist/core/events/operation-id.js +24 -0
  1161. package/dist/core/events/read.d.ts +40 -0
  1162. package/dist/core/events/read.js +639 -0
  1163. package/dist/core/events/serialize.d.ts +4 -0
  1164. package/dist/core/events/serialize.js +199 -0
  1165. package/dist/core/events/source.d.ts +18 -0
  1166. package/dist/core/events/source.js +47 -0
  1167. package/dist/core/events/types.d.ts +68 -0
  1168. package/dist/core/events/types.js +11 -0
  1169. package/dist/core/fault-classifier.d.ts +43 -0
  1170. package/dist/core/fault-classifier.js +143 -0
  1171. package/dist/core/feed/__tests__/coalesce-human.test.d.ts +1 -0
  1172. package/dist/core/feed/__tests__/coalesce-human.test.js +36 -0
  1173. package/dist/core/feed/__tests__/empty-node-guard.test.d.ts +1 -0
  1174. package/dist/core/feed/__tests__/empty-node-guard.test.js +45 -0
  1175. package/dist/core/feed/__tests__/inbox-migrate.test.d.ts +1 -0
  1176. package/dist/core/feed/__tests__/inbox-migrate.test.js +53 -0
  1177. package/dist/core/feed/feed.d.ts +31 -0
  1178. package/dist/core/feed/feed.js +159 -0
  1179. package/dist/core/feed/inbox-migrate.d.ts +11 -0
  1180. package/dist/core/feed/inbox-migrate.js +135 -0
  1181. package/dist/core/feed/inbox.d.ts +84 -0
  1182. package/dist/core/feed/inbox.js +407 -0
  1183. package/dist/core/feed/passive.d.ts +14 -0
  1184. package/dist/core/feed/passive.js +114 -0
  1185. package/dist/core/frontmatter.d.ts +8 -0
  1186. package/dist/core/frontmatter.js +35 -0
  1187. package/dist/core/fs-utils.d.ts +21 -0
  1188. package/dist/core/fs-utils.js +149 -0
  1189. package/dist/core/git.d.ts +22 -0
  1190. package/dist/core/git.js +72 -0
  1191. package/dist/core/help.d.ts +223 -0
  1192. package/dist/core/help.js +275 -0
  1193. package/dist/core/host-exports/__tests__/export-prunes-boot-skill.test.d.ts +1 -0
  1194. package/dist/core/host-exports/__tests__/export-prunes-boot-skill.test.js +84 -0
  1195. package/dist/core/host-exports/builtins.d.ts +50 -0
  1196. package/dist/core/host-exports/builtins.js +68 -0
  1197. package/dist/core/host-exports/export.d.ts +32 -0
  1198. package/dist/core/host-exports/export.js +128 -0
  1199. package/dist/core/inspector/core.d.ts +23 -0
  1200. package/dist/core/inspector/core.js +148 -0
  1201. package/dist/core/inspector/model.d.ts +54 -0
  1202. package/dist/core/inspector/model.js +182 -0
  1203. package/dist/core/inspector/text.d.ts +5 -0
  1204. package/dist/core/inspector/text.js +32 -0
  1205. package/dist/core/inspector/tui.d.ts +3 -0
  1206. package/dist/core/inspector/tui.js +220 -0
  1207. package/dist/core/io.d.ts +73 -0
  1208. package/dist/core/io.js +254 -0
  1209. package/dist/core/keybindings/__tests__/bespoke-consumers.test.d.ts +1 -0
  1210. package/dist/core/keybindings/__tests__/bespoke-consumers.test.js +40 -0
  1211. package/dist/core/keybindings/__tests__/inbox-affordance.test.d.ts +1 -0
  1212. package/dist/core/keybindings/__tests__/inbox-affordance.test.js +38 -0
  1213. package/dist/core/keybindings/__tests__/resolve.test.d.ts +1 -0
  1214. package/dist/core/keybindings/__tests__/resolve.test.js +233 -0
  1215. package/dist/core/keybindings/background-bash.d.ts +13 -0
  1216. package/dist/core/keybindings/background-bash.js +34 -0
  1217. package/dist/core/keybindings/catalog.d.ts +15 -0
  1218. package/dist/core/keybindings/catalog.js +285 -0
  1219. package/dist/core/keybindings/inbox.d.ts +21 -0
  1220. package/dist/core/keybindings/inbox.js +35 -0
  1221. package/dist/core/keybindings/index.d.ts +7 -0
  1222. package/dist/core/keybindings/index.js +6 -0
  1223. package/dist/core/keybindings/match.d.ts +29 -0
  1224. package/dist/core/keybindings/match.js +105 -0
  1225. package/dist/core/keybindings/persistence.d.ts +14 -0
  1226. package/dist/core/keybindings/persistence.js +31 -0
  1227. package/dist/core/keybindings/resolve.d.ts +22 -0
  1228. package/dist/core/keybindings/resolve.js +333 -0
  1229. package/dist/core/keybindings/types.d.ts +37 -0
  1230. package/dist/core/keybindings/types.js +1 -0
  1231. package/dist/core/locked-file.d.ts +24 -0
  1232. package/dist/core/locked-file.js +91 -0
  1233. package/dist/core/log.d.ts +3 -0
  1234. package/dist/core/log.js +27 -0
  1235. package/dist/core/manifest.d.ts +5 -0
  1236. package/dist/core/manifest.js +15 -0
  1237. package/dist/core/memory/inline-ref-grammar.d.ts +53 -0
  1238. package/dist/core/memory/inline-ref-grammar.js +99 -0
  1239. package/dist/core/memory/inline-ref-guidance.d.ts +8 -0
  1240. package/dist/core/memory/inline-ref-guidance.js +35 -0
  1241. package/dist/core/memory/inline-ref-inventory.d.ts +13 -0
  1242. package/dist/core/memory/inline-ref-inventory.js +79 -0
  1243. package/dist/core/memory-resolver.d.ts +106 -0
  1244. package/dist/core/memory-resolver.js +422 -0
  1245. package/dist/core/output.d.ts +35 -0
  1246. package/dist/core/output.js +99 -0
  1247. package/dist/core/pagination.d.ts +33 -0
  1248. package/dist/core/pagination.js +89 -0
  1249. package/dist/core/pool-credential-store.d.ts +34 -0
  1250. package/dist/core/pool-credential-store.js +116 -0
  1251. package/dist/core/predicate.d.ts +65 -0
  1252. package/dist/core/predicate.js +191 -0
  1253. package/dist/core/preview-registry.d.ts +25 -0
  1254. package/dist/core/preview-registry.js +2841 -0
  1255. package/dist/core/profiles/__tests__/env-store.test.d.ts +1 -0
  1256. package/dist/core/profiles/__tests__/env-store.test.js +97 -0
  1257. package/dist/core/profiles/__tests__/root-profile.test.d.ts +1 -0
  1258. package/dist/core/profiles/__tests__/root-profile.test.js +69 -0
  1259. package/dist/core/profiles/default-binding.d.ts +6 -0
  1260. package/dist/core/profiles/default-binding.js +46 -0
  1261. package/dist/core/profiles/env-store.d.ts +22 -0
  1262. package/dist/core/profiles/env-store.js +157 -0
  1263. package/dist/core/profiles/manifest.d.ts +35 -0
  1264. package/dist/core/profiles/manifest.js +371 -0
  1265. package/dist/core/profiles/select.d.ts +42 -0
  1266. package/dist/core/profiles/select.js +673 -0
  1267. package/dist/core/profiles/state-block.d.ts +4 -0
  1268. package/dist/core/profiles/state-block.js +16 -0
  1269. package/dist/core/provider-management.d.ts +12 -0
  1270. package/dist/core/provider-management.js +22 -0
  1271. package/dist/core/render.d.ts +12 -0
  1272. package/dist/core/render.js +127 -0
  1273. package/dist/core/resolver.d.ts +14 -0
  1274. package/dist/core/resolver.js +136 -0
  1275. package/dist/core/runtime/__tests__/command-expansion.test.d.ts +1 -0
  1276. package/dist/core/runtime/__tests__/command-expansion.test.js +25 -0
  1277. package/dist/core/runtime/__tests__/engine-route.test.d.ts +1 -0
  1278. package/dist/core/runtime/__tests__/engine-route.test.js +36 -0
  1279. package/dist/core/runtime/__tests__/install-id-cache-scoping.test.d.ts +1 -0
  1280. package/dist/core/runtime/__tests__/install-id-cache-scoping.test.js +41 -0
  1281. package/dist/core/runtime/__tests__/node-env.test.d.ts +1 -0
  1282. package/dist/core/runtime/__tests__/node-env.test.js +73 -0
  1283. package/dist/core/runtime/__tests__/spawn-at-given-id.test.d.ts +1 -0
  1284. package/dist/core/runtime/__tests__/spawn-at-given-id.test.js +34 -0
  1285. package/dist/core/runtime/__tests__/spawn-env.test.d.ts +1 -0
  1286. package/dist/core/runtime/__tests__/spawn-env.test.js +268 -0
  1287. package/dist/core/runtime/auth-reload.d.ts +7 -0
  1288. package/dist/core/runtime/auth-reload.js +67 -0
  1289. package/dist/core/runtime/banner.d.ts +13 -0
  1290. package/dist/core/runtime/banner.js +51 -0
  1291. package/dist/core/runtime/bearings.d.ts +99 -0
  1292. package/dist/core/runtime/bearings.js +429 -0
  1293. package/dist/core/runtime/boot-root.d.ts +24 -0
  1294. package/dist/core/runtime/boot-root.js +102 -0
  1295. package/dist/core/runtime/branded-host.d.ts +25 -0
  1296. package/dist/core/runtime/branded-host.js +342 -0
  1297. package/dist/core/runtime/broker-cli.d.ts +1 -0
  1298. package/dist/core/runtime/broker-cli.js +81 -0
  1299. package/dist/core/runtime/broker-protocol.d.ts +642 -0
  1300. package/dist/core/runtime/broker-protocol.js +146 -0
  1301. package/dist/core/runtime/broker-request.d.ts +37 -0
  1302. package/dist/core/runtime/broker-request.js +85 -0
  1303. package/dist/core/runtime/broker-sdk.d.ts +36 -0
  1304. package/dist/core/runtime/broker-sdk.js +128 -0
  1305. package/dist/core/runtime/broker.d.ts +245 -0
  1306. package/dist/core/runtime/broker.js +3711 -0
  1307. package/dist/core/runtime/busy.d.ts +32 -0
  1308. package/dist/core/runtime/busy.js +107 -0
  1309. package/dist/core/runtime/canvas-extensions.d.ts +20 -0
  1310. package/dist/core/runtime/canvas-extensions.js +70 -0
  1311. package/dist/core/runtime/close.d.ts +34 -0
  1312. package/dist/core/runtime/close.js +223 -0
  1313. package/dist/core/runtime/command-expansion.d.ts +19 -0
  1314. package/dist/core/runtime/command-expansion.js +104 -0
  1315. package/dist/core/runtime/connectivity.d.ts +12 -0
  1316. package/dist/core/runtime/connectivity.js +73 -0
  1317. package/dist/core/runtime/deliver-live.d.ts +26 -0
  1318. package/dist/core/runtime/deliver-live.js +63 -0
  1319. package/dist/core/runtime/fault-recovery-nudge.d.ts +3 -0
  1320. package/dist/core/runtime/fault-recovery-nudge.js +3 -0
  1321. package/dist/core/runtime/fault-recovery.d.ts +23 -0
  1322. package/dist/core/runtime/fault-recovery.js +83 -0
  1323. package/dist/core/runtime/fault.d.ts +64 -0
  1324. package/dist/core/runtime/fault.js +260 -0
  1325. package/dist/core/runtime/fleet.d.ts +57 -0
  1326. package/dist/core/runtime/fleet.js +35 -0
  1327. package/dist/core/runtime/front-door.d.ts +16 -0
  1328. package/dist/core/runtime/front-door.js +186 -0
  1329. package/dist/core/runtime/host.d.ts +43 -0
  1330. package/dist/core/runtime/host.js +432 -0
  1331. package/dist/core/runtime/interactive-deliver.d.ts +15 -0
  1332. package/dist/core/runtime/interactive-deliver.js +41 -0
  1333. package/dist/core/runtime/kickoff.d.ts +53 -0
  1334. package/dist/core/runtime/kickoff.js +289 -0
  1335. package/dist/core/runtime/launch-prompt.d.ts +8 -0
  1336. package/dist/core/runtime/launch-prompt.js +13 -0
  1337. package/dist/core/runtime/launch.d.ts +191 -0
  1338. package/dist/core/runtime/launch.js +442 -0
  1339. package/dist/core/runtime/lifecycle.d.ts +12 -0
  1340. package/dist/core/runtime/lifecycle.js +121 -0
  1341. package/dist/core/runtime/managed-provider-cooling.d.ts +38 -0
  1342. package/dist/core/runtime/managed-provider-cooling.js +81 -0
  1343. package/dist/core/runtime/memory.d.ts +3 -0
  1344. package/dist/core/runtime/memory.js +18 -0
  1345. package/dist/core/runtime/model-selection.d.ts +41 -0
  1346. package/dist/core/runtime/model-selection.js +118 -0
  1347. package/dist/core/runtime/model-swap.d.ts +27 -0
  1348. package/dist/core/runtime/model-swap.js +70 -0
  1349. package/dist/core/runtime/naming-persist.d.ts +11 -0
  1350. package/dist/core/runtime/naming-persist.js +41 -0
  1351. package/dist/core/runtime/naming.d.ts +26 -0
  1352. package/dist/core/runtime/naming.js +174 -0
  1353. package/dist/core/runtime/node-read.d.ts +36 -0
  1354. package/dist/core/runtime/node-read.js +255 -0
  1355. package/dist/core/runtime/nodes.d.ts +113 -0
  1356. package/dist/core/runtime/nodes.js +330 -0
  1357. package/dist/core/runtime/package-health.d.ts +49 -0
  1358. package/dist/core/runtime/package-health.js +217 -0
  1359. package/dist/core/runtime/persona.d.ts +25 -0
  1360. package/dist/core/runtime/persona.js +132 -0
  1361. package/dist/core/runtime/pi-cli.d.ts +8 -0
  1362. package/dist/core/runtime/pi-cli.js +65 -0
  1363. package/dist/core/runtime/pi-vendored.d.ts +26 -0
  1364. package/dist/core/runtime/pi-vendored.js +77 -0
  1365. package/dist/core/runtime/placement-tmux.d.ts +42 -0
  1366. package/dist/core/runtime/placement-tmux.js +151 -0
  1367. package/dist/core/runtime/placement.d.ts +97 -0
  1368. package/dist/core/runtime/placement.js +394 -0
  1369. package/dist/core/runtime/promote.d.ts +44 -0
  1370. package/dist/core/runtime/promote.js +176 -0
  1371. package/dist/core/runtime/recap.d.ts +8 -0
  1372. package/dist/core/runtime/recap.js +136 -0
  1373. package/dist/core/runtime/recycle.d.ts +14 -0
  1374. package/dist/core/runtime/recycle.js +158 -0
  1375. package/dist/core/runtime/reopen.d.ts +32 -0
  1376. package/dist/core/runtime/reopen.js +119 -0
  1377. package/dist/core/runtime/reset.d.ts +68 -0
  1378. package/dist/core/runtime/reset.js +198 -0
  1379. package/dist/core/runtime/resume-root.d.ts +6 -0
  1380. package/dist/core/runtime/resume-root.js +292 -0
  1381. package/dist/core/runtime/revive-all.d.ts +61 -0
  1382. package/dist/core/runtime/revive-all.js +106 -0
  1383. package/dist/core/runtime/revive.d.ts +58 -0
  1384. package/dist/core/runtime/revive.js +357 -0
  1385. package/dist/core/runtime/roadmap.d.ts +13 -0
  1386. package/dist/core/runtime/roadmap.js +45 -0
  1387. package/dist/core/runtime/session-branch.d.ts +13 -0
  1388. package/dist/core/runtime/session-branch.js +46 -0
  1389. package/dist/core/runtime/session-cycles.d.ts +30 -0
  1390. package/dist/core/runtime/session-cycles.js +77 -0
  1391. package/dist/core/runtime/session-list-cache.d.ts +53 -0
  1392. package/dist/core/runtime/session-list-cache.js +406 -0
  1393. package/dist/core/runtime/situational-context.d.ts +12 -0
  1394. package/dist/core/runtime/situational-context.js +48 -0
  1395. package/dist/core/runtime/situational-live.d.ts +9 -0
  1396. package/dist/core/runtime/situational-live.js +40 -0
  1397. package/dist/core/runtime/spawn-env.d.ts +31 -0
  1398. package/dist/core/runtime/spawn-env.js +179 -0
  1399. package/dist/core/runtime/spawn.d.ts +118 -0
  1400. package/dist/core/runtime/spawn.js +473 -0
  1401. package/dist/core/runtime/stop-guard.d.ts +18 -0
  1402. package/dist/core/runtime/stop-guard.js +77 -0
  1403. package/dist/core/runtime/stop-signals.d.ts +14 -0
  1404. package/dist/core/runtime/stop-signals.js +46 -0
  1405. package/dist/core/runtime/stream-watchdog.d.ts +55 -0
  1406. package/dist/core/runtime/stream-watchdog.js +140 -0
  1407. package/dist/core/runtime/structured-output.d.ts +12 -0
  1408. package/dist/core/runtime/structured-output.js +90 -0
  1409. package/dist/core/runtime/surface-bg.d.ts +11 -0
  1410. package/dist/core/runtime/surface-bg.js +68 -0
  1411. package/dist/core/runtime/tmux-chrome.d.ts +1 -0
  1412. package/dist/core/runtime/tmux-chrome.js +4 -0
  1413. package/dist/core/runtime/tmux.d.ts +247 -0
  1414. package/dist/core/runtime/tmux.js +858 -0
  1415. package/dist/core/runtime/warm-pool.d.ts +51 -0
  1416. package/dist/core/runtime/warm-pool.js +373 -0
  1417. package/dist/core/scope.d.ts +47 -0
  1418. package/dist/core/scope.js +255 -0
  1419. package/dist/core/secrets.d.ts +25 -0
  1420. package/dist/core/secrets.js +55 -0
  1421. package/dist/core/self-update.d.ts +21 -0
  1422. package/dist/core/self-update.js +105 -0
  1423. package/dist/core/session-model/session-state.d.ts +51 -0
  1424. package/dist/core/session-model/session-state.js +170 -0
  1425. package/dist/core/spawn.d.ts +16 -0
  1426. package/dist/core/spawn.js +30 -0
  1427. package/dist/core/stdout-errors.d.ts +3 -0
  1428. package/dist/core/stdout-errors.js +14 -0
  1429. package/dist/core/subscription-state.d.ts +138 -0
  1430. package/dist/core/subscription-state.js +918 -0
  1431. package/dist/core/substrate/ceiling.d.ts +20 -0
  1432. package/dist/core/substrate/ceiling.js +69 -0
  1433. package/dist/core/substrate/gate.d.ts +13 -0
  1434. package/dist/core/substrate/gate.js +21 -0
  1435. package/dist/core/substrate/index.d.ts +11 -0
  1436. package/dist/core/substrate/index.js +25 -0
  1437. package/dist/core/substrate/injected-store.d.ts +10 -0
  1438. package/dist/core/substrate/injected-store.js +55 -0
  1439. package/dist/core/substrate/on-read.d.ts +14 -0
  1440. package/dist/core/substrate/on-read.js +404 -0
  1441. package/dist/core/substrate/render-node.d.ts +11 -0
  1442. package/dist/core/substrate/render-node.js +29 -0
  1443. package/dist/core/substrate/render.d.ts +21 -0
  1444. package/dist/core/substrate/render.js +547 -0
  1445. package/dist/core/substrate/schema.d.ts +125 -0
  1446. package/dist/core/substrate/schema.js +191 -0
  1447. package/dist/core/substrate/session-cache.d.ts +25 -0
  1448. package/dist/core/substrate/session-cache.js +101 -0
  1449. package/dist/core/substrate/subject-fields.d.ts +45 -0
  1450. package/dist/core/substrate/subject-fields.js +38 -0
  1451. package/dist/core/substrate/subject.d.ts +12 -0
  1452. package/dist/core/substrate/subject.js +47 -0
  1453. package/dist/core/timing.d.ts +11 -0
  1454. package/dist/core/timing.js +94 -0
  1455. package/dist/core/tui/__tests__/host-keybindings.test.d.ts +1 -0
  1456. package/dist/core/tui/__tests__/host-keybindings.test.js +113 -0
  1457. package/dist/core/tui/draw.d.ts +100 -0
  1458. package/dist/core/tui/draw.js +359 -0
  1459. package/dist/core/tui/host.d.ts +66 -0
  1460. package/dist/core/tui/host.js +689 -0
  1461. package/dist/core/tui/terminal.d.ts +23 -0
  1462. package/dist/core/tui/terminal.js +101 -0
  1463. package/dist/core/view/__tests__/transport-cache.test.d.ts +1 -0
  1464. package/dist/core/view/__tests__/transport-cache.test.js +62 -0
  1465. package/dist/core/view/__tests__/transport-remote.test.d.ts +1 -0
  1466. package/dist/core/view/__tests__/transport-remote.test.js +98 -0
  1467. package/dist/core/view/bridge.d.ts +10 -0
  1468. package/dist/core/view/bridge.js +31 -0
  1469. package/dist/core/view/chrome.d.ts +9 -0
  1470. package/dist/core/view/chrome.js +22 -0
  1471. package/dist/core/view/contract.d.ts +205 -0
  1472. package/dist/core/view/contract.js +23 -0
  1473. package/dist/core/view/loader.d.ts +31 -0
  1474. package/dist/core/view/loader.js +188 -0
  1475. package/dist/core/view/remote-canvas-target.d.ts +4 -0
  1476. package/dist/core/view/remote-canvas-target.js +30 -0
  1477. package/dist/core/view/stream-local.d.ts +3 -0
  1478. package/dist/core/view/stream-local.js +231 -0
  1479. package/dist/core/view/transport-cache.d.ts +8 -0
  1480. package/dist/core/view/transport-cache.js +38 -0
  1481. package/dist/core/view/transport-local.d.ts +7 -0
  1482. package/dist/core/view/transport-local.js +83 -0
  1483. package/dist/core/view/transport-remote.d.ts +2 -0
  1484. package/dist/core/view/transport-remote.js +58 -0
  1485. package/dist/core/view/transport.d.ts +10 -0
  1486. package/dist/core/view/transport.js +15 -0
  1487. package/dist/core/wake.d.ts +86 -0
  1488. package/dist/core/wake.js +307 -0
  1489. package/dist/core/worktree.d.ts +111 -0
  1490. package/dist/core/worktree.js +732 -0
  1491. package/dist/daemon/__tests__/broker-ownership.test.d.ts +1 -0
  1492. package/dist/daemon/__tests__/broker-ownership.test.js +25 -0
  1493. package/dist/daemon/__tests__/daemon-ownership.test.d.ts +1 -0
  1494. package/dist/daemon/__tests__/daemon-ownership.test.js +27 -0
  1495. package/dist/daemon/api/__tests__/error-seam.test.d.ts +1 -0
  1496. package/dist/daemon/api/__tests__/error-seam.test.js +56 -0
  1497. package/dist/daemon/api/__tests__/full/api-server.test.d.ts +1 -0
  1498. package/dist/daemon/api/__tests__/full/api-server.test.js +369 -0
  1499. package/dist/daemon/api/__tests__/full/b10-attach-modelauth.test.d.ts +1 -0
  1500. package/dist/daemon/api/__tests__/full/b10-attach-modelauth.test.js +220 -0
  1501. package/dist/daemon/api/__tests__/full/push-parity.test.d.ts +1 -0
  1502. package/dist/daemon/api/__tests__/full/push-parity.test.js +199 -0
  1503. package/dist/daemon/api/__tests__/full/worktree-parity.test.d.ts +1 -0
  1504. package/dist/daemon/api/__tests__/full/worktree-parity.test.js +160 -0
  1505. package/dist/daemon/api/__tests__/inbox.test.d.ts +1 -0
  1506. package/dist/daemon/api/__tests__/inbox.test.js +578 -0
  1507. package/dist/daemon/api/bridge.d.ts +21 -0
  1508. package/dist/daemon/api/bridge.js +351 -0
  1509. package/dist/daemon/api/handlers/attach.d.ts +2 -0
  1510. package/dist/daemon/api/handlers/attach.js +56 -0
  1511. package/dist/daemon/api/handlers/canvas.d.ts +2 -0
  1512. package/dist/daemon/api/handlers/canvas.js +463 -0
  1513. package/dist/daemon/api/handlers/crons.d.ts +2 -0
  1514. package/dist/daemon/api/handlers/crons.js +354 -0
  1515. package/dist/daemon/api/handlers/daemon.d.ts +2 -0
  1516. package/dist/daemon/api/handlers/daemon.js +35 -0
  1517. package/dist/daemon/api/handlers/files.d.ts +2 -0
  1518. package/dist/daemon/api/handlers/files.js +64 -0
  1519. package/dist/daemon/api/handlers/focus.d.ts +2 -0
  1520. package/dist/daemon/api/handlers/focus.js +92 -0
  1521. package/dist/daemon/api/handlers/health.d.ts +2 -0
  1522. package/dist/daemon/api/handlers/health.js +65 -0
  1523. package/dist/daemon/api/handlers/human.d.ts +12 -0
  1524. package/dist/daemon/api/handlers/human.js +741 -0
  1525. package/dist/daemon/api/handlers/inbox.d.ts +2 -0
  1526. package/dist/daemon/api/handlers/inbox.js +423 -0
  1527. package/dist/daemon/api/handlers/messages.d.ts +2 -0
  1528. package/dist/daemon/api/handlers/messages.js +290 -0
  1529. package/dist/daemon/api/handlers/modelauth.d.ts +2 -0
  1530. package/dist/daemon/api/handlers/modelauth.js +116 -0
  1531. package/dist/daemon/api/handlers/nodes.d.ts +2 -0
  1532. package/dist/daemon/api/handlers/nodes.js +540 -0
  1533. package/dist/daemon/api/handlers/profiles.d.ts +2 -0
  1534. package/dist/daemon/api/handlers/profiles.js +69 -0
  1535. package/dist/daemon/api/handlers/reports.d.ts +2 -0
  1536. package/dist/daemon/api/handlers/reports.js +144 -0
  1537. package/dist/daemon/api/handlers/subscriptions.d.ts +2 -0
  1538. package/dist/daemon/api/handlers/subscriptions.js +53 -0
  1539. package/dist/daemon/api/handlers/worktree.d.ts +2 -0
  1540. package/dist/daemon/api/handlers/worktree.js +40 -0
  1541. package/dist/daemon/api/map.d.ts +100 -0
  1542. package/dist/daemon/api/map.js +360 -0
  1543. package/dist/daemon/api/router.d.ts +56 -0
  1544. package/dist/daemon/api/router.js +190 -0
  1545. package/dist/daemon/api/server.d.ts +23 -0
  1546. package/dist/daemon/api/server.js +258 -0
  1547. package/dist/daemon/control.d.ts +21 -0
  1548. package/dist/daemon/control.js +22 -0
  1549. package/dist/daemon/cron-run.d.ts +64 -0
  1550. package/dist/daemon/cron-run.js +944 -0
  1551. package/dist/daemon/cron-sink.d.ts +26 -0
  1552. package/dist/daemon/cron-sink.js +43 -0
  1553. package/dist/daemon/crtrd-cli.d.ts +1 -0
  1554. package/dist/daemon/crtrd-cli.js +18 -0
  1555. package/dist/daemon/crtrd.d.ts +89 -0
  1556. package/dist/daemon/crtrd.js +1953 -0
  1557. package/dist/daemon/fleet.d.ts +81 -0
  1558. package/dist/daemon/fleet.js +405 -0
  1559. package/dist/daemon/manage.d.ts +103 -0
  1560. package/dist/daemon/manage.js +389 -0
  1561. package/dist/daemon/pidfile.d.ts +8 -0
  1562. package/dist/daemon/pidfile.js +37 -0
  1563. package/dist/daemon/supervise-cadence.d.ts +1 -0
  1564. package/dist/daemon/supervise-cadence.js +6 -0
  1565. package/dist/index.d.ts +23 -0
  1566. package/dist/index.js +44 -0
  1567. package/dist/pi-extensions/__tests__/canvas-bash-valve.test.d.ts +1 -0
  1568. package/dist/pi-extensions/__tests__/canvas-bash-valve.test.js +31 -0
  1569. package/dist/pi-extensions/__tests__/canvas-context-intro.test.d.ts +1 -0
  1570. package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +182 -0
  1571. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.d.ts +1 -0
  1572. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +467 -0
  1573. package/dist/pi-extensions/__tests__/canvas-stophook-context-nudge.test.d.ts +1 -0
  1574. package/dist/pi-extensions/__tests__/canvas-stophook-context-nudge.test.js +121 -0
  1575. package/dist/pi-extensions/canvas-bash-valve.d.ts +8 -0
  1576. package/dist/pi-extensions/canvas-bash-valve.js +297 -0
  1577. package/dist/pi-extensions/canvas-context-intro.d.ts +91 -0
  1578. package/dist/pi-extensions/canvas-context-intro.js +230 -0
  1579. package/dist/pi-extensions/canvas-doc-substrate.d.ts +44 -0
  1580. package/dist/pi-extensions/canvas-doc-substrate.js +151 -0
  1581. package/dist/pi-extensions/canvas-goal-capture.d.ts +21 -0
  1582. package/dist/pi-extensions/canvas-goal-capture.js +83 -0
  1583. package/dist/pi-extensions/canvas-inbox-watcher.d.ts +25 -0
  1584. package/dist/pi-extensions/canvas-inbox-watcher.js +942 -0
  1585. package/dist/pi-extensions/canvas-passive-context.d.ts +32 -0
  1586. package/dist/pi-extensions/canvas-passive-context.js +134 -0
  1587. package/dist/pi-extensions/canvas-recap.d.ts +41 -0
  1588. package/dist/pi-extensions/canvas-recap.js +263 -0
  1589. package/dist/pi-extensions/canvas-stophook.d.ts +53 -0
  1590. package/dist/pi-extensions/canvas-stophook.js +985 -0
  1591. package/dist/pi-extensions/canvas-structured-output.d.ts +9 -0
  1592. package/dist/pi-extensions/canvas-structured-output.js +155 -0
  1593. package/dist/prompts/review.d.ts +2 -0
  1594. package/dist/prompts/review.js +130 -0
  1595. package/dist/prompts/view.d.ts +7 -0
  1596. package/dist/prompts/view.js +172 -0
  1597. package/dist/suppress-experimental-warnings.d.ts +1 -0
  1598. package/dist/suppress-experimental-warnings.js +15 -0
  1599. package/dist/types.d.ts +297 -0
  1600. package/dist/types.js +153 -0
  1601. package/dist/web/ViewChrome.d.ts +7 -0
  1602. package/dist/web/ViewChrome.js +28 -0
  1603. package/dist/web/ViewPane.d.ts +39 -0
  1604. package/dist/web/ViewPane.js +48 -0
  1605. package/dist/web/index.d.ts +8 -0
  1606. package/dist/web/index.js +22 -0
  1607. package/dist/web/runtime.d.ts +39 -0
  1608. package/dist/web/runtime.js +232 -0
  1609. package/dist/web/states.d.ts +24 -0
  1610. package/dist/web/states.js +24 -0
  1611. package/dist/web/transport-http.d.ts +5 -0
  1612. package/dist/web/transport-http.js +34 -0
  1613. package/dist/web/transport-stream.d.ts +3 -0
  1614. package/dist/web/transport-stream.js +204 -0
  1615. package/dist/web-client/apple-touch-icon.png +0 -0
  1616. package/dist/web-client/assets/fragment-mono-latin-400-normal-BYwT3kSJ.woff +0 -0
  1617. package/dist/web-client/assets/fragment-mono-latin-400-normal-yxdJ5AmL.woff2 +0 -0
  1618. package/dist/web-client/assets/fragment-mono-latin-ext-400-normal-BbKYyvR9.woff2 +0 -0
  1619. package/dist/web-client/assets/fragment-mono-latin-ext-400-normal-CT4YFKeK.woff +0 -0
  1620. package/dist/web-client/assets/index-BpyZGBhI.js +79 -0
  1621. package/dist/web-client/assets/index-CpEl9LTS.css +2 -0
  1622. package/dist/web-client/assets/martian-mono-cyrillic-wght-normal-B84CD5C_.woff2 +0 -0
  1623. package/dist/web-client/assets/martian-mono-latin-ext-wght-normal-DlL6xMw5.woff2 +0 -0
  1624. package/dist/web-client/assets/martian-mono-latin-wght-normal-5W32yIyr.woff2 +0 -0
  1625. package/dist/web-client/assets/workbox-window.prod.es5-Bd17z0YL.js +2 -0
  1626. package/dist/web-client/icon-192.png +0 -0
  1627. package/dist/web-client/icon-512.png +0 -0
  1628. package/dist/web-client/icon-maskable-512.png +0 -0
  1629. package/dist/web-client/index.html +17 -0
  1630. package/dist/web-client/manifest.webmanifest +1 -0
  1631. package/dist/web-client/sw.js +1 -0
  1632. package/docs/compat/hearth-crtr-v1.md +187 -0
  1633. package/docs/compat/hearth-crtr-v2.md +185 -0
  1634. package/docs/compat/hearth-crtr-v3.md +184 -0
  1635. package/docs/compat/hearth-crtr-v4.md +188 -0
  1636. package/docs/compat/hearth-crtr-v5.md +175 -0
  1637. package/docs/public-api.md +77 -0
  1638. package/package.json +156 -0
  1639. package/runtime.lock.json +12919 -0
  1640. package/scripts/install-runtime.mjs +308 -0
  1641. package/scripts/patch-pi-tree-root-ordering.mjs +57 -0
  1642. package/scripts/postinstall.mjs +33 -0
  1643. package/scripts/runtime-lock.mjs +26 -0
@@ -0,0 +1,3711 @@
1
+ // broker.ts — the headless node broker (design §4, §5; plan T4).
2
+ //
3
+ // One broker process per node — the SOLE host. It hosts ONE pi engine IN-PROCESS
4
+ // via the SDK (createAgentSession), is the SOLE writer of the node's session
5
+ // `.jsonl`, listens on `nodeDir(id)/view.sock`, fans the single engine event
6
+ // stream out to N viewers, serializes the drive commands of EVERY writable
7
+ // viewer through one frame loop, and fans blocking extension dialogs out to all
8
+ // of them (first answer wins). The engine has no terminal of its own; a
9
+ // tmux pane (or web tab) is only a viewer of this socket. It
10
+ // runs one turn-cycle and, when the engine settles (the stophook calls
11
+ // ctx.shutdown(), or the engine goes idle), disposes the engine and exits 0 —
12
+ // the daemon then routes revival per the intent the bound stophook set.
13
+ //
14
+ // Engine resolution flows through broker-sdk.ts (`loadBrokerEngine`) so the T11
15
+ // `CRTR_BROKER_ENGINE` test seam can swap a fake engine in. The resource loader
16
+ // + model registry are real SDK construction helpers (NOT part of the swappable
17
+ // engine seam — the fake engine receives the real, extension-loaded loader so
18
+ // the real canvas-stophook fires session_start).
19
+ import { createServer } from 'node:net';
20
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
21
+ import { execFile } from 'node:child_process';
22
+ import { tmpdir } from 'node:os';
23
+ import { join } from 'node:path';
24
+ import { randomUUID } from 'node:crypto';
25
+ import { mark } from '../timing.js';
26
+ import { getAgentDir, initTheme, ModelRegistry, ModelRuntime, } from '@earendil-works/pi-coding-agent';
27
+ import { isContextOverflow } from '@earendil-works/pi-ai';
28
+ import { jobDir, nodeDir, sessionListCachePath, viewSocketPath } from '../canvas/paths.js';
29
+ import { bindBrokerEventSource } from '../events/source.js';
30
+ import { emitEvent } from '../events/emit.js';
31
+ import { operationIdContext } from '../events/operation-id.js';
32
+ import { findMissingPackagePaths } from './package-health.js';
33
+ import { SessionListCache } from './session-list-cache.js';
34
+ import { getNode, updateNode } from '../canvas/index.js';
35
+ import { buildRefInventory } from '../memory/inline-ref-inventory.js';
36
+ import { buildGuidance } from '../memory/inline-ref-guidance.js';
37
+ import { clearSessionCache } from '../substrate/session-cache.js';
38
+ import { FRONT_DOOR_ENV } from './front-door.js';
39
+ import { CANVAS_EXTENSIONS, configuredLadderProviders, equivalentOtherProviderModel, isProviderPinnedModelToken, nextLadderModel, normalizeModel, piInvocationToSdkConfig, } from './launch.js';
40
+ import { assertEngineVersion, loadBrokerEngine } from './broker-sdk.js';
41
+ import { resolveBundledPiPackageDir } from './pi-cli.js';
42
+ import { classify } from '../fault-classifier.js';
43
+ import { PoolCredentialStore } from '../pool-credential-store.js';
44
+ import { clearFault, isModelNotFoundError, isProviderUnconfiguredError, readFault, recordFault, recordFaultForCurrentNode } from './fault.js';
45
+ import { clearCleanAbort, markBusy, markCleanAbort } from './busy.js';
46
+ import { extractCoolingDeadline, MANAGED_PROVIDER_ROTATION_POLICY_UI, } from './managed-provider-cooling.js';
47
+ import { AUTH_FAULT_RECOVERY_BODY, CONNECTION_FAULT_RECOVERY_BODY, PROVIDER_FAULT_RECOVERY_BODY } from './fault-recovery-nudge.js';
48
+ import { markFatalOnExhaust, nextFaultRetry, policyFor } from './fault-recovery.js';
49
+ import { probeOnline as probeOnlineReal } from './connectivity.js';
50
+ import { CRTR_CYCLE_CUSTOM_TYPE, cycleAwareMessages } from './session-cycles.js';
51
+ import { BUILTIN_SLASH_COMMANDS, piSessionsRoot } from './pi-vendored.js';
52
+ import { StreamWatchdog, resolveStreamWatchdogMs, releaseKnownStreamWait, KNOWN_STREAM_WAIT_UI } from './stream-watchdog.js';
53
+ import { isDeterministicCommandExpansion } from './command-expansion.js';
54
+ import { clearPreferredModel, isManagedProvider, readRotationConfig, readSubscriptionPool, rememberPreferredModel, resolveProviderCandidates } from '../subscription-state.js';
55
+ import { encodeFrame, FrameDecoder, FrameOverflowError, BROKER_READ_CAPS, } from './broker-protocol.js';
56
+ const THINKING_LEVELS = new Set(['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']);
57
+ /**
58
+ * pi 0.82 made `ModelRuntime` the canonical model/auth surface and demoted
59
+ * `ModelRegistry` to the synchronous facade over it (`find`/`getAll`/
60
+ * `getAvailable`/`hasConfiguredAuth` off the runtime's snapshot). The broker's
61
+ * model policy is synchronous throughout, so every read goes through this
62
+ * facade; it is a thin wrapper over the LIVE runtime, so constructing one per
63
+ * call reads current state rather than a captured view.
64
+ */
65
+ function registryOf(services) {
66
+ return new ModelRegistry(services.modelRuntime);
67
+ }
68
+ /** The auth.json/models.json pair backing one agent dir's model runtime. */
69
+ function modelRuntimePaths(agentDir) {
70
+ return { authPath: join(agentDir, 'auth.json'), modelsPath: join(agentDir, 'models.json') };
71
+ }
72
+ function parseModelSpec(spec) {
73
+ const i = spec.lastIndexOf(':');
74
+ if (i <= 0)
75
+ return { modelSpec: spec };
76
+ const suffix = spec.slice(i + 1);
77
+ if (!THINKING_LEVELS.has(suffix))
78
+ return { modelSpec: spec };
79
+ return { modelSpec: spec.slice(0, i), thinkingLevel: suffix };
80
+ }
81
+ /**
82
+ * The pinned pi SDK never yields `session.model === undefined` for a model-less
83
+ * boot: Agent.createMutableAgentState() falls back to a hardcoded
84
+ * `{ provider:'unknown', id:'unknown', api:'unknown', … }` stub (DEFAULT_MODEL)
85
+ * whenever no model is supplied at construction, and `session.model` returns it
86
+ * verbatim. This mirrors the SDK's own (non-exported) interactive-mode.js
87
+ * `isUnknownModel()` so the broker recognizes that stub as "no model" wherever it
88
+ * guards the model-less launch path (kickoff deferral, reload_auth re-drive) and
89
+ * refuses to record/broadcast it as a real `unknown/unknown` model.
90
+ */
91
+ export function isUnknownModel(model) {
92
+ return (model !== null &&
93
+ model !== undefined &&
94
+ model.provider === 'unknown' &&
95
+ model.id === 'unknown' &&
96
+ model.api === 'unknown');
97
+ }
98
+ /**
99
+ * Route a writable client's `prompt`/`follow_up` frame against the LIVE session
100
+ * state. The client picks its frame type off a possibly-STALE `isStreaming`
101
+ * snapshot, so the broker is authoritative and the client's choice is a HINT:
102
+ *
103
+ * - m-B (streaming-safe prompt): a `prompt` arriving mid-stream needs
104
+ * PromptOptions.streamingBehavior ("Required if streaming"; prompt() throws
105
+ * without it). "steer" mirrors pi interactive-mode's own
106
+ * Enter-while-streaming submit (`prompt(text, { streamingBehavior: "steer" })`).
107
+ * - m-C (idle-safe follow_up): pi's followUp() only enqueues into a RUNNING
108
+ * agent loop — on an idle session nothing ever drains the queue and the text
109
+ * is silently lost. An idle `follow_up` therefore runs as a prompt now.
110
+ *
111
+ * driveEngine calls this synchronously with `session.isStreaming`, so the
112
+ * routed state cannot go stale between the check and the engine call.
113
+ * C1: images ride as a field on PromptOptions (pi: `prompt(text, { images })`).
114
+ */
115
+ export function resolveEngineRoute(frame, isStreaming) {
116
+ if (frame.type === 'follow_up' && isStreaming)
117
+ return { call: 'followUp' };
118
+ const options = {};
119
+ if (frame.images !== undefined)
120
+ options.images = frame.images;
121
+ if (isStreaming)
122
+ options.streamingBehavior = 'steer';
123
+ return { call: 'prompt', options };
124
+ }
125
+ /**
126
+ * True iff `text` would be consumed by pi's OWN leading-name preflight before
127
+ * `session.prompt/steer/followUp` ever sees it as prose — an extension command
128
+ * (`session.extensionRunner.getCommand`), a `/skill:name` skill, or a `/name`
129
+ * prompt template (mirrors `_tryExecuteExtensionCommand` / `_expandSkillCommand`
130
+ * / `expandPromptTemplate` in the pinned SDK verbatim, including the exact
131
+ * name-parsing substrings). Inline memory-reference guidance must NEVER fire
132
+ * for these — the leading token routes to the engine's own command surface,
133
+ * not prose referencing a memory doc. Each lookup is independently wrapped so
134
+ * an engine missing one accessor (the fake, or a degraded session) degrades
135
+ * that check to "not a match" rather than aborting the whole classification —
136
+ * matching `buildCommandList`'s existing "no live extensionRunner" tolerance.
137
+ */
138
+ export function isLeadingEngineCommand(text, session) {
139
+ if (text.startsWith('/')) {
140
+ const spaceIndex = text.indexOf(' ');
141
+ const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
142
+ try {
143
+ if (session.extensionRunner?.getCommand(commandName))
144
+ return true;
145
+ }
146
+ catch {
147
+ /* engine without a live extensionRunner (e.g. the fake) — not a match */
148
+ }
149
+ }
150
+ if (text.startsWith('/skill:')) {
151
+ const spaceIndex = text.indexOf(' ');
152
+ const skillName = spaceIndex === -1 ? text.slice(7) : text.slice(7, spaceIndex);
153
+ try {
154
+ if (session.resourceLoader?.getSkills().skills.some((s) => s.name === skillName))
155
+ return true;
156
+ }
157
+ catch {
158
+ /* engine without a live resourceLoader — not a match */
159
+ }
160
+ }
161
+ if (text.startsWith('/')) {
162
+ const match = text.match(/^\/([^\s]+)(?:\s+([\s\S]*))?$/);
163
+ const templateName = match?.[1];
164
+ if (templateName !== undefined) {
165
+ try {
166
+ if (session.promptTemplates?.some((t) => t.name === templateName))
167
+ return true;
168
+ }
169
+ catch {
170
+ /* engine without a live promptTemplates getter — not a match */
171
+ }
172
+ }
173
+ }
174
+ return false;
175
+ }
176
+ /**
177
+ * Enumerate every leading-token STRING this session's engine would
178
+ * independently consume as a leading command/skill/template dispatch BEFORE
179
+ * inline memory-reference guidance ever applies — the read-op sibling of
180
+ * `isLeadingEngineCommand`'s point-lookup, gathered as a full enumeration for
181
+ * viewer-side suppression instead of a per-submission membership check. Same
182
+ * three sources, same shape of the leading match `isLeadingEngineCommand`
183
+ * checks (bare extension-command invocation names and prompt-template names,
184
+ * `skill:<name>` for skills), so `/${token}` round-trips through
185
+ * `isLeadingEngineCommand(text, session)` for every emitted token (locked by
186
+ * a test). Kept STRICTLY separate from `buildCommandList` (command
187
+ * completion) and the ref inventory (`RefMeta` rows) — this is neither.
188
+ */
189
+ export function engineLeadingCommandTokens(session) {
190
+ const out = [];
191
+ try {
192
+ for (const c of session.extensionRunner?.getRegisteredCommands() ?? [])
193
+ out.push(c.invocationName);
194
+ }
195
+ catch {
196
+ /* engine without a live extensionRunner (e.g. the fake) — none */
197
+ }
198
+ try {
199
+ for (const s of session.resourceLoader?.getSkills().skills ?? [])
200
+ out.push(`skill:${s.name}`);
201
+ }
202
+ catch {
203
+ /* engine without a live resourceLoader — none */
204
+ }
205
+ try {
206
+ for (const t of session.promptTemplates ?? [])
207
+ out.push(t.name);
208
+ }
209
+ catch {
210
+ /* engine without a live promptTemplates getter — none */
211
+ }
212
+ return out;
213
+ }
214
+ /**
215
+ * Choose how the hidden `crtr-memory-refs` guidance custom message rides
216
+ * alongside a human frame's own engine call (design Flow B). Pure over the
217
+ * frame's routing outcome — never reads live session state itself, so a
218
+ * caller can decide the delivery mode from the exact same `route`
219
+ * `resolveEngineRoute` already computed for the human call.
220
+ *
221
+ * - `steer` frame -> 'steer' (always; no route call exists).
222
+ * - a TRUE mid-stream follow_up (`routeCall === 'followUp'`) -> 'followUp'.
223
+ * - every other `prompt`/`follow_up` routed as `routeCall === 'prompt'`:
224
+ * idle -> 'nextTurn' (the ONLY case guidance precedes the human call —
225
+ * pi's `prompt()` reads+clears its pending-nextTurn queue synchronously
226
+ * into the SAME turn as the new user message); streaming (m-B
227
+ * steer-while-streaming) -> 'steer', never 'nextTurn' (that would attach
228
+ * to a later, unrelated turn).
229
+ */
230
+ export function chooseGuidanceDeliveryMode(frameType, routeCall, isStreaming) {
231
+ if (frameType === 'steer')
232
+ return 'steer';
233
+ if (routeCall === 'followUp')
234
+ return 'followUp';
235
+ return isStreaming ? 'steer' : 'nextTurn';
236
+ }
237
+ /**
238
+ * Deliver a `nextTurn` guidance custom message ATOMICALLY around the idle-
239
+ * prompt call it's meant to ride alongside. Pi's `_pendingNextTurnMessages`
240
+ * queue (the only public seam for `deliverAs:'nextTurn'`) is drained ONLY deep
241
+ * inside `session.prompt()`, well after several await points — extension-
242
+ * command interception, the `input` extension transform, a streaming reroute,
243
+ * model/auth validation, pre-turn compaction — that can return or throw BEFORE
244
+ * the drain ever runs. `sendCustomMessage`'s nextTurn branch pushes
245
+ * synchronously and unconditionally, so any of those early exits strands the
246
+ * pushed guidance in the queue to attach itself to whichever LATER, unrelated
247
+ * prompt happens to reach the drain first.
248
+ *
249
+ * There is no public SDK seam to observe or cancel a pending nextTurn message
250
+ * (`_pendingNextTurnMessages` is TS-`private` only — a plain runtime field, not
251
+ * `#private`), so this reaches into that one field and removes EXACTLY the
252
+ * entry this call pushed, identified by object identity via a locally-
253
+ * generated marker riding the message's own `details` field (never surfaced to
254
+ * the model, and irrelevant if the message WAS consumed normally — `details`
255
+ * is just carried through like any other field once folded into a real turn).
256
+ * Runs UNCONDITIONALLY after the human call settles, success or failure/early-
257
+ * return alike — a "successful" `prompt()` promise can just as easily be an
258
+ * early return that skipped the drain (an intercepted extension command, an
259
+ * `input` transform, a streaming reroute) as a genuine consumed turn, so only
260
+ * checking `.catch` is not enough.
261
+ */
262
+ export async function promptWithNextTurnGuidance(session, guidance, runPrompt) {
263
+ // Must survive Pi's structured-clone step when the queued custom message is
264
+ // folded into the turn. A Symbol reaches that step in `details` and aborts
265
+ // the real prompt with "could not be cloned" before the provider runs.
266
+ const marker = `crtr-memory-refs-pending:${randomUUID()}`;
267
+ let queued = false;
268
+ try {
269
+ await session.sendCustomMessage({ customType: 'crtr-memory-refs', content: guidance, display: false, details: marker }, { deliverAs: 'nextTurn' });
270
+ queued = true;
271
+ }
272
+ catch (err) {
273
+ emitEvent({
274
+ level: 'warn',
275
+ event: 'broker.memory_refs.guidance_send_failed',
276
+ error: err,
277
+ });
278
+ }
279
+ try {
280
+ await runPrompt();
281
+ }
282
+ finally {
283
+ if (queued)
284
+ clearOwnPendingNextTurnGuidance(session, marker);
285
+ }
286
+ }
287
+ /** Splice out exactly the pending-nextTurn entry `marker` identifies (object-
288
+ * identity match on `details`), if it is still present — a no-op (and safe)
289
+ * if `session.prompt()` already drained it normally, or if the session/field
290
+ * shape doesn't match (defensive against an engine without the field, e.g. a
291
+ * future SDK bump or a structurally-different test double). */
292
+ function clearOwnPendingNextTurnGuidance(session, marker) {
293
+ const pending = session._pendingNextTurnMessages;
294
+ if (!Array.isArray(pending))
295
+ return;
296
+ const idx = pending.findIndex((m) => m?.details === marker);
297
+ if (idx !== -1)
298
+ pending.splice(idx, 1);
299
+ }
300
+ /** True iff a parsed JSONL branch-entry line is a hidden (`display:false`)
301
+ * custom message — the general display boundary crossed if left in an
302
+ * exported branch (not just `crtr-memory-refs` guidance; any hidden custom
303
+ * content). Covers both persisted shapes a branch entry can take: a
304
+ * `type:'custom_message'` entry (display at the entry's own top level) and a
305
+ * `type:'message'` entry wrapping a `role:'custom'` message (display nested
306
+ * under `.message`). */
307
+ export function isHiddenJsonlBranchEntry(entry) {
308
+ if (entry === null || typeof entry !== 'object')
309
+ return false;
310
+ const e = entry;
311
+ if (e.type === 'custom_message' && e.display === false)
312
+ return true;
313
+ if (e.type === 'message' && e.message?.role === 'custom' && e.message.display === false)
314
+ return true;
315
+ return false;
316
+ }
317
+ /** Rewrite an already-exported JSONL file in place, dropping every hidden
318
+ * (`display:false`) branch entry while re-chaining `parentId` so the
319
+ * remaining entries stay a single valid linear branch — applying the general
320
+ * display:false boundary to `session.exportToJsonl`'s own output, which
321
+ * serializes every entry verbatim regardless of display. The header line
322
+ * (line 1) is copied through untouched. */
323
+ export function stripHiddenEntriesFromJsonlExport(filePath) {
324
+ const raw = readFileSync(filePath, 'utf8');
325
+ const lines = raw.split('\n').filter((l) => l !== '');
326
+ if (lines.length === 0)
327
+ return;
328
+ const [headerLine, ...entryLines] = lines;
329
+ let prevId = null;
330
+ const kept = [];
331
+ for (const line of entryLines) {
332
+ const entry = JSON.parse(line);
333
+ if (isHiddenJsonlBranchEntry(entry))
334
+ continue;
335
+ kept.push(JSON.stringify({ ...entry, parentId: prevId }));
336
+ if (typeof entry.id === 'string')
337
+ prevId = entry.id;
338
+ }
339
+ writeFileSync(filePath, `${[headerLine, ...kept].join('\n')}\n`);
340
+ }
341
+ function formatModelSpec(model, thinkingLevel) {
342
+ if (model === null || model === undefined || isUnknownModel(model))
343
+ return null;
344
+ const suffix = thinkingLevel !== undefined && thinkingLevel !== '' && thinkingLevel !== 'off' ? `:${thinkingLevel}` : '';
345
+ return `${model.provider}/${model.id}${suffix}`;
346
+ }
347
+ /** The ask-time fork recipe requires an explicit thinking level: `:off` is a
348
+ * real captured choice, not an omitted default that a future runtime may infer
349
+ * differently. */
350
+ function formatExactModelSpec(model, thinkingLevel) {
351
+ const base = formatModelSpec(model, undefined);
352
+ return base === null ? null : `${base}:${thinkingLevel === undefined || thinkingLevel === '' ? 'off' : thinkingLevel}`;
353
+ }
354
+ function faultRetryProbe() {
355
+ const mode = process.env['CRTR_FAULT_RETRY_PROBE'];
356
+ if (mode === 'always')
357
+ return async () => true;
358
+ if (mode === 'never')
359
+ return async () => false;
360
+ return probeOnlineReal;
361
+ }
362
+ // ---------------------------------------------------------------------------
363
+ // Tunables (T3 backpressure / T4 dialog anti-deadlock)
364
+ // ---------------------------------------------------------------------------
365
+ /** Per-viewer outbound high-water mark (M1). A viewer whose unflushed backlog
366
+ * exceeds EITHER bound is DROPPED (socket destroyed) rather than allowed to
367
+ * apply indefinite backpressure to the shared engine. Modeled on pi's RPC
368
+ * output-guard: bound the queue, shed the slow viewer. */
369
+ const MAX_QUEUED_FRAMES = 1000;
370
+ const MAX_PENDING_BYTES = 32 * 1024 * 1024; // 32 MiB
371
+ /** F2 (attach typing-lag fix, 2026-06-09): coalesce window for relayed
372
+ * `message_update` frames. pi emits one per content delta and each carries the
373
+ * FULL accumulated assistant message, so a verbatim relay re-serializes (and
374
+ * every viewer re-parses + re-renders) the whole message per token — O(len²)
375
+ * bytes over a long reply, and measured ~3× the viewer's render CPU vs 75 ms
376
+ * coalescing. Only the LATEST pending update matters (each supersedes the
377
+ * last), so the relay keeps one and flushes it on this timer — or immediately,
378
+ * BEFORE any other event type, preserving ordering (an update never arrives
379
+ * after its message_end). */
380
+ const MESSAGE_UPDATE_COALESCE_MS = 75;
381
+ /** Broker-side default dialog timeout (C2 anti-deadlock, T4). When an extension
382
+ * dialog is forwarded to every writable client, the broker ALWAYS arms a timeout
383
+ * (this default, or a shorter per-dialog `opts.timeout` if the extension passed
384
+ * one) so unanswered dialogs — including ones whose writable viewers all detach
385
+ * — can never hang the agent turn forever. On fire it resolves to the SAFE default
386
+ * (deny for confirm; cancel/undefined for select/input/editor). */
387
+ const DEFAULT_DIALOG_TIMEOUT_MS = 120_000; // 120 s
388
+ /** Admission bound for `runReplacement` (one active head + at most one queued
389
+ * follow-up) and the mutation-through-rebind deadline each admitted head gets
390
+ * once it becomes the tail's head. The SDK has no cancellation primitive for
391
+ * newSession/switchSession/fork/bindExtensions, so an admitted replacement that
392
+ * neither settles nor completes within its deadline leaves runtime/shared state
393
+ * indeterminate — the only safe response is broker-terminal exit, never a retry
394
+ * and never letting a queued replacement start against that uncertain state. */
395
+ const MAX_ADMITTED_REPLACEMENTS = 2; // active + at most one queued
396
+ const REPLACEMENT_DEADLINE_MS = 30_000;
397
+ /** Locally branded sentinel `runReplacement`'s deadline race resolves to when
398
+ * the budget elapses before the runtime promise settles — never compared via
399
+ * string matching. */
400
+ const REPLACEMENT_DEADLINE_EXCEEDED = Symbol('replacement-deadline-exceeded');
401
+ // ---------------------------------------------------------------------------
402
+ // M3 (scout mq5thyli): the active engine session, exposed so broker-cli's FATAL
403
+ // handlers (uncaughtException / unhandledRejection / the runBroker reject) can
404
+ // dispose it before process.exit. The bash tool spawns children `detached` (own
405
+ // pgid); only session.dispose() (→ abortBash / agent.abort → killProcessTree)
406
+ // reaps them. The graceful exit path already disposes; without this the crash
407
+ // path would `process.exit(1)` and ORPHAN every in-flight bash subprocess.
408
+ // ---------------------------------------------------------------------------
409
+ let activeSession = null;
410
+ /** Dispose the live engine session if one exists (idempotent). Called by
411
+ * broker-cli's fatal handlers before exit so a crash never orphans detached
412
+ * bash children. No-op before the session is built or after a clean dispose. */
413
+ export function disposeActiveSession() {
414
+ const s = activeSession;
415
+ activeSession = null;
416
+ if (s !== null) {
417
+ try {
418
+ s.dispose();
419
+ }
420
+ catch {
421
+ /* dispose must not block the fatal exit */
422
+ }
423
+ }
424
+ }
425
+ // ---------------------------------------------------------------------------
426
+ // runBroker — the broker process body (plan T4 steps 1–6)
427
+ // ---------------------------------------------------------------------------
428
+ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), startupTs = new Date().toISOString()) {
429
+ // A clean-abort certificate belongs only to the dead broker that wrote it.
430
+ // Fleet classification consumes it before launching this successor.
431
+ clearCleanAbort(nodeId);
432
+ // The canonical broker event source (job/log.jsonl) is bound before any other
433
+ // work in this function — no path/env/state setup may precede it, and the
434
+ // binder independently validates nodeId before constructing that path.
435
+ bindBrokerEventSource(nodeId);
436
+ // One low-volume, node-correlated boot trace. It deliberately carries only
437
+ // timing and runtime identity — never prompts, commands, tokens, or secrets.
438
+ // broker-cli captures startupAt/startupTs before dynamically loading this
439
+ // module, so module_loaded's elapsed value includes the cold broker import
440
+ // graph, and cli_entry carries the exact process-spawn wall-clock time.
441
+ operationIdContext.fresh(() => {
442
+ emitEvent({
443
+ level: 'info',
444
+ event: 'broker.startup.cli_entry',
445
+ ts: startupTs,
446
+ duration_ms: 0,
447
+ fields: { runtime_version: process.version, engine_version: null, model: null },
448
+ });
449
+ emitEvent({
450
+ level: 'info',
451
+ event: 'broker.startup.module_loaded',
452
+ duration_ms: Number(process.hrtime.bigint() - startupAt) / 1_000_000,
453
+ fields: { runtime_version: process.version, engine_version: null, model: null },
454
+ });
455
+ });
456
+ // Structured, duplicate-suppressed startup milestones reached only once the
457
+ // engine is streaming a turn. `engineVersion` is populated below once
458
+ // assertEngineVersion resolves it; the milestones fire well after that.
459
+ const markedStartupEvents = new Set();
460
+ let engineVersion = null;
461
+ const emitStartupMilestone = (event, model = null) => {
462
+ if (markedStartupEvents.has(event))
463
+ return;
464
+ markedStartupEvents.add(event);
465
+ // Under CRTR_TIMING the same milestone also lands on the cross-process
466
+ // front-door timeline (stderr → this broker's broker.log), so it can be
467
+ // read next to the cli/viewer markers instead of only in the job log.
468
+ mark(event, { duration_ms: Number(process.hrtime.bigint() - startupAt) / 1_000_000 });
469
+ emitEvent({
470
+ level: 'info',
471
+ event,
472
+ duration_ms: Number(process.hrtime.bigint() - startupAt) / 1_000_000,
473
+ fields: { runtime_version: process.version, engine_version: engineVersion, model },
474
+ });
475
+ };
476
+ // Defensive: any `crtr` child the engine/extensions spawn must inherit the
477
+ // front-door recursion guard (runtime/CLAUDE.md fork-bomb invariant). The host
478
+ // (T6) also sets this; we set it again so the broker is self-sufficient.
479
+ process.env[FRONT_DOOR_ENV] = '1';
480
+ // A successful in-turn crtr command stamps this process id into its stop
481
+ // signal, so a crash-revived broker cannot consume a prior broker's outcome.
482
+ process.env['CRTR_BROKER_PID'] = String(process.pid);
483
+ // The broker is PANELESS and detached, but a broker spawned by a tmux-resident
484
+ // parent inherits the parent's TMUX_PANE/TMUX in its env (the launcher spawns
485
+ // with {...process.env}). Left in place, every `crtr` command the engine runs
486
+ // (bash tool, goal-capture naming, a child spawn) would resolve `currentTmux()`
487
+ // to the PARENT's pane/session and act there — e.g. open viewers beside, or
488
+ // close, the parent's pane. Strip both so this broker's children see no tmux
489
+ // context and take their paneless path.
490
+ delete process.env['TMUX_PANE'];
491
+ delete process.env['TMUX'];
492
+ // 1. Version tripwire + load the launch recipe the host serialized.
493
+ const engine = await loadBrokerEngine();
494
+ assertEngineVersion(engine.VERSION);
495
+ engineVersion = engine.VERSION;
496
+ // M4 / M6 (scout mq5thyli) — DELIBERATELY SKIPPED on the 0.78.1 pin. Both
497
+ // `configureHttpDispatcher` (M4: proxy support + the undici decompression fix)
498
+ // and `runMigrations` (M6: config/auth migration for a box where the pi CLI
499
+ // never ran) are CLI-only helpers that the pinned SDK does NOT re-export: the
500
+ // package's `exports` map is `.`-only, neither symbol is in `dist/index.d.ts`,
501
+ // a deep import (`.../dist/core/http-dispatcher.js`) is blocked with
502
+ // ERR_PACKAGE_PATH_NOT_EXPORTED, and the only exports-map entries are `.` and
503
+ // `./hooks` (neither carries these symbols). Per the "verify export; skip-with-
504
+ // note if absent" directive we do not vendor or reach around the export wall.
505
+ // Revisit when the SDK pin bumps to a version that re-exports them publicly.
506
+ const dir = nodeDir(nodeId);
507
+ const launchFile = join(dir, 'broker-launch.json');
508
+ const inv = JSON.parse(readFileSync(launchFile, 'utf8'));
509
+ const cfg = piInvocationToSdkConfig(inv); // also merges inv.env
510
+ // 2–4. Build the engine session via the SERVICES path (C3) — see
511
+ // buildBrokerSession below. Register it so the FATAL exit path (M3) can
512
+ // dispose it and reap detached bash children.
513
+ // `session` + `services` are MUTABLE holders (T3 session-rebind): when the
514
+ // controller drives new_session/switch_session/fork, the AgentSessionRuntime
515
+ // tears down the old session and builds the next one, then runs our rebind
516
+ // callback which reassigns these. Every closure below (buildSnapshot,
517
+ // driveEngine, handleFrame, disposeAndExit) reads through them, so they all
518
+ // follow the live session automatically. `runtime` is undefined for an engine
519
+ // that does not expose the replacement API (the fake-engine fixture).
520
+ // eslint-disable-next-line prefer-const
521
+ let { session, services, resuming, runtime } = await buildBrokerSession(engine, cfg);
522
+ // M3: name the boot session as the process-fatal disposal target immediately —
523
+ // enqueueRebind reassigns this on every later replacement, but without this
524
+ // initial assignment `activeSession` stays null (and disposeActiveSession() a
525
+ // no-op) for every exit that happens before the FIRST session replacement,
526
+ // which is the common case.
527
+ activeSession = session;
528
+ const retryProbe = faultRetryProbe();
529
+ // Initialize pi's process-global theme. The pi binary does this at boot
530
+ // (main.js: initTheme(settingsManager.getTheme(), appMode === 'interactive')),
531
+ // but the SDK convenience path the broker uses does NOT. Without it, ANY
532
+ // extension hook that reads `ctx.ui.theme` throws "Theme not initialized" —
533
+ // and the extension runner SILENTLY swallows that throw and discards the
534
+ // hook's returned payload. The visible symptom was the OAuth billing adapter's
535
+ // before_provider_request handler throwing, so its injected Claude-Code billing
536
+ // header was dropped and subscription turns 400'd ("draws from extra usage").
537
+ // headless ⇒ no file watcher (false). Idempotent global; call once at boot.
538
+ // Always replace an inherited PI_PACKAGE_DIR: a viewer launched from a pnpm
539
+ // path may pass its `.pnpm` package copy down to the daemon/broker, but that
540
+ // copy can omit the JSON theme assets. The broker must use crouter's actual
541
+ // bundled package root, exactly like its incidental pi subprocesses.
542
+ try {
543
+ process.env['PI_PACKAGE_DIR'] = resolveBundledPiPackageDir();
544
+ initTheme(services.settingsManager.getTheme(), false);
545
+ }
546
+ catch (err) {
547
+ emitEvent({
548
+ level: 'error',
549
+ event: 'broker.theme.initialization_failed',
550
+ error: err,
551
+ });
552
+ }
553
+ // Diagnose stale local-path pi packages without persisting the human-facing
554
+ // formatter's prose. The front door retains that actionable presentation.
555
+ try {
556
+ for (const missing of findMissingPackagePaths(getAgentDir())) {
557
+ emitEvent({
558
+ level: 'warn',
559
+ event: 'broker.startup.warning',
560
+ fields: {
561
+ stage: 'package_health',
562
+ source: missing.source,
563
+ resolved_path: missing.resolved,
564
+ },
565
+ });
566
+ }
567
+ }
568
+ catch {
569
+ /* best-effort */
570
+ }
571
+ // N3: the node's display name is re-applied by the rebind lane below so it
572
+ // survives a session replacement (new_session/switch_session/fork), not only at
573
+ // boot.
574
+ // -------------------------------------------------------------------------
575
+ // Socket fan-out state
576
+ // -------------------------------------------------------------------------
577
+ const sockPath = viewSocketPath(nodeId);
578
+ const attachPath = join(jobDir(nodeId), 'attach.json');
579
+ const clients = new Set();
580
+ const pendingDialogs = new Map();
581
+ // Fire-and-forget extension display state must survive a viewer detach. The
582
+ // extension may paint a widget (notably the inactivity brief) while nobody is
583
+ // attached; retain the latest keyed state so the next hello can replay it
584
+ // immediately after welcome instead of making "persistent" mean "only if you
585
+ // happened to be watching when the frame fired".
586
+ const displayStatuses = new Map();
587
+ const displayWidgets = new Map();
588
+ let displayTitle;
589
+ let disposed = false;
590
+ let agentRunActive = false;
591
+ let shutdownRequested = false;
592
+ let server;
593
+ // Every client whose `hello` claimed the writable role and whose transport is
594
+ // still usable. Liveness matters because on a unix socket a peer destroy()
595
+ // delivers EOF (readableEnded) promptly while the matching 'close' can lag
596
+ // arbitrarily behind undrainable pending writes — a departed peer must not be
597
+ // counted as an answerer a dialog fan-out waits on. Roles are FIXED per client,
598
+ // so this is a pure filter, never an arbitration.
599
+ const writableClients = () => {
600
+ const out = [];
601
+ for (const c of clients) {
602
+ if (!c.helloed || c.role !== 'controller')
603
+ continue;
604
+ if (c.socket.destroyed || c.socket.readableEnded || !c.socket.writable)
605
+ continue;
606
+ out.push(c);
607
+ }
608
+ return out;
609
+ };
610
+ // Persist viewer presence to job/attach.json when the set of helloed viewers
611
+ // changes (a hello is accepted, or a client drops/is shed) so out-of-process
612
+ // readers (the GRAPH view's attached-row tint) can see whether a human is
613
+ // watching this paneless node. Presence is a truthful COUNT and nothing more —
614
+ // there is no owner to record. Plain writeFileSync, matching telemetry.json's
615
+ // convention; best-effort — presence writing must never crash the broker.
616
+ // disposeAndExit unlinks the file, so a clean exit never leaves a stale claim
617
+ // (readers additionally trust it only while the node is 'active', fencing off
618
+ // a crash-orphaned file).
619
+ const persistAttachState = () => {
620
+ try {
621
+ let viewers = 0;
622
+ for (const c of clients)
623
+ if (c.helloed)
624
+ viewers += 1;
625
+ const dirPath = jobDir(nodeId);
626
+ if (!existsSync(dirPath))
627
+ mkdirSync(dirPath, { recursive: true });
628
+ writeFileSync(attachPath, JSON.stringify({ viewers, updated: new Date().toISOString() }, null, 2), 'utf8');
629
+ }
630
+ catch {
631
+ /* presence is best-effort; never crash the broker */
632
+ }
633
+ };
634
+ // Shed a client — destroy the socket + remove it — used by both the M1
635
+ // backpressure drop and the G7 frame-overflow drop. 'close' (→ drop) follows
636
+ // the destroy.
637
+ const dropClient = (client, reason) => {
638
+ if (!clients.has(client))
639
+ return; // already gone
640
+ // info, not debug: shedding a viewer (M1 backpressure / G7 frame overflow)
641
+ // must be visible in the canonical log without CRTR_LOG=debug — debug-level
642
+ // events are dropped by emitEvent outside that env, which silently hid
643
+ // every shed.
644
+ emitEvent({
645
+ level: 'info',
646
+ event: 'broker.runtime.diagnostic',
647
+ fields: { message: `dropping viewer ${client.id || '(pre-hello)'} — ${reason}` },
648
+ });
649
+ clients.delete(client);
650
+ persistAttachState(); // a viewer was shed
651
+ try {
652
+ client.socket.destroy();
653
+ }
654
+ catch {
655
+ /* ignore */
656
+ }
657
+ };
658
+ // `bypassHwm` (F1): the catch-up `welcome` carries the full message history and
659
+ // can legitimately be tens of MiB — far larger than a steady-state live frame.
660
+ // It is still ACCOUNTED (so a genuinely stalled client that also floods on live
661
+ // events is still shed by the NEXT frame's check), but it must never be the
662
+ // REASON a healthy client is dropped before it has had a chance to drain it.
663
+ const sendFrame = (client, frame, bypassHwm = false) => {
664
+ let data;
665
+ try {
666
+ data = encodeFrame(frame);
667
+ }
668
+ catch {
669
+ return; // an unserializable frame must never crash the broker
670
+ }
671
+ const bytes = Buffer.byteLength(data);
672
+ // M1 high-water mark: a viewer that has fallen too far behind on draining its
673
+ // socket is shed rather than allowed to grow the broker's memory unbounded.
674
+ if (!bypassHwm &&
675
+ (client.queuedFrames + 1 > MAX_QUEUED_FRAMES ||
676
+ client.pendingBytes + bytes > MAX_PENDING_BYTES)) {
677
+ dropClient(client, `backpressure high-water mark exceeded (queued=${client.queuedFrames}, pending=${client.pendingBytes}B)`);
678
+ return;
679
+ }
680
+ client.queuedFrames += 1;
681
+ client.pendingBytes += bytes;
682
+ try {
683
+ // The per-write completion callback fires when THIS chunk is flushed to the
684
+ // OS — a finer-grained drain signal than a socket-wide 'drain' event, and it
685
+ // keeps the accounting exact. write()'s boolean return is the coarse
686
+ // "buffer full" hint; the per-chunk callback is what we account on.
687
+ client.socket.write(data, () => {
688
+ client.queuedFrames -= 1;
689
+ client.pendingBytes -= bytes;
690
+ });
691
+ }
692
+ catch {
693
+ // Dead socket: undo this frame's accounting; 'close' → drop() cleans the rest.
694
+ client.queuedFrames -= 1;
695
+ client.pendingBytes -= bytes;
696
+ }
697
+ };
698
+ const broadcast = (frame) => {
699
+ for (const c of clients)
700
+ if (c.helloed)
701
+ sendFrame(c, frame);
702
+ };
703
+ /** Retain and broadcast extension UI frames. The three display METHODS of pi's
704
+ * `extension_ui_request` envelope are levels, so their last value is kept for
705
+ * the welcome snapshot; updating a keyed entry moves it to the end, matching
706
+ * pi's insertion-ordered widget behavior. Every other method (notify, the
707
+ * blocking dialogs) simply passes through. */
708
+ const broadcastUi = (frame) => {
709
+ if (frame.type === 'extension_ui_request') {
710
+ if (frame.method === 'setStatus') {
711
+ displayStatuses.delete(frame.statusKey);
712
+ if (frame.statusText !== undefined)
713
+ displayStatuses.set(frame.statusKey, frame.statusText);
714
+ }
715
+ else if (frame.method === 'setWidget') {
716
+ displayWidgets.delete(frame.widgetKey);
717
+ if (frame.widgetLines !== undefined) {
718
+ displayWidgets.set(frame.widgetKey, {
719
+ lines: [...frame.widgetLines],
720
+ placement: frame.widgetPlacement ?? 'aboveEditor',
721
+ });
722
+ }
723
+ }
724
+ else if (frame.method === 'setTitle') {
725
+ displayTitle = frame.title;
726
+ }
727
+ }
728
+ broadcast(frame);
729
+ };
730
+ // ---------------------------------------------------------------------------
731
+ // F2: message_update coalescing. `relayEvent` is the single entry point for
732
+ // engine events headed to viewers: a message_update is held (latest wins) and
733
+ // flushed on a short timer; ANY other event flushes the held update first,
734
+ // synchronously, so cross-type ordering is exactly the engine's. The timer is
735
+ // unref'd — it must never hold the broker process open past dispose.
736
+ // ---------------------------------------------------------------------------
737
+ let pendingUpdate = null;
738
+ let pendingUpdateTimer = null;
739
+ let faultRetryTimer = null;
740
+ let nextSubscriptionGeneration = 0;
741
+ let installedGeneration;
742
+ // Dead-provider-stream detector (see stream-watchdog.ts): poked while a turn
743
+ // is awaiting the provider (NOT while a tool executes — a silent long tool
744
+ // must not trip it), cleared at agent_end. Fires at most once per stall,
745
+ // aborting the session so the fault-retry machinery (armed at agent_settled)
746
+ // re-drives the turn instead of the broker wedging until the daemon's
747
+ // 20-minute hard kick.
748
+ const streamWatchdogMs = resolveStreamWatchdogMs();
749
+ const streamWatchdog = new StreamWatchdog((armedGeneration) => {
750
+ const generation = installedGeneration;
751
+ if (generation === undefined || armedGeneration !== generation.id)
752
+ return;
753
+ emitEvent({
754
+ level: 'debug',
755
+ event: 'broker.runtime.diagnostic',
756
+ fields: { message: `stream watchdog: no engine event for ${streamWatchdogMs}ms while streaming — aborting stalled provider stream` },
757
+ });
758
+ void generation.session.abort().catch((err) => {
759
+ emitEvent({
760
+ level: 'debug',
761
+ event: 'broker.runtime.diagnostic',
762
+ fields: { message: `stream watchdog abort failed: ${String(err)}` },
763
+ });
764
+ });
765
+ }, streamWatchdogMs);
766
+ const flushPendingUpdate = () => {
767
+ if (pendingUpdateTimer !== null) {
768
+ clearTimeout(pendingUpdateTimer);
769
+ pendingUpdateTimer = null;
770
+ }
771
+ if (pendingUpdate !== null) {
772
+ const frame = pendingUpdate;
773
+ pendingUpdate = null;
774
+ broadcast(frame);
775
+ }
776
+ };
777
+ const clearFaultRetryTimer = () => {
778
+ if (faultRetryTimer === null)
779
+ return;
780
+ clearTimeout(faultRetryTimer);
781
+ faultRetryTimer = null;
782
+ };
783
+ const failedRetryProviders = new Set();
784
+ let providerFallbackInFlight = null;
785
+ // Models that have already produced a 404 not_found this session. Guards the
786
+ // not-found fallback below against an infinite swap→re-drive→404 loop when the
787
+ // fallback TARGET is itself unavailable (or equals the model that just failed).
788
+ const notFoundModels = new Set();
789
+ let notFoundFallbackInFlight = null;
790
+ // Tier 3 model-less boot: the first-prompt kickoff can't fire at listen time
791
+ // when the session booted with no model at all. Hold the prompt text here and
792
+ // fire it once a model actually becomes available (reload_auth re-drive,
793
+ // below) instead of the old unconditional call. For the common model-present
794
+ // path this fires immediately at listen time — byte-identical to the prior
795
+ // behavior.
796
+ let kickoffPending;
797
+ let kickoffInFlight = false;
798
+ // Guards the reload_auth re-drive (below) against overlapping /login frames
799
+ // re-running resolveLaunchModel + setModel + kickoff concurrently; the
800
+ // reload/refresh/ack/broadcast ahead of it stay idempotent and always run.
801
+ let reloadAuthInFlight = null;
802
+ // A fresh auth reload is also the explicit recovery trigger for a turn parked
803
+ // on a fatal provider-auth fault. Keep at most one rewind + prompt in flight
804
+ // when the daemon and an attached viewer both fan the same login update.
805
+ let authFaultRedriveInFlight = false;
806
+ // A model-less boot is EITHER a literal `undefined` model (the fake engine) OR
807
+ // the real SDK's unknown-stub (see isUnknownModel) — the real pinned engine
808
+ // NEVER yields `undefined`, so every model-less guard must accept both.
809
+ const sessionHasNoModel = () => session.model === undefined || isUnknownModel(session.model);
810
+ const drivePendingKickoff = () => {
811
+ if (kickoffPending === undefined || kickoffInFlight || sessionHasNoModel())
812
+ return;
813
+ const prompt = kickoffPending;
814
+ kickoffPending = undefined;
815
+ kickoffInFlight = true;
816
+ trackPromptDispatch(operationIdContext.fresh(() => session.prompt(prompt)
817
+ .catch((err) => {
818
+ emitEvent({
819
+ level: 'error',
820
+ event: 'broker.prompt.first_failed',
821
+ error: err,
822
+ });
823
+ })
824
+ .finally(() => {
825
+ kickoffInFlight = false;
826
+ })));
827
+ };
828
+ const maybeSwitchProviderForRetry = (event) => {
829
+ if (event.type !== 'auto_retry_start')
830
+ return;
831
+ const retry = event;
832
+ const kind = classify('pi→provider', retry.errorMessage ?? '').kind;
833
+ if (kind !== 'rate-limit' && kind !== 'connection')
834
+ return;
835
+ if (providerFallbackInFlight !== null)
836
+ return;
837
+ const generation = installedGeneration;
838
+ if (generation === undefined)
839
+ return;
840
+ const candidateSession = session;
841
+ const candidateServices = services;
842
+ // An exact-consult launch (cfg.modelExact) locks the session to the single
843
+ // captured model — never cross-provider-retry, not even the
844
+ // modelProviderPinned restore-toward-pin exception below. Unavailability
845
+ // must fail the turn, not substitute a different model.
846
+ if (cfg.modelExact === true)
847
+ return;
848
+ const currentSpec = formatModelSpec(session.model, session.thinkingLevel);
849
+ if (currentSpec === null)
850
+ return;
851
+ const next = equivalentOtherProviderModel(currentSpec, failedRetryProviders);
852
+ if (next === null)
853
+ return;
854
+ // An explicit provider pin forbids turn-time rotation AWAY to another
855
+ // provider — EXCEPT a switch back TOWARD the recorded restoration pin (a
856
+ // launch that fell back off a cooling pinned provider records the requested
857
+ // managed model). Restoring to the pinned provider honors the pin rather
858
+ // than violating it; every other rotation stays suppressed.
859
+ if (cfg.modelProviderPinned === true) {
860
+ const pin = readRotationConfig().preferredModel;
861
+ const targetSpec = parseModelSpec(next.model).modelSpec;
862
+ const targetProvider = targetSpec.slice(0, targetSpec.indexOf('/'));
863
+ const targetId = targetSpec.slice(targetSpec.indexOf('/') + 1);
864
+ if (pin?.providerId !== targetProvider || pin.modelId !== targetId)
865
+ return;
866
+ }
867
+ const fallbackPromise = (async () => {
868
+ const requestedSpec = parseModelSpec(next.model).modelSpec;
869
+ const slash = requestedSpec.indexOf('/');
870
+ const model = slash > 0
871
+ ? registryOf(candidateServices).find(requestedSpec.slice(0, slash), requestedSpec.slice(slash + 1))
872
+ : undefined;
873
+ const requested = parseModelSpec(next.model);
874
+ const pool = isManagedProvider(requested.modelSpec.slice(0, requested.modelSpec.indexOf('/')))
875
+ ? readSubscriptionPool(requested.modelSpec.slice(0, requested.modelSpec.indexOf('/')))
876
+ : [];
877
+ const resolved = resolveProviderCandidates([{
878
+ providerId: requested.modelSpec.slice(0, requested.modelSpec.indexOf('/')),
879
+ modelId: requested.modelSpec.slice(requested.modelSpec.indexOf('/') + 1),
880
+ thinkingLevel: requested.thinkingLevel,
881
+ registered: model !== undefined,
882
+ authenticated: model !== undefined && (isManagedProvider(requested.modelSpec.slice(0, requested.modelSpec.indexOf('/')))
883
+ ? pool.length > 0
884
+ : registryOf(candidateServices).hasConfiguredAuth(model)),
885
+ coolingUntil: pool.length > 0 && pool.every((entry) => (entry.rateLimitedUntil || 0) > Date.now())
886
+ ? Math.min(...pool.map((entry) => entry.rateLimitedUntil || 0))
887
+ : undefined,
888
+ }]);
889
+ if (resolved.candidates.length === 0 || model === undefined) {
890
+ emitEvent({
891
+ level: 'debug',
892
+ event: 'broker.runtime.diagnostic',
893
+ fields: { message: `provider fallback skipped: ${next.model} is ${resolved.rejected[0]?.reason ?? 'unavailable'}` },
894
+ });
895
+ return;
896
+ }
897
+ // Snapshot the restoration intent before the asynchronous model change;
898
+ // this retry must not replace a launch-recorded requested pin.
899
+ const preferred = readRotationConfig().preferredModel;
900
+ await candidateSession.setModel(model);
901
+ if (installedGeneration !== generation || session !== candidateSession)
902
+ return;
903
+ if (requested.thinkingLevel !== undefined)
904
+ candidateSession.setThinkingLevel(requested.thinkingLevel);
905
+ const currentProvider = currentSpec.slice(0, currentSpec.indexOf('/'));
906
+ const currentModelId = parseModelSpec(currentSpec).modelSpec.slice(currentSpec.indexOf('/') + 1);
907
+ // A launch fallback already records its requested managed model as the
908
+ // restoration pin. Keep that intent through turn-time retries; reaching
909
+ // it makes the temporary pin fulfilled. A normal retry has no such pin,
910
+ // so retain the failed managed model for provider-rotation to restore.
911
+ const switchedProvider = requested.modelSpec.slice(0, requested.modelSpec.indexOf('/'));
912
+ const switchedModelId = requested.modelSpec.slice(switchedProvider.length + 1);
913
+ if (preferred?.providerId === switchedProvider && preferred.modelId === switchedModelId) {
914
+ clearPreferredModel();
915
+ }
916
+ else if (preferred === undefined && isManagedProvider(currentProvider)) {
917
+ rememberPreferredModel({ providerId: currentProvider, modelId: currentModelId });
918
+ }
919
+ failedRetryProviders.add(next.fromProvider);
920
+ broadcastModelChanged(false);
921
+ emitEvent({
922
+ level: 'debug',
923
+ event: 'broker.runtime.diagnostic',
924
+ fields: { message: `provider fallback after ${kind}: ${currentSpec} → ${formatModelSpec(candidateSession.model, candidateSession.thinkingLevel) ?? next.model}` },
925
+ });
926
+ })()
927
+ .catch((err) => {
928
+ emitEvent({
929
+ level: 'debug',
930
+ event: 'broker.runtime.diagnostic',
931
+ fields: { message: `provider fallback failed: ${err instanceof Error ? err.message : String(err)}` },
932
+ });
933
+ })
934
+ .finally(() => {
935
+ if (installedGeneration === generation)
936
+ providerFallbackInFlight = null;
937
+ });
938
+ providerFallbackInFlight = fallbackPromise;
939
+ };
940
+ // A 404 not_found is NOT one of pi's auto-retried kinds (it retries 429/5xx
941
+ // only) and never resolves on its own — the configured model id is wrong or
942
+ // decommissioned (e.g. the default `anthropic/ultra` pointing at a model the
943
+ // account can't reach). Left alone the node errors out, the stophook parks it
944
+ // as a stall, and the daemon force-recycles it onto the SAME bad model in a
945
+ // loop. Instead: fail the node over to the strong-anthropic ladder model and
946
+ // re-drive the turn so the work continues. broadcastModelChanged persists the
947
+ // swap into the durable launch recipe, so a later revive uses the good model
948
+ // too. Fires on the terminal-error message (stopReason 'error'), the signal a
949
+ // non-retried request surfaces.
950
+ //
951
+ // The SAME treatment covers a provider whose credential could not be resolved
952
+ // (`Provider is not configured: <id>`). That error is classified `other`, i.e.
953
+ // fatal, so without this the node parks and every wake re-fails on the same
954
+ // unusable provider — the only escape being a human running `crtr node config
955
+ // --model`. Here it substitutes an equivalent model on the OTHER provider (the
956
+ // same ladder cell `maybeSwitchProviderForRetry` uses), proves that target is
957
+ // actually authenticated before switching, and re-drives.
958
+ const maybeFallbackOnUnusableModel = (event) => {
959
+ if (event.type !== 'message_end')
960
+ return;
961
+ const msg = event.message;
962
+ if (msg === undefined || msg.role !== 'assistant' || msg.stopReason !== 'error')
963
+ return;
964
+ const unconfigured = isProviderUnconfiguredError(msg.errorMessage ?? '');
965
+ if (!unconfigured && !isModelNotFoundError(msg.errorMessage ?? ''))
966
+ return;
967
+ if (cfg.modelProviderPinned === true)
968
+ return;
969
+ // An exact-consult launch must fail closed on a 404 for its locked model,
970
+ // never auto-fail-over to the strong-anthropic ladder.
971
+ if (cfg.modelExact === true)
972
+ return;
973
+ if (notFoundFallbackInFlight !== null)
974
+ return;
975
+ const generation = installedGeneration;
976
+ if (generation === undefined)
977
+ return;
978
+ const candidateSession = session;
979
+ const candidateServices = services;
980
+ const currentSpec = formatModelSpec(session.model, session.thinkingLevel);
981
+ if (currentSpec === null)
982
+ return;
983
+ notFoundModels.add(currentSpec);
984
+ // The strong-anthropic ladder cell — "whatever the current strong anthropic
985
+ // provider model is" (config modelLadders.anthropic.strong), not hardcoded.
986
+ // An unusable PROVIDER (as opposed to a missing model id) prefers the
987
+ // same-strength cell on the other provider, so a node keeps the capability
988
+ // it was launched with instead of being promoted or demoted by the swap.
989
+ const equivalent = unconfigured ? equivalentOtherProviderModel(currentSpec, failedRetryProviders)?.model : undefined;
990
+ const target = normalizeModel(equivalent ?? 'anthropic/strong');
991
+ // Don't loop: the failed model already IS the target, or the target itself
992
+ // 404'd earlier this session. Leave it to the normal stall/recycle path.
993
+ if (target === currentSpec || notFoundModels.has(target)) {
994
+ emitEvent({
995
+ level: 'debug',
996
+ event: 'broker.runtime.diagnostic',
997
+ fields: { message: `not_found fallback skipped: ${currentSpec} has no healthy strong-anthropic target (${target})` },
998
+ });
999
+ return;
1000
+ }
1001
+ const fallbackPromise = (async () => {
1002
+ const targetSpec = parseModelSpec(target).modelSpec;
1003
+ const slash = targetSpec.indexOf('/');
1004
+ const model = slash > 0
1005
+ ? registryOf(candidateServices).find(targetSpec.slice(0, slash), targetSpec.slice(slash + 1))
1006
+ : undefined;
1007
+ if (model === undefined) {
1008
+ emitEvent({
1009
+ level: 'debug',
1010
+ event: 'broker.runtime.diagnostic',
1011
+ fields: { message: `${unconfigured ? 'unconfigured-provider' : 'not_found'} fallback skipped: ${target} is not registered` },
1012
+ });
1013
+ return;
1014
+ }
1015
+ // Swapping onto a second provider that ALSO has no credential would just
1016
+ // repeat the fault under a different name, so prove this one resolves.
1017
+ if (unconfigured && !registryOf(candidateServices).hasConfiguredAuth(model)) {
1018
+ emitEvent({
1019
+ level: 'debug',
1020
+ event: 'broker.runtime.diagnostic',
1021
+ fields: { message: `unconfigured-provider fallback skipped: ${target} is not authenticated either` },
1022
+ });
1023
+ return;
1024
+ }
1025
+ const requested = parseModelSpec(target);
1026
+ await candidateSession.setModel(model);
1027
+ if (installedGeneration !== generation || session !== candidateSession)
1028
+ return;
1029
+ if (requested.thinkingLevel !== undefined)
1030
+ candidateSession.setThinkingLevel(requested.thinkingLevel);
1031
+ broadcastModelChanged();
1032
+ const resolved = formatModelSpec(candidateSession.model, candidateSession.thinkingLevel) ?? target;
1033
+ emitEvent({
1034
+ level: 'debug',
1035
+ event: 'broker.runtime.diagnostic',
1036
+ fields: { message: `${unconfigured ? 'unconfigured-provider' : 'not_found'} fallback: ${currentSpec} unavailable → ${resolved}; re-driving turn` },
1037
+ });
1038
+ // Re-drive: a 404 left the turn errored, so the model swap alone changes
1039
+ // nothing until the next prompt — re-issue so the now-healthy model picks
1040
+ // the task back up. We fire on message_end, BEFORE the errored turn has
1041
+ // fully settled, so wait for the engine to go idle first: prompt() THROWS
1042
+ // if called while streaming without a streamingBehavior, which would swap
1043
+ // the model but silently skip the continuation.
1044
+ await candidateSession.agent.waitForIdle();
1045
+ if (installedGeneration !== generation || session !== candidateSession)
1046
+ return;
1047
+ // The parked fatal describes a model this session no longer uses. Clearing
1048
+ // it after the errored turn has settled (waitForIdle is past agent_end, so
1049
+ // publishSettledProviderFault has already written it) keeps a recovered
1050
+ // node from reviving straight back into fault-recovery chrome.
1051
+ if (unconfigured)
1052
+ clearFault(nodeId, { link: 'pi→provider' });
1053
+ await operationIdContext.fresh(() => session.prompt(unconfigured
1054
+ ? `The previous model (${currentSpec}) had no usable provider credential, so you were automatically switched to ${resolved}. Continue the task from where the failed turn left off.`
1055
+ : `The previous model (${currentSpec}) was unavailable (provider 404 not_found), so you were automatically switched to ${resolved}. Continue the task from where the failed turn left off.`));
1056
+ })()
1057
+ .catch((err) => {
1058
+ emitEvent({
1059
+ level: 'debug',
1060
+ event: 'broker.runtime.diagnostic',
1061
+ fields: { message: `${unconfigured ? 'unconfigured-provider' : 'not_found'} fallback failed: ${err instanceof Error ? err.message : String(err)}` },
1062
+ });
1063
+ })
1064
+ .finally(() => {
1065
+ if (installedGeneration === generation)
1066
+ notFoundFallbackInFlight = null;
1067
+ });
1068
+ notFoundFallbackInFlight = fallbackPromise;
1069
+ };
1070
+ const isActiveAutoFault = (fault) => {
1071
+ if (fault === null)
1072
+ return false;
1073
+ if (fault.retry.disposition !== 'auto')
1074
+ return false;
1075
+ if (fault.retry.by !== 'daemon')
1076
+ return false;
1077
+ return fault.kind === 'connection' || fault.kind === 'rate-limit' || fault.kind === 'overloaded';
1078
+ };
1079
+ const faultRetryDelayMs = (fault, now) => {
1080
+ if (fault.retry.nextAt !== undefined) {
1081
+ const ms = Date.parse(fault.retry.nextAt) - now;
1082
+ return Number.isFinite(ms) ? Math.max(0, ms) : null;
1083
+ }
1084
+ const plan = nextFaultRetry(fault, now);
1085
+ return plan?.delayMs ?? null;
1086
+ };
1087
+ // Node timers accept only a signed int32 delay. Long fault deadlines (for
1088
+ // example an invalid credential's remediation backoff) are chunked so they
1089
+ // cannot overflow into an immediate retry.
1090
+ const MAX_TIMEOUT_MS = 2_147_483_647;
1091
+ const clearAndArmFaultRetry = (delayMs, generation) => {
1092
+ if (installedGeneration !== generation)
1093
+ return;
1094
+ clearFaultRetryTimer();
1095
+ const chunkMs = Math.min(Math.max(0, delayMs), MAX_TIMEOUT_MS);
1096
+ const remainingMs = Math.max(0, delayMs - chunkMs);
1097
+ faultRetryTimer = setTimeout(() => {
1098
+ faultRetryTimer = null;
1099
+ if (installedGeneration !== generation)
1100
+ return;
1101
+ if (remainingMs > 0) {
1102
+ clearAndArmFaultRetry(remainingMs, generation);
1103
+ return;
1104
+ }
1105
+ void runFaultRetry().catch((err) => {
1106
+ emitEvent({
1107
+ level: 'debug',
1108
+ event: 'broker.runtime.diagnostic',
1109
+ fields: { message: `fault retry fire failed: ${err instanceof Error ? err.message : String(err)}` },
1110
+ });
1111
+ });
1112
+ }, chunkMs);
1113
+ if (typeof faultRetryTimer.unref === 'function')
1114
+ faultRetryTimer.unref();
1115
+ };
1116
+ const scheduleFaultRetry = () => {
1117
+ const generation = installedGeneration;
1118
+ if (generation === undefined || session.isStreaming)
1119
+ return;
1120
+ if (faultRetryTimer !== null)
1121
+ return;
1122
+ const fault = readFault(nodeId);
1123
+ if (!isActiveAutoFault(fault)) {
1124
+ clearFaultRetryTimer();
1125
+ return;
1126
+ }
1127
+ const delayMs = faultRetryDelayMs(fault, Date.now());
1128
+ if (delayMs === null)
1129
+ return;
1130
+ clearAndArmFaultRetry(delayMs, generation);
1131
+ };
1132
+ const publishSettledProviderFault = (generation) => {
1133
+ const agentEnd = generation.stagedProviderAgentEnd;
1134
+ const overflowFailure = generation.stagedOverflowFailure;
1135
+ generation.stagedProviderAgentEnd = null;
1136
+ generation.stagedOverflowFailure = null;
1137
+ const settledSession = generation.session;
1138
+ const messages = Array.isArray(agentEnd?.messages) ? agentEnd.messages : [];
1139
+ const last = [...messages].reverse().find((message) => typeof message === 'object' && message !== null && message.role === 'assistant');
1140
+ if (overflowFailure !== null) {
1141
+ // The settled terminal outcome supersedes any auto marker retained while
1142
+ // the broker re-drove this same turn; fault priority must not preserve a
1143
+ // now-obsolete retry plan over Pi's definitive recovery result.
1144
+ clearFault(nodeId, { link: 'pi→provider' });
1145
+ recordFault(nodeId, {
1146
+ link: 'pi→provider',
1147
+ op: 'context overflow recovery',
1148
+ kind: 'context-overflow',
1149
+ retry: { disposition: 'fatal' },
1150
+ message: overflowFailure.errorMessage,
1151
+ anchorEntryId: settledSession.sessionManager.getLeafId?.() ?? undefined,
1152
+ });
1153
+ return;
1154
+ }
1155
+ if (last?.stopReason !== 'error')
1156
+ return;
1157
+ const raw = typeof last.errorMessage === 'string'
1158
+ ? last.errorMessage
1159
+ : 'engine error (no errorMessage recorded)';
1160
+ const uncompactedOverflow = isContextOverflow(last, settledSession.model?.contextWindow);
1161
+ const classified = classify('pi→provider', last);
1162
+ const coolingDeadline = extractCoolingDeadline(last);
1163
+ if (uncompactedOverflow)
1164
+ clearFault(nodeId, { link: 'pi→provider' });
1165
+ recordFault(nodeId, {
1166
+ link: 'pi→provider',
1167
+ op: 'generation turn',
1168
+ kind: uncompactedOverflow ? 'context-overflow' : classified.kind,
1169
+ retry: uncompactedOverflow
1170
+ ? { disposition: 'fatal' }
1171
+ : classified.disposition === 'auto'
1172
+ ? { disposition: 'auto', by: 'daemon', ...(coolingDeadline !== undefined ? { nextAt: new Date(coolingDeadline).toISOString() } : {}) }
1173
+ : { disposition: classified.disposition },
1174
+ message: raw,
1175
+ anchorEntryId: settledSession.sessionManager.getLeafId?.() ?? undefined,
1176
+ });
1177
+ };
1178
+ const runFaultRetry = async () => {
1179
+ const generation = installedGeneration;
1180
+ if (generation === undefined)
1181
+ return;
1182
+ const candidateSession = generation.session;
1183
+ if (candidateSession.isStreaming)
1184
+ return;
1185
+ const initial = readFault(nodeId);
1186
+ if (!isActiveAutoFault(initial))
1187
+ return;
1188
+ let fault = initial;
1189
+ if (initial.kind === 'connection') {
1190
+ const online = await retryProbe();
1191
+ // The probe can take seconds. A turn may have started, or the marker may
1192
+ // have been cleared/replaced/recovered, in that window. Re-validate
1193
+ // against fresh state before re-arming or consuming an attempt —
1194
+ // otherwise a stale retry resurrects a recovered episode or double-drives
1195
+ // a now-streaming session.
1196
+ if (installedGeneration !== generation || session !== candidateSession || candidateSession.isStreaming)
1197
+ return;
1198
+ const fresh = readFault(nodeId);
1199
+ if (!isActiveAutoFault(fresh) || fresh.since !== initial.since)
1200
+ return;
1201
+ if (!online) {
1202
+ clearAndArmFaultRetry(policyFor(fresh.kind).baseDelayMs, generation);
1203
+ return;
1204
+ }
1205
+ fault = fresh;
1206
+ }
1207
+ const now = Date.now();
1208
+ const attempt = fault.retry.attempt ?? 0;
1209
+ const policy = policyFor(fault.kind);
1210
+ if (attempt >= policy.max) {
1211
+ recordFault(nodeId, markFatalOnExhaust(fault));
1212
+ return;
1213
+ }
1214
+ const plan = nextFaultRetry(fault, now);
1215
+ if (plan === null) {
1216
+ recordFault(nodeId, markFatalOnExhaust(fault));
1217
+ return;
1218
+ }
1219
+ const anchorEntryId = fault.anchorEntryId;
1220
+ if (anchorEntryId !== undefined) {
1221
+ try {
1222
+ const leafId = candidateSession.sessionManager.getLeafId?.() ?? null;
1223
+ if (leafId !== anchorEntryId) {
1224
+ await candidateSession.navigateTree(anchorEntryId);
1225
+ if (installedGeneration !== generation || session !== candidateSession)
1226
+ return;
1227
+ }
1228
+ }
1229
+ catch (err) {
1230
+ emitEvent({
1231
+ level: 'debug',
1232
+ event: 'broker.runtime.diagnostic',
1233
+ fields: { message: `fault retry rewind skipped: ${err instanceof Error ? err.message : String(err)}` },
1234
+ });
1235
+ }
1236
+ }
1237
+ if (installedGeneration !== generation || session !== candidateSession)
1238
+ return;
1239
+ recordFault(nodeId, plan.fault);
1240
+ await operationIdContext.fresh(() => session.prompt(fault.kind === 'connection' ? CONNECTION_FAULT_RECOVERY_BODY : PROVIDER_FAULT_RECOVERY_BODY));
1241
+ };
1242
+ // Waiters for "the engine ACCEPTED a prompt and a turn is now running". A
1243
+ // routed prompt's own promise settles only when the whole TURN ends, which is
1244
+ // minutes too late to ack a deliver; agent_start is the engine's own signal
1245
+ // that the call got past its model/auth gate. The deliver ack races these two.
1246
+ const turnAcceptedWaiters = new Set();
1247
+ const notifyTurnAccepted = () => {
1248
+ if (turnAcceptedWaiters.size === 0)
1249
+ return;
1250
+ const waiters = [...turnAcceptedWaiters];
1251
+ turnAcceptedWaiters.clear();
1252
+ for (const waiter of waiters)
1253
+ waiter();
1254
+ };
1255
+ const relayEvent = (event, generation) => {
1256
+ maybeSwitchProviderForRetry(event);
1257
+ maybeFallbackOnUnusableModel(event);
1258
+ const type = event.type;
1259
+ if (type === 'agent_start') {
1260
+ notifyTurnAccepted();
1261
+ emitStartupMilestone('broker.startup.agent_start', formatModelSpec(session.model, session.thinkingLevel));
1262
+ }
1263
+ if (type === 'message_update') {
1264
+ const message = event.message;
1265
+ if (message?.role === 'assistant')
1266
+ emitStartupMilestone('broker.startup.provider_first_delta', formatModelSpec(session.model, session.thinkingLevel));
1267
+ }
1268
+ if (type === 'message_end') {
1269
+ const message = event.message;
1270
+ if (message?.role === 'assistant')
1271
+ emitStartupMilestone('broker.startup.assistant_message_end', formatModelSpec(session.model, session.thinkingLevel));
1272
+ }
1273
+ if (type === 'agent_start' || type === 'turn_start')
1274
+ clearFaultRetryTimer();
1275
+ // The watchdog covers ONLY time awaiting the provider. `session.isStreaming`
1276
+ // stays true through tool execution, so a legitimately silent tool lasting
1277
+ // longer than the timeout would otherwise be aborted mid-run: disarm while
1278
+ // any tool executes and rearm once the last one ends. Multiple tools can run
1279
+ // concurrently, so track in-flight ids and only rearm when the set drains.
1280
+ if (type === 'agent_end') {
1281
+ // Clear the in-flight set too: an abnormal run can emit agent_end after a
1282
+ // tool_execution_start with no matching tool_execution_end. A leftover id
1283
+ // would keep executingTools non-empty forever, gating every future poke
1284
+ // (size === 0) and permanently disarming the watchdog until a rebind.
1285
+ generation.executingTools.clear();
1286
+ streamWatchdog.reset();
1287
+ }
1288
+ else if (type === 'tool_execution_start') {
1289
+ generation.executingTools.add(event.toolCallId ?? '');
1290
+ streamWatchdog.clear();
1291
+ }
1292
+ else if (type === 'tool_execution_end') {
1293
+ generation.executingTools.delete(event.toolCallId ?? '');
1294
+ if (generation.executingTools.size === 0 && generation.session.isStreaming)
1295
+ streamWatchdog.poke(generation.id);
1296
+ }
1297
+ else if (generation.session.isStreaming && generation.executingTools.size === 0) {
1298
+ streamWatchdog.poke(generation.id);
1299
+ }
1300
+ if (type === 'message_update') {
1301
+ pendingUpdate = event; // latest full-message update supersedes the held one
1302
+ if (pendingUpdateTimer === null) {
1303
+ pendingUpdateTimer = setTimeout(flushPendingUpdate, MESSAGE_UPDATE_COALESCE_MS);
1304
+ if (typeof pendingUpdateTimer.unref === 'function')
1305
+ pendingUpdateTimer.unref();
1306
+ }
1307
+ return;
1308
+ }
1309
+ // Any non-update event: flush the held update FIRST so a viewer never sees
1310
+ // (e.g.) message_end before the update it supersedes — then relay verbatim.
1311
+ flushPendingUpdate();
1312
+ broadcast(event);
1313
+ };
1314
+ // Persist a live model switch into the node's durable launch recipe so it
1315
+ // survives a yield/revive. pi's `/model` (→ set_model/cycle_model) only
1316
+ // mutates the in-memory engine; without this the node reverts to its
1317
+ // persona/spawn model on the next revive. We write the full `provider/id`
1318
+ // (which normalizeModel passes through unchanged) to BOTH model_override (so
1319
+ // polymorphs preserve it via buildLaunchSpec) and launch.model (the recipe
1320
+ // buildPiArgv replays on revive). Best-effort: a degenerate/ephemeral node
1321
+ // with no canvas row just skips persistence. CRTR_NODE_ID is set in the
1322
+ // broker's own env (merged by piInvocationToSdkConfig at boot).
1323
+ // `pinnedOverride`, when passed, is the durable-pin decision for THIS model
1324
+ // change — decided by the caller from the change's RAW pre-normalize token
1325
+ // (an explicit set_model). Omitted — an algorithmic model change (cycle_model,
1326
+ // cycle_ladder, a provider-fallback/not-found/cooling re-drive) — the existing
1327
+ // pin is carried through UNCHANGED via the `meta.launch` spread, exactly as
1328
+ // before: those changes never re-decide a pin the user didn't ask to change.
1329
+ //
1330
+ // `cfg.modelProviderPinned` is this broker's LIVE authoritative pin state —
1331
+ // every in-process policy read (the retry-fallback gate, the model-not-found
1332
+ // gate, and pinnedToFirst on a reload_auth re-drive) consults it directly, not
1333
+ // the boot-time launch recipe. Mutate it here, synchronously, BEFORE the
1334
+ // best-effort durable write below, so an explicit set_model takes effect for
1335
+ // the very next policy decision even when the canvas row is unreachable
1336
+ // (a degenerate/ephemeral node). Only ever reached from the success path of a
1337
+ // completed `set_model` — never before the engine call it decided from has
1338
+ // actually succeeded.
1339
+ const persistModelChoice = (pinnedOverride) => {
1340
+ if (pinnedOverride !== undefined)
1341
+ cfg.modelProviderPinned = pinnedOverride;
1342
+ const nodeId = process.env['CRTR_NODE_ID'];
1343
+ const spec = formatModelSpec(session.model, session.thinkingLevel);
1344
+ if (nodeId === undefined || nodeId === '' || spec === null)
1345
+ return;
1346
+ try {
1347
+ const meta = getNode(nodeId);
1348
+ if (meta === null)
1349
+ return;
1350
+ const launch = meta.launch !== undefined
1351
+ ? { ...meta.launch, model: spec, ...(pinnedOverride !== undefined ? { modelProviderPinned: pinnedOverride } : {}) }
1352
+ : undefined;
1353
+ updateNode(nodeId, { model_override: spec, ...(launch !== undefined ? { launch } : {}) });
1354
+ }
1355
+ catch {
1356
+ // Persistence is best-effort; the live switch already succeeded.
1357
+ }
1358
+ };
1359
+ // pi emits no AgentSessionEvent for a model switch, so after a successful
1360
+ // set_model/cycle_model the broker announces it itself — otherwise the new
1361
+ // model reaches no viewer (the requester gets only a bare ack) and every
1362
+ // footer shows the stale model until the next unrelated event.
1363
+ const broadcastModelChanged = (persist = true, pinnedOverride) => {
1364
+ if (persist)
1365
+ persistModelChoice(pinnedOverride);
1366
+ broadcast({ type: 'model_changed', model: isUnknownModel(session.model) ? undefined : session.model, spec: formatModelSpec(session.model, session.thinkingLevel) });
1367
+ };
1368
+ const buildSnapshot = () => ({
1369
+ messages: snapshotMessages(session),
1370
+ stats: session.getSessionStats(),
1371
+ // pi's `get_state` (rpc-mode.js), field for field, from the same getters.
1372
+ // The ONE divergence: a model-less session (undefined OR the SDK
1373
+ // unknown-stub) reports no model, matching broadcastModelChanged — never a
1374
+ // fictitious `unknown` selection.
1375
+ state: {
1376
+ model: isUnknownModel(session.model) ? undefined : session.model,
1377
+ thinkingLevel: session.thinkingLevel,
1378
+ isStreaming: session.isStreaming,
1379
+ isCompacting: session.isCompacting,
1380
+ steeringMode: session.steeringMode,
1381
+ followUpMode: session.followUpMode,
1382
+ sessionFile: session.sessionFile,
1383
+ sessionId: session.sessionId,
1384
+ sessionName: session.sessionName,
1385
+ autoCompactionEnabled: session.autoCompactionEnabled,
1386
+ messageCount: session.messages.length,
1387
+ pendingMessageCount: session.pendingMessageCount,
1388
+ },
1389
+ // Broker-owned levels (see BrokerSnapshot.display). Copied out so a later
1390
+ // mutation of the retained maps cannot alias a frame already serialized.
1391
+ display: {
1392
+ statuses: Object.fromEntries(displayStatuses),
1393
+ widgets: Object.fromEntries([...displayWidgets].map(([k, w]) => [k, { lines: [...w.lines], placement: w.placement }])),
1394
+ title: displayTitle,
1395
+ },
1396
+ });
1397
+ // Send a client its catch-up snapshot. welcome.pending_dialog carries a single
1398
+ // still-in-flight dialog to a WRITABLE client attaching mid-dialog; an observer
1399
+ // can never answer one, so observers get null. The pendingDialogs map is
1400
+ // insertion-ordered — the first entry is the canonical one carried here; any
1401
+ // extras are replayed explicitly by the caller (rare: concurrent dialogs).
1402
+ const sendWelcome = (client) => {
1403
+ const first = client.role === 'controller' ? pendingDialogs.values().next().value : undefined;
1404
+ sendFrame(client, {
1405
+ type: 'welcome',
1406
+ snapshot: buildSnapshot(),
1407
+ role: client.role,
1408
+ pending_dialog: first !== undefined ? first.request : null,
1409
+ agentDir: getAgentDir(),
1410
+ }, true);
1411
+ // No display_* replay: the retained extension state rides IN the snapshot
1412
+ // (`snapshot.display`), so catch-up is one atomic frame. A viewer attaching
1413
+ // after an idle brief was painted still sees it on first paint, and there is
1414
+ // no window between `welcome` and the replay where chrome renders blank.
1415
+ };
1416
+ // Attach-mid-dialog replay: a dialog raised before this client attached is
1417
+ // still pending (a peer detaching never cancels one — see makeBrokerUiContext),
1418
+ // so a writable client joining afterwards is handed every in-flight dialog and
1419
+ // becomes a full peer in the race to answer it. `welcome.pending_dialog` already
1420
+ // carried the FIRST entry, so this replays only the extras.
1421
+ const replayExtraPendingDialogsTo = (client) => {
1422
+ const pend = [...pendingDialogs.values()];
1423
+ for (let i = 1; i < pend.length; i++)
1424
+ sendFrame(client, pend[i].request);
1425
+ };
1426
+ // After a session-replacing op (new_session/switch_session/fork) the engine's
1427
+ // entire message history changed, so every attached viewer must rebuild from a
1428
+ // fresh snapshot of the NEW session.
1429
+ const reWelcomeAll = () => {
1430
+ for (const c of clients)
1431
+ if (c.helloed)
1432
+ sendWelcome(c);
1433
+ };
1434
+ // -------------------------------------------------------------------------
1435
+ // The single exit helper (plan §0): dispose the engine, close + unlink the
1436
+ // socket, exit 0. Idempotent — every trigger converges here.
1437
+ // -------------------------------------------------------------------------
1438
+ const disposeAndExit = (_reason, status = 0) => {
1439
+ if (disposed)
1440
+ return;
1441
+ disposed = true;
1442
+ streamWatchdog.reset();
1443
+ // disposeActiveSession atomically clears the process-global pointer before
1444
+ // disposing — whichever engine session (committed or an in-flight rebind
1445
+ // candidate) it currently names, matching broker-cli's fatal-handler primitive.
1446
+ disposeActiveSession();
1447
+ try {
1448
+ server?.close();
1449
+ }
1450
+ catch {
1451
+ /* ignore */
1452
+ }
1453
+ // Finite and best-effort: destroy every currently tracked client socket so
1454
+ // teardown below can't race a viewer write, then drop the set. Do not wait
1455
+ // for 'close' events.
1456
+ for (const client of clients) {
1457
+ try {
1458
+ client.socket.destroy();
1459
+ }
1460
+ catch {
1461
+ /* ignore */
1462
+ }
1463
+ }
1464
+ clients.clear();
1465
+ try {
1466
+ if (existsSync(sockPath))
1467
+ unlinkSync(sockPath);
1468
+ }
1469
+ catch {
1470
+ /* ignore */
1471
+ }
1472
+ try {
1473
+ // A clean exit never leaves a stale presence claim behind.
1474
+ if (existsSync(attachPath))
1475
+ unlinkSync(attachPath);
1476
+ }
1477
+ catch {
1478
+ /* ignore */
1479
+ }
1480
+ process.exit(status);
1481
+ };
1482
+ // A stophook can request shutdown while pi is still unwinding a completed
1483
+ // turn. Let that turn reach agent_settled first: inbox delivery commits its
1484
+ // durable cursor there, and only there. Explicit operator shutdown and OS
1485
+ // signals remain abrupt through disposeAndExit.
1486
+ const requestShutdown = () => {
1487
+ if (disposed || shutdownRequested)
1488
+ return;
1489
+ if (!agentRunActive) {
1490
+ disposeAndExit('shutdown-hook');
1491
+ return;
1492
+ }
1493
+ shutdownRequested = true;
1494
+ };
1495
+ // -------------------------------------------------------------------------
1496
+ // Extension-dialog routing (C2). makeBrokerUiContext owns the dialogPromise;
1497
+ // here we just give it the three broker-side hooks it needs: the live set of
1498
+ // writable clients (empty = nobody can answer → noOp fallback), how to forward a
1499
+ // dialog to one of them, and the pending-dialog registry whichever one answers
1500
+ // first settles through. The zero-writable path NEVER hangs and NEVER waits on a
1501
+ // per-dialog timeout (design §5.4's timeout premise is false — see
1502
+ // makeBrokerUiContext).
1503
+ // -------------------------------------------------------------------------
1504
+ const uiContext = makeBrokerUiContext({
1505
+ writable: writableClients,
1506
+ forward: (client, request) => sendFrame(client, request),
1507
+ pending: pendingDialogs,
1508
+ broadcast: broadcastUi,
1509
+ providerRotationPolicy: () => cfg.modelExact === true
1510
+ ? { allowCrossProviderFallback: false, reason: 'model-exact' }
1511
+ : cfg.modelProviderPinned === true
1512
+ ? { allowCrossProviderFallback: false, reason: 'provider-pinned' }
1513
+ : { allowCrossProviderFallback: true },
1514
+ // A deliberate managed-provider cooldown wait is known liveness, not a wedged
1515
+ // stream: pause the dead-stream watchdog for its duration. Only pause for a
1516
+ // finite future deadline, and resume ONLY while the same turn is still
1517
+ // awaiting the provider (streaming, no tool running) so a wait that outlived
1518
+ // its turn can't rearm a stale session; otherwise fully reset.
1519
+ onKnownStreamWait: (deadlineMs) => {
1520
+ if (!Number.isFinite(deadlineMs) || deadlineMs <= Date.now())
1521
+ return () => { };
1522
+ const generation = installedGeneration;
1523
+ if (generation === undefined)
1524
+ return () => { };
1525
+ const waitingSession = generation.session;
1526
+ streamWatchdog.pause();
1527
+ return () => {
1528
+ if (installedGeneration !== generation || session !== waitingSession)
1529
+ return;
1530
+ releaseKnownStreamWait(streamWatchdog, generation.id, waitingSession.isStreaming, generation.executingTools.size > 0);
1531
+ };
1532
+ },
1533
+ });
1534
+ // -------------------------------------------------------------------------
1535
+ // 3+4 (plan): bind the FULL canvas extensions (mode 'rpc') AND fan the single
1536
+ // engine event stream to all clients, VERBATIM — both wrapped in one
1537
+ // ticketed rebind lane so a session-replacing op (new_session/switch_session/fork)
1538
+ // can re-run them against the NEW session. This mirrors pi's rpc-mode callback:
1539
+ // the AgentSessionRuntime tears down the old
1540
+ // session, builds the next, then calls this back; we re-bind extensions and
1541
+ // re-subscribe so the canvas hooks + the fan-out follow the live session.
1542
+ //
1543
+ // bindExtensions fires session_start → the stophook records pi_session_id/file
1544
+ // + recordPid(pid); the shutdownHandler is the precise broker-exit trigger (the
1545
+ // stophook calls ctx.shutdown() only in its done / idle-release / refresh
1546
+ // branches, NOT on reprompt). A requested in-turn shutdown exits after the
1547
+ // subscriber observes agent_settled, while every other branch stays alive. The
1548
+ // relay must not infer shutdown from raw agent_end: that would override the
1549
+ // stophook's routing decision. shutdownHandler + the inbound `shutdown` frame
1550
+ // are the only exit triggers.
1551
+ //
1552
+ // m7: the subscriber body is wrapped in try/catch — a synchronous throw out of
1553
+ // a subscriber otherwise propagates as a run failure that would crash the broker.
1554
+ // -------------------------------------------------------------------------
1555
+ let unsubscribe;
1556
+ const removeInstalledSubscription = () => {
1557
+ const installed = unsubscribe;
1558
+ installed?.();
1559
+ unsubscribe = undefined;
1560
+ installedGeneration = undefined;
1561
+ };
1562
+ let rebindTail = Promise.resolve();
1563
+ let latestRebindRequest = 0;
1564
+ const captureSystemPrompt = (candidateSession) => {
1565
+ try {
1566
+ const dir = jobDir(nodeId);
1567
+ mkdirSync(dir, { recursive: true });
1568
+ writeFileSync(join(dir, 'system-prompt.md'), candidateSession.systemPrompt, 'utf8');
1569
+ }
1570
+ catch {
1571
+ /* non-fatal: diagnostic only */
1572
+ }
1573
+ };
1574
+ const executeSubscriptionCallback = (generation, event) => {
1575
+ try {
1576
+ if (event.type === 'agent_start')
1577
+ agentRunActive = true;
1578
+ if (event.type === 'agent_end') {
1579
+ generation.stagedProviderAgentEnd = event;
1580
+ // A continuation after failed compaction owns the final settled outcome.
1581
+ generation.stagedOverflowFailure = null;
1582
+ }
1583
+ if (event.type === 'compaction_end' &&
1584
+ event.reason === 'overflow' &&
1585
+ event.willRetry === false &&
1586
+ typeof event.errorMessage === 'string') {
1587
+ generation.stagedOverflowFailure = { errorMessage: event.errorMessage };
1588
+ }
1589
+ if (event.type === 'agent_settled') {
1590
+ agentRunActive = false;
1591
+ publishSettledProviderFault(generation);
1592
+ if (shutdownRequested) {
1593
+ disposeAndExit('shutdown-after-settle');
1594
+ return;
1595
+ }
1596
+ }
1597
+ relayEvent(event, generation);
1598
+ if (event.type === 'agent_settled')
1599
+ scheduleFaultRetry();
1600
+ }
1601
+ catch (err) {
1602
+ emitEvent({
1603
+ level: 'debug',
1604
+ event: 'broker.runtime.diagnostic',
1605
+ fields: { message: `event relay threw: ${String(err)}` },
1606
+ });
1607
+ }
1608
+ };
1609
+ const enqueueRebind = (candidateSession, candidateServices) => {
1610
+ const ticket = ++latestRebindRequest;
1611
+ const task = rebindTail.then(async () => {
1612
+ if (ticket !== latestRebindRequest)
1613
+ return;
1614
+ installedGeneration?.executingTools.clear();
1615
+ removeInstalledSubscription();
1616
+ // This candidate is now the engine session a mid-rebind fatal cleanup must
1617
+ // dispose — set before the awaited bindExtensions so a timeout/fatal during
1618
+ // that await disposes the candidate rather than the stale committed session.
1619
+ // Shared session/services ownership still only commits below, post-await.
1620
+ activeSession = candidateSession;
1621
+ streamWatchdog.reset();
1622
+ agentRunActive = false;
1623
+ if (pendingUpdateTimer !== null) {
1624
+ clearTimeout(pendingUpdateTimer);
1625
+ pendingUpdateTimer = null;
1626
+ }
1627
+ pendingUpdate = null;
1628
+ clearFaultRetryTimer();
1629
+ providerFallbackInFlight = null;
1630
+ notFoundFallbackInFlight = null;
1631
+ reloadAuthInFlight = null;
1632
+ await candidateSession.bindExtensions({
1633
+ uiContext,
1634
+ // 'rpc' — NOT 'print'. Per pi docs/rpc.md:1078 this is exactly our shape:
1635
+ // a headless engine whose UI methods are functional through a remote client
1636
+ // (our uiContext relays them as `display_*` / `extension_ui_request` frames).
1637
+ // 'print' would tell every extension `ctx.mode === 'print'` — pi's own label
1638
+ // for "extensions run but can't prompt" — while we hand them a uiContext that
1639
+ // can. (`ctx.hasUI` is derived from uiContext identity, not from mode, so it
1640
+ // was already true; this fixes the mode label the extensions actually read.)
1641
+ // The `ctx.mode === 'tui'` guards in canvas-view/-resume/-commands stay dead.
1642
+ mode: 'rpc',
1643
+ shutdownHandler: requestShutdown,
1644
+ });
1645
+ if (ticket !== latestRebindRequest)
1646
+ return;
1647
+ removeInstalledSubscription();
1648
+ session = candidateSession;
1649
+ services = candidateServices;
1650
+ if (cfg.editorName !== undefined && cfg.editorName !== '') {
1651
+ try {
1652
+ candidateSession.setSessionName(cfg.editorName);
1653
+ }
1654
+ catch {
1655
+ /* non-fatal: naming is cosmetic */
1656
+ }
1657
+ }
1658
+ const generation = {
1659
+ id: ++nextSubscriptionGeneration,
1660
+ session: candidateSession,
1661
+ fallback: { blocked: false },
1662
+ stagedProviderAgentEnd: null,
1663
+ stagedOverflowFailure: null,
1664
+ executingTools: new Set(),
1665
+ };
1666
+ unsubscribe = session.subscribe((event) => {
1667
+ if (installedGeneration !== generation)
1668
+ return;
1669
+ const ambientId = operationIdContext.current();
1670
+ if (ambientId !== undefined) {
1671
+ executeSubscriptionCallback(generation, event);
1672
+ return;
1673
+ }
1674
+ const fallback = generation.fallback;
1675
+ if (event.type === 'turn_start' && fallback.storedId !== undefined) {
1676
+ fallback.storedId = undefined;
1677
+ fallback.blocked = true;
1678
+ operationIdContext.fresh(() => {
1679
+ emitEvent({
1680
+ level: 'warn',
1681
+ event: 'broker.turn.fallback_overlap',
1682
+ fields: { generation: generation.id, engine_event: event.type },
1683
+ });
1684
+ executeSubscriptionCallback(generation, event);
1685
+ });
1686
+ return;
1687
+ }
1688
+ if (event.type === 'turn_start' && !fallback.blocked) {
1689
+ operationIdContext.fresh((operationId) => {
1690
+ fallback.storedId = operationId;
1691
+ executeSubscriptionCallback(generation, event);
1692
+ });
1693
+ return;
1694
+ }
1695
+ const storedId = fallback.storedId;
1696
+ if (storedId !== undefined && !fallback.blocked) {
1697
+ operationIdContext.run(storedId, () => {
1698
+ executeSubscriptionCallback(generation, event);
1699
+ if (event.type === 'agent_settled' && fallback.storedId === storedId) {
1700
+ fallback.storedId = undefined;
1701
+ }
1702
+ });
1703
+ return;
1704
+ }
1705
+ executeSubscriptionCallback(generation, event);
1706
+ });
1707
+ installedGeneration = generation;
1708
+ captureSystemPrompt(candidateSession);
1709
+ });
1710
+ rebindTail = task.catch(() => undefined);
1711
+ return task;
1712
+ };
1713
+ // Register the rebind BEFORE the first bind so a replacement triggered during
1714
+ // startup is honored; then use the same ticketed lane for the initial install.
1715
+ runtime?.setRebindSession((candidateSession) => enqueueRebind(candidateSession, runtime.services));
1716
+ await enqueueRebind(session, services);
1717
+ // -------------------------------------------------------------------------
1718
+ // Drive the engine on behalf of a writable client.
1719
+ // -------------------------------------------------------------------------
1720
+ // ---------------------------------------------------------------------------
1721
+ // Inline memory-reference inventory (design: inline memory references) — a
1722
+ // single broker-level memo shared by BOTH the `list_memory_refs` read-op
1723
+ // reply and per-submission guidance resolution below, so a burst of reads +
1724
+ // submissions between reloads never re-walks the corpus per call. Cleared
1725
+ // (work item 3) only on a SUCCESSFUL `session.reload()` — a failed reload
1726
+ // leaves the prior memo intact, since nothing about the corpus is known to
1727
+ // have changed.
1728
+ // ---------------------------------------------------------------------------
1729
+ let memoizedRefInventory = null;
1730
+ const getRefInventory = () => {
1731
+ if (memoizedRefInventory === null)
1732
+ memoizedRefInventory = buildRefInventory();
1733
+ return memoizedRefInventory;
1734
+ };
1735
+ const buildListMemoryRefsData = (id) => ({
1736
+ type: 'data',
1737
+ id,
1738
+ kind: 'list_memory_refs',
1739
+ refs: getRefInventory().refs,
1740
+ engineLeadingCommandNames: engineLeadingCommandTokens(session),
1741
+ });
1742
+ /** Resolve the hidden guidance text for a human frame's verbatim text, or
1743
+ * `null` when no guidance should be sent (a leading engine command/skill/
1744
+ * template, no resolvable refs, or a degraded inventory/guidance build —
1745
+ * work item 6: any failure here degrades to "no guidance", logged once as a
1746
+ * structured warning, and NEVER blocks/alters the human delivery below). */
1747
+ const resolveFrameGuidance = (text) => {
1748
+ try {
1749
+ if (isLeadingEngineCommand(text, session))
1750
+ return null;
1751
+ return buildGuidance(text, getRefInventory().names);
1752
+ }
1753
+ catch (err) {
1754
+ emitEvent({
1755
+ level: 'warn',
1756
+ event: 'broker.memory_refs.guidance_resolve_failed',
1757
+ error: err,
1758
+ });
1759
+ return null;
1760
+ }
1761
+ };
1762
+ // Prompt dispatches whose promise has not yet settled. `session.prompt()`
1763
+ // does async setup (input transforms, model/auth validation, compaction)
1764
+ // BEFORE `_isAgentRunActive` flips, so for a short window after a routed
1765
+ // deliver the engine reads idle and `session.abort()` is a silent no-op — an
1766
+ // id-bearing abort racing that window would miss the turn entirely (verified
1767
+ // live). The counter lets it distinguish "genuinely idle" from "turn still
1768
+ // igniting" and wait for the run to become abortable.
1769
+ let pendingPromptDispatches = 0;
1770
+ const trackPromptDispatch = (p) => {
1771
+ pendingPromptDispatches++;
1772
+ const tracked = Promise.resolve(p);
1773
+ void tracked.finally(() => {
1774
+ pendingPromptDispatches--;
1775
+ });
1776
+ return tracked;
1777
+ };
1778
+ /**
1779
+ * Drive the engine for one client frame. For a prompt/follow_up frame this
1780
+ * returns a promise that settles with the engine call's OUTCOME — `undefined`
1781
+ * when the call succeeded, the rejection value when it did not — so a caller
1782
+ * that must report success (the `deliver` ack) can read it instead of assuming
1783
+ * the dispatch worked. Every other frame type keeps the fire-and-forget shape
1784
+ * and returns `undefined`.
1785
+ */
1786
+ const driveEngine = (client, frame) => {
1787
+ // The engine call's failure, captured for the returned outcome. relayError
1788
+ // still relays the error frame; recording it is what lets the caller ack
1789
+ // from the outcome rather than from the fact that a call was made at all.
1790
+ let failure;
1791
+ const relayError = (err) => {
1792
+ failure = err;
1793
+ sendFrame(client, { type: 'error', code: 'engine_error', message: String(err) });
1794
+ };
1795
+ // Hidden `crtr-memory-refs` guidance rides ALONGSIDE the human frame's own
1796
+ // engine call, never rewriting `frame.text` — see resolveFrameGuidance /
1797
+ // chooseGuidanceDeliveryMode above. A failed SEND (as opposed to a failed
1798
+ // resolve, handled above) is likewise logged-and-swallowed: it must never
1799
+ // reach `relayError`, which is reserved for the human call's own outcome.
1800
+ const sendGuidance = (guidance, deliverAs) => session
1801
+ .sendCustomMessage({ customType: 'crtr-memory-refs', content: guidance, display: false }, { deliverAs })
1802
+ .catch((err) => {
1803
+ emitEvent({
1804
+ level: 'warn',
1805
+ event: 'broker.memory_refs.guidance_send_failed',
1806
+ error: err,
1807
+ });
1808
+ });
1809
+ switch (frame.type) {
1810
+ case 'prompt':
1811
+ case 'follow_up': {
1812
+ // Settle on the engine call's outcome: `failure` is set by relayError
1813
+ // before the caught promise resolves, so reading it after the chain is
1814
+ // the call's verdict.
1815
+ const settle = (p) => p.then(() => failure);
1816
+ const route = resolveEngineRoute(frame, session.isStreaming);
1817
+ const guidance = resolveFrameGuidance(frame.text);
1818
+ if (guidance === null) {
1819
+ if (route.call === 'prompt') {
1820
+ // A routed prompt STARTS a turn — including an idle follow_up
1821
+ // escalated by m-C — so it gets a fresh operation id. A true
1822
+ // (mid-stream) followUp rides the current turn's operation, like
1823
+ // steer.
1824
+ return settle(trackPromptDispatch(operationIdContext.fresh(() => session.prompt(frame.text, route.options).catch(relayError))));
1825
+ }
1826
+ // C1: pi's followUp() takes images as a POSITIONAL 2nd arg (`followUp(text, images?)`).
1827
+ return settle(session.followUp(frame.text, frame.images).catch(relayError));
1828
+ }
1829
+ const deliverAs = chooseGuidanceDeliveryMode(frame.type, route.call, session.isStreaming);
1830
+ if (route.call === 'prompt') {
1831
+ if (deliverAs === 'nextTurn') {
1832
+ // The ONLY ordering where guidance precedes the human call — pi's
1833
+ // prompt() reads+clears its pending-nextTurn queue synchronously
1834
+ // at call time, injecting it into the SAME turn as the new user
1835
+ // message. promptWithNextTurnGuidance queues the guidance and then
1836
+ // ATOMICALLY clears its own pending entry if prompt() settles
1837
+ // without ever reaching that drain (an early throw/return leaves
1838
+ // nothing stranded to leak into a later, unrelated turn).
1839
+ return settle(trackPromptDispatch(promptWithNextTurnGuidance(session, guidance, () => operationIdContext.fresh(() => session.prompt(frame.text, route.options))).catch(relayError)));
1840
+ }
1841
+ return settle(trackPromptDispatch(operationIdContext.fresh(() => session.prompt(frame.text, route.options).catch(relayError).then(() => sendGuidance(guidance, deliverAs)))));
1842
+ }
1843
+ // deliverAs is always 'followUp' here per chooseGuidanceDeliveryMode's
1844
+ // contract (a followUp routeCall never yields 'nextTurn').
1845
+ return settle(session.followUp(frame.text, frame.images).catch(relayError).then(() => sendGuidance(guidance, deliverAs)));
1846
+ }
1847
+ case 'steer': {
1848
+ const guidance = resolveFrameGuidance(frame.text);
1849
+ // C1: pi's steer() takes images as a POSITIONAL 2nd arg (`steer(text, images?)`).
1850
+ if (guidance === null) {
1851
+ void session.steer(frame.text, frame.images).catch(relayError);
1852
+ break;
1853
+ }
1854
+ void session.steer(frame.text, frame.images).catch(relayError).then(() => sendGuidance(guidance, 'steer'));
1855
+ break;
1856
+ }
1857
+ case 'abort': {
1858
+ // esc/abort cancels whatever is live: a running `!` command (abortBash)
1859
+ // takes precedence over the agent turn, mirroring pi interactive mode.
1860
+ if (session.isBashRunning) {
1861
+ session.abortBash();
1862
+ if (frame.id !== undefined)
1863
+ ackTo(client, 'abort', true, 'bash', frame.id);
1864
+ break;
1865
+ }
1866
+ if (frame.id === undefined) {
1867
+ // A viewer's Esc — unchanged instant semantics, no ack.
1868
+ void session.abort().catch(relayError);
1869
+ break;
1870
+ }
1871
+ // An id-bearing abort (a one-shot daemon interrupt) must close the
1872
+ // pre-turn window: a routed deliver whose prompt() is still in async
1873
+ // setup reads as idle and abort() would silently miss it. If a prompt
1874
+ // dispatch is pending, wait (bounded) for the run to become abortable
1875
+ // — or for every dispatch to settle without one (an early return) —
1876
+ // then abort and ack with what was actually interrupted.
1877
+ void (async () => {
1878
+ try {
1879
+ if (!session.isStreaming && pendingPromptDispatches > 0) {
1880
+ const deadline = Date.now() + 5_000;
1881
+ while (Date.now() < deadline && !session.isStreaming && pendingPromptDispatches > 0) {
1882
+ await new Promise((r) => setTimeout(r, 25));
1883
+ }
1884
+ }
1885
+ const detail = session.isStreaming ? 'turn' : 'idle';
1886
+ await session.abort();
1887
+ ackTo(client, 'abort', true, detail, frame.id);
1888
+ }
1889
+ catch (err) {
1890
+ engineErrorTo(client, frame.id)(err);
1891
+ }
1892
+ })();
1893
+ break;
1894
+ }
1895
+ case 'bash': {
1896
+ // `!`/`!!` bash: run via session.executeBash (records a bashExecution
1897
+ // context message, starts NO agent turn) and relay its lifecycle to ALL
1898
+ // viewers as bash_start/bash_output/bash_end so each renders the live
1899
+ // component. Serialized like pi's interactive mode — reject a second `!`
1900
+ // while one is already running rather than racing _bashAbortController.
1901
+ const command = frame.command;
1902
+ if (command.trim() === '')
1903
+ break;
1904
+ if (session.isBashRunning) {
1905
+ sendFrame(client, {
1906
+ type: 'error',
1907
+ code: 'bash_busy',
1908
+ message: 'A bash command is already running',
1909
+ });
1910
+ break;
1911
+ }
1912
+ const exclude = frame.excludeFromContext === true;
1913
+ // Order before any held assistant message_update, so a `!` run mid-stream
1914
+ // never renders ahead of the update it follows.
1915
+ flushPendingUpdate();
1916
+ broadcast({ type: 'bash_start', command, excludeFromContext: exclude });
1917
+ void session
1918
+ .executeBash(command, (chunk) => broadcast({ type: 'bash_output', chunk }), {
1919
+ excludeFromContext: exclude,
1920
+ })
1921
+ .then((result) => {
1922
+ broadcast({
1923
+ type: 'bash_end',
1924
+ exitCode: result.exitCode,
1925
+ cancelled: result.cancelled,
1926
+ truncated: result.truncated,
1927
+ fullOutputPath: result.fullOutputPath,
1928
+ });
1929
+ })
1930
+ .catch(relayError);
1931
+ break;
1932
+ }
1933
+ }
1934
+ };
1935
+ // -------------------------------------------------------------------------
1936
+ // Command-op helpers (T3, §2.3). The write guard is hoisted here (it was
1937
+ // inlined twice) and reused by all mutating ops; the ack/error replies
1938
+ // and a few resolvers keep the per-op cases one-liners.
1939
+ // -------------------------------------------------------------------------
1940
+ /** Reject a read-only client for a mutating op. The decision reads this
1941
+ * client's OWN fixed role — there is no shared writer slot, so any number of
1942
+ * writable clients pass concurrently. Returns true when rejected (the caller
1943
+ * should `break`). */
1944
+ const notWritable = (client, what, id) => {
1945
+ if (client.role === 'controller')
1946
+ return false;
1947
+ sendFrame(client, {
1948
+ type: 'error',
1949
+ code: 'read_only',
1950
+ message: `a read-only client may not ${what}`,
1951
+ // M1: echo a correlated request's id so its pending-by-id promise rejects
1952
+ // rather than hanging (e.g. a read-only `dequeue`).
1953
+ ...(id !== undefined ? { id } : {}),
1954
+ });
1955
+ return true;
1956
+ };
1957
+ const ackTo = (client, op, ok = true, detail, id) => sendFrame(client, {
1958
+ type: 'ack',
1959
+ for: op,
1960
+ ok,
1961
+ ...(detail !== undefined ? { detail } : {}),
1962
+ ...(id !== undefined ? { id } : {}),
1963
+ });
1964
+ const engineErrorTo = (client, id) => (err) => sendFrame(client, {
1965
+ type: 'error',
1966
+ code: 'engine_error',
1967
+ message: String(err),
1968
+ // M1: a correlated read-op / dequeue failure echoes its id so the viewer's
1969
+ // pending-by-id promise rejects instead of hanging; uncorrelated callers
1970
+ // pass no id (unchanged behavior).
1971
+ ...(id !== undefined ? { id } : {}),
1972
+ });
1973
+ /** Resolve a `provider/id` model spec against the LIVE services registry (which
1974
+ * carries any extension-registered providers). Pure: returns undefined on a
1975
+ * malformed spec or an unknown model. */
1976
+ const findModelSpec = (spec) => {
1977
+ const { modelSpec } = parseModelSpec(spec);
1978
+ const slash = modelSpec.indexOf('/');
1979
+ if (slash <= 0)
1980
+ return undefined;
1981
+ return registryOf(services).find(modelSpec.slice(0, slash), modelSpec.slice(slash + 1));
1982
+ };
1983
+ /** Resolve a model from a free-text query — `/model opus`, `/model fable`. Tries
1984
+ * the exact `provider/id` spec first; failing that, searches the registry the
1985
+ * way the picker's search box does (case-insensitive substring on the model id
1986
+ * and the `provider/id` key) and returns the BEST match, preferring authed/
1987
+ * available models. Returns undefined only when nothing matches. */
1988
+ const resolveModelQuery = (query) => {
1989
+ const exact = findModelSpec(query);
1990
+ if (exact)
1991
+ return exact;
1992
+ const q = query.trim().toLowerCase();
1993
+ if (q === '')
1994
+ return undefined;
1995
+ let all;
1996
+ try {
1997
+ all = registryOf(services).getAll();
1998
+ }
1999
+ catch {
2000
+ return undefined;
2001
+ }
2002
+ let availableKeys;
2003
+ try {
2004
+ availableKeys = new Set(registryOf(services).getAvailable().map(modelKey));
2005
+ }
2006
+ catch {
2007
+ availableKeys = new Set();
2008
+ }
2009
+ // Rank: exact id (100) > exact provider/id (90) > id prefix (80) > id
2010
+ // substring (60) > provider/id substring (40); +5 if the model is available.
2011
+ const rank = (m) => {
2012
+ const id = m.id.toLowerCase();
2013
+ const key = modelKey(m).toLowerCase();
2014
+ let base = -1;
2015
+ if (id === q)
2016
+ base = 100;
2017
+ else if (key === q)
2018
+ base = 90;
2019
+ else if (id.startsWith(q))
2020
+ base = 80;
2021
+ else if (id.includes(q))
2022
+ base = 60;
2023
+ else if (key.includes(q))
2024
+ base = 40;
2025
+ else
2026
+ return -1;
2027
+ return base + (availableKeys.has(modelKey(m)) ? 5 : 0);
2028
+ };
2029
+ let best;
2030
+ let bestRank = 0;
2031
+ for (const m of all) {
2032
+ const r = rank(m);
2033
+ if (r > bestRank) {
2034
+ bestRank = r;
2035
+ best = m;
2036
+ }
2037
+ }
2038
+ return best;
2039
+ };
2040
+ const buildCommandList = () => {
2041
+ const out = [];
2042
+ for (const b of BUILTIN_SLASH_COMMANDS) {
2043
+ out.push({ name: b.name, description: b.description, source: 'builtin' });
2044
+ }
2045
+ try {
2046
+ for (const c of session.extensionRunner.getRegisteredCommands()) {
2047
+ const expansion = c.expansion;
2048
+ out.push({
2049
+ name: c.invocationName,
2050
+ description: c.description ?? '',
2051
+ source: 'command',
2052
+ ...(isDeterministicCommandExpansion(expansion) ? { expansion } : {}),
2053
+ });
2054
+ }
2055
+ }
2056
+ catch {
2057
+ /* an engine without a live extensionRunner (e.g. the fake) — builtins only */
2058
+ }
2059
+ try {
2060
+ for (const t of session.promptTemplates) {
2061
+ out.push({ name: t.name, description: t.description, source: 'template', template: t.content });
2062
+ }
2063
+ }
2064
+ catch {
2065
+ /* ignore */
2066
+ }
2067
+ return out;
2068
+ };
2069
+ // -------------------------------------------------------------------------
2070
+ // Read-op data builders (operator-view picker payloads, §5 Unit A). Each is a
2071
+ // PURE getter read against the live engine session — the data a native pi
2072
+ // picker's constructor needs, serialized for the viewer. Not role-gated
2073
+ // (read-only, like get_commands), so the web bridge's observer connection can
2074
+ // populate pickers too.
2075
+ // -------------------------------------------------------------------------
2076
+ // A model-less session's model is EITHER undefined OR the SDK unknown-stub;
2077
+ // both map to the WireModelRef no-model value (null), so list_models.current
2078
+ // and get_settings.settings.model never disclose a fictitious `unknown/unknown`.
2079
+ const toModelRef = (m) => m === undefined || isUnknownModel(m) ? null : { provider: m.provider, id: m.id };
2080
+ const modelKey = (m) => `${m.provider}/${m.id}`;
2081
+ const buildListModelsData = (id) => {
2082
+ const reg = registryOf(session);
2083
+ let availableIds = [];
2084
+ try {
2085
+ availableIds = reg.getAvailable().map(modelKey);
2086
+ }
2087
+ catch {
2088
+ /* getAvailable touches auth config; degrade to "none known available" */
2089
+ }
2090
+ return {
2091
+ type: 'data',
2092
+ id,
2093
+ kind: 'list_models',
2094
+ models: reg.getAll(),
2095
+ current: toModelRef(session.model),
2096
+ availableIds,
2097
+ scopedModels: session.scopedModels.map((s) => ({
2098
+ model: s.model,
2099
+ thinkingLevel: s.thinkingLevel,
2100
+ })),
2101
+ enabledModelIds: session.settingsManager.getEnabledModels() ?? null,
2102
+ };
2103
+ };
2104
+ const buildScopedModelsData = (id) => ({
2105
+ type: 'data',
2106
+ id,
2107
+ kind: 'list_scoped_models',
2108
+ allModels: registryOf(session).getAll(),
2109
+ enabledModelIds: session.settingsManager.getEnabledModels() ?? null,
2110
+ });
2111
+ const buildGetTreeData = (id) => {
2112
+ const sm = session.sessionManager;
2113
+ const forkPoints = session.getUserMessagesForForking().map((u) => ({
2114
+ id: u.entryId,
2115
+ text: u.text,
2116
+ timestamp: sm.getEntry(u.entryId)?.timestamp,
2117
+ }));
2118
+ return {
2119
+ type: 'data',
2120
+ id,
2121
+ kind: 'get_tree',
2122
+ tree: sm.getTree(),
2123
+ currentLeafId: sm.getLeafId(),
2124
+ forkPoints,
2125
+ };
2126
+ };
2127
+ const buildSettingsData = (id) => {
2128
+ const sm = session.settingsManager;
2129
+ // Theme names are enumerable broker-side via the resource loader. The theme
2130
+ // SUBMENU itself is viewer-local (theme is a viewer-only concern), but the
2131
+ // settings menu still shows currentTheme/availableThemes, so include them.
2132
+ let availableThemes = [];
2133
+ try {
2134
+ availableThemes = session.resourceLoader
2135
+ .getThemes()
2136
+ .themes.map((t) => t.name ?? '')
2137
+ .filter((n) => n !== '');
2138
+ }
2139
+ catch {
2140
+ /* loader without themes — viewer falls back to its local theme registry */
2141
+ }
2142
+ const settings = {
2143
+ autoCompact: session.autoCompactionEnabled,
2144
+ showImages: sm.getShowImages(),
2145
+ imageWidthCells: sm.getImageWidthCells(),
2146
+ autoResizeImages: sm.getImageAutoResize(),
2147
+ blockImages: sm.getBlockImages(),
2148
+ enableSkillCommands: sm.getEnableSkillCommands(),
2149
+ steeringMode: session.steeringMode,
2150
+ followUpMode: session.followUpMode,
2151
+ transport: sm.getTransport(),
2152
+ httpIdleTimeoutMs: sm.getHttpIdleTimeoutMs(),
2153
+ thinkingLevel: session.thinkingLevel,
2154
+ availableThinkingLevels: session.getAvailableThinkingLevels(),
2155
+ currentTheme: sm.getTheme() ?? '',
2156
+ // Light/dark appearance is a property of the terminal the VIEWER is attached
2157
+ // to; the headless broker has none, so emit a placeholder. The viewer owns
2158
+ // the theme submenu and resolves real terminal appearance locally.
2159
+ terminalTheme: 'dark',
2160
+ availableThemes,
2161
+ hideThinkingBlock: sm.getHideThinkingBlock(),
2162
+ collapseChangelog: sm.getCollapseChangelog(),
2163
+ enableInstallTelemetry: sm.getEnableInstallTelemetry(),
2164
+ doubleEscapeAction: sm.getDoubleEscapeAction(),
2165
+ treeFilterMode: sm.getTreeFilterMode(),
2166
+ showHardwareCursor: sm.getShowHardwareCursor(),
2167
+ editorPaddingX: sm.getEditorPaddingX(),
2168
+ autocompleteMaxVisible: sm.getAutocompleteMaxVisible(),
2169
+ quietStartup: sm.getQuietStartup(),
2170
+ clearOnShrink: sm.getClearOnShrink(),
2171
+ showTerminalProgress: sm.getShowTerminalProgress(),
2172
+ warnings: sm.getWarnings(),
2173
+ defaultProjectTrust: sm.getDefaultProjectTrust(),
2174
+ showCacheMissNotices: sm.getShowCacheMissNotices(),
2175
+ outputPad: sm.getOutputPad(),
2176
+ autoRetry: session.autoRetryEnabled,
2177
+ model: toModelRef(session.model),
2178
+ };
2179
+ return { type: 'data', id, kind: 'get_settings', settings };
2180
+ };
2181
+ // Cache per-session wire info keyed by (path, mtime, size) so reopening the
2182
+ // picker doesn't re-read the whole corpus (see session-list-cache.ts).
2183
+ const sessionListCache = new SessionListCache(sessionListCachePath());
2184
+ // Pre-warm the cwd scope (the picker's default tab) in the background so the
2185
+ // first /resume open on this fresh broker is already warm.
2186
+ sessionListCache.prewarm(session.sessionManager.getSessionDir());
2187
+ const buildSessionsData = async (id, scope) => {
2188
+ const sm = session.sessionManager;
2189
+ const sessionDir = sm.getSessionDir();
2190
+ const sessions = scope === 'all'
2191
+ ? await sessionListCache.listAll(piSessionsRoot())
2192
+ : await sessionListCache.listDir(sessionDir);
2193
+ return {
2194
+ type: 'data',
2195
+ id,
2196
+ kind: 'list_sessions',
2197
+ scope,
2198
+ sessions,
2199
+ currentSessionFile: session.sessionFile,
2200
+ };
2201
+ };
2202
+ /** Runtime-mutating viewer replacements are serialized through their complete
2203
+ * rebind, re-welcome, and client settlement. Admission is bounded to at most
2204
+ * one active head plus one queued follow-up (`MAX_ADMITTED_REPLACEMENTS`); a
2205
+ * request over that cap is a definite, correlated rejection that never touches
2206
+ * the tail. Each admitted head gets one `REPLACEMENT_DEADLINE_MS` budget
2207
+ * covering the full runtime mutation through its awaited rebind. The SDK has
2208
+ * no cancellation primitive for these mutations, so a rejection or an expired
2209
+ * deadline after admission leaves runtime/shared state indeterminate — the
2210
+ * only truthful response is broker-terminal exit, never a retry, and the tail
2211
+ * never advances to the queued closure against that uncertain state. */
2212
+ let replacementTail = Promise.resolve();
2213
+ let admittedReplacements = 0;
2214
+ const runReplacement = (client, op, run) => {
2215
+ if (runtime === undefined) {
2216
+ sendFrame(client, {
2217
+ type: 'error',
2218
+ code: 'engine_error',
2219
+ message: 'session replacement unsupported at this engine pin',
2220
+ });
2221
+ return;
2222
+ }
2223
+ if (admittedReplacements >= MAX_ADMITTED_REPLACEMENTS) {
2224
+ operationIdContext.fresh(() => {
2225
+ emitEvent({
2226
+ level: 'warn',
2227
+ event: 'broker.session_replacement.rejected',
2228
+ outcome: 'skipped',
2229
+ fields: {
2230
+ op,
2231
+ reason: 'capacity',
2232
+ admitted: MAX_ADMITTED_REPLACEMENTS,
2233
+ cap: MAX_ADMITTED_REPLACEMENTS,
2234
+ },
2235
+ });
2236
+ });
2237
+ ackTo(client, op, false, `session replacement busy (${MAX_ADMITTED_REPLACEMENTS} active or queued)`);
2238
+ return;
2239
+ }
2240
+ admittedReplacements++;
2241
+ const task = replacementTail.then(async () => {
2242
+ try {
2243
+ // The whole attempt — success, cancellation, rejection, and timeout —
2244
+ // executes inside one fresh operation scope so its outcome event (of
2245
+ // whichever kind fires) carries one coherent operation id.
2246
+ await operationIdContext.fresh(async () => {
2247
+ const startedAt = process.hrtime.bigint();
2248
+ let timer;
2249
+ try {
2250
+ const deadline = new Promise((resolve) => {
2251
+ timer = setTimeout(() => resolve(REPLACEMENT_DEADLINE_EXCEEDED), REPLACEMENT_DEADLINE_MS);
2252
+ });
2253
+ const outcome = await Promise.race([run(runtime), deadline]);
2254
+ if (outcome === REPLACEMENT_DEADLINE_EXCEEDED) {
2255
+ emitEvent({
2256
+ level: 'error',
2257
+ event: 'broker.session_replacement.timed_out',
2258
+ outcome: 'failed',
2259
+ duration_ms: Number(process.hrtime.bigint() - startedAt) / 1_000_000,
2260
+ fields: { op, deadline_ms: REPLACEMENT_DEADLINE_MS, mutation_outcome: 'indeterminate' },
2261
+ });
2262
+ ackTo(client, op, false, 'session replacement timed out; broker restarting because session state is indeterminate');
2263
+ disposeAndExit('session-replacement-timeout', 1);
2264
+ return;
2265
+ }
2266
+ if (!outcome.cancelled)
2267
+ reWelcomeAll();
2268
+ ackTo(client, op, !outcome.cancelled, outcome.cancelled ? outcome.detail ?? 'cancelled by extension' : undefined);
2269
+ }
2270
+ catch (err) {
2271
+ emitEvent({
2272
+ level: 'error',
2273
+ event: 'broker.session_replacement.failed',
2274
+ outcome: 'failed',
2275
+ error: err,
2276
+ fields: { op, mutation_outcome: 'indeterminate' },
2277
+ });
2278
+ ackTo(client, op, false, 'session replacement failed; broker restarting because session state is indeterminate');
2279
+ disposeAndExit('session-replacement-failed', 1);
2280
+ }
2281
+ finally {
2282
+ clearTimeout(timer);
2283
+ }
2284
+ });
2285
+ }
2286
+ finally {
2287
+ admittedReplacements--;
2288
+ }
2289
+ });
2290
+ replacementTail = task.catch(() => undefined);
2291
+ };
2292
+ const handleFrame = (client, frame) => {
2293
+ switch (frame.type) {
2294
+ case 'hello': {
2295
+ // Admit at the REQUESTED role — unconditionally. `controller` is a
2296
+ // per-client write capability, not a singleton slot, so an arriving
2297
+ // writable client never demotes (or is demoted by) an already-attached
2298
+ // one: N terminal panes and browser tabs are all writable at once.
2299
+ //
2300
+ // A repeated hello on an already-helloed socket must not RE-role it: the
2301
+ // role is fixed for the socket's lifetime, so every downstream gate and
2302
+ // every already-fanned-out dialog keep the authority the first welcome
2303
+ // stated. A duplicate hello still re-sends the catch-up snapshot.
2304
+ const firstHello = !client.helloed;
2305
+ client.id = frame.client_id;
2306
+ if (firstHello) {
2307
+ client.helloed = true;
2308
+ client.role = frame.role === 'controller' ? 'controller' : 'observer';
2309
+ }
2310
+ sendWelcome(client);
2311
+ if (firstHello)
2312
+ persistAttachState(); // a helloed viewer arrived
2313
+ // welcome carried the FIRST pending dialog; replay any extras so a writable
2314
+ // client attaching mid-dialog can answer every in-flight dialog.
2315
+ if (client.role === 'controller')
2316
+ replayExtraPendingDialogsTo(client);
2317
+ break;
2318
+ }
2319
+ case 'prompt':
2320
+ case 'steer':
2321
+ case 'follow_up':
2322
+ case 'abort':
2323
+ case 'bash': {
2324
+ if (notWritable(client, 'drive the engine'))
2325
+ break;
2326
+ driveEngine(client, frame);
2327
+ break;
2328
+ }
2329
+ case 'deliver': {
2330
+ // Interactive delivery from a one-shot server client (crtrd's
2331
+ // `POST /messages` `delivery:'interactive'` path). Routed HERE,
2332
+ // broker-side, because only the broker authoritatively knows streaming
2333
+ // state: the synthesized `prompt` frame reuses resolveEngineRoute's m-B
2334
+ // routing (idle → prompt(); mid-stream → prompt({streamingBehavior:
2335
+ // 'steer'}), pi's own Enter-while-streaming submit) plus the memory-refs
2336
+ // guidance path — byte-identical behavior to a viewer's Enter. Acked
2337
+ // once ACCEPTED (correlated by the frame's id) so the daemon returns
2338
+ // deterministically; `detail` reports which route was taken.
2339
+ //
2340
+ // The ack reports the engine call's OUTCOME, never merely the fact that
2341
+ // a call was made: `session.prompt()` rejects outright when the model's
2342
+ // provider has no credential, and acking unconditionally reported
2343
+ // `delivered:true` for a message the engine had refused — the caller's
2344
+ // one-shot client then closed before the error frame arrived and the
2345
+ // message vanished with no trace. A prompt route therefore acks on the
2346
+ // race between acceptance (agent_start) and rejection; a steer route
2347
+ // joins a turn already running, so no new agent_start is coming and its
2348
+ // ack stays at routing time.
2349
+ if (notWritable(client, 'drive the engine', frame.id))
2350
+ break;
2351
+ const via = session.isStreaming ? 'steer' : 'prompt';
2352
+ let delivered = false;
2353
+ const ackAccepted = () => {
2354
+ if (delivered)
2355
+ return;
2356
+ delivered = true;
2357
+ ackTo(client, 'deliver', true, via, frame.id);
2358
+ };
2359
+ if (via === 'prompt')
2360
+ turnAcceptedWaiters.add(ackAccepted);
2361
+ const outcome = driveEngine(client, {
2362
+ type: 'prompt',
2363
+ text: frame.text,
2364
+ ...(frame.images !== undefined ? { images: frame.images } : {}),
2365
+ });
2366
+ if (via === 'steer' || outcome === undefined) {
2367
+ turnAcceptedWaiters.delete(ackAccepted);
2368
+ ackAccepted();
2369
+ break;
2370
+ }
2371
+ void outcome.then((failure) => {
2372
+ turnAcceptedWaiters.delete(ackAccepted);
2373
+ if (failure === undefined) {
2374
+ ackAccepted();
2375
+ return;
2376
+ }
2377
+ // The error frame already went to the client, but a one-shot deliver
2378
+ // client is gone by then — record it where it survives.
2379
+ emitEvent({ level: 'error', event: 'broker.prompt.deliver_failed', error: failure });
2380
+ if (delivered)
2381
+ return;
2382
+ delivered = true;
2383
+ ackTo(client, 'deliver', false, String(failure), frame.id);
2384
+ });
2385
+ break;
2386
+ }
2387
+ case 'extension_ui_response': {
2388
+ if (notWritable(client, 'answer dialogs'))
2389
+ break;
2390
+ // First response for this id wins: `resolve` settles the entry and deletes
2391
+ // it, so every later response from a peer that was also fanned this dialog
2392
+ // finds nothing here and is a silent no-op.
2393
+ pendingDialogs.get(frame.id)?.resolve(frame);
2394
+ break;
2395
+ }
2396
+ // --- extended engine-command ops (T3, §1.2 floor set) ------------------
2397
+ case 'set_model': {
2398
+ if (notWritable(client, 'set the model'))
2399
+ break;
2400
+ const requested = parseModelSpec(frame.model);
2401
+ let model;
2402
+ let normalized;
2403
+ try {
2404
+ // Tier/alias specs (`opus`, `strong`, `anthropic/strong`, …) MUST resolve
2405
+ // through the config ladder before touching the registry: the fuzzy
2406
+ // registry search rank-ties `opus` between claude-3-opus-20240229 and
2407
+ // claude-opus-4-8, and the deprecated 2024 model wins on catalog order.
2408
+ // Concrete `provider/id` specs and genuine substrings (`fable`) pass
2409
+ // through normalizeModel unchanged and take the exact/search path below.
2410
+ normalized = parseModelSpec(normalizeModel(requested.modelSpec));
2411
+ model = resolveModelQuery(normalized.modelSpec);
2412
+ }
2413
+ catch (err) {
2414
+ // N2: registry.find should never throw on the real SDK, but a degenerate
2415
+ // engine must still get a reply rather than a silently-dropped frame.
2416
+ engineErrorTo(client)(err);
2417
+ break;
2418
+ }
2419
+ if (model === undefined) {
2420
+ sendFrame(client, {
2421
+ type: 'error',
2422
+ code: 'engine_error',
2423
+ message: `no model matching '${frame.model}' in the registry`,
2424
+ });
2425
+ break;
2426
+ }
2427
+ void session
2428
+ .setModel(model)
2429
+ .then(() => {
2430
+ // The caller's explicit `:thinking` wins; else the ladder cell's.
2431
+ const thinking = requested.thinkingLevel ?? normalized.thinkingLevel;
2432
+ if (thinking !== undefined)
2433
+ session.setThinkingLevel(thinking);
2434
+ ackTo(client, 'set_model');
2435
+ // frame.pinned, when the sender pre-normalized `frame.model` (e.g.
2436
+ // setModelLive), carries the RAW-token pin decision explicitly.
2437
+ // Otherwise frame.model itself IS the raw/explicit token (a
2438
+ // slash-command arg, or a concrete picker pick) and derives it.
2439
+ broadcastModelChanged(true, frame.pinned !== undefined ? frame.pinned : isProviderPinnedModelToken(frame.model));
2440
+ })
2441
+ .catch(engineErrorTo(client));
2442
+ break;
2443
+ }
2444
+ case 'deliver_custom_message': {
2445
+ if (notWritable(client, 'deliver a custom message'))
2446
+ break;
2447
+ // Never triggers a turn either way. `nextTurn` folds the message into
2448
+ // whatever turn comes next (situational context); omitting it pushes
2449
+ // straight onto the message list so it lands FIRST in a still-empty
2450
+ // session (a freshly-claimed spare's bearings). The inbox watcher's own
2451
+ // situational send uses `followUp` + triggerTurn because there its job
2452
+ // IS to wake the node.
2453
+ void session
2454
+ .sendCustomMessage({
2455
+ customType: frame.customType,
2456
+ content: frame.content,
2457
+ display: true,
2458
+ ...(frame.details !== undefined ? { details: frame.details } : {}),
2459
+ }, ...(frame.deliverAs !== undefined ? [{ deliverAs: frame.deliverAs }] : []))
2460
+ .then(() => ackTo(client, 'deliver_custom_message'))
2461
+ .catch(engineErrorTo(client));
2462
+ break;
2463
+ }
2464
+ case 'cycle_model': {
2465
+ if (notWritable(client, 'cycle the model'))
2466
+ break;
2467
+ void session
2468
+ .cycleModel(frame.direction)
2469
+ .then(() => {
2470
+ ackTo(client, 'cycle_model');
2471
+ broadcastModelChanged();
2472
+ })
2473
+ .catch(engineErrorTo(client));
2474
+ break;
2475
+ }
2476
+ case 'cycle_ladder': {
2477
+ if (notWritable(client, 'cycle the model ladder'))
2478
+ break;
2479
+ // Resolve the next interleaved-ladder rung from the current model+thinking
2480
+ // spec, then reuse the set_model path (registry resolve + thinking apply).
2481
+ // The cycle only offers signed-in provider columns — same rule as the
2482
+ // setup wizard's Models panel: pool occupancy for a managed provider
2483
+ // (read live, so a login that landed mid-session counts immediately),
2484
+ // else the registry's auth status. When nobody is signed in the helper
2485
+ // returns every provider, so the cycle stays walkable pre-login.
2486
+ const offeredProviders = (() => {
2487
+ try {
2488
+ const registry = registryOf(services);
2489
+ return configuredLadderProviders((providerId) => (isManagedProvider(providerId) && readSubscriptionPool(providerId).length > 0)
2490
+ || registry.getProviderAuthStatus(providerId).configured);
2491
+ }
2492
+ catch {
2493
+ return undefined; // auth state unreadable — cycle the full ladder
2494
+ }
2495
+ })();
2496
+ const currentSpec = formatModelSpec(session.model, session.thinkingLevel) ?? '';
2497
+ const nextSpec = parseModelSpec(nextLadderModel(currentSpec, frame.direction === 'backward' ? -1 : 1, offeredProviders));
2498
+ let ladderModel;
2499
+ try {
2500
+ ladderModel = resolveModelQuery(nextSpec.modelSpec);
2501
+ }
2502
+ catch (err) {
2503
+ engineErrorTo(client)(err);
2504
+ break;
2505
+ }
2506
+ if (ladderModel === undefined) {
2507
+ sendFrame(client, {
2508
+ type: 'error',
2509
+ code: 'engine_error',
2510
+ message: `ladder model '${nextSpec.modelSpec}' not in the registry`,
2511
+ });
2512
+ break;
2513
+ }
2514
+ void session
2515
+ .setModel(ladderModel)
2516
+ .then(() => {
2517
+ if (nextSpec.thinkingLevel !== undefined)
2518
+ session.setThinkingLevel(nextSpec.thinkingLevel);
2519
+ ackTo(client, 'cycle_ladder');
2520
+ broadcastModelChanged();
2521
+ })
2522
+ .catch(engineErrorTo(client));
2523
+ break;
2524
+ }
2525
+ case 'cycle_thinking': {
2526
+ if (notWritable(client, 'cycle the thinking level'))
2527
+ break;
2528
+ try {
2529
+ session.cycleThinkingLevel();
2530
+ ackTo(client, 'cycle_thinking');
2531
+ }
2532
+ catch (err) {
2533
+ engineErrorTo(client)(err);
2534
+ }
2535
+ break;
2536
+ }
2537
+ case 'dequeue': {
2538
+ if (notWritable(client, 'dequeue messages', frame.id))
2539
+ break;
2540
+ try {
2541
+ const { steering, followUp } = session.clearQueue();
2542
+ sendFrame(client, { type: 'data', id: frame.id, kind: 'dequeue', steering, followUp });
2543
+ }
2544
+ catch (err) {
2545
+ engineErrorTo(client, frame.id)(err);
2546
+ }
2547
+ break;
2548
+ }
2549
+ case 'set_thinking_level': {
2550
+ if (notWritable(client, 'set the thinking level'))
2551
+ break;
2552
+ try {
2553
+ session.setThinkingLevel(frame.level);
2554
+ ackTo(client, 'set_thinking_level');
2555
+ }
2556
+ catch (err) {
2557
+ engineErrorTo(client)(err);
2558
+ }
2559
+ break;
2560
+ }
2561
+ case 'set_auto_retry': {
2562
+ if (notWritable(client, 'set auto-retry'))
2563
+ break;
2564
+ try {
2565
+ session.setAutoRetryEnabled(frame.enabled);
2566
+ ackTo(client, 'set_auto_retry');
2567
+ }
2568
+ catch (err) {
2569
+ engineErrorTo(client)(err);
2570
+ }
2571
+ break;
2572
+ }
2573
+ case 'set_auto_compaction': {
2574
+ if (notWritable(client, 'set auto-compaction'))
2575
+ break;
2576
+ try {
2577
+ session.setAutoCompactionEnabled(frame.enabled);
2578
+ ackTo(client, 'set_auto_compaction');
2579
+ }
2580
+ catch (err) {
2581
+ engineErrorTo(client)(err);
2582
+ }
2583
+ break;
2584
+ }
2585
+ case 'compact': {
2586
+ if (notWritable(client, 'compact the session'))
2587
+ break;
2588
+ void session
2589
+ .compact(frame.instructions)
2590
+ .then(() => ackTo(client, 'compact'))
2591
+ .catch(engineErrorTo(client));
2592
+ break;
2593
+ }
2594
+ case 'set_session_name': {
2595
+ if (notWritable(client, 'rename the session'))
2596
+ break;
2597
+ try {
2598
+ session.setSessionName(frame.name);
2599
+ ackTo(client, 'set_session_name');
2600
+ }
2601
+ catch (err) {
2602
+ engineErrorTo(client)(err);
2603
+ }
2604
+ break;
2605
+ }
2606
+ case 'get_commands': {
2607
+ // OBSERVERS may call this: the merged command inventory is static
2608
+ // (extensions + templates + skills + builtins) and drives nothing in the
2609
+ // engine, so it is not a controller-gated op. The web bridge fetches it on
2610
+ // its observer-by-default upstream connection to populate the palette.
2611
+ // The merged command list rides in ack.detail as JSON (the foundation's
2612
+ // AckFrame.detail field) — the viewer (T6) JSON.parses it when for ===
2613
+ // 'get_commands'. Keeps every command op a uniform ack reply.
2614
+ try {
2615
+ ackTo(client, 'get_commands', true, JSON.stringify(buildCommandList()));
2616
+ }
2617
+ catch (err) {
2618
+ engineErrorTo(client)(err);
2619
+ }
2620
+ break;
2621
+ }
2622
+ // --- read-op picker data (§5 Unit A) ----------------------------------
2623
+ // NOT controller-gated: read-only, like get_commands. The web bridge's
2624
+ // observer connection populates pickers through these too.
2625
+ case 'list_models': {
2626
+ try {
2627
+ sendFrame(client, buildListModelsData(frame.id));
2628
+ }
2629
+ catch (err) {
2630
+ engineErrorTo(client, frame.id)(err);
2631
+ }
2632
+ break;
2633
+ }
2634
+ case 'list_scoped_models': {
2635
+ try {
2636
+ sendFrame(client, buildScopedModelsData(frame.id));
2637
+ }
2638
+ catch (err) {
2639
+ engineErrorTo(client, frame.id)(err);
2640
+ }
2641
+ break;
2642
+ }
2643
+ case 'list_memory_refs': {
2644
+ try {
2645
+ sendFrame(client, buildListMemoryRefsData(frame.id));
2646
+ }
2647
+ catch (err) {
2648
+ engineErrorTo(client, frame.id)(err);
2649
+ }
2650
+ break;
2651
+ }
2652
+ case 'get_tree': {
2653
+ try {
2654
+ sendFrame(client, buildGetTreeData(frame.id));
2655
+ }
2656
+ catch (err) {
2657
+ engineErrorTo(client, frame.id)(err);
2658
+ }
2659
+ break;
2660
+ }
2661
+ case 'get_settings': {
2662
+ try {
2663
+ sendFrame(client, buildSettingsData(frame.id));
2664
+ }
2665
+ catch (err) {
2666
+ engineErrorTo(client, frame.id)(err);
2667
+ }
2668
+ break;
2669
+ }
2670
+ case 'list_sessions': {
2671
+ // Session listing reads the session dir off disk — async; reply when it
2672
+ // resolves (or relay the error, correlated by id so the viewer doesn't hang).
2673
+ void buildSessionsData(frame.id, frame.scope ?? 'cwd')
2674
+ .then((d) => sendFrame(client, d))
2675
+ .catch(engineErrorTo(client, frame.id));
2676
+ break;
2677
+ }
2678
+ case 'get_human_fork_coordinates': {
2679
+ // NOT controller-gated: this is a pure observer read. Branch creation is
2680
+ // intentionally delegated to a detached SDK manager in the ask publisher;
2681
+ // calling createBranchedSession here corrupts this live manager's identity.
2682
+ try {
2683
+ const sm = session.sessionManager;
2684
+ const sourceFile = sm.getSessionFile();
2685
+ const leafId = sm.getLeafId();
2686
+ if (sourceFile === undefined || sourceFile === '' || leafId === null) {
2687
+ engineErrorTo(client, frame.id)(new Error('no persisted source session leaf'));
2688
+ break;
2689
+ }
2690
+ sendFrame(client, {
2691
+ type: 'data',
2692
+ id: frame.id,
2693
+ kind: 'human_fork_coordinates',
2694
+ sourceFile,
2695
+ sourceSessionId: sm.getSessionId(),
2696
+ leafId,
2697
+ modelSpec: formatExactModelSpec(session.model, session.thinkingLevel),
2698
+ });
2699
+ }
2700
+ catch (err) {
2701
+ engineErrorTo(client, frame.id)(err);
2702
+ }
2703
+ break;
2704
+ }
2705
+ case 'navigate_tree': {
2706
+ if (notWritable(client, 'navigate the session tree'))
2707
+ break;
2708
+ // navigateTree rewinds IN-PLACE (same session file, new leaf) and emits no
2709
+ // relayed event, so every viewer must be re-snapshotted onto the rewound
2710
+ // transcript — same reWelcomeAll the session-replacing ops use. The ack's
2711
+ // detail carries the navigated-to user message's text (pi parity: the
2712
+ // interactive tree navigator restores it to the editor for re-editing).
2713
+ void session
2714
+ .navigateTree(frame.targetId, frame.options)
2715
+ .then((r) => {
2716
+ if (!r.cancelled)
2717
+ reWelcomeAll();
2718
+ ackTo(client, 'navigate_tree', !r.cancelled, r.editorText);
2719
+ })
2720
+ .catch(engineErrorTo(client));
2721
+ break;
2722
+ }
2723
+ case 'reload': {
2724
+ if (notWritable(client, 'reload'))
2725
+ break;
2726
+ // A SUCCESSFUL reload invalidates the memoized ref inventory (work item
2727
+ // 3) so the next read-op/submission re-walks the corpus, AND clears the
2728
+ // shared substrate parse cache buildRefInventory reads through —
2729
+ // otherwise a fresh buildRefInventory() call would still see the stale
2730
+ // cached docs. A FAILED reload (`.catch`) leaves both untouched: nothing
2731
+ // about the corpus is known to have changed.
2732
+ void session
2733
+ .reload()
2734
+ .then(() => {
2735
+ memoizedRefInventory = null;
2736
+ clearSessionCache();
2737
+ ackTo(client, 'reload');
2738
+ })
2739
+ .catch(engineErrorTo(client));
2740
+ break;
2741
+ }
2742
+ case 'export': {
2743
+ if (notWritable(client, 'export the session'))
2744
+ break;
2745
+ if (frame.format === 'jsonl') {
2746
+ try {
2747
+ const outPath = session.exportToJsonl(frame.path);
2748
+ stripHiddenEntriesFromJsonlExport(outPath);
2749
+ ackTo(client, 'export');
2750
+ }
2751
+ catch (err) {
2752
+ engineErrorTo(client)(err);
2753
+ }
2754
+ }
2755
+ else {
2756
+ void session
2757
+ .exportToHtml(frame.path)
2758
+ .then(() => ackTo(client, 'export'))
2759
+ .catch(engineErrorTo(client));
2760
+ }
2761
+ break;
2762
+ }
2763
+ case 'new_session': {
2764
+ if (notWritable(client, 'start a new session'))
2765
+ break;
2766
+ runReplacement(client, 'new_session', (rt) => rt.newSession());
2767
+ break;
2768
+ }
2769
+ case 'switch_session': {
2770
+ if (notWritable(client, 'switch sessions'))
2771
+ break;
2772
+ runReplacement(client, 'switch_session', (rt) => rt.switchSession(frame.path));
2773
+ break;
2774
+ }
2775
+ case 'fork': {
2776
+ if (notWritable(client, 'fork the session'))
2777
+ break;
2778
+ runReplacement(client, 'fork', (rt) => rt.fork(frame.entryId));
2779
+ break;
2780
+ }
2781
+ case 'clone': {
2782
+ if (notWritable(client, 'clone the session'))
2783
+ break;
2784
+ runReplacement(client, 'clone', async (rt) => {
2785
+ const sm = session.sessionManager;
2786
+ const leafId = sm.getLeafId();
2787
+ if (leafId === null) {
2788
+ return { cancelled: true, detail: 'no current leaf to clone from' };
2789
+ }
2790
+ const newPath = sm.createBranchedSession(leafId);
2791
+ if (newPath === undefined) {
2792
+ return { cancelled: true, detail: 'createBranchedSession returned no path' };
2793
+ }
2794
+ return rt.switchSession(newPath);
2795
+ });
2796
+ break;
2797
+ }
2798
+ case 'share': {
2799
+ if (notWritable(client, 'share the session'))
2800
+ break;
2801
+ const tmpPath = join(tmpdir(), `pi-share-${Date.now()}.html`);
2802
+ void session.exportToHtml(tmpPath)
2803
+ .then(() => new Promise((resolve, reject) => {
2804
+ execFile('gh', ['gist', 'create', '--secret', tmpPath], { timeout: 30_000, maxBuffer: 1024 * 1024 }, (err, stdout) => {
2805
+ try {
2806
+ if (existsSync(tmpPath))
2807
+ unlinkSync(tmpPath);
2808
+ }
2809
+ catch { /* cleanup */ }
2810
+ if (err) {
2811
+ reject(err);
2812
+ return;
2813
+ }
2814
+ resolve(stdout.trim());
2815
+ });
2816
+ }))
2817
+ .then((url) => {
2818
+ ackTo(client, 'share', true, url);
2819
+ })
2820
+ .catch((err) => {
2821
+ const msg = String(err?.message ?? err);
2822
+ ackTo(client, 'share', false, msg.includes('not found') || msg.includes('gh: command not found') || msg.includes('ENOENT')
2823
+ ? '`gh` CLI not found — install GitHub CLI and authenticate with `gh auth login`'
2824
+ : `share failed: ${msg}`);
2825
+ });
2826
+ break;
2827
+ }
2828
+ case 'reload_auth': {
2829
+ // Open to any client, observers included: reload_auth is an idempotent
2830
+ // local re-read of the shared auth.json + a model-registry refresh. It
2831
+ // does NOT steer the conversation, so there is nothing to role-gate —
2832
+ // which is what lets the daemon's canvas-wide fan (one /login propagated
2833
+ // to every live broker) ride an observer connection.
2834
+ try {
2835
+ // pi 0.82: crouter's CredentialStore reads auth.json fresh under the
2836
+ // lock on every call, so there is no cached credential view left to
2837
+ // reload — only the runtime's models/availability snapshot has to be
2838
+ // recomputed, and that is asynchronous. Every consumer below awaits
2839
+ // the same promise so none of them reads a pre-login snapshot.
2840
+ const authRefreshed = registryOf(services).refresh().catch((err) => {
2841
+ emitEvent({ level: 'warn', event: 'broker.auth_reload.refresh_failed', error: err });
2842
+ });
2843
+ ackTo(client, 'reload_auth');
2844
+ void authRefreshed.then(() => broadcastModelChanged());
2845
+ // Tier 3 re-drive: a model-less boot (no viable candidate at launch)
2846
+ // stays live so the user can /login; once auth actually changes,
2847
+ // re-run the SAME launch policy against the now-refreshed services
2848
+ // and, if a model now resolves, adopt it and fire the deferred
2849
+ // kickoff. handleFrame is not async (broker.ts) so this runs as a
2850
+ // detached IIFE, the same async-in-sync-switch pattern used above
2851
+ // for provider/not-found fallback.
2852
+ const authGeneration = installedGeneration;
2853
+ if (sessionHasNoModel() && authGeneration !== undefined && reloadAuthInFlight === null) {
2854
+ const authSession = session;
2855
+ const authServices = services;
2856
+ reloadAuthInFlight = authGeneration;
2857
+ void (async () => {
2858
+ try {
2859
+ await authRefreshed;
2860
+ const resolved = await resolveLaunchModel(authServices, cfg);
2861
+ if (resolved !== undefined) {
2862
+ await authSession.setModel(resolved.model);
2863
+ if (installedGeneration !== authGeneration || session !== authSession)
2864
+ return;
2865
+ if (resolved.thinkingLevel !== undefined)
2866
+ authSession.setThinkingLevel(resolved.thinkingLevel);
2867
+ broadcastModelChanged();
2868
+ drivePendingKickoff();
2869
+ }
2870
+ }
2871
+ catch (err) {
2872
+ emitEvent({
2873
+ level: 'error',
2874
+ event: 'broker.auth_reload.redrive_failed',
2875
+ error: err,
2876
+ });
2877
+ }
2878
+ finally {
2879
+ if (reloadAuthInFlight === authGeneration)
2880
+ reloadAuthInFlight = null;
2881
+ }
2882
+ })();
2883
+ }
2884
+ const authFault = readFault(nodeId);
2885
+ const authFaultGeneration = installedGeneration;
2886
+ const authFaultSession = session;
2887
+ if (authFault?.link === 'pi→provider'
2888
+ && authFault.kind === 'auth'
2889
+ && !authFaultSession.isStreaming
2890
+ && !authFaultRedriveInFlight) {
2891
+ authFaultRedriveInFlight = true;
2892
+ void (async () => {
2893
+ try {
2894
+ const anchorEntryId = authFault.anchorEntryId;
2895
+ if (anchorEntryId !== undefined) {
2896
+ try {
2897
+ const leafId = authFaultSession.sessionManager.getLeafId?.() ?? null;
2898
+ if (leafId !== anchorEntryId)
2899
+ await authFaultSession.navigateTree(anchorEntryId);
2900
+ if (installedGeneration !== authFaultGeneration || session !== authFaultSession)
2901
+ return;
2902
+ }
2903
+ catch (err) {
2904
+ emitEvent({ level: 'warn', event: 'broker.auth_fault.rewind_skipped', error: err });
2905
+ }
2906
+ }
2907
+ const fresh = readFault(nodeId);
2908
+ if (installedGeneration !== authFaultGeneration
2909
+ || session !== authFaultSession
2910
+ || authFaultSession.isStreaming
2911
+ || fresh?.link !== 'pi→provider'
2912
+ || fresh.kind !== 'auth'
2913
+ || fresh.since !== authFault.since)
2914
+ return;
2915
+ clearFault(nodeId, { link: 'pi→provider' });
2916
+ await authFaultSession.prompt(AUTH_FAULT_RECOVERY_BODY);
2917
+ }
2918
+ catch (err) {
2919
+ emitEvent({ level: 'error', event: 'broker.auth_fault.redrive_failed', error: err });
2920
+ }
2921
+ finally {
2922
+ authFaultRedriveInFlight = false;
2923
+ }
2924
+ })();
2925
+ }
2926
+ }
2927
+ catch (err) {
2928
+ engineErrorTo(client)(err);
2929
+ }
2930
+ break;
2931
+ }
2932
+ case 'bye':
2933
+ client.socket.end();
2934
+ break;
2935
+ case 'shutdown':
2936
+ disposeAndExit('shutdown-frame');
2937
+ break;
2938
+ }
2939
+ };
2940
+ // -------------------------------------------------------------------------
2941
+ // 4 (plan): the socket listener — unlink any stale socket first.
2942
+ // -------------------------------------------------------------------------
2943
+ try {
2944
+ if (existsSync(sockPath))
2945
+ unlinkSync(sockPath);
2946
+ }
2947
+ catch {
2948
+ /* a fresh listen below will surface a real problem */
2949
+ }
2950
+ server = createServer((socket) => {
2951
+ const client = {
2952
+ id: '',
2953
+ role: 'observer',
2954
+ socket,
2955
+ decoder: new FrameDecoder(BROKER_READ_CAPS),
2956
+ helloed: false,
2957
+ pendingBytes: 0,
2958
+ queuedFrames: 0,
2959
+ };
2960
+ clients.add(client);
2961
+ socket.on('data', (chunk) => {
2962
+ let frames;
2963
+ try {
2964
+ frames = client.decoder.push(chunk);
2965
+ }
2966
+ catch (err) {
2967
+ // G7: a client frame over the bounded decoder caps (FrameOverflowError) is
2968
+ // cap-and-dropped — best-effort error frame, then destroy the peer. The
2969
+ // broker survives. The try/catch is around push() itself (not just the
2970
+ // per-frame loop) because push() throws BEFORE returning any frames.
2971
+ if (err instanceof FrameOverflowError) {
2972
+ sendFrame(client, { type: 'error', code: 'frame_overflow', message: err.message });
2973
+ dropClient(client, `frame overflow: ${err.message}`);
2974
+ }
2975
+ else {
2976
+ dropClient(client, `decoder error: ${String(err)}`);
2977
+ }
2978
+ return;
2979
+ }
2980
+ for (const raw of frames) {
2981
+ try {
2982
+ handleFrame(client, raw);
2983
+ }
2984
+ catch {
2985
+ /* one bad frame never crashes the broker */
2986
+ }
2987
+ }
2988
+ });
2989
+ const drop = () => {
2990
+ clients.delete(client);
2991
+ persistAttachState(); // a viewer disconnected
2992
+ // A writable client detaching does NOT cancel in-flight dialogs — they stay
2993
+ // pending under the broker-side default timeout, so a peer that is still
2994
+ // attached (or one that attaches next and gets the replay) can still answer.
2995
+ // Only an answer, the timeout, or an abort resolves one.
2996
+ };
2997
+ socket.on('close', drop);
2998
+ socket.on('error', () => {
2999
+ /* close follows; drop there */
3000
+ });
3001
+ });
3002
+ server.on('error', (err) => {
3003
+ // A recursive error raised during/after disposeAndExit's own server.close()
3004
+ // (or any other exit already in flight) must not emit a second canonical
3005
+ // failure or dispose twice.
3006
+ if (disposed)
3007
+ return;
3008
+ emitEvent({ level: 'error', event: 'broker.socket.failed', error: err });
3009
+ disposeAndExit('socket-error', 1);
3010
+ });
3011
+ server.listen(sockPath, () => {
3012
+ // A launch prompt delivers only after the listener is accepting, so a
3013
+ // root-spawn readiness proof and prompt admission share one boundary. This
3014
+ // includes the one continuation carried by a clean runtime-abort resume.
3015
+ // A model-less tier-3 boot (session.model === undefined) leaves the prompt
3016
+ // pending in kickoffPending; drivePendingKickoff is a no-op until the
3017
+ // reload_auth re-drive resolves a model, at which point it fires. For the
3018
+ // common model-present path this fires immediately — unchanged behavior.
3019
+ if (cfg.firstPrompt !== undefined && cfg.firstPrompt !== '') {
3020
+ kickoffPending = cfg.firstPrompt;
3021
+ drivePendingKickoff();
3022
+ }
3023
+ });
3024
+ // Initialize presence to the truthful zero state at boot — this also
3025
+ // overwrites a stale attach.json a crashed prior incarnation left behind.
3026
+ persistAttachState();
3027
+ // A daemon fleet bounce gives a busy Pi a short, bounded chance to persist
3028
+ // its aborted turn before teardown. `session.abort()` resolves only after Pi
3029
+ // reaches idle (and thus after its message_end persistence); only then may we
3030
+ // leave the durable clean-abort certificate that changes recovery to resume.
3031
+ // A timeout/rejection deliberately leaves no certificate, preserving dirty
3032
+ // crash → recovery-cycle behavior and the fleet's shared SIGKILL deadline.
3033
+ const ABORT_BEFORE_EXIT_MS = 2_500;
3034
+ let signalExitInFlight = false;
3035
+ const abortAndExit = async (reason) => {
3036
+ if (disposed || signalExitInFlight)
3037
+ return;
3038
+ signalExitInFlight = true;
3039
+ const busy = agentRunActive || session.isStreaming || pendingPromptDispatches > 0;
3040
+ if (busy) {
3041
+ const deadline = Date.now() + ABORT_BEFORE_EXIT_MS;
3042
+ // prompt() has a real async pre-turn window where abort() silently no-ops.
3043
+ // Wait only within the daemon's fixed teardown budget for it to become
3044
+ // abortable. A still-pending setup is deliberately marked dirty below so
3045
+ // recovery starts a cycle instead of silently losing the prompt on resume.
3046
+ while (pendingPromptDispatches > 0 && !session.isStreaming && Date.now() < deadline) {
3047
+ await new Promise((resolve) => setTimeout(resolve, 25));
3048
+ }
3049
+ if (session.isStreaming) {
3050
+ const remaining = deadline - Date.now();
3051
+ if (remaining > 0) {
3052
+ let timer;
3053
+ try {
3054
+ const settled = await Promise.race([
3055
+ session.abort().then(() => true, () => false),
3056
+ new Promise((resolve) => { timer = setTimeout(() => resolve(false), remaining); }),
3057
+ ]);
3058
+ if (settled)
3059
+ markCleanAbort(nodeId);
3060
+ }
3061
+ finally {
3062
+ if (timer !== undefined)
3063
+ clearTimeout(timer);
3064
+ }
3065
+ }
3066
+ }
3067
+ else if (pendingPromptDispatches > 0 || agentRunActive) {
3068
+ // The engine never reached an abortable persisted turn in the budget.
3069
+ // Preserve the existing dirty-death recovery cycle rather than certifying
3070
+ // a session that may not contain the prompt it was preparing.
3071
+ markBusy(nodeId);
3072
+ }
3073
+ }
3074
+ disposeAndExit(reason);
3075
+ };
3076
+ process.on('SIGTERM', () => { void abortAndExit('SIGTERM'); });
3077
+ process.on('SIGINT', () => { void abortAndExit('SIGINT'); });
3078
+ }
3079
+ // ---------------------------------------------------------------------------
3080
+ // snapshotMessages — the catch-up snapshot's ordered message history.
3081
+ //
3082
+ // The broker is the SOLE writer of the node's session `.jsonl`, so the session
3083
+ // manager's persisted tree (`buildSessionContext`) is the canonical, complete,
3084
+ // ordered history — byte-identical to what a dormant reader (crouter-web's static
3085
+ // normalizer) reconstructs from the same file. We serve THAT, NOT the agent's
3086
+ // live `session.messages` (`agent.state.messages`), because pi's recovery paths
3087
+ // mutate the live array away from the persisted history: auto-retry, context-
3088
+ // overflow recovery, and compaction each SLICE the errored/superseded assistant
3089
+ // message out of `state.messages` while DELIBERATELY keeping it on disk ("keep in
3090
+ // session for history", agent-session.js). So `session.messages` can OMIT — or, via
3091
+ // branch/compaction reshaping, reorder — a turn the `.jsonl` still holds. Pre-
3092
+ // close that drift is invisible (the live stream and the live array agree); after
3093
+ // a revive the welcome snapshot built from the reloaded array would diverge from
3094
+ // the on-disk history a dormant viewer just saw. Reconstructing from the session
3095
+ // manager makes the live snapshot == the persisted history (single source of
3096
+ // truth); the relayed live stream then continues from there.
3097
+ //
3098
+ // `AgentSession.sessionManager` is a public readonly field of the real pi SDK; the
3099
+ // fake-engine test fixture mirrors the same `sessionManager.buildSessionContext()`
3100
+ // surface (plus, where present, the tree accessors `cycleAwareMessages` reads).
3101
+ // A refresh-yielded node keeps every prior cycle in the SAME `.jsonl` as sibling
3102
+ // branches (session-cycles.ts) — `cycleAwareMessages` replays them ahead of the
3103
+ // live cycle's context so the welcome snapshot renders one continuous
3104
+ // conversation instead of just the post-yield window; single-cycle sessions and
3105
+ // the fake-engine fixture (no tree accessors) fall through to plain
3106
+ // `buildSessionContext().messages` unchanged.
3107
+ export function snapshotMessages(session) {
3108
+ // Cast: the real SDK's `SessionEntry.data` is `unknown` (untyped custom-entry
3109
+ // payload), narrower than `CycleSessionManagerLike`'s structural read of the
3110
+ // `crtr-cycle` marker's `{cycle, fromLeaf}` shape — the fields we read off it
3111
+ // are ones ONLY this module's own `appendCustomEntry` call (above) ever writes.
3112
+ return cycleAwareMessages(session.sessionManager);
3113
+ }
3114
+ // ---------------------------------------------------------------------------
3115
+ // resolveLaunchModel — the SOLE launch model-selection policy, module-scoped
3116
+ // so both buildForManager (initial boot, inside buildBrokerSession below) and
3117
+ // the reload_auth re-drive (inside runBroker, a DIFFERENT function scope) can
3118
+ // invoke the identical decision against a live `services`/`cfg` without
3119
+ // re-running session/service creation. Returns undefined when NO candidate is
3120
+ // viable at all (tier 3) — this is NOT a fault; the caller decides what to do
3121
+ // with it (construct a model-less session at boot, or leave a running session
3122
+ // alone on a failed re-drive attempt).
3123
+ // ---------------------------------------------------------------------------
3124
+ async function resolveLaunchModel(services, cfg) {
3125
+ // Resolve one viable model against the SERVICES registry, which includes
3126
+ // extension-provided providers. `resolveProviderCandidates` is the SOLE
3127
+ // launch selection path — the same viability policy governs turn-time retry
3128
+ // and rotation fallback — so launch selection can never diverge from
3129
+ // rotation. A requested model gets first refusal (its provider pinned when
3130
+ // the caller pinned it), then the authenticated registry order decides.
3131
+ const requested = cfg.model ? parseModelSpec(cfg.model) : undefined;
3132
+ const faultLaunchModel = (message) => {
3133
+ recordFaultForCurrentNode({
3134
+ link: 'crtr→pi',
3135
+ op: 'model resolution at launch',
3136
+ kind: 'model-not-found',
3137
+ retry: { disposition: 'fatal' },
3138
+ message,
3139
+ });
3140
+ throw new Error(`[broker] FATAL: ${message}`);
3141
+ };
3142
+ if (requested !== undefined && requested.modelSpec.indexOf('/') <= 0) {
3143
+ faultLaunchModel(`model '${cfg.model}' has no 'provider/id' form and cannot be resolved`);
3144
+ }
3145
+ const requestedProvider = requested?.modelSpec.slice(0, requested.modelSpec.indexOf('/'));
3146
+ const requestedId = requested?.modelSpec.slice(requested.modelSpec.indexOf('/') + 1);
3147
+ const registry = registryOf(services);
3148
+ // The requested model's SAME-STRENGTH cell on the OTHER provider — the one
3149
+ // substitution that preserves the caller's intent, since a persona asks for a
3150
+ // STRENGTH (`explore: openai/light`) and the ladder only picks the id. It is
3151
+ // candidate #2, ahead of raw registry order: without it, an unavailable
3152
+ // `openai/light` fell through to whatever the registry happened to list first
3153
+ // (`anthropic/claude-fable-5` — the most expensive model there is) for a scout
3154
+ // that asked for the cheapest. This is the SAME mapping turn-time rotation
3155
+ // already falls back through (`equivalentOtherProviderModel`), so launch
3156
+ // selection and rotation now degrade identically instead of divergently.
3157
+ const ladderFallback = cfg.modelExact === true || cfg.model === undefined
3158
+ ? null
3159
+ : equivalentOtherProviderModel(cfg.model);
3160
+ const ladderCandidate = (() => {
3161
+ if (ladderFallback === null)
3162
+ return undefined;
3163
+ const spec = parseModelSpec(ladderFallback.model);
3164
+ const slash = spec.modelSpec.indexOf('/');
3165
+ if (slash <= 0)
3166
+ return undefined;
3167
+ const provider = spec.modelSpec.slice(0, slash);
3168
+ const id = spec.modelSpec.slice(slash + 1);
3169
+ if (provider === requestedProvider && id === requestedId)
3170
+ return undefined;
3171
+ return { provider, id, thinkingLevel: spec.thinkingLevel };
3172
+ })();
3173
+ const requestedModel = requestedProvider !== undefined && requestedId !== undefined
3174
+ ? registry.find(requestedProvider, requestedId)
3175
+ : undefined;
3176
+ // Enumerate every REGISTERED model (not just the authenticated set) so the
3177
+ // resolver itself classifies each candidate's registered/authenticated/
3178
+ // cooling state and reports a per-candidate rejection reason. Requested
3179
+ // first, then the rest of the registry in order.
3180
+ // cfg.modelExact locks launch selection to ONLY the requested candidate —
3181
+ // the rest of the registry is never spread in, so no cross-model/cross-
3182
+ // provider substitution is even reachable below.
3183
+ const orderedModels = [
3184
+ ...(requestedProvider !== undefined && requestedId !== undefined
3185
+ ? [{ provider: requestedProvider, id: requestedId, model: requestedModel, thinkingLevel: requested?.thinkingLevel }]
3186
+ : []),
3187
+ ...(ladderCandidate !== undefined
3188
+ ? [{
3189
+ provider: ladderCandidate.provider,
3190
+ id: ladderCandidate.id,
3191
+ model: registry.find(ladderCandidate.provider, ladderCandidate.id),
3192
+ thinkingLevel: ladderCandidate.thinkingLevel,
3193
+ }]
3194
+ : []),
3195
+ ...(cfg.modelExact === true ? [] : registry.getAll()
3196
+ .filter((candidate) => (candidate.provider !== requestedProvider || candidate.id !== requestedId)
3197
+ && (ladderCandidate === undefined || candidate.provider !== ladderCandidate.provider || candidate.id !== ladderCandidate.id))
3198
+ .map((candidate) => ({
3199
+ provider: candidate.provider,
3200
+ id: candidate.id,
3201
+ model: candidate,
3202
+ thinkingLevel: undefined,
3203
+ }))),
3204
+ ];
3205
+ // Tier 1/2 policy: a non-managed candidate is only automatic-fallback-eligible
3206
+ // when NO managed provider has an authenticated (pool-occupied) credential
3207
+ // ANYWHERE in the ordered set — regardless of that managed candidate's
3208
+ // current cooling state. A cooling-but-authenticated managed pool still
3209
+ // blocks automatic fallback to a non-managed provider (preserves 4be8be6's
3210
+ // intent: don't silently abandon a paid managed subscription for a
3211
+ // pay-per-token key just because it's rate-limited right now). Only when
3212
+ // there is NO managed-authenticated candidate at all does a registered +
3213
+ // authenticated non-managed model become an acceptable automatic launch
3214
+ // fallback (tier 2); only when even that is unavailable does launch fall all
3215
+ // the way through to a model-less boot (tier 3, below).
3216
+ const hasManagedAuthenticated = orderedModels.some((candidate) => {
3217
+ if (!isManagedProvider(candidate.provider))
3218
+ return false;
3219
+ if (candidate.model === undefined)
3220
+ return false; // not registered
3221
+ return readSubscriptionPool(candidate.provider).length > 0;
3222
+ });
3223
+ const selection = resolveProviderCandidates(orderedModels.map((candidate, index) => {
3224
+ // Managed providers authenticate off their subscription pool's accounts, not
3225
+ // static credentials; a fully-cooling pool marks the candidate cooling.
3226
+ const pool = isManagedProvider(candidate.provider) ? readSubscriptionPool(candidate.provider) : [];
3227
+ const explicit = index === 0 && requestedProvider !== undefined && requestedId !== undefined;
3228
+ return {
3229
+ providerId: candidate.provider,
3230
+ modelId: candidate.id,
3231
+ thinkingLevel: candidate.thinkingLevel,
3232
+ registered: candidate.model !== undefined,
3233
+ // Non-managed models are valid automatic fallback when explicitly
3234
+ // requested, when the candidate IS a managed provider itself, or (tier
3235
+ // 2) when no managed-authenticated candidate exists anywhere in this
3236
+ // launch's candidate set.
3237
+ automaticFallback: explicit || isManagedProvider(candidate.provider) || !hasManagedAuthenticated,
3238
+ authenticated: candidate.model !== undefined && (isManagedProvider(candidate.provider)
3239
+ ? pool.length > 0
3240
+ : registry.hasConfiguredAuth(candidate.model)),
3241
+ coolingUntil: pool.length > 0 && pool.every((entry) => (entry.rateLimitedUntil || 0) > Date.now())
3242
+ ? Math.min(...pool.map((entry) => entry.rateLimitedUntil || 0))
3243
+ : undefined,
3244
+ };
3245
+ }), Date.now(),
3246
+ // An explicit provider pin forbids cross-provider fallback for an
3247
+ // unregistered/unauthenticated request; only a transient managed cooldown
3248
+ // on the pinned provider still boots a viable alternative.
3249
+ { pinnedToFirst: cfg.modelProviderPinned === true && requested !== undefined });
3250
+ const selected = selection.candidates[0];
3251
+ if (selected === undefined) {
3252
+ const rejectionText = selection.rejected.length > 0
3253
+ ? selection.rejected.map(({ candidate, reason }) => `'${candidate.providerId}/${candidate.modelId}' ${reason === 'non-managed-fallback' ? 'non-managed provider is not an automatic fallback' : reason}`).join('; ')
3254
+ : 'no candidates were supplied';
3255
+ // An exact-consult launch must FAIL rather than fall through to a
3256
+ // model-less session — an unavailable captured model faults the launch
3257
+ // (crash), never a silent degrade/substitution.
3258
+ if (cfg.modelExact === true) {
3259
+ return faultLaunchModel(`exact-consult model '${cfg.model}' is unavailable — refusing to substitute (${rejectionText})`);
3260
+ }
3261
+ // Tier 3: no viable candidate at all (genuine zero-auth, OR every managed
3262
+ // pool is cooling AND no non-managed provider is authenticated). This is
3263
+ // NOT a fault — the caller constructs a model-less session so the
3264
+ // socket/listener still comes up and the user can /login; reload_auth
3265
+ // re-invokes this same helper to re-drive once auth changes.
3266
+ emitEvent({
3267
+ level: 'warn',
3268
+ event: 'broker.model.none_viable',
3269
+ fields: {
3270
+ rejected_candidates: selection.rejected.slice(0, 16).map(({ candidate, reason }) => ({
3271
+ provider: candidate.providerId,
3272
+ model: candidate.modelId,
3273
+ reason,
3274
+ })),
3275
+ rejected_candidates_omitted: Math.max(0, selection.rejected.length - 16),
3276
+ },
3277
+ });
3278
+ return undefined;
3279
+ }
3280
+ const model = registry.find(selected.providerId, selected.modelId);
3281
+ if (model === undefined) {
3282
+ return faultLaunchModel(`selected model '${selected.providerId}/${selected.modelId}' disappeared during launch`);
3283
+ }
3284
+ const thinkingLevel = selected.thinkingLevel;
3285
+ // A launch that does not get the model it asked for is a SUBSTITUTION, and it
3286
+ // used to be entirely silent: the session simply came up on another model and
3287
+ // the only trace was the pi `model_change` entry in the .jsonl. One line, only
3288
+ // on the rare path, carrying the per-candidate rejection reasons that decided
3289
+ // it — so "why is this node on the wrong model" is answerable from the log.
3290
+ if (requestedProvider !== undefined &&
3291
+ requestedId !== undefined &&
3292
+ (selected.providerId !== requestedProvider || selected.modelId !== requestedId)) {
3293
+ emitEvent({
3294
+ level: 'warn',
3295
+ event: 'broker.model.launch_fallback',
3296
+ fields: {
3297
+ requested: `${requestedProvider}/${requestedId}`,
3298
+ selected: `${selected.providerId}/${selected.modelId}`,
3299
+ ladder_equivalent: ladderFallback === null ? null : ladderFallback.model,
3300
+ rejected_candidates: selection.rejected.slice(0, 8).map(({ candidate, reason }) => ({
3301
+ provider: candidate.providerId,
3302
+ model: candidate.modelId,
3303
+ reason,
3304
+ })),
3305
+ },
3306
+ });
3307
+ }
3308
+ if (requestedProvider !== undefined &&
3309
+ requestedId !== undefined &&
3310
+ isManagedProvider(requestedProvider) &&
3311
+ requestedModel !== undefined &&
3312
+ (selected.providerId !== requestedProvider || selected.modelId !== requestedId)) {
3313
+ // This launch fallback is temporary: retain the requested managed pin for
3314
+ // provider-rotation's before_agent_start restoration once it is ready.
3315
+ rememberPreferredModel({ providerId: requestedProvider, modelId: requestedId });
3316
+ }
3317
+ return { model, thinkingLevel };
3318
+ }
3319
+ /**
3320
+ * Wrap a live ModelRuntime so a model-less `createAgentSessionFromServices`
3321
+ * call can't have the SDK's own `findInitialModel` silently pick a model
3322
+ * behind crouter's launch policy. Both `getModel()` (step 3: settingsManager's
3323
+ * durable `defaultProvider`/`defaultModel` — persisted into a GLOBAL
3324
+ * settings.json by every `session.setModel()` call the broker itself already
3325
+ * makes at broker.ts:609/675/1606/1650, so this is a live bypass on any
3326
+ * machine that has ever picked a model, not a theoretical corner case) and
3327
+ * `getAvailable()` (step 4: registry default) must both be intercepted —
3328
+ * wrapping only `getAvailable()` leaves `getModel()`'s step-3 path (and the
3329
+ * resume path's model restore, sdk.js:88) wide open. Everything else
3330
+ * (`getModels`, `hasConfiguredAuth`, `refresh`, …) passes through untouched via
3331
+ * `Reflect.get`. Used ONLY
3332
+ * for the one model-less construction call; the `services` returned to every
3333
+ * other caller (reload_auth, hasConfiguredAuth checks, …) keeps the REAL
3334
+ * unwrapped registry.
3335
+ *
3336
+ * CONSTRUCTION-ONLY MASK: the SDK stores `options.services.modelRuntime` as the
3337
+ * session's private `_modelRuntime` for its ENTIRE lifetime and never replaces
3338
+ * it (agent-session-services.js:104-119, sdk.js:253). A permanent mask would
3339
+ * leave list_models (getAvailable→[]), cycle_model, and provider-rotation
3340
+ * fallback (getModel()→undefined) broken forever for any model-less-booted node,
3341
+ * even after a later /login selects a model. Model selection can only happen
3342
+ * DURING construction, so this proxy self-heals: it masks while `masked`, then
3343
+ * `reveal()` (called immediately after createAgentSessionFromServices returns)
3344
+ * flips the SAME retained proxy to a transparent passthrough forever after.
3345
+ */
3346
+ function modelLessRegistry(real) {
3347
+ let masked = true;
3348
+ const runtime = new Proxy(real, {
3349
+ get(target, prop, receiver) {
3350
+ // `getAvailable` is async on ModelRuntime; findInitialModel awaits it.
3351
+ if (masked && prop === 'getAvailable')
3352
+ return async () => [];
3353
+ if (masked && prop === 'getModel')
3354
+ return () => undefined;
3355
+ return Reflect.get(target, prop, receiver);
3356
+ },
3357
+ });
3358
+ return { runtime, reveal: () => { masked = false; } };
3359
+ }
3360
+ // ---------------------------------------------------------------------------
3361
+ // buildBrokerSession (plan T4 steps 2–4) — turn the launch recipe into a live
3362
+ // engine session via the pi SDK SERVICES path. Exported so the C3/C4 real-SDK
3363
+ // regression tests can drive the EXACT production wiring (not the mock).
3364
+ // ---------------------------------------------------------------------------
3365
+ export async function buildBrokerSession(engine, cfg) {
3366
+ // 2. Build cwd-bound runtime services via the SERVICES path (C3) — NOT plain
3367
+ // createAgentSession. createAgentSessionServices builds + reloads the
3368
+ // resource loader (the `-e` canvas extensions + --append-system-prompt) and
3369
+ // REGISTERS extension-provided model providers into the ModelRegistry (it is
3370
+ // also where extension flag values would be applied — the broker recipe
3371
+ // carries none today). Plain createAgentSession does NEITHER, so a node whose
3372
+ // model comes from a custom-provider extension would get NO model. Mirrors pi
3373
+ // main.js (createAgentSessionServices → …FromServices).
3374
+ //
3375
+ // C4 note: the pinned SDK (0.78.1) has NO project-trust concept at all —
3376
+ // project context files (AGENTS.md/CLAUDE.md) load UNCONDITIONALLY (gated
3377
+ // only by `noContextFiles`, left default-false), so the headless "trust
3378
+ // resolves false → context silently dropped" gap cannot occur here. WHEN the
3379
+ // SDK pin bumps to 0.79.0+ (which adds project-trust), pass an explicit
3380
+ // `settingsManager: SettingsManager.create(cfg.cwd, getAgentDir(), { projectTrusted: true })`
3381
+ // so headless trust defaults TRUSTED — and do NOT re-introduce the CLI's
3382
+ // resolveProjectTrust, which returns false with no TTY.
3383
+ //
3384
+ // T3 session-rebind: services + model + session creation is factored into
3385
+ // `buildForManager` so it can serve BOTH the initial boot AND the
3386
+ // AgentSessionRuntime factory it is reused as below (new_session/
3387
+ // switch_session/fork rebuild the session by re-invoking it for a new
3388
+ // SessionManager). Mirrors pi main.js's `createRuntime`.
3389
+ const agentDir = getAgentDir();
3390
+ const buildForManager = async (o) => {
3391
+ // pi 0.82 owns credentials through an app-supplied CredentialStore. Hand it
3392
+ // crouter's, which serves managed providers straight from the subscription
3393
+ // pool and everything else from auth.json — both read fresh under their lock
3394
+ // on every call, because the rotation extension, `/provider-sub`, and the
3395
+ // daemon's login fan all write out of band mid-session and pi's own default
3396
+ // store would serve a cached view with no reload seam.
3397
+ const { authPath, modelsPath } = modelRuntimePaths(o.agentDir);
3398
+ const modelRuntime = await ModelRuntime.create({
3399
+ credentials: new PoolCredentialStore(authPath),
3400
+ modelsPath,
3401
+ });
3402
+ const services = await engine.createAgentSessionServices({
3403
+ cwd: o.cwd,
3404
+ agentDir: o.agentDir,
3405
+ modelRuntime,
3406
+ resourceLoaderOptions: {
3407
+ additionalExtensionPaths: cfg.extensionPaths,
3408
+ appendSystemPrompt: cfg.appendSystemPromptPath !== undefined ? [cfg.appendSystemPromptPath] : undefined,
3409
+ // Suppress pi's <project_context> AGENTS.md/CLAUDE.md injection in the
3410
+ // SYSTEM PROMPT unconditionally — crouter reads project guidance through
3411
+ // the document substrate instead (a migrated AGENTS.md doc rides the
3412
+ // <memory kind="knowledge"> block in the first-message bearings; see
3413
+ // bearings.ts and `crtr sys sync project-guidance`), never pi's own file-based loader.
3414
+ noContextFiles: true,
3415
+ },
3416
+ });
3417
+ const extensionResult = services.resourceLoader.getExtensions();
3418
+ // buildBrokerSession also supports standalone SDK consumers (for example a
3419
+ // custom-provider extension test) that are not canvas nodes. Production
3420
+ // canvas invocations always carry the full mandatory set after preflight;
3421
+ // once any canvas hook is requested, require every one to load.
3422
+ if (cfg.extensionPaths.some((path) => CANVAS_EXTENSIONS.includes(path))) {
3423
+ const loadedPaths = new Set(extensionResult.extensions.map((extension) => extension.path));
3424
+ const missingHooks = CANVAS_EXTENSIONS.filter((path) => !loadedPaths.has(path));
3425
+ if (missingHooks.length > 0) {
3426
+ const errors = extensionResult.errors
3427
+ .filter((error) => CANVAS_EXTENSIONS.includes(error.path))
3428
+ .map((error) => `${error.path}: ${error.error}`)
3429
+ .join('; ');
3430
+ throw new Error(`[broker] FATAL: mandatory canvas extensions failed to load: ${missingHooks.join(', ')}${errors === '' ? '' : ` (${errors})`}`);
3431
+ }
3432
+ }
3433
+ // 3. Resolve one viable model — resolveLaunchModel (module-scoped above) is
3434
+ // the SOLE launch selection policy, shared with the reload_auth re-drive so
3435
+ // a later /login re-runs the identical decision against this same `services`.
3436
+ const resolved = await resolveLaunchModel(services, cfg);
3437
+ // Tier 3: no viable candidate at all. Do NOT fault — construct a live
3438
+ // model-less session so the socket/listener still comes up and the user can
3439
+ // /login; suppress the SDK's own findInitialModel auto-selection
3440
+ // (modelLessRegistry) so a stale global settings.json default or an
3441
+ // available-but-policy-rejected model can't sneak in behind crouter's back.
3442
+ const model = resolved?.model;
3443
+ const thinkingLevel = resolved?.thinkingLevel;
3444
+ const modelLess = resolved === undefined ? modelLessRegistry(services.modelRuntime) : undefined;
3445
+ const constructionServices = modelLess !== undefined
3446
+ ? { ...services, modelRuntime: modelLess.runtime }
3447
+ : services;
3448
+ const created = await engine.createAgentSessionFromServices({
3449
+ services: constructionServices,
3450
+ sessionManager: o.sessionManager,
3451
+ model,
3452
+ thinkingLevel,
3453
+ tools: cfg.tools,
3454
+ sessionStartEvent: o.sessionStartEvent,
3455
+ });
3456
+ // CONSTRUCTION-ONLY MASK — see modelLessRegistry: reveal now that model
3457
+ // selection (construction-time only) is done, so a later /login re-drive's
3458
+ // list_models/cycle_model/provider-rotation see the real registry.
3459
+ modelLess?.reveal();
3460
+ return { ...created, services };
3461
+ };
3462
+ // 4. Select the SessionManager: FORK (spawn-time --fork), OPEN (resume), or
3463
+ // CREATE (fresh). The broker is the sole writer of the resulting .jsonl.
3464
+ const resumePath = cfg.resumeSessionPath;
3465
+ const forking = cfg.forkFrom !== undefined && cfg.forkFrom !== '';
3466
+ if (!forking &&
3467
+ (resumePath === undefined || resumePath === '') &&
3468
+ cfg.resumeSessionId !== undefined &&
3469
+ cfg.resumeSessionId !== '') {
3470
+ // SessionManager.open() takes a FILE path — it only path-normalizes; it does
3471
+ // NOT resolve a bare uuid to its .jsonl the way pi's CLI does (via
3472
+ // SessionManager.list). A bare-id-only resume would open a nonexistent
3473
+ // <cwd>/<uuid> and silently start an EMPTY session. Fail loud: revive always
3474
+ // passes the .jsonl path, so a bare id means an old node that never captured
3475
+ // pi_session_file — mis-resuming it silently is worse than a crash.
3476
+ throw new Error(`[broker] resume requires a session .jsonl PATH; got only a bare id ` +
3477
+ `'${cfg.resumeSessionId}' (pi_session_file was never captured for this node)`);
3478
+ }
3479
+ // A fork is a fresh spawn (resuming=false) so its kickoff firstPrompt fires: a
3480
+ // `node new --fork-from <id>` gives the new node the source's full history AND a
3481
+ // new task to start on. An ordinary resume supplies no prompt, but a
3482
+ // broker-certified runtime-abort resume deliberately carries a continuation.
3483
+ //
3484
+ // A refresh-yield CYCLES instead: `cfg.newCycle` (the `--crtr-new-cycle` token)
3485
+ // marks a revive that reuses this SAME `.jsonl` rather than resuming it — the
3486
+ // session manager opens the file, then resets the leaf and roots a `crtr-cycle`
3487
+ // marker entry (data: `{cycle, fromLeaf}`) as the new cycle's ROOT before the
3488
+ // session is built. Custom entries never enter LLM context, so the freshly
3489
+ // rooted branch is exactly as clean as a brand-new session; `cycleAwareMessages`
3490
+ // (session-cycles.ts) is the display-side counterpart that replays every prior
3491
+ // cycle for snapshot rendering. cycling ⇒ resuming=false, so the kickoff
3492
+ // firstPrompt fires below just like a true fresh launch.
3493
+ const hasSessionPath = !forking && resumePath !== undefined && resumePath !== '';
3494
+ const cycling = hasSessionPath && cfg.newCycle === true;
3495
+ const resuming = hasSessionPath && !cycling;
3496
+ let sessionManager;
3497
+ if (forking) {
3498
+ // Spawn-time fork via pi's REAL fork seam, SessionManager.forkFrom — the exact
3499
+ // method pi's own `--fork` CLI flag uses (main.js forkSessionOrExit). It writes
3500
+ // a NEW session file with a NEW id and a `parentSession` header pointing at the
3501
+ // source, then copies the source's history into it. NOT a naïve `.jsonl` copy
3502
+ // (which would collide session ids and drop the fork metadata), and NOT
3503
+ // runtime.fork() (which forks an entry WITHIN an already-loaded session — there
3504
+ // is no current session to fork at boot). The caller (spawn.ts resolveForkSource)
3505
+ // resolves cfg.forkFrom to an absolute source .jsonl before launch, so a bad ref
3506
+ // fails loudly here (forkFrom throws on an empty/invalid/header-less source).
3507
+ sessionManager = engine.SessionManager.forkFrom(cfg.forkFrom, cfg.cwd);
3508
+ }
3509
+ else if (resuming || cycling) {
3510
+ sessionManager = engine.SessionManager.open(resumePath);
3511
+ if (cycling) {
3512
+ const fromLeaf = sessionManager.getLeafId();
3513
+ sessionManager.resetLeaf();
3514
+ sessionManager.appendCustomEntry(CRTR_CYCLE_CUSTOM_TYPE, {
3515
+ cycle: Number(process.env['CRTR_CYCLES']),
3516
+ fromLeaf,
3517
+ });
3518
+ }
3519
+ }
3520
+ else {
3521
+ sessionManager = engine.SessionManager.create(cfg.cwd);
3522
+ }
3523
+ // When the engine exposes the replacement API (real SDK), wrap the builder in an
3524
+ // AgentSessionRuntime so new_session/switch_session/fork work; the runtime
3525
+ // re-invokes `buildForManager` for each new SessionManager. The fake-engine
3526
+ // fixture omits it — fall back to a single direct build (those three ops then
3527
+ // reply error{engine_error}).
3528
+ if (engine.createAgentSessionRuntime !== undefined) {
3529
+ const factory = async (o) => {
3530
+ const r = await buildForManager({
3531
+ cwd: o.cwd,
3532
+ agentDir: o.agentDir,
3533
+ sessionManager: o.sessionManager,
3534
+ sessionStartEvent: o.sessionStartEvent,
3535
+ });
3536
+ return { ...r, diagnostics: r.services.diagnostics ?? [] };
3537
+ };
3538
+ const runtime = await engine.createAgentSessionRuntime(factory, {
3539
+ cwd: cfg.cwd,
3540
+ agentDir,
3541
+ sessionManager,
3542
+ });
3543
+ return { session: runtime.session, services: runtime.services, resuming, runtime };
3544
+ }
3545
+ const r = await buildForManager({ cwd: cfg.cwd, agentDir, sessionManager });
3546
+ return { session: r.session, services: r.services, resuming, runtime: undefined };
3547
+ }
3548
+ export function makeBrokerUiContext(deps) {
3549
+ // The dialog router. C2 (scout mq5thyli): the design §5.4 premise — that an
3550
+ // unattended dialog auto-resolves on its OWN timeout — is FALSE. `timeout` is
3551
+ // OPTIONAL on dialog opts, editor() takes none, and almost no real extension
3552
+ // passes one (permission-gate / confirm-destructive / plan-mode / subagent all
3553
+ // omit it). A timeout-reliant unattended node therefore deadlocks the agent
3554
+ // turn FOREVER. So with NO WRITABLE client attached we fall back to the SDK's
3555
+ // noOp UI behavior — resolve to the default (deny / cancel / undefined)
3556
+ // IMMEDIATELY, never arming a timer, never waiting. With one or more, the dialog
3557
+ // is fanned out to all of them under a broker-side timeout+abort, so viewers
3558
+ // that attach but never answer cannot hang the turn either.
3559
+ const dialogPromise = (defaultValue, request, parse, opts) => {
3560
+ if (opts?.signal?.aborted)
3561
+ return Promise.resolve(defaultValue);
3562
+ // Snapshot the writable set ONCE, at raise time — the fan-out targets and the
3563
+ // zero-answerer decision must agree.
3564
+ const targets = deps.writable();
3565
+ // C2 (Wave-0, KEEP): nobody can answer at raise time → noOp, resolved at once.
3566
+ // No timer, no wait, no deadlock.
3567
+ if (targets.length === 0)
3568
+ return Promise.resolve(defaultValue);
3569
+ // At least one writable client: register the dialog FIRST (so a client that
3570
+ // attaches mid-dialog gets it replayed and joins the race), fan it out to every
3571
+ // target, and ALWAYS arm a broker-side timeout (C2 anti-deadlock): viewers that
3572
+ // never answer — or all detach — can never hang the turn. Honor a shorter
3573
+ // per-dialog timeout if the extension passed one; otherwise the broker default.
3574
+ // On fire it resolves to the SAFE default (deny/cancel/undefined). A detach
3575
+ // does NOT cancel this — only an answer, the timeout, or an abort.
3576
+ //
3577
+ // EVERY settlement path runs the same `cleanup` (drop the entry, so a later
3578
+ // answer for this id is a no-op) and then broadcasts `extension_ui_dismiss`
3579
+ // keyed to this id, so all peers close their copy of exactly this dialog and
3580
+ // nothing else.
3581
+ return new Promise((resolve) => {
3582
+ let timer;
3583
+ const cleanup = () => {
3584
+ if (timer !== undefined)
3585
+ clearTimeout(timer);
3586
+ opts?.signal?.removeEventListener('abort', onAbort);
3587
+ deps.pending.delete(request.id);
3588
+ };
3589
+ const onAbort = () => {
3590
+ cleanup();
3591
+ // The extension aborted this request out-of-band (e.g. an OAuth loopback
3592
+ // callback won the race against a still-open manual-paste dialog). Tell
3593
+ // every peer to tear down THIS overlay by id — an unanswered dialog whose
3594
+ // broker entry we just dropped would otherwise linger onscreen with nothing
3595
+ // left to answer it. Keyed to request.id so only the abandoned dialog is
3596
+ // dismissed, never an unrelated one.
3597
+ deps.broadcast({ type: 'extension_ui_dismiss', id: request.id });
3598
+ resolve(defaultValue);
3599
+ };
3600
+ opts?.signal?.addEventListener('abort', onAbort, { once: true });
3601
+ // N1: honor the extension's EXPLICIT per-dialog timeout if it passed one
3602
+ // (longer or shorter than the broker default is fine — both are bounded);
3603
+ // otherwise fall back to the broker default. The broker never RELIES on the
3604
+ // extension having passed one (C2).
3605
+ const ms = opts?.timeout !== undefined ? opts.timeout : DEFAULT_DIALOG_TIMEOUT_MS;
3606
+ timer = setTimeout(() => {
3607
+ cleanup();
3608
+ // Same correlated teardown as onAbort: the broker resolved this dialog on
3609
+ // its own timeout, so every peer's overlay (none of which has an
3610
+ // independent timer) must be dismissed by id or it lingers forever.
3611
+ deps.broadcast({ type: 'extension_ui_dismiss', id: request.id });
3612
+ resolve(defaultValue);
3613
+ }, ms);
3614
+ if (typeof timer.unref === 'function')
3615
+ timer.unref();
3616
+ deps.pending.set(request.id, {
3617
+ request,
3618
+ resolve: (r) => {
3619
+ cleanup();
3620
+ // The first answer settled it; tell every OTHER client that was fanned
3621
+ // this dialog (and the answerer, for whom it is an idempotent no-op) to
3622
+ // close its copy. Without this, peers would sit on an overlay whose
3623
+ // answer can never arrive.
3624
+ deps.broadcast({ type: 'extension_ui_dismiss', id: request.id });
3625
+ resolve(parse(r));
3626
+ },
3627
+ });
3628
+ for (const target of targets)
3629
+ deps.forward(target, request);
3630
+ });
3631
+ };
3632
+ const noop = () => { };
3633
+ const ctx = {
3634
+ [MANAGED_PROVIDER_ROTATION_POLICY_UI]: deps.providerRotationPolicy ?? (() => ({ allowCrossProviderFallback: true })),
3635
+ // The broker-owned known-stream-wait control (see BrokerDialogDeps): a
3636
+ // deliberate provider cooldown is KNOWN liveness, so the provider extension
3637
+ // reaches this through the stable symbol to pause the dead-stream watchdog for
3638
+ // the wait's duration. A no-op when the host supplied none.
3639
+ [KNOWN_STREAM_WAIT_UI]: deps.onKnownStreamWait ?? ((_deadlineMs) => () => { }),
3640
+ // pi's ExtensionUIContext exposes `theme`; package extensions (e.g. the OAuth
3641
+ // billing adapter's status rendering) read it from ANY lifecycle hook. The
3642
+ // SDK's noOpUIContext returns the process-global theme proxy here; we mirror
3643
+ // that by reading the same global symbol (populated by initTheme() at broker
3644
+ // boot — see runBroker). Without this property `ctx.ui.theme` is undefined and
3645
+ // the hook throws, which the runner silently swallows while dropping the hook's
3646
+ // returned payload (the OAuth 400 root cause).
3647
+ get theme() {
3648
+ const t = globalThis[Symbol.for('@earendil-works/pi-coding-agent:theme')];
3649
+ if (t === undefined || t === null) {
3650
+ throw new Error('Theme not initialized. Call initTheme() first.');
3651
+ }
3652
+ return t;
3653
+ },
3654
+ select: (title, options, opts) => dialogPromise(undefined, { type: 'extension_ui_request', id: randomUUID(), method: 'select', title, options, timeout: opts?.timeout }, (r) => ('cancelled' in r && r.cancelled ? undefined : 'value' in r ? r.value : undefined), opts),
3655
+ confirm: (title, message, opts) => dialogPromise(false, { type: 'extension_ui_request', id: randomUUID(), method: 'confirm', title, message, timeout: opts?.timeout }, (r) => ('cancelled' in r && r.cancelled ? false : 'confirmed' in r ? r.confirmed : false), opts),
3656
+ input: (title, placeholder, opts) => dialogPromise(undefined, { type: 'extension_ui_request', id: randomUUID(), method: 'input', title, placeholder, timeout: opts?.timeout }, (r) => ('cancelled' in r && r.cancelled ? undefined : 'value' in r ? r.value : undefined), opts),
3657
+ editor: (title, prefill) => dialogPromise(undefined, { type: 'extension_ui_request', id: randomUUID(), method: 'editor', title, prefill }, (r) => ('cancelled' in r && r.cancelled ? undefined : 'value' in r ? r.value : undefined)),
3658
+ // Non-blocking extension-UI relay: broadcast these to all viewers instead of
3659
+ // dropping them, in pi's OWN `extension_ui_request` envelope (rpc-mode.js
3660
+ // emits these three identically). The viewer owns the slots that render them,
3661
+ // and the broker retains their last value for the welcome snapshot. The other
3662
+ // non-blocking methods stay inert.
3663
+ setStatus: (key, text) => deps.broadcast({
3664
+ type: 'extension_ui_request',
3665
+ id: randomUUID(),
3666
+ method: 'setStatus',
3667
+ statusKey: key,
3668
+ statusText: text,
3669
+ }),
3670
+ setWidget: (key, content, options) => {
3671
+ // setWidget's component-factory overload cannot be serialized over the
3672
+ // socket (R1 caveat) — drop it and relay only the string[] form, exactly as
3673
+ // pi's own RPC setWidget carries only `widgetLines`.
3674
+ if (typeof content === 'function')
3675
+ return;
3676
+ deps.broadcast({
3677
+ type: 'extension_ui_request',
3678
+ id: randomUUID(),
3679
+ method: 'setWidget',
3680
+ widgetKey: key,
3681
+ widgetLines: content,
3682
+ widgetPlacement: options?.placement,
3683
+ });
3684
+ },
3685
+ setTitle: (title) => deps.broadcast({ type: 'extension_ui_request', id: randomUUID(), method: 'setTitle', title }),
3686
+ notify: (message, type) => {
3687
+ const notifyType = type === 'warn' ? 'warning' : type;
3688
+ deps.broadcast({ type: 'extension_ui_request', id: randomUUID(), method: 'notify', message, notifyType });
3689
+ },
3690
+ onTerminalInput: () => noop,
3691
+ setWorkingMessage: noop,
3692
+ setWorkingVisible: noop,
3693
+ setWorkingIndicator: noop,
3694
+ setHiddenThinkingLabel: noop,
3695
+ setFooter: noop,
3696
+ setHeader: noop,
3697
+ custom: () => Promise.resolve(undefined),
3698
+ pasteToEditor: noop,
3699
+ setEditorText: noop,
3700
+ getEditorText: () => '',
3701
+ addAutocompleteProvider: noop,
3702
+ setEditorComponent: noop,
3703
+ getEditorComponent: () => undefined,
3704
+ getAllThemes: () => [],
3705
+ getTheme: () => undefined,
3706
+ setTheme: noop,
3707
+ getToolsExpanded: () => false,
3708
+ setToolsExpanded: noop,
3709
+ };
3710
+ return ctx;
3711
+ }