openmausbot 0.1.86 → 0.1.88

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 (216) hide show
  1. package/README.md +4 -3
  2. package/dist/assets/{abnfDiagram-O67JEVCF-DpUZNnFD.js → abnfDiagram-O67JEVCF-qd9VD3GV.js} +1 -1
  3. package/dist/assets/{arc-DW7mrI4p.js → arc-3RwkUjRe.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-CyeZ5WLZ.js → architectureDiagram-NJMV4G6O-CFKSJS7a.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-CBVaNjO7.js → blockDiagram-BEXU5L5S-CG1rEfWD.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-sf8d41uT.js → c4Diagram-YGBWAQC7-CBW9N664.js} +1 -1
  7. package/dist/assets/channel-CnRnO3tF.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-BOGvy7Y_.js → chunk-2Q5K7J3B-2mRfTksB.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-BiF9zyiz.js → chunk-3FUC2YCW-Cv1dpErj.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-EFuZ1SBZ.js → chunk-5VM5RSS4-B7IACJp-.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-pJLNSAT7.js → chunk-GTNCS2PH-An0hZtYa.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-Cb-3_3IT.js → chunk-GWA4HPMP-BcJB2nEm.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-DAf5aogn.js → chunk-JWPE2WC7-CV_W7TN7.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-DR3kiwZz.js → chunk-NETBCI7D-Bcs_kwnA.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-BmU8PeyM.js → chunk-WEXAMYUT-lrx_DxFP.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-Buh8g8OT.js → chunk-XXDRQBXY-DabHlLGM.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-BurfmMWM.js → classDiagram-v2-NBCMYWYE-BnHj6P7d.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-KkGZUEkU.js → cose-bilkent-JH36ORCC-CfLp6UDi.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-C3PpV96d.js → cynefin-OW5HDTMX-Dm4dbVA1.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-Dt8JwJaI.js → cynefinDiagram-VND7K2PF-B9mH-PID.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-DkjN8266.js → dagre-6A5THRUB-DDc_NVQJ.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-DUAJS0JV.js → diagram-22UHCM2B-lwr8Ip8-.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-Dw4KhF1a.js → diagram-3UASUU5V-BLLe6dmI.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-Dd1gmQqf.js → diagram-ATOU4E4O-6eUIq4lz.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-CjwGs0Yw.js → diagram-CDSNMT55-O-pRN7VQ.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-BL-69Zrq.js → diagram-MLGK6HIB-BietvGAI.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-5Zi2N7wo.js → diagram-MPIPVDR6-Bd2Cl62X.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-D9NekWUX.js → ebnfDiagram-ZINNZB2B-B6iDy69W.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-CkaSQjxw.js → elk-276RUBZZ-DQVHfKe0.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-CdAVBV8-.js → erDiagram-OPXOYQCR-BWdVp2tQ.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-Bm_lWWH7.js → flowDiagram-KWPJA3E3-WLE6rKB4.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-FZ8ejYWJ.js → ganttDiagram-FUAMR5RP-BbjS-4gn.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-Dlv9rizZ.js → gitGraphDiagram-X574FWY7-Dpy2QSGq.js} +1 -1
  34. package/dist/assets/index-BIMdIbaw.css +1 -0
  35. package/dist/assets/index-CHnODCr2.js +583 -0
  36. package/dist/assets/{index-h9FFNwTu.js → index-CIaHn5NS.js} +1 -1
  37. package/dist/assets/{infoDiagram-VRGFBTTK-BdQ_gW5B.js → infoDiagram-VRGFBTTK-D_jKWlKe.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-EwssEp2X.js → ishikawaDiagram-OU5B5YK6-DLJohSuK.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-BX1G1vOd.js → journeyDiagram-ZHPQQLJL-DPOFj0qN.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-BB4kAsyC.js → kanban-definition-PNTS6WVX-BI7v-xpK.js} +1 -1
  41. package/dist/assets/{linear-3su2TG93.js → linear-C2pWPSxO.js} +1 -1
  42. package/dist/assets/{mermaid.core-CDD1E0s9.js → mermaid.core-CR-j5WQU.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-Ba50MsNK.js → mindmap-definition-NLK3R4M7-lz9GvXRh.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-DZLkkZlM.js → pegDiagram-GJSIUBJH-Dl4_ia61.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-DaTz77Uq.js → pieDiagram-5QR66LMP-DWuT3n88.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-CpaiidpR.js → quadrantDiagram-O4NWA36T-DXWe9Jx7.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-mN4JrMeI.js → railroadDiagram-XR7U4H2S-COY1IBVY.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-BSNDRpg7.js → requirementDiagram-PLB6GJNP-DnyVuhsQ.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-FngzGNug.js → sankeyDiagram-IPEJSGJF-BSc4NdGK.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-CM4pCC3L.js → sequenceDiagram-PO4LG4MO-kqEU2EGq.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-CRICHXkw.js → sizeCapture-INFHLROL-BvB4bkWD.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-oyTWpxH2.js → stateDiagram-v2-GCMORJYK-BIrqNv7y.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-D9h9mPBq.js → swimlanes-2SLR337P-CFnalQUE.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-C4a8qNGj.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-BJUlN9bW.js → timeline-definition-EJHVYXUP-CsSWJnSQ.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-3uBNkbAb.js → usecaseDiagram-POWQR4AR-DkAmJ1EE.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-D2odalzT.js → vennDiagram-UO4OBE2U-C3vd9drL.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-DHgFaB9S.js → wardleyDiagram-VNRHLVJA-j26CM-pi.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-w5GYM2h0.js → xychartDiagram-PMCCYNJV-BYu1Q_at.js} +1 -1
  60. package/dist/index.html +2 -2
  61. package/dist-server/browser-proxy.js +2 -1
  62. package/dist-server/companion/src/routes.js +8 -1
  63. package/dist-server/connector-proxy.js +105 -1
  64. package/dist-server/container-mcp.js +1032 -165
  65. package/dist-server/drivers/agents-proxy.js +338 -29
  66. package/dist-server/drivers/phone-proxy.js +33 -2
  67. package/dist-server/index.js +40590 -31811
  68. package/dist-server/local-computer-proxy.js +5 -3
  69. package/dist-server/local-computer.js +53 -17
  70. package/dist-server/mcp-gate.js +2 -0
  71. package/dist-server/mcp-server.js +5 -3
  72. package/dist-server/message-search.worker.js +52 -0
  73. package/dist-server/openmausbot.js +4457 -1399
  74. package/dist-server/pair-cli.js +4457 -1399
  75. package/dist-server/permission-proxy.js +29 -9
  76. package/dist-server/server/admission.js +81 -0
  77. package/dist-server/server/aside-queue.js +294 -0
  78. package/dist-server/server/attachments.js +98 -1
  79. package/dist-server/server/auto-approve.js +4 -3
  80. package/dist-server/server/auto-vm-claims.js +1 -0
  81. package/dist-server/server/bot-attachment.js +107 -0
  82. package/dist-server/server/bot-overview.js +19 -0
  83. package/dist-server/server/bot-package.js +18 -292
  84. package/dist-server/server/bot-profile.js +5 -17
  85. package/dist-server/server/box-create-idempotency.js +3 -3
  86. package/dist-server/server/box.js +57 -55
  87. package/dist-server/server/browser-proxy.js +3 -2
  88. package/dist-server/server/browser-runtime.js +37 -1
  89. package/dist-server/server/cli-api-setup.js +1 -0
  90. package/dist-server/server/cli.js +2 -2
  91. package/dist-server/server/command-allowlist.js +154 -0
  92. package/dist-server/server/composio.js +297 -1
  93. package/dist-server/server/computer-wait.js +22 -9
  94. package/dist-server/server/config.js +84 -12
  95. package/dist-server/server/connector-advertisement.js +165 -0
  96. package/dist-server/server/connector-proxy.js +14 -2
  97. package/dist-server/server/connector-verdict.js +168 -0
  98. package/dist-server/server/container-computer.js +61 -1
  99. package/dist-server/server/contracts.js +22 -0
  100. package/dist-server/server/default-model-selection.js +9 -0
  101. package/dist-server/server/delegations.js +85 -34
  102. package/dist-server/server/drivers/acp/core.js +80 -20
  103. package/dist-server/server/drivers/acp/cursor.js +3 -7
  104. package/dist-server/server/drivers/acp/droid.js +4 -7
  105. package/dist-server/server/drivers/acp/gemini.js +4 -4
  106. package/dist-server/server/drivers/acp/grok.js +4 -9
  107. package/dist-server/server/drivers/acp/hermes.js +3 -5
  108. package/dist-server/server/drivers/acp/opencode-go.js +2 -5
  109. package/dist-server/server/drivers/acp/qwen.js +9 -15
  110. package/dist-server/server/drivers/agents-call.js +179 -13
  111. package/dist-server/server/drivers/agents-catalog.js +134 -10
  112. package/dist-server/server/drivers/boxagent.js +160 -36
  113. package/dist-server/server/drivers/builtIn.js +2 -0
  114. package/dist-server/server/drivers/chat-box-tools.js +90 -0
  115. package/dist-server/server/drivers/chat-images.js +54 -0
  116. package/dist-server/server/drivers/chat-mcp-tools.js +38 -8
  117. package/dist-server/server/drivers/chat-tool-approval.js +70 -18
  118. package/dist-server/server/drivers/claude.js +38 -19
  119. package/dist-server/server/drivers/codex-catalog.js +14 -6
  120. package/dist-server/server/drivers/codex-identity.js +2 -1
  121. package/dist-server/server/drivers/codex-instructions.js +48 -6
  122. package/dist-server/server/drivers/codex-release.js +48 -0
  123. package/dist-server/server/drivers/codex-tool-surface.js +20 -0
  124. package/dist-server/server/drivers/codex.js +102 -55
  125. package/dist-server/server/drivers/mistral.js +115 -0
  126. package/dist-server/server/drivers/openai-chat.js +180 -33
  127. package/dist-server/server/drivers/openai-compat.js +1 -0
  128. package/dist-server/server/drivers/permission-command.js +35 -0
  129. package/dist-server/server/drivers/phone-proxy.js +47 -2
  130. package/dist-server/server/drivers/pi.js +306 -28
  131. package/dist-server/server/drivers/prompt-split.js +110 -0
  132. package/dist-server/server/drivers/session-idle.js +29 -0
  133. package/dist-server/server/env-path.js +15 -0
  134. package/dist-server/server/fleet.js +1 -1
  135. package/dist-server/server/group-thread-usage.js +103 -0
  136. package/dist-server/server/guarded-requests.js +2 -2
  137. package/dist-server/server/harness/registry.js +1 -0
  138. package/dist-server/server/harness-capabilities.js +46 -0
  139. package/dist-server/server/hosted-models.js +1 -1
  140. package/dist-server/server/index.js +2396 -505
  141. package/dist-server/server/local-computer.js +72 -23
  142. package/dist-server/server/managed-desktop.js +1 -1
  143. package/dist-server/server/mcp-bridge.js +6 -6
  144. package/dist-server/server/message-db.js +22 -42
  145. package/dist-server/server/message-file.js +9 -1
  146. package/dist-server/server/message-search-query.js +43 -0
  147. package/dist-server/server/message-search-worker.js +80 -0
  148. package/dist-server/server/message-search.worker.js +21 -0
  149. package/dist-server/server/model-requests.js +248 -0
  150. package/dist-server/server/new-bot-defaults.js +115 -0
  151. package/dist-server/server/options-card.js +21 -0
  152. package/dist-server/server/org-library.js +1065 -0
  153. package/dist-server/server/package-export.js +627 -39
  154. package/dist-server/server/package-import.js +458 -0
  155. package/dist-server/server/package-parts.js +87 -0
  156. package/dist-server/server/peer-approval.js +16 -7
  157. package/dist-server/server/peer-delivery.js +60 -0
  158. package/dist-server/server/peer-roster.js +67 -0
  159. package/dist-server/server/presets.js +492 -0
  160. package/dist-server/server/profile-requests.js +66 -39
  161. package/dist-server/server/profile-revision.js +9 -1
  162. package/dist-server/server/profile-versions.js +6 -3
  163. package/dist-server/server/provider-key-check.js +3 -2
  164. package/dist-server/server/published-teams.js +80 -0
  165. package/dist-server/server/room-handoffs.js +19 -3
  166. package/dist-server/server/routes/bot-presets.js +25 -0
  167. package/dist-server/server/routine-requests.js +35 -10
  168. package/dist-server/server/routines.js +41 -1
  169. package/dist-server/server/section-context.js +10 -0
  170. package/dist-server/server/skill-fetch.js +72 -14
  171. package/dist-server/server/skills.js +35 -61
  172. package/dist-server/server/steer-queue.js +3 -2
  173. package/dist-server/server/store.js +291 -17
  174. package/dist-server/server/surface.js +10 -4
  175. package/dist-server/server/system-prompt.js +38 -6
  176. package/dist-server/server/team-backup.js +6 -2
  177. package/dist-server/server/team-computers.js +10 -0
  178. package/dist-server/server/team-library.js +6 -2
  179. package/dist-server/server/team-manifest.js +4 -3
  180. package/dist-server/server/team-setup-requests.js +139 -27
  181. package/dist-server/server/thread-events.js +20 -2
  182. package/dist-server/server/tightening-requests.js +405 -0
  183. package/dist-server/server/tts/chatterbox.js +27 -5
  184. package/dist-server/server/turn-context.js +2 -0
  185. package/dist-server/server/turn-watchdog.js +12 -3
  186. package/dist-server/server/usage-ledger.js +19 -2
  187. package/dist-server/server/vps-computer.js +139 -51
  188. package/dist-server/server/vps-container-mcp.js +7 -0
  189. package/dist-server/server/vps-ssh.js +36 -7
  190. package/dist-server/server/workspace-backup-policy.js +12 -0
  191. package/dist-server/server/workspace-backup.js +42 -5
  192. package/dist-server/server/workspace-backup.worker.js +10 -0
  193. package/dist-server/server/workspace.js +1 -1
  194. package/dist-server/shared/approval-mode.js +1 -1
  195. package/dist-server/shared/ask-question.js +207 -12
  196. package/dist-server/shared/bot-profile.js +15 -0
  197. package/dist-server/shared/command-allowlist.js +1 -0
  198. package/dist-server/shared/credential-request.js +6 -2
  199. package/dist-server/shared/line-diff.js +23 -0
  200. package/dist-server/shared/model-request.js +1 -0
  201. package/dist-server/shared/options-card.js +51 -0
  202. package/dist-server/shared/package-format.js +967 -0
  203. package/dist-server/shared/profile-request.js +5 -4
  204. package/dist-server/shared/skill-md.js +64 -0
  205. package/dist-server/shared/team-backup.js +7 -0
  206. package/dist-server/shared/tightening-request.js +87 -0
  207. package/dist-server/shared/wire.js +4 -0
  208. package/dist-server/src/lib/composer-attachments.js +25 -3
  209. package/dist-server/vps-container-mcp.js +1132 -171
  210. package/dist-server/workspace-backup.worker.js +19474 -0
  211. package/enterprise/README.md +1 -1
  212. package/package.json +1 -1
  213. package/dist/assets/channel-BXFkgus9.js +0 -1
  214. package/dist/assets/index-BGgqlWPJ.css +0 -1
  215. package/dist/assets/index-BtHo8ZKl.js +0 -577
  216. package/dist/assets/swimlanesDiagram-TC7HE7FX-BWrvB2ak.js +0 -8
@@ -1,7 +1,10 @@
1
1
  import { newEventId, newId } from "../contracts.js";
2
+ import { ASK_USER_TOOL, ASK_USER_TOOL_DEFINITION, askQuestionSummary, parseAskQuestions, questionChoices } from "../../shared/ask-question.js";
2
3
  import { redactSecretsInText } from "../redact.js";
3
4
  import { toolDetailPreview } from "../tool-summary.js";
4
5
  import { ChatToolSessionError, mountChatTools } from "./chat-mcp-tools.js";
6
+ import { assertImageTransport, chatImageBudget, chatToolImages, chatUserContent } from "./chat-images.js";
7
+ import { promptHalves, volatileContextNote, withContextNote } from "./prompt-split.js";
5
8
  import { createChatToolApproval } from "./chat-tool-approval.js";
6
9
  import { ChatProtocolError, ChatReasoningDetails, ChatToolCalls, MAX_CHAT_TOOL_CALLS, object } from "./openai-chat-protocol.js";
7
10
  import { appendNative } from "./native.js";
@@ -25,6 +28,26 @@ const usageFrom = (usage) => usage
25
28
  ? { input: usage.prompt_tokens ?? 0, output: usage.completion_tokens ?? 0 }
26
29
  : null;
27
30
  const asError = (value) => value instanceof Error ? value : new Error(String(value));
31
+ class UnsupportedChatToolsError extends Error {
32
+ }
33
+ function rejectsToolsParameter(status, body) {
34
+ if (status !== 400 && status !== 422)
35
+ return false;
36
+ let envelope;
37
+ try {
38
+ envelope = object(JSON.parse(body));
39
+ }
40
+ catch {
41
+ return false;
42
+ }
43
+ const error = object(envelope?.error);
44
+ if (error?.param === "tools" && error.code === "unsupported_parameter")
45
+ return true;
46
+ const message = error?.message ?? envelope?.error;
47
+ return typeof message === "string" && (/\bdoes not support (?:tools|tool calling|function calling)(?:[.!]?$|[.!]?\s)/i.test(message) ||
48
+ /\b(?:tools|tool calling|function calling) (?:is|are) not supported\b/i.test(message) ||
49
+ /^(?:unsupported|unknown|unrecognized) (?:parameter|field):?\s*['"]?tools['"]?[.!]?$/i.test(message.trim()));
50
+ }
28
51
  /** Shared runtime for the three providers that speak OpenAI chat completions. */
29
52
  export function createOpenAIChatRuntime(options) {
30
53
  const { input } = options;
@@ -59,6 +82,7 @@ export function createOpenAIChatRuntime(options) {
59
82
  : timeoutController.signal;
60
83
  const response = await fetch(`${options.apiUrl}/chat/completions`, {
61
84
  method: "POST",
85
+ ...(messages.some(message => Array.isArray(message.content) && message.content.some(part => part.type === "image_url")) ? { redirect: "error" } : {}),
62
86
  headers: { authorization: `Bearer ${options.apiKey}`, "content-type": "application/json" },
63
87
  body: JSON.stringify({
64
88
  ...options.requestBody(model, messages, stream),
@@ -68,7 +92,10 @@ export function createOpenAIChatRuntime(options) {
68
92
  });
69
93
  if (!response.ok) {
70
94
  const body = await response.text().catch(() => "");
71
- throw new Error(`${options.httpErrorLabel} HTTP ${response.status}${body ? `: ${body.slice(0, 200)}` : ""}`);
95
+ const message = `${options.httpErrorLabel} HTTP ${response.status}${body ? `: ${body.slice(0, 200)}` : ""}`;
96
+ if (rejectsToolsParameter(response.status, body))
97
+ throw new UnsupportedChatToolsError(message);
98
+ throw new Error(message);
72
99
  }
73
100
  if (!stream || response.headers.get("content-type")?.includes("application/json")) {
74
101
  const json = await response.json();
@@ -89,7 +116,7 @@ export function createOpenAIChatRuntime(options) {
89
116
  const finishReason = json.choices?.[0]?.finish_reason ?? null;
90
117
  activeSignal.throwIfAborted();
91
118
  return {
92
- text: typeof message?.content === "string" ? message.content : "",
119
+ text: options.contentText ? options.contentText(message?.content) : typeof message?.content === "string" ? message.content : "",
93
120
  reasoning: options.reasoning && typeof reasoning === "string"
94
121
  ? reasoning
95
122
  : "",
@@ -155,7 +182,7 @@ export function createOpenAIChatRuntime(options) {
155
182
  const reasoningDelta = options.reasoning && typeof reasoningPart === "string"
156
183
  ? reasoningPart
157
184
  : "";
158
- const contentDelta = typeof delta?.content === "string" ? delta.content : "";
185
+ const contentDelta = options.contentText ? options.contentText(delta?.content) : typeof delta?.content === "string" ? delta.content : "";
159
186
  if (reasoningDelta) {
160
187
  reasoning += reasoningDelta;
161
188
  onDelta?.(reasoningDelta, "reasoning_text");
@@ -220,24 +247,44 @@ export function createOpenAIChatRuntime(options) {
220
247
  clearTimeout(idleTimer);
221
248
  }
222
249
  };
223
- const messagesFor = (turn) => [
224
- ...(turn.system ? [{ role: "system", content: turn.system }] : []),
225
- ...(turn.transcript ?? []).map((message) => ({
226
- role: message.role,
227
- content: message.text,
228
- })),
229
- { role: "user", content: turn.text },
230
- ];
250
+ const messagesFor = (turn) => {
251
+ // The system message is the head of the resent prefix, so only the
252
+ // stable half belongs there: a volatile edit must not re-price the
253
+ // tools, instructions and transcript the provider already cached.
254
+ // The volatile half rides the newest user message instead, every
255
+ // turn. Unlike a CLI session, this request is rebuilt from the stored
256
+ // transcript, which never contains the delivered notes, so tracking a
257
+ // digest and delivering only on change would leave the model without
258
+ // its memory on unchanged turns. The newest message is fresh input
259
+ // on every request anyway.
260
+ const halves = promptHalves(turn);
261
+ const note = halves.stable !== null ? volatileContextNote(halves.volatile, false) : "";
262
+ const userTurn = note ? { ...turn, text: withContextNote(note, turn.text) } : turn;
263
+ const system = halves.stable ?? turn.system;
264
+ return [
265
+ ...(system ? [{ role: "system", content: system }] : []),
266
+ ...(turn.transcript ?? []).map((message) => ({
267
+ role: message.role,
268
+ content: message.text,
269
+ })),
270
+ { role: "user", content: options.computerUse ? chatUserContent(userTurn) : userTurn.text },
271
+ ];
272
+ };
231
273
  const sendTurn = async (turn) => {
232
274
  if (!options.apiKey)
233
275
  throw new Error(options.missingKeyError);
234
276
  if (active.has(turn.threadId))
235
277
  throw new Error("a turn is already running on this thread");
278
+ if (options.computerUse && (turn.images?.length || turn.integrations?.computer || turn.integrations?.localComputer || turn.integrations?.browser))
279
+ assertImageTransport(options.apiUrl);
236
280
  const turnId = newId();
237
281
  const abort = new AbortController();
238
282
  const messages = messagesFor(turn);
283
+ const retainImages = chatImageBudget();
284
+ for (const message of messages)
285
+ retainImages(message.content);
239
286
  const model = turn.model || options.models().default;
240
- const secrets = [options.apiKey];
287
+ const secrets = [options.apiKey, turn.integrations?.computer?.token, turn.integrations?.computer?.control?.token].filter((value) => Boolean(value));
241
288
  for (const integration of Object.values(turn.integrations ?? {})) {
242
289
  const entries = object(integration);
243
290
  const specs = entries && "command" in entries ? [entries] : Object.values(entries ?? {}).map(object);
@@ -270,10 +317,23 @@ export function createOpenAIChatRuntime(options) {
270
317
  ...base(turn.threadId, turnId), type: "request.resolved", requestId: ask.id,
271
318
  behavior: allowed ? "allow" : "deny", source,
272
319
  }),
320
+ openQuestion: (ask, questions) => {
321
+ const choices = questionChoices(questions);
322
+ emit({
323
+ ...base(turn.threadId, turnId), type: "request.opened", requestType: "question",
324
+ requestId: ask.id, tool: ask.tool, summary: ask.summary,
325
+ questions, ...(choices ? { choices } : {}),
326
+ });
327
+ },
328
+ resolvedQuestion: (ask, answered, source) => emit({
329
+ ...base(turn.threadId, turnId), type: "request.resolved", requestId: ask.id,
330
+ behavior: answered ? "answer" : "deny", source,
331
+ }),
273
332
  });
274
333
  let resolveDone;
275
334
  const done = new Promise((resolve) => { resolveDone = resolve; });
276
- active.set(turn.threadId, { abort, turnId, done, approval });
335
+ const turnEntry = { abort, turnId, done, approval, asides: [] };
336
+ active.set(turn.threadId, turnEntry);
277
337
  emit({ ...base(turn.threadId, turnId), type: "turn.started" });
278
338
  emit({ ...base(turn.threadId, turnId), type: "session.started", sessionId: null, model });
279
339
  void (async () => {
@@ -287,9 +347,25 @@ export function createOpenAIChatRuntime(options) {
287
347
  const denials = [];
288
348
  const seenCalls = new Set();
289
349
  try {
290
- tools = await mountChatTools(options.tools === false ? undefined : turn.integrations, abort.signal);
350
+ tools = await mountChatTools(options.tools === false ? undefined : turn.integrations, abort.signal, options.computerUse);
351
+ let optionalQuestionOnly = options.tools !== false && tools.definitions.length === 0;
352
+ // The runtime's one built-in tool rides the same list: ask_user is
353
+ // how a chat-completions engine reaches a person. An MCP server that
354
+ // squats the name cannot shadow it — dispatch intercepts the name
355
+ // before validate — but the definition is then skipped so the list
356
+ // never advertises two.
357
+ if (options.tools !== false && !tools.definitions.some((definition) => definition.function.name === ASK_USER_TOOL)) {
358
+ tools.definitions.push(ASK_USER_TOOL_DEFINITION);
359
+ }
291
360
  for (let round = 0; round < 16; round++) {
292
361
  abort.signal.throwIfAborted();
362
+ // Drain parked mid-turn input here, before the next completion
363
+ // request: the previous round's tool results are complete, so a
364
+ // user message lands on a consistent array (never inside a tool
365
+ // batch) and the provider sees it as the newest input.
366
+ const parked = turnEntry.asides.splice(0);
367
+ if (parked.length)
368
+ messages.push({ role: "user", content: parked.join("\n\n") });
293
369
  native("out", options.nativeLog.outgoing(turn, messages, model));
294
370
  let attempt = 0;
295
371
  let completion;
@@ -332,6 +408,15 @@ export function createOpenAIChatRuntime(options) {
332
408
  catch (value) {
333
409
  const error = asError(value);
334
410
  const verdict = classifyError(error);
411
+ // Only our optional question changed a previously plain request.
412
+ // A structured parameter rejection has executed nothing; never
413
+ // downgrade mounted tools, streamed output or a handled call.
414
+ if (optionalQuestionOnly && round === 0 && !streamed && !seenCalls.size && !abort.signal.aborted &&
415
+ error instanceof UnsupportedChatToolsError && verdict.reason === "invalid_request") {
416
+ optionalQuestionOnly = false;
417
+ tools.definitions.length = 0;
418
+ continue;
419
+ }
335
420
  // Once a call has been handled, never replay it through a turn retry.
336
421
  if (options.retryScale === undefined || abort.signal.aborted || streamed || seenCalls.size ||
337
422
  error instanceof ChatProtocolError || !verdict.transient || attempt >= RETRY_MAX_ATTEMPTS - 1)
@@ -381,6 +466,7 @@ export function createOpenAIChatRuntime(options) {
381
466
  ...(completion.protocolReasoning ? { reasoning_content: completion.protocolReasoning } : {}),
382
467
  ...(completion.protocolReasoningDetails.length ? { reasoning_details: completion.protocolReasoningDetails } : {}),
383
468
  });
469
+ const screenshotParts = [];
384
470
  for (const call of completion.toolCalls) {
385
471
  abort.signal.throwIfAborted();
386
472
  let result;
@@ -396,26 +482,66 @@ export function createOpenAIChatRuntime(options) {
396
482
  }
397
483
  if (!object(args))
398
484
  throw new ChatProtocolError("tool arguments must be a JSON object");
399
- tools.validate(call.function.name, args);
400
485
  const inputPreview = preview(args);
401
- // Full access is the person's explicit grant to answer every
402
- // prompt. This runtime has no provider reviewer to hand it to,
403
- // so it is honoured here: without it every single tool call on
404
- // an OpenAI-compatible engine stops for a card, and a Chief's
405
- // delegated Full access cannot help either.
406
- const allowed = turn.approvalMode === "full"
407
- || await approval.ask(call.function.name, inputPreview ?? "This tool has no arguments.");
408
- abort.signal.throwIfAborted();
409
- emit({ ...base(turn.threadId, turnId), type: "item.started", itemType: "tool", itemId: call.id,
410
- title: call.function.name, ...(inputPreview ? { input: inputPreview } : {}),
411
- });
412
- started = true;
413
- if (allowed) {
414
- result = await tools.execute(call.function.name, args, abort.signal);
486
+ if (call.function.name === ASK_USER_TOOL) {
487
+ // A question is the person's card, not a permission, so it is
488
+ // handled before the gate below: under Full access that gate
489
+ // would auto-run an unanswered ask, and under Ask it would
490
+ // render Allow/Deny over a question nobody can answer that way.
491
+ const questions = parseAskQuestions(args);
492
+ abort.signal.throwIfAborted();
493
+ emit({ ...base(turn.threadId, turnId), type: "item.started", itemType: "tool", itemId: call.id,
494
+ title: call.function.name, ...(inputPreview ? { input: inputPreview } : {}),
495
+ });
496
+ started = true;
497
+ if (!questions) {
498
+ denials.push(ASK_USER_TOOL);
499
+ result = { ok: false, text: "The ask_user arguments are malformed: pass a JSON object with a questions array of one to six questions, each with a question string and at most twelve options carrying a label. Ask again with valid arguments." };
500
+ }
501
+ else {
502
+ // The tool result is the card's Q:/A: reply verbatim — never
503
+ // a summary, and never words the person did not send.
504
+ const answer = await approval.question(ASK_USER_TOOL, askQuestionSummary(questions), questions);
505
+ abort.signal.throwIfAborted();
506
+ if (answer === null) {
507
+ denials.push(ASK_USER_TOOL);
508
+ result = { ok: false, text: "The person did not answer this question. Do not guess an answer; ask again later or proceed without it." };
509
+ }
510
+ else {
511
+ result = { ok: true, text: answer };
512
+ }
513
+ }
415
514
  }
416
515
  else {
417
- denials.push(call.function.name);
418
- result = { ok: false, text: "Permission denied or expired; the tool was not executed." };
516
+ tools.validate(call.function.name, args);
517
+ // Full access is the person's explicit grant to answer every
518
+ // prompt. This runtime has no provider reviewer to hand it to,
519
+ // so it is honoured here: without it every single tool call on
520
+ // an OpenAI-compatible engine stops for a card, and a Chief's
521
+ // delegated Full access cannot help either.
522
+ const allowed = turn.approvalMode === "full"
523
+ || await approval.ask(call.function.name, inputPreview ?? "This tool has no arguments.");
524
+ abort.signal.throwIfAborted();
525
+ emit({ ...base(turn.threadId, turnId), type: "item.started", itemType: "tool", itemId: call.id,
526
+ title: call.function.name, ...(inputPreview ? { input: inputPreview } : {}),
527
+ });
528
+ started = true;
529
+ if (allowed) {
530
+ result = await tools.execute(call.function.name, args, abort.signal);
531
+ if (result.images?.length) {
532
+ try {
533
+ assertImageTransport(options.apiUrl);
534
+ retainImages(result.images);
535
+ }
536
+ catch (error) {
537
+ throw new ChatToolSessionError(asError(error).message);
538
+ }
539
+ }
540
+ }
541
+ else {
542
+ denials.push(call.function.name);
543
+ result = { ok: false, text: "Permission denied or expired; the tool was not executed." };
544
+ }
419
545
  }
420
546
  }
421
547
  catch (error) {
@@ -433,10 +559,13 @@ export function createOpenAIChatRuntime(options) {
433
559
  if (!result.ok)
434
560
  toolFailed = true;
435
561
  messages.push({ role: "tool", tool_call_id: call.id, content: JSON.stringify({ ok: result.ok, result: text }) });
562
+ screenshotParts.push(...chatToolImages(call.id, result.images));
436
563
  abort.signal.throwIfAborted();
437
564
  if (fatal)
438
565
  throw fatal;
439
566
  }
567
+ if (screenshotParts.length)
568
+ messages.push({ role: "user", content: screenshotParts });
440
569
  }
441
570
  if (!ok)
442
571
  throw new ChatProtocolError("model-call limit reached before a final response");
@@ -488,7 +617,12 @@ export function createOpenAIChatRuntime(options) {
488
617
  : { state: "unavailable", reason: options.unavailableReason },
489
618
  adapter: {
490
619
  provider: options.driverKind,
491
- capabilities: { sessionModelSwitch: "in-session", customMcp: options.tools !== false, agentsMcp: options.tools !== false, composioMcp: options.tools !== false },
620
+ capabilities: { ...(options.computerUse ? { computerMcp: options.tools !== false, cloudComputerMcp: options.tools !== false, localComputerMcp: options.tools !== false,
621
+ browserMcp: options.tools !== false, nativeImageInput: true, images: true } : {}),
622
+ sessionModelSwitch: "in-session", customMcp: options.tools !== false, agentsMcp: options.tools !== false, composioMcp: options.tools !== false,
623
+ // The runtime owns the whole tool loop, so it can always take a
624
+ // user message mid-turn: park it, deliver before the next completion.
625
+ queueing: true },
492
626
  sendTurn,
493
627
  interruptTurn: async (threadId, turnId) => {
494
628
  const turn = active.get(threadId);
@@ -497,7 +631,20 @@ export function createOpenAIChatRuntime(options) {
497
631
  turn.abort.abort();
498
632
  await turn.done;
499
633
  },
500
- respondToRequest: async (threadId, requestId, decision) => active.get(threadId)?.approval.answer(requestId, decision.behavior) ?? "unavailable",
634
+ respondToRequest: async (threadId, requestId, decision) => active.get(threadId)?.approval.answer(requestId, decision.behavior, decision.message) ?? "unavailable",
635
+ steer: async (threadId, text) => {
636
+ // This engine has no external session to respect — parking the
637
+ // words in the live turn's entry IS delivery into the loop, so a
638
+ // successful park is "steered" and a missing turn is the only
639
+ // refusal. The words ride the array at the next round boundary;
640
+ // if the turn settles first, the harness's transcript record
641
+ // keeps them for the next turn (they are never re-sent here).
642
+ const running = active.get(threadId);
643
+ if (!running)
644
+ return "refused";
645
+ running.asides.push(text);
646
+ return "steered";
647
+ },
501
648
  hasSession: (threadId) => active.has(threadId),
502
649
  stopAll: async () => {
503
650
  const turns = [...active.values()];
@@ -133,6 +133,7 @@ export const OpenAICompatDriver = {
133
133
  apiKey,
134
134
  apiUrl: config.url,
135
135
  tools: config.tools,
136
+ computerUse: true,
136
137
  models: () => catalog,
137
138
  refreshModels: fetchModels,
138
139
  requestBody: (model, messages, stream) => ({
@@ -0,0 +1,35 @@
1
+ import { realpathSync } from "node:fs";
2
+ import { isAbsolute } from "node:path";
3
+ /** Approval identity preserves every command byte, including whitespace and
4
+ * newlines. Callers must supply native shell input, never a display preview. */
5
+ export function permissionCommand(command, cwd) {
6
+ if (typeof command !== "string" || !command.trim() || command.includes("\0"))
7
+ return undefined;
8
+ if (typeof cwd !== "string" || !isAbsolute(cwd) || cwd.includes("\0"))
9
+ return undefined;
10
+ return { command, cwd };
11
+ }
12
+ export function permissionLaunchCwd(cwd) {
13
+ try {
14
+ return realpathSync(cwd);
15
+ }
16
+ catch {
17
+ return undefined;
18
+ }
19
+ }
20
+ /** ACP's rawInput is provider-specific. Accept a complete command string and
21
+ * unambiguous absolute directory fields. A relative/invalid/conflicting
22
+ * directory is not evidence of where the agent will execute it. */
23
+ export function acpPermissionCommand(rawInput, launchCwd) {
24
+ if (!rawInput || typeof rawInput !== "object" || Array.isArray(rawInput))
25
+ return undefined;
26
+ const input = rawInput;
27
+ const directories = ["cwd", "workdir", "working_directory", "directory", "dir"]
28
+ .filter((key) => input[key] !== undefined)
29
+ .map((key) => input[key]);
30
+ if (directories.some((value) => typeof value !== "string" || !isAbsolute(value) || value.includes("\0")))
31
+ return undefined;
32
+ if (new Set(directories).size > 1)
33
+ return undefined;
34
+ return permissionCommand(input.command, directories[0] ?? launchCwd);
35
+ }
@@ -6,6 +6,10 @@ import { createInterface } from "node:readline";
6
6
  import { delimiter, join } from "node:path";
7
7
  import { homedir } from "node:os";
8
8
  const MAX_ADB_OUTPUT = 16 * 1024 * 1024;
9
+ // Mirrors the shared-computer blockedReason shape: name the holder's
10
+ // situation, say the call did not happen, and tell the model what beats a
11
+ // blind retry once the phone frees up.
12
+ const PHONE_BUSY_TEXT = "Another thread is using the phone. This call was not performed. Pause phone work until that thread finishes, then read the screen again before acting.";
9
13
  const PACKAGE = /^[A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)+$/;
10
14
  const COMPONENT = /^([A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)+)\/\S+$/;
11
15
  const SAFE_TEXT = /^[A-Za-z0-9 _.,@-]+$/;
@@ -240,6 +244,43 @@ const TOOLS = [
240
244
  { name: "press", description: "Press an Android navigation or keyboard key.", inputSchema: { type: "object", properties: { key: { type: "string", enum: Object.keys(KEYCODES) }, serial: { type: "string" } }, required: ["key"] } },
241
245
  ];
242
246
  const textResult = (text, isError = false) => ({ content: [{ type: "text", text }], ...(isError ? { isError: true } : {}) });
247
+ // The harness reserves the physical phone for one turn at a time, but only
248
+ // from the first real tool call (issue #1663): trigger-term matching decides
249
+ // whether these tools are mounted, never who holds the device. Revalidate
250
+ // every call: retained MCP processes can outlive their turn, whose claim and
251
+ // capability are released at settlement. Reclaiming for the same live owner
252
+ // is idempotent; a blocked caller can retry once the holding turn settles.
253
+ export function createPhoneClaim(env = process.env, fetchImpl = fetch) {
254
+ const url = env.OMB_HARNESS_URL?.trim();
255
+ const token = env.OMB_PHONE_TOKEN?.trim();
256
+ if (!url || !token) {
257
+ // Spawned outside the harness (development, direct debugging): no
258
+ // exclusivity to enforce, exactly the behavior this proxy always had.
259
+ return async () => ({ ok: true });
260
+ }
261
+ return async () => {
262
+ try {
263
+ const response = await fetchImpl(new URL("/api/internal/phone/claim", url), {
264
+ method: "POST",
265
+ redirect: "error",
266
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
267
+ body: "{}",
268
+ signal: AbortSignal.timeout(10_000),
269
+ });
270
+ if (response.ok)
271
+ return { ok: true };
272
+ if (response.status === 409)
273
+ return { ok: false, message: PHONE_BUSY_TEXT };
274
+ if (response.status === 401 || response.status === 403) {
275
+ return { ok: false, message: "This turn no longer has phone access. Start a new turn to use the phone." };
276
+ }
277
+ return { ok: false, message: "OpenMausBot could not reserve the phone for this call. This call was not performed; try again, or start a new turn if it keeps failing." };
278
+ }
279
+ catch {
280
+ return { ok: false, message: "OpenMausBot could not be reached to reserve the phone. This call was not performed; try again." };
281
+ }
282
+ };
283
+ }
243
284
  async function callTool(name, args) {
244
285
  if (name === "status") {
245
286
  const adb = resolveAdbPath();
@@ -334,7 +375,7 @@ async function callTool(name, args) {
334
375
  const send = (message) => process.stdout.write(`${JSON.stringify(message)}\n`);
335
376
  const ok = (id, result) => send({ jsonrpc: "2.0", id, result });
336
377
  const rpcError = (id, code, message) => send({ jsonrpc: "2.0", id, error: { code, message } });
337
- async function handle(message) {
378
+ async function handle(message, ensureClaim) {
338
379
  const id = message.id;
339
380
  const method = message.method;
340
381
  const params = (message.params ?? {});
@@ -350,6 +391,9 @@ async function handle(message) {
350
391
  const name = String(params.name ?? "");
351
392
  if (!TOOLS.some((tool) => tool.name === name))
352
393
  return rpcError(id, -32602, `Unknown tool: ${name}`);
394
+ const claim = await ensureClaim();
395
+ if (!claim.ok)
396
+ return ok(id, textResult(claim.message, true));
353
397
  try {
354
398
  return ok(id, await callTool(name, (params.arguments ?? {})));
355
399
  }
@@ -361,11 +405,12 @@ async function handle(message) {
361
405
  rpcError(id, -32601, `Method not found: ${String(method)}`);
362
406
  }
363
407
  if (process.argv[1] && existsSync(process.argv[1]) && /phone-proxy\.(?:ts|js)$/.test(process.argv[1])) {
408
+ const ensureClaim = createPhoneClaim();
364
409
  const lines = createInterface({ input: process.stdin, terminal: false });
365
410
  lines.on("line", (line) => {
366
411
  try {
367
412
  const message = JSON.parse(line);
368
- void handle(message).catch((error) => rpcError(message.id, -32603, error instanceof Error ? error.message : String(error)));
413
+ void handle(message, ensureClaim).catch((error) => rpcError(message.id, -32603, error instanceof Error ? error.message : String(error)));
369
414
  }
370
415
  catch { }
371
416
  });