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
@@ -3,7 +3,7 @@
3
3
  // ProviderSessionDirectory, recipe step 6: persist the binding from day
4
4
  // one). messages-<threadId>.json holds the folded transcript.
5
5
  import { createHash } from "node:crypto";
6
- import { existsSync, readFileSync, mkdirSync, rmSync, unlinkSync } from "node:fs";
6
+ import { chmodSync, existsSync, readFileSync, mkdirSync, rmSync, statSync, unlinkSync } from "node:fs";
7
7
  import { join } from "node:path";
8
8
  import { writeFileAtomic } from "./atomic.js";
9
9
  import { ensureSections, readSections, changeEmptySection } from "./section-context.js";
@@ -18,6 +18,8 @@ import { pickBotName } from "./names.js";
18
18
  import { redactSecretsInText } from "./redact.js";
19
19
  import { botAvatarProfile } from "../shared/bot-avatar.js";
20
20
  import { approvalModeFor, isApprovalMode } from "../shared/approval-mode.js";
21
+ import { isMentionBoundary, isMentionNameContinuation } from "../shared/mention-boundary.js";
22
+ import { CONNECTOR_SLUG_PATTERN, CONNECTOR_TOOL_NAME_PATTERN } from "../shared/wire.js";
21
23
  export const groupWireProjectionIsExact = true;
22
24
  // Unicode's complete emoji sequences include flags, skin tones and ZWJ
23
25
  // combinations. Also allow unqualified single symbols (e.g. ♥), but not
@@ -30,14 +32,23 @@ export const taskWireProjectionIsExact = true;
30
32
  /** The typed wire projection for one task. Pairs with the assertion above:
31
33
  * returning WireTask means an undeclared server field cannot ride silently. */
32
34
  export function toWireTask(task) {
33
- const { resumeCursors: _resumeCursors, lastInstanceId: _lastInstanceId, handedMessages: _handedMessages, appliedCompactionId: _appliedCompactionId, contextFloor: _contextFloor, lastContextModel: _lastContextModel, ...wire } = task;
35
+ const { resumeCursors: _resumeCursors, lastInstanceId: _lastInstanceId, handedMessages: _handedMessages, appliedCompactionId: _appliedCompactionId, contextFloor: _contextFloor, lastContextModel: _lastContextModel, surfaceSource: _surfaceSource, ...wire } = task;
34
36
  return wire;
35
37
  }
36
38
  const TASK_PATCH_FIELDS = [
37
39
  "title", "projectId", "modelSelection", "approvalMode", "autoApprove", "alwaysAllow",
38
- "unread", "rewound", "archivedAt", "pinnedMessageId", "resumeCursors", "lastInstanceId", "cwd",
39
- "routineRunId", "surface", "appliedCompactionId", "contextFloor", "lastContextModel",
40
+ "unread", "rewound", "archivedAt", "pinned", "pinnedMessageId", "resumeCursors", "lastInstanceId", "cwd",
41
+ "routineRunId", "surface", "surfaceSource", "snoozedUntil", "appliedCompactionId", "contextFloor", "lastContextModel",
40
42
  ];
43
+ /** Only `true` is a pin. false/undefined must not land in bots.json or groups.json. */
44
+ function persistedPin(task) {
45
+ if (task.pinned === true)
46
+ return task;
47
+ if (!("pinned" in task))
48
+ return task;
49
+ const { pinned: _pinned, ...rest } = task;
50
+ return rest;
51
+ }
41
52
  /** Everything the BOT authored is scrubbed of content-shaped secrets before
42
53
  * it is stored: its reply text, a tool title (an ACP engine's title can be
43
54
  * the whole command line) and the command beside it, a permission card's
@@ -86,6 +97,9 @@ function redactBotAuthored(message) {
86
97
  card.held = redactSecretsInText(card.held);
87
98
  if (typeof card.answeredText === "string")
88
99
  card.answeredText = redactSecretsInText(card.answeredText);
100
+ if (card.commandAllowlist && (redactSecretsInText(card.commandAllowlist.command) !== card.commandAllowlist.command ||
101
+ redactSecretsInText(card.commandAllowlist.cwd) !== card.commandAllowlist.cwd))
102
+ delete card.commandAllowlist;
89
103
  // Bot-authored question text sits behind the subtitle the same way a
90
104
  // routine's instructions do, so it is scrubbed on the same boundary.
91
105
  if (card.questionRequest) {
@@ -252,8 +266,76 @@ export function titleFromLlm(raw) {
252
266
  return line.length >= 1 && line.length <= 48 ? line : null;
253
267
  }
254
268
  export const botWireProjectionIsExact = true;
269
+ /** Upper bounds keep a grants patch from becoming a persistence blob; they
270
+ * sit far above any real service's tool count. */
271
+ export const CONNECTOR_SLUGS_MAX = 64;
272
+ export const CONNECTOR_TOOLS_PER_SERVICE_MAX = 500;
273
+ /** Validate and normalize a connectorTools value at the one boundary every
274
+ * writer shares (patchBot). Returns a canonical copy: slugs checked against
275
+ * the service pattern, tool lists deduplicated in order, `"*"` kept as-is.
276
+ * The legacy clear stays a JSON null at the API edge; here callers pass
277
+ * undefined to return a bot to boolean-only behavior. */
278
+ export function parseConnectorTools(value) {
279
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
280
+ return { ok: false, error: "connectorTools must be an object of service slugs to tool grants" };
281
+ }
282
+ const entries = Object.entries(value);
283
+ if (entries.length > CONNECTOR_SLUGS_MAX) {
284
+ return { ok: false, error: `connectorTools may name at most ${CONNECTOR_SLUGS_MAX} services` };
285
+ }
286
+ const grants = {};
287
+ for (const [slug, grant] of entries) {
288
+ if (!CONNECTOR_SLUG_PATTERN.test(slug)) {
289
+ return { ok: false, error: `connectorTools service slugs must be lowercase slugs (got "${slug}")` };
290
+ }
291
+ if (!grant || typeof grant !== "object" || Array.isArray(grant)) {
292
+ return { ok: false, error: `connectorTools.${slug} must be a grant like { tools: "*" } or { tools: ["TOOL_NAME"] }` };
293
+ }
294
+ const keys = Object.keys(grant);
295
+ if (keys.length !== 1 || keys[0] !== "tools") {
296
+ return { ok: false, error: `connectorTools.${slug} accepts only a tools field` };
297
+ }
298
+ const tools = grant.tools;
299
+ if (tools === "*") {
300
+ grants[slug] = { tools: "*" };
301
+ continue;
302
+ }
303
+ if (!Array.isArray(tools) || tools.length === 0) {
304
+ return { ok: false, error: `connectorTools.${slug}.tools must be "*" or a non-empty list of tool names (use {} to grant no tools)` };
305
+ }
306
+ if (tools.length > CONNECTOR_TOOLS_PER_SERVICE_MAX) {
307
+ return { ok: false, error: `connectorTools.${slug}.tools may list at most ${CONNECTOR_TOOLS_PER_SERVICE_MAX} tools` };
308
+ }
309
+ const names = [];
310
+ for (const tool of tools) {
311
+ if (typeof tool !== "string" || !CONNECTOR_TOOL_NAME_PATTERN.test(tool)) {
312
+ return { ok: false, error: `connectorTools.${slug}.tools names must be Composio tool names like GMAIL_SEND_EMAIL` };
313
+ }
314
+ if (!names.includes(tool))
315
+ names.push(tool);
316
+ }
317
+ grants[slug] = { tools: names };
318
+ }
319
+ return { ok: true, grants };
320
+ }
255
321
  const BOTS_FILE = join(DATA_DIR, "bots.json");
256
322
  const GROUPS_FILE = join(DATA_DIR, "groups.json");
323
+ /** The registries carry souls, project paths and per-bot settings, so they are
324
+ * owner-only like the other data-dir stores. A file written by an older
325
+ * release (or loosened by hand) is tightened on load. Best effort: failing to
326
+ * tighten must never stop the fleet from loading, and the next save replaces
327
+ * the file with a 0600 one anyway. Windows has no POSIX mode bits. */
328
+ function tightenRegistryFile(file) {
329
+ if (process.platform === "win32")
330
+ return;
331
+ try {
332
+ if ((statSync(file).mode & 0o077) !== 0)
333
+ chmodSync(file, 0o600);
334
+ }
335
+ catch {
336
+ /* absent, or not ours to change */
337
+ }
338
+ }
257
339
  const messagesFile = (threadId) => join(DATA_DIR, `messages-${threadId}.json`);
258
340
  const COLORS = [
259
341
  "green",
@@ -283,15 +365,15 @@ export function mentionedBots(text, peers) {
283
365
  const found = [];
284
366
  let at = -1;
285
367
  while ((at = lower.indexOf("@", at + 1)) !== -1) {
286
- if (at > 0 && !/\s/.test(text[at - 1]))
368
+ if (!isMentionBoundary(text, at))
287
369
  continue; // user@host, not a tag
288
370
  const rest = lower.slice(at + 1);
289
371
  const hit = candidates.find((p) => {
290
372
  const name = p.name.toLowerCase();
291
373
  if (!rest.startsWith(name))
292
374
  return false;
293
- const after = rest[name.length]; // must not run into a longer word
294
- return after === undefined || !/[a-z0-9]/i.test(after);
375
+ const after = rest.slice(name.length); // must not run into a longer word
376
+ return !isMentionNameContinuation(after);
295
377
  });
296
378
  if (hit && !found.includes(hit))
297
379
  found.push(hit);
@@ -324,8 +406,15 @@ export function normalizeGroupDefaultResponder(value, memberIds, dm = false) {
324
406
  * otherwise the room policy chooses one member, everyone, or nobody. */
325
407
  export function roomResponders(text, members, defaultResponder) {
326
408
  const available = members.filter((member) => !member.hidden);
327
- if (/(?:^|\s)@everyone\b/i.test(text))
328
- return available;
409
+ const everyone = "everyone";
410
+ for (let at = text.indexOf("@"); at !== -1; at = text.indexOf("@", at + 1)) {
411
+ const candidate = text.slice(at + 1, at + 1 + everyone.length);
412
+ if (isMentionBoundary(text, at) &&
413
+ candidate.toLocaleLowerCase() === everyone &&
414
+ !isMentionNameContinuation(text.slice(at + 1 + everyone.length))) {
415
+ return available;
416
+ }
417
+ }
329
418
  const mentioned = mentionedBots(text, available);
330
419
  if (mentioned.length)
331
420
  return mentioned;
@@ -342,15 +431,22 @@ export class Store {
342
431
  groups = [];
343
432
  threads = new Map();
344
433
  defaultSelection;
434
+ completeNewBotSelection;
345
435
  listeners = new Set();
346
436
  /** A broken team registry must not prevent loading independent chat data. */
347
437
  registeringInitialSections = true;
348
438
  /** Room turns and old callers have their own activity slot. Clearing
349
439
  * that slot must not clear a concurrently running independent task. */
350
440
  legacyActivities = new Map();
351
- constructor(defaultSelection) {
441
+ constructor(defaultSelection,
442
+ /** Workspace-wide new-bot defaults (config newBots), applied to every
443
+ * new bot's selection whichever path created it. */
444
+ completeNewBotSelection = (selection) => selection) {
352
445
  this.defaultSelection = defaultSelection;
446
+ this.completeNewBotSelection = completeNewBotSelection;
353
447
  mkdirSync(DATA_DIR, { recursive: true });
448
+ for (const file of [BOTS_FILE, GROUPS_FILE])
449
+ tightenRegistryFile(file);
354
450
  try {
355
451
  this.bots = JSON.parse(readFileSync(BOTS_FILE, "utf8"));
356
452
  }
@@ -517,6 +613,7 @@ export class Store {
517
613
  threadId: g.threadId,
518
614
  title: this.firstUserLine(g.threadId) ?? UNTITLED_TASK,
519
615
  createdAt: g.createdAt,
616
+ updatedAt: g.createdAt,
520
617
  };
521
618
  if (g.pinnedCwd !== undefined)
522
619
  initialTask.pinnedCwd = g.pinnedCwd;
@@ -552,6 +649,7 @@ export class Store {
552
649
  threadId: b.threadId,
553
650
  title: this.firstUserLine(b.threadId) ?? UNTITLED_TASK,
554
651
  createdAt: b.createdAt,
652
+ updatedAt: b.createdAt,
555
653
  resumeCursors: b.resumeCursors ?? {},
556
654
  }];
557
655
  botsMigrated = true;
@@ -564,6 +662,7 @@ export class Store {
564
662
  threadId: b.threadId,
565
663
  title: this.firstUserLine(b.threadId) ?? UNTITLED_TASK,
566
664
  createdAt: b.createdAt,
665
+ updatedAt: b.createdAt,
567
666
  resumeCursors: b.resumeCursors ?? {},
568
667
  };
569
668
  b.tasks.unshift(active);
@@ -619,18 +718,126 @@ export class Store {
619
718
  if (existsSync(legacyFile))
620
719
  mdb.readThread(threadId, legacyFile);
621
720
  }
721
+ // After legacy transcripts are in SQLite, so the first boot sees their
722
+ // newest message instead of stamping createdAt and jumping next launch.
723
+ this.repairThreadUpdatedAts();
724
+ // After the roster is loaded, so identity resolution sees which opener
725
+ // ids are still live.
726
+ this.repairDuplicatePairConversations();
622
727
  this.registeringInitialSections = false;
623
728
  }
624
- saveBots(bots = this.bots) {
625
- this.rememberSections([...this.bots, ...bots].map((bot) => bot.section));
729
+ /** Advance a task's update stamp in memory only. Message writes must not
730
+ * rewrite bots.json; the next snapshot and the startup repair read this. */
731
+ noteThreadActivity(threadId, at) {
732
+ if (!Number.isFinite(at))
733
+ return;
734
+ const advance = (current) => Math.max(current ?? 0, at);
735
+ for (const bot of this.bots) {
736
+ const task = bot.tasks?.find((candidate) => candidate.threadId === threadId);
737
+ if (!task)
738
+ continue;
739
+ const next = advance(task.updatedAt);
740
+ if (task.updatedAt !== next)
741
+ task.updatedAt = next;
742
+ }
743
+ for (const group of this.groups) {
744
+ const task = group.tasks?.find((candidate) => candidate.threadId === threadId);
745
+ if (!task)
746
+ continue;
747
+ const next = advance(task.updatedAt);
748
+ if (task.updatedAt !== next)
749
+ task.updatedAt = next;
750
+ }
751
+ }
752
+ /** Fill missing stamps, and raise a stamp that is older than the newest
753
+ * stored message. Never moves a stamp backwards: there is no per-message
754
+ * delete, and a clock skew must not reshuffle the list on every boot. */
755
+ repairThreadUpdatedAts() {
756
+ const botTasks = this.bots.flatMap((bot) => bot.tasks ?? []);
757
+ const groupTasks = this.groups.flatMap((group) => group.tasks ?? []);
758
+ const latest = mdb.latestMessageAts([...botTasks, ...groupTasks].map((task) => task.threadId));
759
+ let botsDirty = false;
760
+ let groupsDirty = false;
761
+ const repair = (task) => {
762
+ const fromMessages = latest.get(task.threadId);
763
+ const next = fromMessages !== undefined && (task.updatedAt === undefined || fromMessages > task.updatedAt)
764
+ ? fromMessages
765
+ : task.updatedAt ?? task.createdAt;
766
+ if (task.updatedAt === next)
767
+ return false;
768
+ task.updatedAt = next;
769
+ return true;
770
+ };
771
+ for (const task of botTasks)
772
+ if (repair(task))
773
+ botsDirty = true;
774
+ for (const task of groupTasks)
775
+ if (repair(task))
776
+ groupsDirty = true;
777
+ if (botsDirty)
778
+ this.saveBots();
779
+ if (groupsDirty)
780
+ this.saveGroups();
781
+ }
782
+ /** At most one live pair row per (recipient, sender identity). Servers
783
+ * before identity-stable matching minted a second live pair row for a
784
+ * deleted-and-recreated sender while the predecessor's row dangled live
785
+ * forever; collapse those on load by keeping the row the resolver favors
786
+ * (first in the task list — the newest, actively used one) and demoting
787
+ * the rest to closed plain threads. History is kept, never deleted, and
788
+ * a demoted row is never re-adopted: adoption skips closed rows. Runs on
789
+ * every load and touches nothing in a store that already holds the
790
+ * invariant. */
791
+ repairDuplicatePairConversations() {
792
+ let botsDirty = false;
793
+ const sweep = (tasks, repaired) => {
794
+ const pairs = new Map();
795
+ for (const task of tasks ?? []) {
796
+ const by = task.openedBy;
797
+ if (by?.kind !== "pair" || task.closedBy)
798
+ continue;
799
+ const identity = this.openerIdentity(by);
800
+ pairs.set(identity, [...(pairs.get(identity) ?? []), task]);
801
+ }
802
+ for (const rows of pairs.values()) {
803
+ // rows are in task-list order, the same order the resolver's
804
+ // find() favors: the first is the one it keeps using.
805
+ for (const duplicate of rows.slice(1)) {
806
+ const by = duplicate.openedBy;
807
+ const identity = this.openerIdentity(by);
808
+ const { kind: _kind, ...opened } = by;
809
+ duplicate.openedBy = opened;
810
+ duplicate.closedBy = {
811
+ botId: identity,
812
+ name: this.bot(identity)?.name ?? by.name,
813
+ at: Date.now(),
814
+ };
815
+ repaired();
816
+ }
817
+ }
818
+ };
819
+ // Pair rows only ever live on bots: the resolver requires a bot
820
+ // recipient, and group tasks carry no peer stamps.
821
+ for (const bot of this.bots)
822
+ sweep(bot.tasks, () => { botsDirty = true; });
823
+ if (botsDirty)
824
+ this.saveBots();
825
+ }
826
+ saveBots(bots = this.bots, registerSections = true) {
827
+ if (registerSections)
828
+ this.rememberSections([...this.bots, ...bots].map((bot) => bot.section));
626
829
  writeFileAtomic(BOTS_FILE, JSON.stringify(bots.map(({ busy: _busy, activity: _activity, ...bot }) => ({
627
830
  ...bot,
628
- tasks: bot.tasks?.map(({ busy: _taskBusy, activity: _taskActivity, turnStartedAt: _taskTurnStarted, ...task }) => task),
629
- })), null, 2));
831
+ tasks: bot.tasks?.map(({ busy: _taskBusy, activity: _taskActivity, turnStartedAt: _taskTurnStarted, ...task }) => persistedPin(task)),
832
+ })), null, 2), { mode: 0o600 });
630
833
  }
631
- saveGroups() {
632
- this.rememberSections(this.groups.map((group) => group.section));
633
- writeFileAtomic(GROUPS_FILE, JSON.stringify(this.groups.map(({ busyBotId: _busyBotId, turnStartedAt: _turnStartedAt, ...g }) => g), null, 2));
834
+ saveGroups(groups = this.groups, registerSections = true) {
835
+ if (registerSections)
836
+ this.rememberSections(groups.map((group) => group.section));
837
+ writeFileAtomic(GROUPS_FILE, JSON.stringify(groups.map(({ busyBotId: _busyBotId, turnStartedAt: _turnStartedAt, ...g }) => ({
838
+ ...g,
839
+ ...(g.tasks ? { tasks: g.tasks.map((task) => persistedPin(task)) } : {}),
840
+ })), null, 2), { mode: 0o600 });
634
841
  }
635
842
  get sections() { return readSections(); }
636
843
  rememberSections(names) {
@@ -644,6 +851,60 @@ export class Store {
644
851
  console.warn(`[teams] Startup could not register team names; saved teams and shared instructions were left unchanged: ${error.message}`);
645
852
  }
646
853
  }
854
+ /** Rename the same team, preserving its members and existing access grants. */
855
+ renameSection(name, nextName, computers) {
856
+ if (!name || !this.sections.includes(name))
857
+ return "No such team";
858
+ nextName = nextName.trim();
859
+ if (!nextName || nextName.length > 60 || [...nextName].some(char => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127))
860
+ return "Team name must be 1 to 60 characters without control characters";
861
+ if (name === nextName)
862
+ return undefined;
863
+ if (this.sections.includes(nextName) || computers?.forSection(nextName))
864
+ return "A team with that name already exists";
865
+ const members = this.bots.filter(bot => sectionKey(bot.section) === name);
866
+ const rooms = this.groups.filter(group => sectionKey(group.section) === name);
867
+ const affected = this.bots.filter(bot => members.includes(bot) || bot.managedSections?.some(section => sectionKey(section) === name));
868
+ if (affected.some(bot => bot.busy || bot.tasks?.some(task => task.busy)) || rooms.some(group => group.busyBotId)) {
869
+ return "Stop this team's active work before renaming the team";
870
+ }
871
+ // Keep the established empty-team lifecycle: old grants are revoked when
872
+ // an empty identity is removed, so recreating it cannot restore access.
873
+ if (!members.length && !rooms.length && !computers?.forSection(name))
874
+ return this.changeEmptySection(name, nextName);
875
+ const nextBots = this.bots.map(bot => ({ ...bot,
876
+ ...(members.includes(bot) ? { section: nextName } : {}),
877
+ ...(bot.managedSections ? { managedSections: [...new Set(bot.managedSections.map(section => sectionKey(section) === name ? nextName : section))] } : {}),
878
+ }));
879
+ const nextGroups = this.groups.map(group => rooms.includes(group) ? { ...group, section: nextName } : group);
880
+ let computerChanged = false;
881
+ try {
882
+ // Register the new name only after saving the records; otherwise the
883
+ // registry's duplicate-name check would reject this same rename.
884
+ this.saveBots(nextBots, false);
885
+ this.saveGroups(nextGroups, false);
886
+ computerChanged = computers?.renameSection(name, nextName) ?? false;
887
+ changeEmptySection(name, nextName);
888
+ }
889
+ catch (error) {
890
+ this.saveBots(this.bots, false);
891
+ this.saveGroups(this.groups, false);
892
+ if (computerChanged)
893
+ computers.renameSection(nextName, name);
894
+ throw error;
895
+ }
896
+ // Preserve identities held by the schedulers and other store consumers.
897
+ for (let i = 0; i < nextBots.length; i++)
898
+ Object.assign(this.bots[i], nextBots[i]);
899
+ for (let i = 0; i < nextGroups.length; i++)
900
+ Object.assign(this.groups[i], nextGroups[i]);
901
+ for (const bot of affected)
902
+ this.emit({ type: "bot", botId: bot.id });
903
+ for (const room of rooms)
904
+ this.emit({ type: "group", groupId: room.id });
905
+ this.emit({ type: "sections" });
906
+ return undefined;
907
+ }
647
908
  /** Empty-only changes cannot merge teams or silently change anybody's access. */
648
909
  changeEmptySection(name, nextName) {
649
910
  if (!this.sections.includes(name))
@@ -672,6 +933,44 @@ export class Store {
672
933
  this.emit({ type: "sections" });
673
934
  return undefined;
674
935
  }
936
+ /** Remove the team, keeping its bots, rooms and conversations in General. */
937
+ deleteSection(name) {
938
+ if (!name || !this.sections.includes(name))
939
+ return "No such team";
940
+ const members = this.bots.filter(bot => sectionKey(bot.section) === name);
941
+ const rooms = this.groups.filter(group => sectionKey(group.section) === name);
942
+ if (members.some(bot => bot.busy || bot.tasks?.some(task => task.busy)) || rooms.some(group => group.busyBotId)) {
943
+ return "Stop this team's active work before deleting the team";
944
+ }
945
+ if (this.bots.filter(bot => bot.chiefOfStaff && (!sectionKey(bot.section) || sectionKey(bot.section) === name)).length > 1) {
946
+ return "General already has a Chief of Staff. Move or change this team's Chief before deleting the team";
947
+ }
948
+ const nextBots = this.bots.map(bot => ({ ...bot,
949
+ ...(sectionKey(bot.section) === name ? { section: undefined } : {}),
950
+ ...(bot.managedSections ? { managedSections: bot.managedSections.filter(section => sectionKey(section) !== name) } : {}),
951
+ }));
952
+ const nextGroups = this.groups.map(group => sectionKey(group.section) === name ? { ...group, section: undefined } : group);
953
+ try {
954
+ this.saveBots(nextBots);
955
+ this.saveGroups(nextGroups);
956
+ changeEmptySection(name, null);
957
+ }
958
+ catch (error) {
959
+ this.saveBots();
960
+ this.saveGroups();
961
+ throw error;
962
+ }
963
+ for (let i = 0; i < nextBots.length; i++)
964
+ Object.assign(this.bots[i], nextBots[i]);
965
+ for (let i = 0; i < nextGroups.length; i++)
966
+ Object.assign(this.groups[i], nextGroups[i]);
967
+ for (const bot of this.bots)
968
+ this.emit({ type: "bot", botId: bot.id });
969
+ for (const group of rooms)
970
+ this.emit({ type: "group", groupId: group.id });
971
+ this.emit({ type: "sections" });
972
+ return undefined;
973
+ }
675
974
  // ── groups ────────────────────────────────────────────────────────────
676
975
  /** Subscribe to every write. Listeners run after the write and after
677
976
  * save; a throwing listener never breaks the write. */
@@ -715,7 +1014,7 @@ export class Store {
715
1014
  section,
716
1015
  };
717
1016
  if (!dm) {
718
- group.tasks = [{ threadId, title: UNTITLED_TASK, createdAt }];
1017
+ group.tasks = [{ threadId, title: UNTITLED_TASK, createdAt, updatedAt: createdAt }];
719
1018
  group.setupCompletedAt = setup?.completed ? createdAt : null;
720
1019
  group.setupSkippedAt = null;
721
1020
  }
@@ -847,10 +1146,12 @@ export class Store {
847
1146
  const group = this.group(groupId);
848
1147
  if (!group || group.dm)
849
1148
  return null;
1149
+ const createdAt = Date.now();
850
1150
  const task = {
851
1151
  threadId: newId(),
852
1152
  title: title?.trim().slice(0, 80) || UNTITLED_TASK,
853
- createdAt: Date.now(),
1153
+ createdAt,
1154
+ updatedAt: createdAt,
854
1155
  };
855
1156
  group.tasks = [task, ...(group.tasks ?? [])];
856
1157
  if (activate) {
@@ -874,6 +1175,18 @@ export class Store {
874
1175
  this.emit({ type: "group", groupId });
875
1176
  return group;
876
1177
  }
1178
+ setGroupTaskPinned(groupId, threadId, pinned) {
1179
+ const task = this.groupTaskByThread(groupId, threadId);
1180
+ if (!task)
1181
+ return null;
1182
+ if (pinned)
1183
+ task.pinned = true;
1184
+ else
1185
+ delete task.pinned;
1186
+ this.saveGroups();
1187
+ this.emit({ type: "group", groupId });
1188
+ return task;
1189
+ }
877
1190
  renameGroupTask(groupId, threadId, title) {
878
1191
  const task = this.groupTaskByThread(groupId, threadId);
879
1192
  if (!task)
@@ -1003,6 +1316,9 @@ export class Store {
1003
1316
  throw new Error("Cannot import over an existing conversation");
1004
1317
  mdb.importThread(threadId, messages, activeLeafId);
1005
1318
  this.threads.delete(threadId);
1319
+ const newest = messages.reduce((max, message) => Math.max(max, message.at), Number.NEGATIVE_INFINITY);
1320
+ if (Number.isFinite(newest))
1321
+ this.noteThreadActivity(threadId, newest);
1006
1322
  }
1007
1323
  activeLeaf(threadId) {
1008
1324
  return this.thread(threadId).activeLeafId;
@@ -1051,6 +1367,7 @@ export class Store {
1051
1367
  this.emit({ type: "message.patch", threadId, message: pruned });
1052
1368
  }
1053
1369
  }
1370
+ this.noteThreadActivity(threadId, full.at);
1054
1371
  this.emit({ type: "message", threadId, message: full });
1055
1372
  // The first-run quiz is not a live ask. Talking past it hides it so the
1056
1373
  // transcript is just the greeting plus what they said. Cards with a
@@ -1081,6 +1398,7 @@ export class Store {
1081
1398
  this.emit({ type: "message.patch", threadId, message: pruned });
1082
1399
  }
1083
1400
  }
1401
+ this.noteThreadActivity(threadId, full.at);
1084
1402
  this.emit({ type: "message", threadId, message: full });
1085
1403
  // announced after the insert so no client ever sees two siblings
1086
1404
  // claiming the same parent
@@ -1118,8 +1436,10 @@ export class Store {
1118
1436
  return pruned;
1119
1437
  }
1120
1438
  /** Fork the conversation: a new user message that replaces `sourceId`
1121
- * (same parent, new text) and becomes the active leaf. */
1122
- branchMessage(threadId, sourceId, text) {
1439
+ * (same parent, new text) and becomes the active leaf. `sendId` is the
1440
+ * client's identity for this edit, so its instant bubble reconciles onto
1441
+ * the canonical message and a network retry cannot fork twice. */
1442
+ branchMessage(threadId, sourceId, text, sendId) {
1123
1443
  const t = this.thread(threadId);
1124
1444
  const source = t.messages.find((m) => m.id === sourceId);
1125
1445
  if (!source)
@@ -1132,10 +1452,12 @@ export class Store {
1132
1452
  text,
1133
1453
  parentId: source.parentId ?? null,
1134
1454
  replyToId: source.replyToId,
1455
+ ...(sendId ? { sendId } : {}),
1135
1456
  };
1136
1457
  t.messages.push(full);
1137
1458
  t.activeLeafId = full.id;
1138
1459
  mdb.appendMessage(threadId, full);
1460
+ this.noteThreadActivity(threadId, full.at);
1139
1461
  this.emit({ type: "message", threadId, message: full });
1140
1462
  // The message frame alone leaves every client on the OLD branch: a
1141
1463
  // client adopts a new message as its leaf only when it chains onto the
@@ -1188,6 +1510,11 @@ export class Store {
1188
1510
  botByThread(threadId) {
1189
1511
  return this.bots.find((b) => b.threadId === threadId || b.tasks?.some((t) => t.threadId === threadId)) ?? null;
1190
1512
  }
1513
+ /** The one place a new bot's selection is decided: the caller's choice, or
1514
+ * the workspace default, completed with the workspace's new-bot defaults. */
1515
+ newBotSelection(requested) {
1516
+ return this.completeNewBotSelection(requested ?? this.defaultSelection());
1517
+ }
1191
1518
  createBot(profile = {}, opts = {}) {
1192
1519
  this.rememberSections([profile.section]);
1193
1520
  const name = profile.name?.trim() || pickBotName(this.bots.map((b) => b.name));
@@ -1204,8 +1531,10 @@ export class Store {
1204
1531
  color: profile.color ?? COLORS[this.bots.length % COLORS.length],
1205
1532
  ...(profile.mascotExpression ? { mascotExpression: profile.mascotExpression } : {}),
1206
1533
  ...(profile.mascotBody ? { mascotBody: profile.mascotBody } : {}),
1534
+ // Restricted from its first frame: no one else is ever told it exists.
1535
+ ...(profile.visibility && profile.visibility !== "everyone" ? { visibility: structuredClone(profile.visibility) } : {}),
1207
1536
  unread: false,
1208
- modelSelection: profile.modelSelection ?? this.defaultSelection(),
1537
+ modelSelection: this.newBotSelection(profile.modelSelection),
1209
1538
  resumeCursors: {},
1210
1539
  createdAt: Date.now(),
1211
1540
  };
@@ -1215,6 +1544,7 @@ export class Store {
1215
1544
  threadId: bot.threadId,
1216
1545
  title: UNTITLED_THREAD,
1217
1546
  createdAt: bot.createdAt,
1547
+ updatedAt: bot.createdAt,
1218
1548
  resumeCursors: {},
1219
1549
  modelSelection: structuredClone(bot.modelSelection),
1220
1550
  unread: false,
@@ -1266,12 +1596,16 @@ export class Store {
1266
1596
  if (at >= 0 || !operation.threadId || !operation.fields.name || !operation.fields.modelSelection)
1267
1597
  throw new Error("Invalid new bot in team setup");
1268
1598
  const createdAt = Date.now();
1599
+ const modelSelection = this.newBotSelection(operation.fields.modelSelection);
1269
1600
  next = { id: operation.botId, threadId: operation.threadId, name: operation.fields.name,
1270
1601
  title: "", description: "", soul: "", notifications: true, color: COLORS[nextBots.length % COLORS.length], unread: false,
1271
- modelSelection: operation.fields.modelSelection, resumeCursors: {}, createdAt, ...operation.fields,
1602
+ resumeCursors: {}, createdAt, ...operation.fields, modelSelection,
1272
1603
  approvalMode: "ask", autoApprove: false, composio: false, approvePeerComms: false,
1273
- tasks: [{ threadId: operation.threadId, title: UNTITLED_THREAD, createdAt, resumeCursors: {},
1274
- modelSelection: structuredClone(operation.fields.modelSelection), approvalMode: "ask", autoApprove: false,
1604
+ // A Chief's new teammate is seen by exactly the Chief's audience:
1605
+ // a restricted Chief never creates a bot everyone sees.
1606
+ ...(chief.visibility && chief.visibility !== "everyone" ? { visibility: structuredClone(chief.visibility) } : {}),
1607
+ tasks: [{ threadId: operation.threadId, title: UNTITLED_THREAD, createdAt, updatedAt: createdAt, resumeCursors: {},
1608
+ modelSelection: structuredClone(modelSelection), approvalMode: "ask", autoApprove: false,
1275
1609
  unread: false, activity: "idle", busy: false }],
1276
1610
  };
1277
1611
  nextBots.unshift(next);
@@ -1291,6 +1625,8 @@ export class Store {
1291
1625
  }));
1292
1626
  nextBots[at] = next;
1293
1627
  }
1628
+ if (operation.fields.chiefOfStaff === false)
1629
+ delete next.managedSections;
1294
1630
  next.section = sectionKey(next.section) || undefined;
1295
1631
  if (operation.fields.soul !== undefined) {
1296
1632
  next.soulHash = soulHash(operation.fields.soul);
@@ -1299,14 +1635,14 @@ export class Store {
1299
1635
  changed.push(next);
1300
1636
  }
1301
1637
  const result = { state: "applied", newTeams: request.newTeams, bots: changed.map((bot, index) => ({
1302
- id: bot.id, name: bot.name, section: bot.section, modelSelection: structuredClone(bot.modelSelection),
1638
+ id: bot.id, name: bot.name, section: bot.section, modelSelection: structuredClone(bot.modelSelection), chiefOfStaff: Boolean(bot.chiefOfStaff),
1303
1639
  action: request.operations[index].action === "create" ? "created" : "updated",
1304
1640
  })) };
1305
1641
  const chiefAt = nextBots.findIndex((bot) => bot.id === chief.id);
1306
1642
  const nextChief = { ...nextBots[chiefAt], lastTeamSetupReceipt: { requestId: request.requestId, result } };
1307
1643
  // Only the newly-created teams explicitly named in the human review may
1308
1644
  // extend this Chief's reach. Existing teams require owner settings.
1309
- if (request.newTeams.length) {
1645
+ if (request.newTeams.length && nextChief.chiefOfStaff) {
1310
1646
  nextChief.managedSections = managedSections;
1311
1647
  }
1312
1648
  nextBots[chiefAt] = nextChief;
@@ -1371,6 +1707,16 @@ export class Store {
1371
1707
  const bot = this.bot(id);
1372
1708
  if (!bot)
1373
1709
  return null;
1710
+ // Grants are the one field whose shape every writer must share, so the
1711
+ // store normalizes them itself: API patches arrive pre-validated, import
1712
+ // paths pass {}, and an internal caller that skips the parser still
1713
+ // lands canonical data or stops here.
1714
+ if (patch.connectorTools !== undefined) {
1715
+ const parsed = parseConnectorTools(patch.connectorTools);
1716
+ if (!parsed.ok)
1717
+ throw new Error(parsed.error);
1718
+ patch = { ...patch, connectorTools: parsed.grants };
1719
+ }
1374
1720
  // Runtime revocations must become effective in memory even when disk is
1375
1721
  // unavailable. Profile edits use the separate atomic path below.
1376
1722
  Object.assign(bot, patch);
@@ -1481,6 +1827,39 @@ export class Store {
1481
1827
  this.rememberSections([targetSection]);
1482
1828
  return { ok: true, bots: ids.map((id) => this.bot(id)) };
1483
1829
  }
1830
+ /** Apply only the membership edits the user made, in one bots-file write. */
1831
+ updateTeamMembers(section, addIds, removeIds) {
1832
+ const key = sectionKey(section);
1833
+ if (!key || !this.sections.includes(key))
1834
+ return { ok: false, reason: "unavailable" };
1835
+ const adds = new Set(addIds), removes = new Set(removeIds);
1836
+ const ids = new Set([...adds, ...removes]);
1837
+ for (const id of ids) {
1838
+ const bot = this.bot(id);
1839
+ if (!bot || bot.hidden)
1840
+ return { ok: false, reason: "unavailable" };
1841
+ if (adds.has(id) && removes.has(id))
1842
+ return { ok: false, reason: "membership-changed" };
1843
+ if (removes.has(id) && sectionKey(bot.section) !== key)
1844
+ return { ok: false, reason: "membership-changed" };
1845
+ }
1846
+ const next = this.bots.map(bot => ids.has(bot.id)
1847
+ ? { ...bot, section: adds.has(bot.id) ? key : undefined } : bot);
1848
+ for (const destination of [key, ""]) {
1849
+ if (next.filter(bot => bot.chiefOfStaff && sectionKey(bot.section) === destination).length > 1) {
1850
+ return { ok: false, reason: "chief-conflict" };
1851
+ }
1852
+ }
1853
+ if (ids.size) {
1854
+ this.saveBots(next);
1855
+ for (let i = 0; i < next.length; i++)
1856
+ if (ids.has(next[i].id))
1857
+ Object.assign(this.bots[i], next[i]);
1858
+ for (const botId of ids)
1859
+ this.emit({ type: "bot", botId });
1860
+ }
1861
+ return { ok: true, bots: this.bots.filter(bot => ids.has(bot.id)) };
1862
+ }
1484
1863
  /** Legacy bot/room activity occupies its own slot; direct conversations
1485
1864
  * use setTaskActivity so settling one thread cannot clear another. */
1486
1865
  setActivity(botId, activity) {
@@ -1509,6 +1888,13 @@ export class Store {
1509
1888
  task.turnStartedAt = Date.now();
1510
1889
  else if (!busy)
1511
1890
  delete task.turnStartedAt;
1891
+ // Reaching here means the thread just did something — exactly the
1892
+ // "activity" an until-activity snooze waits for, including settling
1893
+ // back to idle after a turn. Persist the wake like any task change.
1894
+ if (task.snoozedUntil === 0) {
1895
+ task.snoozedUntil = undefined;
1896
+ this.saveBots();
1897
+ }
1512
1898
  this.refreshBotActivity(bot);
1513
1899
  this.emit({ type: "bot", botId });
1514
1900
  return bot;
@@ -1551,6 +1937,64 @@ export class Store {
1551
1937
  this.emit({ type: "bot", botId: bot.id });
1552
1938
  return changed;
1553
1939
  }
1940
+ /** Company instance ids became stable across re-enrolment. Moves every
1941
+ * saved reference to an old id onto its replacement in one save: model
1942
+ * choices, native resume cursors and handed-message records. An entry that
1943
+ * already exists under the new id wins over the old one. */
1944
+ renameInstances(ids) {
1945
+ const touches = (record) => Boolean(record && Object.keys(record).some(key => ids.has(key)));
1946
+ const rename = (record) => {
1947
+ const next = {};
1948
+ for (const [key, value] of Object.entries(record)) {
1949
+ const target = ids.get(key);
1950
+ if (!target)
1951
+ next[key] = value;
1952
+ else if (!Object.prototype.hasOwnProperty.call(record, target))
1953
+ next[target] = value;
1954
+ }
1955
+ return next;
1956
+ };
1957
+ const changed = [];
1958
+ for (const bot of this.bots) {
1959
+ let dirty = false;
1960
+ const selected = ids.get(bot.modelSelection.instanceId);
1961
+ if (selected) {
1962
+ bot.modelSelection = { ...bot.modelSelection, instanceId: selected };
1963
+ dirty = true;
1964
+ }
1965
+ if (touches(bot.resumeCursors)) {
1966
+ bot.resumeCursors = rename(bot.resumeCursors);
1967
+ dirty = true;
1968
+ }
1969
+ for (const task of bot.tasks ?? []) {
1970
+ const taskSelected = task.modelSelection && ids.get(task.modelSelection.instanceId);
1971
+ if (task.modelSelection && taskSelected) {
1972
+ task.modelSelection = { ...task.modelSelection, instanceId: taskSelected };
1973
+ dirty = true;
1974
+ }
1975
+ if (touches(task.resumeCursors)) {
1976
+ task.resumeCursors = rename(task.resumeCursors);
1977
+ dirty = true;
1978
+ }
1979
+ if (task.handedMessages && touches(task.handedMessages)) {
1980
+ task.handedMessages = rename(task.handedMessages);
1981
+ dirty = true;
1982
+ }
1983
+ const last = task.lastInstanceId && ids.get(task.lastInstanceId);
1984
+ if (last) {
1985
+ task.lastInstanceId = last;
1986
+ dirty = true;
1987
+ }
1988
+ }
1989
+ if (dirty)
1990
+ changed.push(bot);
1991
+ }
1992
+ if (changed.length)
1993
+ this.saveBots();
1994
+ for (const bot of changed)
1995
+ this.emit({ type: "bot", botId: bot.id });
1996
+ return changed.length;
1997
+ }
1554
1998
  setResumeCursor(botId, instanceId, cursor, threadId) {
1555
1999
  const bot = this.bot(botId);
1556
2000
  if (!bot)
@@ -1795,8 +2239,16 @@ export class Store {
1795
2239
  Object.assign(task, { [key]: structuredClone(patch[key]) });
1796
2240
  }
1797
2241
  }
2242
+ // "Until new activity" ends the moment the thread has something new for
2243
+ // the person, and every unread wake funnels through patchTask — so this
2244
+ // one hook is the whole activity alarm. A time-based snooze is left to
2245
+ // its clock: attention overrides it on screen without clearing it.
2246
+ if (task.snoozedUntil === 0 && patch.unread === true)
2247
+ task.snoozedUntil = undefined;
1798
2248
  if (typeof patch.title === "string")
1799
2249
  task.title = patch.title.trim().slice(0, 80) || UNTITLED_THREAD;
2250
+ if (Object.prototype.hasOwnProperty.call(patch, "pinned") && task.pinned !== true)
2251
+ delete task.pinned;
1800
2252
  if (bot.threadId === threadId)
1801
2253
  this.mirrorActiveTask(bot, task);
1802
2254
  bot.unread = bot.tasks.some((candidate) => candidate.unread);
@@ -1804,6 +2256,28 @@ export class Store {
1804
2256
  this.emit({ type: "bot", botId });
1805
2257
  return task;
1806
2258
  }
2259
+ /** A Works on change is the newest explicit choice, so this bot's
2260
+ * machine-recorded pins that now point somewhere else give way. A pin a
2261
+ * person set, a legacy pin with unknown provenance, and a pin that already
2262
+ * matches the new destination survive. Returns how many pins were cleared. */
2263
+ clearAutoSurfacePins(botId, destination) {
2264
+ const bot = this.bot(botId);
2265
+ if (!bot?.tasks)
2266
+ return 0;
2267
+ let cleared = 0;
2268
+ for (const task of bot.tasks) {
2269
+ if (task.surface === undefined || task.surfaceSource !== "auto" || task.surface === destination)
2270
+ continue;
2271
+ task.surface = undefined;
2272
+ task.surfaceSource = undefined;
2273
+ cleared++;
2274
+ }
2275
+ if (!cleared)
2276
+ return 0;
2277
+ this.saveBots();
2278
+ this.emit({ type: "bot", botId });
2279
+ return cleared;
2280
+ }
1807
2281
  /** Model/provider changes are one configuration transaction: never publish
1808
2282
  * a new provider before its confirmed approval downgrade, or change the
1809
2283
  * default while leaving the selected thread behind after a write failure. */
@@ -1814,8 +2288,8 @@ export class Store {
1814
2288
  return null;
1815
2289
  const patch = { modelSelection: structuredClone(selection),
1816
2290
  ...(resetApprovalToAsk ? { approvalMode: "ask", autoApprove: false, alwaysAllow: [] } : {}) };
1817
- const nextTask = { ...task, ...taskPatch, ...patch,
1818
- ...(typeof taskPatch.title === "string" ? { title: taskPatch.title.trim().slice(0, 80) || UNTITLED_THREAD } : {}) };
2291
+ const nextTask = persistedPin({ ...task, ...taskPatch, ...patch,
2292
+ ...(typeof taskPatch.title === "string" ? { title: taskPatch.title.trim().slice(0, 80) || UNTITLED_THREAD } : {}) });
1819
2293
  // Older threads may still inherit settings. Freeze their effective
1820
2294
  // values before updating the default so "other threads unchanged" also
1821
2295
  // holds for workspaces created before per-thread approval settings.
@@ -1835,6 +2309,20 @@ export class Store {
1835
2309
  this.emit({ type: "bot", botId });
1836
2310
  return task;
1837
2311
  }
2312
+ /** One durable write: never leave only part of a bot's threads updated. */
2313
+ setAllThreadApprovalMode(botId, mode) {
2314
+ const bot = this.bot(botId);
2315
+ if (!bot)
2316
+ return null;
2317
+ const patch = { approvalMode: mode, autoApprove: false, alwaysAllow: [] };
2318
+ const tasks = (bot.tasks ?? []).map(task => ({ ...task, ...patch }));
2319
+ const next = { ...bot, ...patch, approvalGrant: undefined, tasks };
2320
+ this.saveBots(this.bots.map(candidate => candidate === bot ? next : candidate));
2321
+ bot.tasks?.forEach((task, index) => Object.assign(task, tasks[index]));
2322
+ Object.assign(bot, patch, { approvalGrant: undefined });
2323
+ this.emit({ type: "bot", botId });
2324
+ return bot;
2325
+ }
1838
2326
  mirrorActiveTask(bot, task) {
1839
2327
  bot.threadId = task.threadId;
1840
2328
  bot.resumeCursors = structuredClone(task.resumeCursors);
@@ -1843,23 +2331,25 @@ export class Store {
1843
2331
  }
1844
2332
  /** A fresh context on the same bot: new thread, new session, same
1845
2333
  * persona/tools/computer. Becomes the active task. */
1846
- createTask(botId, title, activate = true, projectId, openedBy) {
2334
+ createTask(botId, title, activate = true, projectId, openedBy, approvalMode) {
1847
2335
  const bot = this.bot(botId);
1848
2336
  if (!bot)
1849
2337
  return null;
1850
2338
  if (projectId !== undefined && !this.project(botId, projectId))
1851
2339
  return null;
2340
+ const createdAt = Date.now();
1852
2341
  const task = {
1853
2342
  threadId: newId(),
1854
2343
  title: threadTitleFrom(title),
1855
- createdAt: Date.now(),
2344
+ createdAt,
2345
+ updatedAt: createdAt,
1856
2346
  ...(projectId ? { projectId } : {}),
1857
2347
  ...(openedBy ? { openedBy: structuredClone(openedBy) } : {}),
1858
2348
  resumeCursors: {},
1859
2349
  modelSelection: structuredClone(bot.modelSelection),
1860
- approvalMode: approvalModeFor(bot),
1861
- autoApprove: Boolean(bot.autoApprove),
1862
- alwaysAllow: [...(bot.alwaysAllow ?? [])],
2350
+ approvalMode: approvalMode ?? approvalModeFor(bot),
2351
+ autoApprove: approvalMode ? false : Boolean(bot.autoApprove),
2352
+ alwaysAllow: approvalMode ? [] : [...(bot.alwaysAllow ?? [])],
1863
2353
  unread: false,
1864
2354
  activity: "idle",
1865
2355
  busy: false,
@@ -1932,12 +2422,29 @@ export class Store {
1932
2422
  * interleave in one transcript. `label` names that thread; the caller
1933
2423
  * closes it once its result has been reported. A pair conversation
1934
2424
  * never auto-closes. */
2425
+ /** The identity a peer-opened row belongs to: its opener's id while that
2426
+ * bot lives, else the one live bot the stamp's name still points at —
2427
+ * what a deleted-and-recreated same-name bot inherits — else the dead id
2428
+ * itself. A name two live bots share resolves to nobody's twin:
2429
+ * ambiguous means unmatched, never a wrong merge. */
2430
+ openerIdentity(openedBy) {
2431
+ if (this.bot(openedBy.botId))
2432
+ return openedBy.botId;
2433
+ const named = this.bots.filter((bot) => bot.name === openedBy.name);
2434
+ return named.length === 1 ? named[0].id : openedBy.botId;
2435
+ }
1935
2436
  resolvePairConversation(sender, recipientId, options) {
1936
2437
  if (!this.bot(recipientId))
1937
2438
  return null;
1938
2439
  const title = `@${sender.name}`;
1939
2440
  const opener = (kind, at = Date.now()) => ({ botId: sender.id, name: sender.name, kind, at });
1940
- const fromSender = this.tasks(recipientId).filter((task) => task.openedBy?.botId === sender.id);
2441
+ // Identity-stable: the sender's own rows, plus ones a deleted
2442
+ // predecessor opened when the stamp's name still points at exactly
2443
+ // this bot — a same-name recreation inherits the conversation instead
2444
+ // of minting a twin while the old row dangles live. A name two live
2445
+ // bots share matches nobody's inheritance: refusing the fallback can
2446
+ // cost a new row, never merge two bots' histories.
2447
+ const fromSender = this.tasks(recipientId).filter((task) => task.openedBy && this.openerIdentity(task.openedBy) === sender.id);
1941
2448
  let pair = fromSender.find((task) => task.openedBy?.kind === "pair");
1942
2449
  if (!pair) {
1943
2450
  const lastActivity = (task) => this.messagesTail(task.threadId, 1).messages.at(-1)?.at ?? task.openedBy?.at ?? task.createdAt;
@@ -1966,6 +2473,14 @@ export class Store {
1966
2473
  // back up, never replaced: closing is only the sidebar's idle state.
1967
2474
  if (pair.closedBy)
1968
2475
  this.setTaskClosedBy(recipientId, pair.threadId, null);
2476
+ // An inherited row carries the predecessor's id; rebind it to the
2477
+ // live bot so the name fallback is needed only once — a namesake
2478
+ // appearing later cannot claim the row. The hour it was opened
2479
+ // stays: inheritance is not a new conversation.
2480
+ const inherited = pair.openedBy;
2481
+ if (inherited && inherited.botId !== sender.id) {
2482
+ this.setTaskOpenedBy(recipientId, pair.threadId, { ...inherited, botId: sender.id, name: sender.name });
2483
+ }
1969
2484
  return { task: pair, created: false };
1970
2485
  }
1971
2486
  // The brief is never a title. An 80-character slice of an assignment