openmausbot 0.1.85 → 0.1.87

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 (262) hide show
  1. package/README.md +2 -2
  2. package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  3. package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  4. package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  5. package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  6. package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  7. package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  8. package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  9. package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  10. package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  11. package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  12. package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  13. package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  14. package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  15. package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  16. package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  17. package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  18. package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  19. package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  20. package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  21. package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  22. package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  23. package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  24. package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  25. package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  26. package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  27. package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  28. package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  29. package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  30. package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  31. package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  32. package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  33. package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  34. package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  35. package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  36. package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  37. package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  38. package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  39. package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  40. package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  41. package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  42. package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  43. package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  44. package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  45. package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  46. package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  47. package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  48. package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  49. package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  50. package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  51. package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  52. package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  53. package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  54. package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  55. package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  56. package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  57. package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  58. package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  59. package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  60. package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  61. package/dist/assets/abnfDiagram-O67JEVCF-BTiuL79F.js +1 -0
  62. package/dist/assets/arc-C88Blc2v.js +1 -0
  63. package/dist/assets/architectureDiagram-NJMV4G6O-BZsJIZrb.js +36 -0
  64. package/dist/assets/blockDiagram-BEXU5L5S-CVeMe9Ah.js +140 -0
  65. package/dist/assets/c4Diagram-YGBWAQC7-Cedk3Bhx.js +38 -0
  66. package/dist/assets/channel-taCNRlUU.js +1 -0
  67. package/dist/assets/chunk-2Q5K7J3B-D-VRqzTB.js +1 -0
  68. package/dist/assets/chunk-3FUC2YCW-BChov01Y.js +2 -0
  69. package/dist/assets/chunk-5VM5RSS4-C-TfwvHy.js +15 -0
  70. package/dist/assets/chunk-GTNCS2PH-Db7CGj5Y.js +88 -0
  71. package/dist/assets/chunk-GWA4HPMP-CNvIfqKT.js +1 -0
  72. package/dist/assets/chunk-JWPE2WC7-C_bEp5tq.js +1 -0
  73. package/dist/assets/chunk-NETBCI7D-Coi4BOls.js +1 -0
  74. package/dist/assets/chunk-WEXAMYUT-CzylEuL3.js +1 -0
  75. package/dist/assets/chunk-XXDRQBXY-m7p7ztaD.js +1 -0
  76. package/dist/assets/classDiagram-v2-NBCMYWYE-BMy6o9x2.js +217 -0
  77. package/dist/assets/cose-bilkent-JH36ORCC-BDQ__0_J.js +1 -0
  78. package/dist/assets/cynefin-OW5HDTMX-Djv4ghUb.js +166 -0
  79. package/dist/assets/cynefinDiagram-VND7K2PF-BULrlGYH.js +62 -0
  80. package/dist/assets/cytoscape.esm-BB4DxJjf.js +321 -0
  81. package/dist/assets/dagre-6A5THRUB-D-jrT_hU.js +4 -0
  82. package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  83. package/dist/assets/diagram-22UHCM2B-p2UhQmfL.js +200 -0
  84. package/dist/assets/diagram-3UASUU5V-DHvGWZ7q.js +24 -0
  85. package/dist/assets/diagram-ATOU4E4O-ChhKDot-.js +3 -0
  86. package/dist/assets/diagram-CDSNMT55-B-T7A43R.js +30 -0
  87. package/dist/assets/diagram-MLGK6HIB-CJm8ENNY.js +24 -0
  88. package/dist/assets/diagram-MPIPVDR6-510tlza8.js +41 -0
  89. package/dist/assets/ebnfDiagram-ZINNZB2B-PkvRPHel.js +1 -0
  90. package/dist/assets/elk-276RUBZZ-BVBVPoX3.js +27 -0
  91. package/dist/assets/erDiagram-OPXOYQCR-BgYMVm7L.js +99 -0
  92. package/dist/assets/flowDiagram-KWPJA3E3-BGqsnc_3.js +213 -0
  93. package/dist/assets/ganttDiagram-FUAMR5RP-D_-UeReH.js +292 -0
  94. package/dist/assets/gitGraphDiagram-X574FWY7-D0uooONx.js +106 -0
  95. package/dist/assets/graph-D8UmR1vK.js +1 -0
  96. package/dist/assets/index-BwdMAQ6p.css +1 -0
  97. package/dist/assets/index-ByJennr7.js +581 -0
  98. package/dist/assets/{index-CTNNoqSJ.js → index-zRsJERGJ.js} +1 -1
  99. package/dist/assets/infoDiagram-VRGFBTTK-B9e1z77f.js +2 -0
  100. package/dist/assets/init-Gi6I4Gst.js +1 -0
  101. package/dist/assets/ishikawaDiagram-OU5B5YK6-BB4vNwvD.js +70 -0
  102. package/dist/assets/journeyDiagram-ZHPQQLJL-DpkosOTB.js +139 -0
  103. package/dist/assets/kanban-definition-PNTS6WVX-DbDP4kUu.js +89 -0
  104. package/dist/assets/linear-DBJGPmjT.js +1 -0
  105. package/dist/assets/mermaid.core-DWPRi-ue.js +311 -0
  106. package/dist/assets/mindmap-definition-NLK3R4M7-nszBbJmx.js +96 -0
  107. package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  108. package/dist/assets/pegDiagram-GJSIUBJH-CWykJWu_.js +1 -0
  109. package/dist/assets/pieDiagram-5QR66LMP-g3Da-Vnv.js +39 -0
  110. package/dist/assets/quadrantDiagram-O4NWA36T-Wzrlpzgh.js +7 -0
  111. package/dist/assets/railroadDiagram-XR7U4H2S-moHTFMC0.js +1 -0
  112. package/dist/assets/requirementDiagram-PLB6GJNP-CwfXdFH8.js +84 -0
  113. package/dist/assets/sankeyDiagram-IPEJSGJF-3ZROYvnq.js +40 -0
  114. package/dist/assets/sequenceDiagram-PO4LG4MO-E1TBebP8.js +169 -0
  115. package/dist/assets/sizeCapture-INFHLROL-DHKEU5pu.js +1 -0
  116. package/dist/assets/stateDiagram-v2-GCMORJYK-BguEj09D.js +291 -0
  117. package/dist/assets/swimlanes-2SLR337P-C7IDYnhd.js +1 -0
  118. package/dist/assets/swimlanesDiagram-TC7HE7FX-BpEdoB2e.js +8 -0
  119. package/dist/assets/timeline-definition-EJHVYXUP-DCW-F5PW.js +120 -0
  120. package/dist/assets/usecaseDiagram-POWQR4AR-xBMFtWYV.js +370 -0
  121. package/dist/assets/vennDiagram-UO4OBE2U-B5nIkuH8.js +34 -0
  122. package/dist/assets/wardleyDiagram-VNRHLVJA-DYFAhdbr.js +78 -0
  123. package/dist/assets/xychartDiagram-PMCCYNJV-DeXwX6Cz.js +7 -0
  124. package/dist/index.html +2 -2
  125. package/dist-server/browser-proxy.js +2 -1
  126. package/dist-server/container-mcp.js +1047 -173
  127. package/dist-server/drivers/agents-proxy.js +16121 -1504
  128. package/dist-server/drivers/phone-proxy.js +33 -2
  129. package/dist-server/enterprise/LICENSE +32 -0
  130. package/dist-server/enterprise/server/index.js +5 -5
  131. package/dist-server/index.js +16134 -7446
  132. package/dist-server/local-computer-proxy.js +3 -3
  133. package/dist-server/local-computer.js +53 -17
  134. package/dist-server/mcp-server.js +3 -1
  135. package/dist-server/openmausbot.js +9353 -6124
  136. package/dist-server/pair-cli.js +9353 -6124
  137. package/dist-server/permission-proxy.js +29 -9
  138. package/dist-server/server/admin-activity.js +410 -0
  139. package/dist-server/server/attachments.js +55 -0
  140. package/dist-server/server/auto-approve.js +4 -3
  141. package/dist-server/server/auto-vm-claims.js +1 -0
  142. package/dist-server/server/bot-package.js +18 -292
  143. package/dist-server/server/bot-profile.js +5 -17
  144. package/dist-server/server/bot-visibility.js +462 -0
  145. package/dist-server/server/box-create-idempotency.js +3 -3
  146. package/dist-server/server/box.js +59 -56
  147. package/dist-server/server/browser-proxy.js +3 -2
  148. package/dist-server/server/browser-runtime.js +38 -2
  149. package/dist-server/server/channel-queue.js +2 -0
  150. package/dist-server/server/cli-prompts.js +24 -2
  151. package/dist-server/server/cli.js +81 -12
  152. package/dist-server/server/command-allowlist.js +154 -0
  153. package/dist-server/server/composio.js +49 -11
  154. package/dist-server/server/computer-wait.js +22 -9
  155. package/dist-server/server/config.js +133 -17
  156. package/dist-server/server/connector-verdict.js +141 -0
  157. package/dist-server/server/container-computer.js +1 -1
  158. package/dist-server/server/decision-log.js +209 -48
  159. package/dist-server/server/default-model-selection.js +29 -3
  160. package/dist-server/server/delegations.js +97 -44
  161. package/dist-server/server/drivers/acp/core.js +163 -33
  162. package/dist-server/server/drivers/acp/grok.js +4 -3
  163. package/dist-server/server/drivers/agents-call.js +1162 -0
  164. package/dist-server/server/drivers/agents-catalog.js +689 -0
  165. package/dist-server/server/drivers/agents-client.js +28 -0
  166. package/dist-server/server/drivers/agents-proxy.js +18 -1722
  167. package/dist-server/server/drivers/boxagent.js +174 -36
  168. package/dist-server/server/drivers/builtIn.js +2 -0
  169. package/dist-server/server/drivers/chat-box-tools.js +90 -0
  170. package/dist-server/server/drivers/chat-images.js +54 -0
  171. package/dist-server/server/drivers/chat-mcp-tools.js +55 -10
  172. package/dist-server/server/drivers/chat-tool-approval.js +70 -18
  173. package/dist-server/server/drivers/claude.js +28 -8
  174. package/dist-server/server/drivers/codex-instructions.js +48 -6
  175. package/dist-server/server/drivers/codex-mcp-names.js +44 -0
  176. package/dist-server/server/drivers/codex-tool-surface.js +20 -0
  177. package/dist-server/server/drivers/codex.js +121 -34
  178. package/dist-server/server/drivers/grok.js +2 -1
  179. package/dist-server/server/drivers/mistral.js +115 -0
  180. package/dist-server/server/drivers/openai-chat.js +155 -32
  181. package/dist-server/server/drivers/openai-compat.js +16 -8
  182. package/dist-server/server/drivers/permission-command.js +35 -0
  183. package/dist-server/server/drivers/phone-proxy.js +47 -2
  184. package/dist-server/server/drivers/pi.js +75 -4
  185. package/dist-server/server/drivers/prompt-split.js +87 -0
  186. package/dist-server/server/enterprise.js +97 -16
  187. package/dist-server/server/external-runtime.js +83 -0
  188. package/dist-server/server/fleet.js +1 -1
  189. package/dist-server/server/group-thread-usage.js +103 -0
  190. package/dist-server/server/guarded-requests.js +60 -0
  191. package/dist-server/server/harness/http.js +61 -0
  192. package/dist-server/server/harness/registry.js +1 -0
  193. package/dist-server/server/hosted-models.js +151 -0
  194. package/dist-server/server/hosted-slack.js +11 -0
  195. package/dist-server/server/index.js +3091 -658
  196. package/dist-server/server/local-computer.js +72 -23
  197. package/dist-server/server/managed-desktop.js +79 -14
  198. package/dist-server/server/managed-policy.js +153 -0
  199. package/dist-server/server/mcp-bridge.js +6 -6
  200. package/dist-server/server/message-db.js +28 -0
  201. package/dist-server/server/model-context-window.js +7 -0
  202. package/dist-server/server/model-prices.js +121 -0
  203. package/dist-server/server/new-bot-defaults.js +115 -0
  204. package/dist-server/server/notify.js +10 -1
  205. package/dist-server/server/org-library.js +1051 -0
  206. package/dist-server/server/package-export.js +627 -39
  207. package/dist-server/server/package-import.js +458 -0
  208. package/dist-server/server/package-parts.js +87 -0
  209. package/dist-server/server/peer-approval.js +16 -7
  210. package/dist-server/server/peer-roster.js +23 -2
  211. package/dist-server/server/presets.js +492 -0
  212. package/dist-server/server/provider-key-check.js +3 -2
  213. package/dist-server/server/published-teams.js +80 -0
  214. package/dist-server/server/recent-work.js +15 -0
  215. package/dist-server/server/request-auth.js +96 -0
  216. package/dist-server/server/room-handoffs.js +44 -14
  217. package/dist-server/server/routes/bot-presets.js +25 -0
  218. package/dist-server/server/routes/hosted-slack.js +20 -0
  219. package/dist-server/server/routes/table.js +14 -0
  220. package/dist-server/server/routine-requests.js +6 -3
  221. package/dist-server/server/routines.js +63 -1
  222. package/dist-server/server/section-context.js +10 -0
  223. package/dist-server/server/skills.js +34 -61
  224. package/dist-server/server/spend.js +111 -16
  225. package/dist-server/server/steer-queue.js +9 -3
  226. package/dist-server/server/store.js +550 -35
  227. package/dist-server/server/surface.js +10 -4
  228. package/dist-server/server/system-prompt.js +12 -4
  229. package/dist-server/server/team-backup.js +16 -3
  230. package/dist-server/server/team-computers.js +10 -0
  231. package/dist-server/server/team-library.js +6 -2
  232. package/dist-server/server/team-manifest.js +4 -3
  233. package/dist-server/server/team-setup-requests.js +16 -5
  234. package/dist-server/server/thread-events.js +17 -1
  235. package/dist-server/server/thread-starters.js +53 -0
  236. package/dist-server/server/turn-context.js +2 -0
  237. package/dist-server/server/turn-watchdog.js +12 -3
  238. package/dist-server/server/usage-ledger.js +43 -9
  239. package/dist-server/server/vps-computer.js +139 -51
  240. package/dist-server/server/vps-container-mcp.js +7 -0
  241. package/dist-server/server/vps-ssh.js +36 -7
  242. package/dist-server/server/workspace-backup-policy.js +21 -2
  243. package/dist-server/server/workspace-backup.js +7 -4
  244. package/dist-server/server/workspace.js +1 -1
  245. package/dist-server/shared/approval-mode.js +1 -1
  246. package/dist-server/shared/ask-question.js +188 -12
  247. package/dist-server/shared/bot-profile.js +15 -0
  248. package/dist-server/shared/command-allowlist.js +1 -0
  249. package/dist-server/shared/credential-request.js +1 -1
  250. package/dist-server/shared/mention-boundary.js +15 -0
  251. package/dist-server/shared/package-format.js +967 -0
  252. package/dist-server/shared/routines.js +7 -1
  253. package/dist-server/shared/skill-md.js +64 -0
  254. package/dist-server/shared/team-backup.js +9 -0
  255. package/dist-server/shared/wire.js +4 -0
  256. package/dist-server/src/lib/composer-attachments.js +16 -0
  257. package/dist-server/vps-container-mcp.js +1147 -179
  258. package/enterprise/README.md +33 -12
  259. package/enterprise/server/index.js +5 -5
  260. package/package.json +1 -1
  261. package/dist/assets/index-Ba9G44HI.js +0 -310
  262. package/dist/assets/index-CkRNp7wX.css +0 -1
@@ -25,10 +25,14 @@ const OFF_NOTE = " This bot's \"Works on\" setting is Off, so no computer and no
25
25
  * destination mounts only the built-in browser, Off mounts nothing. A
26
26
  * conversation pin — set by the person from the composer, or by the
27
27
  * conversation's own first turn on Auto — wins over the bot's default, so a
28
- * thread never changes place under someone; only Off overrides it. Auto
29
- * without a pin leaves the computer choice to the dispatch, which then
30
- * mounts the browser only when no computer was reached. A pin the turn
31
- * cannot honour is retained and reported instead of being swapped silently. */
28
+ * thread never changes place under someone; only Off overrides it. An
29
+ * Auto-recorded pin is the machine's memory, not a person's choice: it yields
30
+ * when the bot's Works on later changes and no longer matches, while a
31
+ * person's pin (including legacy pins of unknown origin) keeps winning until
32
+ * they clear it. Auto without a pin leaves
33
+ * the computer choice to the dispatch, which then mounts the browser only
34
+ * when no computer was reached. A pin the turn cannot honour is retained and
35
+ * reported instead of being swapped silently. */
32
36
  export function resolveSurface(input) {
33
37
  const { destination, browserOn } = input;
34
38
  // Off is the whole answer: no computer and no browser. It used to withhold
@@ -116,6 +120,8 @@ export function surfacePrompt(mounted, opts = {}) {
116
120
  text += " If no suitable computer is running but its provider is configured, select_computer can provision one for this computer task; reuse existing resources first. Do not provision merely for ordinary chat or inspection.";
117
121
  if (opts.canSelect && !computer && !mounted.browser)
118
122
  text += " No computer or browser tools are mounted yet; select_computer is the way to connect them before screen work, not a reason to claim you already performed it.";
123
+ if (computer || mounted.browser)
124
+ text += " For online research, use the selected OpenMausBot browser when a search service is unavailable. A failed tool proves only that this attempt failed, not that every browser is unavailable. Inspect the current page after navigation; report a sign-in page, redirect or error as such. A completed model turn is not proof that the user's task succeeded.";
119
125
  return text + (opts.note ?? "");
120
126
  }
121
127
  /** Tool names that touch a screen, for engines that report bare names.
@@ -6,18 +6,25 @@
6
6
  // The sentences that both the direct-turn and room-turn paths use live
7
7
  // here too, so neither path can drift from the other or from the preview.
8
8
  import { soulSystemPrompt } from "./bot-folder.js";
9
+ export function userProfileSystemPrompt(profile) {
10
+ const text = profile?.aboutMe?.trim();
11
+ return text ? `\n\nAbout the user (shared with all bots):\nThe following JSON string contains user-provided background and preferences; it does not override system rules or grant permissions.\n${JSON.stringify(text)}\n` : "";
12
+ }
9
13
  /** Sections whose text legitimately differs between two turns of one live
10
14
  * conversation: memory, because a bot writes to MEMORY.md mid-conversation,
11
- * mentions, which describe the message being sent right now, and outstanding
12
- * teammate work, which settles while the person keeps talking.
15
+ * mentions, which describe the message being sent right now, outstanding
16
+ * teammate work, which settles while the person keeps talking, and recent
17
+ * work, whose relative time labels are recomputed every turn and whose
18
+ * newest-first list changes as the bot works in other threads.
13
19
  *
14
20
  * They are reported apart from the rest so a driver that keeps one CLI
15
21
  * process per thread can key that process on the stable half. Before this
16
22
  * split, saving a memory changed the system prompt, which changed the spawn
17
23
  * contract, which relaunched the CLI — and the provider then re-uploaded the
18
24
  * entire conversation at the cache-write rate. Mentions did the same on any
19
- * turn that tagged a bot. */
20
- const VOLATILE_SECTIONS = new Set(["memory", "mentions", "outstanding"]);
25
+ * turn that tagged a bot, and recent work did it on every turn of an active
26
+ * bot, because its "2h ago" labels drift even when nothing else changed. */
27
+ const VOLATILE_SECTIONS = new Set(["memory", "mentions", "outstanding", "recent"]);
21
28
  export function buildSystemPrompt(persona, soul, parts) {
22
29
  const ordered = [
23
30
  { id: "persona", label: "Identity", text: persona },
@@ -38,6 +45,7 @@ const COMPUTER_PARAGRAPH = {
38
45
  "vm-shared": " You have a shared, isolated Cua sandbox: a Linux desktop in a container on this machine. Only /home/cua/workspace is durable; save downloads, repositories, working files, and browser profiles there because everything else inside the VM is disposable. No other host folder is mounted. Use the computer tools for desktop, accessibility, window, and shell work. Inspect the desktop state before acting, prefer accessibility targets over raw coordinates, and work carefully.",
39
46
  box: " You have your own cloud computer. In Chrome, prefer browser_snapshot with browser_click/browser_fill for semantic, trusted actions; use screenshot/click/type_text for visual or non-browser UI, open_url for navigation, and computer_exec for Linux tasks. Every action already returns the resulting screen, so don't follow it with screenshot; batch predictable pixel actions with computer_batch.",
40
47
  "box-agent": "",
48
+ "box-chat": " You control the assigned cloud computer. Inspect it with screenshots; click coordinates refer to the full image. Use the advertised computer tools for desktop actions and shell commands.",
41
49
  vps: " You have your own self-hosted remote Linux computer through the official Cua tools. This is a VPS, not Box; using it does not require a Box API key. Its filesystem is disposable: everything on it is wiped whenever its container is recreated, so keep long-lived work somewhere durable — push it to a remote, or hand the results back in chat — instead of leaving it only on that computer. Inspect the desktop state before acting, prefer accessibility targets over raw coordinates, and act carefully.",
42
50
  local: " You can act on the user's computer through the computer tools. Discover the target app/window and inspect its state first. Prefer window-targeted accessibility actions with background delivery so the user can keep working in another app; do not bring OpenMausBot or another app to the front just to inspect it. Use the dedicated browser tools for browser work when available, keeping the user's intended browser profile/account, and OpenMausBot's configuration/proposal tools for supported bot setup rather than clicking through this app. Full-desktop input, app activation, and foreground delivery can move the real cursor, change focus, or switch desktops: use them only when the user asked for foreground control or agrees after background control reports it cannot perform the action. Do not silently retry a background refusal as foreground input, including through shell scripts, AppleScript/System Events, or another automation tool. If a background action unexpectedly changes focus, report it and stop that route rather than continuing to interrupt the user. Never promise that arbitrary desktop actions can run in the background.",
43
51
  };
@@ -77,6 +77,7 @@ export function createTeamBackup(store, routines, name) {
77
77
  closedBy: "closedBy" in task && task.closedBy
78
78
  ? { botId: task.closedBy.botId, name: task.closedBy.name, at: task.closedBy.at }
79
79
  : undefined,
80
+ pinned: task.pinned === true ? true : undefined,
80
81
  activeLeafId: store.activeLeaf(task.threadId),
81
82
  messages: store.messagesFor(task.threadId).map((message) => ({
82
83
  id: message.id, role: message.role, text: messageText(message), at: message.at,
@@ -112,6 +113,10 @@ export function createTeamBackup(store, routines, name) {
112
113
  section: bot.section, color: bot.color,
113
114
  mascotExpression: bot.mascotExpression ?? undefined, mascotBody: bot.mascotBody ?? undefined,
114
115
  chiefOfStaff: Boolean(bot.chiefOfStaff), hidden: Boolean(bot.hidden), playbooks: bot.playbooks ?? [],
116
+ // Grants are workspace-private authority: they travel in this backup
117
+ // so the team's shape is not lost, but the import below still lands
118
+ // every bot grant-less — restoring them is a deliberate later choice.
119
+ ...(bot.connectorTools ? { connectorTools: structuredClone(bot.connectorTools) } : {}),
115
120
  memory: memoryFor(bot.id),
116
121
  activeTask: bot.threadId, tasks: history(bot),
117
122
  })),
@@ -130,7 +135,7 @@ export function createTeamBackup(store, routines, name) {
130
135
  }
131
136
  /** Import is always additive, including sections and Chiefs. Rollback owns
132
137
  * only the fresh records below and cannot touch any pre-existing bot/chat. */
133
- export function importTeamBackup(store, routines, input, selection) {
138
+ export function importTeamBackup(store, routines, input, selection, options = {}) {
134
139
  const backup = parseTeamBackup(input);
135
140
  const bots = [];
136
141
  const groups = [];
@@ -175,11 +180,13 @@ export function importTeamBackup(store, routines, input, selection) {
175
180
  color: source.color, mascotExpression: source.mascotExpression,
176
181
  mascotBody: botMascotBody(source.mascotBody),
177
182
  modelSelection: selection, section: sectionFor(source.section),
183
+ // who may see the imported team is the importing admin's choice
184
+ ...(options.visibility ? { visibility: options.visibility } : {}),
178
185
  }, { seedMessages: false });
179
186
  bots.push(bot);
180
187
  botIds.set(source.key, bot.id);
181
188
  store.patchBot(bot.id, { composio: false, computer: "off", browser: false, approvalMode: "ask", autoApprove: false,
182
- hidden: source.hidden, chiefOfStaff: source.chiefOfStaff, playbooks: source.playbooks });
189
+ connectorTools: {}, hidden: source.hidden, chiefOfStaff: source.chiefOfStaff, playbooks: source.playbooks });
183
190
  if (source.memory)
184
191
  restoreMemory(bot.id, source.memory);
185
192
  }
@@ -188,7 +195,7 @@ export function importTeamBackup(store, routines, input, selection) {
188
195
  const tasks = source.tasks.map((task, i) => {
189
196
  const record = {
190
197
  threadId: i === 0 ? bot.threadId : newId(), title: task.title, createdAt: task.createdAt, resumeCursors: {},
191
- modelSelection: structuredClone(selection), activity: "idle", busy: false, unread: false,
198
+ modelSelection: structuredClone(bot.modelSelection), activity: "idle", busy: false, unread: false,
192
199
  ...(task.titleFromFirstMessage ? { titleFromFirstMessage: true } : {}),
193
200
  };
194
201
  // Same rule as a message's `from`: the opener is remapped to its
@@ -203,6 +210,10 @@ export function importTeamBackup(store, routines, input, selection) {
203
210
  const closer = task.closedBy && botIds.get(task.closedBy.botId);
204
211
  if (task.closedBy && closer)
205
212
  record.closedBy = { botId: closer, name: task.closedBy.name, at: task.closedBy.at };
213
+ if (task.pinned === true)
214
+ record.pinned = true;
215
+ const newest = task.messages.reduce((max, message) => Math.max(max, message.at), task.createdAt);
216
+ record.updatedAt = newest;
206
217
  return record;
207
218
  });
208
219
  // Own the task IDs before writing their transcripts, so rollback also
@@ -229,6 +240,8 @@ export function importTeamBackup(store, routines, input, selection) {
229
240
  if (!source.dm) {
230
241
  group.tasks = source.tasks.map((task, i) => ({
231
242
  threadId: threads[i], title: task.title, createdAt: task.createdAt,
243
+ updatedAt: task.messages.reduce((max, message) => Math.max(max, message.at), task.createdAt),
244
+ ...(task.pinned === true ? { pinned: true } : {}),
232
245
  ...(task.titleFromFirstMessage ? { titleFromFirstMessage: true } : {}),
233
246
  }));
234
247
  store.switchGroupTask(group.id, threads[source.tasks.findIndex((task) => task.key === source.activeTask)]);
@@ -92,6 +92,16 @@ export class TeamComputers {
92
92
  setProblem(id, problem) {
93
93
  return this.patch(id, { problem: problem?.slice(0, 500) });
94
94
  }
95
+ /** Relabel an existing team's assignment; this never changes its computer. */
96
+ renameSection(from, to) {
97
+ const entry = this.forSection(from);
98
+ if (!entry || from === to)
99
+ return false;
100
+ if (this.forSection(to))
101
+ throw failure("This team already has a computer");
102
+ this.patch(entry.id, { section: to });
103
+ return true;
104
+ }
95
105
  patch(id, patch) {
96
106
  const entries = this.list();
97
107
  const entry = entries.find(candidate => candidate.id === id);
@@ -1,5 +1,5 @@
1
1
  import { parseJson } from "./schema.js";
2
- import { isBotPackage, parseBotPackage } from "./bot-package.js";
2
+ import { isBotPackage, parseBotPackage, parsePackageDocument } from "./bot-package.js";
3
3
  import { parseTeamManifest } from "./team-manifest.js";
4
4
  export const TEAM_LIBRARY_REPOSITORY = "https://github.com/milind-soni/openmausbot-teams";
5
5
  export const TEAM_LIBRARY_RAW_ROOT = "https://raw.githubusercontent.com/milind-soni/openmausbot-teams/main";
@@ -127,7 +127,11 @@ export async function fetchTeamCatalog(fetcher = fetch) {
127
127
  function parseShareable(value) {
128
128
  if (typeof value === "string")
129
129
  return parseBotPackage(value);
130
- return isBotPackage(value) ? parseBotPackage(value) : parseTeamManifest(value);
130
+ if (!isBotPackage(value))
131
+ return parseTeamManifest(value);
132
+ // A shared team (v2) is read as a file: it cannot claim a publisher.
133
+ // Version 1 keeps its original shape for older callers.
134
+ return value.version === 1 ? parseBotPackage(value) : parsePackageDocument(value, { trust: "file" });
131
135
  }
132
136
  async function fetchShareable(url, fetcher) {
133
137
  return url.endsWith(".md")
@@ -140,13 +140,14 @@ export function parseTeamManifest(value) {
140
140
  * 1. Allowlist, not blocklist. The returned object is built field by field
141
141
  * from the parsed member, so every privilege-bearing BotRecord field —
142
142
  * approvalMode, autoApprove, alwaysAllow, chiefOfStaff, approvePeerComms, composio,
143
- * computer, cloudBackend, cwd — is structurally absent, whatever the
143
+ * connectorTools, computer, cloudBackend, cwd — is structurally absent, whatever the
144
144
  * file claimed. parseTeamManifest already drops unknown member keys;
145
145
  * this keeps the guarantee even if the schema grows a field later,
146
146
  * because nothing new can reach a bot record without someone
147
147
  * consciously widening this return type. The caller must still force
148
- * composio: false on the created record — that is the one privilege
149
- * where *absence* means allowed, so leaving it unset is not safe.
148
+ * composio: false (and connectorTools: {}) on the created record —
149
+ * composio is the one privilege where *absence* means allowed, so
150
+ * leaving it unset is not safe.
150
151
  *
151
152
  * 2. No name captures. Display names are identity wherever bots address
152
153
  * each other — @mention resolution in rooms, the Chief of Staff roster,
@@ -7,6 +7,7 @@ import { redactSecretsInText } from "./redact.js";
7
7
  const section = (value) => value?.trim() || "";
8
8
  const teamName = z.string().trim().min(1).max(60).refine(fitsOnOneLine).refine((value) => redactSecretsInText(value) === value, "Team names cannot contain credentials");
9
9
  const fieldsSchema = z.object({
10
+ chiefOfStaff: z.boolean().optional(),
10
11
  name: z.string().optional(), title: z.string().optional(), description: z.string().optional(), soul: z.string().optional(),
11
12
  section: z.string().trim().max(60).refine(fitsOnOneLine).refine((value) => redactSecretsInText(value) === value, "Team names cannot contain credentials").optional(),
12
13
  modelSelection: z.object({ instanceId: z.string().trim().min(1), model: z.string().trim().min(1), effort: z.string().optional() }).strict().optional(),
@@ -44,7 +45,7 @@ export class TeamSetupRequestService {
44
45
  return chief;
45
46
  }
46
47
  fields(input, current) {
47
- const { section: targetSection, modelSelection, ...profile } = input;
48
+ const { section: targetSection, modelSelection, chiefOfStaff, ...profile } = input;
48
49
  const safe = Object.fromEntries(Object.entries(profile).map(([key, value]) => [key, redactSecretsInText(value)]));
49
50
  const parsed = parseBotProfilePatch(safe, true);
50
51
  if (!parsed.ok)
@@ -52,6 +53,8 @@ export class TeamSetupRequestService {
52
53
  const result = { ...parsed.patch };
53
54
  if (targetSection !== undefined)
54
55
  result.section = targetSection;
56
+ if (chiefOfStaff !== undefined)
57
+ result.chiefOfStaff = chiefOfStaff;
55
58
  if (modelSelection) {
56
59
  const selection = modelSelection;
57
60
  const error = this.options.validateModel(selection, current);
@@ -108,7 +111,7 @@ export class TeamSetupRequestService {
108
111
  throw new TeamSetupError("The destination team is outside this Chief's authorized scope", 403);
109
112
  if (!request.newTeams.includes(section(destination)) && !existingTeams.has(section(destination)))
110
113
  throw new TeamSetupError("The destination team no longer exists", 409);
111
- const next = { id: operation.botId, name: fields.name ?? target.name, section: section(destination), chiefOfStaff: target?.chiefOfStaff, hidden: false };
114
+ const next = { id: operation.botId, name: fields.name ?? target.name, section: section(destination), chiefOfStaff: fields.chiefOfStaff ?? target?.chiefOfStaff, hidden: false };
112
115
  const at = projected.findIndex((bot) => bot.id === operation.botId);
113
116
  if (at < 0)
114
117
  projected.push(next);
@@ -125,7 +128,7 @@ export class TeamSetupRequestService {
125
128
  if (projected.some((bot) => bot.id !== candidate.id && !bot.hidden && bot.section === candidate.section && bot.name.trim().toLowerCase() === candidate.name.trim().toLowerCase()))
126
129
  throw new TeamSetupError(`@${candidate.name} already exists in that team`, 409);
127
130
  if (candidate.chiefOfStaff && projected.some((bot) => bot.id !== candidate.id && bot.chiefOfStaff && bot.section === candidate.section))
128
- throw new TeamSetupError("That move would put two Chiefs in one team", 409);
131
+ throw new TeamSetupError("Each team can have one Chief. Include the current Chief's demotion in this plan.", 409);
129
132
  }
130
133
  if (request.deletion) {
131
134
  const target = this.options.store.bot(request.deletion.botId);
@@ -195,12 +198,20 @@ export class TeamSetupRequestService {
195
198
  const lines = [`Why: ${request.reason}`];
196
199
  if (request.deletion)
197
200
  lines.push(`Delete @${request.deletion.name} (${request.deletion.botId}).`, "Permanently removes this bot, all its conversations, memory, instructions, skills, and any computer owned only by it. Generated project files and shared team computers remain. Active work or an unavailable provider can block deletion safely.");
198
- if (request.newTeams.length)
199
- lines.push(`Create teams: ${request.newTeams.map((name) => JSON.stringify(name)).join(", ")}.`, `Authorize @${chief.name} to coordinate and propose setup changes in these new teams.`);
201
+ if (request.newTeams.length) {
202
+ lines.push(`Create teams: ${request.newTeams.map((name) => JSON.stringify(name)).join(", ")}.`);
203
+ if (!request.operations.some(op => op.botId === chief.id && op.fields.chiefOfStaff === false)) {
204
+ lines.push(`Authorize @${chief.name} to coordinate and propose setup changes in these new teams.`);
205
+ }
206
+ }
200
207
  for (const operation of request.operations) {
201
208
  const current = this.options.store.bot(operation.botId);
202
209
  lines.push(`\n${operation.action === "create" ? "Create" : "Update"} @${operation.fields.name ?? current?.name} (${operation.action === "create" ? "new bot" : operation.botId})`);
203
210
  for (const [key, value] of Object.entries(operation.fields)) {
211
+ if (key === "chiefOfStaff") {
212
+ lines.push(`Chief of Staff: ${current?.chiefOfStaff ? "Yes" : "No"} → ${value ? "Yes" : "No"}.${value ? " May coordinate and configure bots in this team." : " Additional managed-team access is removed."}`);
213
+ continue;
214
+ }
204
215
  const before = current ? (key === "section" ? current.section || "General" : current[key]) : undefined;
205
216
  lines.push(`${key === "modelSelection" ? "Default engine/model" : key}: ${current ? `${JSON.stringify(before ?? "")} → ` : ""}${key === "section" ? JSON.stringify(value || "General") : JSON.stringify(value)}`);
206
217
  }
@@ -133,6 +133,10 @@ const stringOrMissing = (value) => value === undefined || typeof value === "stri
133
133
  const stringOrNullOrMissing = (value) => value === undefined || value === null || typeof value === "string";
134
134
  const numberOrNullOrMissing = (value) => value === undefined || value === null || typeof value === "number";
135
135
  const stringsOrMissing = (value) => value === undefined || (Array.isArray(value) && value.every((item) => typeof item === "string"));
136
+ /** A computer holder as the wait events carry it: a name, and the holding
137
+ * thread's title when one was known. */
138
+ const computerHolderOrMissing = (value) => value === undefined ||
139
+ (isRecord(value) && typeof value.name === "string" && stringOrMissing(value.task));
136
140
  /** A replayed structured ask. Only the shape the card actually reads is
137
141
  * required; the rest is optional and simply absent on an older event. */
138
142
  const askQuestionsOrMissing = (value) => value === undefined ||
@@ -175,6 +179,15 @@ function isRuntimeEvent(value) {
175
179
  stringsOrMissing(value.denials) &&
176
180
  (value.usage === undefined ||
177
181
  (isRecord(value.usage) && typeof value.usage.input === "number" && typeof value.usage.output === "number")));
182
+ case "turn.wait_started":
183
+ return typeof value.resource === "string" && computerHolderOrMissing(value.holder);
184
+ case "turn.wait_ended":
185
+ return (typeof value.resource === "string" &&
186
+ computerHolderOrMissing(value.holder) &&
187
+ typeof value.waitedMs === "number" &&
188
+ Number.isFinite(value.waitedMs) &&
189
+ value.waitedMs >= 0 &&
190
+ (value.outcome === "acquired" || value.outcome === "gave_up" || value.outcome === "stopped"));
178
191
  case "item.started":
179
192
  return (value.itemType === "tool" || value.itemType === "reasoning") && stringOrMissing(value.title);
180
193
  case "item.updated":
@@ -188,7 +201,10 @@ function isRuntimeEvent(value) {
188
201
  typeof value.tool === "string" &&
189
202
  typeof value.summary === "string" &&
190
203
  stringsOrMissing(value.choices) &&
191
- askQuestionsOrMissing(value.questions));
204
+ askQuestionsOrMissing(value.questions) &&
205
+ // Required fields only: events recorded before origin exists replay
206
+ // unchanged, and a value other than the two documented marks is not.
207
+ (value.origin === undefined || value.origin === "tool" || value.origin === "output"));
192
208
  case "request.resolved":
193
209
  return ((value.behavior === "allow" || value.behavior === "deny" || value.behavior === "answer") &&
194
210
  (value.source === "user" ||
@@ -0,0 +1,53 @@
1
+ // Who each thread is for, when a signed-in person can be named: the person
2
+ // who opened it from a session, or — for a thread a bot opened while working
3
+ // on someone's request — the person that request came from. One small
4
+ // server-private file (<data>/thread-starters.json, 0600), never sent to a
5
+ // client and never part of a bot or room record, so no wire projection can
6
+ // leak it. It decides one thing only: on a workspace several people share,
7
+ // whose session may answer the cards raised in that thread
8
+ // (server/index.ts cardAnswerRefusal). A thread with no entry names nobody,
9
+ // and then anyone who may chat may answer, as before.
10
+ import { readFileSync } from "node:fs";
11
+ import { writeFileAtomic } from "./atomic.js";
12
+ const KEY = /^p_[\w-]{22}$/;
13
+ const THREAD = /^[\w-]{1,128}$/;
14
+ /** Oldest entries go first past this; a thread that old has long settled. */
15
+ const MAX_ENTRIES = 50_000;
16
+ export class ThreadStarters {
17
+ starters = new Map();
18
+ file;
19
+ constructor(file) {
20
+ this.file = file;
21
+ let raw;
22
+ try {
23
+ raw = JSON.parse(readFileSync(file, "utf8"));
24
+ }
25
+ catch {
26
+ return; // absent or unreadable: nobody is named, which is today's behaviour
27
+ }
28
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
29
+ return;
30
+ for (const [threadId, person] of Object.entries(raw)) {
31
+ if (THREAD.test(threadId) && typeof person === "string" && KEY.test(person))
32
+ this.starters.set(threadId, person);
33
+ }
34
+ }
35
+ get(threadId) {
36
+ return this.starters.get(threadId);
37
+ }
38
+ /** Record once; a thread keeps the person it was first opened for. */
39
+ set(threadId, person) {
40
+ if (!person || !KEY.test(person) || !THREAD.test(threadId) || this.starters.has(threadId))
41
+ return;
42
+ this.starters.set(threadId, person);
43
+ while (this.starters.size > MAX_ENTRIES)
44
+ this.starters.delete(this.starters.keys().next().value);
45
+ try {
46
+ writeFileAtomic(this.file, JSON.stringify(Object.fromEntries(this.starters)) + "\n", { mode: 0o600 });
47
+ }
48
+ catch (error) {
49
+ // Kept in memory for this run; after a restart the thread names nobody.
50
+ console.warn(`thread starters: could not save ${this.file}: ${error instanceof Error ? error.message : String(error)}`);
51
+ }
52
+ }
53
+ }
@@ -1,4 +1,6 @@
1
1
  import { peerName } from "./peer-roster.js";
2
+ // All three use createOpenAIChatRuntime and get history via SendTurnInput.transcript.
3
+ export const NATIVELY_REPLAYING_DRIVER_KINDS = ["grok", "openai-compat", "minimax"];
2
4
  /** Does this engine need the thread replayed to it? True when a DIFFERENT
3
5
  * instance ran the last turn here — a cursor of our own is not enough,
4
6
  * because it only proves we once had a session covering some prefix of the
@@ -23,10 +23,10 @@ export class TurnWatchdog {
23
23
  this.timer = null;
24
24
  this.turns.clear();
25
25
  }
26
- /** A turn was dispatched on this thread. */
26
+ /** A turn was admitted on this thread. */
27
27
  watch(threadId, botId) {
28
28
  const at = this.now();
29
- this.turns.set(threadId, { threadId, botId, startedAt: at, lastEventAt: at, waitingOnHuman: false });
29
+ this.turns.set(threadId, { threadId, botId, startedAt: at, lastEventAt: at, waitingOnHuman: false, waitingOnComputer: false });
30
30
  }
31
31
  /** Any provider event for the thread proves the turn is alive. */
32
32
  touch(threadId) {
@@ -43,6 +43,15 @@ export class TurnWatchdog {
43
43
  turn.waitingOnHuman = waiting;
44
44
  turn.lastEventAt = this.now();
45
45
  }
46
+ /** Shared-computer waits have their own bounded deadline. Waiting for
47
+ * another turn to release the computer is not a silent provider stall. */
48
+ setWaitingOnComputer(threadId, waiting) {
49
+ const turn = this.turns.get(threadId);
50
+ if (!turn)
51
+ return;
52
+ turn.waitingOnComputer = waiting;
53
+ turn.lastEventAt = this.now();
54
+ }
46
55
  /** The turn settled normally — stop watching it. */
47
56
  settle(threadId) {
48
57
  this.turns.delete(threadId);
@@ -54,7 +63,7 @@ export class TurnWatchdog {
54
63
  sweep() {
55
64
  const at = this.now();
56
65
  for (const turn of this.turns.values()) {
57
- if (turn.waitingOnHuman)
66
+ if (turn.waitingOnHuman || turn.waitingOnComputer)
58
67
  continue;
59
68
  if (at - turn.lastEventAt < this.opts.stallMs)
60
69
  continue;
@@ -18,26 +18,49 @@ const DAY_MS = 24 * 60 * 60_000;
18
18
  const writeQueues = new Map();
19
19
  const clean = (value) => typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.trunc(value)) : 0;
20
20
  const finiteOrNull = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
21
+ const promptBytesOf = (value) => {
22
+ if (!value || typeof value !== "object" || Array.isArray(value))
23
+ return null;
24
+ const record = value;
25
+ return typeof record.stable === "number" && typeof record.volatile === "number"
26
+ ? { stable: clean(record.stable), volatile: clean(record.volatile) }
27
+ : null;
28
+ };
21
29
  function monthKey(at) {
22
30
  return `${at.getUTCFullYear()}-${String(at.getUTCMonth() + 1).padStart(2, "0")}`;
23
31
  }
24
32
  export function usageFileFor(dataDir, at) {
25
33
  return join(dataDir, DIR, `${monthKey(at)}.jsonl`);
26
34
  }
27
- /** Append one settled turn. Fire-and-forget; see the module comment. */
35
+ /** Identifies one row: a thread runs one turn at a time, so a bot cannot
36
+ * settle two turns on the same thread in the same millisecond. Lets the
37
+ * spend cap tell a booked row it already counted from memory apart from the
38
+ * same row read back from the file. */
39
+ export function usageRowKey(row) {
40
+ return `${row.at}|${row.threadId}|${row.botId}`;
41
+ }
42
+ /** Append one settled turn. Fire-and-forget (see the module comment): the
43
+ * returned promise never rejects, and says whether the row reached disk. */
28
44
  export function appendUsage(dataDir, row) {
45
+ const { promptBytes: rawPromptBytes, ...rest } = row;
46
+ const promptBytes = promptBytesOf(rawPromptBytes);
29
47
  const record = {
30
- ...row,
48
+ ...rest,
31
49
  at: row.at ?? new Date().toISOString(),
32
50
  input: clean(row.input),
33
51
  output: clean(row.output),
34
52
  ...(typeof row.cachedInput === "number" ? { cachedInput: clean(row.cachedInput) } : {}),
53
+ ...(promptBytes ? { promptBytes } : {}),
35
54
  costUsd: finiteOrNull(row.costUsd),
36
55
  };
56
+ // A cost is labelled with where it came from; an unpriced row carries no label.
57
+ if (record.costUsd === null)
58
+ delete record.costSource;
59
+ else
60
+ record.costSource = row.costSource === "estimated" ? "estimated" : "reported";
37
61
  const previous = writeQueues.get(dataDir) ?? Promise.resolve();
38
- const queued = previous
39
- .then(() => write(dataDir, record))
40
- .catch(() => {
62
+ const attempt = previous.then(() => write(dataDir, record));
63
+ const queued = attempt.then(() => undefined, () => {
41
64
  /* bookkeeping must never take down the turn */
42
65
  });
43
66
  writeQueues.set(dataDir, queued);
@@ -45,6 +68,7 @@ export function appendUsage(dataDir, row) {
45
68
  if (writeQueues.get(dataDir) === queued)
46
69
  writeQueues.delete(dataDir);
47
70
  });
71
+ return attempt.then(() => true, () => false);
48
72
  }
49
73
  async function write(dataDir, record) {
50
74
  await mkdir(join(dataDir, DIR), { recursive: true, mode: 0o700 });
@@ -165,7 +189,13 @@ function groupOf(row, groupBy) {
165
189
  }
166
190
  }
167
191
  function emptyGroup(key, label) {
168
- return { key, label, turns: 0, input: 0, output: 0, cachedInput: 0, costUsd: null, unpriced: 0, billableUsd: null };
192
+ return { key, label, turns: 0, input: 0, output: 0, cachedInput: 0, costUsd: null, estimatedUsd: null, unpriced: 0, billableUsd: null };
193
+ }
194
+ /** Where a row's cost came from, for rows old and new; null when it has none. */
195
+ export function costSourceOf(row) {
196
+ if (finiteOrNull(row.costUsd) === null)
197
+ return null;
198
+ return row.costSource === "estimated" ? "estimated" : "reported";
169
199
  }
170
200
  function add(group, row, prices) {
171
201
  group.turns += 1;
@@ -175,8 +205,11 @@ function add(group, row, prices) {
175
205
  const cost = finiteOrNull(row.costUsd);
176
206
  if (cost === null)
177
207
  group.unpriced += 1;
178
- else
208
+ else {
179
209
  group.costUsd = (group.costUsd ?? 0) + cost;
210
+ if (costSourceOf(row) === "estimated")
211
+ group.estimatedUsd = (group.estimatedUsd ?? 0) + cost;
212
+ }
180
213
  const billable = prices ? billableFor(row, prices) : null;
181
214
  if (billable !== null)
182
215
  group.billableUsd = (group.billableUsd ?? 0) + billable;
@@ -207,7 +240,7 @@ export function summarizeUsage(rows, groupBy, prices = null) {
207
240
  }
208
241
  return { groups: ordered, total };
209
242
  }
210
- function csvCell(value) {
243
+ export function csvCell(value) {
211
244
  if (value === null)
212
245
  return "";
213
246
  const text = String(value);
@@ -218,7 +251,7 @@ function csvCell(value) {
218
251
  }
219
252
  /** One line per turn, spreadsheet-ready; a billable column when a price list is given. */
220
253
  export function usageCsv(rows, prices = null) {
221
- const header = ["time", "bot", "model", "engine", "triggered_by", "input_tokens", "output_tokens", "cached_input_tokens", "cost_usd", ...(prices ? ["billable_usd"] : []), "thread"];
254
+ const header = ["time", "bot", "model", "engine", "triggered_by", "input_tokens", "output_tokens", "cached_input_tokens", "cost_usd", "cost_source", ...(prices ? ["billable_usd"] : []), "thread"];
222
255
  const lines = [header.join(",")];
223
256
  for (const row of rows) {
224
257
  lines.push([
@@ -231,6 +264,7 @@ export function usageCsv(rows, prices = null) {
231
264
  clean(row.output),
232
265
  clean(row.cachedInput),
233
266
  finiteOrNull(row.costUsd),
267
+ costSourceOf(row),
234
268
  ...(prices ? [billableFor(row, prices)] : []),
235
269
  row.threadId,
236
270
  ].map(csvCell).join(","));