openmausbot 0.1.88 → 0.1.89

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 (82) hide show
  1. package/README.md +1 -1
  2. package/dist/assets/{abnfDiagram-O67JEVCF-qd9VD3GV.js → abnfDiagram-O67JEVCF-BwaMaZEm.js} +1 -1
  3. package/dist/assets/{arc-3RwkUjRe.js → arc-Be-8GKxd.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-CFKSJS7a.js → architectureDiagram-NJMV4G6O-Dp_U2Kv1.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-CG1rEfWD.js → blockDiagram-BEXU5L5S-DQjUjYUk.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-CBW9N664.js → c4Diagram-YGBWAQC7-Bd3aE4Kc.js} +1 -1
  7. package/dist/assets/channel-DqY_gMLg.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-2mRfTksB.js → chunk-2Q5K7J3B-BijjTBeJ.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-Cv1dpErj.js → chunk-3FUC2YCW-CMGdwvzW.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-B7IACJp-.js → chunk-5VM5RSS4-0G09BfsZ.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-An0hZtYa.js → chunk-GTNCS2PH-FOp3vSHe.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-BcJB2nEm.js → chunk-GWA4HPMP-Dv-20VIn.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-CV_W7TN7.js → chunk-JWPE2WC7-CRgrdFcz.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-Bcs_kwnA.js → chunk-NETBCI7D-3hP4f7QB.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-lrx_DxFP.js → chunk-WEXAMYUT-CG98IjlG.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-DabHlLGM.js → chunk-XXDRQBXY-BrSTC9xG.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-BnHj6P7d.js → classDiagram-v2-NBCMYWYE-rpQQQDFX.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-CfLp6UDi.js → cose-bilkent-JH36ORCC-G6RSafHA.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-Dm4dbVA1.js → cynefin-OW5HDTMX-CXjv-IM2.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-B9mH-PID.js → cynefinDiagram-VND7K2PF-Dw07MR9f.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-DDc_NVQJ.js → dagre-6A5THRUB-BwPnlTg2.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-lwr8Ip8-.js → diagram-22UHCM2B-Cm6xcMiz.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-BLLe6dmI.js → diagram-3UASUU5V-CDpogPbE.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-6eUIq4lz.js → diagram-ATOU4E4O-BV8z3Pnc.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-O-pRN7VQ.js → diagram-CDSNMT55-CKCQhOOC.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-BietvGAI.js → diagram-MLGK6HIB-FGvJg69C.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-Bd2Cl62X.js → diagram-MPIPVDR6-CwATvgnS.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-B6iDy69W.js → ebnfDiagram-ZINNZB2B-CH_c3SSC.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-DQVHfKe0.js → elk-276RUBZZ-3guZi71R.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-BWdVp2tQ.js → erDiagram-OPXOYQCR-BMHphR2n.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-WLE6rKB4.js → flowDiagram-KWPJA3E3-Vm5DkW5x.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-BbjS-4gn.js → ganttDiagram-FUAMR5RP-DO-1ct07.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-Dpy2QSGq.js → gitGraphDiagram-X574FWY7-nxeksEIN.js} +1 -1
  34. package/dist/assets/{index-CHnODCr2.js → index-BVPdbq2r.js} +142 -142
  35. package/dist/assets/{index-CIaHn5NS.js → index-Oxk9x45i.js} +1 -1
  36. package/dist/assets/{infoDiagram-VRGFBTTK-D_jKWlKe.js → infoDiagram-VRGFBTTK-Dkcvr0p7.js} +1 -1
  37. package/dist/assets/{ishikawaDiagram-OU5B5YK6-DLJohSuK.js → ishikawaDiagram-OU5B5YK6-BwVibkoO.js} +1 -1
  38. package/dist/assets/{journeyDiagram-ZHPQQLJL-DPOFj0qN.js → journeyDiagram-ZHPQQLJL-ByiBZqgG.js} +1 -1
  39. package/dist/assets/{kanban-definition-PNTS6WVX-BI7v-xpK.js → kanban-definition-PNTS6WVX-qqjgXBF3.js} +1 -1
  40. package/dist/assets/{linear-C2pWPSxO.js → linear-CtJnKe6A.js} +1 -1
  41. package/dist/assets/{mermaid.core-CR-j5WQU.js → mermaid.core-BEiDklrY.js} +5 -5
  42. package/dist/assets/{mindmap-definition-NLK3R4M7-lz9GvXRh.js → mindmap-definition-NLK3R4M7-DfIYHia9.js} +1 -1
  43. package/dist/assets/{pegDiagram-GJSIUBJH-Dl4_ia61.js → pegDiagram-GJSIUBJH-CYyRqa1a.js} +1 -1
  44. package/dist/assets/{pieDiagram-5QR66LMP-DWuT3n88.js → pieDiagram-5QR66LMP-hOToy_zH.js} +1 -1
  45. package/dist/assets/{quadrantDiagram-O4NWA36T-DXWe9Jx7.js → quadrantDiagram-O4NWA36T-DKAsNN9I.js} +1 -1
  46. package/dist/assets/{railroadDiagram-XR7U4H2S-COY1IBVY.js → railroadDiagram-XR7U4H2S-BSA-d8JU.js} +1 -1
  47. package/dist/assets/{requirementDiagram-PLB6GJNP-DnyVuhsQ.js → requirementDiagram-PLB6GJNP-CSmlvb-p.js} +1 -1
  48. package/dist/assets/{sankeyDiagram-IPEJSGJF-BSc4NdGK.js → sankeyDiagram-IPEJSGJF-CD5F6fnG.js} +1 -1
  49. package/dist/assets/{sequenceDiagram-PO4LG4MO-kqEU2EGq.js → sequenceDiagram-PO4LG4MO-JUq5sx6d.js} +1 -1
  50. package/dist/assets/{sizeCapture-INFHLROL-BvB4bkWD.js → sizeCapture-INFHLROL-BjIS-Stk.js} +1 -1
  51. package/dist/assets/{stateDiagram-v2-GCMORJYK-BIrqNv7y.js → stateDiagram-v2-GCMORJYK-DklkUxOg.js} +1 -1
  52. package/dist/assets/{swimlanes-2SLR337P-CFnalQUE.js → swimlanes-2SLR337P-CsjIU6be.js} +1 -1
  53. package/dist/assets/swimlanesDiagram-TC7HE7FX-Bk14M0_B.js +8 -0
  54. package/dist/assets/{timeline-definition-EJHVYXUP-CsSWJnSQ.js → timeline-definition-EJHVYXUP-BkFqK7AB.js} +1 -1
  55. package/dist/assets/{usecaseDiagram-POWQR4AR-DkAmJ1EE.js → usecaseDiagram-POWQR4AR-70gJB1EU.js} +1 -1
  56. package/dist/assets/{vennDiagram-UO4OBE2U-C3vd9drL.js → vennDiagram-UO4OBE2U-83Tm62dN.js} +1 -1
  57. package/dist/assets/{wardleyDiagram-VNRHLVJA-j26CM-pi.js → wardleyDiagram-VNRHLVJA-tTmseoX9.js} +1 -1
  58. package/dist/assets/{xychartDiagram-PMCCYNJV-BYu1Q_at.js → xychartDiagram-PMCCYNJV-R1LBA55a.js} +1 -1
  59. package/dist/index.html +1 -1
  60. package/dist-server/container-mcp.js +8 -0
  61. package/dist-server/index.js +44902 -44655
  62. package/dist-server/openmausbot.js +219 -110
  63. package/dist-server/pair-cli.js +219 -110
  64. package/dist-server/server/automatic-recovery.js +15 -0
  65. package/dist-server/server/cli-api-setup.js +0 -1
  66. package/dist-server/server/config.js +9 -0
  67. package/dist-server/server/container-computer.js +10 -0
  68. package/dist-server/server/contracts.js +10 -0
  69. package/dist-server/server/drivers/acp/core.js +124 -17
  70. package/dist-server/server/drivers/acp/qwen.js +3 -0
  71. package/dist-server/server/drivers/boxagent.js +1 -1
  72. package/dist-server/server/drivers/openai-chat.js +5 -1
  73. package/dist-server/server/harness-capabilities.js +18 -3
  74. package/dist-server/server/index.js +139 -56
  75. package/dist-server/server/system-prompt.js +11 -0
  76. package/dist-server/server/webhooks.js +21 -3
  77. package/dist-server/src/lib/composer-attachments.js +17 -0
  78. package/dist-server/vps-container-mcp.js +8 -0
  79. package/dist-server/workspace-backup.worker.js +8 -0
  80. package/package.json +1 -1
  81. package/dist/assets/channel-CnRnO3tF.js +0 -1
  82. package/dist/assets/swimlanesDiagram-TC7HE7FX-C4a8qNGj.js +0 -8
@@ -17799,10 +17799,18 @@ function qualifiedModelLabel(id, qualifier, options = {}) {
17799
17799
  if (!qualifier || redundantWhen(id, qualifier)) return id;
17800
17800
  return `${id}${decorate(qualifier)}`;
17801
17801
  }
17802
- var eventCounter, newEventId, newId;
17802
+ var TurnNotStartedError, eventCounter, newEventId, newId;
17803
17803
  var init_contracts = __esm({
17804
17804
  "server/contracts.ts"() {
17805
17805
  "use strict";
17806
+ TurnNotStartedError = class extends Error {
17807
+ turnId;
17808
+ constructor(turnId, message2) {
17809
+ super(message2);
17810
+ this.name = "TurnNotStartedError";
17811
+ this.turnId = turnId;
17812
+ }
17813
+ };
17806
17814
  eventCounter = 0;
17807
17815
  newEventId = () => `ev-${Date.now().toString(36)}-${(eventCounter++).toString(36)}`;
17808
17816
  newId = () => crypto.randomUUID();
@@ -19012,6 +19020,7 @@ function saveConfig(patch, options = {}) {
19012
19020
  if (checkedPatch.language !== void 0) disk.language = checkedPatch.language;
19013
19021
  if (checkedPatch.customDomain !== void 0) disk.customDomain = checkedPatch.customDomain;
19014
19022
  if (checkedPatch.signIn !== void 0) disk.signIn = checkedPatch.signIn;
19023
+ if (checkedPatch.automaticRecovery !== void 0) disk.automaticRecovery = checkedPatch.automaticRecovery;
19015
19024
  if (checkedPatch.defaultModelSelection !== void 0) {
19016
19025
  disk.defaultModelSelection = checkedPatch.defaultModelSelection;
19017
19026
  if (disk.newBotDefaults) {
@@ -19167,7 +19176,7 @@ function instanceConfigs(cfg) {
19167
19176
  }
19168
19177
  return map3;
19169
19178
  }
19170
- var optionalText, SSH_ALIAS, LEGACY_BROWSER_PROFILE_ID, BROWSER_PROFILE_ID, MIN_ROOM_TURN_TIMEOUT_MINUTES, MAX_ROOM_TURN_TIMEOUT_MINUTES, DEFAULT_ROOM_HANDOFF_LIFETIME_MINUTES, DEFAULT_ROOM_HANDOFF_MIN_RUNWAY_MINUTES, DEFAULT_ROOM_HANDOFF_HARD_CAP_MINUTES, DEFAULT_MAX_CONCURRENT_BOT_THREADS, MAX_CONCURRENT_BOT_THREADS, MIN_THREAD_EVENT_LOG_BYTES, MAX_THREAD_EVENT_LOG_BYTES, MIN_LOCAL_VM_MAX_INSTANCES, MAX_LOCAL_VM_MAX_INSTANCES, CDP_PORT, CDP_URL, vpsConfigSchema, browserEngineConfigSchema, roomConfigSchema, localVmConfigSchema, browserProfileSchema, legacyBrowserProfileSchema, legacyBrowserProfilesSchema, storedBrowserProfilesSchema, browserProfilesSchema, featureConfigSchema, onboardingConfigSchema, instanceConfigSchema, instanceConfigMapSchema, defaultModelSelectionSchema, threadsConfigSchema, newBotsConfigSchema, newBotsPatchSchema, threadsPatchSchema, appConfigSchema, storedAppConfigSchema, appConfigPatchSchema, jsonObjectSchema, DATA_DIR, LEGACY_DATA_DIR, EVENTS_DIR, NATIVE_DIR, lastIgnoredConfigWarning, WORKSPACE_CREDENTIAL_ENV, CONTROL_PLANE_ENV, CONTROL_PLANE_ENV_PREFIX, PROVIDER_CREDENTIAL_ENV, configSaveListeners;
19179
+ var optionalText, SSH_ALIAS, LEGACY_BROWSER_PROFILE_ID, BROWSER_PROFILE_ID, MIN_ROOM_TURN_TIMEOUT_MINUTES, MAX_ROOM_TURN_TIMEOUT_MINUTES, DEFAULT_ROOM_HANDOFF_LIFETIME_MINUTES, DEFAULT_ROOM_HANDOFF_MIN_RUNWAY_MINUTES, DEFAULT_ROOM_HANDOFF_HARD_CAP_MINUTES, DEFAULT_MAX_CONCURRENT_BOT_THREADS, MAX_CONCURRENT_BOT_THREADS, MIN_THREAD_EVENT_LOG_BYTES, MAX_THREAD_EVENT_LOG_BYTES, MIN_LOCAL_VM_MAX_INSTANCES, MAX_LOCAL_VM_MAX_INSTANCES, CDP_PORT, CDP_URL, vpsConfigSchema, browserEngineConfigSchema, roomConfigSchema, localVmConfigSchema, browserProfileSchema, legacyBrowserProfileSchema, legacyBrowserProfilesSchema, storedBrowserProfilesSchema, browserProfilesSchema, featureConfigSchema, onboardingConfigSchema, instanceConfigSchema, instanceConfigMapSchema, defaultModelSelectionSchema, automaticRecoverySchema, threadsConfigSchema, newBotsConfigSchema, newBotsPatchSchema, threadsPatchSchema, appConfigSchema, storedAppConfigSchema, appConfigPatchSchema, jsonObjectSchema, DATA_DIR, LEGACY_DATA_DIR, EVENTS_DIR, NATIVE_DIR, lastIgnoredConfigWarning, WORKSPACE_CREDENTIAL_ENV, CONTROL_PLANE_ENV, CONTROL_PLANE_ENV_PREFIX, PROVIDER_CREDENTIAL_ENV, configSaveListeners;
19171
19180
  var init_config = __esm({
19172
19181
  "server/config.ts"() {
19173
19182
  "use strict";
@@ -19307,6 +19316,13 @@ var init_config = __esm({
19307
19316
  (selection) => selection.variant === void 0 || selection.effort === void 0,
19308
19317
  "choose either a model variant or an effort level"
19309
19318
  );
19319
+ automaticRecoverySchema = external_exports.object({
19320
+ enabled: external_exports.boolean(),
19321
+ backup: defaultModelSelectionSchema.optional()
19322
+ }).strict().refine(
19323
+ (recovery) => !recovery.enabled || recovery.backup !== void 0,
19324
+ { message: "Choose a backup model before enabling automatic recovery", path: ["backup"] }
19325
+ );
19310
19326
  threadsConfigSchema = external_exports.object({
19311
19327
  maxConcurrentPerBot: external_exports.number().int().min(1).max(MAX_CONCURRENT_BOT_THREADS),
19312
19328
  /** Cap each per-thread events/ and native/ NDJSON log at this many
@@ -19335,6 +19351,7 @@ var init_config = __esm({
19335
19351
  * addresses or `@domain` entries; admins get every scope, members chat only. */
19336
19352
  signIn: external_exports.object({ admins: external_exports.array(external_exports.string().max(320)).max(500).optional(), members: external_exports.array(external_exports.string().max(320)).max(5e3).optional() }).optional(),
19337
19353
  defaultModelSelection: defaultModelSelectionSchema.optional(),
19354
+ automaticRecovery: automaticRecoverySchema.optional(),
19338
19355
  newBotDefaults: newBotDefaultsSchema.optional(),
19339
19356
  newBots: newBotsConfigSchema.optional(),
19340
19357
  /** CLI-only launch preferences. Never enable remote access implicitly. */
@@ -21501,6 +21518,101 @@ var init_session_idle = __esm({
21501
21518
  }
21502
21519
  });
21503
21520
 
21521
+ // shared/provider-safety.ts
21522
+ function isProviderSafetyBlock(message2) {
21523
+ return /\bblocked by (?:our|the provider['’]s) safety systems\b|\bsafety monitoring\b.{0,100}\b(?:paused|ended|blocked)\b|\b(?:safety_check_failed|safety_policy_violation)\b/i.test(message2);
21524
+ }
21525
+ var init_provider_safety = __esm({
21526
+ "shared/provider-safety.ts"() {
21527
+ "use strict";
21528
+ }
21529
+ });
21530
+
21531
+ // server/drivers/retry.ts
21532
+ function classifyError(err) {
21533
+ const text4 = messageOf(err);
21534
+ if (isProviderSafetyBlock(text4)) return { transient: false, reason: "provider_safety" };
21535
+ if (err && "exitCode" in err) {
21536
+ const { exitCode: code } = err;
21537
+ if (code !== null && code < 0) return { transient: false, reason: "interrupted" };
21538
+ for (const { pattern, reason } of TRANSIENT_PATTERNS) {
21539
+ if (pattern.test(text4)) return { transient: true, reason };
21540
+ }
21541
+ for (const { pattern, reason } of TERMINAL_PATTERNS) {
21542
+ if (pattern.test(text4)) return { transient: false, reason };
21543
+ }
21544
+ return { transient: false, reason: "terminal_exit" };
21545
+ }
21546
+ for (const { pattern, reason } of TERMINAL_PATTERNS) {
21547
+ if (pattern.test(text4)) return { transient: false, reason };
21548
+ }
21549
+ for (const { pattern, reason } of TRANSIENT_PATTERNS) {
21550
+ if (pattern.test(text4)) return { transient: true, reason };
21551
+ }
21552
+ return { transient: false, reason: "unknown" };
21553
+ }
21554
+ function computeBackoff(attempt, random = Math.random) {
21555
+ const base2 = BACKOFF_BASE_MS[Math.min(Math.max(attempt, 0), BACKOFF_BASE_MS.length - 1)];
21556
+ const jitter = base2 * 0.25;
21557
+ return Math.round(base2 - jitter + random() * jitter * 2);
21558
+ }
21559
+ function interruptibleDelay(ms, signal) {
21560
+ let timer = null;
21561
+ let onCancel = null;
21562
+ const promise2 = new Promise((resolve12) => {
21563
+ timer = setTimeout(() => resolve12("elapsed"), Math.max(1, ms));
21564
+ timer.unref?.();
21565
+ if (signal?.aborted) return resolve12("cancelled");
21566
+ onCancel = () => {
21567
+ clearTimeout(timer);
21568
+ resolve12("cancelled");
21569
+ };
21570
+ signal?.addEventListener("abort", onCancel, { once: true });
21571
+ });
21572
+ return {
21573
+ promise: promise2,
21574
+ cancel: () => onCancel?.()
21575
+ };
21576
+ }
21577
+ var RETRY_MAX_ATTEMPTS, BACKOFF_BASE_MS, TRANSIENT_PATTERNS, TERMINAL_PATTERNS, messageOf;
21578
+ var init_retry = __esm({
21579
+ "server/drivers/retry.ts"() {
21580
+ "use strict";
21581
+ init_provider_safety();
21582
+ RETRY_MAX_ATTEMPTS = 3;
21583
+ BACKOFF_BASE_MS = [1e3, 3e3, 8e3];
21584
+ TRANSIENT_PATTERNS = [
21585
+ { pattern: /\b(?:429|rate.?limit|too many requests)\b/i, reason: "rate_limited" },
21586
+ { pattern: /\boverloaded\b|\bcapacity\b/i, reason: "overloaded" },
21587
+ { pattern: /\b5\d{2}\b|\binternal server error\b|\bbad gateway\b|\bservice unavailable\b/i, reason: "server_error" },
21588
+ {
21589
+ pattern: /\b(?:econnreset|econnrefused|epipe|etimedout|eai_again|connection reset|connection refused|socket hang up|network error|fetch failed)\b/i,
21590
+ reason: "connection_reset"
21591
+ },
21592
+ { pattern: /\btimeout(ed)?\b|\btimed? out\b/i, reason: "timeout" }
21593
+ ];
21594
+ TERMINAL_PATTERNS = [
21595
+ {
21596
+ pattern: /\b(?:40[13]|unauthorized|forbidden|invalid api key|missing bearer|authentication required|not logged in|logged out)\b/i,
21597
+ reason: "auth"
21598
+ },
21599
+ { pattern: /\b402\b|\bquota\b|\bbilling\b|\bsubscription\b/i, reason: "quota" },
21600
+ { pattern: /\bmodel not found\b|\bunknown model\b|\bdoes not exist for model\b|\bunsupported model\b/i, reason: "unknown_model" },
21601
+ { pattern: /\b400\b|\b422\b|\binvalid request\b|\bmalformed\b|\bunexpected status\b/i, reason: "invalid_request" },
21602
+ { pattern: /\b404\b|\bno such thread\b|\bthread gone\b/i, reason: "not_found" },
21603
+ // interrupt/cancel vocabulary from the drivers' own stop paths — a turn the
21604
+ // user stopped must never come back as an auto-retry
21605
+ { pattern: /\b(?:interrupted|cancelled by user)\b/i, reason: "interrupted" }
21606
+ ];
21607
+ messageOf = (err) => {
21608
+ if (!err) return "";
21609
+ if (err instanceof Error) return `${err.message}${err.cause ? ` ${String(err.cause)}` : ""}`;
21610
+ if ("text" in err) return err.text;
21611
+ return [err.stderr ?? "", ""].join(" ").trim();
21612
+ };
21613
+ }
21614
+ });
21615
+
21504
21616
  // server/drivers/acp/core.ts
21505
21617
  import { homedir as homedir7 } from "node:os";
21506
21618
  import { lstat, mkdir as mkdir3, readFile, realpath, stat as stat2, writeFile } from "node:fs/promises";
@@ -21529,6 +21641,12 @@ function acpRpcError(value, method) {
21529
21641
  acpSessionFailure: value?.code === -32603
21530
21642
  });
21531
21643
  }
21644
+ function trackRunningTool(current, update, defaultStatus) {
21645
+ if (typeof update.toolCallId !== "string" || !update.toolCallId) return;
21646
+ const status = update.status ?? defaultStatus;
21647
+ if (status === "completed" || status === "failed") current.runningTools.delete(update.toolCallId);
21648
+ else if (status === "pending" || status === "in_progress") current.runningTools.add(update.toolCallId);
21649
+ }
21532
21650
  function acpVariantOption(result) {
21533
21651
  const option = (Array.isArray(result?.configOptions) ? result.configOptions : []).find(
21534
21652
  (entry) => entry?.type === "select" && typeof entry.id === "string" && (entry.id === "effort" || entry.category === "thought_level")
@@ -21676,6 +21794,18 @@ function createAcpDriver(support11) {
21676
21794
  const listeners = /* @__PURE__ */ new Set();
21677
21795
  const active = /* @__PURE__ */ new Map();
21678
21796
  const sessions = /* @__PURE__ */ new Map();
21797
+ const retiring = /* @__PURE__ */ new Map();
21798
+ const retireChild = async (threadId, child) => {
21799
+ let children = retiring.get(threadId);
21800
+ if (!children) retiring.set(threadId, children = /* @__PURE__ */ new Set());
21801
+ children.add(child);
21802
+ const stopped = await killCliTree(child);
21803
+ if (stopped) {
21804
+ children.delete(child);
21805
+ if (!children.size && retiring.get(threadId) === children) retiring.delete(threadId);
21806
+ }
21807
+ return stopped;
21808
+ };
21679
21809
  const { idleMs: SESSION_IDLE_MS } = sessionIdlePolicy("ACP");
21680
21810
  const closeSession = (threadId, why) => {
21681
21811
  const session = sessions.get(threadId);
@@ -21689,10 +21819,13 @@ function createAcpDriver(support11) {
21689
21819
  session.child.stdin.end();
21690
21820
  } catch {
21691
21821
  }
21692
- const kill = setTimeout(() => {
21693
- void killCliTree(session.child);
21694
- }, 5e3);
21695
- kill.unref?.();
21822
+ if (support11.restartOnMcpChange) void retireChild(threadId, session.child);
21823
+ else {
21824
+ const kill = setTimeout(() => {
21825
+ void killCliTree(session.child);
21826
+ }, 5e3);
21827
+ kill.unref?.();
21828
+ }
21696
21829
  };
21697
21830
  const armIdle = (threadId) => {
21698
21831
  const session = sessions.get(threadId);
@@ -21783,6 +21916,7 @@ function createAcpDriver(support11) {
21783
21916
  const current = session.current;
21784
21917
  if (!current || current.state.settled) return;
21785
21918
  current.state.settled = true;
21919
+ current.acknowledge();
21786
21920
  if (current.interruptTimer) clearTimeout(current.interruptTimer);
21787
21921
  for (const finish of current.asks.values()) finish("cancel", "system");
21788
21922
  session.acp.failAll(new Error("turn settled"));
@@ -21842,7 +21976,7 @@ function createAcpDriver(support11) {
21842
21976
  if (!(idleMs && idleMs > 0)) return;
21843
21977
  if (idleTimer) clearTimeout(idleTimer);
21844
21978
  idleTimer = setTimeout(() => {
21845
- if (session.current?.asks.size) {
21979
+ if (session.current?.asks.size || session.current?.runningTools.size) {
21846
21980
  armIdle2();
21847
21981
  return;
21848
21982
  }
@@ -21956,6 +22090,7 @@ function createAcpDriver(support11) {
21956
22090
  }
21957
22091
  };
21958
22092
  const handleServerRequest = (msg, current) => {
22093
+ current.state.startupActivity = true;
21959
22094
  if (msg.method === "fs/read_text_file" || msg.method === "fs/write_text_file") {
21960
22095
  void handleClientFileRequest(msg);
21961
22096
  return;
@@ -22059,6 +22194,9 @@ function createAcpDriver(support11) {
22059
22194
  if (current && !current.state.settled && session.sessionId && p2.sessionId === session.sessionId) current.receiveModelVariants(p2.update);
22060
22195
  return;
22061
22196
  }
22197
+ if (current && ["agent_message_chunk", "agent_thought_chunk", "tool_call", "tool_call_update"].includes(p2.update?.sessionUpdate)) {
22198
+ current.state.startupActivity = true;
22199
+ }
22062
22200
  if (!current || !current.state.promptSent) return;
22063
22201
  const u4 = p2.update ?? {};
22064
22202
  switch (u4.sessionUpdate) {
@@ -22090,6 +22228,7 @@ function createAcpDriver(support11) {
22090
22228
  }
22091
22229
  case "tool_call": {
22092
22230
  current.flushAssistantText();
22231
+ trackRunningTool(current, u4, "pending");
22093
22232
  emit({
22094
22233
  ...base2(threadId, current.turnId),
22095
22234
  type: "item.started",
@@ -22102,6 +22241,7 @@ function createAcpDriver(support11) {
22102
22241
  break;
22103
22242
  }
22104
22243
  case "tool_call_update": {
22244
+ trackRunningTool(current, u4);
22105
22245
  if (u4.status === "completed" || u4.status === "failed") {
22106
22246
  current.state.producedItem = true;
22107
22247
  emit({
@@ -22251,7 +22391,7 @@ function createAcpDriver(support11) {
22251
22391
  }
22252
22392
  const pooled = sessions.get(threadId);
22253
22393
  let session;
22254
- if (pooled && !pooled.dead && !pooled.closing && pooled.contractKey === contractKey) {
22394
+ if (pooled && !pooled.dead && !pooled.closing && pooled.contractKey === contractKey && (!support11.restartOnMcpChange || pooled.sessionKey === sessionKey)) {
22255
22395
  if (pooled.idleTimer) clearTimeout(pooled.idleTimer);
22256
22396
  pooled.idleTimer = null;
22257
22397
  session = pooled;
@@ -22264,7 +22404,15 @@ function createAcpDriver(support11) {
22264
22404
  sessions.set(threadId, session);
22265
22405
  }
22266
22406
  const request = (method, params, timeoutMs, receive, idleMs, idleMessage) => session.acp.request(method, params, timeoutMs, receive, idleMs, idleMessage);
22267
- const state = { settled: false, promptSent: false, text: "", producedItem: false };
22407
+ const state = { settled: false, promptSent: false, text: "", producedItem: false, startupActivity: false, stopped: false };
22408
+ let acknowledge = () => {
22409
+ };
22410
+ let rejectStartup = (_error) => {
22411
+ };
22412
+ const startupAck = turn.startupRecovery ? new Promise((resolve12, reject) => {
22413
+ acknowledge = () => resolve12({ turnId });
22414
+ rejectStartup = reject;
22415
+ }) : null;
22268
22416
  const asks = /* @__PURE__ */ new Map();
22269
22417
  const modelOf = (result) => {
22270
22418
  const option = (Array.isArray(result?.configOptions) ? result.configOptions : []).find(
@@ -22309,12 +22457,15 @@ function createAcpDriver(support11) {
22309
22457
  turnConfig,
22310
22458
  controlsHost,
22311
22459
  state,
22460
+ acknowledge,
22312
22461
  asks,
22462
+ runningTools: /* @__PURE__ */ new Set(),
22313
22463
  interruptTimer: null,
22314
22464
  flushAssistantText,
22315
22465
  receiveModelVariants
22316
22466
  };
22317
22467
  const interrupt = () => {
22468
+ state.stopped = true;
22318
22469
  if (session.sessionId) {
22319
22470
  session.acp.send({ jsonrpc: "2.0", method: "session/cancel", params: { sessionId: session.sessionId } });
22320
22471
  if (current.interruptTimer) clearTimeout(current.interruptTimer);
@@ -22327,12 +22478,24 @@ function createAcpDriver(support11) {
22327
22478
  closeSession(threadId, "stop");
22328
22479
  }
22329
22480
  };
22330
- active.set(threadId, { stop: () => closeSession(threadId, "stop"), interrupt, turnId, asks });
22481
+ active.set(threadId, { stop: () => {
22482
+ state.stopped = true;
22483
+ closeSession(threadId, "stop");
22484
+ }, interrupt, turnId, asks });
22331
22485
  emit({ ...base2(threadId, turnId), type: "turn.started" });
22332
22486
  session.current = current;
22333
22487
  (async () => {
22334
22488
  try {
22335
22489
  const handshake = async () => {
22490
+ for (const child of retiring.get(threadId) ?? []) {
22491
+ const stopped = await retireChild(threadId, child);
22492
+ if (state.settled || session.closing) throw new Error("session closed");
22493
+ if (!stopped) {
22494
+ closeSession(threadId, "replace-failed");
22495
+ throw new Error(`${support11.displayName} could not close its previous tool session. Try again.`);
22496
+ }
22497
+ }
22498
+ if (state.settled || session.closing) throw new Error("session closed");
22336
22499
  if (!session.initResult) {
22337
22500
  session.initResult = await request(
22338
22501
  "initialize",
@@ -22524,7 +22687,9 @@ ${promptInput.text}` : promptInput.text;
22524
22687
  if (turn.variant !== void 0 && requestedVariantOption().currentValue !== turn.variant) {
22525
22688
  throw new Error(`${support11.displayName} changed variant before the prompt`);
22526
22689
  }
22690
+ if (state.settled || state.stopped || session.closing) throw new Error("turn stopped");
22527
22691
  state.promptSent = true;
22692
+ acknowledge();
22528
22693
  const promptIdleMs = promptIdleTimeoutMs();
22529
22694
  const result = await request(
22530
22695
  "session/prompt",
@@ -22532,7 +22697,7 @@ ${promptInput.text}` : promptInput.text;
22532
22697
  void 0,
22533
22698
  void 0,
22534
22699
  promptIdleMs,
22535
- `${DRIVER_KIND8} went fully silent ${Math.round(promptIdleMs / 1e3)} s after the message and the turn was stopped. Raise OPENMAUS_ACP_PROMPT_IDLE_TIMEOUT_MS if this model legitimately takes longer to answer.`
22700
+ `${DRIVER_KIND8} sent nothing for ${Math.round(promptIdleMs / 1e3)} s with no tool running, so the turn was stopped as stuck. Send the message again to retry. On a self-hosted server, OPENMAUS_ACP_PROMPT_IDLE_TIMEOUT_MS sets this limit (0 turns it off).`
22536
22701
  );
22537
22702
  if (pendingSplitReceipt) {
22538
22703
  writePromptSplitReceipt(DRIVER_KIND8, pendingSplitReceipt.key, pendingSplitReceipt.receipt);
@@ -22563,6 +22728,32 @@ ${promptInput.text}` : promptInput.text;
22563
22728
  const message2 = e instanceof Error ? e.message : String(e);
22564
22729
  const code = support11.classifyError?.(e);
22565
22730
  const needsAuth = code === "invalid_credentials" || code === "inactive_subscription" || message2 === support11.loginNote;
22731
+ const failure = classifyError({ text: message2 });
22732
+ const transientStartup = failure.transient || failure.reason === "unknown" && code === "upstream_outage";
22733
+ const denied = /\b(?:(?:permission|access) denied|(?:approval|permission) (?:required|denied|rejected)|requires? (?:approval|permission)|policy (?:restriction|violation)|(?:blocked|denied|restricted) by (?:the )?policy)\b/i.test(message2);
22734
+ if (turn.startupRecovery && transientStartup && !needsAuth && !denied && (!code || code === "upstream_outage") && ![-32700, -32600, -32601, -32602].includes(e?.code) && !state.promptSent && !state.startupActivity && !state.producedItem && !state.text && !state.stopped && !session.closing && !asks.size && !current.runningTools.size) {
22735
+ state.settled = true;
22736
+ session.current = null;
22737
+ if (current.interruptTimer) clearTimeout(current.interruptTimer);
22738
+ closeSession(threadId, "startup-recovery");
22739
+ const stopped = await killCliTree(session.child).catch(() => false);
22740
+ if (current.interruptTimer) clearTimeout(current.interruptTimer);
22741
+ active.delete(threadId);
22742
+ if (stopped && !state.stopped) {
22743
+ rejectStartup(new TurnNotStartedError(turnId, message2));
22744
+ return;
22745
+ }
22746
+ acknowledge();
22747
+ if (!state.stopped) emit({ ...base2(threadId, turnId), type: "runtime.error", message: message2 });
22748
+ emit({
22749
+ ...base2(threadId, turnId),
22750
+ type: "turn.completed",
22751
+ ok: state.stopped,
22752
+ stopReason: state.stopped ? "cancelled" : "rpc_error",
22753
+ cost: null
22754
+ });
22755
+ return;
22756
+ }
22566
22757
  emit({
22567
22758
  ...base2(threadId, turnId),
22568
22759
  type: "runtime.error",
@@ -22576,7 +22767,7 @@ ${promptInput.text}` : promptInput.text;
22576
22767
  }
22577
22768
  }
22578
22769
  })();
22579
- return { turnId };
22770
+ return startupAck ?? { turnId };
22580
22771
  };
22581
22772
  const snapshot = async () => {
22582
22773
  const env = childEnv();
@@ -22632,6 +22823,9 @@ ${promptInput.text}` : promptInput.text;
22632
22823
  stopAll: async () => {
22633
22824
  for (const { stop } of active.values()) stop();
22634
22825
  for (const threadId of Array.from(sessions.keys())) closeSession(threadId, "stopAll");
22826
+ for (const [threadId, children] of retiring) {
22827
+ for (const child of children) void retireChild(threadId, child);
22828
+ }
22635
22829
  },
22636
22830
  onEvent: (listener) => {
22637
22831
  listeners.add(listener);
@@ -22641,6 +22835,9 @@ ${promptInput.text}` : promptInput.text;
22641
22835
  dispose: async () => {
22642
22836
  for (const { stop } of active.values()) stop();
22643
22837
  for (const threadId of Array.from(sessions.keys())) closeSession(threadId, "dispose");
22838
+ for (const [threadId, children] of retiring) {
22839
+ for (const child of children) void retireChild(threadId, child);
22840
+ }
22644
22841
  listeners.clear();
22645
22842
  }
22646
22843
  };
@@ -22666,6 +22863,7 @@ var init_core3 = __esm({
22666
22863
  init_redact2();
22667
22864
  init_resume_recovery();
22668
22865
  init_session_idle();
22866
+ init_retry();
22669
22867
  envOr = (key, fallback) => Number(process.env[key] ?? fallback);
22670
22868
  INIT_TIMEOUT = envOr("OPENMAUS_ACP_INIT_TIMEOUT_MS", 3e5);
22671
22869
  SESSION_CONFIG_TIMEOUT = envOr("OPENMAUS_ACP_SESSION_CONFIG_TIMEOUT_MS", 3e5);
@@ -24071,7 +24269,7 @@ var init_boxagent = __esm({
24071
24269
  snapshot,
24072
24270
  adapter: {
24073
24271
  provider: DRIVER_KIND,
24074
- capabilities: { sessionModelSwitch: "in-session" },
24272
+ capabilities: { sessionModelSwitch: "in-session", remoteAgent: true, usesCloudComputer: true },
24075
24273
  sendTurn,
24076
24274
  interruptTurn: async (threadId) => active.get(threadId)?.cancel(),
24077
24275
  respondToRequest: async (threadId, requestId, decision) => {
@@ -24524,101 +24722,6 @@ var init_mcp_gate_config = __esm({
24524
24722
  }
24525
24723
  });
24526
24724
 
24527
- // shared/provider-safety.ts
24528
- function isProviderSafetyBlock(message2) {
24529
- return /\bblocked by (?:our|the provider['’]s) safety systems\b|\bsafety monitoring\b.{0,100}\b(?:paused|ended|blocked)\b|\b(?:safety_check_failed|safety_policy_violation)\b/i.test(message2);
24530
- }
24531
- var init_provider_safety = __esm({
24532
- "shared/provider-safety.ts"() {
24533
- "use strict";
24534
- }
24535
- });
24536
-
24537
- // server/drivers/retry.ts
24538
- function classifyError(err) {
24539
- const text4 = messageOf(err);
24540
- if (isProviderSafetyBlock(text4)) return { transient: false, reason: "provider_safety" };
24541
- if (err && "exitCode" in err) {
24542
- const { exitCode: code } = err;
24543
- if (code !== null && code < 0) return { transient: false, reason: "interrupted" };
24544
- for (const { pattern, reason } of TRANSIENT_PATTERNS) {
24545
- if (pattern.test(text4)) return { transient: true, reason };
24546
- }
24547
- for (const { pattern, reason } of TERMINAL_PATTERNS) {
24548
- if (pattern.test(text4)) return { transient: false, reason };
24549
- }
24550
- return { transient: false, reason: "terminal_exit" };
24551
- }
24552
- for (const { pattern, reason } of TERMINAL_PATTERNS) {
24553
- if (pattern.test(text4)) return { transient: false, reason };
24554
- }
24555
- for (const { pattern, reason } of TRANSIENT_PATTERNS) {
24556
- if (pattern.test(text4)) return { transient: true, reason };
24557
- }
24558
- return { transient: false, reason: "unknown" };
24559
- }
24560
- function computeBackoff(attempt, random = Math.random) {
24561
- const base2 = BACKOFF_BASE_MS[Math.min(Math.max(attempt, 0), BACKOFF_BASE_MS.length - 1)];
24562
- const jitter = base2 * 0.25;
24563
- return Math.round(base2 - jitter + random() * jitter * 2);
24564
- }
24565
- function interruptibleDelay(ms, signal) {
24566
- let timer = null;
24567
- let onCancel = null;
24568
- const promise2 = new Promise((resolve12) => {
24569
- timer = setTimeout(() => resolve12("elapsed"), Math.max(1, ms));
24570
- timer.unref?.();
24571
- if (signal?.aborted) return resolve12("cancelled");
24572
- onCancel = () => {
24573
- clearTimeout(timer);
24574
- resolve12("cancelled");
24575
- };
24576
- signal?.addEventListener("abort", onCancel, { once: true });
24577
- });
24578
- return {
24579
- promise: promise2,
24580
- cancel: () => onCancel?.()
24581
- };
24582
- }
24583
- var RETRY_MAX_ATTEMPTS, BACKOFF_BASE_MS, TRANSIENT_PATTERNS, TERMINAL_PATTERNS, messageOf;
24584
- var init_retry = __esm({
24585
- "server/drivers/retry.ts"() {
24586
- "use strict";
24587
- init_provider_safety();
24588
- RETRY_MAX_ATTEMPTS = 3;
24589
- BACKOFF_BASE_MS = [1e3, 3e3, 8e3];
24590
- TRANSIENT_PATTERNS = [
24591
- { pattern: /\b(?:429|rate.?limit|too many requests)\b/i, reason: "rate_limited" },
24592
- { pattern: /\boverloaded\b|\bcapacity\b/i, reason: "overloaded" },
24593
- { pattern: /\b5\d{2}\b|\binternal server error\b|\bbad gateway\b|\bservice unavailable\b/i, reason: "server_error" },
24594
- {
24595
- pattern: /\b(?:econnreset|econnrefused|epipe|etimedout|eai_again|connection reset|connection refused|socket hang up|network error|fetch failed)\b/i,
24596
- reason: "connection_reset"
24597
- },
24598
- { pattern: /\btimeout(ed)?\b|\btimed? out\b/i, reason: "timeout" }
24599
- ];
24600
- TERMINAL_PATTERNS = [
24601
- {
24602
- pattern: /\b(?:40[13]|unauthorized|forbidden|invalid api key|missing bearer|authentication required|not logged in|logged out)\b/i,
24603
- reason: "auth"
24604
- },
24605
- { pattern: /\b402\b|\bquota\b|\bbilling\b|\bsubscription\b/i, reason: "quota" },
24606
- { pattern: /\bmodel not found\b|\bunknown model\b|\bdoes not exist for model\b|\bunsupported model\b/i, reason: "unknown_model" },
24607
- { pattern: /\b400\b|\b422\b|\binvalid request\b|\bmalformed\b|\bunexpected status\b/i, reason: "invalid_request" },
24608
- { pattern: /\b404\b|\bno such thread\b|\bthread gone\b/i, reason: "not_found" },
24609
- // interrupt/cancel vocabulary from the drivers' own stop paths — a turn the
24610
- // user stopped must never come back as an auto-retry
24611
- { pattern: /\b(?:interrupted|cancelled by user)\b/i, reason: "interrupted" }
24612
- ];
24613
- messageOf = (err) => {
24614
- if (!err) return "";
24615
- if (err instanceof Error) return `${err.message}${err.cause ? ` ${String(err.cause)}` : ""}`;
24616
- if ("text" in err) return err.text;
24617
- return [err.stderr ?? "", ""].join(" ").trim();
24618
- };
24619
- }
24620
- });
24621
-
24622
24725
  // server/drivers/claude.ts
24623
24726
  import { createHash as createHash7, randomBytes as randomBytes2 } from "node:crypto";
24624
24727
  import { chmodSync as chmodSync4, mkdirSync as mkdirSync10, mkdtempSync as mkdtempSync2, readFileSync as readFileSync8, rmSync as rmSync6, unlinkSync as unlinkSync3, writeFileSync as writeFileSync5 } from "node:fs";
@@ -38100,6 +38203,10 @@ function createOpenAIChatRuntime(options) {
38100
38203
  capabilities: {
38101
38204
  ...options.computerUse ? {
38102
38205
  computerMcp: options.tools !== false,
38206
+ // Same gate as cloudComputerMcp: with tools off the runtime cannot
38207
+ // mount the leased Box descriptor either. The fleet invariant test
38208
+ // pins usesCloudComputer === (remoteAgent || cloudComputerMcp).
38209
+ usesCloudComputer: options.tools !== false,
38103
38210
  cloudComputerMcp: options.tools !== false,
38104
38211
  localComputerMcp: options.tools !== false,
38105
38212
  browserMcp: options.tools !== false,
@@ -39989,6 +40096,9 @@ var init_qwen = __esm({
39989
40096
  // A raw -m only changes the model within the saved provider. ACP switches
39990
40097
  // the complete route and confirms it before any prompt leaves OMB.
39991
40098
  spawnArgs: (config2, turn) => ["--acp", ...qwenApprovalArgs(config2.fullAuto, turn.approvalMode)],
40099
+ // Qwen's live session/load returns its cached MCP clients, ignoring fresh
40100
+ // credentials. Cold-load the same conversation when those inputs change.
40101
+ restartOnMcpChange: true,
39992
40102
  selectModel: { configId: "model" },
39993
40103
  pickAuthMethod: () => null,
39994
40104
  authFailure: "continue",
@@ -48599,8 +48709,7 @@ var init_cli_api_setup = __esm({
48599
48709
  API_ENDPOINTS = [
48600
48710
  { id: "openai", label: "OpenAI API", url: "https://api.openai.com/v1", keyUrl: "https://platform.openai.com/api-keys" },
48601
48711
  { id: "openrouter", label: "OpenRouter", url: "https://openrouter.ai/api/v1", keyUrl: "https://openrouter.ai/settings/keys" },
48602
- { id: "groq", label: "Groq", url: "https://api.groq.com/openai/v1", keyUrl: "https://console.groq.com/keys" },
48603
- { id: "requesty", label: "Requesty", url: "https://router.requesty.ai/v1", keyUrl: "https://app.requesty.ai/api-keys" }
48712
+ { id: "groq", label: "Groq", url: "https://api.groq.com/openai/v1", keyUrl: "https://console.groq.com/keys" }
48604
48713
  ];
48605
48714
  SetupApiError = class extends Error {
48606
48715
  };
@@ -0,0 +1,15 @@
1
+ import { supportsWorkspaceFiles } from "./workspace.js";
2
+ /** An automatic switch must not strand the task without its tools or move
3
+ * its filesystem to another machine. Approval and organisation policy are
4
+ * checked by the ordinary model-switch and turn-admission gates. */
5
+ export function recoveryCapabilityError(from, to) {
6
+ if (from.capabilities.remoteAgent || to.capabilities.remoteAgent ||
7
+ supportsWorkspaceFiles(from.driverKind) !== supportsWorkspaceFiles(to.driverKind)) {
8
+ return "The backup cannot use the same workspace.";
9
+ }
10
+ const tools = ["agentsMcp", "computerMcp", "cloudComputerMcp", "composioMcp",
11
+ "phoneMcp", "browserMcp", "localComputerMcp", "customMcp", "images"];
12
+ if (tools.some(key => from.capabilities[key] === true && to.capabilities[key] !== true)) {
13
+ return "The backup does not support all of this engine's tools and attachments.";
14
+ }
15
+ }
@@ -4,7 +4,6 @@ export const API_ENDPOINTS = [
4
4
  { id: "openai", label: "OpenAI API", url: "https://api.openai.com/v1", keyUrl: "https://platform.openai.com/api-keys" },
5
5
  { id: "openrouter", label: "OpenRouter", url: "https://openrouter.ai/api/v1", keyUrl: "https://openrouter.ai/settings/keys" },
6
6
  { id: "groq", label: "Groq", url: "https://api.groq.com/openai/v1", keyUrl: "https://console.groq.com/keys" },
7
- { id: "requesty", label: "Requesty", url: "https://router.requesty.ai/v1", keyUrl: "https://app.requesty.ai/api-keys" },
8
7
  ];
9
8
  export function normalizeApiUrl(value) {
10
9
  const input = value.trim();
@@ -295,6 +295,10 @@ const defaultModelSelectionSchema = z.object({
295
295
  effort: z.enum(EFFORT_LEVELS).optional(),
296
296
  variant: z.string().refine(isModelVariant, "invalid model variant").optional(),
297
297
  }).refine((selection) => selection.variant === undefined || selection.effort === undefined, "choose either a model variant or an effort level");
298
+ const automaticRecoverySchema = z.object({
299
+ enabled: z.boolean(),
300
+ backup: defaultModelSelectionSchema.optional(),
301
+ }).strict().refine((recovery) => !recovery.enabled || recovery.backup !== undefined, { message: "Choose a backup model before enabling automatic recovery", path: ["backup"] });
298
302
  const threadsConfigSchema = z.object({
299
303
  maxConcurrentPerBot: z.number().int().min(1).max(MAX_CONCURRENT_BOT_THREADS),
300
304
  /** Cap each per-thread events/ and native/ NDJSON log at this many
@@ -327,6 +331,7 @@ const appConfigSchema = z.object({
327
331
  * addresses or `@domain` entries; admins get every scope, members chat only. */
328
332
  signIn: z.object({ admins: z.array(z.string().max(320)).max(500).optional(), members: z.array(z.string().max(320)).max(5000).optional() }).optional(),
329
333
  defaultModelSelection: defaultModelSelectionSchema.optional(),
334
+ automaticRecovery: automaticRecoverySchema.optional(),
330
335
  newBotDefaults: newBotDefaultsSchema.optional(),
331
336
  newBots: newBotsConfigSchema.optional(),
332
337
  /** CLI-only launch preferences. Never enable remote access implicitly. */
@@ -622,6 +627,7 @@ export const FLEET_NEUTRAL_KEYS = new Set([
622
627
  "vps",
623
628
  "rooms",
624
629
  "threads",
630
+ "automaticRecovery",
625
631
  "context",
626
632
  "localVm",
627
633
  "features",
@@ -953,6 +959,9 @@ export function saveConfig(patch, options = {}) {
953
959
  disk.customDomain = checkedPatch.customDomain;
954
960
  if (checkedPatch.signIn !== undefined)
955
961
  disk.signIn = checkedPatch.signIn;
962
+ // Replace the section so clearing a backup cannot revive the old selection.
963
+ if (checkedPatch.automaticRecovery !== undefined)
964
+ disk.automaticRecovery = checkedPatch.automaticRecovery;
956
965
  // A selection is replaced as one value, so changing engines also clears
957
966
  // an effort level omitted from the new selection.
958
967
  if (checkedPatch.defaultModelSelection !== undefined) {
@@ -7,6 +7,7 @@
7
7
  // typing, screenshots, accessibility, or window discovery.
8
8
  import { execFile } from "node:child_process";
9
9
  import { createHash, randomBytes } from "node:crypto";
10
+ import { lstatSync } from "node:fs";
10
11
  import { chmod, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
11
12
  import { tmpdir } from "node:os";
12
13
  import { join, resolve } from "node:path";
@@ -68,6 +69,15 @@ export function perBotLocalVmTarget(botId) {
68
69
  label: digest,
69
70
  };
70
71
  }
72
+ /** Only provisioning creates this durable directory; idle removal keeps it. */
73
+ export function localVmWorkspaceExists(target) {
74
+ try {
75
+ return lstatSync(target.workspaceDir, { throwIfNoEntry: false })?.isDirectory() === true;
76
+ }
77
+ catch {
78
+ return false;
79
+ }
80
+ }
71
81
  const LINUX_WHEELS = {
72
82
  x86_64: {
73
83
  url: "https://files.pythonhosted.org/packages/fa/d7/a43008a328a40c85e7bc706fc20235b9abedc75e28b413817655153157ff/cua_driver-0.20.0-py3-none-manylinux_2_31_x86_64.whl",
@@ -12,6 +12,16 @@ export class ProviderError extends Error {
12
12
  this.code = code;
13
13
  }
14
14
  }
15
+ /** The driver proved this attempt never reached a prompt or client-side action,
16
+ * and its owned process has stopped. The harness may safely recover it. */
17
+ export class TurnNotStartedError extends Error {
18
+ turnId;
19
+ constructor(turnId, message) {
20
+ super(message);
21
+ this.name = "TurnNotStartedError";
22
+ this.turnId = turnId;
23
+ }
24
+ }
15
25
  /** Variants are opaque provider IDs, not the cross-engine effort enum. */
16
26
  export function isModelVariant(value) {
17
27
  return typeof value === "string" && value.length > 0 && value.length <= 256 &&