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
@@ -8,10 +8,11 @@ var MAX_OPTIONS = 12;
8
8
  var MAX_QUESTION_TEXT = 400;
9
9
  var MAX_LABEL = 120;
10
10
  var MAX_DESCRIPTION = 400;
11
+ var MAX_CUSTOM_ANSWER = 2e3;
11
12
  function text(value, limit) {
12
13
  if (typeof value !== "string") return void 0;
13
14
  const trimmed = value.trim();
14
- return trimmed ? trimmed.slice(0, limit) : void 0;
15
+ return trimmed ? trimmed.slice(0, limit).trim() : void 0;
15
16
  }
16
17
  function isRecord(value) {
17
18
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -59,19 +60,38 @@ function parseAskQuestions(input) {
59
60
  }
60
61
  return questions.length ? questions : null;
61
62
  }
63
+ var OMB_ASK_FENCE = /(^|\n)[ \t]{0,3}(`{3,}|~{3,})[ \t]*omb-ask[ \t]*\r?\n([\s\S]*?)\r?\n[ \t]{0,3}\2[ \t]*(?=\r?\n|$)/;
64
+ var OMB_ASK_FENCE_GLOBAL = new RegExp(OMB_ASK_FENCE.source, OMB_ASK_FENCE.flags + "g");
65
+ var MAX_ANSWER_ECHO = 6 * MAX_CUSTOM_ANSWER;
62
66
  function questionAnswersByQuestion(message, questions) {
63
- const answers = {};
64
- const known = new Map(questions.map((entry) => [entry.question, entry.question]));
65
- for (const block of message.split("\n\n")) {
66
- const match = /^Q: ([\s\S]+?)\nA: ([\s\S]+)$/.exec(block.trim());
67
- if (!match) continue;
68
- const question = known.get(match[1].trim());
69
- if (question) answers[question] = match[2].trim();
67
+ return questionAnswersById(message, questions.map((question) => ({ id: question.question, question })));
68
+ }
69
+ function questionAnswersById(message, questions) {
70
+ const answers = /* @__PURE__ */ Object.create(null);
71
+ const idByText = /* @__PURE__ */ new Map();
72
+ const ambiguous = /* @__PURE__ */ new Set();
73
+ for (const { id, question } of questions) {
74
+ if (idByText.has(question.question)) {
75
+ ambiguous.add(question.question);
76
+ continue;
77
+ }
78
+ idByText.set(question.question, id);
79
+ }
80
+ const blocks = message.matchAll(/(?:^|\n\n)Q: ([\s\S]+?)\nA: ([\s\S]*?)(?=\n\nQ: |$)/g);
81
+ let sawBlock = false;
82
+ for (const match of blocks) {
83
+ sawBlock = true;
84
+ const asked = match[1].trim();
85
+ if (ambiguous.has(asked)) continue;
86
+ const id = idByText.get(asked);
87
+ const answer = match[2].trim();
88
+ if (id && answer) answers[id] = answer;
70
89
  }
71
90
  if (Object.keys(answers).length) return answers;
91
+ if (sawBlock) return {};
72
92
  const only = questions.length === 1 ? questions[0] : void 0;
73
93
  const flat = message.trim();
74
- return only && flat ? { [only.question]: flat } : {};
94
+ return only && flat ? { [only.id]: flat } : {};
75
95
  }
76
96
 
77
97
  // server/permission-proxy.ts
@@ -0,0 +1,81 @@
1
+ // One admission decision for every surface that starts, steers, or parks
2
+ // work. Each seam used to hand-roll its own busy policy; they agreed by
3
+ // convention and drifted by accident. This module is the frozen record of
4
+ // what each surface does today (M1): same inputs, same outputs, one place.
5
+ //
6
+ // Purity is the contract: no IO, no store reads, no adapter calls. Callers
7
+ // gather state, call admit(), and apply the returned decision with their own
8
+ // side effects. Anything that can settle a turn (an awaited steer, a slow
9
+ // disk) belongs to the caller, which re-reads state and asks again.
10
+ /** The shared admission decision. Pure: same surface, message, and state
11
+ * always return the same decision, with no side effects. */
12
+ export function admit(surface, message = {}, state = {}) {
13
+ switch (surface) {
14
+ case "direct-busy": {
15
+ // Steer-first for plain text into a capable engine; every mechanical
16
+ // clamp (attachments, a pending surface choice, an incapable engine)
17
+ // parks the words for the next ordinary turn instead.
18
+ const steerable = !message.carriesImages && !message.pendingComputerSelection && message.engineCanSteer === true;
19
+ return steerable ? { action: "steer" } : { action: "queue" };
20
+ }
21
+ case "direct": {
22
+ if (state.atCapacity || state.threadBusy || state.groupTurn || state.parksBehindCoordination) {
23
+ // Capacity outranks a room turn in the receipt because the person
24
+ // can fix it (finish a thread); coordination parking is reasonless.
25
+ const reason = state.atCapacity ? "capacity" : state.groupTurn ? "group-turn" : undefined;
26
+ return { action: "queue", reason };
27
+ }
28
+ return { action: "start" };
29
+ }
30
+ case "room":
31
+ // Room sends never auto-steer; the queue is the policy. Human override
32
+ // is the head-only manual steer below.
33
+ return state.roomWorking ? { action: "queue" } : { action: "start" };
34
+ case "room-steer": {
35
+ if (!message.speakerPresent || message.engineCanSteer !== true) {
36
+ // An incapable room keeps its queue, exactly like an incapable 1:1
37
+ // engine; the request succeeds without delivering.
38
+ return { action: "queue" };
39
+ }
40
+ if (!message.isQueueHead) {
41
+ return { action: "refuse", code: "queue-head-only" };
42
+ }
43
+ return { action: "steer" };
44
+ }
45
+ case "guarded": {
46
+ // A guarded send is an external contract: never queued, never steered,
47
+ // never started under stale permissions. Any busy shape refuses.
48
+ if (state.botBusy || state.threadBusy || state.atCapacity || state.parksBehindCoordination || state.groupTurn) {
49
+ return { action: "refuse", code: "guarded_busy" };
50
+ }
51
+ return { action: "start" };
52
+ }
53
+ case "unattended": {
54
+ // Routines and webhooks share startTurn's admission; busy means a
55
+ // missed or failed run, not a queue — their receipts are the
56
+ // monitoring feature.
57
+ if (state.botPresent === false)
58
+ return { action: "refuse", code: "missing" };
59
+ if (state.atCapacity || state.groupTurn)
60
+ return { action: "refuse", code: "busy" };
61
+ return { action: "start" };
62
+ }
63
+ case "peer": {
64
+ // Exactly what startTurn admits for a direct thread: the landing
65
+ // thread free, a free slot, no live room turn. Never the whole-bot
66
+ // busy flag.
67
+ if (state.threadBusy || state.atCapacity || state.groupTurn)
68
+ return { action: "refuse", code: "busy" };
69
+ return { action: "start" };
70
+ }
71
+ case "opened-thread": {
72
+ // A self-opened thread is brand new: its own busy flag is impossible,
73
+ // so only a full slot list or a live room turn can park it. Provenance
74
+ // (peerAsk, unattended) survives the wait in the queue payload.
75
+ if (state.atCapacity || state.groupTurn) {
76
+ return { action: "queue", reason: state.atCapacity ? "capacity" : "group-turn" };
77
+ }
78
+ return { action: "start" };
79
+ }
80
+ }
81
+ }
@@ -0,0 +1,294 @@
1
+ // The aside lane: peer context handed to a busy thread's RUNNING turn.
2
+ //
3
+ // A peer send that arrives mid-turn used to degrade straight to the
4
+ // delegation queue — full work, later turn, no reply until then. When the
5
+ // engine running that turn has a mid-turn seam (Adapter.steer), the same
6
+ // words can instead be folded into the live turn at its next step boundary
7
+ // as an ASIDE: context the model reads and usually ignores. The person's
8
+ // lane keeps real steering (parent authority changes the turn's course);
9
+ // an aside never claims that authority, and the envelope says so.
10
+ //
11
+ // Durability follows steer-queue's shape (chat_followups rows, restore
12
+ // only what never dispatched) with one aside-specific rule: the row id is
13
+ // the messageId dedupe key, it is written BEFORE the seam call, and the
14
+ // injected line lands on the transcript with queueId = row id BEFORE the
15
+ // row is deleted. A crash in either window is then self-healing: restore
16
+ // skips any row whose transcript already shows its marker (the words ran;
17
+ // never replay them) and requeues only the rest. "indeterminate" seam
18
+ // outcomes count as injected for the same reason — the words may already
19
+ // be running, and running them twice is the one mistake this lane must
20
+ // never make.
21
+ import { newId } from "./contracts.js";
22
+ import { chatFollowups, saveChatFollowup, settleChatFollowups } from "./message-db.js";
23
+ import { peerName } from "./peer-roster.js";
24
+ const asides = new Map(); // threadId → waiting asides
25
+ /** Threads with an injection or drain in flight: seam calls are awaited
26
+ * work, so two boundaries firing close together must not double-process
27
+ * the same words. */
28
+ const inFlight = new Set();
29
+ /** The mandatory non-steering envelope (audit §5.2). Every mid-turn channel
30
+ * OMB can ride is steering-branded, so the words themselves must say what
31
+ * they are: peer context from a named sender, not a course change. */
32
+ export function asideEnvelope(fromName, text) {
33
+ // Peer words sit between fixed markers, and a peer cannot be allowed to
34
+ // forge the closing marker inside its own text: that would end the aside
35
+ // early and make the batched context ambiguous downstream.
36
+ const body = text.replace(/\[\s*end\s+aside\s*\]/gi, "(end aside)");
37
+ return [
38
+ `[aside from @${peerName(fromName)} — peer context, not steering; continue your current plan unless this directly changes a fact you are using]`,
39
+ body,
40
+ "[end aside]",
41
+ ].join("\n");
42
+ }
43
+ /** Rebuild the in-memory lane after a restart. Only rows whose dispatch
44
+ * never began come back; a row whose transcript already shows its queueId
45
+ * marker was injected before the process died, and those words must never
46
+ * run twice, so the row is retired instead of requeued. */
47
+ export function restoreAsideMessages(store) {
48
+ asides.clear();
49
+ for (const row of chatFollowups("aside")) {
50
+ if (row.status !== "pending")
51
+ continue;
52
+ if (store.messagesFor(row.threadId).some((message) => message.queueId === row.id)) {
53
+ settleChatFollowups([row.id], null);
54
+ continue;
55
+ }
56
+ const entry = asides.get(row.threadId) ?? { botId: row.ownerId, items: [] };
57
+ if (entry.botId !== row.ownerId)
58
+ throw new Error("queued aside belongs to another bot");
59
+ entry.items.push({
60
+ messageId: row.id,
61
+ text: row.payload.text,
62
+ prompt: row.payload.prompt ?? row.payload.text,
63
+ aside: row.payload.aside ?? { fromBotId: row.ownerId, fromBotName: "teammate", commsDepth: 0 },
64
+ trigger: row.payload.trigger,
65
+ });
66
+ asides.set(row.threadId, entry);
67
+ }
68
+ }
69
+ /** Persist one aside and park it for injection. The row exists before any
70
+ * seam call so a crash can only ever re-queue words that never ran. */
71
+ export function queueAsideMessage(botId, threadId, text, options) {
72
+ const id = newId();
73
+ const item = {
74
+ messageId: id,
75
+ text,
76
+ prompt: asideEnvelope(options.fromBotName, text),
77
+ aside: {
78
+ fromBotId: options.fromBotId,
79
+ fromBotName: options.fromBotName,
80
+ ...(options.unattended ? { unattended: true } : {}),
81
+ commsDepth: options.commsDepth,
82
+ },
83
+ trigger: options.trigger,
84
+ };
85
+ const entry = asides.get(threadId) ?? { botId, items: [] };
86
+ if (entry.botId !== botId)
87
+ throw new Error("queued aside belongs to another bot");
88
+ saveChatFollowup({
89
+ id,
90
+ kind: "aside",
91
+ ownerId: botId,
92
+ threadId,
93
+ payload: { text: item.text, prompt: item.prompt, aside: item.aside, ...(item.trigger ? { trigger: item.trigger } : {}) },
94
+ });
95
+ entry.items.push(item);
96
+ asides.set(threadId, entry);
97
+ return { id };
98
+ }
99
+ /** Everything waiting for this thread, in arrival order. */
100
+ export function pendingAsides(botId, threadId) {
101
+ const entry = asides.get(threadId);
102
+ return entry?.botId === botId ? [...entry.items] : [];
103
+ }
104
+ /** Record delivered asides: one transcript line per item (enveloped text,
105
+ * queueId marker, peer provenance), then the durable rows are deleted. The
106
+ * marker write precedes the delete, so the crash window between them heals
107
+ * as "already injected" on restore rather than as a replay. */
108
+ export function recordInjectedAsides(store, threadId, items) {
109
+ const appended = [];
110
+ for (const item of items) {
111
+ appended.push(store.appendMessage(threadId, {
112
+ role: "user",
113
+ kind: "text",
114
+ text: item.prompt,
115
+ queueId: item.messageId,
116
+ aside: true,
117
+ peerAsk: {
118
+ botId: item.aside.fromBotId,
119
+ name: item.aside.fromBotName,
120
+ ...(item.aside.unattended ? { unattended: true } : {}),
121
+ },
122
+ }));
123
+ }
124
+ settleChatFollowups(items.map((item) => item.messageId), null);
125
+ // The words are delivered and their rows retired; the in-memory lane must
126
+ // agree, or a later boundary would batch them into another injection.
127
+ const entry = asides.get(threadId);
128
+ if (entry) {
129
+ const delivered = new Set(items.map((item) => item.messageId));
130
+ const remaining = entry.items.filter((item) => !delivered.has(item.messageId));
131
+ if (remaining.length)
132
+ asides.set(threadId, { botId: entry.botId, items: remaining });
133
+ else
134
+ asides.delete(threadId);
135
+ }
136
+ return appended;
137
+ }
138
+ /** Drop a thread's waiting asides: the bot or task is gone, so no turn will
139
+ * ever read them. Tombstoned like a cancelled follow-up. */
140
+ export function cancelAsides(threadId) {
141
+ const entry = asides.get(threadId);
142
+ if (!entry)
143
+ return;
144
+ settleChatFollowups(entry.items.map((item) => item.messageId), "cancelled");
145
+ asides.delete(threadId);
146
+ }
147
+ /** Fold everything waiting on this thread into its running turn.
148
+ *
149
+ * The rows are marked "dispatching" BEFORE the seam call: a crash at any
150
+ * point then leaves words the restart recovery path treats as may-have-run
151
+ * (recovered to the transcript with a review notice, never re-executed as
152
+ * a turn). "refused" — provably not delivered — returns them to "pending"
153
+ * so the next boundary can try again. "steered" and "indeterminate" both
154
+ * record delivery: the words may already be running, and running them
155
+ * twice is the one mistake this lane must never make.
156
+ *
157
+ * The batch leaves the shared queue BEFORE the seam call and stays out
158
+ * for the whole await: while the words may already be running inside the
159
+ * turn, no later boundary may be able to see them again, and a cancel
160
+ * landing mid-seam must not resurrect them. Only a "refused" outcome on a
161
+ * lane that is still ours puts the words back — ahead of anything that
162
+ * arrived during the call.
163
+ *
164
+ * Returns null when nothing is waiting or the thread is not this bot's;
165
+ * otherwise whether the batch was delivered and how many items it held. */
166
+ export async function attemptAsideInjection(store, botId, threadId, inject) {
167
+ const entry = asides.get(threadId);
168
+ if (!entry || entry.botId !== botId || entry.items.length === 0)
169
+ return null;
170
+ if (inFlight.has(threadId))
171
+ return { delivered: false, count: entry.items.length };
172
+ const batch = entry.items;
173
+ entry.items = [];
174
+ const ids = batch.map((item) => item.messageId);
175
+ settleChatFollowups(ids, "dispatching");
176
+ inFlight.add(threadId);
177
+ let outcome;
178
+ try {
179
+ outcome = await inject(botId, threadId, batch.map((item) => item.prompt).join("\n\n"));
180
+ }
181
+ catch {
182
+ outcome = "indeterminate";
183
+ }
184
+ finally {
185
+ inFlight.delete(threadId);
186
+ }
187
+ if (outcome === "refused") {
188
+ if (asides.get(threadId) === entry) {
189
+ // Still our lane: back on the queue, ahead of anything that
190
+ // arrived during the seam call.
191
+ entry.items = [...batch, ...entry.items];
192
+ settleChatFollowups(ids, "pending");
193
+ }
194
+ else {
195
+ // cancelAsides took the lane while the seam call was awaited: the
196
+ // words never ran, but their lane is gone, so they retire with it
197
+ // instead of coming back as phantom rows after a restart.
198
+ settleChatFollowups(ids, "cancelled");
199
+ }
200
+ return { delivered: false, count: batch.length };
201
+ }
202
+ try {
203
+ recordInjectedAsides(store, threadId, batch);
204
+ }
205
+ catch (error) {
206
+ // The seam says the words may already be running; a failed transcript
207
+ // write must never hand them to a later boundary. Retire the rows
208
+ // loudly instead of replaying them.
209
+ console.error("aside-queue: could not record injected asides", error);
210
+ settleChatFollowups(ids, null);
211
+ if (asides.get(threadId) === entry && entry.items.length === 0)
212
+ asides.delete(threadId);
213
+ }
214
+ return { delivered: true, count: batch.length };
215
+ }
216
+ /**
217
+ * One boundary pass over the aside lane.
218
+ *
219
+ * Injection first: a thread already running a seam-capable turn gets every
220
+ * waiting aside folded in as ONE steer call (envelopes joined by a blank
221
+ * line — one boundary, one batch). Otherwise, when the thread is idle and
222
+ * unblocked, the asides degrade to a single enveloped follow-up turn, the
223
+ * same shape the steer-queue gives queued person messages. Callers run the
224
+ * steer drain before this one, so at a shared boundary the person's queued
225
+ * correction lands ahead of peer context.
226
+ */
227
+ export async function drainAsideMessages(options) {
228
+ const { store, inject, run, isBlocked } = options;
229
+ for (const [threadId, entry] of asides) {
230
+ if (inFlight.has(threadId))
231
+ continue;
232
+ const bot = store.projectBotForTask
233
+ ? store.projectBotForTask(entry.botId, threadId)
234
+ : store.bot(entry.botId);
235
+ if (!bot) {
236
+ // the bot or task was deleted while the aside waited
237
+ cancelAsides(threadId);
238
+ continue;
239
+ }
240
+ if (bot.busy) {
241
+ // Seam attempt: one batched call for everything waiting here.
242
+ await attemptAsideInjection(store, entry.botId, threadId, inject);
243
+ continue;
244
+ }
245
+ if (isBlocked?.(entry.botId, threadId))
246
+ continue;
247
+ // Degrade to one follow-up turn. The entry leaves the map before
248
+ // anything runs, so a settle racing another settle can never fire the
249
+ // same asides twice.
250
+ const items = entry.items;
251
+ const ids = items.map((item) => item.messageId);
252
+ settleChatFollowups(ids, "dispatching");
253
+ asides.delete(threadId);
254
+ const appended = recordDegradedAsides(store, threadId, items);
255
+ const last = appended.at(-1);
256
+ if (!last) {
257
+ settleChatFollowups(ids, null);
258
+ continue;
259
+ }
260
+ const running = run(entry.botId, threadId, items.map((item) => item.prompt).join("\n\n"), last, appended.map((message) => message.id), { trigger: items[0].trigger, aside: items[0].aside });
261
+ void Promise.resolve(running).then(() => settleChatFollowups(ids, null), () => settleChatFollowups(ids, "interrupted")).catch((error) => console.warn("aside-queue: could not settle degraded aside", error));
262
+ }
263
+ }
264
+ /** Append the waiting asides as transcript lines for the degraded turn.
265
+ * Same record shape as injection — enveloped text, queueId marker, peer
266
+ * provenance — so both deliveries read identically downstream. */
267
+ function recordDegradedAsides(store, threadId, items) {
268
+ const appended = [];
269
+ for (const item of items) {
270
+ appended.push(store.appendMessage(threadId, {
271
+ role: "user",
272
+ kind: "text",
273
+ text: item.prompt,
274
+ queueId: item.messageId,
275
+ aside: true,
276
+ peerAsk: {
277
+ botId: item.aside.fromBotId,
278
+ name: item.aside.fromBotName,
279
+ ...(item.aside.unattended ? { unattended: true } : {}),
280
+ },
281
+ }));
282
+ }
283
+ return appended;
284
+ }
285
+ /** Whether a thread currently has an injection or drain in flight. The
286
+ * arrival path uses this to stay honest ("queued") instead of racing a
287
+ * boundary pass that may be folding the same lane right now. */
288
+ export function asideAttemptInFlight(threadId) {
289
+ return inFlight.has(threadId);
290
+ }
291
+ /** Test helper: how many asides remain queued for a thread. */
292
+ export function _asideCount(threadId) {
293
+ return asides.get(threadId)?.items.length ?? 0;
294
+ }
@@ -61,6 +61,9 @@ const IMAGE_MIMES = {
61
61
  * despite using ZIP internally. The claimed mime determines the extension;
62
62
  * an attacker-controlled filename never does. */
63
63
  const FILE_MIMES = {
64
+ "video/mp4": ".mp4",
65
+ "video/webm": ".webm",
66
+ "video/quicktime": ".mov",
64
67
  "audio/opus": ".opus",
65
68
  "audio/ogg": ".ogg",
66
69
  "audio/mpeg": ".mp3",
@@ -539,6 +542,61 @@ export function saveImage(bytes, mime, requestedUploadId) {
539
542
  reservation.release();
540
543
  }
541
544
  }
545
+ /** Persist one synthesized audio note. Providers return audio/mpeg buffers;
546
+ * the same atomic-write and quota discipline as saveImage applies, with a
547
+ * fixed .mp3 extension because the accepted mime determines it exactly. */
548
+ export function saveAudio(bytes, mime) {
549
+ const normalized = mime.split(";")[0].trim().toLowerCase();
550
+ if (normalized !== "audio/mpeg") {
551
+ throw Object.assign(new Error("unsupported audio type"), { status: 400 });
552
+ }
553
+ if (bytes.byteLength === 0)
554
+ throw Object.assign(new Error("empty audio"), { status: 400 });
555
+ if (bytes.byteLength > FILE_MAX_BYTES) {
556
+ throw Object.assign(new Error(`audio exceeds ${FILE_MAX_BYTES} bytes`), { status: 413 });
557
+ }
558
+ ensureAttachmentsDir();
559
+ const reservation = new AttachmentReservation();
560
+ reservation.reserve(bytes.byteLength);
561
+ const id = randomUUID();
562
+ const name = `${id}.mp3`;
563
+ const path = join(ATTACHMENTS_DIR, name);
564
+ const partialPath = join(ATTACHMENTS_DIR, `.openmaus-upload-${id}-${randomUUID()}.partial`);
565
+ activePartials.add(partialPath);
566
+ let partialCleanupFailed = false;
567
+ try {
568
+ writeFileSync(partialPath, bytes, { mode: 0o600, flag: "wx" });
569
+ try {
570
+ linkSync(partialPath, path);
571
+ addCommittedBytes(bytes.byteLength);
572
+ }
573
+ catch (error) {
574
+ if (error.code !== "EEXIST")
575
+ throw error;
576
+ const saved = readFileSync(path);
577
+ if (!saved.equals(bytes)) {
578
+ throw Object.assign(new Error("audio destination already exists with different bytes"), { status: 409 });
579
+ }
580
+ }
581
+ unlinkSync(partialPath);
582
+ return { path, mime: normalized, bytes: bytes.byteLength };
583
+ }
584
+ catch (error) {
585
+ try {
586
+ unlinkSync(partialPath);
587
+ }
588
+ catch {
589
+ partialCleanupFailed = true;
590
+ }
591
+ throw error;
592
+ }
593
+ finally {
594
+ activePartials.delete(partialPath);
595
+ if (partialCleanupFailed)
596
+ invalidateAttachmentAccounting();
597
+ reservation.release();
598
+ }
599
+ }
542
600
  /** HTTP uploads take the same per-ID lock as streamed files. saveImage stays
543
601
  * synchronous for generated avatars, while this wrapper prevents an image
544
602
  * and a document using the same caller-supplied UUID from committing with
@@ -567,7 +625,7 @@ export function attachmentExists(name) {
567
625
  * filename (no separators, no dotfiles) inside ATTACHMENTS_DIR resolve —
568
626
  * the route must never become a general file server for the data dir. */
569
627
  export function readAttachment(name) {
570
- if (!/^[A-Za-z0-9-]+\.(png|jpg|jpeg|gif|webp)$/.test(name))
628
+ if (!/^[A-Za-z0-9-]+\.(png|jpg|jpeg|gif|webp|mp3)$/.test(name))
571
629
  return null;
572
630
  const path = join(ATTACHMENTS_DIR, name);
573
631
  if (extname(path) === ".jpeg")
@@ -589,7 +647,46 @@ function mimeForExt(ext) {
589
647
  return "image/gif";
590
648
  case ".webp":
591
649
  return "image/webp";
650
+ case ".mp3":
651
+ return "audio/mpeg";
592
652
  default:
593
653
  return "application/octet-stream";
594
654
  }
595
655
  }
656
+ /** Parse a single `bytes=` range against a stored audio file. Only the
657
+ * canonical shapes a player sends are honored: `a-b`, `a-`, and `-n`
658
+ * (the last-n-bytes suffix). Anything else — commas, letters, reversed
659
+ * bounds, internal spaces — reads as "not understood" and the caller sends
660
+ * the whole file, which is always a correct answer to a GET. */
661
+ export function parseAudioRange(header, size) {
662
+ const raw = header?.trim();
663
+ if (!raw)
664
+ return { kind: "none" };
665
+ const m = raw.match(/^bytes=(\d*)-(\d*)$/);
666
+ if (!m)
667
+ return { kind: "none" };
668
+ const [, rawStart, rawEnd] = m;
669
+ if (rawStart === "") {
670
+ // `bytes=-` carries no bounds at all: malformed input, not a suffix
671
+ // range, so the caller answers with the whole file. A real zero-length
672
+ // suffix (`bytes=-0`) stays unsatisfiable below, per RFC 9110.
673
+ if (rawEnd === "")
674
+ return { kind: "none" };
675
+ // suffix range: the final n bytes; zero is unsatisfiable by definition
676
+ const suffix = Number(rawEnd);
677
+ if (!suffix)
678
+ return { kind: "unsatisfiable" };
679
+ if (size === 0)
680
+ return { kind: "unsatisfiable" };
681
+ return { kind: "range", start: Math.max(0, size - suffix), end: size - 1 };
682
+ }
683
+ const start = Number(rawStart);
684
+ if (start >= size)
685
+ return { kind: "unsatisfiable" };
686
+ if (rawEnd === "")
687
+ return { kind: "range", start, end: size - 1 };
688
+ const end = Number(rawEnd);
689
+ if (end < start)
690
+ return { kind: "none" };
691
+ return { kind: "range", start, end: Math.min(end, size - 1) };
692
+ }
@@ -4,8 +4,7 @@
4
4
  // provider's own permission mode passed straight through (Claude `auto`,
5
5
  // Grok `--permission-mode`, Codex `approvalsReviewer`, …), and a request that
6
6
  // reaches this process is one the provider left for a person. The only
7
- // verdict the app synthesizes is Full access, because that level is the
8
- // person's explicit, separately confirmed grant to answer every prompt.
7
+ // grants the app applies are Full access and the person's exact saved commands.
9
8
  // Questions never come through here: a bot's question always reaches a human.
10
9
  import { supportsApprovalMode } from "../shared/approval-mode.js";
11
10
  /** A failed delivery is a runtime error, not another permission decision.
@@ -55,7 +54,7 @@ export function delegationInheritsFullAccess(input) {
55
54
  // the permission path in permission-proxy). Approving it there does not
56
55
  // produce an answer: the CLI runs the tool with none and the model is told
57
56
  // "The user did not answer the questions." — a question silently lost.
58
- const ASKS_A_PERSON = new Set(["askuserquestion", "ask_user"]);
57
+ const ASKS_A_PERSON = new Set(["askuserquestion", "ask_user", "omb-ask"]);
59
58
  export function autoVerdict(mode, tool, context) {
60
59
  // A question is for a person, whatever channel it arrived on — and
61
60
  // whatever the mode: even Full has no answer to give, only an approval
@@ -70,6 +69,8 @@ export function autoVerdict(mode, tool, context) {
70
69
  return { approve: `approved ${tool} (full access)`, source: "full-access" };
71
70
  if (context?.requiresExplicitApproval)
72
71
  return { approve: null, source: "explicit-approval-block" };
72
+ if (context?.commandAllowed)
73
+ return { approve: `approved ${tool} (saved command)`, source: "command-allowlist" };
73
74
  if (mode === "auto" || mode === "custom")
74
75
  return { approve: null, source: "native-approval" };
75
76
  return { approve: null, source: "no-grant" };
@@ -18,5 +18,6 @@ export function startAutoVmClaim(table, threadId, generation) {
18
18
  slot.begin = slot.claim().then(() => { slot.claimed = true; }, (error) => {
19
19
  slot.failed = true;
20
20
  slot.failure = error instanceof Error ? error.message : String(error);
21
+ slot.onRejected?.(slot.failure);
21
22
  });
22
23
  }