@crouton-kit/crouter 0.3.18 → 0.3.20

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 (289) hide show
  1. package/dist/build-root.js +6 -0
  2. package/dist/builtin-memory/crouter-development/marketplaces.md +164 -0
  3. package/dist/builtin-memory/crouter-development/personas/base-prompt.md +58 -0
  4. package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +60 -0
  5. package/dist/builtin-memory/crouter-development/personas.md +117 -0
  6. package/dist/builtin-memory/crouter-development/plugins.md +163 -0
  7. package/dist/builtin-memory/design.md +60 -0
  8. package/dist/builtin-memory/development.md +116 -0
  9. package/dist/builtin-memory/planning.md +66 -0
  10. package/dist/builtin-memory/spec.md +99 -0
  11. package/dist/builtin-personas/design/PERSONA.md +1 -0
  12. package/dist/builtin-personas/design/orchestrator.md +2 -1
  13. package/dist/builtin-personas/developer/PERSONA.md +1 -0
  14. package/dist/builtin-personas/developer/orchestrator.md +2 -1
  15. package/dist/builtin-personas/explore/PERSONA.md +1 -0
  16. package/dist/builtin-personas/explore/orchestrator.md +4 -0
  17. package/dist/builtin-personas/general/PERSONA.md +1 -0
  18. package/dist/builtin-personas/general/orchestrator.md +4 -0
  19. package/dist/builtin-personas/lifecycle/resident.md +1 -1
  20. package/dist/builtin-personas/lifecycle/terminal.md +5 -2
  21. package/dist/builtin-personas/orchestration-kernel.md +15 -24
  22. package/dist/builtin-personas/plan/PERSONA.md +1 -0
  23. package/dist/builtin-personas/plan/orchestrator.md +2 -1
  24. package/dist/builtin-personas/plan/reviewers/architecture-fit/PERSONA.md +1 -0
  25. package/dist/builtin-personas/plan/reviewers/code-smells/PERSONA.md +1 -0
  26. package/dist/builtin-personas/plan/reviewers/pattern-consistency/PERSONA.md +1 -0
  27. package/dist/builtin-personas/plan/reviewers/requirements-coverage/PERSONA.md +1 -0
  28. package/dist/builtin-personas/plan/reviewers/security/PERSONA.md +1 -0
  29. package/dist/builtin-personas/review/PERSONA.md +1 -0
  30. package/dist/builtin-personas/review/orchestrator.md +4 -0
  31. package/dist/builtin-personas/runtime-base.md +6 -3
  32. package/dist/builtin-personas/spec/PERSONA.md +1 -0
  33. package/dist/builtin-personas/spec/orchestrator.md +2 -1
  34. package/dist/builtin-personas/waiting.md +8 -0
  35. package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +1 -1
  36. package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +2 -2
  37. package/dist/builtin-views/_lib/states.mjs +161 -0
  38. package/dist/builtin-views/canvas/client.mjs +303 -0
  39. package/dist/builtin-views/canvas/view.mjs +576 -0
  40. package/dist/builtin-views/git-pr/client.mjs +440 -0
  41. package/dist/builtin-views/git-pr/view.mjs +675 -0
  42. package/dist/builtin-views/inbox/_lib/render.mjs +177 -0
  43. package/dist/builtin-views/inbox/sources/gmail.mjs +965 -0
  44. package/dist/builtin-views/inbox/sources/linkedin.mjs +427 -0
  45. package/dist/builtin-views/inbox/view.mjs +889 -0
  46. package/dist/builtin-views/linkedin/client.mjs +610 -0
  47. package/dist/builtin-views/linkedin/view.mjs +1171 -0
  48. package/dist/clients/attach/attach-cmd.d.ts +2 -0
  49. package/dist/clients/attach/attach-cmd.js +354 -0
  50. package/dist/clients/attach/chat-view.d.ts +77 -0
  51. package/dist/clients/attach/chat-view.js +450 -0
  52. package/dist/clients/attach/clipboard-image.d.ts +16 -0
  53. package/dist/clients/attach/clipboard-image.js +113 -0
  54. package/dist/clients/attach/config-load.d.ts +31 -0
  55. package/dist/clients/attach/config-load.js +113 -0
  56. package/dist/clients/attach/extension-dialogs.d.ts +29 -0
  57. package/dist/clients/attach/extension-dialogs.js +101 -0
  58. package/dist/clients/attach/input-controller.d.ts +54 -0
  59. package/dist/clients/attach/input-controller.js +204 -0
  60. package/dist/clients/attach/slash-commands.d.ts +36 -0
  61. package/dist/clients/attach/slash-commands.js +200 -0
  62. package/dist/clients/attach/view-socket.d.ts +48 -0
  63. package/dist/clients/attach/view-socket.js +126 -0
  64. package/dist/commands/attention.js +3 -3
  65. package/dist/commands/canvas-prune.js +1 -1
  66. package/dist/commands/daemon.js +3 -2
  67. package/dist/commands/human/prompts.js +1 -1
  68. package/dist/commands/human/queue.js +54 -8
  69. package/dist/commands/human/shared.d.ts +2 -0
  70. package/dist/commands/memory/find.d.ts +1 -0
  71. package/dist/commands/memory/find.js +180 -0
  72. package/dist/commands/memory/lint.d.ts +1 -0
  73. package/dist/commands/memory/lint.js +140 -0
  74. package/dist/commands/memory/list.d.ts +1 -0
  75. package/dist/commands/memory/list.js +79 -0
  76. package/dist/commands/memory/read.js +103 -0
  77. package/dist/commands/memory/shared.d.ts +30 -0
  78. package/dist/commands/memory/shared.js +122 -0
  79. package/dist/commands/memory/write.d.ts +1 -0
  80. package/dist/commands/memory/write.js +85 -0
  81. package/dist/commands/memory.d.ts +2 -0
  82. package/dist/commands/memory.js +27 -0
  83. package/dist/commands/node.d.ts +3 -2
  84. package/dist/commands/node.js +610 -61
  85. package/dist/commands/pkg/market-manage.js +1 -1
  86. package/dist/commands/push.js +6 -6
  87. package/dist/commands/revive.js +1 -1
  88. package/dist/commands/skill/author.js +1 -1
  89. package/dist/commands/skill/shared.d.ts +1 -8
  90. package/dist/commands/skill/shared.js +2 -55
  91. package/dist/commands/skill.js +9 -14
  92. package/dist/commands/sys/doctor.js +1 -1
  93. package/dist/commands/sys/update.js +1 -1
  94. package/dist/commands/view-cycle.d.ts +2 -0
  95. package/dist/commands/view-cycle.js +125 -0
  96. package/dist/commands/view-list.d.ts +2 -0
  97. package/dist/commands/view-list.js +66 -0
  98. package/dist/commands/view-new.d.ts +2 -0
  99. package/dist/commands/view-new.js +70 -0
  100. package/dist/commands/view-pick.d.ts +2 -0
  101. package/dist/commands/view-pick.js +119 -0
  102. package/dist/commands/view-run.d.ts +2 -0
  103. package/dist/commands/view-run.js +191 -0
  104. package/dist/commands/view.d.ts +2 -0
  105. package/dist/commands/view.js +29 -0
  106. package/dist/core/__tests__/broker-attach-limits.test.d.ts +1 -0
  107. package/dist/core/__tests__/broker-attach-limits.test.js +157 -0
  108. package/dist/core/__tests__/broker-attach-stream.test.d.ts +1 -0
  109. package/dist/core/__tests__/broker-attach-stream.test.js +125 -0
  110. package/dist/core/__tests__/broker-crash-teardown.test.d.ts +1 -0
  111. package/dist/core/__tests__/broker-crash-teardown.test.js +116 -0
  112. package/dist/core/__tests__/broker-dialogs.test.d.ts +1 -0
  113. package/dist/core/__tests__/broker-dialogs.test.js +126 -0
  114. package/dist/core/__tests__/broker-dormant-wake.test.d.ts +1 -0
  115. package/dist/core/__tests__/broker-dormant-wake.test.js +51 -0
  116. package/dist/core/__tests__/broker-lifecycle.test.d.ts +1 -0
  117. package/dist/core/__tests__/broker-lifecycle.test.js +87 -0
  118. package/dist/core/__tests__/broker-sdk-wiring.test.d.ts +1 -0
  119. package/dist/core/__tests__/broker-sdk-wiring.test.js +166 -0
  120. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.d.ts +1 -0
  121. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +102 -0
  122. package/dist/core/__tests__/canvas-inbox-watcher.test.js +2 -36
  123. package/dist/core/__tests__/cascade-close.test.js +12 -2
  124. package/dist/core/__tests__/child-death-wake.test.d.ts +1 -0
  125. package/dist/core/__tests__/child-death-wake.test.js +245 -0
  126. package/dist/core/__tests__/context-intro.test.js +76 -62
  127. package/dist/core/__tests__/daemon-boot.test.js +14 -5
  128. package/dist/core/__tests__/daemon-liveness.test.js +34 -9
  129. package/dist/core/__tests__/detach-focus.test.d.ts +1 -0
  130. package/dist/core/__tests__/detach-focus.test.js +206 -0
  131. package/dist/core/__tests__/draw-style.test.d.ts +1 -0
  132. package/dist/core/__tests__/draw-style.test.js +258 -0
  133. package/dist/core/__tests__/fixtures/c3-custom-provider-ext.d.ts +2 -0
  134. package/dist/core/__tests__/fixtures/c3-custom-provider-ext.js +18 -0
  135. package/dist/core/__tests__/fixtures/fake-engine.d.ts +138 -0
  136. package/dist/core/__tests__/fixtures/fake-engine.js +614 -0
  137. package/dist/core/__tests__/fixtures/fake-pi-host.js +1 -0
  138. package/dist/core/__tests__/flagship-lifecycle.test.js +7 -1
  139. package/dist/core/__tests__/frame-decoder-perf.test.d.ts +1 -0
  140. package/dist/core/__tests__/frame-decoder-perf.test.js +198 -0
  141. package/dist/core/__tests__/helpers/broker-clients.d.ts +43 -0
  142. package/dist/core/__tests__/helpers/broker-clients.js +178 -0
  143. package/dist/core/__tests__/helpers/harness.d.ts +9 -0
  144. package/dist/core/__tests__/helpers/harness.js +111 -1
  145. package/dist/core/__tests__/home-session.test.js +41 -8
  146. package/dist/core/__tests__/human-new-window-regression.test.d.ts +1 -0
  147. package/dist/core/__tests__/human-new-window-regression.test.js +101 -0
  148. package/dist/core/__tests__/kickoff.test.js +37 -3
  149. package/dist/core/__tests__/live-mutation-verbs.test.d.ts +1 -0
  150. package/dist/core/__tests__/live-mutation-verbs.test.js +175 -0
  151. package/dist/core/__tests__/live-mutation.test.js +6 -132
  152. package/dist/core/__tests__/memory.test.js +23 -115
  153. package/dist/core/__tests__/placement-focus.test.js +5 -0
  154. package/dist/core/__tests__/placement-teardown.test.js +54 -11
  155. package/dist/core/__tests__/relaunch.test.js +4 -3
  156. package/dist/core/__tests__/review-render-pane-regression.test.d.ts +1 -0
  157. package/dist/core/__tests__/review-render-pane-regression.test.js +133 -0
  158. package/dist/core/__tests__/spawn-root.test.js +10 -0
  159. package/dist/core/__tests__/spike-harness.test.js +1 -0
  160. package/dist/core/__tests__/wake-bearings.test.d.ts +1 -0
  161. package/dist/core/__tests__/wake-bearings.test.js +156 -0
  162. package/dist/core/__tests__/wake-origin.test.d.ts +1 -0
  163. package/dist/core/__tests__/wake-origin.test.js +110 -0
  164. package/dist/core/bootstrap.js +1 -1
  165. package/dist/core/canvas/browse/__tests__/render.test.js +1 -0
  166. package/dist/core/canvas/browse/app.js +24 -2
  167. package/dist/core/canvas/browse/model.d.ts +38 -2
  168. package/dist/core/canvas/browse/model.js +134 -10
  169. package/dist/core/canvas/browse/render.d.ts +6 -12
  170. package/dist/core/canvas/browse/render.js +72 -104
  171. package/dist/core/canvas/canvas.js +16 -23
  172. package/dist/core/canvas/db.js +46 -0
  173. package/dist/core/canvas/index.d.ts +1 -0
  174. package/dist/core/canvas/index.js +1 -0
  175. package/dist/core/canvas/pid.d.ts +4 -0
  176. package/dist/core/canvas/pid.js +23 -0
  177. package/dist/core/canvas/render.d.ts +11 -2
  178. package/dist/core/canvas/render.js +69 -0
  179. package/dist/core/canvas/types.d.ts +91 -3
  180. package/dist/core/canvas/wakeups.d.ts +76 -0
  181. package/dist/core/canvas/wakeups.js +185 -0
  182. package/dist/core/config.js +4 -1
  183. package/dist/core/frontmatter.js +37 -124
  184. package/dist/core/memory-resolver.d.ts +49 -0
  185. package/dist/core/memory-resolver.js +141 -0
  186. package/dist/core/personas/loader.d.ts +7 -0
  187. package/dist/core/personas/loader.js +36 -5
  188. package/dist/core/personas/resolve.js +13 -3
  189. package/dist/core/predicate.d.ts +63 -0
  190. package/dist/core/predicate.js +189 -0
  191. package/dist/core/resolver.js +26 -5
  192. package/dist/core/runtime/bearings.d.ts +53 -12
  193. package/dist/core/runtime/bearings.js +132 -59
  194. package/dist/core/runtime/broker-cli.d.ts +1 -0
  195. package/dist/core/runtime/broker-cli.js +46 -0
  196. package/dist/core/runtime/broker-protocol.d.ts +332 -0
  197. package/dist/core/runtime/broker-protocol.js +153 -0
  198. package/dist/core/runtime/broker-sdk.d.ts +48 -0
  199. package/dist/core/runtime/broker-sdk.js +72 -0
  200. package/dist/core/runtime/broker.d.ts +55 -0
  201. package/dist/core/runtime/broker.js +1128 -0
  202. package/dist/core/runtime/close.js +35 -6
  203. package/dist/core/runtime/host.d.ts +53 -0
  204. package/dist/core/runtime/host.js +186 -0
  205. package/dist/core/runtime/kickoff.d.ts +2 -1
  206. package/dist/core/runtime/kickoff.js +91 -5
  207. package/dist/core/runtime/launch.d.ts +54 -4
  208. package/dist/core/runtime/launch.js +85 -5
  209. package/dist/core/runtime/lifecycle.js +23 -6
  210. package/dist/core/runtime/memory.d.ts +2 -42
  211. package/dist/core/runtime/memory.js +11 -162
  212. package/dist/core/runtime/nodes.d.ts +37 -0
  213. package/dist/core/runtime/nodes.js +60 -1
  214. package/dist/core/runtime/persona.js +21 -11
  215. package/dist/core/runtime/pi-vendored.d.ts +18 -0
  216. package/dist/core/runtime/pi-vendored.js +49 -0
  217. package/dist/core/runtime/placement.d.ts +32 -14
  218. package/dist/core/runtime/placement.js +206 -52
  219. package/dist/core/runtime/promote.d.ts +0 -6
  220. package/dist/core/runtime/promote.js +4 -12
  221. package/dist/core/runtime/{demote.d.ts → recycle.d.ts} +5 -5
  222. package/dist/core/runtime/{demote.js → recycle.js} +27 -11
  223. package/dist/core/runtime/reset.js +11 -8
  224. package/dist/core/runtime/revive.d.ts +2 -0
  225. package/dist/core/runtime/revive.js +34 -34
  226. package/dist/core/runtime/spawn.d.ts +23 -0
  227. package/dist/core/runtime/spawn.js +77 -23
  228. package/dist/core/runtime/stop-guard.d.ts +1 -1
  229. package/dist/core/runtime/stop-guard.js +6 -1
  230. package/dist/core/runtime/tmux-chrome.d.ts +1 -1
  231. package/dist/core/runtime/tmux-chrome.js +1 -1
  232. package/dist/core/runtime/tmux.d.ts +28 -0
  233. package/dist/core/runtime/tmux.js +80 -6
  234. package/dist/core/scope.d.ts +11 -0
  235. package/dist/core/scope.js +39 -0
  236. package/dist/core/spawn.d.ts +5 -0
  237. package/dist/core/spawn.js +20 -2
  238. package/dist/core/substrate/gate.d.ts +13 -0
  239. package/dist/core/substrate/gate.js +21 -0
  240. package/dist/core/substrate/index.d.ts +7 -0
  241. package/dist/core/substrate/index.js +18 -0
  242. package/dist/core/substrate/on-read.d.ts +14 -0
  243. package/dist/core/substrate/on-read.js +292 -0
  244. package/dist/core/substrate/render.d.ts +25 -0
  245. package/dist/core/substrate/render.js +256 -0
  246. package/dist/core/substrate/schema.d.ts +76 -0
  247. package/dist/core/substrate/schema.js +124 -0
  248. package/dist/core/substrate/session-cache.d.ts +30 -0
  249. package/dist/core/substrate/session-cache.js +77 -0
  250. package/dist/core/substrate/subject.d.ts +41 -0
  251. package/dist/core/substrate/subject.js +54 -0
  252. package/dist/core/tui/contract.d.ts +83 -0
  253. package/dist/core/tui/contract.js +8 -0
  254. package/dist/core/tui/draw.d.ts +96 -0
  255. package/dist/core/tui/draw.js +339 -0
  256. package/dist/core/tui/host.d.ts +29 -0
  257. package/dist/core/tui/host.js +379 -0
  258. package/dist/core/tui/loader.d.ts +16 -0
  259. package/dist/core/tui/loader.js +94 -0
  260. package/dist/core/{canvas/browse → tui}/terminal.js +7 -6
  261. package/dist/core/wake.d.ts +86 -0
  262. package/dist/core/wake.js +308 -0
  263. package/dist/daemon/crtrd.d.ts +29 -4
  264. package/dist/daemon/crtrd.js +662 -46
  265. package/dist/pi-extensions/__tests__/canvas-context-intro.test.d.ts +1 -0
  266. package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +171 -0
  267. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +19 -12
  268. package/dist/pi-extensions/canvas-commands.d.ts +3 -0
  269. package/dist/pi-extensions/canvas-commands.js +10 -0
  270. package/dist/pi-extensions/canvas-context-intro.d.ts +17 -0
  271. package/dist/pi-extensions/canvas-context-intro.js +55 -15
  272. package/dist/pi-extensions/canvas-doc-substrate.d.ts +44 -0
  273. package/dist/pi-extensions/canvas-doc-substrate.js +112 -0
  274. package/dist/pi-extensions/canvas-nav.d.ts +3 -0
  275. package/dist/pi-extensions/canvas-nav.js +87 -23
  276. package/dist/pi-extensions/canvas-stophook.js +17 -8
  277. package/dist/pi-extensions/canvas-view.d.ts +21 -0
  278. package/dist/pi-extensions/canvas-view.js +75 -0
  279. package/dist/prompts/skill.js +19 -26
  280. package/dist/prompts/view.d.ts +7 -0
  281. package/dist/prompts/view.js +101 -0
  282. package/dist/types.d.ts +4 -0
  283. package/dist/types.js +1 -0
  284. package/package.json +9 -4
  285. package/dist/commands/skill/find.d.ts +0 -4
  286. package/dist/commands/skill/find.js +0 -257
  287. package/dist/commands/skill/read.js +0 -91
  288. /package/dist/commands/{skill → memory}/read.d.ts +0 -0
  289. /package/dist/core/{canvas/browse → tui}/terminal.d.ts +0 -0
@@ -0,0 +1,153 @@
1
+ // broker-protocol.ts — the client↔broker wire protocol for the headless node
2
+ // Surface (design §5.2). Pure types + a newline-delimited JSON codec; no I/O, no
3
+ // SDK construction. Consumed by the broker (src/core/runtime/broker.ts) and, in
4
+ // Phase 4, by the `crtr attach` terminal client and the `crtr web` bridge.
5
+ //
6
+ // The transport is one unix socket per node (`nodeDir(id)/view.sock`) speaking
7
+ // newline-delimited JSON frames. Live engine events are relayed VERBATIM — the
8
+ // broker is a transparent multiplexer, so a broker→client frame is either one of
9
+ // the broker's own control frames (welcome/control_changed/error) or a raw pi
10
+ // `AgentSessionEvent` / `extension_ui_request`. The `type` namespaces never
11
+ // collide, so the union below stays a clean discriminated union.
12
+ //
13
+ // The extension-dialog frames mirror pi's RPC types (decision §1.15). pi names
14
+ // the correlation field `id` (design §5.2 wrote `request_id`); we follow pi.
15
+ //
16
+ // NOTE (version-skew, 2026-06-08): §1.15 says to IMPORT pi's now-public
17
+ // `RpcExtensionUIRequest`/`RpcExtensionUIResponse`. The newer pi build re-exports
18
+ // them from the package root, but the npm-published `0.78.1` currently in
19
+ // crouter's node_modules defines them in `dist/modes/rpc/rpc-types.d.ts` WITHOUT
20
+ // re-exporting them (the `exports` map gates off deep subpaths, so even a
21
+ // type-only deep import is unresolvable). They are therefore mirrored locally
22
+ // below — byte-for-byte from pi's `rpc-types.d.ts`. When the published build
23
+ // catches up, delete these two declarations and import them from the package
24
+ // root; nothing else changes (same names, same shapes).
25
+ import { StringDecoder } from 'node:string_decoder';
26
+ // ---------------------------------------------------------------------------
27
+ // Newline-delimited JSON codec (the framing pi solves with `jsonl`, reproduced
28
+ // locally so the protocol module owns no transport dependency)
29
+ // ---------------------------------------------------------------------------
30
+ /** Encode one frame as a single newline-terminated JSON line. */
31
+ export function encodeFrame(frame) {
32
+ return JSON.stringify(frame) + '\n';
33
+ }
34
+ /** Thrown by {@link FrameDecoder.push} when a peer's buffered bytes exceed a cap
35
+ * (C5). The caller catches it, best-effort sends `error{code:'frame_overflow'}`,
36
+ * and DESTROYS that socket — cap-and-drop the peer, never grow-to-OOM. `kind`
37
+ * says which bound tripped: a single oversized frame (`line`) or total backlog
38
+ * (`total`). */
39
+ export class FrameOverflowError extends Error {
40
+ kind;
41
+ bytes;
42
+ cap;
43
+ constructor(kind, bytes, cap) {
44
+ super(`frame decoder overflow (${kind}): ${bytes} bytes exceeds cap ${cap}`);
45
+ this.kind = kind;
46
+ this.bytes = bytes;
47
+ this.cap = cap;
48
+ this.name = 'FrameOverflowError';
49
+ }
50
+ }
51
+ /** Caps the CLIENT uses reading BROKER frames. The `welcome.snapshot` carries the
52
+ * full message history and can be many MiB, so these are generous. Covers
53
+ * realistic long sessions; snapshot CHUNKING is deferred (plan §1.1 known
54
+ * limitation, review m3 — the long-lived broker makes a big welcome a realistic,
55
+ * not pathological, trigger, but the fixed cap is accepted for Phase 4). */
56
+ export const CLIENT_READ_CAPS = {
57
+ maxLineBytes: 256 * 1024 * 1024,
58
+ maxTotalBytes: 256 * 1024 * 1024,
59
+ };
60
+ /** Caps the BROKER uses reading CLIENT frames. Plan §1.1 specified a tight
61
+ * 4/16 MiB, but review M1 requires image-paste frames to fit: a resizeImage-
62
+ * bounded PNG's base64 is a few MiB and would clip a 4 MiB line cap. So these are
63
+ * RAISED above the plan's 4/16 to hold an image-bearing `prompt`/`steer`/
64
+ * `follow_up` frame (M1). Still bounded, so a malicious/buggy client→broker frame
65
+ * is cap-and-dropped, never grow-to-OOM (C5). */
66
+ export const BROKER_READ_CAPS = {
67
+ maxLineBytes: 24 * 1024 * 1024,
68
+ maxTotalBytes: 48 * 1024 * 1024,
69
+ };
70
+ /** Incremental newline-delimited JSON reader: feed raw socket chunks, get back
71
+ * the complete frames decoded so far. Buffers a partial trailing line across
72
+ * pushes; silently drops a malformed (unparseable) line (a viewer must never
73
+ * crash the broker on bad JSON). The caller narrows the `unknown` against the
74
+ * frame unions.
75
+ *
76
+ * BOUNDED (C5): an oversized buffer is the THROW case. If a single push peaks the
77
+ * internal buffer above `maxTotalBytes`, or the surviving (unterminated) partial
78
+ * line exceeds `maxLineBytes`, {@link push} throws {@link FrameOverflowError}
79
+ * instead of returning frames — the caller drops that peer rather than letting a
80
+ * malicious/buggy stream grow the buffer to OOM. Bytes are measured with
81
+ * `Buffer.byteLength`, never string length; a running counter makes each push's
82
+ * size CHECK O(1) (no re-scan of the whole buffer to measure it).
83
+ *
84
+ * AMORTIZED O(total bytes) (perf regression fix, 2026-06-09): the partial line
85
+ * carried across pushes is held as an ARRAY of chunk strings (`parts`), joined
86
+ * only when its terminating `\n` arrives. The `\n` scan looks at each incoming
87
+ * chunk ONCE — never re-scanning the accumulated carry — so a multi-MiB frame
88
+ * arriving in ~64 KiB socket chunks costs O(frame) total instead of O(frame ×
89
+ * chunks). The old `buf += chunk; buf.indexOf('\n')` shape flattened + re-walked
90
+ * the whole carry per chunk: a 16 MiB welcome snapshot cost ~250 ms of
91
+ * event-loop stall in `crtr attach` (typing lag) and the same again in the
92
+ * broker; this shape decodes it in ~13 ms.
93
+ *
94
+ * A `StringDecoder` decodes incoming Buffer chunks: an incomplete trailing
95
+ * multibyte sequence is held back across pushes instead of being mangled to
96
+ * U+FFFD, so a char split at a `node:net` chunk boundary is never corrupted, and
97
+ * `partBytes` counts decoded-string bytes (a raw `chunk.length` would desync
98
+ * and weaken the C5 cap on multibyte/invalid input). */
99
+ export class FrameDecoder {
100
+ caps;
101
+ /** The unterminated partial line carried across pushes, as unjoined chunks
102
+ * (never contains a '\n'). Joined lazily when its newline arrives. */
103
+ parts = [];
104
+ /** Byte length (Buffer.byteLength) of `parts` joined — kept in exact lockstep. */
105
+ partBytes = 0;
106
+ utf8 = new StringDecoder('utf8');
107
+ constructor(caps) {
108
+ this.caps = caps;
109
+ }
110
+ push(chunk) {
111
+ // A string input is already complete text; a Buffer goes through the
112
+ // StringDecoder so a boundary-split multibyte char is buffered, not corrupted.
113
+ const str = typeof chunk === 'string' ? chunk : this.utf8.write(chunk);
114
+ const strBytes = Buffer.byteLength(str);
115
+ // Peak-buffer bound: caps the most we ever hold in one push (carry + this
116
+ // entire chunk, before draining complete frames) — identical accounting to
117
+ // the old whole-buffer check.
118
+ if (this.partBytes + strBytes > this.caps.maxTotalBytes) {
119
+ throw new FrameOverflowError('total', this.partBytes + strBytes, this.caps.maxTotalBytes);
120
+ }
121
+ const out = [];
122
+ let from = 0; // scan offset into `str` — each chunk is scanned exactly once
123
+ let nl;
124
+ while ((nl = str.indexOf('\n', from)) >= 0) {
125
+ const tail = str.slice(from, nl);
126
+ // First line of the push completes the carried partial; later lines live
127
+ // entirely inside `str` (parts is empty after the first join).
128
+ const line = (this.parts.length > 0 ? this.parts.join('') + tail : tail).trim();
129
+ this.parts.length = 0;
130
+ this.partBytes = 0;
131
+ from = nl + 1;
132
+ if (line === '')
133
+ continue;
134
+ try {
135
+ out.push(JSON.parse(line));
136
+ }
137
+ catch {
138
+ /* drop a malformed frame — never throw out of the reader for bad JSON */
139
+ }
140
+ }
141
+ if (from < str.length) {
142
+ const rest = from === 0 ? str : str.slice(from);
143
+ this.parts.push(rest);
144
+ this.partBytes += from === 0 ? strBytes : Buffer.byteLength(rest);
145
+ }
146
+ // Single-frame bound: a partial line that has grown past one frame's cap is a
147
+ // peer streaming bytes with no newline — drop it rather than buffer forever.
148
+ if (this.partBytes > this.caps.maxLineBytes) {
149
+ throw new FrameOverflowError('line', this.partBytes, this.caps.maxLineBytes);
150
+ }
151
+ return out;
152
+ }
153
+ }
@@ -0,0 +1,48 @@
1
+ import { createAgentSessionServices, createAgentSessionFromServices, createAgentSessionRuntime, SessionManager, VERSION } from '@earendil-works/pi-coding-agent';
2
+ export { createAgentSessionServices, createAgentSessionFromServices, createAgentSessionRuntime, SessionManager, VERSION, };
3
+ /**
4
+ * The minimal slice of the pi SDK the broker needs. Both the real package and the
5
+ * T11 `fake-engine` fixture satisfy this shape, so the broker can be driven by
6
+ * either via the `CRTR_BROKER_ENGINE` seam below.
7
+ *
8
+ * C3 (viewer-reuse scout `mq5thyli`): the broker drives the SERVICES path
9
+ * (`createAgentSessionServices` → `createAgentSessionFromServices`), NOT plain
10
+ * `createAgentSession`. Only the services path runs `registerProvider` (extension
11
+ * model-providers) + `applyExtensionFlagValues`, so a node whose model comes from
12
+ * a custom-provider extension actually gets a model. Mirrors pi `main.js`.
13
+ */
14
+ export interface BrokerEngine {
15
+ createAgentSessionServices: typeof createAgentSessionServices;
16
+ createAgentSessionFromServices: typeof createAgentSessionFromServices;
17
+ /**
18
+ * The session-replacement runtime factory (T3 new_session/switch_session/fork).
19
+ * OPTIONAL: the real SDK (0.78.1) exposes it, so production gets full session
20
+ * replacement; the `fake-engine` test fixture does NOT provide it, so the
21
+ * broker degrades those three ops to an `error{engine_error}` reply rather than
22
+ * failing engine validation. Everything else works on both.
23
+ */
24
+ createAgentSessionRuntime?: typeof createAgentSessionRuntime;
25
+ SessionManager: typeof SessionManager;
26
+ VERSION: string;
27
+ }
28
+ /**
29
+ * Resolve the broker's engine module. Defaults to the real SDK; honors the
30
+ * `CRTR_BROKER_ENGINE` env var (T11 test seam) so a lifecycle test can point the
31
+ * broker at `fixtures/fake-engine.ts` without touching production code. This is the
32
+ * sole dynamic-import indirection point — keep all engine resolution flowing
33
+ * through here so the seam stays a single, auditable swap.
34
+ */
35
+ export declare function loadBrokerEngine(): Promise<BrokerEngine>;
36
+ /**
37
+ * Boot-time tripwire: compare the imported SDK `VERSION` against the `pi` binary
38
+ * crouter forks elsewhere (`pi --version`). On mismatch (or if the binary can't be
39
+ * probed) it logs a LOUD warning to stderr and proceeds — it NEVER throws. The v3
40
+ * session format auto-migrates on load, so minor skew round-trips; a hard fail here
41
+ * would risk a grace-revive crash loop. Phase 5 may harden this to fail-fast once
42
+ * the broker is the only host.
43
+ *
44
+ * @param engineVersion the version actually loaded (defaults to the statically
45
+ * imported `VERSION`; pass `engine.VERSION` from `loadBrokerEngine` to assert the
46
+ * version the broker is really driving).
47
+ */
48
+ export declare function assertEngineVersion(engineVersion?: string): void;
@@ -0,0 +1,72 @@
1
+ // broker-sdk.ts — the single import-and-version-assert shim between the headless
2
+ // broker and the pi SDK (`@earendil-works/pi-coding-agent`).
3
+ //
4
+ // crouter has never imported the SDK before — it only ever forked the `pi` binary
5
+ // (see `launch.ts buildPiArgv`). The broker (T4) is the first consumer. This shim
6
+ // is the one place the dependency is named, so:
7
+ // - the version tripwire (`assertEngineVersion`) lives here, and
8
+ // - the `CRTR_BROKER_ENGINE` test seam (T11) has a single dynamic-import
9
+ // indirection point (`loadBrokerEngine`) to swap in the fake engine.
10
+ //
11
+ // Nothing else in the tree imports this yet; it compiles but stays inert so the
12
+ // lifecycle suite stays green until the broker is wired up.
13
+ import { execFileSync } from 'node:child_process';
14
+ import { createAgentSessionServices, createAgentSessionFromServices, createAgentSessionRuntime, SessionManager, VERSION, } from '@earendil-works/pi-coding-agent';
15
+ // Static re-exports — the real SDK surface the broker drives in production.
16
+ export { createAgentSessionServices, createAgentSessionFromServices, createAgentSessionRuntime, SessionManager, VERSION, };
17
+ /**
18
+ * Resolve the broker's engine module. Defaults to the real SDK; honors the
19
+ * `CRTR_BROKER_ENGINE` env var (T11 test seam) so a lifecycle test can point the
20
+ * broker at `fixtures/fake-engine.ts` without touching production code. This is the
21
+ * sole dynamic-import indirection point — keep all engine resolution flowing
22
+ * through here so the seam stays a single, auditable swap.
23
+ */
24
+ export async function loadBrokerEngine() {
25
+ const spec = process.env.CRTR_BROKER_ENGINE ?? '@earendil-works/pi-coding-agent';
26
+ const mod = (await import(spec));
27
+ if (typeof mod.createAgentSessionServices !== 'function' ||
28
+ typeof mod.createAgentSessionFromServices !== 'function' ||
29
+ typeof mod.SessionManager !== 'function') {
30
+ throw new Error(`[broker] engine '${spec}' does not export createAgentSessionServices/` +
31
+ `createAgentSessionFromServices/SessionManager — not a valid pi-SDK-compatible engine`);
32
+ }
33
+ return {
34
+ createAgentSessionServices: mod.createAgentSessionServices,
35
+ createAgentSessionFromServices: mod.createAgentSessionFromServices,
36
+ // Optional — passed through only when the engine exposes it (real SDK yes,
37
+ // fake-engine no). The broker checks for its presence before the three
38
+ // session-replacing ops.
39
+ createAgentSessionRuntime: typeof mod.createAgentSessionRuntime === 'function' ? mod.createAgentSessionRuntime : undefined,
40
+ SessionManager: mod.SessionManager,
41
+ VERSION: typeof mod.VERSION === 'string' ? mod.VERSION : 'unknown',
42
+ };
43
+ }
44
+ /**
45
+ * Boot-time tripwire: compare the imported SDK `VERSION` against the `pi` binary
46
+ * crouter forks elsewhere (`pi --version`). On mismatch (or if the binary can't be
47
+ * probed) it logs a LOUD warning to stderr and proceeds — it NEVER throws. The v3
48
+ * session format auto-migrates on load, so minor skew round-trips; a hard fail here
49
+ * would risk a grace-revive crash loop. Phase 5 may harden this to fail-fast once
50
+ * the broker is the only host.
51
+ *
52
+ * @param engineVersion the version actually loaded (defaults to the statically
53
+ * imported `VERSION`; pass `engine.VERSION` from `loadBrokerEngine` to assert the
54
+ * version the broker is really driving).
55
+ */
56
+ export function assertEngineVersion(engineVersion = VERSION) {
57
+ let binaryVersion;
58
+ try {
59
+ binaryVersion = execFileSync('pi', ['--version'], { encoding: 'utf8' }).trim();
60
+ }
61
+ catch (err) {
62
+ process.stderr.write(`[broker] WARNING: could not run 'pi --version' to verify engine parity ` +
63
+ `(SDK ${engineVersion}): ${err.message}\n`);
64
+ return;
65
+ }
66
+ if (binaryVersion !== engineVersion) {
67
+ process.stderr.write(`[broker] WARNING: pi SDK version mismatch — imported SDK is ${engineVersion} but ` +
68
+ `the 'pi' binary is ${binaryVersion}. The v3 session format auto-migrates on load so ` +
69
+ `minor skew round-trips, but pin '@earendil-works/pi-coding-agent' to the binary version ` +
70
+ `to keep the in-process engine and the forked binary in lockstep.\n`);
71
+ }
72
+ }
@@ -0,0 +1,55 @@
1
+ import { type Socket } from 'node:net';
2
+ import { type AgentSessionRuntime, type AgentSessionServices, type CreateAgentSessionResult, type ExtensionUIContext } from '@earendil-works/pi-coding-agent';
3
+ import { type BrokerSdkConfig } from './launch.js';
4
+ import { type BrokerEngine } from './broker-sdk.js';
5
+ import { FrameDecoder, type ClientRole, type RpcExtensionUIRequest, type RpcExtensionUIResponse } from './broker-protocol.js';
6
+ interface BrokerClient {
7
+ id: string;
8
+ role: ClientRole;
9
+ socket: Socket;
10
+ decoder: FrameDecoder;
11
+ helloed: boolean;
12
+ /** Unflushed outbound bytes handed to `socket.write` but not yet flushed to the
13
+ * OS (M1 backpressure accounting). Incremented before each write, decremented
14
+ * in that write's completion callback. */
15
+ pendingBytes: number;
16
+ /** Outbound frames written but not yet flushed (the queue-depth half of the
17
+ * high-water mark). */
18
+ queuedFrames: number;
19
+ }
20
+ /** A blocking dialog awaiting the controller's response, the broker-side default
21
+ * timeout, or the engine's abort. */
22
+ interface PendingDialog {
23
+ /** The original request (T4) — retained so `welcome.pending_dialog` and the
24
+ * re-route-on-become-controller path can re-deliver a still-pending dialog to
25
+ * a (new) controller. The Wave-0 shape stored only the resolver. */
26
+ request: RpcExtensionUIRequest;
27
+ /** The controller answered — resolve with its parsed response (also clears the
28
+ * broker-side timeout and removes the entry from the registry). */
29
+ resolve: (response: RpcExtensionUIResponse) => void;
30
+ }
31
+ /** Dispose the live engine session if one exists (idempotent). Called by
32
+ * broker-cli's fatal handlers before exit so a crash never orphans detached
33
+ * bash children. No-op before the session is built or after a clean dispose. */
34
+ export declare function disposeActiveSession(): void;
35
+ export declare function runBroker(nodeId: string): Promise<void>;
36
+ export declare function buildBrokerSession(engine: BrokerEngine, cfg: BrokerSdkConfig): Promise<{
37
+ session: CreateAgentSessionResult['session'];
38
+ services: AgentSessionServices;
39
+ resuming: boolean;
40
+ /** The session-replacement runtime, present iff the engine exposes
41
+ * createAgentSessionRuntime (real SDK yes, fake-engine no). The broker wires
42
+ * its new_session/switch_session/fork ops + rebind through it. */
43
+ runtime?: AgentSessionRuntime;
44
+ }>;
45
+ /** Broker-side hooks the UI context needs to route (or noOp) extension dialogs. */
46
+ export interface BrokerDialogDeps {
47
+ /** The controller client, or null when ZERO viewers are attached. */
48
+ controller: () => BrokerClient | null;
49
+ /** Forward a dialog request to the (non-null) controller. */
50
+ forward: (client: BrokerClient, request: RpcExtensionUIRequest) => void;
51
+ /** Pending-dialog registry, keyed by request id (answered via extension_ui_response). */
52
+ pending: Map<string, PendingDialog>;
53
+ }
54
+ export declare function makeBrokerUiContext(deps: BrokerDialogDeps): ExtensionUIContext;
55
+ export {};