openmausbot 0.1.96 → 0.1.98

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 (185) hide show
  1. package/dist/assets/ActivityPanel-H6D3SD39.js +1 -0
  2. package/dist/assets/BotSettingsDialog-Dqz_AiBZ.js +7 -0
  3. package/dist/assets/CloudBackendPicker-iezpJFpF.js +1 -0
  4. package/dist/assets/ComputerPanel-DZPj-IWu.js +1 -0
  5. package/dist/assets/DesktopViewer-utONms1p.js +5 -0
  6. package/dist/assets/InspectorPanel-OVaBVrzC.js +5 -0
  7. package/dist/assets/KeyboardShortcutsModal-gk9mvHj_.js +1 -0
  8. package/dist/assets/LocalVmWorkspace-DLIvd2kC.js +1 -0
  9. package/dist/assets/MacLocalControl-bJv8yiPB.js +1 -0
  10. package/dist/assets/NewBotDialog-1qGOAdUM.js +2 -0
  11. package/dist/assets/RemoteAgentSettingsPanel-DqKr0HkO.js +1 -0
  12. package/dist/assets/RoutinesPage-BM2D4qGi.js +1 -0
  13. package/dist/assets/RoutinesSection-Dny9ra-c.js +1 -0
  14. package/dist/assets/SettingsModal-gC6XJJmt.js +11 -0
  15. package/dist/assets/TeamMapPage-B6fuu4r_.js +8 -0
  16. package/dist/assets/TriggersPanel-j0qTP2CO.js +1 -0
  17. package/dist/assets/VisibilitySection-DT0FqPvu.js +12 -0
  18. package/dist/assets/WebhooksPanel--YQyCjKO.js +3 -0
  19. package/dist/assets/{abnfDiagram-O67JEVCF-Dkbd2hcj.js → abnfDiagram-O67JEVCF-BsjcFX0-.js} +1 -1
  20. package/dist/assets/activity-BxOkayvD.js +1 -0
  21. package/dist/assets/{arc-Duj5Coy2.js → arc-CJNA94c6.js} +1 -1
  22. package/dist/assets/architectureDiagram-NJMV4G6O-dz2ud7lP.js +36 -0
  23. package/dist/assets/{blockDiagram-BEXU5L5S-CWDlTpXe.js → blockDiagram-BEXU5L5S-ChBEALoR.js} +1 -1
  24. package/dist/assets/brain-CFZ7PEXc.js +1 -0
  25. package/dist/assets/{c4Diagram-YGBWAQC7-B0t15PDc.js → c4Diagram-YGBWAQC7-KZNRKDQe.js} +1 -1
  26. package/dist/assets/channel-WhFyujhZ.js +1 -0
  27. package/dist/assets/{chunk-2Q5K7J3B-BJSCL-HV.js → chunk-2Q5K7J3B-BLLwmCET.js} +1 -1
  28. package/dist/assets/{chunk-3FUC2YCW-D3_ILccd.js → chunk-3FUC2YCW-CQxyreBZ.js} +2 -2
  29. package/dist/assets/{chunk-5VM5RSS4-D3dmwT_S.js → chunk-5VM5RSS4-D6sFMW0u.js} +1 -1
  30. package/dist/assets/{chunk-GTNCS2PH-Br73HFPD.js → chunk-GTNCS2PH-BaZ1YgLF.js} +1 -1
  31. package/dist/assets/{chunk-GWA4HPMP-uSsjxubC.js → chunk-GWA4HPMP-CcalphmC.js} +1 -1
  32. package/dist/assets/{chunk-JWPE2WC7-C40VWFyr.js → chunk-JWPE2WC7-CSk8R3ve.js} +1 -1
  33. package/dist/assets/{chunk-NETBCI7D-D6WNNYwU.js → chunk-NETBCI7D-CTmLjA57.js} +1 -1
  34. package/dist/assets/{chunk-WEXAMYUT-DOEhq64P.js → chunk-WEXAMYUT-CM9Z77Ne.js} +1 -1
  35. package/dist/assets/{chunk-XXDRQBXY-BqJEi6Pi.js → chunk-XXDRQBXY-B1fxpMS2.js} +1 -1
  36. package/dist/assets/{classDiagram-v2-NBCMYWYE-ByU_GnxT.js → classDiagram-v2-NBCMYWYE-DCCyoLpj.js} +1 -1
  37. package/dist/assets/computer-control-NLov352p.js +1 -0
  38. package/dist/assets/cose-bilkent-JH36ORCC-KyImBKvE.js +1 -0
  39. package/dist/assets/{cynefin-OW5HDTMX-CZWO7aYj.js → cynefin-OW5HDTMX-fsZc6MGa.js} +1 -1
  40. package/dist/assets/{cynefinDiagram-VND7K2PF-C70-w0la.js → cynefinDiagram-VND7K2PF-CGmHsShB.js} +1 -1
  41. package/dist/assets/{dagre-6A5THRUB-CnhjhExB.js → dagre-6A5THRUB-Dah1h-CH.js} +1 -1
  42. package/dist/assets/{diagram-22UHCM2B-bnjWCNb4.js → diagram-22UHCM2B-BIu71OyV.js} +1 -1
  43. package/dist/assets/{diagram-3UASUU5V-A4uTWoFV.js → diagram-3UASUU5V-gxhUB_Mu.js} +1 -1
  44. package/dist/assets/{diagram-ATOU4E4O-Zi6m3NUl.js → diagram-ATOU4E4O-CBhusUBn.js} +1 -1
  45. package/dist/assets/{diagram-CDSNMT55-DtCcmd5k.js → diagram-CDSNMT55-BdPrc86x.js} +1 -1
  46. package/dist/assets/{diagram-MLGK6HIB-DofuETS3.js → diagram-MLGK6HIB-Clp110R_.js} +1 -1
  47. package/dist/assets/{diagram-MPIPVDR6-Dw2T9PuU.js → diagram-MPIPVDR6-DJKfbugw.js} +1 -1
  48. package/dist/assets/{ebnfDiagram-ZINNZB2B-DbrwPiem.js → ebnfDiagram-ZINNZB2B-DnsltoBu.js} +1 -1
  49. package/dist/assets/{elk-276RUBZZ-BvCt1dCb.js → elk-276RUBZZ-jWdq6gqH.js} +1 -1
  50. package/dist/assets/{erDiagram-OPXOYQCR-Y-QKqPnU.js → erDiagram-OPXOYQCR-DArPVf8u.js} +1 -1
  51. package/dist/assets/{flowDiagram-KWPJA3E3-CDkL1B5w.js → flowDiagram-KWPJA3E3-D2HZ-2f7.js} +1 -1
  52. package/dist/assets/ganttDiagram-FUAMR5RP--o0KIxEo.js +292 -0
  53. package/dist/assets/{gitGraphDiagram-X574FWY7-nL9iFTCv.js → gitGraphDiagram-X574FWY7-CsoXr2D7.js} +1 -1
  54. package/dist/assets/image-plus-BbVjGHin.js +1 -0
  55. package/dist/assets/index-BrPW5HDO.css +1 -0
  56. package/dist/assets/index-C3NqfdVU.js +585 -0
  57. package/dist/assets/{index-DreDnjOu.js → index-CJTp9JEp.js} +1 -1
  58. package/dist/assets/{infoDiagram-VRGFBTTK-C7aHjzsf.js → infoDiagram-VRGFBTTK-g-6lNp7r.js} +1 -1
  59. package/dist/assets/{ishikawaDiagram-OU5B5YK6-KUZsNBTh.js → ishikawaDiagram-OU5B5YK6-B1d6qVTV.js} +1 -1
  60. package/dist/assets/{journeyDiagram-ZHPQQLJL-CGUuodFl.js → journeyDiagram-ZHPQQLJL-BTPuiU6T.js} +1 -1
  61. package/dist/assets/{kanban-definition-PNTS6WVX-Cy6r868Z.js → kanban-definition-PNTS6WVX-bDDuRJvP.js} +1 -1
  62. package/dist/assets/{linear-Bl669148.js → linear-BVVKbEMy.js} +1 -1
  63. package/dist/assets/{mermaid.core-86Xqz5Mr.js → mermaid.core-JJdoKyKB.js} +5 -5
  64. package/dist/assets/{mindmap-definition-NLK3R4M7-G4DzHLrd.js → mindmap-definition-NLK3R4M7-B4aE3T0q.js} +1 -1
  65. package/dist/assets/module-DCUsy2BQ.js +3 -0
  66. package/dist/assets/page-visible-BBo_Zm8g.js +1 -0
  67. package/dist/assets/{pegDiagram-GJSIUBJH-K4eruzY0.js → pegDiagram-GJSIUBJH-DFKQWpR7.js} +1 -1
  68. package/dist/assets/{pieDiagram-5QR66LMP-DYdR9PPq.js → pieDiagram-5QR66LMP-Iv7OKVJi.js} +1 -1
  69. package/dist/assets/{quadrantDiagram-O4NWA36T-BCAh0nEy.js → quadrantDiagram-O4NWA36T-U2YnfwBk.js} +1 -1
  70. package/dist/assets/{railroadDiagram-XR7U4H2S-p0dGRc4g.js → railroadDiagram-XR7U4H2S-CuPy6RqM.js} +1 -1
  71. package/dist/assets/remote-desktop-panel-BNEj7tUx.js +1 -0
  72. package/dist/assets/{requirementDiagram-PLB6GJNP-tLeBG0aL.js → requirementDiagram-PLB6GJNP-bMI5AK7T.js} +1 -1
  73. package/dist/assets/{sankeyDiagram-IPEJSGJF-QRuN16rV.js → sankeyDiagram-IPEJSGJF-BKcFE2WQ.js} +1 -1
  74. package/dist/assets/{sequenceDiagram-PO4LG4MO-ueT1nVoa.js → sequenceDiagram-PO4LG4MO-Bn9q88kl.js} +1 -1
  75. package/dist/assets/{sizeCapture-INFHLROL-wRS2zWg2.js → sizeCapture-INFHLROL-Cvmdq9HD.js} +1 -1
  76. package/dist/assets/{stateDiagram-v2-GCMORJYK-CYzA_hE1.js → stateDiagram-v2-GCMORJYK-JJQYw3eQ.js} +1 -1
  77. package/dist/assets/{swimlanes-2SLR337P-kDnv6LMc.js → swimlanes-2SLR337P-BkSCBEwp.js} +1 -1
  78. package/dist/assets/swimlanesDiagram-TC7HE7FX-CmTirbVT.js +8 -0
  79. package/dist/assets/table-file.worker-KkMx-SNx.js +10 -0
  80. package/dist/assets/{timeline-definition-EJHVYXUP-D1y2UbbH.js → timeline-definition-EJHVYXUP-DsaM4GHr.js} +1 -1
  81. package/dist/assets/{usecaseDiagram-POWQR4AR-IqB4v9HX.js → usecaseDiagram-POWQR4AR-3FOwLq7B.js} +1 -1
  82. package/dist/assets/user-DcZBnAuv.js +1 -0
  83. package/dist/assets/{vennDiagram-UO4OBE2U-BEi7KYVF.js → vennDiagram-UO4OBE2U-DyNjJsKt.js} +1 -1
  84. package/dist/assets/{wardleyDiagram-VNRHLVJA-Dkybo-Yf.js → wardleyDiagram-VNRHLVJA-Y4ZMcnBC.js} +1 -1
  85. package/dist/assets/{xychartDiagram-PMCCYNJV-BAPFJLpg.js → xychartDiagram-PMCCYNJV-BXZpg955.js} +1 -1
  86. package/dist/index.html +2 -2
  87. package/dist-server/cloud-home-start.js +20 -9
  88. package/dist-server/container-mcp.js +8 -0
  89. package/dist-server/desktop-entry.mjs +54 -0
  90. package/dist-server/drivers/agents-proxy.js +26 -9
  91. package/dist-server/index.js +3734 -2360
  92. package/dist-server/openmausbot.js +1664 -1553
  93. package/dist-server/pair-cli.js +1664 -1553
  94. package/dist-server/permission-proxy.js +2 -4
  95. package/dist-server/server/atomic.js +70 -2
  96. package/dist-server/server/auto-approve.js +1 -1
  97. package/dist-server/server/auto-vm-claims.js +13 -1
  98. package/dist-server/server/automatic-recovery.js +1 -2
  99. package/dist-server/server/boat.js +68 -84
  100. package/dist-server/server/bot-overview.js +1 -1
  101. package/dist-server/server/browser-engine.js +38 -1
  102. package/dist-server/server/browser-live.js +112 -25
  103. package/dist-server/server/browser-runtime.js +31 -1
  104. package/dist-server/server/checkpoints.js +275 -44
  105. package/dist-server/server/cli-setup.js +3 -3
  106. package/dist-server/server/cloud-computer-tools.js +11 -4
  107. package/dist-server/server/cloud-home-start.js +4 -4
  108. package/dist-server/server/cloud-home.js +3 -9
  109. package/dist-server/server/cloud-owner.js +1 -1
  110. package/dist-server/server/computer-engine-removal.js +70 -0
  111. package/dist-server/server/computer-selection.js +18 -0
  112. package/dist-server/server/config.js +29 -12
  113. package/dist-server/server/connector-card-text.js +15 -0
  114. package/dist-server/server/connector-scope-hint.js +83 -0
  115. package/dist-server/server/default-model-selection.js +23 -14
  116. package/dist-server/server/delegations.js +11 -5
  117. package/dist-server/server/delta-context.js +6 -1
  118. package/dist-server/server/drivers/acp/core.js +11 -1
  119. package/dist-server/server/drivers/acp/grok.js +47 -5
  120. package/dist-server/server/drivers/acp/opencode-go.js +16 -21
  121. package/dist-server/server/drivers/agents-call.js +1 -1
  122. package/dist-server/server/drivers/agents-catalog.js +27 -7
  123. package/dist-server/server/drivers/agents-proxy.js +1 -0
  124. package/dist-server/server/drivers/builtIn.js +0 -2
  125. package/dist-server/server/drivers/chat-mcp-tools.js +5 -3
  126. package/dist-server/server/drivers/claude-login-auth.js +1 -1
  127. package/dist-server/server/drivers/claude.js +84 -41
  128. package/dist-server/server/drivers/codex-device-auth.js +27 -319
  129. package/dist-server/server/drivers/codex.js +2 -2
  130. package/dist-server/server/drivers/device-auth.js +342 -0
  131. package/dist-server/server/drivers/openai-chat.js +110 -10
  132. package/dist-server/server/drivers/pi.js +184 -49
  133. package/dist-server/server/fleet-agent.js +18 -5
  134. package/dist-server/server/harness/http.js +5 -2
  135. package/dist-server/server/harness/registry.js +19 -0
  136. package/dist-server/server/harness-capabilities.js +7 -4
  137. package/dist-server/server/hosted-models.js +3 -1
  138. package/dist-server/server/index.js +823 -333
  139. package/dist-server/server/key-rejections.js +57 -0
  140. package/dist-server/server/live-call.js +11 -6
  141. package/dist-server/server/managed-policy.js +1 -1
  142. package/dist-server/server/message-file.js +7 -4
  143. package/dist-server/server/model-catalog/presets.js +8 -0
  144. package/dist-server/server/peer-roster.js +10 -2
  145. package/dist-server/server/permission-proxy.js +4 -0
  146. package/dist-server/server/provider-key-check.js +5 -1
  147. package/dist-server/server/request-auth.js +1 -1
  148. package/dist-server/server/room-turn-end.js +15 -0
  149. package/dist-server/server/room-turn-timeout.js +28 -0
  150. package/dist-server/server/routes/live.js +3 -3
  151. package/dist-server/server/routes/thread-models.js +36 -0
  152. package/dist-server/server/routine-requests.js +1 -1
  153. package/dist-server/server/routines.js +3 -3
  154. package/dist-server/server/screen-frame-gate.js +1 -3
  155. package/dist-server/server/skills.js +3 -2
  156. package/dist-server/server/store.js +273 -61
  157. package/dist-server/server/surface.js +29 -49
  158. package/dist-server/server/system-prompt.js +10 -10
  159. package/dist-server/server/team-backup.js +19 -4
  160. package/dist-server/server/thread-model.js +25 -0
  161. package/dist-server/server/turn-continuation.js +168 -0
  162. package/dist-server/server/webhook-ingress.js +8 -3
  163. package/dist-server/server/webhooks.js +40 -5
  164. package/dist-server/server/workspace.js +1 -1
  165. package/dist-server/shared/ask-question.js +0 -70
  166. package/dist-server/shared/cloud-computer.js +4 -6
  167. package/dist-server/shared/credential-request.js +1 -1
  168. package/dist-server/shared/device-sign-in.js +57 -0
  169. package/dist-server/shared/failed-turn.js +1 -4
  170. package/dist-server/shared/place-view.js +409 -0
  171. package/dist-server/shared/team-backup.js +2 -0
  172. package/dist-server/shared/thread-model.js +13 -0
  173. package/dist-server/shared/tool-scope-support.js +1 -2
  174. package/dist-server/vps-container-mcp.js +8 -0
  175. package/dist-server/workspace-backup.worker.js +41 -22
  176. package/package.json +1 -1
  177. package/dist/assets/DesktopViewer-DsnA23KM.js +0 -5
  178. package/dist/assets/architectureDiagram-NJMV4G6O-uYUsDawP.js +0 -36
  179. package/dist/assets/channel-BZxwC-Gw.js +0 -1
  180. package/dist/assets/cose-bilkent-JH36ORCC-D-Uglxqq.js +0 -1
  181. package/dist/assets/ganttDiagram-FUAMR5RP-CKV5D2F6.js +0 -292
  182. package/dist/assets/index-Du7o6Dsf.js +0 -610
  183. package/dist/assets/index-KxG4bWSr.css +0 -1
  184. package/dist/assets/swimlanesDiagram-TC7HE7FX-C7voMDle.js +0 -8
  185. package/dist-server/server/drivers/boatagent.js +0 -443
@@ -40,6 +40,12 @@ const DRIVER_KIND = "piAgent";
40
40
  const PI_ARGS = ["--mode", "rpc", "--no-session"];
41
41
  const PI_MODEL_UPDATE_ARGS = ["update", "--models", "--no-approve"];
42
42
  const NODE_ENV_FLAG = { ELECTRON_RUN_AS_NODE: "1" };
43
+ /** omp's RPC transport caps one frame at 1 MiB and only chunks oversized
44
+ * frames after the client negotiates protocol 2. Sent before the first
45
+ * command on every spawn: stdin order guarantees the declaration lands
46
+ * before the response to that command is encoded, and vanilla pi has no
47
+ * handshake frame to wait for, so nobody gates on a reply. */
48
+ const PI_NEGOTIATE_FRAME = { id: "negotiate", type: "negotiate_protocol", protocolVersion: 2 };
43
49
  /** After this many bare turns on one pi session the full prompt rides
44
50
  * again even without a compaction event: any history rewrite the events
45
51
  * miss (an older pi, a missed line) still loses at most this many turns
@@ -135,12 +141,26 @@ export function buildMcpServers(turn) {
135
141
  }
136
142
  return Object.keys(servers).length ? servers : null;
137
143
  }
144
+ /** One decoded frame → catalog, or null when it is not a successful
145
+ * `get_available_models` response. Every option is `custom` (pi is BYOK)
146
+ * and id is the `provider/modelId` composite the picker and `set_model`
147
+ * both use. */
148
+ function piCatalogFromFrame(frame, fallbackDefault) {
149
+ const res = frame;
150
+ if (res?.type !== "response" || res.command !== "get_available_models" || !res.success)
151
+ return null;
152
+ const options = [];
153
+ for (const m of res.data?.models ?? []) {
154
+ if (!m?.provider || !m?.id)
155
+ continue;
156
+ const id = `${m.provider}/${m.id}`;
157
+ options.push({ id, label: m.name ?? m.id, custom: true, provider: m.provider });
158
+ }
159
+ return { default: fallbackDefault || (options[0]?.id ?? ""), options };
160
+ }
138
161
  /** Pure parser: turn a `get_available_models` stdout blob into a catalog.
139
- * Every option is `custom` (pi is BYOK) and id is the `provider/modelId`
140
- * composite the picker and `set_model` both use. Exported for the test. */
162
+ * Exported for the test. */
141
163
  export function parsePiCatalog(stdout, fallbackDefault = "") {
142
- const options = [];
143
- let def = fallbackDefault;
144
164
  for (const line of stdout.split("\n")) {
145
165
  if (!line.trim())
146
166
  continue;
@@ -151,20 +171,142 @@ export function parsePiCatalog(stdout, fallbackDefault = "") {
151
171
  catch {
152
172
  continue;
153
173
  }
154
- const res = msg;
155
- if (res?.type !== "response" || res.command !== "get_available_models" || !res.success)
156
- continue;
157
- for (const m of res.data?.models ?? []) {
158
- if (!m?.provider || !m?.id)
159
- continue;
160
- const id = `${m.provider}/${m.id}`;
161
- options.push({ id, label: m.name ?? m.id, custom: true, provider: m.provider });
174
+ const catalog = piCatalogFromFrame(msg, fallbackDefault);
175
+ if (catalog)
176
+ return catalog;
177
+ }
178
+ return { default: fallbackDefault, options: [] };
179
+ }
180
+ /** omp's RPC v2 splits any frame larger than 1 MiB into base64 `rpc_chunk`
181
+ * pieces: one `chunkId`/`count`/`byteLength` for the whole frame, a
182
+ * sequential `index`, and `data` carrying one piece. Reassembles one
183
+ * logical frame at a time; any inconsistency drops the partial sequence so
184
+ * a corrupt stream can never feed a half-built frame to the parser.
185
+ * Exported for the test. */
186
+ export class PiRpcChunks {
187
+ /** omp's advertised cap on one reassembled frame. */
188
+ static MAX_BYTES = 64 * 1024 * 1024;
189
+ /** omp's cap on one decoded piece, which also bounds the piece count. */
190
+ static MAX_PIECE_BYTES = 256 * 1024;
191
+ static MAX_COUNT = PiRpcChunks.MAX_BYTES / PiRpcChunks.MAX_PIECE_BYTES;
192
+ chunkId = null;
193
+ count = 0;
194
+ byteLength = 0;
195
+ bytes = 0;
196
+ parts = [];
197
+ /** Feed one `rpc_chunk`; returns the reassembled frame when the last
198
+ * piece arrives, or null while the sequence is incomplete or broken. */
199
+ accept(message) {
200
+ const { chunkId, index, count, byteLength, data } = message;
201
+ if (typeof chunkId !== "string" || chunkId.length === 0 || typeof data !== "string" ||
202
+ // Bound the base64 before decoding it, so an oversized piece is
203
+ // rejected without allocating its bytes.
204
+ data.length === 0 || data.length > Math.ceil(PiRpcChunks.MAX_PIECE_BYTES / 3) * 4 ||
205
+ !Number.isInteger(index) || !Number.isInteger(count) || !Number.isInteger(byteLength) ||
206
+ index < 0 || count < 2 || count > PiRpcChunks.MAX_COUNT ||
207
+ index >= count ||
208
+ byteLength < 0 || byteLength > PiRpcChunks.MAX_BYTES) {
209
+ this.reset();
210
+ return null;
211
+ }
212
+ if (chunkId !== this.chunkId) {
213
+ // A new sequence may only start at index 0; a different chunkId
214
+ // mid-sequence means the previous one can never complete.
215
+ if (index !== 0) {
216
+ this.reset();
217
+ return null;
218
+ }
219
+ this.chunkId = chunkId;
220
+ this.count = count;
221
+ this.byteLength = byteLength;
222
+ this.parts = [];
223
+ this.bytes = 0;
224
+ }
225
+ else if (count !== this.count || byteLength !== this.byteLength) {
226
+ this.reset();
227
+ return null;
228
+ }
229
+ const part = Buffer.from(data, "base64");
230
+ // Every piece must carry bytes: an empty one would let a sequence grow
231
+ // `parts` without ever approaching `byteLength`.
232
+ if (index !== this.parts.length || part.length === 0 || part.length > PiRpcChunks.MAX_PIECE_BYTES ||
233
+ this.bytes + part.length > this.byteLength) {
234
+ this.reset();
235
+ return null;
236
+ }
237
+ this.parts.push(part);
238
+ this.bytes += part.length;
239
+ if (this.parts.length < this.count)
240
+ return null;
241
+ const complete = this.bytes === this.byteLength;
242
+ const text = Buffer.concat(this.parts).toString("utf8");
243
+ this.reset();
244
+ if (!complete)
245
+ return null;
246
+ try {
247
+ const frame = JSON.parse(text);
248
+ return frame && typeof frame === "object" && !Array.isArray(frame)
249
+ ? frame
250
+ : null;
251
+ }
252
+ catch {
253
+ return null;
162
254
  }
163
- break;
164
255
  }
165
- if (!def && options.length)
166
- def = options[0].id;
167
- return { default: def, options };
256
+ reset() {
257
+ this.chunkId = null;
258
+ this.count = 0;
259
+ this.byteLength = 0;
260
+ this.bytes = 0;
261
+ this.parts = [];
262
+ }
263
+ }
264
+ /** Decode one pi stdout line: a plain JSON frame, or one piece of an
265
+ * `rpc_chunk` sequence (omp's protocol v2) with the reassembled frame
266
+ * returned once complete. Non-JSON lines, scalars and broken sequences
267
+ * resolve null and are skipped. Exported for the test. */
268
+ export function decodePiFrame(line, chunks) {
269
+ let parsed;
270
+ try {
271
+ parsed = JSON.parse(line);
272
+ }
273
+ catch {
274
+ return null;
275
+ }
276
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
277
+ return null;
278
+ const frame = parsed;
279
+ return frame.type === "rpc_chunk" ? chunks.accept(frame) : frame;
280
+ }
281
+ /** Longest stdout line a reader buffers: omp's cap on one whole frame. */
282
+ const PI_MAX_LINE = PiRpcChunks.MAX_BYTES;
283
+ /** Split a pi child's stdout into non-blank lines for `onLine`, which
284
+ * returns true to stop reading. A line longer than PI_MAX_LINE is dropped
285
+ * through its newline instead of growing the buffer without bound.
286
+ * Exported for the test. */
287
+ export function piLineReader(onLine) {
288
+ let buf = "";
289
+ let dropping = false;
290
+ return (chunk) => {
291
+ buf += chunk;
292
+ let nl;
293
+ while ((nl = buf.indexOf("\n")) !== -1) {
294
+ const line = buf.slice(0, nl);
295
+ buf = buf.slice(nl + 1);
296
+ if (dropping) {
297
+ dropping = false;
298
+ continue;
299
+ }
300
+ if (line.length > PI_MAX_LINE || !line.trim())
301
+ continue;
302
+ if (onLine(line))
303
+ return;
304
+ }
305
+ if (buf.length > PI_MAX_LINE) {
306
+ buf = "";
307
+ dropping = true;
308
+ }
309
+ };
168
310
  }
169
311
  /** Split a picker id into pi's `{provider, modelId}`. Accepts both the
170
312
  * native `provider/modelId` composite and a live-host `host::model`
@@ -306,7 +448,6 @@ function readPiDefaultModel(env) {
306
448
  export async function fetchPiModels(cli, env) {
307
449
  const child = spawnCli(cli, PI_ARGS, { stdio: ["pipe", "pipe", "pipe"], env });
308
450
  return new Promise((resolve) => {
309
- let buf = "";
310
451
  let done = false;
311
452
  const fallbackDefault = readPiDefaultModel(env);
312
453
  const finish = (catalog) => {
@@ -323,26 +464,20 @@ export async function fetchPiModels(cli, env) {
323
464
  };
324
465
  const timer = setTimeout(() => finish({ default: "", options: [] }), 15_000);
325
466
  timer.unref?.();
467
+ const chunks = new PiRpcChunks();
326
468
  child.stdout.setEncoding("utf8");
327
- child.stdout.on("data", (chunk) => {
328
- buf += chunk;
329
- let nl;
330
- while ((nl = buf.indexOf("\n")) !== -1) {
331
- const line = buf.slice(0, nl);
332
- buf = buf.slice(nl + 1);
333
- if (!line.trim())
334
- continue;
335
- const parsed = parsePiCatalog(line + "\n", fallbackDefault);
336
- if (parsed.options.length || line.includes('"get_available_models"')) {
337
- clearTimeout(timer);
338
- finish(parsed);
339
- return;
340
- }
341
- }
342
- });
469
+ child.stdout.on("data", piLineReader((line) => {
470
+ const frame = decodePiFrame(line, chunks);
471
+ if (frame?.command !== "get_available_models")
472
+ return false;
473
+ clearTimeout(timer);
474
+ finish(piCatalogFromFrame(frame, fallbackDefault) ?? { default: fallbackDefault, options: [] });
475
+ return true;
476
+ }));
343
477
  child.on("error", () => finish({ default: "", options: [] }));
344
478
  child.on("close", () => finish({ default: "", options: [] }));
345
479
  try {
480
+ child.stdin.write(`${JSON.stringify(PI_NEGOTIATE_FRAME)}\n`);
346
481
  child.stdin.write(JSON.stringify({ id: "catalog", type: "get_available_models" }) + "\n");
347
482
  }
348
483
  catch {
@@ -542,7 +677,6 @@ export const PiDriver = {
542
677
  throw err;
543
678
  }
544
679
  })();
545
- let buf = "";
546
680
  let assistantText = "";
547
681
  // set when a compaction event arrives this turn; gates the receipt
548
682
  // write below so a compacted-around delivery is never claimed
@@ -575,6 +709,11 @@ export const PiDriver = {
575
709
  appendNative(threadId, { dir: "out", source: "pi.rpc", msg: piNativeLogMessage(obj) });
576
710
  child.stdin.write(JSON.stringify(obj) + "\n");
577
711
  };
712
+ // Declare protocol 2 on the same stdin before the first command, so an
713
+ // omp-style runtime chunks any oversized frame it sends back. Vanilla
714
+ // pi refuses the unknown command; no waiter is keyed for the reply,
715
+ // so either answer is ignored.
716
+ send(PI_NEGOTIATE_FRAME);
578
717
  /** Emit buffered assistant text as its own item, then clear it. */
579
718
  const flushAssistantText = () => {
580
719
  const text = assistantText;
@@ -911,23 +1050,19 @@ export const PiDriver = {
911
1050
  return;
912
1051
  }
913
1052
  };
1053
+ const chunks = new PiRpcChunks();
914
1054
  child.stdout.setEncoding("utf8");
915
- child.stdout.on("data", (chunk) => {
916
- buf += chunk;
917
- let nl;
918
- while ((nl = buf.indexOf("\n")) !== -1) {
919
- const line = buf.slice(0, nl);
920
- buf = buf.slice(nl + 1);
921
- if (!line.trim())
922
- continue;
923
- try {
924
- onEvent(JSON.parse(line));
925
- }
926
- catch {
927
- /* skip non-JSON line */
928
- }
1055
+ child.stdout.on("data", piLineReader((line) => {
1056
+ const frame = decodePiFrame(line, chunks);
1057
+ if (!frame)
1058
+ return;
1059
+ try {
1060
+ onEvent(frame);
929
1061
  }
930
- });
1062
+ catch {
1063
+ /* skip a frame the event handler can't consume */
1064
+ }
1065
+ }));
931
1066
  child.on("error", (err) => {
932
1067
  const fail = describeSpawnFailure(err, config.cli);
933
1068
  rejectWaiters(new Error(fail.message));
@@ -9,15 +9,28 @@ import { dirname } from "node:path";
9
9
  import { defaultFleetDeps, executePlan, loadRegistry, markProvisioningFailed, planFleetAction } from "./fleet-cli.js";
10
10
  import { assertSlug, fleetLayout, fleetUser, openRouterModelIds, workspaceDataDir } from "./fleet.js";
11
11
  const MAX_BODY = 256 * 1024;
12
- function readBody(req) {
12
+ export function readBody(req) {
13
13
  return new Promise((resolve, reject) => {
14
- let text = "";
14
+ const chunks = [];
15
+ let bytes = 0;
16
+ let tooLarge = false;
15
17
  req.on("data", (chunk) => {
16
- text += chunk.toString("utf8");
17
- if (text.length > MAX_BODY)
18
- reject(Object.assign(new Error("request too large"), { status: 413 }));
18
+ if (tooLarge)
19
+ return;
20
+ bytes += chunk.length;
21
+ if (bytes > MAX_BODY) {
22
+ // Keep draining so the 413 can still be sent, but stop holding bytes.
23
+ tooLarge = true;
24
+ chunks.length = 0;
25
+ return reject(Object.assign(new Error("request too large"), { status: 413 }));
26
+ }
27
+ chunks.push(chunk);
19
28
  });
20
29
  req.on("end", () => {
30
+ if (tooLarge)
31
+ return;
32
+ // Decode once: a character split across two chunks stays whole.
33
+ const text = Buffer.concat(chunks).toString("utf8");
21
34
  if (!text.trim())
22
35
  return resolve({});
23
36
  try {
@@ -20,7 +20,7 @@ export function json(res, status, body) {
20
20
  }
21
21
  export function readBody(req, limit = 1_000_000) {
22
22
  return new Promise((resolve, reject) => {
23
- let data = "";
23
+ const chunks = [];
24
24
  let bytes = 0;
25
25
  let done = false;
26
26
  const fail = (status, msg) => {
@@ -40,11 +40,14 @@ export function readBody(req, limit = 1_000_000) {
40
40
  // receiving the useful 413 response.
41
41
  return fail(413, "body too large");
42
42
  }
43
- data += c;
43
+ chunks.push(typeof c === "string" ? Buffer.from(c) : c);
44
44
  });
45
45
  req.on("end", () => {
46
46
  if (done)
47
47
  return;
48
+ // Decode once, after the last chunk: a character split across two
49
+ // chunks turns into U+FFFD when each chunk is decoded on its own.
50
+ const data = Buffer.concat(chunks).toString("utf8");
48
51
  let body;
49
52
  try {
50
53
  body = data ? JSON.parse(data) : {};
@@ -35,6 +35,9 @@ export class ProviderRegistry {
35
35
  /** decoded per-instance `cli` overrides, for describe() — drivers spawn
36
36
  * from their own config; this map only reports what was configured */
37
37
  cliByInstance = new Map();
38
+ /** What describe() last read of each live instance. A turn's start checks
39
+ * a thread's own engine against it without waiting on that engine's CLI. */
40
+ lastSnapshots = new Map();
38
41
  driversByKind;
39
42
  /** Where Settings-driven npm installs go; the data directory by default. */
40
43
  enginesBaseDir;
@@ -100,6 +103,16 @@ export class ProviderRegistry {
100
103
  get(instanceId) {
101
104
  return this.byId.get(instanceId)?.live ?? null;
102
105
  }
106
+ /** The snapshot describe() last read of this live instance; absent before
107
+ * the first read, or for an instance that is not live. */
108
+ lastSnapshot(instanceId) {
109
+ return this.byId.get(instanceId)?.live ? this.lastSnapshots.get(instanceId) : undefined;
110
+ }
111
+ /** How the instance's driver is presented and paid for (EngineAccess). */
112
+ access(instanceId) {
113
+ const entry = this.byId.get(instanceId);
114
+ return entry ? this.driversByKind.get(entry.shadow?.driverKind ?? entry.live.driverKind)?.metadata.access ?? "subscription" : undefined;
115
+ }
103
116
  /** The configured executable for instance-scoped maintenance actions.
104
117
  * This deliberately comes from the registry/config, never an HTTP body. */
105
118
  cliTarget(instanceId) {
@@ -211,6 +224,10 @@ export class ProviderRegistry {
211
224
  catch (e) {
212
225
  snapshot = { state: "unavailable", reason: e instanceof Error ? e.message : String(e) };
213
226
  }
227
+ // Only while this is still the live instance: a reload may have
228
+ // replaced it during the read.
229
+ if (this.byId.get(inst.instanceId)?.live === inst)
230
+ this.lastSnapshots.set(inst.instanceId, snapshot);
214
231
  return {
215
232
  instanceId: inst.instanceId,
216
233
  driverKind: inst.driverKind,
@@ -256,11 +273,13 @@ export class ProviderRegistry {
256
273
  await Promise.allSettled(this.instances().map((i) => i.dispose()));
257
274
  this.byId.clear();
258
275
  this.cliByInstance.clear();
276
+ this.lastSnapshots.clear();
259
277
  }
260
278
  async dispose(instanceId) {
261
279
  const entry = this.byId.get(instanceId);
262
280
  this.byId.delete(instanceId);
263
281
  this.cliByInstance.delete(instanceId);
282
+ this.lastSnapshots.delete(instanceId);
264
283
  await entry?.live?.dispose();
265
284
  }
266
285
  }
@@ -9,10 +9,12 @@
9
9
  // never drifts from what a turn is actually shown.
10
10
  import { APPROVAL_MODES, supportsApprovalMode as driverKindSupportsApprovalMode } from "../shared/approval-mode.js";
11
11
  import { availableTools } from "./drivers/agents-catalog.js";
12
- /** The widest turn the harness can build: every optional integration on and
13
- * a coordinating (room) turn, where the room-only coordination tools exist.
14
- * Both sides of a comparison get the same profile; only the driver's own
15
- * capability can differ. */
12
+ /** The widest turn the harness can build: every optional integration on, a
13
+ * Chief of Staff (the only role shown the Chief-only tools) and a
14
+ * coordinating (room) turn, where the room-only coordination tools exist.
15
+ * So the count is an upper bound, not one bot's catalog. Both sides of a
16
+ * comparison get the same profile; only the driver's own capability can
17
+ * differ. */
16
18
  const ENVELOPE = {
17
19
  externalRuntime: false,
18
20
  coordinating: true,
@@ -22,6 +24,7 @@ const ENVELOPE = {
22
24
  voiceNotes: true,
23
25
  // Not a tool it adds: a Cloud home only hides its Local VM shell.
24
26
  cloudHome: false,
27
+ chief: true,
25
28
  botId: "",
26
29
  };
27
30
  /** Extra card lines for an engine switch, empty when nothing changes. */
@@ -138,12 +138,14 @@ export function hostedModelPolicy(dataDirectory, env = process.env) {
138
138
  const previousDefault = bot.modelSelection;
139
139
  const nextDefault = select(previousDefault);
140
140
  for (const task of store.tasks(bot.id)) {
141
+ // A thread without a model of its own follows the bot onto its
142
+ // assigned model; a thread's own model is moved onto one itself.
141
143
  const current = task.modelSelection ?? previousDefault;
142
144
  const next = select(current);
143
145
  const changed = JSON.stringify(next) !== JSON.stringify(current);
144
146
  if (changed || changedRoute)
145
147
  store.patchTask(bot.id, task.threadId, {
146
- modelSelection: next, resumeCursors: {}, lastInstanceId: undefined, rewound: true,
148
+ ...(task.modelSelection ? { modelSelection: next } : {}), resumeCursors: {}, lastInstanceId: undefined, rewound: true,
147
149
  lastContextModel: undefined, appliedCompactionId: undefined,
148
150
  });
149
151
  }