openmausbot 0.1.88 → 0.1.90

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 (147) hide show
  1. package/README.md +1 -1
  2. package/dist/assets/{abnfDiagram-O67JEVCF-qd9VD3GV.js → abnfDiagram-O67JEVCF-DUyXd0kL.js} +1 -1
  3. package/dist/assets/{arc-3RwkUjRe.js → arc-q73qM-At.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-CFKSJS7a.js → architectureDiagram-NJMV4G6O-D33O3Wur.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-CG1rEfWD.js → blockDiagram-BEXU5L5S-DLsJeoXC.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-CBW9N664.js → c4Diagram-YGBWAQC7-DnaljZnu.js} +1 -1
  7. package/dist/assets/channel-BCuyHjFV.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-2mRfTksB.js → chunk-2Q5K7J3B-Da0h2i79.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-Cv1dpErj.js → chunk-3FUC2YCW-CfzHi54f.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-B7IACJp-.js → chunk-5VM5RSS4-LjE4d9Jf.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-An0hZtYa.js → chunk-GTNCS2PH-DY-UGmvc.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-BcJB2nEm.js → chunk-GWA4HPMP-BqeGhyhP.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-CV_W7TN7.js → chunk-JWPE2WC7-BYa9EBAg.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-Bcs_kwnA.js → chunk-NETBCI7D-vTrDtWyb.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-lrx_DxFP.js → chunk-WEXAMYUT-ic5tXadM.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-DabHlLGM.js → chunk-XXDRQBXY-DvRCY-c6.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-BnHj6P7d.js → classDiagram-v2-NBCMYWYE-DVbmgz_L.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-CfLp6UDi.js → cose-bilkent-JH36ORCC-vdUs2n57.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-Dm4dbVA1.js → cynefin-OW5HDTMX-BvLGlV3U.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-B9mH-PID.js → cynefinDiagram-VND7K2PF-C4GcJQDt.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-DDc_NVQJ.js → dagre-6A5THRUB-BMSaHpt_.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-lwr8Ip8-.js → diagram-22UHCM2B-D5b9UBhC.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-BLLe6dmI.js → diagram-3UASUU5V-CIKLlB7Y.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-6eUIq4lz.js → diagram-ATOU4E4O-BzXb-7k7.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-O-pRN7VQ.js → diagram-CDSNMT55-HsTvJ7uT.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-BietvGAI.js → diagram-MLGK6HIB-Db9ySNbQ.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-Bd2Cl62X.js → diagram-MPIPVDR6-D6BKbpqY.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-B6iDy69W.js → ebnfDiagram-ZINNZB2B-Bq067L0t.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-DQVHfKe0.js → elk-276RUBZZ-DhaD7fNJ.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-BWdVp2tQ.js → erDiagram-OPXOYQCR-fwIhfz36.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-WLE6rKB4.js → flowDiagram-KWPJA3E3-DygIyu0V.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-BbjS-4gn.js → ganttDiagram-FUAMR5RP-DPYfSO96.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-Dpy2QSGq.js → gitGraphDiagram-X574FWY7-DhkHeCle.js} +1 -1
  34. package/dist/assets/index-CRvo-DS2.css +1 -0
  35. package/dist/assets/index-CWYGlA63.js +588 -0
  36. package/dist/assets/{index-CIaHn5NS.js → index-G1PHKGyh.js} +1 -1
  37. package/dist/assets/{infoDiagram-VRGFBTTK-D_jKWlKe.js → infoDiagram-VRGFBTTK-DstBLxME.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-DLJohSuK.js → ishikawaDiagram-OU5B5YK6-CrIsGgch.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-DPOFj0qN.js → journeyDiagram-ZHPQQLJL-CYypGDbB.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-BI7v-xpK.js → kanban-definition-PNTS6WVX-DGjPt8E_.js} +1 -1
  41. package/dist/assets/{linear-C2pWPSxO.js → linear-Dwc3Hrru.js} +1 -1
  42. package/dist/assets/{mermaid.core-CR-j5WQU.js → mermaid.core-Dc4OePBL.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-lz9GvXRh.js → mindmap-definition-NLK3R4M7-DV-vepj-.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-Dl4_ia61.js → pegDiagram-GJSIUBJH-lx5Kj-n5.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-DWuT3n88.js → pieDiagram-5QR66LMP-Bq7Vmm15.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-DXWe9Jx7.js → quadrantDiagram-O4NWA36T-DeqeqOBj.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-COY1IBVY.js → railroadDiagram-XR7U4H2S-CXwRQzSC.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-DnyVuhsQ.js → requirementDiagram-PLB6GJNP-8tAeoB88.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-BSc4NdGK.js → sankeyDiagram-IPEJSGJF-BG5zjGWU.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-kqEU2EGq.js → sequenceDiagram-PO4LG4MO-DKF3iLcw.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-BvB4bkWD.js → sizeCapture-INFHLROL-B-y1qUgT.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-BIrqNv7y.js → stateDiagram-v2-GCMORJYK-MkzPgxAc.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-CFnalQUE.js → swimlanes-2SLR337P-DnbaOEwH.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-BZQ07DGE.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-CsSWJnSQ.js → timeline-definition-EJHVYXUP-Bb4xG_Bt.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-DkAmJ1EE.js → usecaseDiagram-POWQR4AR-JgkR9pFf.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-C3vd9drL.js → vennDiagram-UO4OBE2U-CIPIrAYe.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-j26CM-pi.js → wardleyDiagram-VNRHLVJA-BjCLuUvJ.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-BYu1Q_at.js → xychartDiagram-PMCCYNJV-DKgiaYxT.js} +1 -1
  60. package/dist/index.html +2 -2
  61. package/dist-server/cloud-home-start.js +14795 -0
  62. package/dist-server/container-mcp.js +60 -4
  63. package/dist-server/drivers/agents-proxy.js +20 -10
  64. package/dist-server/index.js +48924 -46153
  65. package/dist-server/openmausbot.js +544 -253
  66. package/dist-server/pair-cli.js +544 -253
  67. package/dist-server/permission-proxy.js +1 -1
  68. package/dist-server/server/admission.js +46 -0
  69. package/dist-server/server/automatic-recovery.js +15 -0
  70. package/dist-server/server/{box-create-idempotency.js → boat-create-idempotency.js} +32 -31
  71. package/dist-server/server/{box-delete-journal.js → boat-delete-journal.js} +26 -25
  72. package/dist-server/server/{box.js → boat.js} +330 -328
  73. package/dist-server/server/bot-profile.js +7 -1
  74. package/dist-server/server/channel-queue.js +58 -18
  75. package/dist-server/server/checkpoints.js +98 -52
  76. package/dist-server/server/claim-idle.js +33 -0
  77. package/dist-server/server/cli-api-setup.js +0 -1
  78. package/dist-server/server/cloud-backend.js +5 -5
  79. package/dist-server/server/cloud-home-start.js +103 -0
  80. package/dist-server/server/cloud-home.js +218 -0
  81. package/dist-server/server/cloud-overflow.js +167 -0
  82. package/dist-server/server/composio.js +5 -0
  83. package/dist-server/server/computer-observation.js +2 -2
  84. package/dist-server/server/config.js +124 -6
  85. package/dist-server/server/container-computer.js +23 -0
  86. package/dist-server/server/contracts.js +10 -0
  87. package/dist-server/server/control-client.js +2 -2
  88. package/dist-server/server/delegations.js +11 -3
  89. package/dist-server/server/drivers/acp/core.js +129 -21
  90. package/dist-server/server/drivers/acp/qwen.js +3 -0
  91. package/dist-server/server/drivers/agents-call.js +5 -1
  92. package/dist-server/server/drivers/agents-catalog.js +9 -3
  93. package/dist-server/server/drivers/{boxagent.js → boatagent.js} +14 -13
  94. package/dist-server/server/drivers/builtIn.js +2 -2
  95. package/dist-server/server/drivers/{chat-box-tools.js → chat-boat-tools.js} +3 -3
  96. package/dist-server/server/drivers/chat-mcp-tools.js +2 -2
  97. package/dist-server/server/drivers/claude.js +177 -13
  98. package/dist-server/server/drivers/native.js +1 -1
  99. package/dist-server/server/drivers/openai-chat.js +19 -4
  100. package/dist-server/server/drivers/pi.js +3 -3
  101. package/dist-server/server/harness-capabilities.js +18 -3
  102. package/dist-server/server/hosted-models.js +6 -1
  103. package/dist-server/server/index.js +1163 -530
  104. package/dist-server/server/local-vm-inventory.js +11 -1
  105. package/dist-server/server/local-vm-seat-pool.js +141 -0
  106. package/dist-server/server/managed-policy.js +4 -1
  107. package/dist-server/server/memory-capture.js +194 -0
  108. package/dist-server/server/memory-entries.js +76 -0
  109. package/dist-server/server/memory-organize.js +102 -0
  110. package/dist-server/server/memory-tidy.js +144 -0
  111. package/dist-server/server/memory-topics.js +169 -0
  112. package/dist-server/server/memory-upkeep.js +452 -0
  113. package/dist-server/server/message-db.js +49 -5
  114. package/dist-server/server/permission-proxy.js +3 -3
  115. package/dist-server/server/profile-learned.js +98 -0
  116. package/dist-server/server/profile-versions.js +45 -11
  117. package/dist-server/server/recall.js +172 -0
  118. package/dist-server/server/redact.js +1 -1
  119. package/dist-server/server/room-cwd.js +1 -1
  120. package/dist-server/server/room-handoffs.js +61 -6
  121. package/dist-server/server/routes/bot-memory.js +215 -0
  122. package/dist-server/server/routine-requests.js +27 -16
  123. package/dist-server/server/routines.js +10 -6
  124. package/dist-server/server/steer-queue.js +41 -10
  125. package/dist-server/server/store.js +25 -1
  126. package/dist-server/server/surface.js +1 -1
  127. package/dist-server/server/system-prompt.js +16 -5
  128. package/dist-server/server/team-computers.js +2 -2
  129. package/dist-server/server/tightening-requests.js +34 -4
  130. package/dist-server/server/tool-summary.js +2 -2
  131. package/dist-server/server/tts/elevenlabs.js +1 -1
  132. package/dist-server/server/turn-resources.js +87 -11
  133. package/dist-server/server/vps-computer.js +1 -1
  134. package/dist-server/server/webhooks.js +21 -3
  135. package/dist-server/server/workspace.js +57 -22
  136. package/dist-server/shared/ask-question.js +7 -5
  137. package/dist-server/shared/bot-avatar.js +19 -0
  138. package/dist-server/shared/credential-request.js +3 -3
  139. package/dist-server/shared/tool-surface.js +1 -1
  140. package/dist-server/src/lib/composer-attachments.js +26 -0
  141. package/dist-server/vps-container-mcp.js +60 -4
  142. package/dist-server/workspace-backup.worker.js +60 -4
  143. package/package.json +1 -1
  144. package/dist/assets/channel-CnRnO3tF.js +0 -1
  145. package/dist/assets/index-BIMdIbaw.css +0 -1
  146. package/dist/assets/index-CHnODCr2.js +0 -583
  147. package/dist/assets/swimlanesDiagram-TC7HE7FX-C4a8qNGj.js +0 -8
@@ -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";
@@ -435,7 +435,7 @@ function askQuestions(ask) {
435
435
  function askSummary(ask) {
436
436
  const questions = askQuestions(ask);
437
437
  if (questions)
438
- return askQuestionSummary(questions).slice(0, 300);
438
+ return askQuestionSummary(questions);
439
439
  return askInputSummary(ask.input) ?? ask.tool ?? "tool";
440
440
  }
441
441
  /** Where the hook helper reads this thread's current turn token. Stable per
@@ -752,6 +752,109 @@ function firstText(content) {
752
752
  }
753
753
  return "";
754
754
  }
755
+ /** A turn's own cost from the CLI's total_cost_usd, which is not a per-turn
756
+ * figure: it is "cumulative across turns in streaming-input sessions — each
757
+ * result carries the running total so far" (2.1.282), and a retained process
758
+ * runs turn after turn. So a turn costs the growth since the total its
759
+ * process reported for the turn before — or, for a process's first turn,
760
+ * since the total the CLI restored on --resume (see restoredCostBase). With
761
+ * no known start (null) the turn keeps its whole figure. A total that went
762
+ * down is not the same count, so it is taken whole too rather than booked as
763
+ * a negative cost. Rounding to 1e-10 USD removes only the float noise of the
764
+ * subtraction. */
765
+ export function turnCostFromRunningTotal(total, previous) {
766
+ if (total === null)
767
+ return null;
768
+ if (previous === null || total < previous)
769
+ return total;
770
+ return Number((total - previous).toFixed(10));
771
+ }
772
+ export function claudeCostSnapshot(total, modelUsage) {
773
+ if (typeof total !== "number" || !Number.isFinite(total))
774
+ return null;
775
+ const count = (value) => (typeof value === "number" && Number.isFinite(value) ? value : 0);
776
+ const models = {};
777
+ if (modelUsage && typeof modelUsage === "object" && !Array.isArray(modelUsage)) {
778
+ for (const [model, raw] of Object.entries(modelUsage)) {
779
+ if (!raw || typeof raw !== "object")
780
+ continue;
781
+ const u = raw;
782
+ models[model] = [count(u.inputTokens), count(u.cacheReadInputTokens), count(u.cacheCreationInputTokens), count(u.outputTokens)];
783
+ }
784
+ }
785
+ return { total, models };
786
+ }
787
+ /** The running total a resumed session already carried before this
788
+ * process's first turn. On --resume the CLI (2.1.282) restores the session's
789
+ * cost from an earlier state — not always the latest one this driver saw —
790
+ * so that turn's total_cost_usd and modelUsage include the earlier turns.
791
+ * The restored state is the earlier state that sits inside the new counts
792
+ * and leaves exactly this turn's own usage: in one model (usage leaves out
793
+ * side calls such as a Haiku title) or summed over all models (a turn split
794
+ * between two); nothing restored is 0. When no state fits exactly — the CLI
795
+ * saved work that never reported a result, like an interrupted turn — the
796
+ * latest state inside the new counts stands, so that work is booked once,
797
+ * with this turn. Either way the latest state wins, not the highest total:
798
+ * a resume that went back to an older state leaves later, lower totals. */
799
+ export function restoredCostBase(earlier, current, usage) {
800
+ const turn = [usage.input, usage.cacheRead, usage.cacheWrite, usage.output];
801
+ const nothing = { total: 0, models: {} };
802
+ let exact = null;
803
+ let inside = 0;
804
+ // oldest first: the session's states in the order they were recorded
805
+ for (const state of [nothing, ...earlier]) {
806
+ const within = Object.entries(state.models).every(([model, counts]) => counts.every((n, i) => n <= (current.models[model]?.[i] ?? 0)));
807
+ if (!within)
808
+ continue;
809
+ inside = state.total;
810
+ const growth = Object.entries(current.models).map(([model, counts]) => counts.map((n, i) => n - (state.models[model]?.[i] ?? 0)));
811
+ const isTurn = (counts) => counts.every((n, i) => n === turn[i]);
812
+ const summed = turn.map((_, i) => growth.reduce((sum, counts) => sum + counts[i], 0));
813
+ if (growth.some(isTurn) || isTurn(summed))
814
+ exact = state.total;
815
+ }
816
+ return exact ?? inside;
817
+ }
818
+ /** Each Claude session's latest cost states, so the first turn after a
819
+ * --resume can tell what the CLI restored — after an app restart too. Small
820
+ * by design: a few states for the most recent sessions. */
821
+ const COST_HISTORY_FILE = join(DATA_DIR, "claude-cost-history.json");
822
+ const COST_HISTORY_SESSIONS = 100;
823
+ const COST_HISTORY_STATES = 8;
824
+ function isCostSnapshot(value) {
825
+ if (!value || typeof value !== "object")
826
+ return false;
827
+ const { total, models } = value;
828
+ return typeof total === "number" && !!models && typeof models === "object" &&
829
+ Object.values(models).every((counts) => Array.isArray(counts) && counts.length === 4 && counts.every((n) => typeof n === "number"));
830
+ }
831
+ function readCostHistory() {
832
+ try {
833
+ const parsed = JSON.parse(readFileSync(COST_HISTORY_FILE, "utf8"));
834
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
835
+ return {};
836
+ return Object.fromEntries(Object.entries(parsed).map(([id, states]) => [id, Array.isArray(states) ? states.filter(isCostSnapshot) : []]));
837
+ }
838
+ catch {
839
+ return {};
840
+ }
841
+ }
842
+ function recordCostState(sessionId, state) {
843
+ const history = readCostHistory();
844
+ const states = [...(history[sessionId] ?? []), state].slice(-COST_HISTORY_STATES);
845
+ // most recent session last, so the oldest ones are dropped first
846
+ delete history[sessionId];
847
+ history[sessionId] = states;
848
+ const ids = Object.keys(history);
849
+ for (const id of ids.slice(0, Math.max(0, ids.length - COST_HISTORY_SESSIONS)))
850
+ delete history[id];
851
+ try {
852
+ writeFileAtomic(COST_HISTORY_FILE, JSON.stringify(history), { mode: 0o600 });
853
+ }
854
+ catch {
855
+ // a lost state only means a later resume keeps its whole figure
856
+ }
857
+ }
755
858
  /** Claude's stream-json input accepts the same image source blocks as the
756
859
  * Anthropic Messages API. Keep the old string form for text-only turns so a
757
860
  * CLI update cannot disturb the overwhelmingly common path. */
@@ -1155,7 +1258,7 @@ export const ClaudeDriver = {
1155
1258
  mcpServers.ogb = { command: process.execPath, args: [PERM_PROXY_PATH, socketPath], env: { ...NODE_ENV_FLAG }, alwaysLoad: true };
1156
1259
  allowed.push("mcp__ogb");
1157
1260
  // 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
1261
+ // header, the boat token in the computer proxy's env, the comms token in
1159
1262
  // the agents proxy's env. On argv every one of those is world-readable
1160
1263
  // through `ps` for the life of the turn, to any local process. The CLI
1161
1264
  // accepts a FILE for this flag, so the secrets go in a 0600 file that
@@ -1414,11 +1517,12 @@ export const ClaudeDriver = {
1414
1517
  idleTimer: null,
1415
1518
  closing: false,
1416
1519
  stderr: "",
1520
+ costTotal: undefined,
1417
1521
  };
1418
1522
  sessions.set(threadId, session);
1419
1523
  // settles the TURN, not the process: the CLI stays for the next
1420
1524
  // message until it has been quiet for SESSION_IDLE_MS
1421
- const settle = (ok, stopReason, cost = null, usage) => {
1525
+ const settle = (ok, stopReason, total = null, usage) => {
1422
1526
  const t = session.turn;
1423
1527
  if (!t || t.settled)
1424
1528
  return;
@@ -1450,11 +1554,31 @@ export const ClaudeDriver = {
1450
1554
  // retry resumes on a fresh process; healthy sibling sessions stay warm.
1451
1555
  if (stopReason === "update_required")
1452
1556
  closeSession(threadId, "update required");
1557
+ // `total` is the CLI's running total for this process; the harness
1558
+ // books turn.completed.cost as this turn's own spend
1559
+ const cost = turnCostFromRunningTotal(total, session.costTotal ?? null);
1560
+ if (total !== null)
1561
+ session.costTotal = total;
1453
1562
  emit({ ...base(threadId, t.turnId), type: "turn.completed", ok, stopReason, cost, ...(usage ? { usage } : {}) });
1454
1563
  if (session.child.exitCode === null && !session.closing)
1455
1564
  armIdle(threadId);
1456
1565
  };
1457
1566
  const currentTurnId = () => session.turn?.turnId ?? turnId;
1567
+ // The process's first result with a cost says what --resume restored,
1568
+ // which its turns are measured from; every result is kept for a later
1569
+ // resume. A result without one (an API error) decides nothing yet.
1570
+ const noteCostState = (total, modelUsage, usage) => {
1571
+ const state = claudeCostSnapshot(total, modelUsage);
1572
+ if (!state)
1573
+ return;
1574
+ if (session.costTotal === undefined) {
1575
+ session.costTotal = session.sessionId
1576
+ ? restoredCostBase(readCostHistory()[session.sessionId] ?? [], state, usage)
1577
+ : null;
1578
+ }
1579
+ if (session.sessionId)
1580
+ recordCostState(session.sessionId, state);
1581
+ };
1458
1582
  const handleLine = (line) => {
1459
1583
  if (session.closing)
1460
1584
  return;
@@ -1575,11 +1699,19 @@ export const ClaudeDriver = {
1575
1699
  // and deny approvals while that user turn is still running.
1576
1700
  if (o.origin?.kind === "task-notification")
1577
1701
  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.
1702
+ // result.usage is this turn's own figure, "per-turn in
1703
+ // streaming-input sessions" (2.1.282) even on a retained process.
1704
+ // cache reads count as input: they are billed (at the cache rate)
1705
+ // and they fill the window — but they are reported separately
1706
+ // too, so the UI can show how much of the figure was context
1707
+ // re-read rather than new text. total_cost_usd is instead the
1708
+ // process's running total; settle() books this turn's share.
1709
+ noteCostState(o.total_cost_usd, o.modelUsage, {
1710
+ input: o.usage?.input_tokens || 0,
1711
+ cacheRead: o.usage?.cache_read_input_tokens || 0,
1712
+ cacheWrite: o.usage?.cache_creation_input_tokens || 0,
1713
+ output: o.usage?.output_tokens || 0,
1714
+ });
1583
1715
  settle(o.is_error !== true, session.turn?.authFailed
1584
1716
  ? "auth_required"
1585
1717
  : session.turn?.updateRequired
@@ -1877,10 +2009,11 @@ export const ClaudeDriver = {
1877
2009
  * summaries can contain paths, commands, or secrets, so the generic
1878
2010
  * `claude -p "prompt"` shape is not safe for review. No tools or MCP
1879
2011
  * 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"], {
2012
+ const generateReview = (prompt, signal, onUsage) => new Promise((resolve, reject) => {
2013
+ const model = config.managedModels?.[0] ?? "claude-haiku-4-5";
2014
+ const child = spawnCli(config.cli, ["-p", "--model", model, "--output-format", onUsage ? "json" : "text"], {
1882
2015
  stdio: ["pipe", "pipe", "pipe"],
1883
- env: environment(config.managedModels?.[0] ?? "claude-haiku-4-5"),
2016
+ env: environment(model),
1884
2017
  });
1885
2018
  let stdout = "";
1886
2019
  let stderr = "";
@@ -1919,6 +2052,37 @@ export const ClaudeDriver = {
1919
2052
  });
1920
2053
  child.on("error", (error) => finish(error));
1921
2054
  child.on("close", (code) => {
2055
+ if (settled)
2056
+ return;
2057
+ if (onUsage) {
2058
+ try {
2059
+ const result = JSON.parse(stdout);
2060
+ if (!result || result.type !== "result")
2061
+ throw new Error("Claude text generation returned no result");
2062
+ const count = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
2063
+ const cachedInput = count(result.usage?.cache_read_input_tokens);
2064
+ const inputs = [count(result.usage?.input_tokens), cachedInput, count(result.usage?.cache_creation_input_tokens)]
2065
+ .filter((value) => value !== undefined);
2066
+ const models = result.modelUsage && typeof result.modelUsage === "object" && !Array.isArray(result.modelUsage)
2067
+ ? Object.keys(result.modelUsage) : [];
2068
+ onUsage({
2069
+ model: models.length === 1 ? models[0] : models.find(candidate => candidate === model || candidate.startsWith(`${model}-`)) ?? model,
2070
+ input: inputs.length ? inputs.reduce((sum, value) => sum + value, 0) : undefined,
2071
+ output: count(result.usage?.output_tokens),
2072
+ cachedInput,
2073
+ costUsd: count(result.total_cost_usd),
2074
+ });
2075
+ if (result.is_error === true)
2076
+ throw new Error(typeof result.result === "string" && result.result.trim() ? result.result : stderr.trim() || "Claude text generation failed");
2077
+ if (typeof result.result !== "string")
2078
+ throw new Error("Claude text generation returned no text");
2079
+ stdout = result.result;
2080
+ }
2081
+ catch (error) {
2082
+ finish(error instanceof Error ? error : new Error(String(error)));
2083
+ return;
2084
+ }
2085
+ }
1922
2086
  if (code === 0)
1923
2087
  finish();
1924
2088
  else
@@ -2000,7 +2164,7 @@ export const ClaudeDriver = {
2000
2164
  return () => listeners.delete(listener);
2001
2165
  },
2002
2166
  },
2003
- generateText: (prompt, options) => generateReview(prompt, options?.signal),
2167
+ generateText: (prompt, options) => generateReview(prompt, options?.signal, options?.onUsage),
2004
2168
  reviewPermission: generateReview,
2005
2169
  dispose: async () => {
2006
2170
  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)}`);
@@ -617,7 +628,11 @@ export function createOpenAIChatRuntime(options) {
617
628
  : { state: "unavailable", reason: options.unavailableReason },
618
629
  adapter: {
619
630
  provider: options.driverKind,
620
- capabilities: { ...(options.computerUse ? { computerMcp: options.tools !== false, cloudComputerMcp: options.tools !== false, localComputerMcp: options.tools !== false,
631
+ capabilities: { ...(options.computerUse ? { computerMcp: options.tools !== false,
632
+ // Same gate as cloudComputerMcp: with tools off the runtime cannot
633
+ // mount the leased Boat descriptor either. The fleet invariant test
634
+ // pins usesCloudComputer === (remoteAgent || cloudComputerMcp).
635
+ usesCloudComputer: options.tools !== false, cloudComputerMcp: options.tools !== false, localComputerMcp: options.tools !== false,
621
636
  browserMcp: options.tools !== false, nativeImageInput: true, images: true } : {}),
622
637
  sessionModelSwitch: "in-session", customMcp: options.tools !== false, agentsMcp: options.tools !== false, composioMcp: options.tools !== false,
623
638
  // The runtime owns the whole tool loop, so it can always take a
@@ -657,9 +672,9 @@ export function createOpenAIChatRuntime(options) {
657
672
  return () => listeners.delete(listener);
658
673
  },
659
674
  },
660
- generateText: async (prompt, { signal } = {}) => {
675
+ generateText: async (prompt, { signal, onUsage } = {}) => {
661
676
  const model = options.generateModel?.() ?? options.models().default;
662
- 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);
663
678
  if (toolCalls.length)
664
679
  throw new ChatProtocolError("provider returned tool calls to a text-only helper");
665
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 {
@@ -7,7 +7,7 @@
7
7
  // uses one envelope profile and lets the per-driver capability decide the
8
8
  // delta; the tool names themselves come from the real catalog, so this
9
9
  // never drifts from what a turn is actually shown.
10
- import { APPROVAL_MODES, supportsApprovalMode } from "../shared/approval-mode.js";
10
+ import { APPROVAL_MODES, supportsApprovalMode as driverKindSupportsApprovalMode } from "../shared/approval-mode.js";
11
11
  import { availableTools } from "./drivers/agents-catalog.js";
12
12
  /** The widest turn the harness can build: every optional integration on and
13
13
  * a coordinating (room) turn, where the room-only coordination tools exist.
@@ -35,8 +35,8 @@ export function harnessCapabilityLines(from, to) {
35
35
  : "Loses peer coordination and every team tool: " + names.length + " tools including " + coordination.join(", ") + " go away.");
36
36
  }
37
37
  if (from.driverKind !== to.driverKind) {
38
- const lost = APPROVAL_MODES.filter((mode) => supportsApprovalMode(from.driverKind, mode) && !supportsApprovalMode(to.driverKind, mode));
39
- const gained = APPROVAL_MODES.filter((mode) => !supportsApprovalMode(from.driverKind, mode) && supportsApprovalMode(to.driverKind, mode));
38
+ const lost = APPROVAL_MODES.filter((mode) => driverKindSupportsApprovalMode(from.driverKind, mode) && !driverKindSupportsApprovalMode(to.driverKind, mode));
39
+ const gained = APPROVAL_MODES.filter((mode) => !driverKindSupportsApprovalMode(from.driverKind, mode) && driverKindSupportsApprovalMode(to.driverKind, mode));
40
40
  if (lost.length)
41
41
  lines.push("Loses approval levels: " + lost.join(", ") + ".");
42
42
  if (gained.length)
@@ -44,3 +44,18 @@ export function harnessCapabilityLines(from, to) {
44
44
  }
45
45
  return lines;
46
46
  }
47
+ /** Binds the registry to the approval-support check so each gate passes its
48
+ * model selection — or a driver view it already holds — and stops
49
+ * unwrapping registry.cliTarget(...)?.driverKind at every call site. The
50
+ * lookup prefers shadow state, so a mid-reconfig instance answers with the
51
+ * driver it would actually run. */
52
+ export function createApprovalModeSupport(registry) {
53
+ return function supportsApprovalMode(target, mode) {
54
+ const driverKind = target === undefined
55
+ ? undefined
56
+ : "driverKind" in target
57
+ ? target.driverKind
58
+ : registry.cliTarget(target.instanceId)?.driverKind;
59
+ return driverKindSupportsApprovalMode(driverKind, mode);
60
+ };
61
+ }
@@ -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.");