openmausbot 0.1.89 → 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 (140) hide show
  1. package/dist/assets/{abnfDiagram-O67JEVCF-BwaMaZEm.js → abnfDiagram-O67JEVCF-DUyXd0kL.js} +1 -1
  2. package/dist/assets/{arc-Be-8GKxd.js → arc-q73qM-At.js} +1 -1
  3. package/dist/assets/{architectureDiagram-NJMV4G6O-Dp_U2Kv1.js → architectureDiagram-NJMV4G6O-D33O3Wur.js} +1 -1
  4. package/dist/assets/{blockDiagram-BEXU5L5S-DQjUjYUk.js → blockDiagram-BEXU5L5S-DLsJeoXC.js} +1 -1
  5. package/dist/assets/{c4Diagram-YGBWAQC7-Bd3aE4Kc.js → c4Diagram-YGBWAQC7-DnaljZnu.js} +1 -1
  6. package/dist/assets/channel-BCuyHjFV.js +1 -0
  7. package/dist/assets/{chunk-2Q5K7J3B-BijjTBeJ.js → chunk-2Q5K7J3B-Da0h2i79.js} +1 -1
  8. package/dist/assets/{chunk-3FUC2YCW-CMGdwvzW.js → chunk-3FUC2YCW-CfzHi54f.js} +2 -2
  9. package/dist/assets/{chunk-5VM5RSS4-0G09BfsZ.js → chunk-5VM5RSS4-LjE4d9Jf.js} +1 -1
  10. package/dist/assets/{chunk-GTNCS2PH-FOp3vSHe.js → chunk-GTNCS2PH-DY-UGmvc.js} +1 -1
  11. package/dist/assets/{chunk-GWA4HPMP-Dv-20VIn.js → chunk-GWA4HPMP-BqeGhyhP.js} +1 -1
  12. package/dist/assets/{chunk-JWPE2WC7-CRgrdFcz.js → chunk-JWPE2WC7-BYa9EBAg.js} +1 -1
  13. package/dist/assets/{chunk-NETBCI7D-3hP4f7QB.js → chunk-NETBCI7D-vTrDtWyb.js} +1 -1
  14. package/dist/assets/{chunk-WEXAMYUT-CG98IjlG.js → chunk-WEXAMYUT-ic5tXadM.js} +1 -1
  15. package/dist/assets/{chunk-XXDRQBXY-BrSTC9xG.js → chunk-XXDRQBXY-DvRCY-c6.js} +1 -1
  16. package/dist/assets/{classDiagram-v2-NBCMYWYE-rpQQQDFX.js → classDiagram-v2-NBCMYWYE-DVbmgz_L.js} +1 -1
  17. package/dist/assets/{cose-bilkent-JH36ORCC-G6RSafHA.js → cose-bilkent-JH36ORCC-vdUs2n57.js} +1 -1
  18. package/dist/assets/{cynefin-OW5HDTMX-CXjv-IM2.js → cynefin-OW5HDTMX-BvLGlV3U.js} +1 -1
  19. package/dist/assets/{cynefinDiagram-VND7K2PF-Dw07MR9f.js → cynefinDiagram-VND7K2PF-C4GcJQDt.js} +1 -1
  20. package/dist/assets/{dagre-6A5THRUB-BwPnlTg2.js → dagre-6A5THRUB-BMSaHpt_.js} +1 -1
  21. package/dist/assets/{diagram-22UHCM2B-Cm6xcMiz.js → diagram-22UHCM2B-D5b9UBhC.js} +1 -1
  22. package/dist/assets/{diagram-3UASUU5V-CDpogPbE.js → diagram-3UASUU5V-CIKLlB7Y.js} +1 -1
  23. package/dist/assets/{diagram-ATOU4E4O-BV8z3Pnc.js → diagram-ATOU4E4O-BzXb-7k7.js} +1 -1
  24. package/dist/assets/{diagram-CDSNMT55-CKCQhOOC.js → diagram-CDSNMT55-HsTvJ7uT.js} +1 -1
  25. package/dist/assets/{diagram-MLGK6HIB-FGvJg69C.js → diagram-MLGK6HIB-Db9ySNbQ.js} +1 -1
  26. package/dist/assets/{diagram-MPIPVDR6-CwATvgnS.js → diagram-MPIPVDR6-D6BKbpqY.js} +1 -1
  27. package/dist/assets/{ebnfDiagram-ZINNZB2B-CH_c3SSC.js → ebnfDiagram-ZINNZB2B-Bq067L0t.js} +1 -1
  28. package/dist/assets/{elk-276RUBZZ-3guZi71R.js → elk-276RUBZZ-DhaD7fNJ.js} +1 -1
  29. package/dist/assets/{erDiagram-OPXOYQCR-BMHphR2n.js → erDiagram-OPXOYQCR-fwIhfz36.js} +1 -1
  30. package/dist/assets/{flowDiagram-KWPJA3E3-Vm5DkW5x.js → flowDiagram-KWPJA3E3-DygIyu0V.js} +1 -1
  31. package/dist/assets/{ganttDiagram-FUAMR5RP-DO-1ct07.js → ganttDiagram-FUAMR5RP-DPYfSO96.js} +1 -1
  32. package/dist/assets/{gitGraphDiagram-X574FWY7-nxeksEIN.js → gitGraphDiagram-X574FWY7-DhkHeCle.js} +1 -1
  33. package/dist/assets/index-CRvo-DS2.css +1 -0
  34. package/dist/assets/index-CWYGlA63.js +588 -0
  35. package/dist/assets/{index-Oxk9x45i.js → index-G1PHKGyh.js} +1 -1
  36. package/dist/assets/{infoDiagram-VRGFBTTK-Dkcvr0p7.js → infoDiagram-VRGFBTTK-DstBLxME.js} +1 -1
  37. package/dist/assets/{ishikawaDiagram-OU5B5YK6-BwVibkoO.js → ishikawaDiagram-OU5B5YK6-CrIsGgch.js} +1 -1
  38. package/dist/assets/{journeyDiagram-ZHPQQLJL-ByiBZqgG.js → journeyDiagram-ZHPQQLJL-CYypGDbB.js} +1 -1
  39. package/dist/assets/{kanban-definition-PNTS6WVX-qqjgXBF3.js → kanban-definition-PNTS6WVX-DGjPt8E_.js} +1 -1
  40. package/dist/assets/{linear-CtJnKe6A.js → linear-Dwc3Hrru.js} +1 -1
  41. package/dist/assets/{mermaid.core-BEiDklrY.js → mermaid.core-Dc4OePBL.js} +5 -5
  42. package/dist/assets/{mindmap-definition-NLK3R4M7-DfIYHia9.js → mindmap-definition-NLK3R4M7-DV-vepj-.js} +1 -1
  43. package/dist/assets/{pegDiagram-GJSIUBJH-CYyRqa1a.js → pegDiagram-GJSIUBJH-lx5Kj-n5.js} +1 -1
  44. package/dist/assets/{pieDiagram-5QR66LMP-hOToy_zH.js → pieDiagram-5QR66LMP-Bq7Vmm15.js} +1 -1
  45. package/dist/assets/{quadrantDiagram-O4NWA36T-DKAsNN9I.js → quadrantDiagram-O4NWA36T-DeqeqOBj.js} +1 -1
  46. package/dist/assets/{railroadDiagram-XR7U4H2S-BSA-d8JU.js → railroadDiagram-XR7U4H2S-CXwRQzSC.js} +1 -1
  47. package/dist/assets/{requirementDiagram-PLB6GJNP-CSmlvb-p.js → requirementDiagram-PLB6GJNP-8tAeoB88.js} +1 -1
  48. package/dist/assets/{sankeyDiagram-IPEJSGJF-CD5F6fnG.js → sankeyDiagram-IPEJSGJF-BG5zjGWU.js} +1 -1
  49. package/dist/assets/{sequenceDiagram-PO4LG4MO-JUq5sx6d.js → sequenceDiagram-PO4LG4MO-DKF3iLcw.js} +1 -1
  50. package/dist/assets/{sizeCapture-INFHLROL-BjIS-Stk.js → sizeCapture-INFHLROL-B-y1qUgT.js} +1 -1
  51. package/dist/assets/{stateDiagram-v2-GCMORJYK-DklkUxOg.js → stateDiagram-v2-GCMORJYK-MkzPgxAc.js} +1 -1
  52. package/dist/assets/{swimlanes-2SLR337P-CsjIU6be.js → swimlanes-2SLR337P-DnbaOEwH.js} +1 -1
  53. package/dist/assets/swimlanesDiagram-TC7HE7FX-BZQ07DGE.js +8 -0
  54. package/dist/assets/{timeline-definition-EJHVYXUP-BkFqK7AB.js → timeline-definition-EJHVYXUP-Bb4xG_Bt.js} +1 -1
  55. package/dist/assets/{usecaseDiagram-POWQR4AR-70gJB1EU.js → usecaseDiagram-POWQR4AR-JgkR9pFf.js} +1 -1
  56. package/dist/assets/{vennDiagram-UO4OBE2U-83Tm62dN.js → vennDiagram-UO4OBE2U-CIPIrAYe.js} +1 -1
  57. package/dist/assets/{wardleyDiagram-VNRHLVJA-tTmseoX9.js → wardleyDiagram-VNRHLVJA-BjCLuUvJ.js} +1 -1
  58. package/dist/assets/{xychartDiagram-PMCCYNJV-R1LBA55a.js → xychartDiagram-PMCCYNJV-DKgiaYxT.js} +1 -1
  59. package/dist/index.html +2 -2
  60. package/dist-server/cloud-home-start.js +14795 -0
  61. package/dist-server/container-mcp.js +52 -4
  62. package/dist-server/drivers/agents-proxy.js +20 -10
  63. package/dist-server/index.js +8985 -6461
  64. package/dist-server/openmausbot.js +327 -145
  65. package/dist-server/pair-cli.js +327 -145
  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} +330 -328
  71. package/dist-server/server/bot-profile.js +7 -1
  72. package/dist-server/server/channel-queue.js +58 -18
  73. package/dist-server/server/checkpoints.js +98 -52
  74. package/dist-server/server/claim-idle.js +33 -0
  75. package/dist-server/server/cloud-backend.js +5 -5
  76. package/dist-server/server/cloud-home-start.js +103 -0
  77. package/dist-server/server/cloud-home.js +218 -0
  78. package/dist-server/server/cloud-overflow.js +167 -0
  79. package/dist-server/server/composio.js +5 -0
  80. package/dist-server/server/computer-observation.js +2 -2
  81. package/dist-server/server/config.js +115 -6
  82. package/dist-server/server/container-computer.js +13 -0
  83. package/dist-server/server/control-client.js +2 -2
  84. package/dist-server/server/delegations.js +11 -3
  85. package/dist-server/server/drivers/acp/core.js +5 -4
  86. package/dist-server/server/drivers/agents-call.js +5 -1
  87. package/dist-server/server/drivers/agents-catalog.js +9 -3
  88. package/dist-server/server/drivers/{boxagent.js → boatagent.js} +13 -12
  89. package/dist-server/server/drivers/builtIn.js +2 -2
  90. package/dist-server/server/drivers/{chat-box-tools.js → chat-boat-tools.js} +3 -3
  91. package/dist-server/server/drivers/chat-mcp-tools.js +2 -2
  92. package/dist-server/server/drivers/claude.js +177 -13
  93. package/dist-server/server/drivers/native.js +1 -1
  94. package/dist-server/server/drivers/openai-chat.js +15 -4
  95. package/dist-server/server/drivers/pi.js +3 -3
  96. package/dist-server/server/hosted-models.js +6 -1
  97. package/dist-server/server/index.js +1029 -479
  98. package/dist-server/server/local-vm-inventory.js +11 -1
  99. package/dist-server/server/local-vm-seat-pool.js +141 -0
  100. package/dist-server/server/managed-policy.js +4 -1
  101. package/dist-server/server/memory-capture.js +194 -0
  102. package/dist-server/server/memory-entries.js +76 -0
  103. package/dist-server/server/memory-organize.js +102 -0
  104. package/dist-server/server/memory-tidy.js +144 -0
  105. package/dist-server/server/memory-topics.js +169 -0
  106. package/dist-server/server/memory-upkeep.js +452 -0
  107. package/dist-server/server/message-db.js +49 -5
  108. package/dist-server/server/permission-proxy.js +3 -3
  109. package/dist-server/server/profile-learned.js +98 -0
  110. package/dist-server/server/profile-versions.js +45 -11
  111. package/dist-server/server/recall.js +172 -0
  112. package/dist-server/server/redact.js +1 -1
  113. package/dist-server/server/room-cwd.js +1 -1
  114. package/dist-server/server/room-handoffs.js +61 -6
  115. package/dist-server/server/routes/bot-memory.js +215 -0
  116. package/dist-server/server/routine-requests.js +27 -16
  117. package/dist-server/server/routines.js +10 -6
  118. package/dist-server/server/steer-queue.js +41 -10
  119. package/dist-server/server/store.js +25 -1
  120. package/dist-server/server/surface.js +1 -1
  121. package/dist-server/server/system-prompt.js +5 -5
  122. package/dist-server/server/team-computers.js +2 -2
  123. package/dist-server/server/tightening-requests.js +34 -4
  124. package/dist-server/server/tool-summary.js +2 -2
  125. package/dist-server/server/tts/elevenlabs.js +1 -1
  126. package/dist-server/server/turn-resources.js +87 -11
  127. package/dist-server/server/vps-computer.js +1 -1
  128. package/dist-server/server/workspace.js +57 -22
  129. package/dist-server/shared/ask-question.js +7 -5
  130. package/dist-server/shared/bot-avatar.js +19 -0
  131. package/dist-server/shared/credential-request.js +3 -3
  132. package/dist-server/shared/tool-surface.js +1 -1
  133. package/dist-server/src/lib/composer-attachments.js +9 -0
  134. package/dist-server/vps-container-mcp.js +52 -4
  135. package/dist-server/workspace-backup.worker.js +52 -4
  136. package/package.json +1 -1
  137. package/dist/assets/channel-DqY_gMLg.js +0 -1
  138. package/dist/assets/index-BIMdIbaw.css +0 -1
  139. package/dist/assets/index-BVPdbq2r.js +0 -583
  140. package/dist/assets/swimlanesDiagram-TC7HE7FX-Bk14M0_B.js +0 -8
@@ -69,6 +69,19 @@ export function perBotLocalVmTarget(botId) {
69
69
  label: digest,
70
70
  };
71
71
  }
72
+ /** A pool-mode seat (issue #1654): one of N desktops shared by every
73
+ * conversation, addressed by seat index. Identities stay disjoint from the
74
+ * shared singleton and from per-bot digests, so discovery, labels, and lease
75
+ * lanes never confuse the three modes. */
76
+ export function poolLocalVmTarget(seat) {
77
+ return {
78
+ key: `pool:${seat}`,
79
+ containerName: `${CONTAINER}-p${seat}`,
80
+ workspaceDir: join(DATA_DIR, "vm-homes", `pool-${seat}`),
81
+ viewerPort: null,
82
+ label: `pool-${seat}`,
83
+ };
84
+ }
72
85
  /** Only provisioning creates this durable directory; idle removal keeps it. */
73
86
  export function localVmWorkspaceExists(target) {
74
87
  try {
@@ -1,8 +1,8 @@
1
1
  // The proxy-side half of computer control. The harness keeps the record of
2
2
  // who is driving (server/computer-control.ts); the per-turn computer
3
3
  // processes consult it through this client before acting, because the
4
- // action paths themselves never traverse the harness — a box click goes
5
- // straight to the box's REST API, and a Local VM / VPS click rides a
4
+ // action paths themselves never traverse the harness — a boat click goes
5
+ // straight to the boat's REST API, and a Local VM / VPS click rides a
6
6
  // transparent stdio bridge into Cua Driver.
7
7
  //
8
8
  // Failure posture: CLOSED once configured. A missing/expired turn capability
@@ -259,8 +259,10 @@ export function pendingDelegationSnapshot() {
259
259
  })));
260
260
  }
261
261
  /** How many handoffs one turn may queue. Small on purpose: this is the only
262
- * thing standing between a confused bot and a fan-out of real turns. */
263
- const MAX_QUEUED_PER_THREAD = 4;
262
+ * thing standing between a confused bot and a fan-out of real turns. Six,
263
+ * not four: a lead running a weekly check-in over a five-member team is the
264
+ * ordinary case, and hitting the cap there silently dropped one teammate. */
265
+ const MAX_QUEUED_PER_THREAD = 6;
264
266
  /** Validate and enqueue a delegation. Pushes a "Delegated to @B: reason"
265
267
  * chip to the source thread so the user can see what was queued. */
266
268
  export function queueDelegation(bus, from, item, maxDepth, sourceThreadId = from.threadId) {
@@ -838,7 +840,13 @@ export function buildDelegationFailurePrompt(targetName, reason) {
838
840
  "Take over: tell the user what failed in plain terms, then decide the next step — retry with a narrower task, do the work yourself, or propose an alternative. Do not re-delegate the exact same task unchanged.",
839
841
  ].join("\n\n");
840
842
  }
841
- export const DELEGATION_WAKE_MAX_PER_WINDOW = 3;
843
+ // Must not be smaller than MAX_QUEUED_PER_THREAD: every delegate reply is
844
+ // one wake, so a fan-out that the queue cap allows has to be resumable
845
+ // within the window. Three failed a five-member weekly check-in whose
846
+ // replies all landed inside four minutes ("Delegation follow-up limit
847
+ // reached"), while the same routine passed a week earlier only because the
848
+ // replies happened to spread over twenty.
849
+ export const DELEGATION_WAKE_MAX_PER_WINDOW = 6;
842
850
  export const DELEGATION_WAKE_WINDOW_MS = 5 * 60 * 1000;
843
851
  /** Bounded auto-wake budget per source thread. A delegation completion
844
852
  * wakes the source; if that source re-delegates and the new completion
@@ -43,7 +43,7 @@ export function skipSubscriptionAuthForLocalInject(model) {
43
43
  import { newEventId, newId, TurnNotStartedError } from "../../contracts.js";
44
44
  import { augmentedPath } from "../../env-path.js";
45
45
  import { supportsApprovalMode } from "../../../shared/approval-mode.js";
46
- import { parseAskQuestions, parseChoices, questionAnswersByQuestion } from "../../../shared/ask-question.js";
46
+ import { MAX_QUESTION_TEXT, parseAskQuestions, parseChoices, questionAnswersByQuestion } from "../../../shared/ask-question.js";
47
47
  import { appendNative } from "../native.js";
48
48
  import { acpPermissionCommand, permissionLaunchCwd } from "../permission-command.js";
49
49
  import { commandSummary, toolDetailPreview } from "../../tool-summary.js";
@@ -275,7 +275,7 @@ export function createAcpDriver(support) {
275
275
  };
276
276
  const allowedCredentials = new Set(support.credentialEnv ?? []);
277
277
  // two lists, one rule: foreign PROVIDER keys must not flip a CLI's
278
- // billing off its own login, and WORKSPACE credentials (box token,
278
+ // billing off its own login, and WORKSPACE credentials (boat token,
279
279
  // voice key, …) are the harness's secrets — riding along in
280
280
  // `...process.env` is not a grant. A driver keeps only what its
281
281
  // credentialEnv allowlist names.
@@ -439,7 +439,7 @@ export function createAcpDriver(support) {
439
439
  }
440
440
  // The bot's computer, mounted exactly like the Claude driver does:
441
441
  // host and sandbox Cua connections expose Cua Driver's own MCP server.
442
- // (A cloud box is not mounted here at all: a cloud turn runs ON the box.)
442
+ // (A cloud boat is not mounted here at all: a cloud turn runs ON the boat.)
443
443
  if (turn.integrations?.localComputer) {
444
444
  const local = turn.integrations.localComputer;
445
445
  servers.push({
@@ -722,7 +722,8 @@ export function createAcpDriver(support) {
722
722
  }
723
723
  const tool = kind === "execute" ? "shell" : kind === "edit" ? "edit" : kind || "tool";
724
724
  const isShellCommand = !isQuestion && kind === "execute" && !/^mcp(?:__|[.:])/i.test(String(toolCall.title ?? ""));
725
- const summary = String(toolCall.rawInput?.command ?? toolCall.title ?? tool).slice(0, 200);
725
+ const rawSummary = String(toolCall.rawInput?.command ?? toolCall.title ?? tool);
726
+ const summary = rawSummary.slice(0, isQuestion ? MAX_QUESTION_TEXT : 200);
726
727
  // One structured question beside the flat choices: the richer card
727
728
  // renders from it while older clients keep answering through
728
729
  // `choices`. Built once here so the emit and the answer path can
@@ -278,7 +278,7 @@ function routineFields(args) {
278
278
  const runOn = destination(args.run_on ?? args.runOn);
279
279
  const timeoutMinutes = args.timeout_minutes ?? args.timeoutMinutes;
280
280
  if (runOn != null && runOn !== "maus" && runOn !== "cloud") {
281
- 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 Box-hosted agent. Legacy "cloud" also means Box.' };
281
+ 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 Boat-hosted agent. Legacy "cloud" also means Boat.' };
282
282
  }
283
283
  if (timeoutMinutes != null && (typeof timeoutMinutes !== "number" || !Number.isInteger(timeoutMinutes) || timeoutMinutes < 5 || timeoutMinutes > 240)) {
284
284
  return { fields, error: "timeout_minutes must be a whole number from 5 to 240. Use clear_timeout to remove a limit." };
@@ -982,11 +982,14 @@ export async function callTool(name, args, context) {
982
982
  if (!routineId || !action) {
983
983
  return { text: "propose_routine_action needs a routine_id and supported action.", isError: true };
984
984
  }
985
+ const forBotId = String(args.for_bot_id ?? "").trim();
985
986
  const body = {
986
987
  fromBotId: BOT_ID,
987
988
  fromThreadId: THREAD_ID,
988
989
  action,
989
990
  routineId,
991
+ // JSON.stringify drops the key entirely when no target was named
992
+ ...(forBotId ? { forBotId } : {}),
990
993
  };
991
994
  if (action === "update") {
992
995
  if (!jsonRecord(args.changes)) {
@@ -1098,6 +1101,7 @@ export async function callTool(name, args, context) {
1098
1101
  action: args.action,
1099
1102
  text: args.text,
1100
1103
  oldText: args.old_text,
1104
+ ...(typeof args.until === "string" && args.until.trim() ? { until: args.until.trim() } : {}),
1101
1105
  }),
1102
1106
  });
1103
1107
  if (r.error || r.ok !== true) {
@@ -128,8 +128,9 @@ const ROUTINE_FIELDS_SCHEMA = {
128
128
  schedule: ROUTINE_SCHEDULE_SCHEMA,
129
129
  run_on: {
130
130
  type: "string",
131
+ // "box" is Boat's historical run_on destination id (agents wire contract).
131
132
  enum: ["maus", "box"],
132
- description: "Default maus keeps the bot's selected model and configured computer, INCLUDING a self-hosted VPS. Omit this field for normal schedules. box explicitly switches the agent to the Box-hosted runner; it requires Box setup and is not the generic cloud/VPS option. Legacy cloud values from list_routines mean box, not VPS.",
133
+ description: "Default maus keeps the bot's selected model and configured computer, INCLUDING a self-hosted VPS. Omit this field for normal schedules. box explicitly switches the agent to the Boat-hosted runner; it requires Boat setup and is not the generic cloud/VPS option. Legacy cloud values from list_routines mean box, not VPS.",
133
134
  },
134
135
  timeout_minutes: {
135
136
  type: "integer",
@@ -293,7 +294,7 @@ const toolDefinitions = (externalRuntime) => [
293
294
  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.",
294
295
  inputSchema: { type: "object", additionalProperties: false, properties: {
295
296
  surface: { type: "string", enum: ["auto", "cloud", "vm", "local", "browser"],
296
- description: "auto = suitable configured computer, cloud = remote Box/VPS, vm = isolated Local VM, local = user's own desktop, browser = built-in browser. Omit to list." },
297
+ 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." },
297
298
  } },
298
299
  },
299
300
  {
@@ -523,6 +524,7 @@ const toolDefinitions = (externalRuntime) => [
523
524
  action: { type: "string", enum: ["append", "replace", "remove", "supersede"] },
524
525
  text: { type: "string", minLength: 1, description: "Non-blank new text for append, replace, or supersede: the fact itself, without a date or bullet. Omit for remove; use remove to delete a passage." },
525
526
  old_text: { type: "string", minLength: 1, description: "Exact unique existing passage for replace, supersede, or remove. Omit for append." },
527
+ until: { type: "string", pattern: "^\\d{4}-\\d{2}-\\d{2}$", description: "Optional, for append or supersede: YYYY-MM-DD, the last day a temporary fact holds (an exam this weekend, a trip next week). After that day the entry is hidden from your memory." },
526
528
  },
527
529
  required: ["action"],
528
530
  },
@@ -614,12 +616,16 @@ const toolDefinitions = (externalRuntime) => [
614
616
  },
615
617
  {
616
618
  name: "propose_routine_action",
617
- description: "Prepare a user-requested change to one of this bot's existing routines. Use list_routines first to get the routine id." + PROPOSAL_OUTCOME,
619
+ description: "Prepare a user-requested change to one of this bot's existing routines. Use list_routines first to get the routine id. If the user asks to change ANOTHER bot's routine and that bot is in your section, call list_bots and pass that bot's id as for_bot_id; the routine keeps its owner and every run keeps that bot's engine and permissions." + PROPOSAL_OUTCOME,
618
620
  inputSchema: {
619
621
  type: "object",
620
622
  additionalProperties: false,
621
623
  properties: {
622
624
  routine_id: { type: "string", minLength: 1, description: "Routine id from list_routines." },
625
+ for_bot_id: {
626
+ type: "string",
627
+ description: "Only when the requested change targets ANOTHER bot's routine and that bot is in your section: that bot's id from list_bots. Learn the routine id from that bot's own routines (it can run list_routines). Omit to change one of your own routines.",
628
+ },
623
629
  action: {
624
630
  type: "string",
625
631
  enum: ["update", "pause", "resume", "run_now", "delete"],
@@ -1,9 +1,10 @@
1
1
  import { newEventId, newId } from "../contracts.js";
2
2
  import { appendNative } from "./native.js";
3
3
  import { OMB_ASK_TOOL, answerWithoutPreamble, askQuestionSummary, capAnswerEcho, ombAskProtocolPrompt, parseOmbAskQuestions, questionChoices, stripOmbAskBlock, } from "../../shared/ask-question.js";
4
+ // "boxAgent" is Boat's historical driver kind (catalog and wire); keep it.
4
5
  const DRIVER_KIND = "boxAgent";
5
6
  // 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
+ const BOAT_API = process.env.OMB_BOX_API || "https://ascii.dev/api/box/v1";
7
8
  const MODELS = {
8
9
  default: "claude-fable-5",
9
10
  options: [
@@ -15,14 +16,14 @@ const MODELS = {
15
16
  /** Any fence whose info string names the ask protocol, even when its body
16
17
  * does not parse: the marker for "the model tried to ask and failed". */
17
18
  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,
19
+ /** The boat runs every harness boat.dev ships (claude-code, codex, pi, opencode,
19
20
  * prime-agent, kimi). Which one a model id belongs to comes from the public
20
21
  * catalog, `GET /api/provider-models` at the API root: an object keyed by
21
22
  * harness, each with its `models`. A bot that arrives here from another engine
22
23
  * carries that engine's model id, so this is what lets it keep its model. */
23
24
  let catalog = null;
24
25
  async function loadCatalog() {
25
- const root = BOX_API.replace(/\/api\/box\/v1\/?$/, "");
26
+ const root = BOAT_API.replace(/\/api\/box\/v1\/?$/, "");
26
27
  catalog = await fetch(`${root}/api/provider-models`, { signal: AbortSignal.timeout(15_000) })
27
28
  .then((res) => (res.ok ? res.json() : null))
28
29
  .catch(() => null);
@@ -44,7 +45,7 @@ function decodeConfig(raw) {
44
45
  ...(typeof o.askTimeoutMs === "number" ? { askTimeoutMs: o.askTimeoutMs } : {}),
45
46
  };
46
47
  }
47
- export const BoxAgentDriver = {
48
+ export const BoatAgentDriver = {
48
49
  driverKind: DRIVER_KIND,
49
50
  metadata: { displayName: "Computer", supportsMultipleInstances: false },
50
51
  models: MODELS,
@@ -74,7 +75,7 @@ export const BoxAgentDriver = {
74
75
  createdAt: new Date().toISOString(),
75
76
  });
76
77
  const api = async (path, opts = {}) => {
77
- const res = await fetch(`${BOX_API}${path}`, {
78
+ const res = await fetch(`${BOAT_API}${path}`, {
78
79
  ...opts,
79
80
  headers: { authorization: `Bearer ${token}`, "content-type": "application/json", ...opts.headers },
80
81
  signal: opts.signal ?? AbortSignal.timeout(30_000),
@@ -120,7 +121,7 @@ export const BoxAgentDriver = {
120
121
  });
121
122
  appendNative(threadId, { dir: "out", source: "box.prompt", msg: { model, prompt: promptText, response: started } });
122
123
  // 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
124
+ // id:<boat id>} — never fall back to the bare id, it's the boat's
124
125
  return started?.promptRun?.id ?? started?.prompt?.id ?? started?.promptId ?? null;
125
126
  };
126
127
  const promptId = await postPrompt(prompt);
@@ -149,7 +150,7 @@ export const BoxAgentDriver = {
149
150
  const seen = new Set();
150
151
  let lastText = "";
151
152
  let pendingText = "";
152
- /** Why the box could not answer (login expired, model refused, …). */
153
+ /** Why the boat could not answer (login expired, model refused, …). */
153
154
  let problem = null;
154
155
  /** Emit unflushed deltas as assistant_text and reset pendingText.
155
156
  * The omb-ask block is protocol, not prose: it streamed raw (the
@@ -229,7 +230,7 @@ export const BoxAgentDriver = {
229
230
  return { ok: ok && !cancelled, stopReason: cancelled ? "interrupted" : stopReason };
230
231
  }
231
232
  // 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
233
+ // same accounting, and the answer reaches the boat the way the ask
233
234
  // contract promised — Q:/A: blocks, capped for echo.
234
235
  const continuation = [
235
236
  capAnswerEcho(answerWithoutPreamble(reply)),
@@ -238,7 +239,7 @@ export const BoxAgentDriver = {
238
239
  ].join("\n");
239
240
  const nextPromptId = await postPrompt(continuation);
240
241
  // Stop can land while the continuation POST is in flight: the
241
- // interrupt inside cancel() then hits a box with no active run,
242
+ // interrupt inside cancel() then hits a boat with no active run,
242
243
  // and the continuation would start after it. Interrupt the run
243
244
  // that just started before ending the turn.
244
245
  if (cancelled) {
@@ -247,9 +248,9 @@ export const BoxAgentDriver = {
247
248
  }
248
249
  return await settleRun(nextPromptId);
249
250
  };
250
- /** Poll one box run to its settle. */
251
+ /** Poll one boat run to its settle. */
251
252
  const settleRun = async (runPromptId) => {
252
- const startedAt = Date.now(); // one 30-min ceiling per box run, ask chains included
253
+ const startedAt = Date.now(); // one 30-min ceiling per boat run, ask chains included
253
254
  lastText = "";
254
255
  pendingText = "";
255
256
  problem = null;
@@ -355,7 +356,7 @@ export const BoxAgentDriver = {
355
356
  };
356
357
  const snapshot = async () => {
357
358
  if (!token) {
358
- return { state: "unavailable", reason: 'no Box token — add {"box":{"token":"…"}} to ~/.openmausbot/config.json' };
359
+ return { state: "unavailable", reason: 'no Boat token — add {"box":{"token":"…"}} to ~/.openmausbot/config.json' };
359
360
  }
360
361
  try {
361
362
  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";
@@ -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.