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,10 +1,35 @@
1
- // Codex owns history; this receipt only remembers which bot rules it accepted.
1
+ // Codex owns history; this receipt only remembers which bot rules and which
2
+ // volatile context digest the native thread last carried.
2
3
  import { createHash } from "node:crypto";
3
4
  import { mkdirSync, readFileSync } from "node:fs";
4
5
  import { join } from "node:path";
5
6
  import { DATA_DIR } from "../config.js";
6
7
  import { writeFileAtomic } from "../atomic.js";
7
8
  const digest = (value) => createHash("sha256").update(value).digest("hex");
9
+ const isFingerprint = (value) => /^[0-9a-f]{64}$/.test(value);
10
+ function parseReceipt(raw) {
11
+ if (!raw)
12
+ return null;
13
+ const trimmed = raw.trim();
14
+ if (isFingerprint(trimmed))
15
+ return { instructions: trimmed };
16
+ try {
17
+ const parsed = JSON.parse(trimmed);
18
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
19
+ const record = parsed;
20
+ if (typeof record.instructions === "string" && isFingerprint(record.instructions)) {
21
+ return {
22
+ instructions: record.instructions,
23
+ ...(typeof record.volatile === "string" && isFingerprint(record.volatile) ? { volatile: record.volatile } : {}),
24
+ };
25
+ }
26
+ }
27
+ }
28
+ catch {
29
+ /* a corrupt receipt is simply unknown */
30
+ }
31
+ return null;
32
+ }
8
33
  /** Preserve native configured rules when overriding Codex's developer slot. */
9
34
  export function codexDeveloperInstructions(config, botInstructions) {
10
35
  if (!config || typeof config !== "object" || Array.isArray(config)) {
@@ -20,19 +45,21 @@ export function codexDeveloperInstructions(config, botInstructions) {
20
45
  ? `${botInstructions || "No OpenMausBot bot-specific instructions remain."}\n\n${configured}`
21
46
  : botInstructions;
22
47
  }
23
- export async function syncCodexInstructions(key, nativeThreadId, instructions, resumed, request) {
48
+ export async function syncCodexInstructions(key, nativeThreadId, instructions, volatile, resumed, request, mentionTurn = false) {
24
49
  const directory = join(DATA_DIR, "codex-instructions");
25
50
  const path = join(directory, `${digest(JSON.stringify([key, nativeThreadId]))}.sha256`);
26
51
  const fingerprint = digest(instructions);
52
+ const volatileFingerprint = digest(volatile);
27
53
  let previous;
28
54
  try {
29
- previous = readFileSync(path, "utf8");
55
+ previous = parseReceipt(readFileSync(path, "utf8"));
30
56
  }
31
57
  catch (error) {
32
58
  if (error.code !== "ENOENT")
33
59
  throw error;
60
+ previous = null;
34
61
  }
35
- if (resumed && previous !== fingerprint) {
62
+ if (resumed && previous?.instructions !== fingerprint) {
36
63
  // thread/resume config overrides are used after compaction, but Codex
37
64
  // 0.153.4 keeps the old initial developer message until then. Append a
38
65
  // developer update ONLY on change (or first adoption of an old session).
@@ -59,8 +86,23 @@ export async function syncCodexInstructions(key, nativeThreadId, instructions, r
59
86
  throw error;
60
87
  }
61
88
  }
62
- if (!resumed || previous !== fingerprint) {
89
+ // A mention describes the turn it rides: deliver it on every tagged turn,
90
+ // even when the volatile text is byte-identical to the previous turn's.
91
+ const deliverVolatile = !resumed || previous?.volatile !== volatileFingerprint || mentionTurn;
92
+ const hadVolatile = typeof previous?.volatile === "string" && previous.volatile !== digest("");
93
+ if (!resumed || previous?.instructions !== fingerprint) {
94
+ // Instructions-only, and only after inject_items acknowledged: the
95
+ // volatile digest stays pending until the driver commits it after the
96
+ // provider accepts the turn, so a submission the provider rejected
97
+ // redelivers its context on the next attempt.
63
98
  mkdirSync(directory, { recursive: true });
64
- writeFileAtomic(path, fingerprint, { mode: 0o600 });
99
+ writeFileAtomic(path, JSON.stringify({ instructions: fingerprint }), { mode: 0o600 });
65
100
  }
101
+ const commitVolatile = previous?.instructions === fingerprint && previous?.volatile === volatileFingerprint
102
+ ? null
103
+ : () => {
104
+ mkdirSync(directory, { recursive: true });
105
+ writeFileAtomic(path, JSON.stringify({ instructions: fingerprint, volatile: volatileFingerprint }), { mode: 0o600 });
106
+ };
107
+ return { deliverVolatile, hadVolatile, commitVolatile };
66
108
  }
@@ -0,0 +1,48 @@
1
+ // Release freshness is independent of account-specific model availability.
2
+ function parseCodexReleaseVersion(value) {
3
+ const match = /^(?:.*\bcodex-cli\s+)?v?(\d+)\.(\d+)\.(\d+)(-[0-9a-z.-]+)?(?:\+[0-9a-z.-]+)?$/i.exec(value.trim());
4
+ return match ? { parts: match.slice(1, 4).map(Number), prerelease: Boolean(match[4]) } : null;
5
+ }
6
+ export function codexVersionBehind(installed, latest) {
7
+ const a = parseCodexReleaseVersion(installed);
8
+ const b = parseCodexReleaseVersion(latest);
9
+ if (!a || !b || b.prerelease)
10
+ return false;
11
+ for (let i = 0; i < 3; i++) {
12
+ if (a.parts[i] !== b.parts[i])
13
+ return a.parts[i] < b.parts[i];
14
+ }
15
+ return a.prerelease;
16
+ }
17
+ export function createCodexReleaseReader(fetchImpl = fetch, now = Date.now) {
18
+ let cached;
19
+ let expires = 0;
20
+ return () => {
21
+ if (cached && now() < expires)
22
+ return cached;
23
+ expires = now() + 60 * 60 * 1000;
24
+ cached = (async () => {
25
+ try {
26
+ const response = await fetchImpl("https://registry.npmjs.org/@openai/codex/latest", {
27
+ signal: AbortSignal.timeout(3000),
28
+ headers: { Accept: "application/json" },
29
+ });
30
+ if (!response.ok)
31
+ throw new Error("Release lookup failed");
32
+ const payload = await response.json();
33
+ const version = typeof payload.version === "string" ? payload.version : "";
34
+ const parsed = parseCodexReleaseVersion(version);
35
+ if (!parsed || parsed.prerelease)
36
+ throw new Error("Invalid stable release");
37
+ return version;
38
+ }
39
+ catch {
40
+ // Offline checks must not make an otherwise usable engine unavailable.
41
+ expires = now() + 5 * 60 * 1000;
42
+ return null;
43
+ }
44
+ })();
45
+ return cached;
46
+ };
47
+ }
48
+ export const readLatestCodexRelease = createCodexReleaseReader();
@@ -0,0 +1,20 @@
1
+ // OpenMausBot owns computer selection and mounts its scoped MCP servers.
2
+ // Keep the standalone Codex installation untouched: these overrides apply
3
+ // only to the child process, including resumed conversations and retries.
4
+ // The desktop-app browser is not available in Codex CLI. Enabling its tool
5
+ // without a desktop connection exposes an unusable route; native search is
6
+ // independent and must remain governed by the user's/provider's config.
7
+ // https://developers.openai.com/codex/browser
8
+ export function codexToolSurfaceArgs() {
9
+ return [
10
+ "-c", "features.browser_use=false",
11
+ "-c", "features.browser_use_external=false",
12
+ "-c", "features.computer_use=false",
13
+ // Disable the matching instructions as well as the native tool. Use an
14
+ // inline table: quoted dotted keys are interpreted literally by some CLIs.
15
+ "-c", 'plugins={ "browser@openai-bundled" = { enabled = false }, "computer-use@openai-bundled" = { enabled = false }, "unified-computer-use@openai-bundled" = { enabled = false } }',
16
+ // Native web search does not require the desktop browser connection.
17
+ // Preserve the user's/provider's search mode instead of disabling it for
18
+ // every OpenMausBot process after a failure in one search route.
19
+ ];
20
+ }
@@ -1,3 +1,4 @@
1
+ import { codexToolSurfaceArgs } from "./codex-tool-surface.js";
1
2
  // Codex driver — upstream CodexDriver skeleton over agentcal's
2
3
  // drivers/codex.js runtime: the official `codex` CLI headless over its
3
4
  // app-server JSON-RPC protocol (newline-delimited JSON on stdio).
@@ -21,16 +22,18 @@ import { codexLocalProviderArgs } from "./local-inject.js";
21
22
  import { augmentedPath, splitCliString } from "../env-path.js";
22
23
  import { classifyError, computeBackoff, interruptibleDelay, RETRY_MAX_ATTEMPTS } from "./retry.js";
23
24
  import { appendNative } from "./native.js";
25
+ import { permissionCommand, permissionLaunchCwd } from "./permission-command.js";
24
26
  import { commandSummary, toolDetailPreview } from "../tool-summary.js";
25
27
  import { codexDeveloperInstructions, syncCodexInstructions } from "./codex-instructions.js";
28
+ import { volatileContextNote, withContextNote } from "./prompt-split.js";
26
29
  import { CodexDeviceAuthController } from "./codex-device-auth.js";
27
30
  import { codexAccountEmail } from "./codex-identity.js";
28
31
  import { classifyResumeFailure, mayReplay, recoveryPromptFor } from "../resume-recovery.js";
29
32
  import { extractMcpImages } from "../mcp-tool-images.js";
33
+ import { parseProtocolAskQuestions, questionAnswersById, questionChoices } from "../../shared/ask-question.js";
34
+ import { codexVersionBehind, readLatestCodexRelease } from "./codex-release.js";
30
35
  export { decodeCodexSelection, readCodexModelCatalog, STATIC_CODEX_MODELS } from "./codex-catalog.js";
31
36
  const DRIVER_KIND = "codex";
32
- const ASTRA_MODEL_ID = "gpt-6-astra";
33
- const ASTRA_MIN_CODEX_VERSION = [0, 153, 1];
34
37
  class CodexRpcError extends Error {
35
38
  code;
36
39
  constructor(error) {
@@ -45,23 +48,6 @@ function missingNativeCodexThread(error, cursor) {
45
48
  return error instanceof CodexRpcError && error.code === -32600 &&
46
49
  error.message === `no rollout found for thread id ${cursor}`;
47
50
  }
48
- /** Whether an installed Codex predates the release that exposes GPT-6 Astra
49
- * through app-server. Unknown version formats stay quiet: a bad guess should
50
- * never nag someone whose custom build may already support the model. */
51
- export function codexPredatesAstra(version) {
52
- const value = version.trim();
53
- const match = /\bcodex-cli\s+v?(\d+)\.(\d+)\.(\d+)(?:[-+][0-9a-z.-]+)?(?![\d.])\b/i.exec(value)
54
- ?? /^v?(\d+)\.(\d+)\.(\d+)(?:[-+][0-9a-z.-]+)?$/i.exec(value);
55
- if (!match)
56
- return false;
57
- const installed = match.slice(1, 4).map(Number);
58
- for (let i = 0; i < ASTRA_MIN_CODEX_VERSION.length; i += 1) {
59
- if (installed[i] !== ASTRA_MIN_CODEX_VERSION[i]) {
60
- return installed[i] < ASTRA_MIN_CODEX_VERSION[i];
61
- }
62
- }
63
- return false;
64
- }
65
51
  /** Ask the configured executable to update itself. This matters when the user
66
52
  * selected a non-PATH Codex: installing a second global copy would leave
67
53
  * OpenMausBot pointing at the old binary. */
@@ -81,13 +67,13 @@ export function codexUpdateCommand(cli, platform = process.platform) {
81
67
  const command = (tokens.length > 0 ? tokens : [trimmed]).map(quote).join(" ");
82
68
  return platform === "win32" ? `& ${command} update` : `${command} update`;
83
69
  }
84
- function codexAstraUpdate(version, models, cli) {
85
- if (models.options.some((model) => model.id === ASTRA_MODEL_ID) || !codexPredatesAstra(version)) {
70
+ async function codexReleaseUpdate(version, cli) {
71
+ const latest = await readLatestCodexRelease();
72
+ if (!latest || !codexVersionBehind(version, latest))
86
73
  return undefined;
87
- }
88
74
  return {
89
- title: "Update Codex for GPT-6 Astra",
90
- message: "This Codex version predates Astra support. Update it, then refresh models. Astra must also be available to your signed-in ChatGPT account.",
75
+ title: `Update Codex to ${latest}`,
76
+ message: `A newer stable Codex CLI is available (installed: ${version}). Update it, then refresh models. Model availability also depends on your signed-in account.`,
91
77
  command: codexUpdateCommand(cli),
92
78
  };
93
79
  }
@@ -126,7 +112,6 @@ export function managedCodexArgs(config) {
126
112
  "-c", "shell_environment_policy.ignore_default_excludes=false",
127
113
  ];
128
114
  }
129
- const QUESTION_TIMEOUT_NOTE = "No answer was given — use your best judgment.";
130
115
  const DENY_TIMEOUT_NOTE = "OpenMausBot: nobody answered this permission request in time. Skip this action and finish what you can without it.";
131
116
  const skippedSseServers = new Set();
132
117
  const renamedMcpServers = new Set();
@@ -275,6 +260,21 @@ function namedApprovalParams(mode) {
275
260
  },
276
261
  };
277
262
  }
263
+ /** Keep the turn on the complete sandbox resolved by native start/resume. */
264
+ function withResolvedSandbox(params, session) {
265
+ const requested = plainRecord(params.turn.sandboxPolicy);
266
+ // Named permission profiles own their policy; do not mix both selectors.
267
+ if (!requested)
268
+ return params;
269
+ const sandbox = plainRecord(plainRecord(session)?.sandbox);
270
+ if (!sandbox || typeof sandbox.type !== "string") {
271
+ throw new Error("Codex did not return its resolved sandbox policy. Update Codex, then retry; the turn was not started because its permissions could not be verified.");
272
+ }
273
+ if (sandbox.type !== requested.type) {
274
+ throw new Error("Codex did not apply the requested sandbox mode; cannot safely start the turn.");
275
+ }
276
+ return { ...params, turn: { ...params.turn, sandboxPolicy: sandbox } };
277
+ }
278
278
  function effectiveApprovalPolicy(value) {
279
279
  if (value === "untrusted" || value === "on-request" || value === "never")
280
280
  return value;
@@ -587,7 +587,7 @@ export const CodexDriver = {
587
587
  const retryScale = Number(process.env.FAKE_CODEX_RETRY_SCALE ?? "1");
588
588
  const launchAttempt = async (attempt) => {
589
589
  const env = childEnv();
590
- const appServerArgs = ["app-server", ...(config.managed ? managedCodexArgs(config.managed) : codexLocalProviderArgs(env, turn.model))];
590
+ const appServerArgs = ["app-server", ...(config.managed ? managedCodexArgs(config.managed) : codexLocalProviderArgs(env, turn.model)), ...codexToolSurfaceArgs()];
591
591
  if (turn.integrations?.composio) {
592
592
  mountMcpServer(appServerArgs, env, "openmausbot_connectors", turn.integrations.composio);
593
593
  }
@@ -626,6 +626,7 @@ export const CodexDriver = {
626
626
  const prefix = "mcp_servers.openmausbot_phone";
627
627
  appServerArgs.push("-c", `${prefix}.command=${JSON.stringify(bridge.command)}`, "-c", `${prefix}.args=${JSON.stringify(bridge.args)}`, "-c", `${prefix}.env_vars=${JSON.stringify(Object.keys(bridge.env))}`, "-c", `${prefix}.default_tools_approval_mode="auto"`);
628
628
  }
629
+ const commandCwd = permissionLaunchCwd(turn.cwd ?? homedir());
629
630
  const child = spawnCli(config.cli, appServerArgs, {
630
631
  cwd: turn.cwd ?? homedir(),
631
632
  env,
@@ -838,23 +839,22 @@ export const CodexDriver = {
838
839
  }
839
840
  return;
840
841
  }
841
- // One ask card carries one question honestly: its choices would come
842
- // from the first question alone and its one reply (including the
843
- // timeout note) would be copied into every question id (#1237).
844
- // Refuse the bundled call with a teaching error instead of
845
- // fabricating per-question answers.
846
- if (isQuestion && (!Array.isArray(params.questions) || params.questions.length !== 1)) {
847
- const bundled = Array.isArray(params.questions) && params.questions.length > 1;
842
+ // The whole set rides one card, answered per id: the protocol pairs
843
+ // each question with its own id, and the card's single reply is
844
+ // mapped back block by block (or as the flat single-question
845
+ // fallback). The only refusal left is an ask with nothing answerable
846
+ // — #1237 is closed: a bundle no longer copies one answer into
847
+ // every id.
848
+ const protocolQuestions = isQuestion ? parseProtocolAskQuestions(params.questions) : null;
849
+ if (isQuestion && !protocolQuestions) {
848
850
  send({
849
851
  jsonrpc: "2.0",
850
852
  id: msg.id,
851
853
  error: {
852
854
  code: -32602,
853
- message: bundled
854
- ? `ask supports one question per call; this request bundled ${params.questions.length}. Split it into separate asks, one question each.`
855
- : Array.isArray(params.questions)
856
- ? "ask supports one question per call; this request sent none."
857
- : "ask supports one question per call; params.questions must be an array with exactly one question.",
855
+ message: Array.isArray(params.questions)
856
+ ? "ask needs at least one answerable question — a string id and question text each; this request sent none."
857
+ : "ask needs params.questions to be an array of questions, each with a string id and question text.",
858
858
  },
859
859
  });
860
860
  return;
@@ -894,23 +894,36 @@ export const CodexDriver = {
894
894
  ? (mcpAppApproval?.summary ?? (typeof params.message === "string" ? params.message : "MCP access requested"))
895
895
  : typeof params.command === "string"
896
896
  ? params.command
897
- : Array.isArray(params.questions)
898
- ? params.questions.map((q) => q.question ?? q.header).filter(Boolean).join(" · ")
899
- : typeof params.reason === "string"
900
- ? params.reason
901
- : tool;
902
- const choices = isQuestion
903
- ? (params.questions?.[0]?.options ?? []).map((o) => o.label).slice(0, 5)
904
- : undefined;
897
+ : protocolQuestions
898
+ ? protocolQuestions.map(({ question }) => question.question).join(" · ")
899
+ : Array.isArray(params.questions)
900
+ ? params.questions.map((q) => q.question ?? q.header).filter(Boolean).join(" · ")
901
+ : typeof params.reason === "string"
902
+ ? params.reason
903
+ : tool;
904
+ // Flat choices only when one non-multiselect question can actually
905
+ // be answered by a bare reply; a bundle's first-question buttons
906
+ // would be an unusable lie for flat clients.
907
+ const choices = isQuestion && protocolQuestions ? questionChoices(protocolQuestions.map(({ question }) => question)) : undefined;
905
908
  const finish = (behavior, message, source = "user") => {
906
909
  if (!asks.delete(requestId))
907
910
  return;
908
911
  clearTimeout(timer);
909
912
  if (isQuestion) {
910
- const answers = {};
911
- for (const q of Array.isArray(params.questions) ? params.questions : []) {
912
- answers[q.id] = { answers: [message || QUESTION_TIMEOUT_NOTE] };
913
- }
913
+ // Only the person's reply is filed: Q:/A: blocks mapped to the
914
+ // id that asked them, or the flat fallback for a single
915
+ // question. Timeout and turn teardown send empty answers so
916
+ // every id reads unanswered — system notes never occupy the
917
+ // slot the model reads as the person's words (the rule
918
+ // permission-proxy already follows).
919
+ const mapped = behavior === "answer" && source === "user" && typeof message === "string"
920
+ ? questionAnswersById(message, protocolQuestions ?? [])
921
+ : {};
922
+ // Null prototype so an opaque id like __proto__ becomes a real
923
+ // answer key instead of hitting the inherited setter.
924
+ const answers = Object.create(null);
925
+ for (const [id, answer] of Object.entries(mapped))
926
+ answers[id] = { answers: [answer] };
914
927
  send({ jsonrpc: "2.0", id: msg.id, result: { answers } });
915
928
  }
916
929
  else {
@@ -922,7 +935,11 @@ export const CodexDriver = {
922
935
  }
923
936
  emit({ ...base(threadId, turnId), type: "request.resolved", requestId, behavior, source });
924
937
  };
925
- const timer = setTimeout(() => (isQuestion ? finish("answer", QUESTION_TIMEOUT_NOTE, "timeout") : finish("deny", DENY_TIMEOUT_NOTE, "timeout")), 15 * 60_000);
938
+ const timer = setTimeout(
939
+ // A timed-out question resolves as denied, not answered: the card
940
+ // closes with no reply (answers stay empty either way), and the
941
+ // resolve event must not claim an answer that never happened.
942
+ () => finish("deny", isQuestion ? undefined : DENY_TIMEOUT_NOTE, "timeout"), 15 * 60_000);
926
943
  timer.unref?.();
927
944
  asks.set(requestId, finish);
928
945
  emit({
@@ -932,9 +949,15 @@ export const CodexDriver = {
932
949
  requestType: isQuestion ? "question" : "permission",
933
950
  tool,
934
951
  summary,
952
+ command: method === "execCommandApproval" || method === "item/commandExecution/requestApproval"
953
+ ? permissionCommand(params.command, params.cwd ?? (
954
+ // Helpers may have a different workspace from their parent.
955
+ !params.threadId || params.threadId === codexThreadId ? commandCwd : undefined)) : undefined,
935
956
  choices,
957
+ ...(protocolQuestions ? { questions: protocolQuestions.map((pair) => pair.question) } : {}),
936
958
  approvalScope: controlsHost ? "local-computer" : undefined,
937
- requiresExplicitApproval: isAdditionalPermission || undefined,
959
+ requiresExplicitApproval: isAdditionalPermission || ((method === "execCommandApproval" || method === "item/commandExecution/requestApproval") &&
960
+ (params.additionalPermissions != null || params.networkApprovalContext != null)) || undefined,
938
961
  });
939
962
  };
940
963
  const handleNotification = (msg) => {
@@ -1315,7 +1338,18 @@ export const CodexDriver = {
1315
1338
  ? "Codex rejected its configuration as invalid. Check ~/.codex/config.toml (or CODEX_HOME) for a broken entry, then retry."
1316
1339
  : "Could not read Codex configuration; cannot safely update bot instructions. Retry after checking Codex.");
1317
1340
  }
1318
- const developerInstructions = codexDeveloperInstructions(effectiveConfig, turn.system ?? "");
1341
+ // Only the stable half of the prompt belongs in the developer slot:
1342
+ // it is the part that must survive compaction unchanged, and any
1343
+ // change to it invalidates the provider's cached prefix. The volatile
1344
+ // half (memory, mentions, outstanding teammate work) is delivered
1345
+ // inside the turn that changed it, after the cached prefix, the same
1346
+ // contract SendTurnInput.systemStable documents. Without the split
1347
+ // the driver keeps the previous single-block behaviour.
1348
+ const stableInstructions = typeof turn.systemStable === "string" && typeof turn.systemVolatile === "string"
1349
+ ? turn.systemStable
1350
+ : null;
1351
+ const promptSplit = stableInstructions !== null;
1352
+ const developerInstructions = codexDeveloperInstructions(effectiveConfig, stableInstructions ?? turn.system ?? "");
1319
1353
  let approvalParams;
1320
1354
  if (approvalMode === "custom") {
1321
1355
  // config/read returns the effective global + project config for this
@@ -1358,6 +1392,7 @@ export const CodexDriver = {
1358
1392
  approvalParams = approvalParams.fallback;
1359
1393
  resumed = await resumeThread();
1360
1394
  }
1395
+ approvalParams = withResolvedSandbox(approvalParams, resumed);
1361
1396
  codexThreadId = resumed?.thread?.id ?? cursor;
1362
1397
  resumedNativeThread = true;
1363
1398
  }
@@ -1403,12 +1438,20 @@ export const CodexDriver = {
1403
1438
  approvalParams = approvalParams.fallback;
1404
1439
  started = await startThread();
1405
1440
  }
1441
+ approvalParams = withResolvedSandbox(approvalParams, started);
1406
1442
  codexThreadId = started?.thread?.id ?? null;
1407
1443
  startedModel = started?.model ?? null;
1408
1444
  }
1409
1445
  if (!codexThreadId)
1410
1446
  throw new Error("Codex did not return a native thread id");
1411
- await syncCodexInstructions(threadId, codexThreadId, developerInstructions, resumedNativeThread, request);
1447
+ const { deliverVolatile, hadVolatile, commitVolatile } = await syncCodexInstructions(threadId, codexThreadId, developerInstructions, promptSplit ? turn.systemVolatile ?? "" : "", resumedNativeThread, request, Boolean(turn.mentionTurn));
1448
+ // A changed volatile half rides the next user input as a labelled
1449
+ // context block. It never touches the developer slot, so an
1450
+ // ordinary memory write or roster change neither appends a second
1451
+ // copy of the prompt to history nor re-uploads the conversation.
1452
+ if (deliverVolatile) {
1453
+ promptText = withContextNote(volatileContextNote(promptSplit ? turn.systemVolatile ?? "" : "", hadVolatile), promptText);
1454
+ }
1412
1455
  emit({ ...base(threadId, turnId), type: "session.started", sessionId: codexThreadId, model: startedModel ?? turn.model ?? null, ...(rebuiltFromReplay ? { rebuilt: true } : {}) });
1413
1456
  const turnInput = [
1414
1457
  ...(promptText ? [{ type: "text", text: promptText }] : []),
@@ -1442,6 +1485,10 @@ export const CodexDriver = {
1442
1485
  approvalParams = approvalParams.fallback;
1443
1486
  await startTurn();
1444
1487
  }
1488
+ // turn/start accepted the input: only now may the receipt claim the
1489
+ // volatile half was delivered, so a rejected turn redelivers on retry.
1490
+ if (commitVolatile)
1491
+ commitVolatile();
1445
1492
  }
1446
1493
  catch (e) {
1447
1494
  const failure = e instanceof Error ? e : { text: String(e) };
@@ -1514,7 +1561,7 @@ export const CodexDriver = {
1514
1561
  version,
1515
1562
  authenticated,
1516
1563
  ...(email ? { account: { email } } : {}),
1517
- update: codexAstraUpdate(version, models, config.cli),
1564
+ update: await codexReleaseUpdate(version, config.cli),
1518
1565
  billing: "subscription",
1519
1566
  };
1520
1567
  };
@@ -0,0 +1,115 @@
1
+ import { z } from "zod";
2
+ import { createOpenAIChatRuntime } from "./openai-chat.js";
3
+ const DEFAULT_URL = "https://api.mistral.ai/v1";
4
+ const DEFAULT_MODELS = {
5
+ default: "mistral-large-latest",
6
+ options: [
7
+ { id: "mistral-large-latest", label: "Mistral Large (latest)" },
8
+ { id: "mistral-small-latest", label: "Mistral Small (latest)" },
9
+ ],
10
+ };
11
+ const configSchema = z.object({
12
+ url: z.string().trim().url().default(DEFAULT_URL),
13
+ model: z.string().trim().min(1).optional(),
14
+ tools: z.boolean().optional(),
15
+ });
16
+ function decodeConfig(raw) {
17
+ const config = configSchema.parse(raw ?? {});
18
+ config.url = config.url.replace(/\/+$/, "");
19
+ const url = new URL(config.url);
20
+ if (url.protocol !== "https:" && !(url.protocol === "http:" &&
21
+ ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname))) {
22
+ throw new Error("Mistral requires HTTPS, except for a local test endpoint.");
23
+ }
24
+ return config;
25
+ }
26
+ const modelCard = z.object({
27
+ id: z.string().min(1),
28
+ name: z.string().nullable().optional(),
29
+ aliases: z.array(z.string()).optional(),
30
+ archived: z.boolean().optional(),
31
+ capabilities: z.object({ completion_chat: z.boolean().optional() }).optional(),
32
+ max_context_length: z.number().int().positive().optional(),
33
+ });
34
+ /** Mistral can return plain text or a list of typed content chunks. */
35
+ function contentText(content) {
36
+ if (typeof content === "string")
37
+ return content;
38
+ if (!Array.isArray(content))
39
+ return "";
40
+ return content.map((chunk) => chunk?.type === "text" && typeof chunk.text === "string" ? chunk.text : "").join("");
41
+ }
42
+ export const MistralDriver = {
43
+ driverKind: "mistral",
44
+ metadata: { displayName: "Mistral (API)", supportsMultipleInstances: true, access: "api" },
45
+ models: DEFAULT_MODELS,
46
+ install: {
47
+ docsUrl: "https://console.mistral.ai/api-keys",
48
+ signInCommand: "Save a Mistral API key in Settings → Connections, or set MISTRAL_API_KEY on the server.",
49
+ },
50
+ decodeConfig,
51
+ defaultConfig: () => decodeConfig({}),
52
+ async create(input) {
53
+ const { config } = input;
54
+ const apiKey = (input.environment.MISTRAL_API_KEY ?? process.env.MISTRAL_API_KEY ?? "").trim();
55
+ const withConfiguredModel = (options) => {
56
+ const preferred = config.model ?? DEFAULT_MODELS.default;
57
+ if (config.model && !options.some((option) => option.id === config.model)) {
58
+ options = [{ id: config.model, label: config.model }, ...options];
59
+ }
60
+ return { default: options.some((option) => option.id === preferred) ? preferred : options[0].id, options };
61
+ };
62
+ let catalog = withConfiguredModel(DEFAULT_MODELS.options);
63
+ const refreshModels = async () => {
64
+ if (!apiKey)
65
+ return;
66
+ try {
67
+ const response = await fetch(`${config.url}/models`, {
68
+ headers: { authorization: `Bearer ${apiKey}` },
69
+ redirect: "error",
70
+ signal: AbortSignal.timeout(8_000),
71
+ });
72
+ if (!response.ok)
73
+ return;
74
+ const json = await response.json();
75
+ const rows = Array.isArray(json) ? json : z.object({ data: z.array(z.unknown()) }).parse(json).data;
76
+ const options = [];
77
+ const seen = new Set();
78
+ for (const row of rows) {
79
+ const parsed = modelCard.safeParse(row);
80
+ if (!parsed.success || parsed.data.archived || parsed.data.capabilities?.completion_chat === false)
81
+ continue;
82
+ const card = parsed.data;
83
+ for (const id of [card.id, ...(card.aliases ?? [])]) {
84
+ if (!id.trim() || seen.has(id))
85
+ continue;
86
+ seen.add(id);
87
+ options.push({ id, label: id === card.id ? card.name?.trim() || id : id,
88
+ ...(card.max_context_length ? { contextWindow: card.max_context_length } : {}) });
89
+ }
90
+ }
91
+ if (options.length)
92
+ catalog = withConfiguredModel(options);
93
+ }
94
+ catch {
95
+ // A failed refresh keeps the last usable catalog, including custom models.
96
+ }
97
+ };
98
+ if (apiKey)
99
+ void refreshModels();
100
+ return createOpenAIChatRuntime({
101
+ input, driverKind: "mistral", apiKey, apiUrl: config.url,
102
+ tools: config.tools, models: () => catalog, refreshModels, contentText,
103
+ requestBody: (model, messages, stream) => ({ model, messages, stream }),
104
+ httpErrorLabel: "Mistral",
105
+ missingKeyError: "Save a Mistral API key in Settings → Connections, or set MISTRAL_API_KEY.",
106
+ unavailableReason: "No Mistral API key — open Settings → Connections.",
107
+ timeoutMs: 180_000, billing: "metered", includeUsageInCompleted: true,
108
+ nativeLog: {
109
+ source: "mistral.chat.completions",
110
+ outgoing: (_turn, messages, model) => ({ model, messageCount: messages.length }),
111
+ incoming: ({ text, usage }) => ({ textLength: text.length, usage }),
112
+ },
113
+ });
114
+ },
115
+ };