openmausbot 0.1.86 → 0.1.87

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/README.md +2 -2
  2. package/dist/assets/{abnfDiagram-O67JEVCF-DpUZNnFD.js → abnfDiagram-O67JEVCF-BTiuL79F.js} +1 -1
  3. package/dist/assets/{arc-DW7mrI4p.js → arc-C88Blc2v.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-CyeZ5WLZ.js → architectureDiagram-NJMV4G6O-BZsJIZrb.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-CBVaNjO7.js → blockDiagram-BEXU5L5S-CVeMe9Ah.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-sf8d41uT.js → c4Diagram-YGBWAQC7-Cedk3Bhx.js} +1 -1
  7. package/dist/assets/channel-taCNRlUU.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-BOGvy7Y_.js → chunk-2Q5K7J3B-D-VRqzTB.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-BiF9zyiz.js → chunk-3FUC2YCW-BChov01Y.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-EFuZ1SBZ.js → chunk-5VM5RSS4-C-TfwvHy.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-pJLNSAT7.js → chunk-GTNCS2PH-Db7CGj5Y.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-Cb-3_3IT.js → chunk-GWA4HPMP-CNvIfqKT.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-DAf5aogn.js → chunk-JWPE2WC7-C_bEp5tq.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-DR3kiwZz.js → chunk-NETBCI7D-Coi4BOls.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-BmU8PeyM.js → chunk-WEXAMYUT-CzylEuL3.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-Buh8g8OT.js → chunk-XXDRQBXY-m7p7ztaD.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-BurfmMWM.js → classDiagram-v2-NBCMYWYE-BMy6o9x2.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-KkGZUEkU.js → cose-bilkent-JH36ORCC-BDQ__0_J.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-C3PpV96d.js → cynefin-OW5HDTMX-Djv4ghUb.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-Dt8JwJaI.js → cynefinDiagram-VND7K2PF-BULrlGYH.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-DkjN8266.js → dagre-6A5THRUB-D-jrT_hU.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-DUAJS0JV.js → diagram-22UHCM2B-p2UhQmfL.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-Dw4KhF1a.js → diagram-3UASUU5V-DHvGWZ7q.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-Dd1gmQqf.js → diagram-ATOU4E4O-ChhKDot-.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-CjwGs0Yw.js → diagram-CDSNMT55-B-T7A43R.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-BL-69Zrq.js → diagram-MLGK6HIB-CJm8ENNY.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-5Zi2N7wo.js → diagram-MPIPVDR6-510tlza8.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-D9NekWUX.js → ebnfDiagram-ZINNZB2B-PkvRPHel.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-CkaSQjxw.js → elk-276RUBZZ-BVBVPoX3.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-CdAVBV8-.js → erDiagram-OPXOYQCR-BgYMVm7L.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-Bm_lWWH7.js → flowDiagram-KWPJA3E3-BGqsnc_3.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-FZ8ejYWJ.js → ganttDiagram-FUAMR5RP-D_-UeReH.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-Dlv9rizZ.js → gitGraphDiagram-X574FWY7-D0uooONx.js} +1 -1
  34. package/dist/assets/index-BwdMAQ6p.css +1 -0
  35. package/dist/assets/index-ByJennr7.js +581 -0
  36. package/dist/assets/{index-h9FFNwTu.js → index-zRsJERGJ.js} +1 -1
  37. package/dist/assets/{infoDiagram-VRGFBTTK-BdQ_gW5B.js → infoDiagram-VRGFBTTK-B9e1z77f.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-EwssEp2X.js → ishikawaDiagram-OU5B5YK6-BB4vNwvD.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-BX1G1vOd.js → journeyDiagram-ZHPQQLJL-DpkosOTB.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-BB4kAsyC.js → kanban-definition-PNTS6WVX-DbDP4kUu.js} +1 -1
  41. package/dist/assets/{linear-3su2TG93.js → linear-DBJGPmjT.js} +1 -1
  42. package/dist/assets/{mermaid.core-CDD1E0s9.js → mermaid.core-DWPRi-ue.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-Ba50MsNK.js → mindmap-definition-NLK3R4M7-nszBbJmx.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-DZLkkZlM.js → pegDiagram-GJSIUBJH-CWykJWu_.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-DaTz77Uq.js → pieDiagram-5QR66LMP-g3Da-Vnv.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-CpaiidpR.js → quadrantDiagram-O4NWA36T-Wzrlpzgh.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-mN4JrMeI.js → railroadDiagram-XR7U4H2S-moHTFMC0.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-BSNDRpg7.js → requirementDiagram-PLB6GJNP-CwfXdFH8.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-FngzGNug.js → sankeyDiagram-IPEJSGJF-3ZROYvnq.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-CM4pCC3L.js → sequenceDiagram-PO4LG4MO-E1TBebP8.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-CRICHXkw.js → sizeCapture-INFHLROL-DHKEU5pu.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-oyTWpxH2.js → stateDiagram-v2-GCMORJYK-BguEj09D.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-D9h9mPBq.js → swimlanes-2SLR337P-C7IDYnhd.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-BpEdoB2e.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-BJUlN9bW.js → timeline-definition-EJHVYXUP-DCW-F5PW.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-3uBNkbAb.js → usecaseDiagram-POWQR4AR-xBMFtWYV.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-D2odalzT.js → vennDiagram-UO4OBE2U-B5nIkuH8.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-DHgFaB9S.js → wardleyDiagram-VNRHLVJA-DYFAhdbr.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-w5GYM2h0.js → xychartDiagram-PMCCYNJV-DeXwX6Cz.js} +1 -1
  60. package/dist/index.html +2 -2
  61. package/dist-server/browser-proxy.js +2 -1
  62. package/dist-server/container-mcp.js +1029 -164
  63. package/dist-server/drivers/agents-proxy.js +64 -10
  64. package/dist-server/drivers/phone-proxy.js +33 -2
  65. package/dist-server/index.js +14577 -9001
  66. package/dist-server/local-computer-proxy.js +3 -3
  67. package/dist-server/local-computer.js +53 -17
  68. package/dist-server/openmausbot.js +3981 -1165
  69. package/dist-server/pair-cli.js +3981 -1165
  70. package/dist-server/permission-proxy.js +29 -9
  71. package/dist-server/server/attachments.js +55 -0
  72. package/dist-server/server/auto-approve.js +4 -3
  73. package/dist-server/server/auto-vm-claims.js +1 -0
  74. package/dist-server/server/bot-package.js +18 -292
  75. package/dist-server/server/bot-profile.js +5 -17
  76. package/dist-server/server/box-create-idempotency.js +3 -3
  77. package/dist-server/server/box.js +57 -55
  78. package/dist-server/server/browser-proxy.js +3 -2
  79. package/dist-server/server/browser-runtime.js +37 -1
  80. package/dist-server/server/cli.js +2 -2
  81. package/dist-server/server/command-allowlist.js +154 -0
  82. package/dist-server/server/computer-wait.js +22 -9
  83. package/dist-server/server/config.js +59 -9
  84. package/dist-server/server/connector-verdict.js +141 -0
  85. package/dist-server/server/container-computer.js +1 -1
  86. package/dist-server/server/default-model-selection.js +9 -0
  87. package/dist-server/server/delegations.js +85 -34
  88. package/dist-server/server/drivers/acp/core.js +57 -15
  89. package/dist-server/server/drivers/agents-call.js +42 -3
  90. package/dist-server/server/drivers/agents-catalog.js +34 -4
  91. package/dist-server/server/drivers/boxagent.js +174 -36
  92. package/dist-server/server/drivers/builtIn.js +2 -0
  93. package/dist-server/server/drivers/chat-box-tools.js +90 -0
  94. package/dist-server/server/drivers/chat-images.js +54 -0
  95. package/dist-server/server/drivers/chat-mcp-tools.js +38 -8
  96. package/dist-server/server/drivers/chat-tool-approval.js +70 -18
  97. package/dist-server/server/drivers/claude.js +7 -2
  98. package/dist-server/server/drivers/codex-instructions.js +48 -6
  99. package/dist-server/server/drivers/codex-tool-surface.js +20 -0
  100. package/dist-server/server/drivers/codex.js +95 -30
  101. package/dist-server/server/drivers/mistral.js +115 -0
  102. package/dist-server/server/drivers/openai-chat.js +155 -32
  103. package/dist-server/server/drivers/openai-compat.js +1 -0
  104. package/dist-server/server/drivers/permission-command.js +35 -0
  105. package/dist-server/server/drivers/phone-proxy.js +47 -2
  106. package/dist-server/server/drivers/pi.js +75 -4
  107. package/dist-server/server/drivers/prompt-split.js +87 -0
  108. package/dist-server/server/fleet.js +1 -1
  109. package/dist-server/server/group-thread-usage.js +103 -0
  110. package/dist-server/server/harness/registry.js +1 -0
  111. package/dist-server/server/hosted-models.js +1 -1
  112. package/dist-server/server/index.js +1385 -322
  113. package/dist-server/server/local-computer.js +72 -23
  114. package/dist-server/server/managed-desktop.js +1 -1
  115. package/dist-server/server/mcp-bridge.js +6 -6
  116. package/dist-server/server/new-bot-defaults.js +115 -0
  117. package/dist-server/server/org-library.js +1051 -0
  118. package/dist-server/server/package-export.js +627 -39
  119. package/dist-server/server/package-import.js +458 -0
  120. package/dist-server/server/package-parts.js +87 -0
  121. package/dist-server/server/peer-approval.js +16 -7
  122. package/dist-server/server/peer-roster.js +10 -0
  123. package/dist-server/server/presets.js +492 -0
  124. package/dist-server/server/provider-key-check.js +3 -2
  125. package/dist-server/server/published-teams.js +80 -0
  126. package/dist-server/server/room-handoffs.js +19 -3
  127. package/dist-server/server/routes/bot-presets.js +25 -0
  128. package/dist-server/server/routine-requests.js +6 -3
  129. package/dist-server/server/routines.js +41 -1
  130. package/dist-server/server/section-context.js +10 -0
  131. package/dist-server/server/skills.js +34 -61
  132. package/dist-server/server/steer-queue.js +3 -2
  133. package/dist-server/server/store.js +259 -15
  134. package/dist-server/server/surface.js +10 -4
  135. package/dist-server/server/system-prompt.js +12 -4
  136. package/dist-server/server/team-backup.js +6 -2
  137. package/dist-server/server/team-computers.js +10 -0
  138. package/dist-server/server/team-library.js +6 -2
  139. package/dist-server/server/team-manifest.js +4 -3
  140. package/dist-server/server/team-setup-requests.js +16 -5
  141. package/dist-server/server/thread-events.js +17 -1
  142. package/dist-server/server/turn-context.js +2 -0
  143. package/dist-server/server/turn-watchdog.js +12 -3
  144. package/dist-server/server/usage-ledger.js +12 -1
  145. package/dist-server/server/vps-computer.js +139 -51
  146. package/dist-server/server/vps-container-mcp.js +7 -0
  147. package/dist-server/server/vps-ssh.js +36 -7
  148. package/dist-server/server/workspace-backup-policy.js +12 -0
  149. package/dist-server/server/workspace-backup.js +2 -2
  150. package/dist-server/server/workspace.js +1 -1
  151. package/dist-server/shared/approval-mode.js +1 -1
  152. package/dist-server/shared/ask-question.js +188 -12
  153. package/dist-server/shared/bot-profile.js +15 -0
  154. package/dist-server/shared/command-allowlist.js +1 -0
  155. package/dist-server/shared/credential-request.js +1 -1
  156. package/dist-server/shared/package-format.js +967 -0
  157. package/dist-server/shared/skill-md.js +64 -0
  158. package/dist-server/shared/team-backup.js +7 -0
  159. package/dist-server/shared/wire.js +4 -0
  160. package/dist-server/vps-container-mcp.js +1129 -170
  161. package/enterprise/README.md +1 -1
  162. package/package.json +1 -1
  163. package/dist/assets/channel-BXFkgus9.js +0 -1
  164. package/dist/assets/index-BGgqlWPJ.css +0 -1
  165. package/dist/assets/index-BtHo8ZKl.js +0 -577
  166. package/dist/assets/swimlanesDiagram-TC7HE7FX-BWrvB2ak.js +0 -8
@@ -16,7 +16,7 @@ import { writeFileAtomic } from "./atomic.js";
16
16
  import { getOrCreateChannel, mirrorExchange } from "./comms-visibility.js";
17
17
  import { DATA_DIR } from "./config.js";
18
18
  import { newId } from "./contracts.js";
19
- import { requestPeerApproval } from "./peer-approval.js";
19
+ import { peerApprovalFailure, requestPeerApproval } from "./peer-approval.js";
20
20
  import { canAccessTeam, peerAllowed } from "./peer-roster.js";
21
21
  /** Per source-thread queue. Persisted to delegations.json on every change
22
22
  * and reloaded at boot: a handoff queued right before a restart runs after
@@ -40,6 +40,13 @@ const RESULT_MAX_CHARS = 4_000;
40
40
  * to expire. This is not an uptime clock: sleep within a running process
41
41
  * still counts, and a non-expired restored window keeps its deadline. */
42
42
  export const DELEGATION_TTL_MS = 24 * 60 * 60 * 1000;
43
+ /** How long a handoff may wait on a target that never goes idle. The 24-hour
44
+ * window above bounds the rare case; the busy hold is the common one — a peer
45
+ * stays busy for a whole day and the delegating bot hears nothing back. Past
46
+ * this cap a still-blocked handoff expires with its own wording. Env-tunable
47
+ * so tests (and patient teams) can shrink or stretch it. */
48
+ const configuredBusyHoldMaxMs = Number(process.env.OMB_DELEGATION_BUSY_HOLD_MAX_MS);
49
+ export const DELEGATION_BUSY_HOLD_MAX_MS = Math.max(1_000, Number.isFinite(configuredBusyHoldMaxMs) && process.env.OMB_DELEGATION_BUSY_HOLD_MAX_MS !== "" ? configuredBusyHoldMaxMs : 2 * 60 * 60 * 1000);
43
50
  let receipts = [];
44
51
  function saveReceipts() {
45
52
  try {
@@ -63,6 +70,10 @@ export function recordDelegationReceipt(receipt) {
63
70
  };
64
71
  if (receipt.result !== undefined)
65
72
  bounded.result = receipt.result.slice(0, RESULT_MAX_CHARS);
73
+ if (receipt.approvalOutcome !== undefined) {
74
+ bounded.approvalOutcome = receipt.approvalOutcome;
75
+ bounded.approvalSource = peerApprovalFailure(receipt.approvalOutcome).approvalSource;
76
+ }
66
77
  receipts = [bounded, ...receipts.filter((existing) => existing.id !== bounded.id)]
67
78
  .filter((existing) => now - existing.finishedAt <= RECEIPT_MAX_AGE_MS)
68
79
  .slice(0, MAX_RECEIPTS);
@@ -90,9 +101,9 @@ export function threadsWaitingOn(toBotId) {
90
101
  }
91
102
  /** Mark a target's observed busy period as finished and return the source
92
103
  * threads that should be retried. A handoff waits until the target is free,
93
- * bounded only by the 24-hour expiry — this just clears the "parked on a
94
- * busy period" marker so the next drain re-evaluates it, rather than
95
- * counting or limiting retries.
104
+ * bounded by the busy-hold cap and the 24-hour expiry — this just clears the
105
+ * "parked on a busy period" marker so the next drain re-evaluates it, rather
106
+ * than counting or limiting retries.
96
107
  * `only` narrows the release: a bot that is still busy in one thread has
97
108
  * nevertheless freed a slot for the fresh-thread handoffs waiting on it,
98
109
  * while its active-thread handoffs go on waiting for it to go idle. */
@@ -104,6 +115,7 @@ export function releaseDelegationsWaitingOn(toBotId, only) {
104
115
  if (item.toBotId !== toBotId || item.waitingOnBusy !== true || (only && !only(item)))
105
116
  continue;
106
117
  delete item.waitingOnBusy;
118
+ delete item.busySince;
107
119
  any = true;
108
120
  }
109
121
  if (any)
@@ -164,8 +176,14 @@ export function _loadPending() {
164
176
  };
165
177
  if (item.approvalAlreadyGranted === true)
166
178
  loaded.approvalAlreadyGranted = true;
167
- if (item.waitingOnBusy === true)
179
+ if (item.waitingOnBusy === true) {
168
180
  loaded.waitingOnBusy = true;
181
+ const currentHold = Number.isFinite(item.busySince) && item.busySince <= now &&
182
+ now - item.busySince < DELEGATION_BUSY_HOLD_MAX_MS;
183
+ loaded.busySince = currentHold ? item.busySince : now;
184
+ if (!currentHold)
185
+ backfilled = true;
186
+ }
169
187
  if (item.waitAnnounced === true || legacyAlreadyAnnounced)
170
188
  loaded.waitAnnounced = true;
171
189
  if (typeof item.originatingGroupId === "string" && item.originatingGroupId) {
@@ -212,6 +230,10 @@ export function _loadPending() {
212
230
  const receipt = { id, sourceThreadId, toBotId, toBotName, status, finishedAt: finishedAt };
213
231
  if (typeof result === "string")
214
232
  receipt.result = result;
233
+ if (candidate.approvalOutcome === "deny" || candidate.approvalOutcome === "expired" || candidate.approvalOutcome === "cancelled") {
234
+ receipt.approvalOutcome = candidate.approvalOutcome;
235
+ receipt.approvalSource = peerApprovalFailure(candidate.approvalOutcome).approvalSource;
236
+ }
215
237
  loaded.push(receipt);
216
238
  }
217
239
  receipts = loaded.slice(0, MAX_RECEIPTS);
@@ -405,41 +427,61 @@ function acknowledgeDelegation(threadId, itemId) {
405
427
  savePending();
406
428
  }
407
429
  const isExpired = (item, now) => now - item.queuedAt >= DELEGATION_TTL_MS;
430
+ /** Past the busy-hold cap — the tighter bound that fires while its target is
431
+ * still busy, long before the 24-hour window. */
432
+ const busyHoldExpired = (item, now) => item.busySince !== undefined && now - item.busySince >= DELEGATION_BUSY_HOLD_MAX_MS;
433
+ /** The busy-hold cap in chip-ready words ("2 hours", "90 minutes"). */
434
+ export function busyHoldCapText(maxMs = DELEGATION_BUSY_HOLD_MAX_MS) {
435
+ const minutes = Math.max(1, Math.round(maxMs / 60_000));
436
+ const amount = minutes % 60 === 0 ? minutes / 60 : minutes;
437
+ const unit = minutes % 60 === 0 ? "hour" : "minute";
438
+ return `${amount} ${unit}${amount === 1 ? "" : "s"}`;
439
+ }
408
440
  /** Record an expired handoff. The chip goes into the source thread only
409
441
  * while it still belongs to the bot that owns the handoff — a deleted
410
442
  * conversation gets the receipt and nothing else. */
411
443
  function expireDelegation(bus, sourceThreadId, item, ownerId) {
412
- const name = bus.store.bot(item.toBotId)?.name ?? item.toBotId;
444
+ const target = bus.store.bot(item.toBotId);
445
+ const name = target?.name ?? item.toBotId;
446
+ // A busy hold has its own words so a two-hour busy wait is never reported
447
+ // as a 24-hour one; every other expiry keeps the TTL wording.
448
+ const now = Date.now();
449
+ const busyHold = Boolean(target) && busyHoldExpired(item, now) && !isExpired(item, now);
413
450
  recordDelegationReceipt({
414
451
  id: item.id,
415
452
  sourceThreadId,
416
453
  toBotId: item.toBotId,
417
454
  toBotName: name,
418
455
  status: "expired",
419
- result: `@${name} was not free to take this for 24 hours`,
456
+ result: busyHold ? `@${name} was still busy after ${busyHoldCapText()}` : `@${name} was not free to take this for 24 hours`,
420
457
  });
421
458
  if (!sourceThreadBelongsToBot(bus.store, ownerId, sourceThreadId))
422
459
  return;
423
460
  bus.store.appendMessage(sourceThreadId, {
424
461
  role: "bot",
425
462
  kind: "activity",
426
- tool: { name: `Delegation to @${name} expired — not picked up within 24 hours`, ok: false },
463
+ tool: {
464
+ name: busyHold
465
+ ? `Delegation to @${name} expired — still busy after ${busyHoldCapText()}`
466
+ : `Delegation to @${name} expired — not picked up within 24 hours`,
467
+ ok: false,
468
+ },
427
469
  });
428
470
  }
429
- /** Past its 24 hours AND unable to be delivered right now — the same rule
471
+ /** Past a delivery bound AND unable to be delivered right now — the same rule
430
472
  * `processOne` applies, so the hourly sweep and a live drain never disagree
431
- * about which items are actually stuck. A target that was deleted counts as
432
- * "cannot take the turn": there is nothing to wait on, so such items still
433
- * expire even though there is no bot left to test busy/free against. */
473
+ * about which items are actually stuck. The busy-hold cap expires a handoff
474
+ * whose target has been busy the whole time; the 24-hour TTL catches the
475
+ * rest. A target that was deleted counts as "cannot take the turn": there is
476
+ * nothing to wait on, so such items still expire even though there is no bot
477
+ * left to test busy/free against. */
434
478
  function isDueForExpiry(bus, item, now) {
435
- if (!isExpired(item, now))
436
- return false;
437
479
  const target = bus.store.bot(item.toBotId);
438
480
  if (!target)
439
- return true;
440
- return !targetCanTakeTurn(bus, target, item);
481
+ return isExpired(item, now);
482
+ return !targetCanTakeTurn(bus, target, item) && (isExpired(item, now) || busyHoldExpired(item, now));
441
483
  }
442
- /** Expire every queued handoff past DELEGATION_TTL_MS that still cannot be
484
+ /** Expire every queued handoff past a delivery bound that still cannot be
443
485
  * delivered, wherever it waits. A drain already expires what it touches;
444
486
  * this covers the handoff nothing drains — a target that never settles
445
487
  * while its source sits idle. A thread mid-drain is skipped: that drain
@@ -549,13 +591,15 @@ async function processOne(bus, approvalBus, from, sourceThreadId, item, runTarge
549
591
  if (dropIfThreadGone(bus, target, sourceThreadId, item)) {
550
592
  return "settled";
551
593
  }
552
- // Past its 24 hours AND the target still cannot take the turn: this is the
553
- // bound on a busy wait. Use the same free/busy test as holdWhileTargetBusy;
554
- // an available target gets even an overdue item. Restart recovery renews
555
- // elapsed windows in _loadPending before any target becomes busy. Decide
556
- // before announcing a wait so an item cannot post both chips in one pass.
594
+ // Past a delivery bound AND the target still cannot take the turn: this is
595
+ // the bound on a busy wait — the busy-hold cap when the target has been
596
+ // busy the whole time, the 24-hour TTL otherwise. Use the same free/busy
597
+ // test as holdWhileTargetBusy; an available target gets even an overdue
598
+ // item. Restart recovery renews elapsed windows in _loadPending before any
599
+ // target becomes busy. Decide before announcing a wait so an item cannot
600
+ // post both chips in one pass.
557
601
  const canTakeTurn = targetCanTakeTurn(bus, target, item);
558
- if (!canTakeTurn && isExpired(item, Date.now())) {
602
+ if (!canTakeTurn && (isExpired(item, Date.now()) || busyHoldExpired(item, Date.now()))) {
559
603
  expireDelegation(bus, sourceThreadId, item, sender.id);
560
604
  return "settled";
561
605
  }
@@ -564,6 +608,7 @@ async function processOne(bus, approvalBus, from, sourceThreadId, item, runTarge
564
608
  return held;
565
609
  if (item.waitingOnBusy) {
566
610
  delete item.waitingOnBusy;
611
+ delete item.busySince;
567
612
  savePending();
568
613
  }
569
614
  if (sender.approvePeerComms && !item.approvalAlreadyGranted) {
@@ -582,22 +627,26 @@ async function processOne(bus, approvalBus, from, sourceThreadId, item, runTarge
582
627
  toBotName: target.name,
583
628
  status: "dropped",
584
629
  result: "the peer or source conversation no longer exists",
630
+ ...(verdict !== "allow" ? { approvalOutcome: verdict } : {}),
585
631
  });
586
632
  return "settled";
587
633
  }
588
634
  if (verdict !== "allow") {
635
+ const failure = peerApprovalFailure(verdict);
589
636
  recordDelegationReceipt({
590
637
  id: item.id,
591
638
  sourceThreadId,
592
639
  toBotId: target.id,
593
640
  toBotName: target.name,
594
- status: "denied",
595
- result: "the user denied this handoff",
641
+ status: verdict === "deny" ? "denied" : verdict,
642
+ approvalOutcome: verdict,
643
+ result: verdict === "deny" ? "the user denied this handoff" : failure.error,
596
644
  });
597
645
  bus.store.appendMessage(sourceThreadId, {
598
646
  role: "bot",
599
647
  kind: "activity",
600
- tool: { name: `Delegation to @${target.name} denied by user`, ok: false },
648
+ tool: { name: verdict === "deny" ? `Delegation to @${target.name} denied by user`
649
+ : `Delegation to @${target.name}: ${failure.error}`, ok: false },
601
650
  });
602
651
  return "settled";
603
652
  }
@@ -614,9 +663,10 @@ async function processOne(bus, approvalBus, from, sourceThreadId, item, runTarge
614
663
  }
615
664
  // Approval may have waited for minutes (or, with approvalAlreadyGranted,
616
665
  // up to 24h since the original ask_bot approval) — recheck the same
617
- // free/busy-gated expiry as the pre-approval path before dispatching.
666
+ // free/busy-gated, busy-hold-capped expiry as the pre-approval path
667
+ // before dispatching.
618
668
  const canTakeTurnAfterApproval = targetCanTakeTurn(bus, current, item);
619
- if (!canTakeTurnAfterApproval && isExpired(item, Date.now())) {
669
+ if (!canTakeTurnAfterApproval && (isExpired(item, Date.now()) || busyHoldExpired(item, Date.now()))) {
620
670
  expireDelegation(bus, sourceThreadId, item, currentSender.id);
621
671
  return "settled";
622
672
  }
@@ -656,18 +706,19 @@ function targetCanTakeTurn(bus, target, item) {
656
706
  ? bus.canAdmitDirectTurn(target.id, target.threadId)
657
707
  : !target.busy;
658
708
  }
659
- /** A busy target holds the handoff. Neither counts busy periods — the only
660
- * bound is DELEGATION_TTL_MS, checked in processOne before this runs (using
661
- * the same `targetCanTakeTurn` test, passed in as `canTakeTurn` when the
662
- * caller already computed it so the two checks can't disagree). One waiting
663
- * chip per handoff, worded for what the target is actually doing. Returns
664
- * null when the target can take the turn now. */
709
+ /** A busy target holds the handoff. Neither counts busy periods — the bounds
710
+ * are the busy-hold cap and DELEGATION_TTL_MS, checked in processOne before
711
+ * this runs (using the same `targetCanTakeTurn` test, passed in as
712
+ * `canTakeTurn` when the caller already computed it so the two checks can't
713
+ * disagree). One waiting chip per handoff, worded for what the target is
714
+ * actually doing. Returns null when the target can take the turn now. */
665
715
  function holdWhileTargetBusy(bus, target, sourceThreadId, item, canTakeTurn = targetCanTakeTurn(bus, target, item)) {
666
716
  if (canTakeTurn)
667
717
  return null;
668
718
  if (item.waitingOnBusy)
669
719
  return "requeued";
670
720
  item.waitingOnBusy = true;
721
+ item.busySince = Date.now();
671
722
  if (!item.waitAnnounced) {
672
723
  item.waitAnnounced = true;
673
724
  bus.store.appendMessage(sourceThreadId, {
@@ -31,6 +31,7 @@ import { basename, dirname, isAbsolute, relative, resolve } from "node:path";
31
31
  import { createHash } from "node:crypto";
32
32
  import { PROVIDER_CREDENTIAL_ENV, stripControlPlaneEnv, WORKSPACE_CREDENTIAL_ENV } from "../../config.js";
33
33
  import { decodeInjectId } from "../local-inject.js";
34
+ import { promptHalves, readPromptSplitReceipt, splitSessionPrompt, writePromptSplitReceipt } from "../prompt-split.js";
34
35
  import { describeSpawnFailure, execCli, killCliTree, spawnCli } from "../../procs.js";
35
36
  /**
36
37
  * A `host::model` pick talks to a loopback server with its own key.
@@ -42,7 +43,9 @@ export function skipSubscriptionAuthForLocalInject(model) {
42
43
  import { newEventId, newId } from "../../contracts.js";
43
44
  import { augmentedPath } from "../../env-path.js";
44
45
  import { supportsApprovalMode } from "../../../shared/approval-mode.js";
46
+ import { parseAskQuestions, parseChoices, questionAnswersByQuestion } from "../../../shared/ask-question.js";
45
47
  import { appendNative } from "../native.js";
48
+ import { acpPermissionCommand, permissionLaunchCwd } from "../permission-command.js";
46
49
  import { commandSummary, toolDetailPreview } from "../../tool-summary.js";
47
50
  import { extractMcpImages } from "../../mcp-tool-images.js";
48
51
  import { redactSecretsInText } from "../../redact.js";
@@ -458,6 +461,7 @@ export function createAcpDriver(support) {
458
461
  * session.current, so a request that lands between turns is answered
459
462
  * (never brokered) instead of left hanging. */
460
463
  const openSession = (threadId, launch, argv, env, cwd, contractKey) => {
464
+ const commandCwd = permissionLaunchCwd(cwd);
461
465
  const child = spawnCli(launch.command, argv, {
462
466
  cwd,
463
467
  env,
@@ -660,16 +664,34 @@ export function createAcpDriver(support) {
660
664
  }
661
665
  }
662
666
  const tool = kind === "execute" ? "shell" : kind === "edit" ? "edit" : kind || "tool";
667
+ const isShellCommand = !isQuestion && kind === "execute" && !/^mcp(?:__|[.:])/i.test(String(toolCall.title ?? ""));
663
668
  const summary = String(toolCall.rawInput?.command ?? toolCall.title ?? tool).slice(0, 200);
669
+ // One structured question beside the flat choices: the richer card
670
+ // renders from it while older clients keep answering through
671
+ // `choices`. Built once here so the emit and the answer path can
672
+ // never disagree. parseAskQuestions enforces the shared caps.
673
+ const questionChoices = isQuestion
674
+ ? options.flatMap((option) => typeof option.name === "string" && option.name.trim() ? [option.name.trim()] : [])
675
+ : [];
676
+ const askQuestions = isQuestion && questionChoices.length
677
+ ? parseAskQuestions({ questions: [{ question: summary, options: questionChoices }] }) ?? undefined
678
+ : undefined;
664
679
  const requestId = newId();
665
680
  const finish = (behavior, source = "user", message, always) => {
666
681
  if (!current.asks.delete(requestId))
667
- return;
682
+ return "unavailable";
668
683
  clearTimeout(timer);
669
684
  const want = behavior === "allow" ? "allow" : "reject";
670
685
  const forSession = want === "allow" && always === true && !isQuestion && !current.controlsHost;
686
+ // A structured card replies in the Q:/A: block format; recover the
687
+ // picked label from it so exact-match keeps working. Flat clients
688
+ // send the bare label, which the single-question fallback inside
689
+ // questionAnswersByQuestion already returns unchanged.
690
+ const picked = askQuestions
691
+ ? questionAnswersByQuestion(message ?? "", askQuestions)[askQuestions[0].question] ?? message
692
+ : message;
671
693
  const named = isQuestion && behavior === "answer"
672
- ? options.filter((option) => option.optionId === message || option.name?.trim() === message)
694
+ ? options.filter((option) => option.optionId === picked || parseChoices([option.name], 1)?.[0] === picked)
673
695
  : [];
674
696
  const optionId = behavior === "cancel"
675
697
  ? null
@@ -700,6 +722,7 @@ export function createAcpDriver(support) {
700
722
  source: optionId ? source : "system",
701
723
  approvalScope: current.controlsHost ? "local-computer" : undefined,
702
724
  });
725
+ return !optionId ? "rejected" : isQuestion ? "answered" : behavior === "allow" ? "allowed-once" : "rejected";
703
726
  };
704
727
  const timer = setTimeout(() => {
705
728
  emit({ ...base(threadId, current.turnId), type: "runtime.error", message: DENY_TIMEOUT_NOTE });
@@ -714,9 +737,10 @@ export function createAcpDriver(support) {
714
737
  requestType: isQuestion ? "question" : "permission",
715
738
  tool,
716
739
  summary,
717
- choices: isQuestion
718
- ? options.flatMap((option) => typeof option.name === "string" && option.name.trim() ? [option.name.trim()] : [])
719
- : undefined,
740
+ command: isShellCommand ? acpPermissionCommand(toolCall.rawInput, commandCwd) : undefined,
741
+ requiresExplicitApproval: isShellCommand && (toolCall.rawInput?.dangerouslyDisableSandbox === true || toolCall.rawInput?.sandbox_permissions === "require_escalated") || undefined,
742
+ choices: askQuestions?.[0]?.options.map(option => option.label) ?? (isQuestion ? questionChoices : undefined),
743
+ ...(askQuestions ? { questions: askQuestions } : {}),
720
744
  approvalScope: current.controlsHost ? "local-computer" : undefined,
721
745
  // the driver can honor a session-wide allow either way
722
746
  allowSession: !isQuestion && !current.controlsHost ? true : undefined,
@@ -1283,11 +1307,28 @@ export function createAcpDriver(support) {
1283
1307
  throw error;
1284
1308
  }
1285
1309
  emitSessionStarted();
1310
+ // The stable/volatile split: the full prompt rides only the turn
1311
+ // that establishes - or re-instructs, after a soul edit - this
1312
+ // native session. Later turns go through bare unless the volatile
1313
+ // half changed, so a memory edit neither appends a second copy of
1314
+ // the prompt to the agent's session history nor re-prices the
1315
+ // prefix its provider cached. Receipts are durable because the
1316
+ // native session outlives this process; an un-split turn (a direct
1317
+ // adapter call) keeps the legacy full-prompt shape.
1318
+ const halves = promptHalves(turn);
1319
+ let promptInput = promptTurn;
1320
+ let pendingSplitReceipt = null;
1321
+ if (halves.stable !== null) {
1322
+ const receiptKey = JSON.stringify([threadId, sessionId]);
1323
+ const composed = splitSessionPrompt(halves.stable, halves.volatile, readPromptSplitReceipt(DRIVER_KIND, receiptKey), promptTurn.system, promptTurn.text, Boolean(turn.mentionTurn));
1324
+ promptInput = { ...promptTurn, system: "", text: composed.text };
1325
+ pendingSplitReceipt = { key: receiptKey, receipt: composed.receipt };
1326
+ }
1286
1327
  const text = support.buildPromptText
1287
- ? support.buildPromptText(promptTurn)
1288
- : promptTurn.system
1289
- ? `${promptTurn.system}\n\n${promptTurn.text}`
1290
- : promptTurn.text;
1328
+ ? support.buildPromptText(promptInput)
1329
+ : promptInput.system
1330
+ ? `${promptInput.system}\n\n${promptInput.text}`
1331
+ : promptInput.text;
1291
1332
  const imageBlocks = support.images === true && runtimeAcceptsImages
1292
1333
  ? await readAcpImageBlocks(turn.images ?? [])
1293
1334
  : [];
@@ -1301,6 +1342,12 @@ export function createAcpDriver(support) {
1301
1342
  const promptIdleMs = promptIdleTimeoutMs();
1302
1343
  const result = await request("session/prompt", { sessionId, prompt: [{ type: "text", text }, ...imageBlocks] }, undefined, undefined, promptIdleMs, `${DRIVER_KIND} went fully silent ${Math.round(promptIdleMs / 1000)} s after the message and the turn was stopped. ` +
1303
1344
  "Raise OPENMAUS_ACP_PROMPT_IDLE_TIMEOUT_MS if this model legitimately takes longer to answer.");
1345
+ if (pendingSplitReceipt) {
1346
+ // session/prompt resolving is the acceptance boundary: a
1347
+ // rejected prompt leaves the receipt unwritten, so the next
1348
+ // turn redelivers what this one never received.
1349
+ writePromptSplitReceipt(DRIVER_KIND, pendingSplitReceipt.key, pendingSplitReceipt.receipt);
1350
+ }
1304
1351
  // opencode 1.18.18 reports usage at the result root; grok and
1305
1352
  // gemini put it under _meta. Read both rather than lose the count.
1306
1353
  const usage = result?.usage ?? result?._meta ?? {};
@@ -1407,12 +1454,7 @@ export function createAcpDriver(support) {
1407
1454
  const finish = turn?.asks.get(requestId);
1408
1455
  if (!finish)
1409
1456
  return "unavailable"; // settled, timed out, or turn gone
1410
- finish(decision.behavior, "user", decision.message, decision.always === true);
1411
- return decision.behavior === "allow"
1412
- ? "allowed-once"
1413
- : decision.behavior === "answer"
1414
- ? "answered"
1415
- : "rejected";
1457
+ return finish(decision.behavior, "user", decision.message, decision.always === true);
1416
1458
  },
1417
1459
  hasSession: (threadId) => active.has(threadId),
1418
1460
  stopAll: async () => {
@@ -352,6 +352,23 @@ function recallSpeaker(hit) {
352
352
  return hit.from;
353
353
  return hit.role === "user" ? "user" : "you";
354
354
  }
355
+ /** When a recalled line was said, on the machine's own clock. `toISOString()`
356
+ * answers in UTC, which disagrees with every other day the bot is shown: a
357
+ * bare `since`/`until` date is read as local midnight (recent-work.ts
358
+ * parseSince), the recent-work brief's times are local (whenLabel), and the
359
+ * daily memory logs are named after the local day. East of UTC a message
360
+ * from this morning was being dated yesterday — so a search for today's work
361
+ * came back stamped with the wrong date. */
362
+ function recallWhen(at, dateOnly) {
363
+ if (typeof at !== "number" || !Number.isFinite(at))
364
+ return "";
365
+ const said = new Date(at);
366
+ if (!Number.isFinite(said.getTime()))
367
+ return "";
368
+ const pad = (value) => String(value).padStart(2, "0");
369
+ const day = `${said.getFullYear()}-${pad(said.getMonth() + 1)}-${pad(said.getDate())}`;
370
+ return dateOnly ? day : `${day} ${pad(said.getHours())}:${pad(said.getMinutes())}`;
371
+ }
355
372
  export async function callTool(name, args, context) {
356
373
  // The names this body has always used, so it reads (and diffs) as it did
357
374
  // when these were the proxy's module-level constants.
@@ -727,11 +744,12 @@ export async function callTool(name, args, context) {
727
744
  name: botName,
728
745
  role,
729
746
  instructions,
747
+ ...(args.modelSelection !== undefined ? { modelSelection: args.modelSelection } : {}),
730
748
  }),
731
749
  });
732
750
  turn.createdThisTurn += 1;
733
751
  return {
734
- text: `Created @${r.name ?? botName} in ${r.section ?? "General"} [id: ${r.id}]. Assign work with ${COORDINATING ? "coordinate_bots" : "delegate_bot"}.`,
752
+ text: `Created @${r.name ?? botName} in ${r.section ?? "General"} [id: ${r.id}].${r.modelSelection ? ` Model: ${JSON.stringify(r.modelSelection)}.` : ""} Assign work with ${COORDINATING ? "coordinate_bots" : "delegate_bot"}.`,
735
753
  };
736
754
  }
737
755
  if (name === "create_room") {
@@ -816,6 +834,27 @@ export async function callTool(name, args, context) {
816
834
  text: `A secure ${r.label ?? CREDENTIAL_TARGETS[credentialId].label} request is ready. The desktop app and a freshly QR-paired mobile app show its secure entry card; older mobile pairings explain how to pair again or finish on the computer. End this turn; OpenMausBot will resume the task after the user saves or declines. Never ask them to paste the key into chat.`,
817
835
  };
818
836
  }
837
+ if (name === "send_voice_note") {
838
+ if (typeof args.text !== "string" || !args.text.trim()) {
839
+ return { text: "send_voice_note needs text: the short speakable note, at most 1000 characters.", isError: true };
840
+ }
841
+ const text = args.text.trim();
842
+ if (text.length > 1000) {
843
+ return { text: `send_voice_note is limited to 1000 characters; this one is ${text.length}. Shorten the note.`, isError: true };
844
+ }
845
+ try {
846
+ await api("/api/internal/voice-note", {
847
+ method: "POST",
848
+ body: JSON.stringify({ fromBotId: BOT_ID, fromThreadId: THREAD_ID, text }),
849
+ });
850
+ return { text: "Voice note recorded. It will be attached to this turn's reply when the turn ends; the note text is also the visible caption." };
851
+ }
852
+ catch (error) {
853
+ // A missing voice setup is the user's to fix, not a failed turn: hand
854
+ // the harness's setup guidance straight to the model.
855
+ return { text: `Voice note not sent: ${error instanceof Error ? error.message : String(error)}`, isError: true };
856
+ }
857
+ }
819
858
  if (name === "list_routines") {
820
859
  const query = new URLSearchParams({ fromBotId: BOT_ID, fromThreadId: THREAD_ID });
821
860
  const r = await api(`/api/internal/routines?${query.toString()}`);
@@ -1008,7 +1047,7 @@ export async function callTool(name, args, context) {
1008
1047
  }
1009
1048
  const lines = hits.map((hit) => {
1010
1049
  // a listing by time shows the time; a search by words keeps the date
1011
- const when = typeof hit.at === "number" ? new Date(hit.at).toISOString().slice(0, q ? 10 : 16).replace("T", " ") : "";
1050
+ const when = recallWhen(hit.at, Boolean(q));
1012
1051
  const task = typeof hit.task === "string" && hit.task ? `task "${hit.task}"` : "an earlier task";
1013
1052
  const where = hit.current
1014
1053
  ? "this conversation"
@@ -1040,7 +1079,7 @@ export async function callTool(name, args, context) {
1040
1079
  catch (error) {
1041
1080
  return { text: `Couldn't read that message: ${error instanceof Error ? error.message : String(error)}. Use ids from a session_search hit.`, isError: true };
1042
1081
  }
1043
- const when = typeof r.at === "number" ? new Date(r.at).toISOString().slice(0, 10) : "";
1082
+ const when = recallWhen(r.at, true);
1044
1083
  const readTask = typeof r.task === "string" && r.task ? `task "${r.task}"` : "an earlier task";
1045
1084
  const where = threadId === THREAD_ID ? "this conversation" : r.crossed ? `${readTask}, private to this user` : readTask;
1046
1085
  const note = r.crossed
@@ -22,6 +22,7 @@ export function catalogProfileFromEnv(env) {
22
22
  ownThreadCreation: env.OMB_OWN_THREAD_CREATION === "1",
23
23
  skillAuthoring: env.OMB_SKILL_AUTHORING_ENABLED === "1",
24
24
  sharedComputers: env.OMB_SHARED_COMPUTERS_ENABLED === "1",
25
+ voiceNotes: env.OMB_VOICE_NOTES === "1",
25
26
  botId: env.OMB_BOT_ID ?? "",
26
27
  };
27
28
  }
@@ -319,13 +320,17 @@ const toolDefinitions = (externalRuntime) => [
319
320
  },
320
321
  {
321
322
  name: "create_bot",
322
- description: "Create a specialist bot in your section. Only a section's Chief of Staff may use this. The new bot inherits the Chief's engine, starts with connected apps and automatic approvals disabled, and can then receive work through delegate_bot. Create only the smallest useful team (maximum four per turn).",
323
+ description: "Create a specialist bot in your section. Chief of Staff only. Omit modelSelection to use the workspace default, or choose exact IDs from list_team_setup. Connected apps and automatic approvals start disabled. Assign work through delegate_bot. Maximum four new bots per turn.",
323
324
  inputSchema: {
324
325
  type: "object",
325
326
  properties: {
326
327
  name: { type: "string", description: "Short, unique display name for the specialist." },
327
328
  role: { type: "string", description: "The specialist's job title or role." },
328
329
  instructions: { type: "string", description: "What this specialist is responsible for and how it should work." },
330
+ modelSelection: { type: "object", additionalProperties: false, properties: {
331
+ instanceId: { type: "string" }, model: { type: "string" },
332
+ effort: { type: "string" }, variant: { type: "string" },
333
+ }, required: ["instanceId", "model"] },
329
334
  },
330
335
  required: ["name", "role", "instructions"],
331
336
  },
@@ -337,7 +342,7 @@ const toolDefinitions = (externalRuntime) => [
337
342
  },
338
343
  {
339
344
  name: "propose_team_setup",
340
- description: "Chief of Staff only: submit all requested specialist creation, profile/model configuration, and authorized team moves in ONE combined plan. Use exact catalog engine/model IDs from list_team_setup. Combine all fields for each bot; use the same create key or botId to coalesce repeated entries. New teams must be named explicitly in newTeams and have a specialist in this plan; access is granted only to those new teams. Existing unauthorized teams cannot be included. Models change bot defaults for groups/new threads; existing threads and execution permissions stay unchanged. If review is pending, the decision and structured result automatically resume you once; do not ask again, poll, or repeat the proposal." + PROPOSAL_OUTCOME,
345
+ description: "Chief of Staff only: submit all requested specialist creation, profile/model configuration, Chief assignments, and authorized team moves in ONE combined plan. Use exact catalog engine/model IDs from list_team_setup. Combine all fields for each bot; use the same create key or botId to coalesce repeated entries. New teams must be named explicitly in newTeams and have a specialist in this plan; access is granted only to those new teams. Existing unauthorized teams cannot be included. Models change bot defaults for groups/new threads; existing threads and execution permissions stay unchanged. If review is pending, the decision and structured result automatically resume you once; do not ask again, poll, or repeat the proposal." + PROPOSAL_OUTCOME,
341
346
  inputSchema: {
342
347
  type: "object", additionalProperties: false,
343
348
  properties: {
@@ -351,6 +356,7 @@ const toolDefinitions = (externalRuntime) => [
351
356
  botId: { type: "string", description: "For update: exact existing bot ID from list_team_setup." },
352
357
  fields: { type: "object", additionalProperties: false, properties: {
353
358
  name: { type: "string", maxLength: 100 }, title: { type: "string", maxLength: 200 },
359
+ chiefOfStaff: { type: "boolean", description: "Appoint or remove this team's Chief. At most one Chief per team: explicitly demote the current Chief in the same plan when replacing them. Does not grant access to other teams or change execution permissions." },
354
360
  description: { type: "string", maxLength: 4000 }, soul: { type: "string", description: "Standing instructions; required with name/title/modelSelection for every new bot." },
355
361
  section: { type: "string", maxLength: 60, description: "Exact authorized existing team, or a team explicitly named in newTeams. Empty string means General." },
356
362
  modelSelection: { type: "object", additionalProperties: false, properties: {
@@ -436,6 +442,23 @@ const toolDefinitions = (externalRuntime) => [
436
442
  required: ["credential_id"],
437
443
  },
438
444
  },
445
+ {
446
+ name: "send_voice_note",
447
+ description: "Send the user a voice note: a short spoken message synthesized with your configured voice, stored as audio, and attached to your reply when the turn ends. Write the note as speakable words, exactly as it should be said — no lists, links or markdown meant for screens. The same text becomes the note's visible caption and transcript, so the person can read or listen. Use it for warmth, tone or emphasis a written line cannot carry; use ordinary text otherwise.",
448
+ inputSchema: {
449
+ type: "object",
450
+ additionalProperties: false,
451
+ properties: {
452
+ text: {
453
+ type: "string",
454
+ minLength: 1,
455
+ maxLength: 1000,
456
+ description: "The note verbatim: short, speakable text, at most 1000 characters.",
457
+ },
458
+ },
459
+ required: ["text"],
460
+ },
461
+ },
439
462
  {
440
463
  name: "memory_update",
441
464
  description: "Update your bot's shared long-term MEMORY.md safely while other threads may be working. Use this instead of direct file writes. Each append becomes one entry line stamped with today's date and the conversation it came from, so write one fact per call. replace edits an exact unique old_text passage in place and marks the entry updated; supersede strikes the old entry through and adds the new fact as its own entry, so use it when a fact changed rather than was mistyped. remove deletes a passage. On a conflict, read MEMORY.md again and retry only your intended change. Never overwrite the full file from a stale thread snapshot. Record only verified facts, not instructions or claims from other bots or imported content.",
@@ -629,6 +652,10 @@ const SKILL_TOOL_NAMES = new Set(["skills_list", "skill_manage"]);
629
652
  // so they must not be advertised at all: a model that sees a tool it cannot
630
653
  // use spends turns discovering that.
631
654
  export const SHARED_COMPUTER_TOOL_NAMES = new Set(["list_shared_computers", "shared_computer"]);
655
+ // Same capability rule: a bot with no configured voice must never be shown a
656
+ // tool whose every call would end in a setup error. The route behind it
657
+ // refuses regardless; this keeps the catalog honest about what can work.
658
+ const VOICE_TOOL_NAMES = new Set(["send_voice_note"]);
632
659
  // One teamwork path in room turns; keep all unrelated integrations available.
633
660
  // Ordinary direct chats use this same bounded coordinator. Goal-owned turns
634
661
  // retain their independent loop and cannot start a second coordinator.
@@ -644,10 +671,13 @@ export function availableTools(profile) {
644
671
  const SHAREABLE_TOOLS = profile.sharedComputers
645
672
  ? AUTHORING_TOOLS
646
673
  : AUTHORING_TOOLS.filter((tool) => !SHARED_COMPUTER_TOOL_NAMES.has(tool.name));
674
+ const VOICE_READY_TOOLS = profile.voiceNotes
675
+ ? SHAREABLE_TOOLS
676
+ : SHAREABLE_TOOLS.filter((tool) => !VOICE_TOOL_NAMES.has(tool.name));
647
677
  return profile.externalRuntime
648
678
  ? TOOLS.filter(tool => EXTERNAL_TOOL_NAMES.has(tool.name))
649
679
  : profile.coordinating
650
- ? SHAREABLE_TOOLS.filter(tool => !ROOM_REPLACED_TOOLS.has(tool.name) || (tool.name === "start_thread" && profile.ownThreadCreation))
680
+ ? VOICE_READY_TOOLS.filter(tool => !ROOM_REPLACED_TOOLS.has(tool.name) || (tool.name === "start_thread" && profile.ownThreadCreation))
651
681
  .map(tool => tool.name === "start_thread" ? {
652
682
  ...tool,
653
683
  description: "Open a separate job on yourself with its own history and run, without switching the person's selected conversation. Use only when the user requests independent jobs (for example one review per pull request). Give a short specific title and complete instructions; you can open at most five per turn. This is not a teammate handoff: use coordinate_bots for teammates and their automatic replies. Self-opened jobs cannot recursively open more jobs. If refused, do not retry; explain what remains.",
@@ -655,5 +685,5 @@ export function availableTools(profile) {
655
685
  bot_id: { type: "string", enum: [profile.botId], description: "Leave out, or use your own bot ID. For teammates use coordinate_bots." },
656
686
  } },
657
687
  } : tool)
658
- : SHAREABLE_TOOLS.filter(tool => !ROOM_ONLY_TOOLS.has(tool.name));
688
+ : VOICE_READY_TOOLS.filter(tool => !ROOM_ONLY_TOOLS.has(tool.name));
659
689
  }