openmausbot 0.1.84 → 0.1.86

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 (230) hide show
  1. package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  2. package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  3. package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  4. package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  5. package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  6. package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  7. package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  8. package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  9. package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  10. package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  11. package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  12. package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  13. package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  14. package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  15. package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  16. package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  17. package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  18. package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  19. package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  20. package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  21. package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  22. package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  23. package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  24. package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  25. package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  26. package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  27. package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  28. package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  29. package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  30. package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  31. package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  32. package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  33. package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  34. package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  35. package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  36. package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  37. package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  38. package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  39. package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  40. package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  41. package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  42. package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  43. package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  44. package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  45. package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  46. package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  47. package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  48. package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  49. package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  50. package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  51. package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  52. package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  53. package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  54. package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  55. package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  56. package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  57. package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  58. package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  59. package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  60. package/dist/assets/abnfDiagram-O67JEVCF-DpUZNnFD.js +1 -0
  61. package/dist/assets/arc-DW7mrI4p.js +1 -0
  62. package/dist/assets/architectureDiagram-NJMV4G6O-CyeZ5WLZ.js +36 -0
  63. package/dist/assets/blockDiagram-BEXU5L5S-CBVaNjO7.js +140 -0
  64. package/dist/assets/c4Diagram-YGBWAQC7-sf8d41uT.js +38 -0
  65. package/dist/assets/channel-BXFkgus9.js +1 -0
  66. package/dist/assets/chunk-2Q5K7J3B-BOGvy7Y_.js +1 -0
  67. package/dist/assets/chunk-3FUC2YCW-BiF9zyiz.js +2 -0
  68. package/dist/assets/chunk-5VM5RSS4-EFuZ1SBZ.js +15 -0
  69. package/dist/assets/chunk-GTNCS2PH-pJLNSAT7.js +88 -0
  70. package/dist/assets/chunk-GWA4HPMP-Cb-3_3IT.js +1 -0
  71. package/dist/assets/chunk-JWPE2WC7-DAf5aogn.js +1 -0
  72. package/dist/assets/chunk-NETBCI7D-DR3kiwZz.js +1 -0
  73. package/dist/assets/chunk-WEXAMYUT-BmU8PeyM.js +1 -0
  74. package/dist/assets/chunk-XXDRQBXY-Buh8g8OT.js +1 -0
  75. package/dist/assets/classDiagram-v2-NBCMYWYE-BurfmMWM.js +217 -0
  76. package/dist/assets/cose-bilkent-JH36ORCC-KkGZUEkU.js +1 -0
  77. package/dist/assets/cynefin-OW5HDTMX-C3PpV96d.js +166 -0
  78. package/dist/assets/cynefinDiagram-VND7K2PF-Dt8JwJaI.js +62 -0
  79. package/dist/assets/cytoscape.esm-BB4DxJjf.js +321 -0
  80. package/dist/assets/dagre-6A5THRUB-DkjN8266.js +4 -0
  81. package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  82. package/dist/assets/diagram-22UHCM2B-DUAJS0JV.js +200 -0
  83. package/dist/assets/diagram-3UASUU5V-Dw4KhF1a.js +24 -0
  84. package/dist/assets/diagram-ATOU4E4O-Dd1gmQqf.js +3 -0
  85. package/dist/assets/diagram-CDSNMT55-CjwGs0Yw.js +30 -0
  86. package/dist/assets/diagram-MLGK6HIB-BL-69Zrq.js +24 -0
  87. package/dist/assets/diagram-MPIPVDR6-5Zi2N7wo.js +41 -0
  88. package/dist/assets/ebnfDiagram-ZINNZB2B-D9NekWUX.js +1 -0
  89. package/dist/assets/elk-276RUBZZ-CkaSQjxw.js +27 -0
  90. package/dist/assets/erDiagram-OPXOYQCR-CdAVBV8-.js +99 -0
  91. package/dist/assets/flowDiagram-KWPJA3E3-Bm_lWWH7.js +213 -0
  92. package/dist/assets/ganttDiagram-FUAMR5RP-FZ8ejYWJ.js +292 -0
  93. package/dist/assets/gitGraphDiagram-X574FWY7-Dlv9rizZ.js +106 -0
  94. package/dist/assets/graph-D8UmR1vK.js +1 -0
  95. package/dist/assets/index-BGgqlWPJ.css +1 -0
  96. package/dist/assets/index-BtHo8ZKl.js +577 -0
  97. package/dist/assets/{index-DNa2umw-.js → index-h9FFNwTu.js} +1 -1
  98. package/dist/assets/infoDiagram-VRGFBTTK-BdQ_gW5B.js +2 -0
  99. package/dist/assets/init-Gi6I4Gst.js +1 -0
  100. package/dist/assets/ishikawaDiagram-OU5B5YK6-EwssEp2X.js +70 -0
  101. package/dist/assets/journeyDiagram-ZHPQQLJL-BX1G1vOd.js +139 -0
  102. package/dist/assets/kanban-definition-PNTS6WVX-BB4kAsyC.js +89 -0
  103. package/dist/assets/linear-3su2TG93.js +1 -0
  104. package/dist/assets/mermaid.core-CDD1E0s9.js +311 -0
  105. package/dist/assets/mindmap-definition-NLK3R4M7-Ba50MsNK.js +96 -0
  106. package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  107. package/dist/assets/pegDiagram-GJSIUBJH-DZLkkZlM.js +1 -0
  108. package/dist/assets/pieDiagram-5QR66LMP-DaTz77Uq.js +39 -0
  109. package/dist/assets/quadrantDiagram-O4NWA36T-CpaiidpR.js +7 -0
  110. package/dist/assets/railroadDiagram-XR7U4H2S-mN4JrMeI.js +1 -0
  111. package/dist/assets/requirementDiagram-PLB6GJNP-BSNDRpg7.js +84 -0
  112. package/dist/assets/sankeyDiagram-IPEJSGJF-FngzGNug.js +40 -0
  113. package/dist/assets/sequenceDiagram-PO4LG4MO-CM4pCC3L.js +169 -0
  114. package/dist/assets/sizeCapture-INFHLROL-CRICHXkw.js +1 -0
  115. package/dist/assets/stateDiagram-v2-GCMORJYK-oyTWpxH2.js +291 -0
  116. package/dist/assets/swimlanes-2SLR337P-D9h9mPBq.js +1 -0
  117. package/dist/assets/swimlanesDiagram-TC7HE7FX-BWrvB2ak.js +8 -0
  118. package/dist/assets/timeline-definition-EJHVYXUP-BJUlN9bW.js +120 -0
  119. package/dist/assets/usecaseDiagram-POWQR4AR-3uBNkbAb.js +370 -0
  120. package/dist/assets/vennDiagram-UO4OBE2U-D2odalzT.js +34 -0
  121. package/dist/assets/wardleyDiagram-VNRHLVJA-DHgFaB9S.js +78 -0
  122. package/dist/assets/xychartDiagram-PMCCYNJV-w5GYM2h0.js +7 -0
  123. package/dist/index.html +2 -2
  124. package/dist-server/companion/src/routes.js +1 -0
  125. package/dist-server/container-mcp.js +231 -17
  126. package/dist-server/drivers/agents-proxy.js +16097 -1411
  127. package/dist-server/enterprise/LICENSE +32 -0
  128. package/dist-server/enterprise/server/index.js +6 -5
  129. package/dist-server/hooks/omb-hook.js +74 -0
  130. package/dist-server/index.js +12495 -7584
  131. package/dist-server/local-computer-proxy.js +83 -1
  132. package/dist-server/local-computer.js +14 -2
  133. package/dist-server/mcp-server.js +3 -1
  134. package/dist-server/openmausbot.js +5458 -4587
  135. package/dist-server/pair-cli.js +5458 -4587
  136. package/dist-server/proxy-paths.js +1 -0
  137. package/dist-server/server/admin-activity.js +410 -0
  138. package/dist-server/server/agent-tool-policy.js +1 -0
  139. package/dist-server/server/bot-package.js +2 -0
  140. package/dist-server/server/bot-visibility.js +462 -0
  141. package/dist-server/server/box.js +25 -5
  142. package/dist-server/server/browser-engine.js +87 -10
  143. package/dist-server/server/browser-live.js +7 -5
  144. package/dist-server/server/browser-runtime.js +66 -11
  145. package/dist-server/server/channel-queue.js +2 -0
  146. package/dist-server/server/checkpoints.js +62 -18
  147. package/dist-server/server/cli-prompts.js +26 -2
  148. package/dist-server/server/cli.js +79 -10
  149. package/dist-server/server/commands.js +27 -0
  150. package/dist-server/server/compaction-summary.js +78 -0
  151. package/dist-server/server/composio.js +49 -11
  152. package/dist-server/server/config.js +137 -14
  153. package/dist-server/server/context-budget.js +29 -0
  154. package/dist-server/server/context-rebuild.js +72 -0
  155. package/dist-server/server/decision-log.js +209 -48
  156. package/dist-server/server/default-model-selection.js +20 -3
  157. package/dist-server/server/delegations.js +11 -9
  158. package/dist-server/server/digest.js +130 -0
  159. package/dist-server/server/drivers/acp/core.js +637 -238
  160. package/dist-server/server/drivers/acp/grok.js +4 -3
  161. package/dist-server/server/drivers/agents-call.js +1123 -0
  162. package/dist-server/server/drivers/agents-catalog.js +659 -0
  163. package/dist-server/server/drivers/agents-client.js +28 -0
  164. package/dist-server/server/drivers/agents-proxy.js +18 -1683
  165. package/dist-server/server/drivers/agents-result.js +23 -0
  166. package/dist-server/server/drivers/chat-mcp-tools.js +17 -2
  167. package/dist-server/server/drivers/claude.js +83 -16
  168. package/dist-server/server/drivers/codex-mcp-names.js +44 -0
  169. package/dist-server/server/drivers/codex.js +32 -4
  170. package/dist-server/server/drivers/grok.js +2 -1
  171. package/dist-server/server/drivers/openai-chat.js +66 -45
  172. package/dist-server/server/drivers/openai-compat.js +16 -8
  173. package/dist-server/server/enterprise.js +97 -16
  174. package/dist-server/server/external-runtime.js +83 -0
  175. package/dist-server/server/guarded-requests.js +60 -0
  176. package/dist-server/server/harness/http.js +61 -0
  177. package/dist-server/server/hooks/omb-hook.js +103 -0
  178. package/dist-server/server/hosted-models.js +151 -0
  179. package/dist-server/server/hosted-slack.js +11 -0
  180. package/dist-server/server/index.js +2810 -678
  181. package/dist-server/server/local-computer.js +21 -2
  182. package/dist-server/server/managed-desktop.js +79 -14
  183. package/dist-server/server/managed-policy.js +153 -0
  184. package/dist-server/server/mcp-bridge.js +10 -2
  185. package/dist-server/server/mcp-tool-images.js +37 -0
  186. package/dist-server/server/mcp-tool-schema.js +108 -0
  187. package/dist-server/server/message-db.js +108 -21
  188. package/dist-server/server/message-file.js +3 -2
  189. package/dist-server/server/model-context-window.js +7 -0
  190. package/dist-server/server/model-prices.js +121 -0
  191. package/dist-server/server/notify.js +10 -1
  192. package/dist-server/server/package-export.js +1 -0
  193. package/dist-server/server/peer-roster.js +17 -4
  194. package/dist-server/server/provider-icon.js +20 -0
  195. package/dist-server/server/proxy-paths.js +1 -0
  196. package/dist-server/server/recent-work.js +15 -0
  197. package/dist-server/server/request-auth.js +97 -0
  198. package/dist-server/server/room-handoffs.js +41 -13
  199. package/dist-server/server/routes/hosted-slack.js +20 -0
  200. package/dist-server/server/routes/table.js +14 -0
  201. package/dist-server/server/routine-requests.js +39 -1
  202. package/dist-server/server/routines.js +65 -9
  203. package/dist-server/server/shared-computer-control.js +31 -4
  204. package/dist-server/server/spend.js +111 -16
  205. package/dist-server/server/steer-queue.js +12 -1
  206. package/dist-server/server/store.js +384 -33
  207. package/dist-server/server/system-prompt.js +4 -4
  208. package/dist-server/server/team-backup.js +19 -2
  209. package/dist-server/server/thread-starters.js +53 -0
  210. package/dist-server/server/tool-results.js +72 -0
  211. package/dist-server/server/tts/grok.js +74 -0
  212. package/dist-server/server/tts/index.js +19 -1
  213. package/dist-server/server/usage-ledger.js +31 -8
  214. package/dist-server/server/webhooks.js +28 -0
  215. package/dist-server/server/workspace-backup-policy.js +9 -2
  216. package/dist-server/server/workspace-backup.js +7 -4
  217. package/dist-server/shared/approval-mode.js +7 -1
  218. package/dist-server/shared/digest.js +1 -0
  219. package/dist-server/shared/markdown-windows-paths.js +36 -0
  220. package/dist-server/shared/mention-boundary.js +15 -0
  221. package/dist-server/shared/provider-icon.js +110 -0
  222. package/dist-server/shared/routines.js +7 -1
  223. package/dist-server/shared/team-backup.js +5 -0
  224. package/dist-server/src/lib/composer-attachments.js +16 -0
  225. package/dist-server/vps-container-mcp.js +236 -22
  226. package/enterprise/README.md +33 -12
  227. package/enterprise/server/index.js +6 -5
  228. package/package.json +1 -1
  229. package/dist/assets/index-BbU5REzd.js +0 -310
  230. package/dist/assets/index-CLGfYlx_.css +0 -1
@@ -15,44 +15,88 @@
15
15
  // credentials included), and fire-and-forget — an audit log must never
16
16
  // take down the decision it is auditing.
17
17
  //
18
+ // Layout and retention follow the usage ledger: one file per UTC month under
19
+ // <data>/decisions/YYYY-MM.ndjson, and a month file is deleted only once the
20
+ // whole month is older than the retention window (default 180 days, config
21
+ // `decisions.retentionDays` or OMB_DECISION_RETENTION_DAYS), so at least that
22
+ // much history is always kept. Older servers wrote one decisions.ndjson that
23
+ // rotated to .1 at 4 MB; both are still read, never written, and removed by
24
+ // the same window once their last row is older than it.
25
+ //
18
26
  // Deliberately NOT covered: ask_bot peer-approval cards. They never cross
19
27
  // the runtime bus (peer-approval.ts appends its cards straight to the
20
28
  // store), so wiring them here would mean a second, parallel tap — a
21
29
  // separate change if it earns its keep.
22
- import { readFileSync } from "node:fs";
23
- import { appendFile, rename, stat } from "node:fs/promises";
24
- import { join } from "node:path";
30
+ import { AsyncLocalStorage } from "node:async_hooks";
31
+ import { readdirSync, readFileSync } from "node:fs";
32
+ import { appendFile, mkdir, readdir, stat, unlink } from "node:fs/promises";
33
+ import { basename, join } from "node:path";
25
34
  import { redactSecrets } from "./redact.js";
26
- const FILE_NAME = "decisions.ndjson";
27
- // Rotation is logrotate at its simplest: when the live file crosses the cap
28
- // it becomes `.1` (clobbering the previous `.1`) and a fresh file starts.
29
- // Total disk is bounded at ~2× the cap; at a few hundred bytes per row that
30
- // is years of human-scale approvals, and anything fancier — dated segments,
31
- // compression — is more machinery than an audit trail this size warrants.
32
- const MAX_BYTES = 4 * 1024 * 1024;
35
+ import { csvCell } from "./usage-ledger.js";
36
+ const DIR = "decisions";
37
+ const LEGACY_FILE = "decisions.ndjson";
38
+ const MONTH_FILE = /^(\d{4})-(\d{2})\.ndjson$/;
39
+ const DAY_MS = 24 * 60 * 60_000;
40
+ const PRUNE_EVERY_MS = 60 * 60_000;
41
+ export const DEFAULT_DECISION_RETENTION_DAYS = 180;
42
+ export const MAX_DECISION_RETENTION_DAYS = 3650;
33
43
  const writeQueues = new Map();
34
- async function writeDecision(dataDir, row, maxBytes) {
35
- const file = join(dataDir, FILE_NAME);
36
- try {
37
- if ((await stat(file)).size >= maxBytes)
38
- await rename(file, `${file}.1`);
39
- }
40
- catch {
41
- /* no live file yet — nothing to rotate */
44
+ const lastPrune = new Map();
45
+ let retentionDays = () => DEFAULT_DECISION_RETENTION_DAYS;
46
+ /** The server binds its configured window once; see decisionRetentionDays. */
47
+ export function bindDecisionRetention(provider) {
48
+ retentionDays = provider;
49
+ }
50
+ /** The window in force now. The admin activity log (admin-activity.ts)
51
+ * keeps its months for the same time, so one setting covers both. */
52
+ export function boundRetentionDays() {
53
+ return retentionDays();
54
+ }
55
+ /** The retention window in days: OMB_DECISION_RETENTION_DAYS when it is a
56
+ * whole number in range, else the configured value, else 180. */
57
+ export function decisionRetentionDays(configured, env = process.env) {
58
+ const fromEnv = env.OMB_DECISION_RETENTION_DAYS?.trim();
59
+ const parsed = fromEnv ? Number(fromEnv) : NaN;
60
+ if (Number.isInteger(parsed) && parsed >= 1 && parsed <= MAX_DECISION_RETENTION_DAYS)
61
+ return parsed;
62
+ if (typeof configured === "number" && Number.isInteger(configured) && configured >= 1 && configured <= MAX_DECISION_RETENTION_DAYS)
63
+ return configured;
64
+ return DEFAULT_DECISION_RETENTION_DAYS;
65
+ }
66
+ const actorScope = new AsyncLocalStorage();
67
+ /** Run `work` as a person's card answer: every `source: "user"` row it
68
+ * writes names `actor`, without each resolver having to thread it through. */
69
+ export function withDecisionActor(actor, work) {
70
+ return actorScope.run(actor, work);
71
+ }
72
+ function monthKey(at) {
73
+ return `${at.getUTCFullYear()}-${String(at.getUTCMonth() + 1).padStart(2, "0")}`;
74
+ }
75
+ export function decisionFileFor(dataDir, at) {
76
+ return join(dataDir, DIR, `${monthKey(at)}.ndjson`);
77
+ }
78
+ async function writeDecision(dataDir, record) {
79
+ const at = new Date(record.at);
80
+ await mkdir(join(dataDir, DIR), { recursive: true, mode: 0o700 });
81
+ await appendFile(decisionFileFor(dataDir, at), JSON.stringify(record) + "\n", { mode: 0o600 });
82
+ if (at.getTime() - (lastPrune.get(dataDir) ?? 0) >= PRUNE_EVERY_MS) {
83
+ lastPrune.set(dataDir, at.getTime());
84
+ await pruneDecisions(dataDir, retentionDays(), at);
42
85
  }
43
- const record = redactSecrets({ at: new Date().toISOString(), ...row });
44
- await appendFile(file, JSON.stringify(record) + "\n", { mode: 0o600 });
45
86
  }
46
87
  /** Append one decision row. Fire-and-forget, mirroring the event bus tee:
47
88
  * the fold that calls this is delivering approvals and cards, and a full
48
89
  * disk must not turn into denied tools. */
49
- export function appendDecision(dataDir, row, opts) {
90
+ export function appendDecision(dataDir, row) {
91
+ const actor = row.actor ?? (row.source === "user" ? actorScope.getStore() : undefined);
92
+ // Redact now, not when the queue drains: the row is what was true at the
93
+ // moment of the decision.
94
+ const record = redactSecrets({ at: new Date().toISOString(), ...row, ...(actor ? { actor } : {}) });
50
95
  const previous = writeQueues.get(dataDir) ?? Promise.resolve();
51
- // Serialize stat → optional rotate → append for each directory. Without
52
- // this queue two simultaneous approvals can both rotate, overwrite .1,
53
- // or append out of decision order.
96
+ // Serialize appends (and the occasional prune) per directory, so two
97
+ // simultaneous approvals keep their decision order.
54
98
  const queued = previous
55
- .then(() => writeDecision(dataDir, row, opts?.maxBytes ?? MAX_BYTES))
99
+ .then(() => writeDecision(dataDir, record))
56
100
  .catch(() => {
57
101
  /* logging must never take down the fold */
58
102
  });
@@ -67,40 +111,157 @@ export function appendDecision(dataDir, row, opts) {
67
111
  export async function flushDecisionLog(dataDir) {
68
112
  await writeQueues.get(dataDir);
69
113
  }
114
+ /** Delete what the window no longer covers: a month file once the first
115
+ * instant after its month is at or before `now - days`, a legacy file once
116
+ * its last write is. Returns the names removed. Never throws. */
117
+ export async function pruneDecisions(dataDir, days, now = new Date()) {
118
+ const cutoff = now.getTime() - days * DAY_MS;
119
+ const removed = await pruneMonthFiles(join(dataDir, DIR), days, now);
120
+ for (const name of [`${LEGACY_FILE}.1`, LEGACY_FILE]) {
121
+ try {
122
+ if ((await stat(join(dataDir, name))).mtimeMs > cutoff)
123
+ continue;
124
+ await unlink(join(dataDir, name));
125
+ removed.push(name);
126
+ }
127
+ catch {
128
+ /* absent */
129
+ }
130
+ }
131
+ return removed;
132
+ }
133
+ /** Delete the `YYYY-MM.ndjson` files in `dir` whose whole month is at or
134
+ * before `now - days`. Returns the names removed. Never throws. Shared with
135
+ * the admin activity log. */
136
+ export async function pruneMonthFiles(dir, days, now = new Date()) {
137
+ const cutoff = now.getTime() - days * DAY_MS;
138
+ const removed = [];
139
+ let names = [];
140
+ try {
141
+ names = await readdir(dir);
142
+ }
143
+ catch {
144
+ /* nothing written in the new layout yet */
145
+ }
146
+ for (const name of names) {
147
+ const match = MONTH_FILE.exec(name);
148
+ if (!match || Date.UTC(Number(match[1]), Number(match[2]), 1) > cutoff)
149
+ continue;
150
+ try {
151
+ await unlink(join(dir, name));
152
+ removed.push(name);
153
+ }
154
+ catch {
155
+ /* already gone, or not ours to remove */
156
+ }
157
+ }
158
+ return removed;
159
+ }
70
160
  const isDecisionRow = (value) => typeof value === "object" &&
71
161
  value !== null &&
72
162
  typeof value.at === "string" &&
73
163
  typeof value.threadId === "string" &&
74
164
  typeof value.decision === "string" &&
75
165
  typeof value.source === "string";
76
- /** The newest `limit` rows, oldest first — the same order the inspector
77
- * uses for thread events. Reads `.1` before the live file so a request
78
- * right after a rotation still sees history instead of a nearly empty
79
- * log; whole-file reads are fine here because rotation caps both files,
80
- * unlike the unbounded per-thread event logs that need a tail walk. */
81
- export function readDecisions(dataDir, limit) {
82
- const file = join(dataDir, FILE_NAME);
166
+ function readRows(path) {
167
+ let text;
168
+ try {
169
+ text = readFileSync(path, "utf8");
170
+ }
171
+ catch {
172
+ return [];
173
+ }
83
174
  const rows = [];
84
- for (const path of [`${file}.1`, file]) {
85
- let text;
175
+ for (const line of text.split("\n")) {
176
+ if (!line)
177
+ continue;
86
178
  try {
87
- text = readFileSync(path, "utf8");
179
+ const value = JSON.parse(line);
180
+ if (isDecisionRow(value))
181
+ rows.push(value);
88
182
  }
89
183
  catch {
90
- continue;
184
+ /* a line torn mid-write — skip the fragment, keep the rest */
91
185
  }
92
- for (const line of text.split("\n")) {
93
- if (!line)
94
- continue;
95
- try {
96
- const value = JSON.parse(line);
97
- if (isDecisionRow(value))
98
- rows.push(value);
99
- }
100
- catch {
101
- /* a line torn mid-write — skip the fragment, keep the rest */
102
- }
186
+ }
187
+ return rows;
188
+ }
189
+ /** Every file that may hold rows, oldest first: the legacy pair (which only
190
+ * ever holds rows older than any month file), then the months in order. */
191
+ function sources(dataDir) {
192
+ let months = [];
193
+ try {
194
+ months = readdirSync(join(dataDir, DIR)).filter((name) => MONTH_FILE.test(name)).sort();
195
+ }
196
+ catch {
197
+ /* no month files yet */
198
+ }
199
+ return [join(dataDir, `${LEGACY_FILE}.1`), join(dataDir, LEGACY_FILE), ...months.map((name) => join(dataDir, DIR, name))];
200
+ }
201
+ /** The newest `limit` rows, oldest first — the same order the inspector
202
+ * uses for thread events. Reads month files newest first and stops once it
203
+ * has enough, so a long history costs no more than the page asked for. */
204
+ export function readDecisions(dataDir, limit) {
205
+ const files = sources(dataDir);
206
+ const chunks = [];
207
+ let count = 0;
208
+ for (let index = files.length - 1; index >= 0 && count < limit; index -= 1) {
209
+ const rows = readRows(files[index]);
210
+ chunks.unshift(rows);
211
+ count += rows.length;
212
+ }
213
+ return chunks.flat().slice(-limit);
214
+ }
215
+ /** Every row whose time falls inside the range, oldest first. */
216
+ export function readDecisionRange(dataDir, range) {
217
+ const from = range.from.getTime();
218
+ const to = range.to.getTime();
219
+ const first = monthKey(range.from);
220
+ const last = monthKey(range.to);
221
+ const rows = [];
222
+ for (const file of sources(dataDir)) {
223
+ // Month files outside the range are skipped unread; legacy files have no month.
224
+ const month = MONTH_FILE.exec(basename(file));
225
+ if (month && (`${month[1]}-${month[2]}` < first || `${month[1]}-${month[2]}` > last))
226
+ continue;
227
+ for (const row of readRows(file)) {
228
+ const at = Date.parse(row.at);
229
+ if (at >= from && at <= to)
230
+ rows.push(row);
103
231
  }
104
232
  }
105
- return rows.slice(-limit);
233
+ return rows;
234
+ }
235
+ export function actorLabel(actor) {
236
+ if (!actor)
237
+ return "";
238
+ if (actor.kind === "loopback")
239
+ return "This computer";
240
+ if (actor.kind === "worker")
241
+ return "Local service";
242
+ return actor.email ?? actor.label;
243
+ }
244
+ /** One line per decision, spreadsheet-ready. Cells go through the usage
245
+ * CSV's formula neutralising, and every row through redaction again, so an
246
+ * export never carries more than the log itself may. */
247
+ export function decisionsCsv(rows) {
248
+ const header = ["time", "decision", "source", "bot", "tool", "summary", "rule", "unattended", "answered_by", "thread", "request"];
249
+ const lines = [header.join(",")];
250
+ for (const raw of rows) {
251
+ const row = redactSecrets(raw);
252
+ lines.push([
253
+ row.at,
254
+ row.decision,
255
+ row.source,
256
+ row.botName || row.botId || "",
257
+ row.tool ?? "",
258
+ row.summary ?? "",
259
+ row.rule ?? "",
260
+ row.unattended ? "yes" : "",
261
+ actorLabel(row.actor),
262
+ row.threadId,
263
+ row.requestId ?? "",
264
+ ].map(csvCell).join(","));
265
+ }
266
+ return lines.join("\n") + "\n";
106
267
  }
@@ -1,10 +1,14 @@
1
+ const claudeFirst = (instances) => instances.find((instance) => instance.driverKind === "claudeAgent") ?? instances[0];
1
2
  /** A saved choice is intentional: an unavailable provider or removed model
2
3
  * sends new bots to setup instead of silently changing their provider. */
3
- export function selectDefaultModelSelection(instances, preferred) {
4
+ export function selectDefaultModelSelection(instances, preferred, context = {}) {
4
5
  if (preferred) {
5
6
  const instance = instances.find((candidate) => candidate.instanceId === preferred.instanceId);
6
7
  if (instance?.snapshot.state !== "available" ||
7
8
  instance.snapshot.authenticated === false ||
9
+ // An organisation that disallows the saved engine makes it unusable,
10
+ // exactly like an unavailable one: setup, never another provider.
11
+ context.refusal?.(instance) !== undefined ||
8
12
  (preferred.variant !== undefined && !instance.capabilities?.modelVariants) ||
9
13
  !(instance.models.default === preferred.model || instance.models.options.some((model) => model.id === preferred.model))) {
10
14
  return { instanceId: "", model: "" };
@@ -16,7 +20,20 @@ export function selectDefaultModelSelection(instances, preferred) {
16
20
  delete selection.effort;
17
21
  return selection;
18
22
  }
19
- const available = instances.filter((instance) => instance.snapshot.state === "available");
20
- const pick = available.find((instance) => instance.driverKind === "claudeAgent") ?? available[0];
23
+ // The organisation's policy is inert (no refusal) unless this desktop is enrolled.
24
+ const available = instances.filter((instance) => instance.snapshot.state === "available" && context.refusal?.(instance) === undefined);
25
+ const company = context.company;
26
+ let pick;
27
+ if (company && instances.some((instance) => company(instance.instanceId))) {
28
+ // Enrolled: an installed but signed-out personal Claude would send every
29
+ // new bot to setup although a Company model can run. Prefer instances that
30
+ // can run a turn now; a working personal engine still wins, so billing
31
+ // stays the person's explicit choice.
32
+ const ready = available.filter((instance) => instance.access === "custom" || instance.snapshot.authenticated !== false);
33
+ pick = claudeFirst(ready.filter((instance) => !company(instance.instanceId))) ?? claudeFirst(ready) ?? claudeFirst(available);
34
+ }
35
+ else {
36
+ pick = claudeFirst(available);
37
+ }
21
38
  return { instanceId: pick?.instanceId ?? "", model: pick?.models.default ?? "" };
22
39
  }
@@ -642,17 +642,19 @@ async function processOne(bus, approvalBus, from, sourceThreadId, item, runTarge
642
642
  await runTarget(item.toBotId, prefixed, item.depth + 1, sourceThreadId, channel, item.id, sender.id, item.targetThreadId);
643
643
  return "dispatched";
644
644
  }
645
- /** "Is the target free to take this handoff right now?" What "busy" means
646
- * depends on where the turn will run: a classic delegation lands in the
647
- * target's active thread, so it needs the bot to be idle; a fresh-thread
648
- * handoff needs only a free slot. This is the single free/busy test shared
649
- * by the expiry decision in `processOne` and the hold decision below, so
650
- * the two can never disagree about whether a handoff could have been
651
- * delivered right now. */
645
+ /** "Is the target free to take this handoff right now?" Both shapes ask for
646
+ * a free capacity slot, never whole-bot idleness: a classic delegation lands
647
+ * in the target's standing thread and applies the same admission startTurn
648
+ * uses for a direct turn there, while a fresh-thread handoff needs any free
649
+ * slot. This is the single free/busy test shared by the expiry decision in
650
+ * `processOne` and the hold decision below, so the two can never disagree
651
+ * about whether a handoff could have been delivered right now. */
652
652
  function targetCanTakeTurn(bus, target, item) {
653
653
  return item.targetThreadId
654
654
  ? (bus.threadSlotFree ? bus.threadSlotFree(target.id) : !target.busy)
655
- : !target.busy;
655
+ : bus.canAdmitDirectTurn
656
+ ? bus.canAdmitDirectTurn(target.id, target.threadId)
657
+ : !target.busy;
656
658
  }
657
659
  /** A busy target holds the handoff. Neither counts busy periods — the only
658
660
  * bound is DELEGATION_TTL_MS, checked in processOne before this runs (using
@@ -724,7 +726,7 @@ function sourceThreadBelongsToBot(store, botId, threadId) {
724
726
  * have moved either bot, or narrowed the sender's peers, in between. */
725
727
  function dropIfUnreachable(bus, sender, target, sourceThreadId, item) {
726
728
  const sectionsDiffer = !canAccessTeam(sender, target.section);
727
- if (!sectionsDiffer && !target.hidden && peerAllowed(sender, target.id))
729
+ if (!sectionsDiffer && !target.hidden && peerAllowed(sender, target))
728
730
  return false;
729
731
  const reason = sectionsDiffer
730
732
  ? "bots now belong to different sections"
@@ -0,0 +1,130 @@
1
+ /** Evidence is observed, not inferred from a provider's brand. In particular,
2
+ * OpenAI-compatible and Grok turns now expose structured tool activity too. */
3
+ export function coverageForDriver(_driverKind, fullResults = false, observedTools = false) {
4
+ return fullResults ? "full" : observedTools ? "preview" : "none";
5
+ }
6
+ /** True when every tool row of the turn carries a delivered (untruncated)
7
+ * result — what a PostToolUse hook gives us — so the digest may claim
8
+ * "full" evidence. One preview-only row means the turn is "preview". */
9
+ export function toolEvidence(activities, turnId) {
10
+ const rows = activities.filter((m) => m.kind === "activity" && m.tool?.itemId && m.turnId === turnId);
11
+ return rows.length > 0 && rows.every((m) => m.tool?.fullResult === true);
12
+ }
13
+ export const MAX_TOOLS = 8;
14
+ export const MAX_FILES = 20;
15
+ export const MAX_MEMORY = 20;
16
+ export const REPLY_CHARS = 200;
17
+ export const RENDER_BYTES = 1_500;
18
+ export function buildTurnDigest(input) {
19
+ const byName = new Map();
20
+ for (const m of input.activities) {
21
+ if (m.kind !== "activity" || !m.tool?.itemId || m.turnId !== input.turnId)
22
+ continue;
23
+ const entry = byName.get(m.tool.name) ?? { name: fitBytes(m.tool.name, 160), count: 0, failed: 0 };
24
+ entry.count += 1;
25
+ if (m.tool.ok === false)
26
+ entry.failed += 1;
27
+ if (entry.sample === undefined && m.tool.summary)
28
+ entry.sample = fitBytes(m.tool.summary.replace(/\s+/g, " "), 300);
29
+ byName.set(m.tool.name, entry);
30
+ }
31
+ // busiest first; ties keep first-seen order (Map preserves insertion)
32
+ const ranked = [...byName.values()].sort((a, b) => b.count - a.count);
33
+ const tools = ranked.slice(0, MAX_TOOLS);
34
+ const toolsDropped = ranked.length - tools.length;
35
+ const memory = input.memory.slice(0, MAX_MEMORY).map((row) => ({
36
+ path: fitBytes(row.path, 512),
37
+ kind: (row.kind === "edited" ? "updated" : row.kind),
38
+ }));
39
+ const digest = {
40
+ turnId: input.turnId,
41
+ botId: input.botId,
42
+ threadId: input.threadId,
43
+ at: input.at,
44
+ durationMs: input.durationMs,
45
+ tools,
46
+ toolCalls: ranked.reduce((sum, tool) => sum + tool.count, 0),
47
+ ...(toolsDropped > 0 ? { toolsDropped } : {}),
48
+ ...(input.files ? { files: boundFiles(input.files) } : {}),
49
+ memory,
50
+ ...(input.memory.length > memory.length ? { memoryDropped: input.memory.length - memory.length } : {}),
51
+ reply: firstSentence(input.reply),
52
+ ...(input.usage ? { usage: input.usage } : {}),
53
+ hookCoverage: input.hookCoverage,
54
+ };
55
+ return digest;
56
+ }
57
+ function boundFiles(files) {
58
+ let budget = MAX_FILES;
59
+ let truncated = files.truncated ?? 0;
60
+ const take = (list) => {
61
+ const kept = list.slice(0, Math.max(0, budget)).map(path => fitBytes(path, 512));
62
+ truncated += list.length - kept.length;
63
+ budget -= kept.length;
64
+ return kept;
65
+ };
66
+ const changed = take(files.changed);
67
+ const added = take(files.added);
68
+ const deleted = take(files.deleted);
69
+ return { changed, added, deleted, ...(truncated > 0 ? { truncated } : {}) };
70
+ }
71
+ function firstSentence(reply) {
72
+ const flat = reply.replace(/\s+/g, " ").trim();
73
+ const end = flat.search(/[.!?](\s|$)/);
74
+ const sentence = end === -1 ? flat : flat.slice(0, end + 1);
75
+ return sentence.slice(0, REPLY_CHARS);
76
+ }
77
+ /** The transcript row's text: one paragraph, FTS-indexed, ≤ RENDER_BYTES. */
78
+ export function renderDigest(d) {
79
+ const parts = ["[digest]"];
80
+ if (d.tools.length) {
81
+ const tools = d.tools.map((t) => `${t.name} ×${t.count}${t.failed ? ` (${t.failed} failed)` : ""}`).join(", ");
82
+ parts.push(`tools: ${tools}${d.toolsDropped ? ` +${d.toolsDropped} more` : ""}${d.hookCoverage === "preview" ? " (from tool previews)" : ""}`);
83
+ }
84
+ else if (d.hookCoverage === "none") {
85
+ parts.push("no tool activity observed in this turn");
86
+ }
87
+ else {
88
+ parts.push("no tool calls");
89
+ }
90
+ if (d.files) {
91
+ const files = [];
92
+ if (d.files.changed.length)
93
+ files.push(`changed ${d.files.changed.join(", ")}`);
94
+ if (d.files.added.length)
95
+ files.push(`added ${d.files.added.join(", ")}`);
96
+ if (d.files.deleted.length)
97
+ files.push(`deleted ${d.files.deleted.join(", ")}`);
98
+ if (d.files.truncated)
99
+ files.push(`+${d.files.truncated} more paths`);
100
+ parts.push(files.length ? `files: ${files.join("; ")}` : "files: none changed");
101
+ }
102
+ if (d.memory.length)
103
+ parts.push(`memory: ${d.memory.map((m) => `${m.kind} ${m.path}`).join(", ")}`);
104
+ if (d.memoryDropped)
105
+ parts.push(`+${d.memoryDropped} more memory changes`);
106
+ if (d.reply)
107
+ parts.push(`reply: ${d.reply}`);
108
+ return fitBytes(parts.join(" · "), RENDER_BYTES);
109
+ }
110
+ /** One bracketed line for a context rebuild — never multi-line, so it sits
111
+ * in a transcript replay or a room window like any other speaker line. */
112
+ export function digestPromptLine(d, botName) {
113
+ const body = renderDigest(d).replace(/^\[digest\]\s*/, "").replace(/\s+/g, " ").trim();
114
+ return `[What ${botName} did in an earlier turn: ${body}]`;
115
+ }
116
+ /** Trim to a byte budget on a character boundary, marking the cut. */
117
+ function fitBytes(text, max) {
118
+ if (Buffer.byteLength(text, "utf8") <= max)
119
+ return text;
120
+ const marker = " […]";
121
+ let out = "";
122
+ let bytes = Buffer.byteLength(marker);
123
+ for (const character of text) {
124
+ bytes += Buffer.byteLength(character);
125
+ if (bytes > max)
126
+ break;
127
+ out += character;
128
+ }
129
+ return out + marker;
130
+ }