openmausbot 0.1.89 → 0.1.91

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 (168) hide show
  1. package/dist/assets/{abnfDiagram-O67JEVCF-BwaMaZEm.js → abnfDiagram-O67JEVCF-yP-w8pRq.js} +1 -1
  2. package/dist/assets/{arc-Be-8GKxd.js → arc-DsivfZEh.js} +1 -1
  3. package/dist/assets/{architectureDiagram-NJMV4G6O-Dp_U2Kv1.js → architectureDiagram-NJMV4G6O-CILw7lzT.js} +1 -1
  4. package/dist/assets/{blockDiagram-BEXU5L5S-DQjUjYUk.js → blockDiagram-BEXU5L5S-DEQUV3HF.js} +1 -1
  5. package/dist/assets/{c4Diagram-YGBWAQC7-Bd3aE4Kc.js → c4Diagram-YGBWAQC7-lSXCZoKA.js} +1 -1
  6. package/dist/assets/channel-DrfGMu7X.js +1 -0
  7. package/dist/assets/{chunk-2Q5K7J3B-BijjTBeJ.js → chunk-2Q5K7J3B-QCD5bgWP.js} +1 -1
  8. package/dist/assets/{chunk-3FUC2YCW-CMGdwvzW.js → chunk-3FUC2YCW-C5-iZJuL.js} +2 -2
  9. package/dist/assets/{chunk-5VM5RSS4-0G09BfsZ.js → chunk-5VM5RSS4-cxsawMpY.js} +1 -1
  10. package/dist/assets/{chunk-GTNCS2PH-FOp3vSHe.js → chunk-GTNCS2PH-DV8tV8H7.js} +1 -1
  11. package/dist/assets/{chunk-GWA4HPMP-Dv-20VIn.js → chunk-GWA4HPMP-CJFZyfbW.js} +1 -1
  12. package/dist/assets/{chunk-JWPE2WC7-CRgrdFcz.js → chunk-JWPE2WC7-BigbMXco.js} +1 -1
  13. package/dist/assets/{chunk-NETBCI7D-3hP4f7QB.js → chunk-NETBCI7D-oVxTfpDy.js} +1 -1
  14. package/dist/assets/{chunk-WEXAMYUT-CG98IjlG.js → chunk-WEXAMYUT-CH4BiroW.js} +1 -1
  15. package/dist/assets/{chunk-XXDRQBXY-BrSTC9xG.js → chunk-XXDRQBXY-KEDa6gax.js} +1 -1
  16. package/dist/assets/{classDiagram-v2-NBCMYWYE-rpQQQDFX.js → classDiagram-v2-NBCMYWYE-BiPjfqVv.js} +1 -1
  17. package/dist/assets/{cose-bilkent-JH36ORCC-G6RSafHA.js → cose-bilkent-JH36ORCC-Caxew3OY.js} +1 -1
  18. package/dist/assets/{cynefin-OW5HDTMX-CXjv-IM2.js → cynefin-OW5HDTMX-B_Pui369.js} +1 -1
  19. package/dist/assets/{cynefinDiagram-VND7K2PF-Dw07MR9f.js → cynefinDiagram-VND7K2PF-X64ydIhP.js} +1 -1
  20. package/dist/assets/{dagre-6A5THRUB-BwPnlTg2.js → dagre-6A5THRUB-TqYCI1E1.js} +1 -1
  21. package/dist/assets/{diagram-22UHCM2B-Cm6xcMiz.js → diagram-22UHCM2B-TeOt3rAQ.js} +1 -1
  22. package/dist/assets/{diagram-3UASUU5V-CDpogPbE.js → diagram-3UASUU5V-D70BLhDA.js} +1 -1
  23. package/dist/assets/{diagram-ATOU4E4O-BV8z3Pnc.js → diagram-ATOU4E4O-BbQDLwUC.js} +1 -1
  24. package/dist/assets/{diagram-CDSNMT55-CKCQhOOC.js → diagram-CDSNMT55-ChNJZ1u-.js} +1 -1
  25. package/dist/assets/{diagram-MLGK6HIB-FGvJg69C.js → diagram-MLGK6HIB-DMBl2APa.js} +1 -1
  26. package/dist/assets/{diagram-MPIPVDR6-CwATvgnS.js → diagram-MPIPVDR6-CEqDchOp.js} +1 -1
  27. package/dist/assets/{ebnfDiagram-ZINNZB2B-CH_c3SSC.js → ebnfDiagram-ZINNZB2B-BVDKRp_-.js} +1 -1
  28. package/dist/assets/{elk-276RUBZZ-3guZi71R.js → elk-276RUBZZ-CRkofO9B.js} +1 -1
  29. package/dist/assets/{erDiagram-OPXOYQCR-BMHphR2n.js → erDiagram-OPXOYQCR-DruOMh5e.js} +1 -1
  30. package/dist/assets/{flowDiagram-KWPJA3E3-Vm5DkW5x.js → flowDiagram-KWPJA3E3-8GMQeB7t.js} +1 -1
  31. package/dist/assets/{ganttDiagram-FUAMR5RP-DO-1ct07.js → ganttDiagram-FUAMR5RP-ORE9jcsM.js} +1 -1
  32. package/dist/assets/{gitGraphDiagram-X574FWY7-nxeksEIN.js → gitGraphDiagram-X574FWY7-BmsedsWh.js} +1 -1
  33. package/dist/assets/index-BlnCEcU6.js +588 -0
  34. package/dist/assets/{index-Oxk9x45i.js → index-DKNEM8Q6.js} +1 -1
  35. package/dist/assets/index-DVxjOvID.css +1 -0
  36. package/dist/assets/{infoDiagram-VRGFBTTK-Dkcvr0p7.js → infoDiagram-VRGFBTTK-E_p7AooJ.js} +1 -1
  37. package/dist/assets/{ishikawaDiagram-OU5B5YK6-BwVibkoO.js → ishikawaDiagram-OU5B5YK6-Y-B2mKda.js} +1 -1
  38. package/dist/assets/{journeyDiagram-ZHPQQLJL-ByiBZqgG.js → journeyDiagram-ZHPQQLJL-eA8RELcK.js} +1 -1
  39. package/dist/assets/{kanban-definition-PNTS6WVX-qqjgXBF3.js → kanban-definition-PNTS6WVX-DqIL2e8e.js} +1 -1
  40. package/dist/assets/{linear-CtJnKe6A.js → linear-yC_29R6e.js} +1 -1
  41. package/dist/assets/{mermaid.core-BEiDklrY.js → mermaid.core-VhEMkSND.js} +5 -5
  42. package/dist/assets/{mindmap-definition-NLK3R4M7-DfIYHia9.js → mindmap-definition-NLK3R4M7-oJ7kyQZy.js} +1 -1
  43. package/dist/assets/{pegDiagram-GJSIUBJH-CYyRqa1a.js → pegDiagram-GJSIUBJH-BmfYcY0O.js} +1 -1
  44. package/dist/assets/{pieDiagram-5QR66LMP-hOToy_zH.js → pieDiagram-5QR66LMP-DKvgP7w9.js} +1 -1
  45. package/dist/assets/{quadrantDiagram-O4NWA36T-DKAsNN9I.js → quadrantDiagram-O4NWA36T-BEKL3W0L.js} +1 -1
  46. package/dist/assets/{railroadDiagram-XR7U4H2S-BSA-d8JU.js → railroadDiagram-XR7U4H2S-BEsiqR5s.js} +1 -1
  47. package/dist/assets/{requirementDiagram-PLB6GJNP-CSmlvb-p.js → requirementDiagram-PLB6GJNP-DWBcWvAP.js} +1 -1
  48. package/dist/assets/{sankeyDiagram-IPEJSGJF-CD5F6fnG.js → sankeyDiagram-IPEJSGJF-rpZ2_O4s.js} +1 -1
  49. package/dist/assets/{sequenceDiagram-PO4LG4MO-JUq5sx6d.js → sequenceDiagram-PO4LG4MO-B99HQdJR.js} +1 -1
  50. package/dist/assets/{sizeCapture-INFHLROL-BjIS-Stk.js → sizeCapture-INFHLROL-BjN0qT5T.js} +1 -1
  51. package/dist/assets/{stateDiagram-v2-GCMORJYK-DklkUxOg.js → stateDiagram-v2-GCMORJYK-BbsVgGsC.js} +1 -1
  52. package/dist/assets/{swimlanes-2SLR337P-CsjIU6be.js → swimlanes-2SLR337P-DG3gKkeR.js} +1 -1
  53. package/dist/assets/swimlanesDiagram-TC7HE7FX-BCZhxl7e.js +8 -0
  54. package/dist/assets/{timeline-definition-EJHVYXUP-BkFqK7AB.js → timeline-definition-EJHVYXUP-BTHOdtF_.js} +1 -1
  55. package/dist/assets/{usecaseDiagram-POWQR4AR-70gJB1EU.js → usecaseDiagram-POWQR4AR-acLSLFwc.js} +1 -1
  56. package/dist/assets/{vennDiagram-UO4OBE2U-83Tm62dN.js → vennDiagram-UO4OBE2U-C5JzNX9G.js} +1 -1
  57. package/dist/assets/{wardleyDiagram-VNRHLVJA-tTmseoX9.js → wardleyDiagram-VNRHLVJA-DL1-PrR_.js} +1 -1
  58. package/dist/assets/{xychartDiagram-PMCCYNJV-R1LBA55a.js → xychartDiagram-PMCCYNJV-Bvr1477p.js} +1 -1
  59. package/dist/index.html +2 -2
  60. package/dist-server/cloud-home-start.js +14810 -0
  61. package/dist-server/container-mcp.js +136 -36
  62. package/dist-server/drivers/agents-proxy.js +31 -10
  63. package/dist-server/index.js +48251 -44124
  64. package/dist-server/openmausbot.js +628 -292
  65. package/dist-server/pair-cli.js +628 -292
  66. package/dist-server/permission-proxy.js +1 -1
  67. package/dist-server/server/admission.js +46 -0
  68. package/dist-server/server/{box-create-idempotency.js → boat-create-idempotency.js} +32 -31
  69. package/dist-server/server/{box-delete-journal.js → boat-delete-journal.js} +26 -25
  70. package/dist-server/server/{box.js → boat.js} +381 -334
  71. package/dist-server/server/bot-overview.js +7 -4
  72. package/dist-server/server/bot-profile.js +7 -1
  73. package/dist-server/server/channel-queue.js +58 -18
  74. package/dist-server/server/checkpoints.js +98 -52
  75. package/dist-server/server/claim-idle.js +33 -0
  76. package/dist-server/server/cloud-backend.js +5 -5
  77. package/dist-server/server/cloud-home-start.js +121 -0
  78. package/dist-server/server/cloud-home.js +241 -0
  79. package/dist-server/server/cloud-lending.js +103 -0
  80. package/dist-server/server/cloud-move-http.js +268 -0
  81. package/dist-server/server/cloud-move.js +418 -0
  82. package/dist-server/server/cloud-overflow.js +167 -0
  83. package/dist-server/server/composio.js +5 -0
  84. package/dist-server/server/computer-observation.js +2 -2
  85. package/dist-server/server/config.js +156 -9
  86. package/dist-server/server/container-computer.js +13 -0
  87. package/dist-server/server/control-client.js +2 -2
  88. package/dist-server/server/decider/index.js +238 -0
  89. package/dist-server/server/decider/jev.js +183 -0
  90. package/dist-server/server/decider/log.js +62 -0
  91. package/dist-server/server/decider/relay.js +52 -0
  92. package/dist-server/server/decider/room-routing.js +88 -0
  93. package/dist-server/server/decider/types.js +5 -0
  94. package/dist-server/server/delegations.js +11 -3
  95. package/dist-server/server/drivers/acp/core.js +5 -4
  96. package/dist-server/server/drivers/agents-call.js +5 -1
  97. package/dist-server/server/drivers/agents-catalog.js +25 -3
  98. package/dist-server/server/drivers/{boxagent.js → boatagent.js} +30 -21
  99. package/dist-server/server/drivers/builtIn.js +2 -2
  100. package/dist-server/server/drivers/{chat-box-tools.js → chat-boat-tools.js} +3 -3
  101. package/dist-server/server/drivers/chat-mcp-tools.js +2 -2
  102. package/dist-server/server/drivers/claude.js +178 -13
  103. package/dist-server/server/drivers/native.js +1 -1
  104. package/dist-server/server/drivers/openai-chat.js +15 -4
  105. package/dist-server/server/drivers/pi.js +3 -3
  106. package/dist-server/server/group-goal-run.js +6 -4
  107. package/dist-server/server/harness-capabilities.js +2 -0
  108. package/dist-server/server/hosted-models.js +6 -1
  109. package/dist-server/server/included-services.js +70 -0
  110. package/dist-server/server/index.js +1462 -576
  111. package/dist-server/server/local-vm-inventory.js +11 -1
  112. package/dist-server/server/local-vm-seat-pool.js +141 -0
  113. package/dist-server/server/managed-policy.js +4 -1
  114. package/dist-server/server/memory-capture.js +194 -0
  115. package/dist-server/server/memory-entries.js +76 -0
  116. package/dist-server/server/memory-organize.js +102 -0
  117. package/dist-server/server/memory-tidy.js +144 -0
  118. package/dist-server/server/memory-topics.js +169 -0
  119. package/dist-server/server/memory-upkeep.js +452 -0
  120. package/dist-server/server/message-db.js +49 -5
  121. package/dist-server/server/model-context-window.js +4 -4
  122. package/dist-server/server/package-export.js +17 -10
  123. package/dist-server/server/package-import.js +6 -3
  124. package/dist-server/server/package-parts.js +3 -1
  125. package/dist-server/server/permission-proxy.js +3 -3
  126. package/dist-server/server/profile-learned.js +98 -0
  127. package/dist-server/server/profile-versions.js +45 -11
  128. package/dist-server/server/recall.js +172 -0
  129. package/dist-server/server/redact.js +1 -1
  130. package/dist-server/server/request-auth.js +2 -0
  131. package/dist-server/server/room-cwd.js +1 -1
  132. package/dist-server/server/room-handoffs.js +61 -6
  133. package/dist-server/server/routes/bot-memory.js +215 -0
  134. package/dist-server/server/routes/decider.js +19 -0
  135. package/dist-server/server/routine-requests.js +27 -16
  136. package/dist-server/server/routines.js +10 -6
  137. package/dist-server/server/shared-computers.js +116 -30
  138. package/dist-server/server/steer-queue.js +41 -10
  139. package/dist-server/server/store.js +42 -3
  140. package/dist-server/server/surface.js +14 -1
  141. package/dist-server/server/system-prompt.js +16 -5
  142. package/dist-server/server/team-backup.js +6 -2
  143. package/dist-server/server/team-computers.js +2 -2
  144. package/dist-server/server/team-manifest.js +11 -3
  145. package/dist-server/server/tightening-requests.js +34 -4
  146. package/dist-server/server/tool-summary.js +2 -2
  147. package/dist-server/server/tts/elevenlabs.js +12 -7
  148. package/dist-server/server/tts/index.js +17 -10
  149. package/dist-server/server/turn-resources.js +87 -11
  150. package/dist-server/server/vps-computer.js +1 -1
  151. package/dist-server/server/workspace-backup-policy.js +22 -3
  152. package/dist-server/server/workspace-backup.js +25 -8
  153. package/dist-server/server/workspace.js +57 -22
  154. package/dist-server/shared/ask-question.js +7 -5
  155. package/dist-server/shared/bot-avatar.js +19 -0
  156. package/dist-server/shared/cloud-home.js +4 -0
  157. package/dist-server/shared/credential-request.js +3 -3
  158. package/dist-server/shared/package-format.js +30 -4
  159. package/dist-server/shared/team-backup.js +20 -3
  160. package/dist-server/shared/tool-surface.js +1 -1
  161. package/dist-server/src/lib/composer-attachments.js +9 -0
  162. package/dist-server/vps-container-mcp.js +136 -36
  163. package/dist-server/workspace-backup.worker.js +165 -35
  164. package/package.json +1 -1
  165. package/dist/assets/channel-DqY_gMLg.js +0 -1
  166. package/dist/assets/index-BIMdIbaw.css +0 -1
  167. package/dist/assets/index-BVPdbq2r.js +0 -583
  168. package/dist/assets/swimlanesDiagram-TC7HE7FX-Bk14M0_B.js +0 -8
@@ -1,9 +1,9 @@
1
1
  import { newEventId, newId } from "../contracts.js";
2
+ import { boatCredential, boatProviderApi } from "../included-services.js";
2
3
  import { appendNative } from "./native.js";
3
4
  import { OMB_ASK_TOOL, answerWithoutPreamble, askQuestionSummary, capAnswerEcho, ombAskProtocolPrompt, parseOmbAskQuestions, questionChoices, stripOmbAskBlock, } from "../../shared/ask-question.js";
5
+ // "boxAgent" is Boat's historical driver kind (catalog and wire); keep it.
4
6
  const DRIVER_KIND = "boxAgent";
5
- // overridable so tests and a dev backend can be pointed at instead of the live provider
6
- const BOX_API = process.env.OMB_BOX_API || "https://ascii.dev/api/box/v1";
7
7
  const MODELS = {
8
8
  default: "claude-fable-5",
9
9
  options: [
@@ -15,14 +15,19 @@ const MODELS = {
15
15
  /** Any fence whose info string names the ask protocol, even when its body
16
16
  * does not parse: the marker for "the model tried to ask and failed". */
17
17
  const ASK_FENCE_ANY = /(^|\n)[ \t]{0,3}(`{3,}|~{3,})[ \t]*omb-ask\b/;
18
- /** The box runs every harness boat.dev ships (claude-code, codex, pi, opencode,
18
+ /** The boat runs every harness boat.dev ships (claude-code, codex, pi, opencode,
19
19
  * prime-agent, kimi). Which one a model id belongs to comes from the public
20
20
  * catalog, `GET /api/provider-models` at the API root: an object keyed by
21
21
  * harness, each with its `models`. A bot that arrives here from another engine
22
- * carries that engine's model id, so this is what lets it keep its model. */
22
+ * carries that engine's model id, so this is what lets it keep its model. The
23
+ * root is the API in use (Boat, or Cloud Pro's relay), so it is cached per root. */
23
24
  let catalog = null;
24
- async function loadCatalog() {
25
- const root = BOX_API.replace(/\/api\/box\/v1\/?$/, "");
25
+ let catalogRoot = "";
26
+ async function loadCatalog(api) {
27
+ const root = api.replace(/\/api\/box\/v1\/?$/, "");
28
+ if (catalog && catalogRoot === root)
29
+ return;
30
+ catalogRoot = root;
26
31
  catalog = await fetch(`${root}/api/provider-models`, { signal: AbortSignal.timeout(15_000) })
27
32
  .then((res) => (res.ok ? res.json() : null))
28
33
  .catch(() => null);
@@ -44,7 +49,7 @@ function decodeConfig(raw) {
44
49
  ...(typeof o.askTimeoutMs === "number" ? { askTimeoutMs: o.askTimeoutMs } : {}),
45
50
  };
46
51
  }
47
- export const BoxAgentDriver = {
52
+ export const BoatAgentDriver = {
48
53
  driverKind: DRIVER_KIND,
49
54
  metadata: { displayName: "Computer", supportsMultipleInstances: false },
50
55
  models: MODELS,
@@ -52,7 +57,11 @@ export const BoxAgentDriver = {
52
57
  defaultConfig: () => decodeConfig({}),
53
58
  async create(input) {
54
59
  const { instanceId, config } = input;
55
- const token = input.environment.BOX_TOKEN ?? process.env.BOX_TOKEN ?? "";
60
+ // The person's own token (instanceConfigs injects it), else Cloud Pro's
61
+ // included one, with the one base URL each may be sent to. Resolved per
62
+ // request, like every other Boat call.
63
+ const ownToken = input.environment.BOX_TOKEN ?? process.env.BOX_TOKEN;
64
+ const account = () => boatCredential(ownToken);
56
65
  const listeners = new Set();
57
66
  const active = new Map();
58
67
  /** One open ask per thread: the turn-held transport's pending card. The
@@ -74,9 +83,10 @@ export const BoxAgentDriver = {
74
83
  createdAt: new Date().toISOString(),
75
84
  });
76
85
  const api = async (path, opts = {}) => {
77
- const res = await fetch(`${BOX_API}${path}`, {
86
+ const credential = account();
87
+ const res = await fetch(`${credential?.api ?? boatProviderApi()}${path}`, {
78
88
  ...opts,
79
- headers: { authorization: `Bearer ${token}`, "content-type": "application/json", ...opts.headers },
89
+ headers: { authorization: `Bearer ${credential?.token ?? ""}`, "content-type": "application/json", ...opts.headers },
80
90
  signal: opts.signal ?? AbortSignal.timeout(30_000),
81
91
  });
82
92
  const body = await res.json().catch(() => null);
@@ -89,7 +99,7 @@ export const BoxAgentDriver = {
89
99
  const { threadId } = turn;
90
100
  const computer = turn.integrations?.computer;
91
101
  const boxId = computer && (!computer.kind || computer.kind === "box") ? computer.boxId : undefined;
92
- if (!token)
102
+ if (!account())
93
103
  throw new Error('box not configured — add {"box":{"token":"…"}} to ~/.openmausbot/config.json');
94
104
  if (!boxId) {
95
105
  throw new Error("this bot has no computer yet — open the Computer panel and provision one");
@@ -112,15 +122,14 @@ export const BoxAgentDriver = {
112
122
  .filter((s) => s !== undefined)
113
123
  .join("\n");
114
124
  const postPrompt = async (promptText) => {
115
- if (!catalog)
116
- await loadCatalog();
125
+ await loadCatalog(account()?.api ?? boatProviderApi());
117
126
  const started = await api(`/boxes/${boxId}/prompt`, {
118
127
  method: "POST",
119
128
  body: JSON.stringify({ ...providerFor(model), prompt: promptText }),
120
129
  });
121
130
  appendNative(threadId, { dir: "out", source: "box.prompt", msg: { model, prompt: promptText, response: started } });
122
131
  // real shape (2026-08): {type:"prompt.queued", promptId, promptRun:{id,…},
123
- // id:<box id>} — never fall back to the bare id, it's the box's
132
+ // id:<boat id>} — never fall back to the bare id, it's the boat's
124
133
  return started?.promptRun?.id ?? started?.prompt?.id ?? started?.promptId ?? null;
125
134
  };
126
135
  const promptId = await postPrompt(prompt);
@@ -149,7 +158,7 @@ export const BoxAgentDriver = {
149
158
  const seen = new Set();
150
159
  let lastText = "";
151
160
  let pendingText = "";
152
- /** Why the box could not answer (login expired, model refused, …). */
161
+ /** Why the boat could not answer (login expired, model refused, …). */
153
162
  let problem = null;
154
163
  /** Emit unflushed deltas as assistant_text and reset pendingText.
155
164
  * The omb-ask block is protocol, not prose: it streamed raw (the
@@ -229,7 +238,7 @@ export const BoxAgentDriver = {
229
238
  return { ok: ok && !cancelled, stopReason: cancelled ? "interrupted" : stopReason };
230
239
  }
231
240
  // The turn is still open, so this prompt continues it: same turnId,
232
- // same accounting, and the answer reaches the box the way the ask
241
+ // same accounting, and the answer reaches the boat the way the ask
233
242
  // contract promised — Q:/A: blocks, capped for echo.
234
243
  const continuation = [
235
244
  capAnswerEcho(answerWithoutPreamble(reply)),
@@ -238,7 +247,7 @@ export const BoxAgentDriver = {
238
247
  ].join("\n");
239
248
  const nextPromptId = await postPrompt(continuation);
240
249
  // Stop can land while the continuation POST is in flight: the
241
- // interrupt inside cancel() then hits a box with no active run,
250
+ // interrupt inside cancel() then hits a boat with no active run,
242
251
  // and the continuation would start after it. Interrupt the run
243
252
  // that just started before ending the turn.
244
253
  if (cancelled) {
@@ -247,9 +256,9 @@ export const BoxAgentDriver = {
247
256
  }
248
257
  return await settleRun(nextPromptId);
249
258
  };
250
- /** Poll one box run to its settle. */
259
+ /** Poll one boat run to its settle. */
251
260
  const settleRun = async (runPromptId) => {
252
- const startedAt = Date.now(); // one 30-min ceiling per box run, ask chains included
261
+ const startedAt = Date.now(); // one 30-min ceiling per boat run, ask chains included
253
262
  lastText = "";
254
263
  pendingText = "";
255
264
  problem = null;
@@ -354,8 +363,8 @@ export const BoxAgentDriver = {
354
363
  return { turnId };
355
364
  };
356
365
  const snapshot = async () => {
357
- if (!token) {
358
- return { state: "unavailable", reason: 'no Box token — add {"box":{"token":"…"}} to ~/.openmausbot/config.json' };
366
+ if (!account()) {
367
+ return { state: "unavailable", reason: 'no Boat token — add {"box":{"token":"…"}} to ~/.openmausbot/config.json' };
359
368
  }
360
369
  try {
361
370
  await api("/me");
@@ -1,5 +1,5 @@
1
1
  import { AntigravityDriver } from "./antigravity.js";
2
- import { BoxAgentDriver } from "./boxagent.js";
2
+ import { BoatAgentDriver } from "./boatagent.js";
3
3
  import { ClaudeDriver } from "./claude.js";
4
4
  import { CodexDriver } from "./codex.js";
5
5
  import { GrokDriver } from "./grok.js";
@@ -32,7 +32,7 @@ export const BUILT_IN_DRIVERS = [
32
32
  ClaudeDriver,
33
33
  CodexDriver,
34
34
  AntigravityDriver,
35
- BoxAgentDriver,
35
+ BoatAgentDriver,
36
36
  MinimaxDriver,
37
37
  MistralDriver,
38
38
  ];
@@ -1,4 +1,4 @@
1
- import { isolatedRemoteCommand, runCommand, screenshotBox } from "../box.js";
1
+ import { isolatedRemoteCommand, runCommand, screenshotBoat } from "../boat.js";
2
2
  import { createControlClient, CONTROL_REFUSAL_PLAIN } from "../control-client.js";
3
3
  const coordinate = { type: "integer", minimum: 0, maximum: 32767 };
4
4
  const tool = (name, description, properties = {}, required = []) => ({ name, description, inputSchema: { type: "object", properties, required, additionalProperties: false } });
@@ -19,7 +19,7 @@ const buttons = { left: 1, middle: 2, right: 3 };
19
19
  const scrollButtons = { up: 4, down: 5, left: 6, right: 7 };
20
20
  /** Uses only the leased descriptor supplied by the harness. Schema validation
21
21
  * and user approval happen in the shared chat tool executor before this call. */
22
- export class ChatBoxClient {
22
+ export class ChatBoatClient {
23
23
  closed = false;
24
24
  control;
25
25
  descriptor;
@@ -45,7 +45,7 @@ export class ChatBoxClient {
45
45
  const config = { box: { token: this.descriptor.token } };
46
46
  const id = this.descriptor.boxId;
47
47
  if (name === "screenshot") {
48
- const shot = await screenshotBox(config, "", id, { signal, nativeSize: true });
48
+ const shot = await screenshotBoat(config, "", id, { signal, nativeSize: true });
49
49
  signal.throwIfAborted();
50
50
  return { content: [{ type: "image", mimeType: "image/jpeg", data: shot.png }] };
51
51
  }
@@ -7,7 +7,7 @@ import { stripControlPlaneEnv } from "../config.js";
7
7
  import { augmentedPath } from "../env-path.js";
8
8
  import { killCliTree, spawnCli } from "../procs.js";
9
9
  import { chatImage } from "./chat-images.js";
10
- import { ChatBoxClient } from "./chat-box-tools.js";
10
+ import { ChatBoatClient } from "./chat-boat-tools.js";
11
11
  /** The transport cannot safely continue this turn. A dispatched operation may
12
12
  * already have taken effect, so callers must not retry it through a new round. */
13
13
  export class ChatToolSessionError extends Error {
@@ -314,7 +314,7 @@ export async function mountChatTools(integrations, signal, computerUse = false)
314
314
  throw aborted();
315
315
  // Every mounted MCP server can return images when the caller enables
316
316
  // image delivery, including custom servers. Text stays bounded below.
317
- const client = "boxId" in descriptor ? new ChatBoxClient(descriptor) : new ChatMcpClient(descriptor, computerUse);
317
+ const client = "boxId" in descriptor ? new ChatBoatClient(descriptor) : new ChatMcpClient(descriptor, computerUse);
318
318
  clients.push(client);
319
319
  return { name, client, tools: await client.tools(signal) };
320
320
  }));
@@ -7,7 +7,7 @@
7
7
  // Integrations become MCP servers on the CLI:
8
8
  // - Composio Sessions (connected apps → tools) over streamable HTTP
9
9
  // - the bot's cloud computer (boat.dev) via server/computer-proxy.ts
10
- // — screenshot/exec/open_url, the CUA-on-the-box bridge
10
+ // — screenshot/exec/open_url, the CUA-on-the-boat bridge
11
11
  import { createHash, randomBytes } from "node:crypto";
12
12
  import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
13
13
  import { createServer as createNetServer } from "node:net";
@@ -328,6 +328,7 @@ export const STATIC_CLAUDE_MODELS = {
328
328
  { id: "claude-fable-5", label: "Claude Fable 5" },
329
329
  { id: "claude-opus-5-5", label: "Claude Opus 5.5", contextWindow: 1_000_000 },
330
330
  { id: "claude-opus-5", label: "Claude Opus 5" },
331
+ { id: "claude-sonnet-5-5", label: "Claude Sonnet 5.5", contextWindow: 1_000_000 },
331
332
  { id: "claude-sonnet-5", label: "Claude Sonnet 5" },
332
333
  { id: "claude-haiku-4-5", label: "Claude Haiku 4.5" },
333
334
  ],
@@ -435,7 +436,7 @@ function askQuestions(ask) {
435
436
  function askSummary(ask) {
436
437
  const questions = askQuestions(ask);
437
438
  if (questions)
438
- return askQuestionSummary(questions).slice(0, 300);
439
+ return askQuestionSummary(questions);
439
440
  return askInputSummary(ask.input) ?? ask.tool ?? "tool";
440
441
  }
441
442
  /** Where the hook helper reads this thread's current turn token. Stable per
@@ -752,6 +753,109 @@ function firstText(content) {
752
753
  }
753
754
  return "";
754
755
  }
756
+ /** A turn's own cost from the CLI's total_cost_usd, which is not a per-turn
757
+ * figure: it is "cumulative across turns in streaming-input sessions — each
758
+ * result carries the running total so far" (2.1.282), and a retained process
759
+ * runs turn after turn. So a turn costs the growth since the total its
760
+ * process reported for the turn before — or, for a process's first turn,
761
+ * since the total the CLI restored on --resume (see restoredCostBase). With
762
+ * no known start (null) the turn keeps its whole figure. A total that went
763
+ * down is not the same count, so it is taken whole too rather than booked as
764
+ * a negative cost. Rounding to 1e-10 USD removes only the float noise of the
765
+ * subtraction. */
766
+ export function turnCostFromRunningTotal(total, previous) {
767
+ if (total === null)
768
+ return null;
769
+ if (previous === null || total < previous)
770
+ return total;
771
+ return Number((total - previous).toFixed(10));
772
+ }
773
+ export function claudeCostSnapshot(total, modelUsage) {
774
+ if (typeof total !== "number" || !Number.isFinite(total))
775
+ return null;
776
+ const count = (value) => (typeof value === "number" && Number.isFinite(value) ? value : 0);
777
+ const models = {};
778
+ if (modelUsage && typeof modelUsage === "object" && !Array.isArray(modelUsage)) {
779
+ for (const [model, raw] of Object.entries(modelUsage)) {
780
+ if (!raw || typeof raw !== "object")
781
+ continue;
782
+ const u = raw;
783
+ models[model] = [count(u.inputTokens), count(u.cacheReadInputTokens), count(u.cacheCreationInputTokens), count(u.outputTokens)];
784
+ }
785
+ }
786
+ return { total, models };
787
+ }
788
+ /** The running total a resumed session already carried before this
789
+ * process's first turn. On --resume the CLI (2.1.282) restores the session's
790
+ * cost from an earlier state — not always the latest one this driver saw —
791
+ * so that turn's total_cost_usd and modelUsage include the earlier turns.
792
+ * The restored state is the earlier state that sits inside the new counts
793
+ * and leaves exactly this turn's own usage: in one model (usage leaves out
794
+ * side calls such as a Haiku title) or summed over all models (a turn split
795
+ * between two); nothing restored is 0. When no state fits exactly — the CLI
796
+ * saved work that never reported a result, like an interrupted turn — the
797
+ * latest state inside the new counts stands, so that work is booked once,
798
+ * with this turn. Either way the latest state wins, not the highest total:
799
+ * a resume that went back to an older state leaves later, lower totals. */
800
+ export function restoredCostBase(earlier, current, usage) {
801
+ const turn = [usage.input, usage.cacheRead, usage.cacheWrite, usage.output];
802
+ const nothing = { total: 0, models: {} };
803
+ let exact = null;
804
+ let inside = 0;
805
+ // oldest first: the session's states in the order they were recorded
806
+ for (const state of [nothing, ...earlier]) {
807
+ const within = Object.entries(state.models).every(([model, counts]) => counts.every((n, i) => n <= (current.models[model]?.[i] ?? 0)));
808
+ if (!within)
809
+ continue;
810
+ inside = state.total;
811
+ const growth = Object.entries(current.models).map(([model, counts]) => counts.map((n, i) => n - (state.models[model]?.[i] ?? 0)));
812
+ const isTurn = (counts) => counts.every((n, i) => n === turn[i]);
813
+ const summed = turn.map((_, i) => growth.reduce((sum, counts) => sum + counts[i], 0));
814
+ if (growth.some(isTurn) || isTurn(summed))
815
+ exact = state.total;
816
+ }
817
+ return exact ?? inside;
818
+ }
819
+ /** Each Claude session's latest cost states, so the first turn after a
820
+ * --resume can tell what the CLI restored — after an app restart too. Small
821
+ * by design: a few states for the most recent sessions. */
822
+ const COST_HISTORY_FILE = join(DATA_DIR, "claude-cost-history.json");
823
+ const COST_HISTORY_SESSIONS = 100;
824
+ const COST_HISTORY_STATES = 8;
825
+ function isCostSnapshot(value) {
826
+ if (!value || typeof value !== "object")
827
+ return false;
828
+ const { total, models } = value;
829
+ return typeof total === "number" && !!models && typeof models === "object" &&
830
+ Object.values(models).every((counts) => Array.isArray(counts) && counts.length === 4 && counts.every((n) => typeof n === "number"));
831
+ }
832
+ function readCostHistory() {
833
+ try {
834
+ const parsed = JSON.parse(readFileSync(COST_HISTORY_FILE, "utf8"));
835
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
836
+ return {};
837
+ return Object.fromEntries(Object.entries(parsed).map(([id, states]) => [id, Array.isArray(states) ? states.filter(isCostSnapshot) : []]));
838
+ }
839
+ catch {
840
+ return {};
841
+ }
842
+ }
843
+ function recordCostState(sessionId, state) {
844
+ const history = readCostHistory();
845
+ const states = [...(history[sessionId] ?? []), state].slice(-COST_HISTORY_STATES);
846
+ // most recent session last, so the oldest ones are dropped first
847
+ delete history[sessionId];
848
+ history[sessionId] = states;
849
+ const ids = Object.keys(history);
850
+ for (const id of ids.slice(0, Math.max(0, ids.length - COST_HISTORY_SESSIONS)))
851
+ delete history[id];
852
+ try {
853
+ writeFileAtomic(COST_HISTORY_FILE, JSON.stringify(history), { mode: 0o600 });
854
+ }
855
+ catch {
856
+ // a lost state only means a later resume keeps its whole figure
857
+ }
858
+ }
755
859
  /** Claude's stream-json input accepts the same image source blocks as the
756
860
  * Anthropic Messages API. Keep the old string form for text-only turns so a
757
861
  * CLI update cannot disturb the overwhelmingly common path. */
@@ -1155,7 +1259,7 @@ export const ClaudeDriver = {
1155
1259
  mcpServers.ogb = { command: process.execPath, args: [PERM_PROXY_PATH, socketPath], env: { ...NODE_ENV_FLAG }, alwaysLoad: true };
1156
1260
  allowed.push("mcp__ogb");
1157
1261
  // The MCP config carries credentials — a Composio consumer key in a
1158
- // header, the box token in the computer proxy's env, the comms token in
1262
+ // header, the boat token in the computer proxy's env, the comms token in
1159
1263
  // the agents proxy's env. On argv every one of those is world-readable
1160
1264
  // through `ps` for the life of the turn, to any local process. The CLI
1161
1265
  // accepts a FILE for this flag, so the secrets go in a 0600 file that
@@ -1414,11 +1518,12 @@ export const ClaudeDriver = {
1414
1518
  idleTimer: null,
1415
1519
  closing: false,
1416
1520
  stderr: "",
1521
+ costTotal: undefined,
1417
1522
  };
1418
1523
  sessions.set(threadId, session);
1419
1524
  // settles the TURN, not the process: the CLI stays for the next
1420
1525
  // message until it has been quiet for SESSION_IDLE_MS
1421
- const settle = (ok, stopReason, cost = null, usage) => {
1526
+ const settle = (ok, stopReason, total = null, usage) => {
1422
1527
  const t = session.turn;
1423
1528
  if (!t || t.settled)
1424
1529
  return;
@@ -1450,11 +1555,31 @@ export const ClaudeDriver = {
1450
1555
  // retry resumes on a fresh process; healthy sibling sessions stay warm.
1451
1556
  if (stopReason === "update_required")
1452
1557
  closeSession(threadId, "update required");
1558
+ // `total` is the CLI's running total for this process; the harness
1559
+ // books turn.completed.cost as this turn's own spend
1560
+ const cost = turnCostFromRunningTotal(total, session.costTotal ?? null);
1561
+ if (total !== null)
1562
+ session.costTotal = total;
1453
1563
  emit({ ...base(threadId, t.turnId), type: "turn.completed", ok, stopReason, cost, ...(usage ? { usage } : {}) });
1454
1564
  if (session.child.exitCode === null && !session.closing)
1455
1565
  armIdle(threadId);
1456
1566
  };
1457
1567
  const currentTurnId = () => session.turn?.turnId ?? turnId;
1568
+ // The process's first result with a cost says what --resume restored,
1569
+ // which its turns are measured from; every result is kept for a later
1570
+ // resume. A result without one (an API error) decides nothing yet.
1571
+ const noteCostState = (total, modelUsage, usage) => {
1572
+ const state = claudeCostSnapshot(total, modelUsage);
1573
+ if (!state)
1574
+ return;
1575
+ if (session.costTotal === undefined) {
1576
+ session.costTotal = session.sessionId
1577
+ ? restoredCostBase(readCostHistory()[session.sessionId] ?? [], state, usage)
1578
+ : null;
1579
+ }
1580
+ if (session.sessionId)
1581
+ recordCostState(session.sessionId, state);
1582
+ };
1458
1583
  const handleLine = (line) => {
1459
1584
  if (session.closing)
1460
1585
  return;
@@ -1575,11 +1700,19 @@ export const ClaudeDriver = {
1575
1700
  // and deny approvals while that user turn is still running.
1576
1701
  if (o.origin?.kind === "task-notification")
1577
1702
  break;
1578
- // result.usage is this invocation's total — one process per turn,
1579
- // so it is the turn's figure. cache reads count as input: they
1580
- // are billed (at the cache rate) and they fill the window — but
1581
- // they are reported separately too, so the UI can show how much
1582
- // of the figure was context re-read rather than new text.
1703
+ // result.usage is this turn's own figure, "per-turn in
1704
+ // streaming-input sessions" (2.1.282) even on a retained process.
1705
+ // cache reads count as input: they are billed (at the cache rate)
1706
+ // and they fill the window — but they are reported separately
1707
+ // too, so the UI can show how much of the figure was context
1708
+ // re-read rather than new text. total_cost_usd is instead the
1709
+ // process's running total; settle() books this turn's share.
1710
+ noteCostState(o.total_cost_usd, o.modelUsage, {
1711
+ input: o.usage?.input_tokens || 0,
1712
+ cacheRead: o.usage?.cache_read_input_tokens || 0,
1713
+ cacheWrite: o.usage?.cache_creation_input_tokens || 0,
1714
+ output: o.usage?.output_tokens || 0,
1715
+ });
1583
1716
  settle(o.is_error !== true, session.turn?.authFailed
1584
1717
  ? "auth_required"
1585
1718
  : session.turn?.updateRequired
@@ -1877,10 +2010,11 @@ export const ClaudeDriver = {
1877
2010
  * summaries can contain paths, commands, or secrets, so the generic
1878
2011
  * `claude -p "prompt"` shape is not safe for review. No tools or MCP
1879
2012
  * servers are mounted in this isolated process. */
1880
- const generateReview = (prompt, signal) => new Promise((resolve, reject) => {
1881
- const child = spawnCli(config.cli, ["-p", "--model", config.managedModels?.[0] ?? "claude-haiku-4-5", "--output-format", "text"], {
2013
+ const generateReview = (prompt, signal, onUsage) => new Promise((resolve, reject) => {
2014
+ const model = config.managedModels?.[0] ?? "claude-haiku-4-5";
2015
+ const child = spawnCli(config.cli, ["-p", "--model", model, "--output-format", onUsage ? "json" : "text"], {
1882
2016
  stdio: ["pipe", "pipe", "pipe"],
1883
- env: environment(config.managedModels?.[0] ?? "claude-haiku-4-5"),
2017
+ env: environment(model),
1884
2018
  });
1885
2019
  let stdout = "";
1886
2020
  let stderr = "";
@@ -1919,6 +2053,37 @@ export const ClaudeDriver = {
1919
2053
  });
1920
2054
  child.on("error", (error) => finish(error));
1921
2055
  child.on("close", (code) => {
2056
+ if (settled)
2057
+ return;
2058
+ if (onUsage) {
2059
+ try {
2060
+ const result = JSON.parse(stdout);
2061
+ if (!result || result.type !== "result")
2062
+ throw new Error("Claude text generation returned no result");
2063
+ const count = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
2064
+ const cachedInput = count(result.usage?.cache_read_input_tokens);
2065
+ const inputs = [count(result.usage?.input_tokens), cachedInput, count(result.usage?.cache_creation_input_tokens)]
2066
+ .filter((value) => value !== undefined);
2067
+ const models = result.modelUsage && typeof result.modelUsage === "object" && !Array.isArray(result.modelUsage)
2068
+ ? Object.keys(result.modelUsage) : [];
2069
+ onUsage({
2070
+ model: models.length === 1 ? models[0] : models.find(candidate => candidate === model || candidate.startsWith(`${model}-`)) ?? model,
2071
+ input: inputs.length ? inputs.reduce((sum, value) => sum + value, 0) : undefined,
2072
+ output: count(result.usage?.output_tokens),
2073
+ cachedInput,
2074
+ costUsd: count(result.total_cost_usd),
2075
+ });
2076
+ if (result.is_error === true)
2077
+ throw new Error(typeof result.result === "string" && result.result.trim() ? result.result : stderr.trim() || "Claude text generation failed");
2078
+ if (typeof result.result !== "string")
2079
+ throw new Error("Claude text generation returned no text");
2080
+ stdout = result.result;
2081
+ }
2082
+ catch (error) {
2083
+ finish(error instanceof Error ? error : new Error(String(error)));
2084
+ return;
2085
+ }
2086
+ }
1922
2087
  if (code === 0)
1923
2088
  finish();
1924
2089
  else
@@ -2000,7 +2165,7 @@ export const ClaudeDriver = {
2000
2165
  return () => listeners.delete(listener);
2001
2166
  },
2002
2167
  },
2003
- generateText: (prompt, options) => generateReview(prompt, options?.signal),
2168
+ generateText: (prompt, options) => generateReview(prompt, options?.signal, options?.onUsage),
2004
2169
  reviewPermission: generateReview,
2005
2170
  dispose: async () => {
2006
2171
  try {
@@ -11,7 +11,7 @@ export function appendNative(threadId, entry) {
11
11
  const file = join(NATIVE_DIR, `${threadId}.ndjson`);
12
12
  try {
13
13
  // The session-setup messages carry the credentials the agent is handed —
14
- // the box and comms tokens ride inside session/new's mcpServers env, and
14
+ // the boat and comms tokens ride inside session/new's mcpServers env, and
15
15
  // an MCP header can carry a Composio key. These files are ordinary
16
16
  // 0644 files people paste into bug reports, so values are masked while
17
17
  // the shape stays intact.
@@ -64,7 +64,7 @@ export function createOpenAIChatRuntime(options) {
64
64
  turnId,
65
65
  createdAt: new Date().toISOString(),
66
66
  });
67
- const complete = async (messages, model, stream, signal, onDelta, tools = []) => {
67
+ const complete = async (messages, model, stream, signal, onDelta, tools = [], onUsage) => {
68
68
  // Idle timer that is renewed on every received chunk during streaming
69
69
  const timeoutController = new AbortController();
70
70
  let idleTimer = null;
@@ -99,6 +99,17 @@ export function createOpenAIChatRuntime(options) {
99
99
  }
100
100
  if (!stream || response.headers.get("content-type")?.includes("application/json")) {
101
101
  const json = await response.json();
102
+ if (onUsage) {
103
+ activeSignal.throwIfAborted();
104
+ const count = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
105
+ onUsage({
106
+ model: typeof json.model === "string" && json.model.trim() ? json.model : model,
107
+ input: count(json.usage?.prompt_tokens),
108
+ output: count(json.usage?.completion_tokens),
109
+ cachedInput: count(json.usage?.prompt_tokens_details?.cached_tokens),
110
+ costUsd: count(json.usage?.cost),
111
+ });
112
+ }
102
113
  const bodyError = providerError(json);
103
114
  if (bodyError)
104
115
  throw new ChatProtocolError(`provider returned a completion error: ${bodyError.slice(0, 200)}`);
@@ -619,7 +630,7 @@ export function createOpenAIChatRuntime(options) {
619
630
  provider: options.driverKind,
620
631
  capabilities: { ...(options.computerUse ? { computerMcp: options.tools !== false,
621
632
  // Same gate as cloudComputerMcp: with tools off the runtime cannot
622
- // mount the leased Box descriptor either. The fleet invariant test
633
+ // mount the leased Boat descriptor either. The fleet invariant test
623
634
  // pins usesCloudComputer === (remoteAgent || cloudComputerMcp).
624
635
  usesCloudComputer: options.tools !== false, cloudComputerMcp: options.tools !== false, localComputerMcp: options.tools !== false,
625
636
  browserMcp: options.tools !== false, nativeImageInput: true, images: true } : {}),
@@ -661,9 +672,9 @@ export function createOpenAIChatRuntime(options) {
661
672
  return () => listeners.delete(listener);
662
673
  },
663
674
  },
664
- generateText: async (prompt, { signal } = {}) => {
675
+ generateText: async (prompt, { signal, onUsage } = {}) => {
665
676
  const model = options.generateModel?.() ?? options.models().default;
666
- const { text, reasoning, toolCalls } = await complete([{ role: "user", content: prompt }], model, false, signal);
677
+ const { text, reasoning, toolCalls } = await complete([{ role: "user", content: prompt }], model, false, signal, undefined, [], onUsage);
667
678
  if (toolCalls.length)
668
679
  throw new ChatProtocolError("provider returned tool calls to a text-only helper");
669
680
  return text.trim() ? text : reasoning;
@@ -465,7 +465,7 @@ export const PiDriver = {
465
465
  ensurePiInjectModel(turn.model, { ...process.env, ...input.environment });
466
466
  }
467
467
  // integrations → stdio MCP servers for the pi-mcp-extension. The config
468
- // carries credentials (box token, composio key, comms token), so it goes
468
+ // carries credentials (boat token, composio key, comms token), so it goes
469
469
  // into a 0600 temp file removed when the turn settles — never on argv.
470
470
  const mcpServers = buildMcpServers(turn);
471
471
  let mcpTempDir = null;
@@ -476,7 +476,7 @@ export const PiDriver = {
476
476
  }
477
477
  catch (err) {
478
478
  // A failed write must not leave the temp dir behind — a partial file
479
- // could still hold the box token / composio key / comms token.
479
+ // could still hold the boat token / composio key / comms token.
480
480
  try {
481
481
  rmSync(mcpTempDir, { recursive: true, force: true });
482
482
  }
@@ -488,7 +488,7 @@ export const PiDriver = {
488
488
  }
489
489
  const childArgs = mcpServers ? [...PI_ARGS, "-e", SPAWNED_PROXIES.piMcpExtension] : PI_ARGS;
490
490
  // spawnCli can throw synchronously (unresolvable CLI); if it does, the
491
- // 0600 temp file with the box token / composio key / comms token must
491
+ // 0600 temp file with the boat token / composio key / comms token must
492
492
  // not be left on disk — settle() never runs because no child existed.
493
493
  const child = (() => {
494
494
  try {
@@ -66,12 +66,14 @@ export function parseGroupGoalDecision(text) {
66
66
  }
67
67
  return { visibleText, decision: null };
68
68
  }
69
- /** Explicit room lead wins. Otherwise prefer an in-room Chief, then the
70
- * first active member. A goal run never recruits somebody outside the room. */
69
+ /** Explicit room lead (or an Auto room's fallback) wins. Otherwise prefer
70
+ * an in-room Chief, then the first active member. A goal run never recruits
71
+ * somebody outside the room. */
71
72
  export function selectGroupGoalCoordinator(members, responder) {
72
73
  const active = members.filter((member) => !member.hidden);
73
- if (responder.kind === "member") {
74
- const explicit = active.find((member) => member.id === responder.botId);
74
+ const lead = responder.kind === "member" ? responder.botId : responder.kind === "auto" ? responder.fallbackBotId : undefined;
75
+ if (lead) {
76
+ const explicit = active.find((member) => member.id === lead);
75
77
  if (explicit)
76
78
  return explicit;
77
79
  }
@@ -20,6 +20,8 @@ const ENVELOPE = {
20
20
  skillAuthoring: true,
21
21
  sharedComputers: true,
22
22
  voiceNotes: true,
23
+ // Not a tool it adds: a Cloud home only hides its Local VM shell.
24
+ cloudHome: false,
23
25
  botId: "",
24
26
  };
25
27
  /** Extra card lines for an engine switch, empty when nothing changes. */
@@ -2,6 +2,7 @@ import { existsSync, lstatSync, mkdirSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { z } from "zod";
4
4
  import { writeFileAtomic } from "./atomic.js";
5
+ import { cloudHomeConfigured } from "./cloud-home.js";
5
6
  import { hostedWorkspaceConfiguration } from "./enterprise.js";
6
7
  export const HOSTED_MODEL_POLICY_HEADER = "X-Omb-Hosted-Model-Policy";
7
8
  export const HOSTED_MODEL_SETUP_ERROR = "No company models are assigned to this workspace. Ask your administrator to enable model access in Admin.";
@@ -15,10 +16,14 @@ const catalogSchema = z.object({
15
16
  }).strict();
16
17
  const providerFor = { claude: "anthropic", codex: "openai", opencode: "openrouter" };
17
18
  /** Operator-only policy. An ordinary desktop has neither input and keeps its
18
- * personal providers. Partial policy configuration must never enable them. */
19
+ * personal providers. Partial policy configuration must never enable them.
20
+ * A Cloud home never routes to a model gateway: it ignores both inputs
21
+ * (cloud-home.ts), and its person signs in with their own engines. */
19
22
  export function hostedModelPolicy(dataDirectory, env = process.env) {
20
23
  if (env.OMB_HOSTED_MODELS === undefined && env.OMB_HOSTED_MODEL_TOKEN === undefined)
21
24
  return null;
25
+ if (cloudHomeConfigured(env))
26
+ return null;
22
27
  const hosted = hostedWorkspaceConfiguration(env);
23
28
  if (!hosted?.portalMembership || env.OMB_DESKTOP_PARENT === "1" || !/^omb_workspace_[A-Za-z0-9_-]{43}$/.test(env.OMB_HOSTED_MODEL_TOKEN ?? "")) {
24
29
  throw new Error("Hosted model access requires complete portal-managed configuration.");