openmausbot 0.1.96 → 0.1.97

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 (123) hide show
  1. package/dist/assets/{DesktopViewer-DsnA23KM.js → DesktopViewer-CPidMCtt.js} +1 -1
  2. package/dist/assets/{abnfDiagram-O67JEVCF-Dkbd2hcj.js → abnfDiagram-O67JEVCF-k1UCNXb7.js} +1 -1
  3. package/dist/assets/{arc-Duj5Coy2.js → arc-BUWGa9UU.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-uYUsDawP.js → architectureDiagram-NJMV4G6O-DiPor3UT.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-CWDlTpXe.js → blockDiagram-BEXU5L5S-Ag9nLW4H.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-B0t15PDc.js → c4Diagram-YGBWAQC7-Bh0sjgUa.js} +1 -1
  7. package/dist/assets/channel-DR8eD6QW.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-BJSCL-HV.js → chunk-2Q5K7J3B-BFBcIIqu.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-D3_ILccd.js → chunk-3FUC2YCW-Dvfpex-f.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-D3dmwT_S.js → chunk-5VM5RSS4-CMjP2YZW.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-Br73HFPD.js → chunk-GTNCS2PH-BryoeTc7.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-uSsjxubC.js → chunk-GWA4HPMP-BhfvcchO.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-C40VWFyr.js → chunk-JWPE2WC7-bcCVp8rp.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-D6WNNYwU.js → chunk-NETBCI7D-yKIjIlkQ.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-DOEhq64P.js → chunk-WEXAMYUT-D3Zqbmup.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-BqJEi6Pi.js → chunk-XXDRQBXY-CjVoIMG6.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-ByU_GnxT.js → classDiagram-v2-NBCMYWYE-mUCWdVyH.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-D-Uglxqq.js → cose-bilkent-JH36ORCC-BBmR1bbX.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-CZWO7aYj.js → cynefin-OW5HDTMX-BpRrxoO4.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-C70-w0la.js → cynefinDiagram-VND7K2PF-D3VBWCPK.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-CnhjhExB.js → dagre-6A5THRUB-DxEIACOO.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-bnjWCNb4.js → diagram-22UHCM2B-BANgZEOS.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-A4uTWoFV.js → diagram-3UASUU5V-Cnq7nJir.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-Zi6m3NUl.js → diagram-ATOU4E4O-4TBDIWnz.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-DtCcmd5k.js → diagram-CDSNMT55-CD_B39D4.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-DofuETS3.js → diagram-MLGK6HIB-CzvnFhEC.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-Dw2T9PuU.js → diagram-MPIPVDR6-C4w2SNml.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-DbrwPiem.js → ebnfDiagram-ZINNZB2B-gkUEBm2s.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-BvCt1dCb.js → elk-276RUBZZ-B_56X6mw.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-Y-QKqPnU.js → erDiagram-OPXOYQCR-C_oByoDS.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-CDkL1B5w.js → flowDiagram-KWPJA3E3-CR9xXJkC.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-CKV5D2F6.js → ganttDiagram-FUAMR5RP-gtYBTu0B.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-nL9iFTCv.js → gitGraphDiagram-X574FWY7-VJ6-Tp_p.js} +1 -1
  34. package/dist/assets/{index-DreDnjOu.js → index-Cp4S4NOL.js} +1 -1
  35. package/dist/assets/index-CpaIXRLp.js +614 -0
  36. package/dist/assets/index-Z81SMsgR.css +1 -0
  37. package/dist/assets/{infoDiagram-VRGFBTTK-C7aHjzsf.js → infoDiagram-VRGFBTTK-2Lu9te4D.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-KUZsNBTh.js → ishikawaDiagram-OU5B5YK6-DR49XY-e.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-CGUuodFl.js → journeyDiagram-ZHPQQLJL-ZoR0M305.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-Cy6r868Z.js → kanban-definition-PNTS6WVX-DLKHxrO-.js} +1 -1
  41. package/dist/assets/{linear-Bl669148.js → linear-BASsfWly.js} +1 -1
  42. package/dist/assets/{mermaid.core-86Xqz5Mr.js → mermaid.core-BVidiPkk.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-G4DzHLrd.js → mindmap-definition-NLK3R4M7-g_Qx0U5C.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-K4eruzY0.js → pegDiagram-GJSIUBJH-DI9Cjs8w.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-DYdR9PPq.js → pieDiagram-5QR66LMP-sRI0Uhr5.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-BCAh0nEy.js → quadrantDiagram-O4NWA36T-CamKTby3.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-p0dGRc4g.js → railroadDiagram-XR7U4H2S-DOLlkbf3.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-tLeBG0aL.js → requirementDiagram-PLB6GJNP-DrRQ5r97.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-QRuN16rV.js → sankeyDiagram-IPEJSGJF-DGS_lXcX.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-ueT1nVoa.js → sequenceDiagram-PO4LG4MO-CkjWqwtf.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-wRS2zWg2.js → sizeCapture-INFHLROL-CepRgful.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-CYzA_hE1.js → stateDiagram-v2-GCMORJYK-hykr-9RS.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-kDnv6LMc.js → swimlanes-2SLR337P-CKOHEZbI.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-D-8vqZ4-.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-D1y2UbbH.js → timeline-definition-EJHVYXUP-CB8pJ6AC.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-IqB4v9HX.js → usecaseDiagram-POWQR4AR-DrclJHdl.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-BEi7KYVF.js → vennDiagram-UO4OBE2U-zS23qjv8.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-Dkybo-Yf.js → wardleyDiagram-VNRHLVJA-52jgeKxz.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-BAPFJLpg.js → xychartDiagram-PMCCYNJV-DSdl9rQ8.js} +1 -1
  60. package/dist/index.html +2 -2
  61. package/dist-server/cloud-home-start.js +1 -1
  62. package/dist-server/drivers/agents-proxy.js +5 -5
  63. package/dist-server/index.js +2379 -1596
  64. package/dist-server/openmausbot.js +857 -1012
  65. package/dist-server/pair-cli.js +857 -1012
  66. package/dist-server/permission-proxy.js +0 -4
  67. package/dist-server/server/auto-approve.js +1 -1
  68. package/dist-server/server/auto-vm-claims.js +13 -1
  69. package/dist-server/server/automatic-recovery.js +1 -2
  70. package/dist-server/server/boat.js +68 -84
  71. package/dist-server/server/bot-overview.js +1 -1
  72. package/dist-server/server/browser-engine.js +38 -1
  73. package/dist-server/server/browser-live.js +112 -25
  74. package/dist-server/server/browser-runtime.js +31 -1
  75. package/dist-server/server/checkpoints.js +275 -44
  76. package/dist-server/server/cli-setup.js +3 -3
  77. package/dist-server/server/cloud-computer-tools.js +11 -4
  78. package/dist-server/server/cloud-home-start.js +1 -1
  79. package/dist-server/server/cloud-home.js +3 -9
  80. package/dist-server/server/cloud-owner.js +1 -1
  81. package/dist-server/server/computer-engine-removal.js +70 -0
  82. package/dist-server/server/computer-selection.js +18 -0
  83. package/dist-server/server/config.js +7 -11
  84. package/dist-server/server/connector-scope-hint.js +83 -0
  85. package/dist-server/server/default-model-selection.js +20 -14
  86. package/dist-server/server/delegations.js +11 -5
  87. package/dist-server/server/delta-context.js +6 -1
  88. package/dist-server/server/drivers/acp/core.js +1 -1
  89. package/dist-server/server/drivers/acp/opencode-go.js +16 -21
  90. package/dist-server/server/drivers/agents-call.js +1 -1
  91. package/dist-server/server/drivers/agents-catalog.js +3 -3
  92. package/dist-server/server/drivers/builtIn.js +0 -2
  93. package/dist-server/server/drivers/claude.js +49 -30
  94. package/dist-server/server/drivers/codex.js +2 -2
  95. package/dist-server/server/drivers/openai-chat.js +98 -6
  96. package/dist-server/server/drivers/pi.js +184 -49
  97. package/dist-server/server/index.js +555 -279
  98. package/dist-server/server/live-call.js +11 -6
  99. package/dist-server/server/managed-policy.js +1 -1
  100. package/dist-server/server/model-catalog/presets.js +8 -0
  101. package/dist-server/server/peer-roster.js +10 -2
  102. package/dist-server/server/room-turn-end.js +15 -0
  103. package/dist-server/server/routes/live.js +3 -3
  104. package/dist-server/server/routine-requests.js +1 -1
  105. package/dist-server/server/routines.js +1 -1
  106. package/dist-server/server/screen-frame-gate.js +1 -3
  107. package/dist-server/server/store.js +125 -1
  108. package/dist-server/server/surface.js +29 -49
  109. package/dist-server/server/system-prompt.js +10 -10
  110. package/dist-server/server/turn-continuation.js +162 -0
  111. package/dist-server/server/workspace.js +1 -1
  112. package/dist-server/shared/ask-question.js +0 -70
  113. package/dist-server/shared/cloud-computer.js +4 -6
  114. package/dist-server/shared/credential-request.js +1 -1
  115. package/dist-server/shared/failed-turn.js +1 -4
  116. package/dist-server/shared/place-view.js +409 -0
  117. package/dist-server/shared/tool-scope-support.js +1 -2
  118. package/package.json +1 -1
  119. package/dist/assets/channel-BZxwC-Gw.js +0 -1
  120. package/dist/assets/index-Du7o6Dsf.js +0 -610
  121. package/dist/assets/index-KxG4bWSr.css +0 -1
  122. package/dist/assets/swimlanesDiagram-TC7HE7FX-C7voMDle.js +0 -8
  123. package/dist-server/server/drivers/boatagent.js +0 -443
@@ -0,0 +1,18 @@
1
+ /** The option a request picks, and whether it is the computer this turn
2
+ * already has. `current` is the turn's mounted place. `startsOnFirstCall`
3
+ * says that place's computer is the one this turn's first computer call
4
+ * creates or wakes (the bot's own cloud computer): it is this turn's before
5
+ * it is up, so asking for it, or for Auto, keeps it instead of restarting
6
+ * the request somewhere else. */
7
+ export function pickComputer(options, requested, current, startsOnFirstCall) {
8
+ const isCurrent = (option) => option.surface === current && (option.ready || (startsOnFirstCall && option.available));
9
+ const option = requested === "auto"
10
+ ? options.find(isCurrent)
11
+ ?? options.find((candidate) => candidate.ready && candidate.surface === "vm")
12
+ ?? options.find((candidate) => candidate.ready)
13
+ ?? options.find((candidate) => candidate.canStart)
14
+ ?? options.find((candidate) => candidate.canCreate && candidate.surface === "vm")
15
+ ?? options.find((candidate) => candidate.canCreate)
16
+ : options.find((candidate) => candidate.surface === requested);
17
+ return { option, current: option !== undefined && isCurrent(option) };
18
+ }
@@ -7,6 +7,7 @@ import { join } from "node:path";
7
7
  import { z } from "zod";
8
8
  import { normalizeImageGenerationUrl } from "../shared/image-generation.js";
9
9
  import { writeFileAtomic } from "./atomic.js";
10
+ import { withoutComputerEngine } from "./computer-engine-removal.js";
10
11
  import { newBotDefaultsSchema } from "./new-bot-defaults.js";
11
12
  import { EFFORT_LEVELS } from "../shared/wire.js";
12
13
  import { isModelVariant } from "./contracts.js";
@@ -1443,10 +1444,10 @@ export function instanceOwnsRouting(cfg, entry, routingDefaults) {
1443
1444
  }
1444
1445
  /** The credential env instanceConfigs() injects for one driver at runtime.
1445
1446
  * Each secret goes only to the driver that actually reads it: the API-key
1446
- * Grok driver reads XAI_API_KEY, the Computer driver reads BOX_TOKEN, and
1447
- * OpenCode reads OPENCODE_API_KEY. Every other engine brings its own
1448
- * login, so handing it a key it never uses would only put that key in the
1449
- * environment of an unrelated child process. */
1447
+ * Grok driver reads XAI_API_KEY and OpenCode reads OPENCODE_API_KEY. Every
1448
+ * other engine brings its own login, so handing it a key it never uses would
1449
+ * only put that key in the environment of an unrelated child process. The
1450
+ * Boat token reaches no engine at all: the harness alone talks to Boat. */
1450
1451
  function injectedEnvironment(cfg, instanceId, driver) {
1451
1452
  const environment = new Map();
1452
1453
  if (driver === "mistral" && cfg.mistral?.key)
@@ -1485,11 +1486,6 @@ function injectedEnvironment(cfg, instanceId, driver) {
1485
1486
  if (driver === "openai-compat" && cfg.openaiCompat?.url)
1486
1487
  environment.set("OPENAI_COMPAT_URL", cfg.openaiCompat.url);
1487
1488
  }
1488
- // driverKind "boxAgent" and env BOX_TOKEN keep their historical names.
1489
- // Only the person's own token: without one the driver itself falls back
1490
- // to Cloud Pro's included token, which never enters an environment map.
1491
- if (driver === "boxAgent" && cfg.box?.token)
1492
- environment.set("BOX_TOKEN", cfg.box.token);
1493
1489
  if (driver === "opencodeGo") {
1494
1490
  // Keys for OpenCode's other providers, under the names OpenCode reads.
1495
1491
  for (const [name, key] of Object.entries(openCodeProviderKeys(cfg)))
@@ -1551,7 +1547,6 @@ export function instanceConfigs(cfg) {
1551
1547
  chatgpt: { driver: "codex", displayName: "ChatGPT plan", config: { authMode: "chatgpt-plan" } },
1552
1548
  antigravity: { driver: "antigravityAgent" },
1553
1549
  opencodeGo: { driver: "opencodeGo" },
1554
- computer: { driver: "boxAgent" },
1555
1550
  openaiCompat: { driver: "openai-compat" },
1556
1551
  mistral: { driver: "mistral" },
1557
1552
  cerebras: { driver: "cerebras" },
@@ -1578,7 +1573,8 @@ export function instanceConfigs(cfg) {
1578
1573
  ...CUSTOM_ONLY,
1579
1574
  };
1580
1575
  const configured = cfg.instances && Object.keys(cfg.instances).length ? cfg.instances : null;
1581
- const map = configured ? { ...configured } : { ...DEFAULT_FLEET };
1576
+ // A fleet saved before the Computer engine was removed may still name it.
1577
+ const map = withoutComputerEngine(configured ? { ...configured } : { ...DEFAULT_FLEET });
1582
1578
  // Product fleets pick up newly shipped engines. A one-off test/shadow map
1583
1579
  // (no claude/grok/codex) is left exactly as written.
1584
1580
  if (configured &&
@@ -0,0 +1,83 @@
1
+ // A connected-app call refused for a missing OAuth permission (MOCA-273).
2
+ //
3
+ // Google answers 403 ACCESS_TOKEN_SCOPE_INSUFFICIENT when the account's grant
4
+ // lacks a scope the action needs, such as gmail.settings.basic for a filter.
5
+ // Composio's default auth config for a toolkit never asks for that scope, so
6
+ // reconnecting cannot fix it and a bot that only sees the 403 retries or
7
+ // asks the person to reconnect. The fix is the person's own auth config in
8
+ // their Composio project, which OpenMausBot already prefers
9
+ // (listCustomAuthConfigs). This appends that explanation to the tool result,
10
+ // leaving the provider's own error untouched.
11
+ const SCOPE_ERROR = /ACCESS_TOKEN_SCOPE_INSUFFICIENT|insufficient authentication scopes|insufficientPermissions/i;
12
+ /** The Google scope a known tool needs, when the tool name says which. */
13
+ function requiredScope(slug) {
14
+ if (!slug.startsWith("GMAIL_"))
15
+ return null;
16
+ if (/FORWARD|SEND_AS|DELEGATE/.test(slug))
17
+ return "https://www.googleapis.com/auth/gmail.settings.sharing";
18
+ if (/FILTER|VACATION|AUTO_REPLY|IMAP|POP|LANGUAGE/.test(slug))
19
+ return "https://www.googleapis.com/auth/gmail.settings.basic";
20
+ return null;
21
+ }
22
+ function appName(slug) {
23
+ const toolkit = slug.split("_")[0] ?? "";
24
+ if (toolkit === "GMAIL")
25
+ return "Gmail";
26
+ if (toolkit.startsWith("GOOGLE"))
27
+ return "Google";
28
+ return toolkit.charAt(0) + toolkit.slice(1).toLowerCase();
29
+ }
30
+ /** The note a bot reads after its call was refused for a missing permission. */
31
+ export function scopeHint(slugs) {
32
+ const slug = slugs.find((candidate) => requiredScope(candidate)) ?? slugs[0] ?? "";
33
+ const app = slug ? appName(slug) : "This app";
34
+ const scope = slug ? requiredScope(slug) : null;
35
+ const permission = scope ? `the ${scope} permission` : "the permission this action needs";
36
+ return `OpenMausBot note: ${app} refused this because the connected account has not granted ${permission}. ` +
37
+ `Reconnecting will not add it: the default Composio connection never asks for it. ` +
38
+ `To allow it, the person creates their own ${app} auth config in their Composio project that includes ${permission}, ` +
39
+ `then reconnects ${app} under Connected apps; OpenMausBot uses that config automatically. ` +
40
+ `Tell the person this. Do not retry this call.`;
41
+ }
42
+ /** Adds the note to one JSON-RPC frame whose tool result is a scope refusal. */
43
+ function annotateFrame(frame, slugs) {
44
+ const result = frame?.result;
45
+ if (!result || !Array.isArray(result.content))
46
+ return false;
47
+ const refused = result.content.some((item) => item && typeof item === "object" && typeof item.text === "string" &&
48
+ SCOPE_ERROR.test(item.text));
49
+ if (!refused)
50
+ return false;
51
+ result.content.push({ type: "text", text: scopeHint(slugs) });
52
+ return true;
53
+ }
54
+ /**
55
+ * The relayed MCP response, with the note added when a tool call came back
56
+ * refused for a missing OAuth permission. Anything else, including a body it
57
+ * cannot read, is returned byte for byte.
58
+ */
59
+ export function withScopeHint(bytes, contentType, slugs) {
60
+ const text = new TextDecoder().decode(bytes);
61
+ if (!SCOPE_ERROR.test(text))
62
+ return bytes;
63
+ try {
64
+ if (/text\/event-stream/i.test(contentType)) {
65
+ let changed = false;
66
+ const lines = text.split("\n").map((line) => {
67
+ if (!line.startsWith("data:"))
68
+ return line;
69
+ const frame = JSON.parse(line.slice(5));
70
+ if (!annotateFrame(frame, slugs))
71
+ return line;
72
+ changed = true;
73
+ return `data: ${JSON.stringify(frame)}`;
74
+ });
75
+ return changed ? new TextEncoder().encode(lines.join("\n")) : bytes;
76
+ }
77
+ const frame = JSON.parse(text);
78
+ return annotateFrame(frame, slugs) ? new TextEncoder().encode(JSON.stringify(frame)) : bytes;
79
+ }
80
+ catch {
81
+ return bytes;
82
+ }
83
+ }
@@ -1,6 +1,19 @@
1
1
  const claudeFirst = (instances) => instances.find((instance) => instance.driverKind === "claudeAgent") ?? instances[0];
2
- /** A saved choice is intentional: an unavailable provider or removed model
3
- * sends new bots to setup instead of silently changing their provider. */
2
+ /** The ready rule: an engine can run a turn now when it is available, the
3
+ * organisation allows it, and it is not signed out. A custom endpoint brings
4
+ * its own credential, so its sign-in state does not count. */
5
+ export function readyToRun(instance, context = {}) {
6
+ return instance.snapshot.state === "available" && context.refusal?.(instance) === undefined &&
7
+ (instance.access === "custom" || instance.snapshot.authenticated !== false);
8
+ }
9
+ /** The engine a new bot gets, and the one a bot moved off a removed engine
10
+ * gets (computer-engine-removal.ts): one picker for both.
11
+ *
12
+ * A saved choice is intentional: an unavailable provider or removed model
13
+ * sends new bots to setup instead of silently changing their provider.
14
+ * Without one, an engine that can run a turn now wins (Claude first), then the
15
+ * first available one. On an enrolled desktop a working personal engine
16
+ * still beats a Company one, so billing stays the person's explicit choice. */
4
17
  export function selectDefaultModelSelection(instances, preferred, context = {}) {
5
18
  if (preferred) {
6
19
  const instance = instances.find((candidate) => candidate.instanceId === preferred.instanceId);
@@ -22,19 +35,12 @@ export function selectDefaultModelSelection(instances, preferred, context = {})
22
35
  }
23
36
  // The organisation's policy is inert (no refusal) unless this desktop is enrolled.
24
37
  const available = instances.filter((instance) => instance.snapshot.state === "available" && context.refusal?.(instance) === undefined);
38
+ const ready = available.filter((instance) => readyToRun(instance, context));
25
39
  const company = context.company;
26
- let pick;
27
- if (company && instances.some((instance) => company(instance.instanceId))) {
28
- // Enrolled: an installed but signed-out personal Claude would send every
29
- // new bot to setup although a Company model can run. Prefer instances that
30
- // can run a turn now; a working personal engine still wins, so billing
31
- // stays the person's explicit choice.
32
- const ready = available.filter((instance) => instance.access === "custom" || instance.snapshot.authenticated !== false);
33
- pick = claudeFirst(ready.filter((instance) => !company(instance.instanceId))) ?? claudeFirst(ready) ?? claudeFirst(available);
34
- }
35
- else {
36
- pick = claudeFirst(available);
37
- }
40
+ const personal = company && instances.some((instance) => company(instance.instanceId))
41
+ ? claudeFirst(ready.filter((instance) => !company(instance.instanceId)))
42
+ : undefined;
43
+ const pick = personal ?? claudeFirst(ready) ?? claudeFirst(available);
38
44
  return { instanceId: pick?.instanceId ?? "", model: pick?.models.default ?? "" };
39
45
  }
40
46
  /** Complete a new bot's selection with the workspace's new-bot effort. An
@@ -195,6 +195,8 @@ export function _loadPending() {
195
195
  }
196
196
  if (item.oneWay === true)
197
197
  loaded.oneWay = true;
198
+ if (item.completionOwner === "external")
199
+ loaded.completionOwner = "external";
198
200
  return [loaded];
199
201
  });
200
202
  if (items.length)
@@ -401,7 +403,7 @@ onSettled) {
401
403
  if (outcome === "settled" && stillQueued) {
402
404
  const receipt = findDelegationReceipt(item.id);
403
405
  try {
404
- if (receipt && !item.oneWay)
406
+ if (receipt && wakesSource(item))
405
407
  onSettled?.(receipt);
406
408
  }
407
409
  catch (error) {
@@ -445,6 +447,10 @@ function appendDeliveryMessage(bus, sourceThreadId, item, message) {
445
447
  return;
446
448
  bus.store.appendMessage(threadId, message);
447
449
  }
450
+ /** A settled handoff resumes the bot that queued it, unless its outcome
451
+ * belongs elsewhere: to a one-way send's recipient, or to an external
452
+ * runtime that polls for the receipt. */
453
+ const wakesSource = (item) => !item.oneWay && item.completionOwner !== "external";
448
454
  const isExpired = (item, now) => now - item.queuedAt >= DELEGATION_TTL_MS;
449
455
  /** Past the busy-hold cap — the tighter bound that fires while its target is
450
456
  * still busy, long before the 24-hour window. */
@@ -528,15 +534,15 @@ export function expireStaleDelegations(bus, now, onSettled) {
528
534
  expireDelegation(bus, threadId, item, ownerId ?? item.sourceBotId);
529
535
  const receipt = findDelegationReceipt(item.id);
530
536
  if (receipt)
531
- expired.push({ receipt, oneWay: item.oneWay === true });
537
+ expired.push({ receipt, wake: wakesSource(item) });
532
538
  }
533
539
  }
534
540
  if (!expired.length)
535
541
  return 0;
536
542
  savePending();
537
- for (const { receipt, oneWay } of expired) {
543
+ for (const { receipt, wake } of expired) {
538
544
  try {
539
- if (!oneWay)
545
+ if (wake)
540
546
  onSettled?.(receipt);
541
547
  }
542
548
  catch (error) {
@@ -717,7 +723,7 @@ async function processOne(bus, approvalBus, from, sourceThreadId, item, runTarge
717
723
  const prefixed = item.targetThreadId
718
724
  ? item.message
719
725
  : `[Delegated by @${sender.name}, another bot in this OpenMausBot workspace. Do the work and reply directly.]\n\n${item.message}${reasonLine}`;
720
- await runTarget(item.toBotId, prefixed, item.oneWay ? 0 : item.depth + 1, sourceThreadId, channel, item.id, sender.id, item.targetThreadId, item.oneWay === true);
726
+ await runTarget(item.toBotId, prefixed, item.oneWay ? 0 : item.depth + 1, sourceThreadId, channel, item.id, sender.id, item.targetThreadId, item.oneWay === true, item.completionOwner);
721
727
  return "dispatched";
722
728
  }
723
729
  /** "Is the target free to take this handoff right now?" Both shapes ask for
@@ -109,6 +109,7 @@ export function recordHanded(state, order, received) {
109
109
  return {
110
110
  ...(state.session === undefined ? {} : { session: state.session }),
111
111
  ...(state.config === undefined ? {} : { config: state.config }),
112
+ ...(state.effort === undefined ? {} : { effort: state.effort }),
112
113
  ...(state.omitted === undefined ? {} : { omitted: state.omitted }),
113
114
  ...(through === undefined ? {} : { through }),
114
115
  ids: sorted,
@@ -241,10 +242,14 @@ export class Handoffs {
241
242
  ? pending.resumeCursor === undefined ? pending.started : pending.recovery
242
243
  : existing === undefined ? pending.resumed : undefined;
243
244
  if (!start) {
245
+ if (typeof pending.effort === "string" && existing?.session === session && existing.effort !== pending.effort) {
246
+ this.store.write(pending.botId, threadId, pending.instanceId, { ...existing, effort: pending.effort });
247
+ }
244
248
  this.add(threadId, pending, [...pending.placed, ...pending.carried]);
245
249
  return;
246
250
  }
247
- this.store.write(pending.botId, threadId, pending.instanceId, recordHanded({ session, config: pending.config, ...(start.omitted === undefined ? {} : { omitted: start.omitted }), ids: [] }, this.store.order(threadId), start.sent));
251
+ this.store.write(pending.botId, threadId, pending.instanceId, recordHanded({ session, config: pending.config, ...(pending.effort === undefined ? {} : { effort: pending.effort }),
252
+ ...(start.omitted === undefined ? {} : { omitted: start.omitted }), ids: [] }, this.store.order(threadId), start.sent));
248
253
  }
249
254
  add(threadId, pending, received) {
250
255
  const session = pending.session ?? pending.resumeCursor;
@@ -350,7 +350,7 @@ export function createAcpDriver(support) {
350
350
  }
351
351
  // The operator's own secrets are outside any driver's allowlist.
352
352
  stripControlPlaneEnv(env);
353
- support.transformEnv?.(env, activeConfig, instanceId);
353
+ support.transformEnv?.(env, activeConfig, instanceId, input.environment);
354
354
  return env;
355
355
  };
356
356
  let models = support.models;
@@ -432,8 +432,6 @@ export function resetOpenCodeModelCache() {
432
432
  /** Compatibility export for older tests/imports while the product migrates
433
433
  * from the Go-only name. */
434
434
  export const resetOpenCodeGoModelCache = resetOpenCodeModelCache;
435
- /** The names live in config.ts, beside the keys the owner saves for OpenCode. */
436
- export { OPENCODE_PROVIDER_ENV };
437
435
  /** Whether OpenCode may read provider keys from the server's own
438
436
  * environment. Only when that environment is the person's own shell: not on a
439
437
  * Cloud home, a hosted team workspace or an organisation-managed desktop, and
@@ -453,23 +451,16 @@ let providerKeysAllowed = () => openCodeProviderKeysAllowed({
453
451
  export function setOpenCodeProviderKeyPolicy(allowed) {
454
452
  providerKeysAllowed = allowed;
455
453
  }
456
- let ownProviderKeys = () => ({});
457
- /** The keys the owner saved for OpenCode in Settings (config.ts
458
- * openCodeProviderKeys). The server sets this beside the policy above. */
459
- export function setOpenCodeOwnProviderKeys(keys) {
460
- ownProviderKeys = keys;
461
- }
462
- function withholdProviderKeysWhenManaged(env) {
454
+ /** Where OpenCode may not read the server's own environment, a provider key
455
+ * reaches it only through its instance environment: a key the owner saved
456
+ * in Settings (config.ts injectedEnvironment), which is theirs on any
457
+ * server. The same name riding along in the server's env stays out. */
458
+ function withholdProviderKeysWhenManaged(env, _config, _instanceId, instanceEnvironment) {
463
459
  if (providerKeysAllowed())
464
460
  return;
465
- const own = ownProviderKeys();
466
461
  for (const key of OPENCODE_PROVIDER_ENV) {
467
- // A key the owner saved for OpenCode in Settings is theirs to use on any
468
- // server. Only that exact value: the server's own key under the same
469
- // name still stays out.
470
- if (own[key] !== undefined && env[key] === own[key])
471
- continue;
472
- delete env[key];
462
+ if (!Object.hasOwn(instanceEnvironment, key))
463
+ delete env[key];
473
464
  }
474
465
  }
475
466
  function opencodeConfigDir(env) {
@@ -766,8 +757,10 @@ function allowOwnedDirectories(env, botId, cwd) {
766
757
  /** Account failures in plain words, with the fix, naming the provider that
767
758
  * refused: with provider keys and `opencode auth login` a model may be
768
759
  * OpenRouter's or Anthropic's, not Zen's. Each stays under the 160
769
- * characters a chat error row shows. */
770
- export function describeOpenCodeAccountError(code, model) {
760
+ * characters a chat error row shows. Zen and Go take the key saved in
761
+ * Settings. On a Cloud the owner has no terminal for `opencode auth login`:
762
+ * another provider's key is saved under Keys for other OpenCode providers. */
763
+ export function describeOpenCodeAccountError(code, model, where = { cloudHome: cloudHomeConfigured() }) {
771
764
  const provider = model && model.includes("/") ? model.slice(0, model.indexOf("/")) : "";
772
765
  const zen = provider === "opencode";
773
766
  const go = provider === "opencode-go";
@@ -776,8 +769,10 @@ export function describeOpenCodeAccountError(code, model) {
776
769
  switch (code) {
777
770
  case "invalid_credentials":
778
771
  return zen || go || !provider
779
- ? "OpenCode rejected its key, or has none for this model. Fix it in Settings → API keys or with `opencode auth login`."
780
- : `OpenCode's ${name} key for this model is missing or was rejected. Fix it with \`opencode auth login\`, or choose another model.`;
772
+ ? "OpenCode rejected its key, or has none for this model. Add or replace the OpenCode key in Settings → API keys."
773
+ : where.cloudHome
774
+ ? `OpenCode's ${name} key for this model is missing or was rejected. Save it under Keys for other OpenCode providers in Settings → API keys.`
775
+ : `OpenCode's ${name} key for this model is missing or was rejected. Fix it with \`opencode auth login\`, or choose another model.`;
781
776
  case "insufficient_funds":
782
777
  return zen || !provider
783
778
  ? "Your OpenCode Zen balance has run out. Add credit at opencode.ai, or choose one of Zen's free models for this bot."
@@ -812,7 +807,7 @@ const support = (loadCatalog) => ({
812
807
  models: NO_MODELS,
813
808
  defaultCli: "opencode",
814
809
  nativeSource: "opencode.acp",
815
- loginNote: "OpenCode has no usable models — run `opencode auth login` or connect a provider in the OpenCode app",
810
+ loginNote: "OpenCode has no usable models. Add an OpenCode key in Settings → API keys.",
816
811
  install: {
817
812
  command: {
818
813
  darwin: "npm install -g opencode-ai",
@@ -259,7 +259,7 @@ function routineFields(args) {
259
259
  const runOn = destination(args.run_on ?? args.runOn);
260
260
  const timeoutMinutes = args.timeout_minutes ?? args.timeoutMinutes;
261
261
  if (runOn != null && runOn !== "maus" && runOn !== "cloud") {
262
- return { fields, error: 'Use run_on="maus" for the bot’s current model and configured computer (including VPS), or run_on="box" only for the bot’s Boat cloud computer. Legacy "cloud" also means Boat.' };
262
+ return { fields, error: 'Use run_on="maus" for the bot’s current model and configured computer (including VPS), or run_on="box" only for the bot’s cloud computer. Legacy "cloud" also means box.' };
263
263
  }
264
264
  if (timeoutMinutes != null && (typeof timeoutMinutes !== "number" || !Number.isInteger(timeoutMinutes) || timeoutMinutes < 5 || timeoutMinutes > 240)) {
265
265
  return { fields, error: "timeout_minutes must be a whole number from 5 to 240. Use clear_timeout to remove a limit." };
@@ -132,7 +132,7 @@ const ROUTINE_FIELDS_SCHEMA = {
132
132
  type: "string",
133
133
  // "box" is Boat's historical run_on destination id (agents wire contract).
134
134
  enum: ["maus", "box"],
135
- description: "Default maus keeps the bot's selected model and configured computer, INCLUDING a self-hosted VPS. Omit this field for normal schedules. box runs on the bot's Boat cloud computer, same model; it requires Boat setup and is not the generic cloud/VPS option. Legacy cloud values from list_routines mean box, not VPS.",
135
+ description: "Default maus keeps the bot's selected model and configured computer, INCLUDING a self-hosted VPS. Omit this field for normal schedules. box runs on the bot's cloud computer, same model; it needs cloud computers set up and is not the generic cloud/VPS option. Legacy cloud values from list_routines mean box, not VPS.",
136
136
  },
137
137
  timeout_minutes: {
138
138
  type: "integer",
@@ -303,7 +303,7 @@ const toolDefinitions = (externalRuntime) => [
303
303
  description: "Choose where this conversation does computer work. Call with no arguments to inspect actual available choices and the current place. For a task needing computer interaction, select the requested place, or auto to choose a suitable configured computer without asking the user to use menus. OpenMausBot reuses an existing computer first; with a configured provider it can start or provision one when needed. Do not provision for ordinary chat or just to inspect availability. A pending result means end this turn immediately: OpenMausBot updates the conversation selector and resumes the original request with that computer's real tools. Do not use the old tools after requesting a switch, repeat the task, or claim the action is done. This cannot change permissions, override Off, or switch a teammate/routine/channel.",
304
304
  inputSchema: { type: "object", additionalProperties: false, properties: {
305
305
  surface: { type: "string", enum: ["auto", "cloud", "vm", "local", "browser"],
306
- description: "auto = suitable configured computer, cloud = remote Boat/VPS, vm = isolated Local VM, local = user's own desktop, browser = built-in browser. Omit to list." },
306
+ description: "auto = suitable configured computer, cloud = the bot's cloud computer or self-hosted VPS, vm = isolated Local VM, local = user's own desktop, browser = built-in browser. Omit to list." },
307
307
  } },
308
308
  },
309
309
  {
@@ -854,7 +854,7 @@ const WATCHER_TOOL_NAMES = new Set(["create_options_card"]);
854
854
  const LOCAL_VM_TOOL_NAMES = new Set(["vm_exec"]);
855
855
  const CLOUD_HOME_SURFACE = {
856
856
  type: "string", enum: ["auto", "cloud", "browser"],
857
- description: "auto = suitable configured computer, cloud = remote Boat/VPS, browser = built-in browser. Omit to list. This server runs in the cloud: the user's own computer and a Local VM are not places here.",
857
+ description: "auto = suitable configured computer, cloud = the bot's cloud computer, browser = built-in browser. Omit to list. This server runs in the cloud: the user's own computer and a Local VM are not places here.",
858
858
  };
859
859
  /** The tools one turn is shown, exactly as tools/list serializes them. */
860
860
  export function availableTools(profile) {
@@ -1,5 +1,4 @@
1
1
  import { AntigravityDriver } from "./antigravity.js";
2
- import { BoatAgentDriver } from "./boatagent.js";
3
2
  import { ClaudeDriver } from "./claude.js";
4
3
  import { CodexDriver } from "./codex.js";
5
4
  import { GrokDriver } from "./grok.js";
@@ -33,7 +32,6 @@ export const BUILT_IN_DRIVERS = [
33
32
  ClaudeDriver,
34
33
  CodexDriver,
35
34
  AntigravityDriver,
36
- BoatAgentDriver,
37
35
  MinimaxDriver,
38
36
  MistralDriver,
39
37
  CerebrasDriver,
@@ -1145,8 +1145,20 @@ export const ClaudeDriver = {
1145
1145
  // logical turn's stop handle in `active` while it sets up, so Stop is
1146
1146
  // never a silent no-op between two CLI processes of the same turn.
1147
1147
  const relaunch = logicalTurnId !== undefined;
1148
- if (active.has(threadId) && !relaunch)
1149
- throw new Error("a turn is already running on this thread");
1148
+ if (!relaunch) {
1149
+ // Stop is acknowledged at once and the process tree reaped after it
1150
+ // (taskkill is asynchronous on Windows); the stopped turn leaves
1151
+ // `active` only when its close is handled. A Stop that lands while
1152
+ // a turn is still starting lets the harness send the next message
1153
+ // before then. That turn waits for the stopped process to be gone,
1154
+ // never overlapping its helpers, instead of being refused.
1155
+ const stopped = sessions.get(threadId);
1156
+ if (stopped?.turn?.stopRequested && active.get(threadId)?.turnId === stopped.turn.turnId &&
1157
+ await killCliTree(stopped.child))
1158
+ await stopped.closed;
1159
+ if (active.has(threadId))
1160
+ throw new Error("a turn is already running on this thread");
1161
+ }
1150
1162
  // A bot-level mode is authoritative for this turn. In particular, an
1151
1163
  // old provider instance may still be configured with
1152
1164
  // `bypassPermissions`; Ask/Auto must restore Claude's interactive
@@ -1459,7 +1471,7 @@ export const ClaudeDriver = {
1459
1471
  if (!turn.sessionReset && live && !live.turn && !live.closing && live.child.exitCode === null && live.argsKey === argsKey && (!sessionId || sessionId === live.sessionId)) {
1460
1472
  if (live.idleTimer)
1461
1473
  clearTimeout(live.idleTimer);
1462
- live.turn = { turnId, input: turn, retryAbort, settled: false, sawStreamDelta: false, pendingSteers: new Set(), deferred: null, continuationGrace: null, continuationSilence: null };
1474
+ live.turn = { turnId, input: turn, retryAbort, settled: false, sawStreamDelta: false, awaitingInit: true, pendingSteers: new Set(), deferred: null, continuationGrace: null, continuationSilence: null };
1463
1475
  active.set(threadId, { stop: () => {
1464
1476
  if (live.turn)
1465
1477
  live.turn.stopRequested = true;
@@ -1475,20 +1487,13 @@ export const ClaudeDriver = {
1475
1487
  : claudeUserMessage(withVolatileNote(turn.text, volatile), turn.images);
1476
1488
  live.volatile = volatile;
1477
1489
  const running = live.turn;
1478
- const written = await writeUser(live, threadId, message);
1479
- if (!written && !running?.stopRequested) {
1480
- active.delete(threadId);
1481
- live.turn = null;
1490
+ // A warm process can end between turns just as the next one is
1491
+ // written: this driver learns of an exit only when the event loop
1492
+ // gets to it (later still on Windows), so the check above can find it
1493
+ // live and the write then fail. Nothing was submitted; its close
1494
+ // resumes this same turn on a fresh process (see `awaitingInit`).
1495
+ if (!(await writeUser(live, threadId, message)) && !running?.stopRequested)
1482
1496
  closeSession(threadId, "stdin write failed");
1483
- retryState.delete(threadId);
1484
- if (mcpConfigPath) {
1485
- try {
1486
- rmSync(dirname(mcpConfigPath), { recursive: true, force: true });
1487
- }
1488
- catch { }
1489
- }
1490
- throw new Error("claude session stdin is not writable");
1491
- }
1492
1497
  // the MCP config was for the first spawn; nothing to clean here
1493
1498
  if (mcpConfigPath) {
1494
1499
  try {
@@ -1630,7 +1635,9 @@ export const ClaudeDriver = {
1630
1635
  cleanupUnownedLaunch();
1631
1636
  throw error;
1632
1637
  }
1638
+ let markClosed;
1633
1639
  const session = {
1640
+ closed: new Promise((resolve) => { markClosed = resolve; }),
1634
1641
  child,
1635
1642
  broker,
1636
1643
  mcpConfigPath,
@@ -1749,6 +1756,8 @@ export const ClaudeDriver = {
1749
1756
  break;
1750
1757
  }
1751
1758
  session.sawInit = true;
1759
+ if (session.turn)
1760
+ session.turn.awaitingInit = false;
1752
1761
  session.nativePermissionMode = typeof o.permissionMode === "string" ? o.permissionMode : null;
1753
1762
  if (typeof o.session_id === "string")
1754
1763
  session.sessionId = o.session_id;
@@ -2051,11 +2060,18 @@ export const ClaudeDriver = {
2051
2060
  const retry = retryState.get(threadId) ?? { attempt: 0, cancelled: false };
2052
2061
  const message = `claude exited ${code} before result${session.stderr ? `: ${session.stderr.trim().slice(-300)}` : ""}`;
2053
2062
  const verdict = classifyError({ exitCode: code, stderr: message });
2063
+ // A process kept warm from an earlier turn ended before it
2064
+ // announced this one: it never took the prompt. That is a session
2065
+ // ending between turns, not a failed turn, so the same turn
2066
+ // resumes the session on a fresh process at once — no retry row,
2067
+ // no retry budget spent. (A fresh process is never retained, so
2068
+ // this happens at most once per turn.)
2069
+ const endedBeforeTurn = session.turn.awaitingInit === true;
2054
2070
  if (!retry.cancelled &&
2055
- code !== 0 &&
2056
- verdict.transient &&
2057
- !session.turn.sawStreamDelta &&
2058
- retry.attempt < RETRY_MAX_ATTEMPTS - 1) {
2071
+ (endedBeforeTurn || (code !== 0 &&
2072
+ verdict.transient &&
2073
+ !session.turn.sawStreamDelta &&
2074
+ retry.attempt < RETRY_MAX_ATTEMPTS - 1))) {
2059
2075
  // the CLI is gone but the TURN continues: keep the thread busy,
2060
2076
  // emit no terminal event, and relaunch after the backoff. The
2061
2077
  // `active` entry STAYS — it is what makes an interrupt during
@@ -2077,15 +2093,18 @@ export const ClaudeDriver = {
2077
2093
  }
2078
2094
  sessions.delete(threadId);
2079
2095
  session.turn = null;
2080
- retry.attempt++;
2081
- const delayMs = computeBackoff(retry.attempt - 1);
2082
- emit({
2083
- ...base(threadId, turnId),
2084
- type: "turn.retrying",
2085
- attempt: retry.attempt,
2086
- delayMs,
2087
- reason: verdict.reason,
2088
- });
2096
+ let delayMs = 0;
2097
+ if (!endedBeforeTurn) {
2098
+ retry.attempt++;
2099
+ delayMs = computeBackoff(retry.attempt - 1);
2100
+ emit({
2101
+ ...base(threadId, turnId),
2102
+ type: "turn.retrying",
2103
+ attempt: retry.attempt,
2104
+ delayMs,
2105
+ reason: verdict.reason,
2106
+ });
2107
+ }
2089
2108
  void (async () => {
2090
2109
  const wait = interruptibleDelay(delayMs * retryScale, retryAbort.signal);
2091
2110
  await wait.promise;
@@ -2225,7 +2244,7 @@ export const ClaudeDriver = {
2225
2244
  };
2226
2245
  child.on("close", (code) => {
2227
2246
  session.finishClose = () => finalizeClose(code);
2228
- void session.finishClose();
2247
+ void session.finishClose().finally(markClosed);
2229
2248
  });
2230
2249
  const stop = () => {
2231
2250
  if (session.turn)
@@ -1711,8 +1711,8 @@ export const CodexDriver = {
1711
1711
  // back. The app-server offers no way to clear a level either:
1712
1712
  // "" is rejected outright and thread/start takes no effort at
1713
1713
  // all. So a thread keeps the last level it was sent until it is
1714
- // sent another, and choosing Default lands on the bot's next new
1715
- // thread rather than the current one.
1714
+ // sent another; back on Default, the harness gives a thread that
1715
+ // holds a level a new thread instead of resuming it (server/index.ts).
1716
1716
  ...(turn.effort ? { effort: turn.effort } : {}),
1717
1717
  });
1718
1718
  };