openmausbot 0.1.94 → 0.1.96

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 (188) hide show
  1. package/dist/assets/{DesktopViewer-bxM7NV3N.js → DesktopViewer-DsnA23KM.js} +1 -1
  2. package/dist/assets/{abnfDiagram-O67JEVCF-BhMdJhRJ.js → abnfDiagram-O67JEVCF-Dkbd2hcj.js} +1 -1
  3. package/dist/assets/{arc-B5itKDcx.js → arc-Duj5Coy2.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-C5d7UY6U.js → architectureDiagram-NJMV4G6O-uYUsDawP.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-BrZIhMmu.js → blockDiagram-BEXU5L5S-CWDlTpXe.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-7I0ArfbD.js → c4Diagram-YGBWAQC7-B0t15PDc.js} +1 -1
  7. package/dist/assets/channel-BZxwC-Gw.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-BnEsyN36.js → chunk-2Q5K7J3B-BJSCL-HV.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-BXATQ52s.js → chunk-3FUC2YCW-D3_ILccd.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-B0BpTkEx.js → chunk-5VM5RSS4-D3dmwT_S.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-BtLTz6FR.js → chunk-GTNCS2PH-Br73HFPD.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-D6gcZOYt.js → chunk-GWA4HPMP-uSsjxubC.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-DCLrosvf.js → chunk-JWPE2WC7-C40VWFyr.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-B9JsknV9.js → chunk-NETBCI7D-D6WNNYwU.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-CsoTxw7z.js → chunk-WEXAMYUT-DOEhq64P.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-0F_frB0c.js → chunk-XXDRQBXY-BqJEi6Pi.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-DkrpnflX.js → classDiagram-v2-NBCMYWYE-ByU_GnxT.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-BNDwYBt9.js → cose-bilkent-JH36ORCC-D-Uglxqq.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-JUkNr1Mk.js → cynefin-OW5HDTMX-CZWO7aYj.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-DKeh9N4M.js → cynefinDiagram-VND7K2PF-C70-w0la.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-ve-6T_wX.js → dagre-6A5THRUB-CnhjhExB.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-BuJ2d8rT.js → diagram-22UHCM2B-bnjWCNb4.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-BgOs_Ni-.js → diagram-3UASUU5V-A4uTWoFV.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-DMK6VmcP.js → diagram-ATOU4E4O-Zi6m3NUl.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-DksASlcY.js → diagram-CDSNMT55-DtCcmd5k.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-Bgn8I5k6.js → diagram-MLGK6HIB-DofuETS3.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-zK0jqO8N.js → diagram-MPIPVDR6-Dw2T9PuU.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-vKrbzTwr.js → ebnfDiagram-ZINNZB2B-DbrwPiem.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-Bzv2mPNf.js → elk-276RUBZZ-BvCt1dCb.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-CvOyYuxu.js → erDiagram-OPXOYQCR-Y-QKqPnU.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-CtbnDbc0.js → flowDiagram-KWPJA3E3-CDkL1B5w.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-c4dMRYyG.js → ganttDiagram-FUAMR5RP-CKV5D2F6.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-B34uaQho.js → gitGraphDiagram-X574FWY7-nL9iFTCv.js} +1 -1
  34. package/dist/assets/{index-CPiqc_DN.js → index-DreDnjOu.js} +1 -1
  35. package/dist/assets/index-Du7o6Dsf.js +610 -0
  36. package/dist/assets/index-KxG4bWSr.css +1 -0
  37. package/dist/assets/{infoDiagram-VRGFBTTK-o64ma4CI.js → infoDiagram-VRGFBTTK-C7aHjzsf.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-DV4C9BAm.js → ishikawaDiagram-OU5B5YK6-KUZsNBTh.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-Bf0AHk1H.js → journeyDiagram-ZHPQQLJL-CGUuodFl.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-B1UMSKmE.js → kanban-definition-PNTS6WVX-Cy6r868Z.js} +1 -1
  41. package/dist/assets/{linear-BpKhuyLm.js → linear-Bl669148.js} +1 -1
  42. package/dist/assets/{mermaid.core-B_BKNCw5.js → mermaid.core-86Xqz5Mr.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-DeARruT1.js → mindmap-definition-NLK3R4M7-G4DzHLrd.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-BOQEk_1D.js → pegDiagram-GJSIUBJH-K4eruzY0.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-EIfoiTZo.js → pieDiagram-5QR66LMP-DYdR9PPq.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-GgZwT-fi.js → quadrantDiagram-O4NWA36T-BCAh0nEy.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-D9rDQomj.js → railroadDiagram-XR7U4H2S-p0dGRc4g.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-DAh1MIeo.js → requirementDiagram-PLB6GJNP-tLeBG0aL.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-CPr6Uz64.js → sankeyDiagram-IPEJSGJF-QRuN16rV.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-BU6VIdcm.js → sequenceDiagram-PO4LG4MO-ueT1nVoa.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-qF2HO41r.js → sizeCapture-INFHLROL-wRS2zWg2.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-DlJ8G4MD.js → stateDiagram-v2-GCMORJYK-CYzA_hE1.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-DzYE8I54.js → swimlanes-2SLR337P-kDnv6LMc.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-C7voMDle.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-B7wzEQ0A.js → timeline-definition-EJHVYXUP-D1y2UbbH.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-BvGgrxIP.js → usecaseDiagram-POWQR4AR-IqB4v9HX.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-CEECdgQg.js → vennDiagram-UO4OBE2U-BEi7KYVF.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-XQHA0P_P.js → wardleyDiagram-VNRHLVJA-Dkybo-Yf.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-B5mLcZI2.js → xychartDiagram-PMCCYNJV-BAPFJLpg.js} +1 -1
  60. package/dist/index.html +2 -2
  61. package/dist-server/cloud-home-start.js +27 -13
  62. package/dist-server/companion/src/listener.js +39 -18
  63. package/dist-server/companion/src/routes.js +39 -0
  64. package/dist-server/container-mcp.js +16 -29
  65. package/dist-server/drivers/agents-proxy.js +161 -82
  66. package/dist-server/enterprise/server/index.js +2 -1
  67. package/dist-server/{browser-proxy.js → harness-mcp-proxy.js} +34 -19
  68. package/dist-server/index.js +4991 -4908
  69. package/dist-server/local-computer.js +2 -1
  70. package/dist-server/openmausbot.js +906 -1785
  71. package/dist-server/pair-cli.js +906 -1785
  72. package/dist-server/proxy-paths.js +2 -1
  73. package/dist-server/server/account-signin.js +0 -2
  74. package/dist-server/server/activity.js +201 -0
  75. package/dist-server/server/admin-activity.js +1 -4
  76. package/dist-server/server/auto-approve.js +25 -4
  77. package/dist-server/server/automatic-recovery.js +1 -1
  78. package/dist-server/server/boat-create-idempotency.js +0 -7
  79. package/dist-server/server/boat-delete-journal.js +0 -4
  80. package/dist-server/server/boat.js +4 -4
  81. package/dist-server/server/bot-package.js +1 -19
  82. package/dist-server/server/browser-runtime.js +1 -2
  83. package/dist-server/server/checkpoints.js +90 -148
  84. package/dist-server/server/chief-of-staff.js +9 -9
  85. package/dist-server/server/cli.js +33 -26
  86. package/dist-server/server/cloud-backend.js +0 -4
  87. package/dist-server/server/cloud-computer-tools.js +110 -0
  88. package/dist-server/server/cloud-home-start.js +12 -15
  89. package/dist-server/server/cloud-home.js +0 -5
  90. package/dist-server/server/cloud-lending.js +0 -3
  91. package/dist-server/server/cloud-move-http.js +44 -30
  92. package/dist-server/server/cloud-move.js +20 -18
  93. package/dist-server/server/composio.js +41 -5
  94. package/dist-server/server/computer-control.js +0 -15
  95. package/dist-server/server/computer-observation.js +0 -74
  96. package/dist-server/server/config.js +121 -65
  97. package/dist-server/server/container-computer.js +23 -24
  98. package/dist-server/server/control-client.js +4 -10
  99. package/dist-server/server/decider/jev.js +1 -4
  100. package/dist-server/server/decider/log.js +1 -3
  101. package/dist-server/server/decision-log.js +1 -4
  102. package/dist-server/server/delegations.js +3 -2
  103. package/dist-server/server/drivers/acp/core.js +11 -8
  104. package/dist-server/server/drivers/acp/opencode-go.js +17 -16
  105. package/dist-server/server/drivers/agents-call.js +88 -92
  106. package/dist-server/server/drivers/agents-catalog.js +90 -11
  107. package/dist-server/server/drivers/agents-proxy.js +10 -31
  108. package/dist-server/server/drivers/antigravity.js +1 -4
  109. package/dist-server/server/drivers/boatagent.js +23 -4
  110. package/dist-server/server/drivers/cerebras.js +3 -3
  111. package/dist-server/server/drivers/chat-mcp-tools.js +4 -7
  112. package/dist-server/server/drivers/claude-login-auth.js +2 -11
  113. package/dist-server/server/drivers/claude.js +16 -14
  114. package/dist-server/server/drivers/codex-device-auth.js +3 -1
  115. package/dist-server/server/drivers/codex.js +5 -4
  116. package/dist-server/server/drivers/grok.js +1 -1
  117. package/dist-server/server/drivers/openai-chat-protocol.js +4 -2
  118. package/dist-server/server/drivers/openai-chat.js +18 -11
  119. package/dist-server/server/drivers/prompt-split.js +4 -4
  120. package/dist-server/server/drivers/retry.js +8 -4
  121. package/dist-server/server/engine-install.js +1 -5
  122. package/dist-server/server/harness/bus.js +49 -0
  123. package/dist-server/server/harness/registry.js +0 -1
  124. package/dist-server/server/{browser-proxy.js → harness-mcp-proxy.js} +40 -20
  125. package/dist-server/server/index.js +1190 -971
  126. package/dist-server/server/lending-memory.js +12 -8
  127. package/dist-server/server/mcp-registry.js +35 -0
  128. package/dist-server/server/memory-entries.js +38 -8
  129. package/dist-server/server/memory-organize.js +1 -8
  130. package/dist-server/server/memory-topics.js +4 -4
  131. package/dist-server/server/memory-upkeep.js +13 -15
  132. package/dist-server/server/message-file.js +9 -13
  133. package/dist-server/server/outbound-counts.js +79 -0
  134. package/dist-server/server/outbound-requests.js +60 -0
  135. package/dist-server/server/package-import.js +1 -1
  136. package/dist-server/server/peer-delivery.js +5 -19
  137. package/dist-server/server/peer-roster.js +30 -4
  138. package/dist-server/server/profile-versions.js +0 -31
  139. package/dist-server/server/provider-key-check.js +2 -1
  140. package/dist-server/server/proxy-paths.js +2 -1
  141. package/dist-server/server/restart.js +40 -0
  142. package/dist-server/server/room-handoffs.js +17 -13
  143. package/dist-server/server/routes/bot-memory.js +0 -24
  144. package/dist-server/server/routine-requests.js +2 -5
  145. package/dist-server/server/routines.js +10 -5
  146. package/dist-server/server/server-launcher.js +53 -0
  147. package/dist-server/server/setup-mode.js +0 -2
  148. package/dist-server/server/skill-learn.js +0 -1
  149. package/dist-server/server/skill-library.js +0 -15
  150. package/dist-server/server/store.js +10 -2
  151. package/dist-server/server/surface.js +69 -28
  152. package/dist-server/server/system-prompt.js +17 -7
  153. package/dist-server/server/team-manifest.js +1 -6
  154. package/dist-server/server/team-memory.js +209 -0
  155. package/dist-server/server/thread-events.js +2 -2
  156. package/dist-server/server/tunnel.js +4 -1
  157. package/dist-server/server/turn-images.js +1 -21
  158. package/dist-server/server/turn-resources.js +19 -150
  159. package/dist-server/server/usage-ledger.js +3 -1
  160. package/dist-server/server/vps-computer.js +4 -14
  161. package/dist-server/server/workspace.js +87 -50
  162. package/dist-server/server-launcher.js +72 -0
  163. package/dist-server/shared/activity.js +3 -0
  164. package/dist-server/shared/ask-question.js +1 -1
  165. package/dist-server/shared/bot-avatar.js +8 -0
  166. package/dist-server/shared/cloud-computer.js +12 -0
  167. package/dist-server/shared/connector-scopes.js +78 -0
  168. package/dist-server/shared/failed-turn.js +25 -0
  169. package/dist-server/shared/live-approval.js +2 -2
  170. package/dist-server/shared/outbound.js +128 -0
  171. package/dist-server/shared/package-format.js +3 -3
  172. package/dist-server/shared/pairing-link.js +144 -0
  173. package/dist-server/shared/tool-surface.js +3 -2
  174. package/dist-server/vps-container-mcp.js +16 -29
  175. package/dist-server/workspace-backup.worker.js +14 -27
  176. package/enterprise/server/index.js +2 -1
  177. package/package.json +1 -1
  178. package/dist/assets/channel-CdJX6i8k.js +0 -1
  179. package/dist/assets/index-CPYrGsv1.js +0 -610
  180. package/dist/assets/index-CzT42GR8.css +0 -1
  181. package/dist/assets/swimlanesDiagram-TC7HE7FX-D-U4GtLQ.js +0 -8
  182. package/dist-server/server/claim-idle.js +0 -33
  183. package/dist-server/server/cloud-overflow.js +0 -167
  184. package/dist-server/server/drivers/chat-boat-tools.js +0 -90
  185. package/dist-server/server/drivers/grokagent.js +0 -4
  186. package/dist-server/server/openmaus-status-capsule.js +0 -329
  187. package/dist-server/server/tightening-requests.js +0 -435
  188. package/dist-server/shared/tightening-request.js +0 -87
@@ -1,34 +1,12 @@
1
- // Agent-to-agent comms MCP proxy — spawned as an MCP server inside a bot's
2
- // agent process (via the "agents" integration). Exposes peer, routine, and
3
- // skill tools routed back through the harness so the harness stays the
4
- // single owner of turns, permissions, and recursion limits. The coordination
5
- // tools are:
6
- //
7
- // list_bots() → the other bots in this section + their status
8
- // list_rooms() → the shared rooms this bot may post into
9
- // post_to_room(group_id, message) → put ONE message in a room; nobody's
10
- // turn starts, so nobody replies
11
- // ask_bot(bot_id, msg) → send msg to that bot, wait, return its reply
12
- // delegate_bot(bot_id, msg, reason?) → hand the task to a peer ASYNC: returns
13
- // immediately, the peer runs after your
14
- // current turn finishes, the result is
15
- // delivered to the source conversation
16
- // start_thread(title, msg, bot_id?) → open a real thread — on yourself for
17
- // separate work, or on a teammate as a
18
- // handoff that runs on its own
19
- // create_bot(name, role, instructions) → Chiefs can add a specialist to
20
- // their own section
21
- // create_room / manage_room → Chiefs manage own-section rooms,
22
- // never move bots or sections
23
- // request_credential(id, reason?) → show a secure, allowlisted key card
24
- // list_routines() → inspect this bot's scheduled work
25
- // propose_routine(...) → apply or request confirmation for a new routine
26
- // propose_routine_action(...) → apply or request confirmation for a routine change
27
- // propose_profile(...) → apply or request confirmation for a profile change
1
+ // The "agents" MCP server — spawned inside a bot's agent process (via the
2
+ // "agents" integration). Exposes the teammate, room, routine, memory and
3
+ // skill tools, each routed back through the harness so the harness stays the
4
+ // single owner of turns, permissions, and recursion limits.
28
5
  //
29
6
  // Speaks raw JSON-RPC 2.0 over stdio (no MCP SDK — house style, matches
30
- // computer-proxy / permission-proxy). All state comes from env, injected by
31
- // the harness when it builds the integration:
7
+ // permission-proxy). All state comes from env, injected by the harness when
8
+ // it builds the integration (catalogProfileFromEnv and
9
+ // toolCallContextFromEnv read it). The main ones:
32
10
  // OMB_HARNESS_URL base URL of the harness (http://127.0.0.1:8799)
33
11
  // OMB_BOT_ID the calling bot's id (excluded from list_bots; sender)
34
12
  // OMB_COMMS_TOKEN shared secret for the localhost-only internal endpoints
@@ -44,7 +22,8 @@ import readline from "node:readline";
44
22
  import { availableTools, catalogProfileFromEnv } from "./agents-catalog.js";
45
23
  import { callTool, capResult, toolCallContextFromEnv } from "./agents-call.js";
46
24
  const AVAILABLE_TOOLS = availableTools(catalogProfileFromEnv(process.env));
47
- // One proxy process serves one turn, so its per-turn guards start here.
25
+ // A warm engine keeps this process across its turns; the harness keeps
26
+ // every per-turn limit, so nothing here counts.
48
27
  const CONTEXT = toolCallContextFromEnv(process.env);
49
28
  const send = (msg) => process.stdout.write(JSON.stringify(msg) + "\n");
50
29
  const ok = (id, result) => send({ jsonrpc: "2.0", id, result });
@@ -61,7 +40,7 @@ async function handle(msg) {
61
40
  ok(id, {
62
41
  protocolVersion: params.protocolVersion ?? "2024-11-05",
63
42
  capabilities: { tools: {} },
64
- serverInfo: { name: "opengrokbot-agents", version: "0.1.0" },
43
+ serverInfo: { name: "openmausbot-agents", version: "0.1.0" },
65
44
  });
66
45
  return;
67
46
  case "notifications/initialized":
@@ -1,5 +1,5 @@
1
1
  import { createAcpDriver } from "./acp/core.js";
2
- import { AntigravityAuthController, antigravityProfileAuthenticated, catalogFromAntigravityConfigOptions, prepareAntigravityProfile, probeAntigravityModels, validateAntigravityRuntime, } from "./antigravity-acp.js";
2
+ import { AntigravityAuthController, antigravityProfileAuthenticated, prepareAntigravityProfile, probeAntigravityModels, validateAntigravityRuntime, } from "./antigravity-acp.js";
3
3
  import { antigravityManagedInstallAvailable, installAntigravityRuntime, resolveAntigravityRuntime, } from "./antigravity-runtime.js";
4
4
  import { resolveAntigravityReleaseAsset } from "./antigravity-release.js";
5
5
  import { VERIFICATION_TEMP_KEY, scheduleAntigravityTempSweep } from "./antigravity-temp.js";
@@ -20,9 +20,6 @@ export function antigravityPermissionMode(fullAuto, approvalMode) {
20
20
  return "yolo";
21
21
  return approvalMode === "edits" ? "auto_edit" : "default";
22
22
  }
23
- export function antigravityModelsFromSession(value) {
24
- return catalogFromAntigravityConfigOptions(value, STATIC_ANTIGRAVITY_MODELS.default);
25
- }
26
23
  const managedAsset = resolveAntigravityReleaseAsset();
27
24
  const support = {
28
25
  driverKind: "antigravityAgent",
@@ -43,6 +43,22 @@ const providerFor = (model) => {
43
43
  }
44
44
  return { provider: model.startsWith("gpt") ? "codex" : "claude-code", model };
45
45
  };
46
+ /** Boat answers a prompt it cannot run with 409 provider_not_configured: the
47
+ * Boat has no AI sign-in for the harness the model needs. Say that, and the
48
+ * one thing the person can do about it here. */
49
+ export const PROVIDER_NOT_CONFIGURED_MESSAGE = "The Computer engine has no AI sign-in on its cloud computer. Choose another engine in this bot's settings.";
50
+ /** An OMB Cloud's included Boat account has no agent sign-ins and never
51
+ * will: the operator's account must not run (or pay for) customers' models. */
52
+ export const COMPUTER_ENGINE_CLOUD_UNAVAILABLE = "The Computer engine isn't available on OpenMaus Cloud — choose another engine.";
53
+ /** The words of a Boat error envelope ({code, message, error:{code,
54
+ * message}}), never a bare code when Boat sent a sentence. */
55
+ export function boatErrorMessage(body, status) {
56
+ const code = body?.code ?? body?.error?.code;
57
+ if (code === "provider_not_configured")
58
+ return PROVIDER_NOT_CONFIGURED_MESSAGE;
59
+ const message = body?.message ?? body?.error?.message ?? (typeof body?.error === "string" ? body.error : undefined) ?? code;
60
+ return typeof message === "string" && message.trim() ? message : `box HTTP ${status}`;
61
+ }
46
62
  function decodeConfig(raw) {
47
63
  const o = (raw ?? {});
48
64
  return {
@@ -91,9 +107,8 @@ export const BoatAgentDriver = {
91
107
  signal: opts.signal ?? AbortSignal.timeout(30_000),
92
108
  });
93
109
  const body = await res.json().catch(() => null);
94
- if (!res.ok || body?.ok === false) {
95
- throw new Error(body?.code ?? body?.error ?? `box HTTP ${res.status}`);
96
- }
110
+ if (!res.ok || body?.ok === false)
111
+ throw new Error(boatErrorMessage(body, res.status));
97
112
  return body;
98
113
  };
99
114
  const sendTurn = async (turn) => {
@@ -103,6 +118,8 @@ export const BoatAgentDriver = {
103
118
  const boxId = computer && (!computer.kind || computer.kind === "box") ? computer.boxId : undefined;
104
119
  if (!account())
105
120
  throw new Error('box not configured — add {"box":{"token":"…"}} to ~/.openmausbot/config.json');
121
+ if (account()?.included)
122
+ throw new Error(COMPUTER_ENGINE_CLOUD_UNAVAILABLE);
106
123
  if (!boxId) {
107
124
  throw new Error("this bot has no computer yet — open the Computer panel and provision one");
108
125
  }
@@ -368,6 +385,8 @@ export const BoatAgentDriver = {
368
385
  if (!account()) {
369
386
  return { state: "unavailable", reason: 'no Boat token — add {"box":{"token":"…"}} to ~/.openmausbot/config.json' };
370
387
  }
388
+ if (account()?.included)
389
+ return { state: "unavailable", reason: COMPUTER_ENGINE_CLOUD_UNAVAILABLE };
371
390
  try {
372
391
  await api("/me");
373
392
  return { state: "available", authenticated: true, version: null };
@@ -385,7 +404,7 @@ export const BoatAgentDriver = {
385
404
  snapshot,
386
405
  adapter: {
387
406
  provider: DRIVER_KIND,
388
- capabilities: { sessionModelSwitch: "in-session", remoteAgent: true, usesCloudComputer: true },
407
+ capabilities: { sessionModelSwitch: "in-session", remoteAgent: true },
389
408
  sendTurn,
390
409
  interruptTurn: async (threadId) => active.get(threadId)?.cancel(),
391
410
  respondToRequest: async (threadId, requestId, decision) => {
@@ -37,7 +37,7 @@ export const CerebrasDriver = {
37
37
  install: {
38
38
  docsUrl: "https://cloud.cerebras.ai/",
39
39
  settings: "connections",
40
- signInCommand: "Save a Cerebras API key in Settings → Connections, or set CEREBRAS_API_KEY on the server.",
40
+ signInCommand: "Save a Cerebras API key in Settings → API keys, or set CEREBRAS_API_KEY on the server.",
41
41
  },
42
42
  decodeConfig,
43
43
  defaultConfig: () => decodeConfig({}),
@@ -95,8 +95,8 @@ export const CerebrasDriver = {
95
95
  ...(stream ? { stream_options: { include_usage: true } } : {}),
96
96
  }),
97
97
  httpErrorLabel: "Cerebras",
98
- missingKeyError: "Save a Cerebras API key in Settings → Connections, or set CEREBRAS_API_KEY.",
99
- unavailableReason: "No Cerebras API key — open Settings → Connections.",
98
+ missingKeyError: "Save a Cerebras API key in Settings → API keys, or set CEREBRAS_API_KEY.",
99
+ unavailableReason: "No Cerebras API key — open Settings → API keys.",
100
100
  timeoutMs: 180_000, billing: "metered", includeUsageInCompleted: true,
101
101
  nativeLog: {
102
102
  source: "cerebras.chat.completions",
@@ -7,7 +7,6 @@ import { stripControlPlaneEnv } from "../config.js";
7
7
  import { augmentedPath } from "../env-path.js";
8
8
  import { killCliTree, spawnCli } from "../procs.js";
9
9
  import { chatImage } from "./chat-images.js";
10
- import { ChatBoatClient } from "./chat-boat-tools.js";
11
10
  import { mcpStdioServer } from "../mcp-gate-config.js";
12
11
  import { allowsTool, canUseMcpServer, parseToolScope } from "../../shared/tool-scope.js";
13
12
  /** The transport cannot safely continue this turn. A dispatched operation may
@@ -238,7 +237,7 @@ const validatorOptions = {
238
237
  // or describe an open tuple. Ajv still enforces every constraint.
239
238
  strictRequired: false, strictTypes: false, strictTuples: false,
240
239
  };
241
- function compileSchema(schema) {
240
+ export function compileToolSchema(schema) {
242
241
  const dialect = schema.$schema;
243
242
  if (dialect !== undefined && dialect !== "http://json-schema.org/draft-07/schema#" && dialect !== "https://json-schema.org/draft/2020-12/schema") {
244
243
  throw new Error("MCP tool schema uses an unsupported dialect; use JSON Schema draft-07 or 2020-12");
@@ -291,8 +290,6 @@ export async function mountChatTools(integrations, signal, computerUse = false,
291
290
  const scope = parsed.scope;
292
291
  const servers = [];
293
292
  const eligible = (server) => scope === undefined || canUseMcpServer(scope, server);
294
- if (computerUse && integrations?.computer && eligible("computer"))
295
- servers.push(["computer", integrations.computer]);
296
293
  if (computerUse && integrations?.localComputer && eligible("computer"))
297
294
  servers.push(["computer", integrations.localComputer]);
298
295
  if (computerUse && integrations?.browser && eligible("browser"))
@@ -339,10 +336,10 @@ export async function mountChatTools(integrations, signal, computerUse = false,
339
336
  throw aborted();
340
337
  // Every mounted MCP server can return images when the caller enables
341
338
  // image delivery, including custom servers. Text stays bounded below.
342
- const client = "boxId" in descriptor ? new ChatBoatClient(descriptor) : new ChatMcpClient(descriptor, computerUse);
339
+ const client = new ChatMcpClient(descriptor, computerUse);
343
340
  clients.push(client);
344
341
  const include = (tool) => scope === undefined || (object(tool) && typeof tool.name === "string" && allowsTool(scope, { kind: "mcp", server: name, name: tool.name }));
345
- const tools = client instanceof ChatMcpClient ? await client.tools(signal, include) : (await client.tools()).filter(include);
342
+ const tools = await client.tools(signal, include);
346
343
  return { name, client, builtInBrowser: descriptor === integrations?.browser, tools };
347
344
  }));
348
345
  for (const mount of mounts) {
@@ -360,7 +357,7 @@ export async function mountChatTools(integrations, signal, computerUse = false,
360
357
  throw new Error("MCP tools require an object input schema");
361
358
  if (Buffer.byteLength(JSON.stringify(tool.inputSchema)) > SCHEMA_BYTES)
362
359
  throw new Error("MCP tool schema exceeds the 64KB limit");
363
- const schema = compileSchema(tool.inputSchema);
360
+ const schema = compileToolSchema(tool.inputSchema);
364
361
  const parameters = { ...tool.inputSchema };
365
362
  const constraints = {};
366
363
  if (computerUse) {
@@ -1,23 +1,14 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { realpathSync } from "node:fs";
3
2
  import { homedir } from "node:os";
4
- import { basename, dirname, isAbsolute, join, resolve } from "node:path";
3
+ import { isAbsolute, join, resolve } from "node:path";
5
4
  import { stripVTControlCharacters } from "node:util";
6
5
  import { killCliTree, spawnCli } from "../procs.js";
6
+ import { canonicalPath } from "./codex-device-auth.js";
7
7
  const MAX_OUTPUT = 32_768;
8
8
  /** Where Anthropic's sign-in lives. Anything else in the CLI's output is not a link we show. */
9
9
  const SIGN_IN_HOSTS = ["claude.com", "claude.ai", "console.anthropic.com", "platform.claude.com"];
10
10
  // One login per credential home at a time; turns are never blocked by this.
11
11
  const authenticatingHomes = new Set();
12
- function canonicalPath(path) {
13
- try {
14
- return realpathSync(path);
15
- }
16
- catch {
17
- const parent = dirname(path);
18
- return parent === path ? path : join(canonicalPath(parent), basename(path));
19
- }
20
- }
21
12
  /** The sign-in link, or null for anything that is not Anthropic's own page over https. */
22
13
  export function claudeSignInLink(value) {
23
14
  if (!value)
@@ -6,8 +6,9 @@
6
6
  //
7
7
  // Integrations become MCP servers on the CLI:
8
8
  // - Composio Sessions (connected apps → tools) over streamable HTTP
9
- // - the bot's cloud computer (boat.dev) via server/computer-proxy.ts
10
- // — screenshot/exec/open_url, the CUA-on-the-boat bridge
9
+ // - every computer (this Mac, a Local VM, a VPS, or a Boat cloud
10
+ // computer through server/harness-mcp-proxy.ts computer) as the one
11
+ // stdio `computer` server in turn.integrations.localComputer
11
12
  import { createHash, randomBytes, randomUUID } from "node:crypto";
12
13
  import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
13
14
  import { createServer as createNetServer } from "node:net";
@@ -339,7 +340,7 @@ export function claudeCliUpdate(version, cli) {
339
340
  const effects = [
340
341
  ...(missing.includes("--autocompact") ? ["no compaction window picked by OpenMausBot"] : []),
341
342
  ...(missing.includes("--setting-sources") ? ["bots still see this machine's own Claude Code setup"] : []),
342
- ...(missing.includes("--system-prompt-snapshot") ? ["coordinated resumed turns cannot refresh stale system prompts"] : []),
343
+ ...(missing.includes("--system-prompt-snapshot") ? ["resumed turns cannot refresh stale system prompts"] : []),
343
344
  ];
344
345
  return {
345
346
  title: "Update Claude Code for context controls",
@@ -1034,10 +1035,11 @@ export const ClaudeDriver = {
1034
1035
  // harness snapshots every instance whenever it describes them — app
1035
1036
  // load, the Engines page, and right after `claude update`, which is
1036
1037
  // exactly when the answer changes — so a turn normally finds it filled.
1037
- // Most flags before any snapshot assume a current CLI; autocompact
1038
- // requires confirmed help support. A coordinated turn checks first
1039
- // because the snapshot-refresh flag is newer than the
1040
- // other context controls and an unknown flag would reject that request.
1038
+ // A turn that finds it empty reads the version itself first: the
1039
+ // snapshot-refresh flag every turn passes is newer than the other context
1040
+ // controls, and an unknown flag would reject the turn. If that read
1041
+ // fails, most flags assume a current CLI; the snapshot flag needs a
1042
+ // confirmed version and autocompact a confirmed help listing.
1041
1043
  let cliVersion = null;
1042
1044
  let cliVersionChecked = false;
1043
1045
  // Whether `claude --help` lists --autocompact, read once per CLI version
@@ -1203,7 +1205,7 @@ export const ClaudeDriver = {
1203
1205
  args.push("--disallowedTools", config.disallowedTools.join(","));
1204
1206
  }
1205
1207
  const turnEnvironment = environment();
1206
- if ((turn.refreshSystemPrompt || turn.guestConfined || turn.toolScope !== undefined) && !cliVersionChecked) {
1208
+ if (!cliVersionChecked) {
1207
1209
  const version = await readCliVersion(turnEnvironment);
1208
1210
  if (version) {
1209
1211
  cliVersion = parseClaudeCliVersion(version);
@@ -1246,12 +1248,12 @@ export const ClaudeDriver = {
1246
1248
  if (compactWindow && cliHasAutocompact === true) {
1247
1249
  args.push("--autocompact", compactWindow);
1248
1250
  }
1249
- // An old pair conversation can still carry its first assignment in
1250
- // Claude's recorded system prompt. The current brief rides in the user
1251
- // turn, so refresh the recorded prompt on --resume too. Gated by the
1252
- // version floor like every other flag the CLI may predate: an unknown
1253
- // flag is a hard argument error, not a graceful degrade.
1254
- if (turn.refreshSystemPrompt && cliVersionChecked && claudeCliSupports(cliVersion, "--system-prompt-snapshot")) {
1251
+ // A resumed conversation can still carry an earlier assignment, place
1252
+ // or teammate list in Claude's recorded system prompt, so every turn
1253
+ // refreshes the recorded prompt, on --resume too. Gated by the version
1254
+ // floor like every other flag the CLI may predate: an unknown flag is a
1255
+ // hard argument error, not a graceful degrade.
1256
+ if (cliVersionChecked && claudeCliSupports(cliVersion, "--system-prompt-snapshot")) {
1255
1257
  args.push("--system-prompt-snapshot", "off");
1256
1258
  }
1257
1259
  const turnModel = config.managed ? turn.model : await resolveClaudeTurnModel(turn.model, turnEnvironment);
@@ -9,7 +9,9 @@ const MAX_OUTPUT = 16_384;
9
9
  // Two configured instances can use the same on-disk login. Only the auth
10
10
  // operation is exclusive: normal turns are not blocked by this lock.
11
11
  const authenticatingHomes = new Set();
12
- function canonicalPath(path) {
12
+ /** The real path of a credential home, so two spellings of one home share
13
+ * one sign-in lock. A missing tail keeps its name under its real parent. */
14
+ export function canonicalPath(path) {
13
15
  try {
14
16
  return realpathSync(path);
15
17
  }
@@ -1567,10 +1567,11 @@ export const CodexDriver = {
1567
1567
  // Only the stable half of the prompt belongs in the developer slot:
1568
1568
  // it is the part that must survive compaction unchanged, and any
1569
1569
  // change to it invalidates the provider's cached prefix. The volatile
1570
- // half (memory, mentions, outstanding teammate work) is delivered
1571
- // inside the turn that changed it, after the cached prefix, the same
1572
- // contract SendTurnInput.systemStable documents. Without the split
1573
- // the driver keeps the previous single-block behaviour.
1570
+ // half (the sections in VOLATILE_SECTIONS, system-prompt.ts) is
1571
+ // delivered inside the turn that changed it, after the cached
1572
+ // prefix, the same contract SendTurnInput.systemStable documents.
1573
+ // Without the split the driver keeps the previous single-block
1574
+ // behaviour.
1574
1575
  const stableInstructions = typeof turn.systemStable === "string" && typeof turn.systemVolatile === "string"
1575
1576
  ? turn.systemStable
1576
1577
  : null;
@@ -45,7 +45,7 @@ export const GrokDriver = {
45
45
  generateModel: () => "grok-3-mini",
46
46
  nativeLog: {
47
47
  source: "xai.chat.completions",
48
- outgoing: (_turn, messages, model) => ({ model, messages }),
48
+ outgoing: (_turn, messages, model) => ({ model, messageCount: messages.length }),
49
49
  incoming: ({ text, usage }) => ({ text, usage }),
50
50
  },
51
51
  });
@@ -1,4 +1,6 @@
1
- export const MAX_CHAT_TOOL_CALLS = 32;
1
+ /** Calls one model reply may carry (its index bound). The turn's own total is
2
+ * MAX_TURN_TOOL_CALLS in openai-chat.ts. */
3
+ const MAX_REPLY_TOOL_CALLS = 32;
2
4
  const MAX_ARGUMENT_CHARS = 256_000;
3
5
  export class ChatProtocolError extends Error {
4
6
  }
@@ -36,7 +38,7 @@ export class ChatToolCalls {
36
38
  for (const [position, raw] of value.entries()) {
37
39
  const delta = object(raw);
38
40
  const index = streaming ? delta?.index : position;
39
- if (!delta || !Number.isInteger(index) || Number(index) < 0 || Number(index) >= MAX_CHAT_TOOL_CALLS) {
41
+ if (!delta || !Number.isInteger(index) || Number(index) < 0 || Number(index) >= MAX_REPLY_TOOL_CALLS) {
40
42
  throw new ChatProtocolError("provider returned an invalid tool-call index");
41
43
  }
42
44
  const call = this.calls.get(Number(index)) ?? { id: "", type: "function", function: { name: "", arguments: "" } };
@@ -7,7 +7,7 @@ import { ChatToolSessionError, mountChatTools } from "./chat-mcp-tools.js";
7
7
  import { assertImageTransport, chatImageBudget, chatToolImages, chatUserContent } from "./chat-images.js";
8
8
  import { promptHalves, volatileContextNote, withContextNote } from "./prompt-split.js";
9
9
  import { createChatToolApproval } from "./chat-tool-approval.js";
10
- import { ChatProtocolError, ChatReasoningDetails, ChatToolCalls, MAX_CHAT_TOOL_CALLS, object } from "./openai-chat-protocol.js";
10
+ import { ChatProtocolError, ChatReasoningDetails, ChatToolCalls, object } from "./openai-chat-protocol.js";
11
11
  import { appendNative } from "./native.js";
12
12
  import { classifyError, computeBackoff, interruptibleDelay, RETRY_MAX_ATTEMPTS } from "./retry.js";
13
13
  /** The message of a JSON error body a provider returned with HTTP 200.
@@ -31,6 +31,15 @@ const usageFrom = (usage) => usage
31
31
  const asError = (value) => value instanceof Error ? value : new Error(String(value));
32
32
  class UnsupportedChatToolsError extends Error {
33
33
  }
34
+ /** One turn's safety stops: model steps, and tool calls across all of them.
35
+ * Tool-heavy models (a memory server, a browser) take dozens of steps, and
36
+ * a stopped turn cannot simply be continued — the next turn is rebuilt from
37
+ * the text transcript, which holds none of these tool results. The calls
38
+ * that ran have taken effect, so the stop asks only for what is left: a
39
+ * retry of the whole task would repeat them. */
40
+ const MAX_CHAT_ROUNDS = 64;
41
+ const MAX_TURN_TOOL_CALLS = 200;
42
+ const stoppedAfter = (count) => `Stopped after ${count} without a final answer. The steps so far already ran, so ask only for what's left.`;
34
43
  const NUDGE_ANNOUNCED_ACTION = "You said what you would do next but called no tool. Do it now with your tools, or reply with your final answer if nothing is left to do.";
35
44
  /** A short reply that only announces a next step, with nothing to answer. */
36
45
  export function announcesAction(text) {
@@ -325,7 +334,7 @@ export function createOpenAIChatRuntime(options) {
325
334
  throw new Error(options.missingKeyError);
326
335
  if (active.has(turn.threadId))
327
336
  throw new Error("a turn is already running on this thread");
328
- if (options.computerUse && (turn.images?.length || turn.integrations?.computer || turn.integrations?.localComputer || turn.integrations?.browser))
337
+ if (options.computerUse && (turn.images?.length || turn.integrations?.localComputer || turn.integrations?.browser))
329
338
  assertImageTransport(options.apiUrl);
330
339
  const turnId = newId();
331
340
  const abort = new AbortController();
@@ -334,7 +343,7 @@ export function createOpenAIChatRuntime(options) {
334
343
  for (const message of messages)
335
344
  retainImages(message.content);
336
345
  const model = turn.model || options.models().default;
337
- const secrets = [options.apiKey, turn.integrations?.computer?.token, turn.integrations?.computer?.control?.token].filter((value) => Boolean(value));
346
+ const secrets = [options.apiKey].filter((value) => Boolean(value));
338
347
  for (const integration of Object.values(turn.integrations ?? {})) {
339
348
  const entries = object(integration);
340
349
  const specs = entries && "command" in entries ? [entries] : Object.values(entries ?? {}).map(object);
@@ -413,7 +422,7 @@ export function createOpenAIChatRuntime(options) {
413
422
  if (questionAllowed && !tools.definitions.some((definition) => definition.function.name === ASK_USER_TOOL)) {
414
423
  tools.definitions.push(ASK_USER_TOOL_DEFINITION);
415
424
  }
416
- for (let round = 0; round < 16; round++) {
425
+ for (let round = 0; round < MAX_CHAT_ROUNDS; round++) {
417
426
  abort.signal.throwIfAborted();
418
427
  // Drain parked mid-turn input here, before the next completion
419
428
  // request: the previous round's tool results are complete, so a
@@ -531,8 +540,9 @@ export function createOpenAIChatRuntime(options) {
531
540
  throw new ChatProtocolError("provider reused a tool-call ID; refusing to repeat an operation");
532
541
  seenCalls.add(call.id);
533
542
  }
534
- if (seenCalls.size > MAX_CHAT_TOOL_CALLS)
535
- throw new ChatProtocolError("tool-call limit reached");
543
+ // The whole batch is refused before any of it runs.
544
+ if (seenCalls.size > MAX_TURN_TOOL_CALLS)
545
+ throw new ChatProtocolError(stoppedAfter(`${MAX_TURN_TOOL_CALLS} tool calls`));
536
546
  messages.push({ role: "assistant", content: completion.text || null, tool_calls: completion.toolCalls,
537
547
  ...(completion.protocolReasoning && !reasoningReplayRejected.has(model) ? { [options.reasoningReplayField ?? "reasoning_content"]: completion.protocolReasoning } : {}),
538
548
  ...(completion.protocolReasoningDetails.length ? { reasoning_details: completion.protocolReasoningDetails } : {}),
@@ -645,7 +655,7 @@ export function createOpenAIChatRuntime(options) {
645
655
  messages.push({ role: "user", content: screenshotParts });
646
656
  }
647
657
  if (!ok)
648
- throw new ChatProtocolError("model-call limit reached before a final response");
658
+ throw new ChatProtocolError(stoppedAfter(`${MAX_CHAT_ROUNDS} steps`));
649
659
  }
650
660
  catch (value) {
651
661
  stopReason = abort.signal.aborted ? "interrupted" : stopReason ?? "error";
@@ -695,10 +705,7 @@ export function createOpenAIChatRuntime(options) {
695
705
  adapter: {
696
706
  provider: options.driverKind,
697
707
  capabilities: { ...(options.computerUse ? { computerMcp: options.tools !== false,
698
- // Same gate as cloudComputerMcp: with tools off the runtime cannot
699
- // mount the leased Boat descriptor either. The fleet invariant test
700
- // pins usesCloudComputer === (remoteAgent || cloudComputerMcp).
701
- usesCloudComputer: options.tools !== false, cloudComputerMcp: options.tools !== false, localComputerMcp: options.tools !== false,
708
+ localComputerMcp: options.tools !== false,
702
709
  browserMcp: options.tools !== false, nativeImageInput: true, images: true } : {}),
703
710
  sessionModelSwitch: "in-session", customMcp: options.tools !== false, agentsMcp: options.tools !== false, composioMcp: options.tools !== false,
704
711
  // The runtime owns the whole tool loop, so it can always take a
@@ -1,9 +1,9 @@
1
1
  // The stable/volatile prompt split, shared by the drivers that deliver it.
2
2
  // The stable half is everything that must stay byte-identical for a
3
3
  // provider's cached prefix (or a spawned CLI's session contract) to
4
- // survive; the volatile half (memory, mentions, outstanding teammate
5
- // work, recent work) legitimately changes mid-conversation and reaches
6
- // the model inside the turn that changed it, after the cacheable prefix.
4
+ // survive; the volatile half (the sections in VOLATILE_SECTIONS,
5
+ // system-prompt.ts) legitimately changes mid-conversation and reaches the
6
+ // model inside the turn that changed it, after the cacheable prefix.
7
7
  import { createHash } from "node:crypto";
8
8
  import { mkdirSync, readFileSync, rmSync } from "node:fs";
9
9
  import { join } from "node:path";
@@ -20,7 +20,7 @@ export function promptHalves(turn) {
20
20
  return { stable: systemStable, volatile: systemVolatile };
21
21
  }
22
22
  export const VOLATILE_CONTEXT_NOTE_PREFIX = "Context from OpenMausBot updated since this conversation started; it replaces any earlier copy:";
23
- export const VOLATILE_CONTEXT_CLEARED_NOTE = "The OpenMausBot context notes from earlier in this conversation (memory, mentions, outstanding teammate work) have been cleared; the standing instructions still apply.";
23
+ export const VOLATILE_CONTEXT_CLEARED_NOTE = "The OpenMausBot context notes from earlier in this conversation have been cleared; the standing instructions still apply.";
24
24
  /** The labelled block that carries a changed volatile half inside a user
25
25
  * turn. A half that is empty and always was needs no note; one that was
26
26
  * cleared announces the removal so the model stops relying on it. */
@@ -17,6 +17,10 @@ const TERMINAL_PATTERNS = [
17
17
  pattern: /\b(?:40[13]|unauthorized|forbidden|invalid api key|missing bearer|authentication required|not logged in|logged out)\b/i,
18
18
  reason: "auth",
19
19
  },
20
+ // A subscription's usage window is hours away, so its limit is terminal
21
+ // for this engine even when the provider phrases it as a rate limit;
22
+ // checked before the transient 429 pattern for that reason.
23
+ { pattern: /\busage limit\b|\bhit your (?:usage )?limit\b|\b(?:weekly|daily|monthly|subscription|usage) limit reached\b|\bout of credits\b/i, reason: "quota" },
20
24
  { pattern: /\b402\b|\bquota\b|\bbilling\b|\bsubscription\b/i, reason: "quota" },
21
25
  { pattern: /\bmodel not found\b|\bunknown model\b|\bdoes not exist for model\b|\bunsupported model\b/i, reason: "unknown_model" },
22
26
  { pattern: /\b400\b|\b422\b|\binvalid request\b|\bmalformed\b|\bunexpected status\b/i, reason: "invalid_request" },
@@ -50,14 +54,14 @@ export function classifyError(err) {
50
54
  const { exitCode: code } = err;
51
55
  if (code !== null && code < 0)
52
56
  return { transient: false, reason: "interrupted" };
53
- for (const { pattern, reason } of TRANSIENT_PATTERNS) {
54
- if (pattern.test(text))
55
- return { transient: true, reason };
56
- }
57
57
  for (const { pattern, reason } of TERMINAL_PATTERNS) {
58
58
  if (pattern.test(text))
59
59
  return { transient: false, reason };
60
60
  }
61
+ for (const { pattern, reason } of TRANSIENT_PATTERNS) {
62
+ if (pattern.test(text))
63
+ return { transient: true, reason };
64
+ }
61
65
  return { transient: false, reason: "terminal_exit" };
62
66
  }
63
67
  for (const { pattern, reason } of TERMINAL_PATTERNS) {
@@ -4,7 +4,7 @@
4
4
  // driver's own install descriptor and never from a request, npm runs with
5
5
  // a fixed argument list, and the binary is found on the engines' PATH
6
6
  // afterwards because that directory is registered ahead of everything else.
7
- import { existsSync, mkdirSync } from "node:fs";
7
+ import { mkdirSync } from "node:fs";
8
8
  import { join } from "node:path";
9
9
  import { stripVTControlCharacters } from "node:util";
10
10
  import { DATA_DIR, stripWorkspaceCredentialEnv } from "./config.js";
@@ -169,7 +169,3 @@ function runNpm(args, env, cwd, timeoutMs) {
169
169
  });
170
170
  });
171
171
  }
172
- /** True once something has been installed here, for status pages. */
173
- export function enginesInstalled(baseDir = DATA_DIR) {
174
- return existsSync(enginesBinDir(baseDir));
175
- }
@@ -4,6 +4,13 @@
4
4
  // a per-thread canonical NDJSON log (the debugging trick both upstream and
5
5
  // agentcal lean on), and delivered to subscribers (the SSE endpoint and
6
6
  // the server-side message folder).
7
+ //
8
+ // Streamed text is merged here, once, for every engine on every kind of
9
+ // install: a provider sends a reply as hundreds of small text deltas, and
10
+ // each would otherwise be its own log line, listener call and SSE frame. The
11
+ // bus holds a thread's text for at most DELTA_MERGE_MS and publishes it as
12
+ // one delta, sooner when anything else happens on that thread, so the order
13
+ // of events never changes.
7
14
  import { appendFileSync } from "node:fs";
8
15
  import { join } from "node:path";
9
16
  import { EVENTS_DIR } from "../config.js";
@@ -11,10 +18,19 @@ import { redactSecrets } from "../redact.js";
11
18
  import { capThreadLog, currentThreadLogCap } from "../thread-log-rotation.js";
12
19
  import { newId } from "../contracts.js";
13
20
  const INCOMPLETE_LOG_MESSAGE = "Canonical event history is incomplete: OpenMausBot could not write one or more events to disk. Live updates will continue.";
21
+ /** How long a thread's streamed text waits to be merged with what follows. */
22
+ const DELTA_MERGE_MS = 50;
23
+ /** Two deltas are one stream when only their text, id and time differ. */
24
+ function sameStream(a, b) {
25
+ return a.provider === b.provider && a.providerInstanceId === b.providerInstanceId && a.turnId === b.turnId &&
26
+ a.itemId === b.itemId && a.streamKind === b.streamKind && a.synthetic === b.synthetic;
27
+ }
14
28
  export class EventBus {
15
29
  listeners = new Set();
16
30
  unsubscribes = new Map();
17
31
  pendingLogWarnings = new Map();
32
+ /** Per thread: the streamed text not yet published, and when it goes. */
33
+ pendingText = new Map();
18
34
  appendLog;
19
35
  constructor(appendLog = appendFileSync) {
20
36
  this.appendLog = appendLog;
@@ -35,6 +51,38 @@ export class EventBus {
35
51
  }
36
52
  }
37
53
  publish(event) {
54
+ const pending = this.pendingText.get(event.threadId);
55
+ if (event.type === "content.delta") {
56
+ if (pending && sameStream(pending.event, event)) {
57
+ pending.event = { ...pending.event, delta: pending.event.delta + event.delta };
58
+ return;
59
+ }
60
+ if (pending)
61
+ this.flushThread(event.threadId);
62
+ const timer = setTimeout(() => this.flushThread(event.threadId), DELTA_MERGE_MS);
63
+ timer.unref?.();
64
+ this.pendingText.set(event.threadId, { event, timer });
65
+ return;
66
+ }
67
+ if (pending)
68
+ this.flushThread(event.threadId);
69
+ this.write(event);
70
+ }
71
+ /** Publish every thread's waiting text now: on detach, and as the server exits. */
72
+ flush() {
73
+ for (const threadId of Array.from(this.pendingText.keys()))
74
+ this.flushThread(threadId);
75
+ }
76
+ flushThread(threadId) {
77
+ const pending = this.pendingText.get(threadId);
78
+ if (!pending)
79
+ return;
80
+ // gone from the map before delivery: a listener may publish on this thread
81
+ this.pendingText.delete(threadId);
82
+ clearTimeout(pending.timer);
83
+ this.write(pending.event);
84
+ }
85
+ write(event) {
38
86
  const pendingWarning = this.pendingLogWarnings.get(event.threadId);
39
87
  const persistedEvents = pendingWarning ? [pendingWarning, redactSecrets(event)] : [redactSecrets(event)];
40
88
  try {
@@ -91,5 +139,6 @@ export class EventBus {
91
139
  detach(instanceId) {
92
140
  this.unsubscribes.get(instanceId)?.();
93
141
  this.unsubscribes.delete(instanceId);
142
+ this.flush();
94
143
  }
95
144
  }
@@ -228,7 +228,6 @@ export class ProviderRegistry {
228
228
  modelVariants: inst.adapter.capabilities.modelVariants === true,
229
229
  queueing: inst.adapter.capabilities.queueing === true,
230
230
  localComputerMcp: inst.adapter.capabilities.localComputerMcp === true,
231
- cloudComputerMcp: inst.adapter.capabilities.cloudComputerMcp === true,
232
231
  approvalReview: inst.reviewPermission !== undefined,
233
232
  },
234
233
  access: driver?.metadata.access ?? "subscription",