openmausbot 0.1.86 → 0.1.88

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 (216) hide show
  1. package/README.md +4 -3
  2. package/dist/assets/{abnfDiagram-O67JEVCF-DpUZNnFD.js → abnfDiagram-O67JEVCF-qd9VD3GV.js} +1 -1
  3. package/dist/assets/{arc-DW7mrI4p.js → arc-3RwkUjRe.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-CyeZ5WLZ.js → architectureDiagram-NJMV4G6O-CFKSJS7a.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-CBVaNjO7.js → blockDiagram-BEXU5L5S-CG1rEfWD.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-sf8d41uT.js → c4Diagram-YGBWAQC7-CBW9N664.js} +1 -1
  7. package/dist/assets/channel-CnRnO3tF.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-BOGvy7Y_.js → chunk-2Q5K7J3B-2mRfTksB.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-BiF9zyiz.js → chunk-3FUC2YCW-Cv1dpErj.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-EFuZ1SBZ.js → chunk-5VM5RSS4-B7IACJp-.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-pJLNSAT7.js → chunk-GTNCS2PH-An0hZtYa.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-Cb-3_3IT.js → chunk-GWA4HPMP-BcJB2nEm.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-DAf5aogn.js → chunk-JWPE2WC7-CV_W7TN7.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-DR3kiwZz.js → chunk-NETBCI7D-Bcs_kwnA.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-BmU8PeyM.js → chunk-WEXAMYUT-lrx_DxFP.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-Buh8g8OT.js → chunk-XXDRQBXY-DabHlLGM.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-BurfmMWM.js → classDiagram-v2-NBCMYWYE-BnHj6P7d.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-KkGZUEkU.js → cose-bilkent-JH36ORCC-CfLp6UDi.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-C3PpV96d.js → cynefin-OW5HDTMX-Dm4dbVA1.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-Dt8JwJaI.js → cynefinDiagram-VND7K2PF-B9mH-PID.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-DkjN8266.js → dagre-6A5THRUB-DDc_NVQJ.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-DUAJS0JV.js → diagram-22UHCM2B-lwr8Ip8-.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-Dw4KhF1a.js → diagram-3UASUU5V-BLLe6dmI.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-Dd1gmQqf.js → diagram-ATOU4E4O-6eUIq4lz.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-CjwGs0Yw.js → diagram-CDSNMT55-O-pRN7VQ.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-BL-69Zrq.js → diagram-MLGK6HIB-BietvGAI.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-5Zi2N7wo.js → diagram-MPIPVDR6-Bd2Cl62X.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-D9NekWUX.js → ebnfDiagram-ZINNZB2B-B6iDy69W.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-CkaSQjxw.js → elk-276RUBZZ-DQVHfKe0.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-CdAVBV8-.js → erDiagram-OPXOYQCR-BWdVp2tQ.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-Bm_lWWH7.js → flowDiagram-KWPJA3E3-WLE6rKB4.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-FZ8ejYWJ.js → ganttDiagram-FUAMR5RP-BbjS-4gn.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-Dlv9rizZ.js → gitGraphDiagram-X574FWY7-Dpy2QSGq.js} +1 -1
  34. package/dist/assets/index-BIMdIbaw.css +1 -0
  35. package/dist/assets/index-CHnODCr2.js +583 -0
  36. package/dist/assets/{index-h9FFNwTu.js → index-CIaHn5NS.js} +1 -1
  37. package/dist/assets/{infoDiagram-VRGFBTTK-BdQ_gW5B.js → infoDiagram-VRGFBTTK-D_jKWlKe.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-EwssEp2X.js → ishikawaDiagram-OU5B5YK6-DLJohSuK.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-BX1G1vOd.js → journeyDiagram-ZHPQQLJL-DPOFj0qN.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-BB4kAsyC.js → kanban-definition-PNTS6WVX-BI7v-xpK.js} +1 -1
  41. package/dist/assets/{linear-3su2TG93.js → linear-C2pWPSxO.js} +1 -1
  42. package/dist/assets/{mermaid.core-CDD1E0s9.js → mermaid.core-CR-j5WQU.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-Ba50MsNK.js → mindmap-definition-NLK3R4M7-lz9GvXRh.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-DZLkkZlM.js → pegDiagram-GJSIUBJH-Dl4_ia61.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-DaTz77Uq.js → pieDiagram-5QR66LMP-DWuT3n88.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-CpaiidpR.js → quadrantDiagram-O4NWA36T-DXWe9Jx7.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-mN4JrMeI.js → railroadDiagram-XR7U4H2S-COY1IBVY.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-BSNDRpg7.js → requirementDiagram-PLB6GJNP-DnyVuhsQ.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-FngzGNug.js → sankeyDiagram-IPEJSGJF-BSc4NdGK.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-CM4pCC3L.js → sequenceDiagram-PO4LG4MO-kqEU2EGq.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-CRICHXkw.js → sizeCapture-INFHLROL-BvB4bkWD.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-oyTWpxH2.js → stateDiagram-v2-GCMORJYK-BIrqNv7y.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-D9h9mPBq.js → swimlanes-2SLR337P-CFnalQUE.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-C4a8qNGj.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-BJUlN9bW.js → timeline-definition-EJHVYXUP-CsSWJnSQ.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-3uBNkbAb.js → usecaseDiagram-POWQR4AR-DkAmJ1EE.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-D2odalzT.js → vennDiagram-UO4OBE2U-C3vd9drL.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-DHgFaB9S.js → wardleyDiagram-VNRHLVJA-j26CM-pi.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-w5GYM2h0.js → xychartDiagram-PMCCYNJV-BYu1Q_at.js} +1 -1
  60. package/dist/index.html +2 -2
  61. package/dist-server/browser-proxy.js +2 -1
  62. package/dist-server/companion/src/routes.js +8 -1
  63. package/dist-server/connector-proxy.js +105 -1
  64. package/dist-server/container-mcp.js +1032 -165
  65. package/dist-server/drivers/agents-proxy.js +338 -29
  66. package/dist-server/drivers/phone-proxy.js +33 -2
  67. package/dist-server/index.js +40590 -31811
  68. package/dist-server/local-computer-proxy.js +5 -3
  69. package/dist-server/local-computer.js +53 -17
  70. package/dist-server/mcp-gate.js +2 -0
  71. package/dist-server/mcp-server.js +5 -3
  72. package/dist-server/message-search.worker.js +52 -0
  73. package/dist-server/openmausbot.js +4457 -1399
  74. package/dist-server/pair-cli.js +4457 -1399
  75. package/dist-server/permission-proxy.js +29 -9
  76. package/dist-server/server/admission.js +81 -0
  77. package/dist-server/server/aside-queue.js +294 -0
  78. package/dist-server/server/attachments.js +98 -1
  79. package/dist-server/server/auto-approve.js +4 -3
  80. package/dist-server/server/auto-vm-claims.js +1 -0
  81. package/dist-server/server/bot-attachment.js +107 -0
  82. package/dist-server/server/bot-overview.js +19 -0
  83. package/dist-server/server/bot-package.js +18 -292
  84. package/dist-server/server/bot-profile.js +5 -17
  85. package/dist-server/server/box-create-idempotency.js +3 -3
  86. package/dist-server/server/box.js +57 -55
  87. package/dist-server/server/browser-proxy.js +3 -2
  88. package/dist-server/server/browser-runtime.js +37 -1
  89. package/dist-server/server/cli-api-setup.js +1 -0
  90. package/dist-server/server/cli.js +2 -2
  91. package/dist-server/server/command-allowlist.js +154 -0
  92. package/dist-server/server/composio.js +297 -1
  93. package/dist-server/server/computer-wait.js +22 -9
  94. package/dist-server/server/config.js +84 -12
  95. package/dist-server/server/connector-advertisement.js +165 -0
  96. package/dist-server/server/connector-proxy.js +14 -2
  97. package/dist-server/server/connector-verdict.js +168 -0
  98. package/dist-server/server/container-computer.js +61 -1
  99. package/dist-server/server/contracts.js +22 -0
  100. package/dist-server/server/default-model-selection.js +9 -0
  101. package/dist-server/server/delegations.js +85 -34
  102. package/dist-server/server/drivers/acp/core.js +80 -20
  103. package/dist-server/server/drivers/acp/cursor.js +3 -7
  104. package/dist-server/server/drivers/acp/droid.js +4 -7
  105. package/dist-server/server/drivers/acp/gemini.js +4 -4
  106. package/dist-server/server/drivers/acp/grok.js +4 -9
  107. package/dist-server/server/drivers/acp/hermes.js +3 -5
  108. package/dist-server/server/drivers/acp/opencode-go.js +2 -5
  109. package/dist-server/server/drivers/acp/qwen.js +9 -15
  110. package/dist-server/server/drivers/agents-call.js +179 -13
  111. package/dist-server/server/drivers/agents-catalog.js +134 -10
  112. package/dist-server/server/drivers/boxagent.js +160 -36
  113. package/dist-server/server/drivers/builtIn.js +2 -0
  114. package/dist-server/server/drivers/chat-box-tools.js +90 -0
  115. package/dist-server/server/drivers/chat-images.js +54 -0
  116. package/dist-server/server/drivers/chat-mcp-tools.js +38 -8
  117. package/dist-server/server/drivers/chat-tool-approval.js +70 -18
  118. package/dist-server/server/drivers/claude.js +38 -19
  119. package/dist-server/server/drivers/codex-catalog.js +14 -6
  120. package/dist-server/server/drivers/codex-identity.js +2 -1
  121. package/dist-server/server/drivers/codex-instructions.js +48 -6
  122. package/dist-server/server/drivers/codex-release.js +48 -0
  123. package/dist-server/server/drivers/codex-tool-surface.js +20 -0
  124. package/dist-server/server/drivers/codex.js +102 -55
  125. package/dist-server/server/drivers/mistral.js +115 -0
  126. package/dist-server/server/drivers/openai-chat.js +180 -33
  127. package/dist-server/server/drivers/openai-compat.js +1 -0
  128. package/dist-server/server/drivers/permission-command.js +35 -0
  129. package/dist-server/server/drivers/phone-proxy.js +47 -2
  130. package/dist-server/server/drivers/pi.js +306 -28
  131. package/dist-server/server/drivers/prompt-split.js +110 -0
  132. package/dist-server/server/drivers/session-idle.js +29 -0
  133. package/dist-server/server/env-path.js +15 -0
  134. package/dist-server/server/fleet.js +1 -1
  135. package/dist-server/server/group-thread-usage.js +103 -0
  136. package/dist-server/server/guarded-requests.js +2 -2
  137. package/dist-server/server/harness/registry.js +1 -0
  138. package/dist-server/server/harness-capabilities.js +46 -0
  139. package/dist-server/server/hosted-models.js +1 -1
  140. package/dist-server/server/index.js +2396 -505
  141. package/dist-server/server/local-computer.js +72 -23
  142. package/dist-server/server/managed-desktop.js +1 -1
  143. package/dist-server/server/mcp-bridge.js +6 -6
  144. package/dist-server/server/message-db.js +22 -42
  145. package/dist-server/server/message-file.js +9 -1
  146. package/dist-server/server/message-search-query.js +43 -0
  147. package/dist-server/server/message-search-worker.js +80 -0
  148. package/dist-server/server/message-search.worker.js +21 -0
  149. package/dist-server/server/model-requests.js +248 -0
  150. package/dist-server/server/new-bot-defaults.js +115 -0
  151. package/dist-server/server/options-card.js +21 -0
  152. package/dist-server/server/org-library.js +1065 -0
  153. package/dist-server/server/package-export.js +627 -39
  154. package/dist-server/server/package-import.js +458 -0
  155. package/dist-server/server/package-parts.js +87 -0
  156. package/dist-server/server/peer-approval.js +16 -7
  157. package/dist-server/server/peer-delivery.js +60 -0
  158. package/dist-server/server/peer-roster.js +67 -0
  159. package/dist-server/server/presets.js +492 -0
  160. package/dist-server/server/profile-requests.js +66 -39
  161. package/dist-server/server/profile-revision.js +9 -1
  162. package/dist-server/server/profile-versions.js +6 -3
  163. package/dist-server/server/provider-key-check.js +3 -2
  164. package/dist-server/server/published-teams.js +80 -0
  165. package/dist-server/server/room-handoffs.js +19 -3
  166. package/dist-server/server/routes/bot-presets.js +25 -0
  167. package/dist-server/server/routine-requests.js +35 -10
  168. package/dist-server/server/routines.js +41 -1
  169. package/dist-server/server/section-context.js +10 -0
  170. package/dist-server/server/skill-fetch.js +72 -14
  171. package/dist-server/server/skills.js +35 -61
  172. package/dist-server/server/steer-queue.js +3 -2
  173. package/dist-server/server/store.js +291 -17
  174. package/dist-server/server/surface.js +10 -4
  175. package/dist-server/server/system-prompt.js +38 -6
  176. package/dist-server/server/team-backup.js +6 -2
  177. package/dist-server/server/team-computers.js +10 -0
  178. package/dist-server/server/team-library.js +6 -2
  179. package/dist-server/server/team-manifest.js +4 -3
  180. package/dist-server/server/team-setup-requests.js +139 -27
  181. package/dist-server/server/thread-events.js +20 -2
  182. package/dist-server/server/tightening-requests.js +405 -0
  183. package/dist-server/server/tts/chatterbox.js +27 -5
  184. package/dist-server/server/turn-context.js +2 -0
  185. package/dist-server/server/turn-watchdog.js +12 -3
  186. package/dist-server/server/usage-ledger.js +19 -2
  187. package/dist-server/server/vps-computer.js +139 -51
  188. package/dist-server/server/vps-container-mcp.js +7 -0
  189. package/dist-server/server/vps-ssh.js +36 -7
  190. package/dist-server/server/workspace-backup-policy.js +12 -0
  191. package/dist-server/server/workspace-backup.js +42 -5
  192. package/dist-server/server/workspace-backup.worker.js +10 -0
  193. package/dist-server/server/workspace.js +1 -1
  194. package/dist-server/shared/approval-mode.js +1 -1
  195. package/dist-server/shared/ask-question.js +207 -12
  196. package/dist-server/shared/bot-profile.js +15 -0
  197. package/dist-server/shared/command-allowlist.js +1 -0
  198. package/dist-server/shared/credential-request.js +6 -2
  199. package/dist-server/shared/line-diff.js +23 -0
  200. package/dist-server/shared/model-request.js +1 -0
  201. package/dist-server/shared/options-card.js +51 -0
  202. package/dist-server/shared/package-format.js +967 -0
  203. package/dist-server/shared/profile-request.js +5 -4
  204. package/dist-server/shared/skill-md.js +64 -0
  205. package/dist-server/shared/team-backup.js +7 -0
  206. package/dist-server/shared/tightening-request.js +87 -0
  207. package/dist-server/shared/wire.js +4 -0
  208. package/dist-server/src/lib/composer-attachments.js +25 -3
  209. package/dist-server/vps-container-mcp.js +1132 -171
  210. package/dist-server/workspace-backup.worker.js +19474 -0
  211. package/enterprise/README.md +1 -1
  212. package/package.json +1 -1
  213. package/dist/assets/channel-BXFkgus9.js +0 -1
  214. package/dist/assets/index-BGgqlWPJ.css +0 -1
  215. package/dist/assets/index-BtHo8ZKl.js +0 -577
  216. package/dist/assets/swimlanesDiagram-TC7HE7FX-BWrvB2ak.js +0 -8
@@ -1,11 +1,11 @@
1
1
  // propose_profile: a bot proposes changes to its own name, title, description,
2
- // SOUL.md, or working folder; confirmed cards and Full Access submissions share
2
+ // SOUL.md, working folder, or alert/voice toggles; confirmed cards and Full Access submissions share
3
3
  // the same validated commit path. Same
4
4
  // shape as routine-requests.ts, much smaller: the profile commits in one
5
5
  // store call, and staleness is a hash of the five fields instead of a
6
6
  // scheduler revision. Everything here is re-validated at confirm time —
7
7
  // a card can sit open for days.
8
- import { lineDiff } from "../shared/line-diff.js";
8
+ import { soulDiffLines } from "../shared/line-diff.js";
9
9
  import { BOT_PROFILE_LIMITS } from "../shared/bot-profile.js";
10
10
  import { PROFILE_REQUEST_FIELDS } from "../shared/profile-request.js";
11
11
  import { parseBotProfilePatch } from "./bot-profile.js";
@@ -15,22 +15,32 @@ import { profileRevision, profileSnapshot } from "./profile-revision.js";
15
15
  import { recordProfileChange } from "./profile-versions.js";
16
16
  import { redactSecretsInText } from "./redact.js";
17
17
  const MAX_REASON = 500;
18
- const MAX_DIFF_LINES = 400;
19
18
  const STALE = "This bot's profile changed after this card was prepared. Ask the bot to review it and propose again.";
20
19
  const NO_SUCH_BOT = "That bot no longer exists";
21
20
  const LABELS = {
22
21
  name: "Name",
23
22
  title: "Title",
24
23
  description: "Description",
24
+ notifications: "Notifications",
25
+ speakReplies: "Speak replies",
25
26
  };
26
27
  const PRIVATE_WORKSPACE = "its private workspace";
27
- const CHOOSE_ONE = "Choose at least one of name, title, description, soul, cwd";
28
+ const CHOOSE_ONE = "Choose at least one of name, title, description, soul, cwd, notifications, speakReplies";
29
+ const toggleText = (value) => (value ? "on" : "off");
30
+ /** The fields that change what a bot is told: every card field except the
31
+ * working folder and the two toggles. */
32
+ const TEXT_FIELDS = ["name", "title", "description", "soul"];
28
33
  export class ProfileRequestError extends Error {
29
34
  status;
30
- constructor(message, status = 400) {
35
+ /** True when no retry of this card can ever succeed — the proposal went
36
+ * stale (revision mismatch, target or folder gone) and the card must
37
+ * settle as expired instead of staying actionable. */
38
+ terminal;
39
+ constructor(message, status = 400, options) {
31
40
  super(message);
32
41
  this.name = "ProfileRequestError";
33
42
  this.status = status;
43
+ this.terminal = options?.terminal === true;
34
44
  }
35
45
  }
36
46
  function reasonText(value) {
@@ -44,8 +54,9 @@ function reasonText(value) {
44
54
  }
45
55
  return redactSecretsInText(trimmed);
46
56
  }
47
- /** `parseBotProfilePatch` would silently accept a key like `notifications` —
48
- * valid for the broader bot patch, not for a profile request. Reject keys
57
+ /** `parseBotProfilePatch` would silently accept a key like `voice` —
58
+ * valid for the broader bot patch, not for a profile request (a voice pick
59
+ * needs the harness catalog rendered on the card). Reject keys
49
60
  * outside the proposable fields ourselves first, for exact copy. */
50
61
  function parseChanges(input) {
51
62
  if (typeof input !== "object" || input === null || Array.isArray(input)) {
@@ -84,6 +95,11 @@ function parseChanges(input) {
84
95
  if (field === "cwd")
85
96
  continue;
86
97
  const value = parsed.patch[field];
98
+ if (field === "notifications" || field === "speakReplies") {
99
+ if (value !== undefined)
100
+ changes[field] = value;
101
+ continue;
102
+ }
87
103
  // This payload is hidden under the card's visible fields, so the store's
88
104
  // shallow card redaction cannot reach it. Scrub before it is persisted.
89
105
  if (typeof value !== "string")
@@ -125,36 +141,27 @@ export function profileCardCopy(target, fullProfile, before, changes, reason) {
125
141
  continue;
126
142
  if (changes[field] === undefined)
127
143
  continue;
144
+ if (field === "notifications" || field === "speakReplies") {
145
+ lines.push(`${LABELS[field]}: ${toggleText(before[field])} → ${toggleText(changes[field])}`);
146
+ continue;
147
+ }
128
148
  lines.push(`${LABELS[field]}: "${before[field] ?? ""}" → "${changes[field]}"`);
129
149
  }
130
150
  if (changes.cwd !== undefined) {
131
151
  lines.push(`Working folder: ${before.cwd || PRIVATE_WORKSPACE} → ${changes.cwd || PRIVATE_WORKSPACE}`);
132
152
  }
133
153
  if (changes.soul !== undefined) {
134
- const beforeSoul = before.soul ?? "";
135
- const afterSoul = changes.soul;
136
- const bytesBefore = Buffer.byteLength(beforeSoul, "utf8");
137
- const bytesAfter = Buffer.byteLength(afterSoul, "utf8");
138
- lines.push(`SOUL.md (${bytesBefore} → ${bytesAfter} bytes):`);
139
- const diff = lineDiff(beforeSoul, afterSoul);
140
- if (diff.length > MAX_DIFF_LINES) {
141
- // Truncating a diff can hide the very instructions being approved.
142
- // The full replacement is already byte-bounded by profile validation
143
- // and is readable in the same generic card on desktop and phones.
144
- lines.push("Large change — complete proposed instructions (replaces the current SOUL.md):");
145
- lines.push(afterSoul || "(empty)");
146
- }
147
- else {
148
- lines.push(...diff);
149
- }
154
+ lines.push(...soulDiffLines(before.soul ?? "", changes.soul));
150
155
  }
151
156
  // The closing line says the consequence of exactly what is on the card:
152
- // a folder change moves where the bot's tools read and write; the other
153
- // fields change what it is told. Either way nothing runs on confirm.
154
- const onlyFolder = Object.keys(changes).every((field) => field === "cwd");
157
+ // a folder change moves where the bot's tools read and write; the text
158
+ // fields change what it is told; a card of only toggles, only a folder,
159
+ // or both carries no instruction change at all. Either way nothing runs
160
+ // on confirm.
161
+ const changesText = TEXT_FIELDS.some((field) => changes[field] !== undefined);
155
162
  if (changes.cwd !== undefined)
156
163
  lines.push(`${target.name}'s tools will read and write files in that folder.`);
157
- lines.push(onlyFolder ? "Nothing runs." : `Changes what ${target.name} is told on every turn. Nothing runs.`);
164
+ lines.push(changesText ? `Changes what ${target.name} is told on every turn. Nothing runs.` : "Nothing runs.");
158
165
  const detail = lines.join("\n");
159
166
  const fields = PROFILE_REQUEST_FIELDS.filter((field) => changes[field] !== undefined);
160
167
  const summary = `${title} · ${fields.join(", ")}`;
@@ -199,13 +206,20 @@ export class ProfileRequestService {
199
206
  const before = {};
200
207
  const finalChanges = {};
201
208
  for (const field of PROFILE_REQUEST_FIELDS) {
202
- const value = changes[field];
203
- if (value === undefined)
204
- continue;
205
- if (value === snapshot[field])
206
- continue;
207
- before[field] = redactSecretsInText(snapshot[field]);
208
- finalChanges[field] = value;
209
+ if (field === "notifications" || field === "speakReplies") {
210
+ const value = changes[field];
211
+ if (value === undefined || value === snapshot[field])
212
+ continue;
213
+ before[field] = snapshot[field];
214
+ finalChanges[field] = value;
215
+ }
216
+ else {
217
+ const value = changes[field];
218
+ if (value === undefined || value === snapshot[field])
219
+ continue;
220
+ before[field] = redactSecretsInText(snapshot[field]);
221
+ finalChanges[field] = value;
222
+ }
209
223
  }
210
224
  if (Object.keys(finalChanges).length === 0) {
211
225
  throw new ProfileRequestError("Nothing would change");
@@ -290,20 +304,29 @@ export class ProfileRequestService {
290
304
  throw new ProfileRequestError("This profile confirmation card is no longer available", 409);
291
305
  return { claimed: true, state: "already_settled", behavior: "allow" };
292
306
  }
307
+ // An expired card is settled terminal state, not a decision waiting on
308
+ // a slower click: the proposal it carried can never be confirmed as
309
+ // prepared, even when the condition that expired it reverses (the
310
+ // revision matches again, the folder comes back). The committed
311
+ // receipt above still recovers a write that already happened; nothing
312
+ // past this point can.
313
+ if (card.expired) {
314
+ return { claimed: true, state: "invalid", error: "This profile request expired before it was confirmed. Ask for a fresh proposal.", status: 409 };
315
+ }
293
316
  if (args.behavior === "deny") {
294
317
  this.store.patchMessage(args.threadId, message.id, { card: { ...card, answered: "deny", held: undefined } });
295
318
  return { claimed: true, state: "denied" };
296
319
  }
297
320
  if (!target)
298
- throw new ProfileRequestError(NO_SUCH_BOT, 404);
321
+ throw new ProfileRequestError(NO_SUCH_BOT, 404, { terminal: true });
299
322
  const crossBot = payload.targetBotId !== payload.botId;
300
323
  if (crossBot && this.validateTarget) {
301
324
  const refusal = this.validateTarget(payload.botId, payload.targetBotId);
302
325
  if (refusal)
303
- throw new ProfileRequestError(refusal, 404);
326
+ throw new ProfileRequestError(refusal, 404, { terminal: true });
304
327
  }
305
328
  if (profileRevision(target) !== payload.expectedRevision) {
306
- throw new ProfileRequestError(STALE, 409);
329
+ throw new ProfileRequestError(STALE, 409, { terminal: true });
307
330
  }
308
331
  const { cwd, ...rest } = payload.changes;
309
332
  const validated = Object.keys(rest).length ? parseChanges(rest) : {};
@@ -314,7 +337,7 @@ export class ProfileRequestService {
314
337
  // applicable as prepared.
315
338
  const checked = validateBotCwd(cwd || null);
316
339
  if (!checked.ok)
317
- throw new ProfileRequestError(checked.error, 409);
340
+ throw new ProfileRequestError(checked.error, 409, { terminal: true });
318
341
  patch.cwd = checked.cwd ?? undefined;
319
342
  }
320
343
  if (!this.store.patchBotProfile(target.id, patch))
@@ -333,12 +356,16 @@ export class ProfileRequestService {
333
356
  const status = error instanceof ProfileRequestError ? error.status : 400;
334
357
  const detail = error instanceof Error ? error.message : String(error);
335
358
  const saved = this.store.bot(payload.targetBotId)?.lastProfileRequestId === payload.requestId;
359
+ // A terminal failure (stale revision, target gone) can never be
360
+ // confirmed as prepared: settle the card as expired with its options
361
+ // removed so it stops looking actionable, and say so in one line.
362
+ const expired = !saved && error instanceof ProfileRequestError && error.terminal;
336
363
  const notice = card.dismissed && card.options.length === 0
337
364
  ? "Profile saved. Recording the operation receipt could not finish; the changes will not be applied again."
338
365
  : "Profile saved. Confirm again to finish recording this decision; the changes will not be applied again.";
339
366
  try {
340
367
  this.store.patchMessage(args.threadId, message.id, {
341
- card: { ...card, held: saved ? notice : redactSecretsInText(detail).slice(0, 500) },
368
+ card: { ...card, ...(expired ? { expired: true, options: [] } : {}), held: saved ? notice : redactSecretsInText(detail).slice(0, 500) },
342
369
  });
343
370
  }
344
371
  catch { /* The durable profile receipt still permits a safe retry. */ }
@@ -4,7 +4,15 @@
4
4
  // routine's expectedUpdatedAt (bots have no updatedAt of their own).
5
5
  import { createHash } from "node:crypto";
6
6
  export function profileSnapshot(bot) {
7
- return { name: bot.name, title: bot.title, description: bot.description, soul: bot.soul ?? "", cwd: bot.cwd ?? "" };
7
+ return {
8
+ name: bot.name,
9
+ title: bot.title,
10
+ description: bot.description,
11
+ soul: bot.soul ?? "",
12
+ cwd: bot.cwd ?? "",
13
+ notifications: bot.notifications ?? true,
14
+ speakReplies: bot.speakReplies ?? false,
15
+ };
8
16
  }
9
17
  export function profileRevision(bot) {
10
18
  // A later proposal can restore identical text. Keep its private receipt
@@ -60,11 +60,14 @@ async function writeRows(botId, rows) {
60
60
  export function recordProfileChange(botId, actor, via, before, after) {
61
61
  const at = Date.now();
62
62
  const rows = [];
63
+ // Toggles land in history as the same on/off wording the card shows;
64
+ // every other proposable field is text.
65
+ const historyText = (value) => value === undefined ? "" : typeof value === "boolean" ? (value ? "on" : "off") : value;
63
66
  for (const field of PROFILE_REQUEST_FIELDS) {
64
- const b = before[field] ?? "";
65
- const a = after[field] ?? "";
67
+ const b = before[field];
68
+ const a = after[field];
66
69
  if (b !== a)
67
- rows.push(rowFor(field, at, actor, via, b, a));
70
+ rows.push(rowFor(field, at, actor, via, historyText(b), historyText(a)));
68
71
  }
69
72
  if (!rows.length)
70
73
  return;
@@ -1,8 +1,9 @@
1
- export const PROVIDER_KEY_KINDS = ["anthropic", "openaiCompat", "xai"];
1
+ export const PROVIDER_KEY_KINDS = ["anthropic", "openaiCompat", "xai", "mistral"];
2
2
  const DEFAULT_URLS = {
3
3
  anthropic: "https://api.anthropic.com",
4
4
  openaiCompat: "https://openrouter.ai/api/v1",
5
5
  xai: "https://api.x.ai/v1",
6
+ mistral: "https://api.mistral.ai/v1",
6
7
  };
7
8
  const MAX_MODELS = 5;
8
9
  function isLoopback(hostname) {
@@ -18,7 +19,7 @@ export function providerModelsUrl(provider, base) {
18
19
  }
19
20
  function modelIds(body) {
20
21
  const record = body && typeof body === "object" ? body : null;
21
- const list = Array.isArray(record?.data) ? record.data : Array.isArray(record?.models) ? record.models : [];
22
+ const list = Array.isArray(body) ? body : Array.isArray(record?.data) ? record.data : Array.isArray(record?.models) ? record.models : [];
22
23
  const ids = [];
23
24
  for (const entry of list) {
24
25
  const item = entry && typeof entry === "object" ? entry : null;
@@ -0,0 +1,80 @@
1
+ // What this installation has shared, per team: the package id and the
2
+ // release last exported, plus the key each bot, group chat and routine was
3
+ // published under.
4
+ //
5
+ // Keys are a package's identity across releases (Admin refuses a release
6
+ // that silently drops one, and later updates match on them). They must not
7
+ // come from names once they exist: renaming a bot, a room, a routine or the
8
+ // team would otherwise read as "everything removed, everything added". So
9
+ // the first export records them here, keyed by record id, and every later
10
+ // export of the same team reuses them. Teams are name strings in OMB, so the
11
+ // team entry is renamed with the section (section-context.ts calls
12
+ // renamePublishedTeam from the one function every rename goes through).
13
+ //
14
+ // Server-private: nothing here is on the wire, and nothing here is a secret.
15
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
16
+ import { join } from "node:path";
17
+ import { z } from "zod";
18
+ import { writeFileAtomic } from "./atomic.js";
19
+ import { DATA_DIR } from "./config.js";
20
+ export const PUBLISHED_TEAMS_FILE = join(DATA_DIR, "published-teams.json");
21
+ const keyMap = z.record(z.string(), z.string());
22
+ const publishedTeamSchema = z.object({
23
+ packageId: z.string().min(1),
24
+ lastRelease: z.string().min(1),
25
+ keys: z.object({ bots: keyMap, rooms: keyMap, routines: keyMap }).default({ bots: {}, rooms: {}, routines: {} }),
26
+ });
27
+ const fileSchema = z.object({ version: z.literal(1), teams: z.record(z.string(), z.unknown()) });
28
+ function load() {
29
+ const teams = new Map();
30
+ if (!existsSync(PUBLISHED_TEAMS_FILE))
31
+ return teams;
32
+ try {
33
+ const file = fileSchema.parse(JSON.parse(readFileSync(PUBLISHED_TEAMS_FILE, "utf8")));
34
+ // Own entries only: a team may be called anything, including __proto__.
35
+ for (const [section, value] of Object.entries(file.teams)) {
36
+ const entry = publishedTeamSchema.safeParse(value);
37
+ if (entry.success)
38
+ teams.set(section, entry.data);
39
+ }
40
+ }
41
+ catch {
42
+ // An unreadable record only costs key stability on the next export,
43
+ // which then derives keys from names as a first export does.
44
+ }
45
+ return teams;
46
+ }
47
+ function save(teams) {
48
+ mkdirSync(DATA_DIR, { recursive: true, mode: 0o700 });
49
+ const out = Object.create(null);
50
+ for (const [section, entry] of teams)
51
+ out[section] = entry;
52
+ writeFileAtomic(PUBLISHED_TEAMS_FILE, JSON.stringify({ version: 1, teams: out }, null, 2), { mode: 0o600 });
53
+ }
54
+ export function readPublishedTeam(section) {
55
+ return load().get(section) ?? null;
56
+ }
57
+ export function writePublishedTeam(section, entry) {
58
+ const teams = load();
59
+ teams.set(section, entry);
60
+ save(teams);
61
+ }
62
+ /** Follow a team rename; null forgets the team (it was deleted). */
63
+ export function renamePublishedTeam(name, nextName) {
64
+ const teams = load();
65
+ const entry = teams.get(name);
66
+ if (!entry || name === nextName)
67
+ return;
68
+ teams.delete(name);
69
+ if (nextName !== null)
70
+ teams.set(nextName, entry);
71
+ save(teams);
72
+ }
73
+ /** "1.2.3" → "1.2.4"; nothing published yet (or unreadable) starts at 1.0.0. */
74
+ export function nextPatchRelease(release) {
75
+ const match = release?.match(/^(\d{1,6})\.(\d{1,6})\.(\d{1,6})$/);
76
+ if (!match)
77
+ return "1.0.0";
78
+ const [major, minor, patch] = match.slice(1).map(Number);
79
+ return patch < 999_999 ? `${major}.${minor}.${patch + 1}` : `${major}.${minor + 1}.0`;
80
+ }
@@ -6,6 +6,7 @@ const nodeSchema = z.object({
6
6
  id: z.string(), rootId: z.string(), parentId: z.string().optional(),
7
7
  groupId: z.string().optional(), threadId: z.string(), botId: z.string(),
8
8
  key: z.string(), text: z.string(), createdAt: z.number(),
9
+ requestBatchKey: z.string().optional(),
9
10
  status: z.enum(["source", "queued", "running", "waiting", "resume", "completed", "failed", "cancelled"]),
10
11
  result: z.string().default(""), reported: z.boolean().default(false),
11
12
  executions: z.number().int().nonnegative().default(0), startedAt: z.number().optional(),
@@ -159,7 +160,7 @@ export class RoomHandoffs {
159
160
  const root = this.root(n);
160
161
  return `Room handoff hard cap exhausted: node was ${n.status} after ${duration(this.now() - root.createdAt)} of the ${duration(this.limits.hardCapMs)} wall-clock cap`;
161
162
  }
162
- enqueue(source, generation, parentId, target, key, text, approvalGranted = false, rework = false, sourceText = "") {
163
+ enqueue(source, generation, parentId, target, key, text, approvalGranted = false, rework = false, sourceText = "", requestBatchKey) {
163
164
  if (this.loadError)
164
165
  throw new Error(this.loadError);
165
166
  let parent = parentId ? this.nodes.get(parentId) : this.nodes.get(generation);
@@ -178,10 +179,17 @@ export class RoomHandoffs {
178
179
  const existing = this.children(parent.id).find(n => n.key === key);
179
180
  if (existing) {
180
181
  if (existing.groupId !== target.groupId || existing.botId !== target.botId || existing.text !== text ||
181
- existing.kind !== kind)
182
+ existing.kind !== kind || existing.requestBatchKey !== requestBatchKey)
182
183
  throw new Error("request_key was already used for different work");
183
184
  return { node: existing, duplicate: true };
184
185
  }
186
+ if (requestBatchKey && target.groupId) {
187
+ const batch = this.children(parent.id).filter(n => n.requestBatchKey === requestBatchKey && n.groupId === target.groupId);
188
+ if (batch.some(n => n.text !== text || n.threadId !== target.threadId))
189
+ throw new Error("request_key was already used for different room work");
190
+ if (batch.some(n => n.startedAt !== undefined))
191
+ throw new Error("This shared room request has already started; use a new request_key for additional recipients");
192
+ }
185
193
  if (!rework && this.children(parent.id).some(n => n.kind === kind &&
186
194
  n.groupId === target.groupId && n.botId === target.botId && n.status === "completed")) {
187
195
  throw new Error("This agent already completed your assignment. Do not send acknowledgements or approvals as new work. Finish with your decision; results return automatically. Only use rework=true for concrete additional work.");
@@ -221,7 +229,7 @@ export class RoomHandoffs {
221
229
  }
222
230
  const node = { ...target, id: randomUUID(), rootId: parent.rootId, parentId: parent.id,
223
231
  key, text, createdAt: this.now(), status: "queued", result: "", reported: false, executions: 0, approvalGranted,
224
- kind };
232
+ kind, ...(target.groupId && requestBatchKey ? { requestBatchKey } : {}) };
225
233
  const problem = this.hooks.validate(node, parent);
226
234
  if (problem)
227
235
  throw new Error(problem);
@@ -239,6 +247,14 @@ export class RoomHandoffs {
239
247
  }
240
248
  return { node, duplicate: false };
241
249
  }
250
+ /** A room brief is displayed once; each recipient keeps its own execution and result. */
251
+ sharedRequest(node) {
252
+ const batch = node.groupId && node.requestBatchKey && node.parentId
253
+ ? this.children(node.parentId).filter(n => n.requestBatchKey === node.requestBatchKey &&
254
+ n.groupId === node.groupId && n.threadId === node.threadId && n.text === node.text)
255
+ : [node];
256
+ return { id: batch[0]?.id ?? node.id, botIds: batch.map(n => n.botId) };
257
+ }
242
258
  sourceSettled(generation, ok) {
243
259
  const node = this.nodes.get(generation);
244
260
  if (!node || node.status !== "source")
@@ -0,0 +1,25 @@
1
+ // Preset bots for New bot (server/presets.ts has the store and the rules).
2
+ // GET lists what New bot offers: the organization's presets first, then
3
+ // imported files', never one from a withdrawn release. DELETE removes an
4
+ // imported preset (the person's own click; no confirm step). Organization
5
+ // presets are Admin's to remove. Both are admin-scoped by default in
6
+ // server/request-auth.ts, like the New bot defaults: a preset adds skills
7
+ // and starter notes to the bot made from it.
8
+ import { listBotPresets, ORG_PRESET_REMOVE_MESSAGE, PRESET_UNAVAILABLE_MESSAGE } from "../presets.js";
9
+ import { PASS } from "./table.js";
10
+ export function createBotPresetRoutes(deps) {
11
+ return async ({ res, path, method, json }) => {
12
+ if (path === "/api/bot-presets" && method === "GET") {
13
+ return json(res, 200, { presets: listBotPresets(deps.presets, deps.orgStatuses()) });
14
+ }
15
+ const m = path.match(/^\/api\/bot-presets\/([\w-]+)$/);
16
+ if (!m || method !== "DELETE")
17
+ return PASS;
18
+ const removed = deps.presets.removeFilePreset(m[1]);
19
+ if (removed === "not_found")
20
+ return json(res, 404, { error: PRESET_UNAVAILABLE_MESSAGE });
21
+ if (removed === "organization")
22
+ return json(res, 409, { error: ORG_PRESET_REMOVE_MESSAGE });
23
+ return json(res, 200, { ok: true });
24
+ };
25
+ }
@@ -211,10 +211,15 @@ const routineRequestCardDataSchema = z.object({
211
211
  }).strict();
212
212
  export class RoutineRequestError extends Error {
213
213
  status;
214
- constructor(message, status = 400) {
214
+ /** True when no retry of this card can ever succeed — the routine moved
215
+ * under the proposal — so the card must settle as expired rather than
216
+ * staying actionable. */
217
+ terminal;
218
+ constructor(message, status = 400, options) {
215
219
  super(message);
216
220
  this.name = "RoutineRequestError";
217
221
  this.status = status;
222
+ this.terminal = options?.terminal === true;
218
223
  }
219
224
  }
220
225
  function text(value, field, max) {
@@ -786,9 +791,9 @@ export function routineRequestFingerprint(payload, messageId) {
786
791
  function verifyManageSnapshot(operation, manager, botId) {
787
792
  const current = ownedRoutine(manager, operation.routineId, botId);
788
793
  if (!current)
789
- throw new RoutineRequestError("That routine no longer exists", 404);
794
+ throw new RoutineRequestError("That routine no longer exists", 404, { terminal: true });
790
795
  if (current.updatedAt !== operation.expectedUpdatedAt) {
791
- throw new RoutineRequestError("That routine changed after this confirmation card was prepared. Ask the bot to review it and propose the action again.", 409);
796
+ throw new RoutineRequestError("That routine changed after this confirmation card was prepared. Ask the bot to review it and propose the action again.", 409, { terminal: true });
792
797
  }
793
798
  return current;
794
799
  }
@@ -828,7 +833,7 @@ function revalidateOperation(operation, manager, botId, now) {
828
833
  if (duplicate) {
829
834
  // The tool cannot choose a result destination. Return the existing ID,
830
835
  // without moving its reports or treating a renamed request as new work.
831
- throw new RoutineRequestError(`An enabled routine with the same instructions and execution settings already exists (${duplicate.id}). Use list_routines to review it, then update or run that routine instead.`, 409);
836
+ throw new RoutineRequestError(`An enabled routine with the same instructions and execution settings already exists (${duplicate.id}). Use list_routines to review it, then update or run that routine instead.`, 409, { terminal: true });
832
837
  }
833
838
  }
834
839
  const current = operation.action === "create"
@@ -848,7 +853,7 @@ function revalidateOperation(operation, manager, botId, now) {
848
853
  }
849
854
  }
850
855
  if (schedule?.type === "once" && schedule.at <= now) {
851
- throw new RoutineRequestError("That one-time schedule is now in the past. Ask the bot to propose a new time.", 409);
856
+ throw new RoutineRequestError("That one-time schedule is now in the past. Ask the bot to propose a new time.", 409, { terminal: true });
852
857
  }
853
858
  if (schedule?.type === "interval") {
854
859
  const base = operation.action === "create"
@@ -923,7 +928,7 @@ export class RoutineRequestService {
923
928
  if (refusal)
924
929
  throw new RoutineRequestError(refusal, 403);
925
930
  }
926
- await this.requireCloudReadiness(operation);
931
+ await this.requireCloudReadiness(operation, botId);
927
932
  // The readiness probe is asynchronous. Another request can edit or
928
933
  // delete the routine while it is in flight, so re-check the captured
929
934
  // revision before rendering and persisting the confirmation snapshot.
@@ -1006,7 +1011,7 @@ export class RoutineRequestService {
1006
1011
  }
1007
1012
  throw new RoutineRequestError(result.state === "invalid" ? result.error : "The routine change could not be applied", result.state === "invalid" ? result.status : 409);
1008
1013
  }
1009
- async requireCloudReadiness(operation) {
1014
+ async requireCloudReadiness(operation, proposerBotId) {
1010
1015
  if (!this.cloudReady || operation.action === "pause" || operation.action === "delete")
1011
1016
  return;
1012
1017
  const definition = effectiveDefinition(operation, this.routines);
@@ -1014,7 +1019,10 @@ export class RoutineRequestService {
1014
1019
  return;
1015
1020
  let readiness;
1016
1021
  try {
1017
- readiness = await this.cloudReady();
1022
+ const targetBotId = operation.action === "create"
1023
+ ? operation.forBot?.botId ?? proposerBotId
1024
+ : this.routines.listRoutines().find(routine => routine.id === operation.routineId)?.botId ?? proposerBotId;
1025
+ readiness = await this.cloudReady(targetBotId);
1018
1026
  }
1019
1027
  catch (error) {
1020
1028
  const detail = error instanceof Error ? error.message : String(error);
@@ -1104,6 +1112,19 @@ export class RoutineRequestService {
1104
1112
  }
1105
1113
  return this.settleApplied(args.threadId, message.id, card, payload, receipt.resultId, receipt.appliedAt);
1106
1114
  }
1115
+ // An expired card is settled terminal state, not a decision waiting on
1116
+ // a slower click: the proposal it carried can never be confirmed as
1117
+ // prepared, even when the routine moves back under it. The committed
1118
+ // receipt above still recovers a write that already happened; nothing
1119
+ // past this point can.
1120
+ if (card.expired) {
1121
+ return {
1122
+ claimed: true,
1123
+ state: "invalid",
1124
+ error: "This routine request expired before it was confirmed. Ask for a fresh proposal.",
1125
+ status: 409,
1126
+ };
1127
+ }
1107
1128
  if (args.behavior === "deny") {
1108
1129
  this.store.patchMessage(args.threadId, message.id, { card: { ...card, answered: "deny", held: undefined } });
1109
1130
  return { claimed: true, state: "denied" };
@@ -1112,7 +1133,7 @@ export class RoutineRequestService {
1112
1133
  if (payload.operation.action === "create" && payload.operation.forBot && this.validateTarget) {
1113
1134
  const refusal = this.validateTarget(payload.botId, payload.operation.forBot);
1114
1135
  if (refusal)
1115
- throw new RoutineRequestError(refusal, 404);
1136
+ throw new RoutineRequestError(refusal, 404, { terminal: true });
1116
1137
  }
1117
1138
  const resultId = this.apply(payload, message.id, fingerprint);
1118
1139
  return this.settleApplied(args.threadId, message.id, card, payload, resultId);
@@ -1120,8 +1141,12 @@ export class RoutineRequestService {
1120
1141
  catch (error) {
1121
1142
  const status = error instanceof RoutineRequestError ? error.status : 400;
1122
1143
  const detail = error instanceof Error ? error.message : String(error);
1144
+ // A terminal failure (the routine moved under the proposal) can never
1145
+ // be confirmed as prepared: settle the card as expired with its
1146
+ // options removed so it stops looking actionable.
1147
+ const expired = error instanceof RoutineRequestError && error.terminal;
1123
1148
  this.store.patchMessage(args.threadId, message.id, {
1124
- card: { ...card, held: redactSecretsInText(detail).slice(0, 500) },
1149
+ card: { ...card, ...(expired ? { expired: true, options: [] } : {}), held: redactSecretsInText(detail).slice(0, 500) },
1125
1150
  });
1126
1151
  return {
1127
1152
  claimed: true,
@@ -7,7 +7,24 @@ import { writeFileAtomic } from "./atomic.js";
7
7
  import { redactSecretsInText } from "./redact.js";
8
8
  import { normalizeCronSchedule, nextCronRuns } from "../shared/routine-schedule.js";
9
9
  import { isRoutineProblemRun } from "../shared/routines.js";
10
+ import { ROUTINE_PARTS } from "./package-parts.js";
10
11
  const persistedSourceThreadId = z.string().trim().min(1).optional().catch(undefined);
12
+ const HASH = /^[a-f0-9]{64}$/;
13
+ function loadInstalledPackage(value) {
14
+ if (!value || typeof value !== "object")
15
+ return undefined;
16
+ const stamp = value;
17
+ if (typeof stamp.installId !== "string" || !/^[a-f0-9]{32}$/.test(stamp.installId) || typeof stamp.key !== "string" || !stamp.key || stamp.key.length > 80)
18
+ return undefined;
19
+ const parts = stamp.parts;
20
+ if (!parts || typeof parts !== "object" || !ROUTINE_PARTS.every((part) => HASH.test(String(parts[part]?.r)) && HASH.test(String(parts[part]?.w))))
21
+ return undefined;
22
+ return {
23
+ installId: stamp.installId,
24
+ key: stamp.key,
25
+ parts: Object.fromEntries(ROUTINE_PARTS.map((part) => [part, { r: parts[part].r, w: parts[part].w }])),
26
+ };
27
+ }
11
28
  const webhookRunReceiptSchema = z.object({
12
29
  webhookId: z.string().min(1).max(200),
13
30
  deliveryId: z.string().min(1).max(200),
@@ -490,9 +507,12 @@ export class RoutineManager {
490
507
  overlap: routine.overlap === "queue" ? "queue" : undefined,
491
508
  skippedRuns: Number.isSafeInteger(routine.skippedRuns) && routine.skippedRuns > 0 ? routine.skippedRuns : undefined,
492
509
  lastSkippedAt: Number.isSafeInteger(routine.lastSkippedAt) && routine.lastSkippedAt >= 0 && routine.lastSkippedAt <= MAX_DATE_MS ? routine.lastSkippedAt : undefined,
510
+ installedPackage: loadInstalledPackage(routine.installedPackage),
493
511
  };
494
512
  if (loaded.timeoutMinutes === undefined)
495
513
  delete loaded.timeoutMinutes;
514
+ if (loaded.installedPackage === undefined)
515
+ delete loaded.installedPackage;
496
516
  delete loaded.failureStreak;
497
517
  return [loaded];
498
518
  })
@@ -582,7 +602,27 @@ export class RoutineManager {
582
602
  .sort((a, b) => (b.finishedAt ?? b.createdAt) - (a.finishedAt ?? a.createdAt) || b.createdAt - a.createdAt);
583
603
  const success = outcomes.findIndex(run => run.status === "completed");
584
604
  const failures = success < 0 ? outcomes.length : success;
585
- return { ...cloneRoutine(routine), ...(failures ? { failureStreak: failures } : {}) };
605
+ const { installedPackage: _installedPackage, ...visible } = cloneRoutine(routine);
606
+ return { ...visible, ...(failures ? { failureStreak: failures } : {}) };
607
+ }
608
+ /** Routines added from the organization's library, with their stamps. */
609
+ packageStamps() {
610
+ return this.routines.flatMap((routine) => routine.installedPackage
611
+ ? [{ routineId: routine.id, botId: routine.botId, enabled: routine.enabled, stamp: structuredClone(routine.installedPackage) }]
612
+ : []);
613
+ }
614
+ /** Record which organization install a just-created routine belongs to. */
615
+ stampInstalledPackage(id, stamp) {
616
+ const clean = loadInstalledPackage(stamp);
617
+ if (!clean)
618
+ throw new Error("Invalid routine package stamp");
619
+ if (!this.routines.some((routine) => routine.id === id))
620
+ return false;
621
+ this.commitMutation(() => {
622
+ const routine = this.routines.find((candidate) => candidate.id === id);
623
+ routine.installedPackage = clean;
624
+ });
625
+ return true;
586
626
  }
587
627
  /** Whether this computer should stay awake for routines: a run is in
588
628
  * flight, or an enabled routine is due within the horizon. The schedule
@@ -9,6 +9,7 @@ import { join } from "node:path";
9
9
  import { z } from "zod";
10
10
  import { writeFileAtomic } from "./atomic.js";
11
11
  import { DATA_DIR } from "./config.js";
12
+ import { renamePublishedTeam } from "./published-teams.js";
12
13
  export const SECTION_CONTEXT_MAX_BYTES = 24_000;
13
14
  export const SECTION_CONTEXTS_FILE = join(DATA_DIR, "section-contexts.json");
14
15
  const sectionContextRecordSchema = z.object({ text: z.string(), updatedAt: z.number().finite() });
@@ -94,6 +95,15 @@ export function changeEmptySection(name, nextName) {
94
95
  if (nextName !== null && context)
95
96
  data.contexts[nextName] = context;
96
97
  saveFile(data);
98
+ // Every team rename and removal funnels through here, so the team's
99
+ // published package id and keys follow it (published-teams.ts). The team
100
+ // itself is already renamed; a failure here costs only key stability.
101
+ try {
102
+ renamePublishedTeam(name, nextName);
103
+ }
104
+ catch (error) {
105
+ console.warn(`[teams] Could not move the shared-package record for this team: ${error.message}`);
106
+ }
97
107
  }
98
108
  export function readSectionContext(section) {
99
109
  const record = loadFile().contexts[sectionContextKey(section)];