openmausbot 0.1.93 → 0.1.95

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 (192) hide show
  1. package/dist/assets/{DesktopViewer-CKNR3SEW.js → DesktopViewer-BwWpc4RK.js} +1 -1
  2. package/dist/assets/{abnfDiagram-O67JEVCF-Ckrm1I1X.js → abnfDiagram-O67JEVCF-jsQkIqTn.js} +1 -1
  3. package/dist/assets/{arc-BBdBuCgB.js → arc-7kei4Hsu.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-CDdA3Hjs.js → architectureDiagram-NJMV4G6O-CRWDFDMq.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-AUrsx1YR.js → blockDiagram-BEXU5L5S-CTKW_IKs.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-BEqQ0Unh.js → c4Diagram-YGBWAQC7-BBTW7-6M.js} +1 -1
  7. package/dist/assets/channel-DS2NbT5x.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-CEEuvd4C.js → chunk-2Q5K7J3B-M_Ox-MkD.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-DyANCiHl.js → chunk-3FUC2YCW-D2YiN-Ud.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-Dx_Slpw9.js → chunk-5VM5RSS4-BmVYJ0f8.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-Dl7h7iiD.js → chunk-GTNCS2PH-BtiLVVnZ.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-BLx07SPb.js → chunk-GWA4HPMP-DFYSb6J6.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-BMxDj3n7.js → chunk-JWPE2WC7-mknKBzgV.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-CN66PT4H.js → chunk-NETBCI7D-DgSYGiOu.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-6HwhsDKw.js → chunk-WEXAMYUT-C7ZvuguH.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-gWnLt7SN.js → chunk-XXDRQBXY-CPqxINpA.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-CMS5l2ea.js → classDiagram-v2-NBCMYWYE-Cd6xfJaC.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-CV7sQwIG.js → cose-bilkent-JH36ORCC-BSXY5qRh.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-CSyl-7vl.js → cynefin-OW5HDTMX-D_lhEMj1.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-BCLKL6az.js → cynefinDiagram-VND7K2PF-Da9FrFwk.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-DiaZs6S1.js → dagre-6A5THRUB-T77jMOXe.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-BeTtmt3I.js → diagram-22UHCM2B-Dkj96X5E.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-CsdBG-QQ.js → diagram-3UASUU5V-CMi9zlD_.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-BczvdbR1.js → diagram-ATOU4E4O-CXK6YUfS.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-DHTTTMhi.js → diagram-CDSNMT55-BE7B1u9Q.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-RTjHyd8z.js → diagram-MLGK6HIB-CCMQphKr.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-BDenHEH_.js → diagram-MPIPVDR6-iHJ_-tGH.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-BSiRSbtL.js → ebnfDiagram-ZINNZB2B-DONsCWQ7.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-BCAElss-.js → elk-276RUBZZ-CU_WOaXk.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-C4_1ZPJe.js → erDiagram-OPXOYQCR-BeYwM04G.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-Bcb4Z75S.js → flowDiagram-KWPJA3E3-Bdw9DqEl.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-BiPOzxzb.js → ganttDiagram-FUAMR5RP-C9iwq3qm.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-B1wtF3ZS.js → gitGraphDiagram-X574FWY7-Dx-VWzUg.js} +1 -1
  34. package/dist/assets/{index-BkYwilkG.js → index-KygHRE3E.js} +1 -1
  35. package/dist/assets/index-NF7XCHAy.css +1 -0
  36. package/dist/assets/index-X_5ydXIE.js +610 -0
  37. package/dist/assets/{infoDiagram-VRGFBTTK-A19No71U.js → infoDiagram-VRGFBTTK-BG7uaEd4.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-BQsqJ_cV.js → ishikawaDiagram-OU5B5YK6-D72WAXnM.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-DAO0pVan.js → journeyDiagram-ZHPQQLJL-DKH1gjYT.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-NepmKEe2.js → kanban-definition-PNTS6WVX-3upm9JUT.js} +1 -1
  41. package/dist/assets/{linear-bq1PTDTW.js → linear-CcR4pc0-.js} +1 -1
  42. package/dist/assets/{mermaid.core-s4LTbQle.js → mermaid.core-C6yU5FSS.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-LtdbgHFH.js → mindmap-definition-NLK3R4M7-DOI2xEuv.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-tdbaCxiK.js → pegDiagram-GJSIUBJH-BVWJSgLd.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-DvkbpW8l.js → pieDiagram-5QR66LMP-jtbYezhq.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-Do4KaH-E.js → quadrantDiagram-O4NWA36T-g0u8mtPX.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-BQWULOAs.js → railroadDiagram-XR7U4H2S-k8689Wny.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-w9kei1FU.js → requirementDiagram-PLB6GJNP-DsJ22PDt.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-D2gRy0e8.js → sankeyDiagram-IPEJSGJF-CneWBRgG.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-a2sO5_Eo.js → sequenceDiagram-PO4LG4MO-DWsFCWap.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-CiPJmFEB.js → sizeCapture-INFHLROL-Bvf0YE3S.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-DAkUUlL7.js → stateDiagram-v2-GCMORJYK-Bxb3Iei8.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-DANf2R8Y.js → swimlanes-2SLR337P-DLkk7W4A.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-C6bZtBXk.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-BKpQmzrT.js → timeline-definition-EJHVYXUP-KR9bIHYC.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-BtI7qLN-.js → usecaseDiagram-POWQR4AR-DfsOrj8t.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-gXJXrAG_.js → vennDiagram-UO4OBE2U-DRSTzG3h.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-GU8Po5-n.js → wardleyDiagram-VNRHLVJA-DtEdhPgT.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-BF4J30g6.js → xychartDiagram-PMCCYNJV-B-XhTxjG.js} +1 -1
  60. package/dist/index.html +2 -2
  61. package/dist-server/cloud-home-start.js +3 -1
  62. package/dist-server/companion/src/routes.js +60 -0
  63. package/dist-server/container-mcp.js +94 -77
  64. package/dist-server/drivers/agents-proxy.js +96 -39
  65. package/dist-server/drivers/pi-mcp-extension.ts +245 -299
  66. package/dist-server/enterprise/server/index.js +3 -1
  67. package/dist-server/{browser-proxy.js → harness-mcp-proxy.js} +35 -19
  68. package/dist-server/index.js +18242 -13645
  69. package/dist-server/local-computer.js +3 -1
  70. package/dist-server/mcp-gate.js +147 -19
  71. package/dist-server/mcp-remote-proxy.js +437 -0
  72. package/dist-server/openmausbot.js +9618 -10058
  73. package/dist-server/pair-cli.js +9618 -10058
  74. package/dist-server/permission-proxy.js +65 -1
  75. package/dist-server/proxy-paths.js +3 -1
  76. package/dist-server/server/account-signin.js +0 -2
  77. package/dist-server/server/activity.js +201 -0
  78. package/dist-server/server/auto-approve.js +39 -11
  79. package/dist-server/server/automatic-recovery.js +1 -1
  80. package/dist-server/server/boat-create-idempotency.js +0 -7
  81. package/dist-server/server/boat-delete-journal.js +0 -4
  82. package/dist-server/server/bot-package.js +1 -19
  83. package/dist-server/server/browser-live.js +14 -5
  84. package/dist-server/server/browser-runtime.js +7 -2
  85. package/dist-server/server/channel-queue.js +4 -1
  86. package/dist-server/server/chief-of-staff.js +2 -3
  87. package/dist-server/server/cli.js +32 -12
  88. package/dist-server/server/cloud-backend.js +0 -4
  89. package/dist-server/server/cloud-computer-tools.js +110 -0
  90. package/dist-server/server/cloud-home.js +0 -1
  91. package/dist-server/server/cloud-lending.js +0 -3
  92. package/dist-server/server/cloud-move-http.js +9 -3
  93. package/dist-server/server/cloud-move.js +18 -0
  94. package/dist-server/server/composio.js +31 -2
  95. package/dist-server/server/computer-control.js +0 -15
  96. package/dist-server/server/computer-observation.js +0 -74
  97. package/dist-server/server/computer-wait.js +33 -13
  98. package/dist-server/server/config.js +73 -11
  99. package/dist-server/server/container-computer.js +7 -19
  100. package/dist-server/server/contracts.js +9 -0
  101. package/dist-server/server/control-client.js +4 -10
  102. package/dist-server/server/decision-log.js +9 -6
  103. package/dist-server/server/delegations.js +47 -22
  104. package/dist-server/server/drivers/acp/core.js +64 -13
  105. package/dist-server/server/drivers/acp/grok.js +208 -3
  106. package/dist-server/server/drivers/agents-call.js +70 -29
  107. package/dist-server/server/drivers/agents-catalog.js +34 -12
  108. package/dist-server/server/drivers/agents-proxy.js +8 -30
  109. package/dist-server/server/drivers/antigravity.js +1 -4
  110. package/dist-server/server/drivers/boatagent.js +25 -4
  111. package/dist-server/server/drivers/cerebras.js +3 -3
  112. package/dist-server/server/drivers/chat-mcp-tools.js +29 -19
  113. package/dist-server/server/drivers/claude.js +33 -19
  114. package/dist-server/server/drivers/codex.js +84 -10
  115. package/dist-server/server/drivers/openai-chat.js +14 -9
  116. package/dist-server/server/drivers/pi.js +48 -4
  117. package/dist-server/server/drivers/retry.js +8 -4
  118. package/dist-server/server/engine-install.js +1 -5
  119. package/dist-server/server/environment.js +3 -0
  120. package/dist-server/server/guarded-requests.js +2 -1
  121. package/dist-server/server/harness/registry.js +0 -1
  122. package/dist-server/server/{browser-proxy.js → harness-mcp-proxy.js} +40 -20
  123. package/dist-server/server/index.js +1929 -507
  124. package/dist-server/server/lending-memory.js +12 -8
  125. package/dist-server/server/live-call-controller.js +868 -0
  126. package/dist-server/server/live-call.js +216 -0
  127. package/dist-server/server/mcp-gate-config.js +43 -12
  128. package/dist-server/server/mcp-gate.js +127 -25
  129. package/dist-server/server/mcp-http.js +46 -8
  130. package/dist-server/server/mcp-remote-proxy.js +108 -0
  131. package/dist-server/server/memory-entries.js +38 -8
  132. package/dist-server/server/memory-organize.js +1 -8
  133. package/dist-server/server/memory-topics.js +4 -4
  134. package/dist-server/server/memory-upkeep.js +13 -15
  135. package/dist-server/server/message-file.js +5 -9
  136. package/dist-server/server/new-bot-defaults.js +3 -0
  137. package/dist-server/server/outbound-counts.js +79 -0
  138. package/dist-server/server/outbound-requests.js +60 -0
  139. package/dist-server/server/package-import.js +1 -1
  140. package/dist-server/server/peer-delivery.js +1 -0
  141. package/dist-server/server/peer-provenance.js +4 -2
  142. package/dist-server/server/permission-proxy.js +15 -1
  143. package/dist-server/server/plan-usage.js +797 -0
  144. package/dist-server/server/proxy-paths.js +3 -1
  145. package/dist-server/server/redact.js +2 -0
  146. package/dist-server/server/request-auth.js +3 -2
  147. package/dist-server/server/room-handoffs.js +5 -13
  148. package/dist-server/server/routes/live.js +98 -0
  149. package/dist-server/server/routine-requests.js +1 -1
  150. package/dist-server/server/routines.js +5 -1
  151. package/dist-server/server/setup-mode.js +0 -2
  152. package/dist-server/server/skill-learn.js +0 -1
  153. package/dist-server/server/skill-library.js +195 -3
  154. package/dist-server/server/skills-library-migration.js +235 -0
  155. package/dist-server/server/skills.js +66 -3
  156. package/dist-server/server/steer-queue.js +13 -1
  157. package/dist-server/server/store.js +41 -5
  158. package/dist-server/server/surface.js +74 -23
  159. package/dist-server/server/system-prompt.js +4 -4
  160. package/dist-server/server/team-backup.js +2 -1
  161. package/dist-server/server/team-memory.js +209 -0
  162. package/dist-server/server/thread-events.js +3 -3
  163. package/dist-server/server/tunnel.js +20 -1
  164. package/dist-server/server/turn-log.js +10 -0
  165. package/dist-server/server/turn-resources.js +124 -0
  166. package/dist-server/server/vps-computer.js +4 -14
  167. package/dist-server/server/workspace.js +85 -47
  168. package/dist-server/shared/activity.js +3 -0
  169. package/dist-server/shared/ask-question.js +13 -0
  170. package/dist-server/shared/bot-avatar.js +8 -0
  171. package/dist-server/shared/call-consent.js +28 -0
  172. package/dist-server/shared/connector-scopes.js +78 -0
  173. package/dist-server/shared/live-approval.js +124 -0
  174. package/dist-server/shared/live-call.js +176 -0
  175. package/dist-server/shared/outbound.js +128 -0
  176. package/dist-server/shared/skill-md.js +10 -0
  177. package/dist-server/shared/team-backup.js +3 -0
  178. package/dist-server/shared/tool-scope-support.js +25 -0
  179. package/dist-server/shared/tool-scope.js +136 -0
  180. package/dist-server/shared/tool-surface.js +3 -2
  181. package/dist-server/src/lib/thread-refs.js +406 -0
  182. package/dist-server/vps-container-mcp.js +94 -77
  183. package/dist-server/workspace-backup.worker.js +89 -100
  184. package/enterprise/server/index.js +3 -1
  185. package/package.json +1 -1
  186. package/dist/assets/channel-BU94VpxE.js +0 -1
  187. package/dist/assets/index-Biji-bfL.css +0 -1
  188. package/dist/assets/index-DZlhoES0.js +0 -597
  189. package/dist/assets/swimlanesDiagram-TC7HE7FX-CdHxG8aM.js +0 -8
  190. package/dist-server/server/drivers/chat-boat-tools.js +0 -90
  191. package/dist-server/server/drivers/grokagent.js +0 -4
  192. package/dist-server/server/openmaus-status-capsule.js +0 -329
@@ -17,6 +17,24 @@ const PROTOCOL_VERSION = "2025-06-18";
17
17
  function isRecord(value) {
18
18
  return value !== null && typeof value === "object" && !Array.isArray(value);
19
19
  }
20
+ /** Validate private proxy configuration without including secrets in errors. */
21
+ export function remoteMcpSpec(value) {
22
+ if (!isRecord(value) || (value.type !== "http" && value.type !== "sse") || typeof value.url !== "string")
23
+ return;
24
+ try {
25
+ const url = new URL(value.url);
26
+ if ((url.protocol !== "http:" && url.protocol !== "https:") || url.username || url.password)
27
+ return;
28
+ const headers = value.headers ?? {};
29
+ if (!isRecord(headers) || !Object.values(headers).every((header) => typeof header === "string"))
30
+ return;
31
+ new Headers(headers);
32
+ return { type: value.type, url: value.url, headers: headers };
33
+ }
34
+ catch {
35
+ return;
36
+ }
37
+ }
20
38
  function unwrap(message) {
21
39
  if (message.error !== undefined) {
22
40
  const detail = isRecord(message.error) && typeof message.error.message === "string" ? message.error.message : "request failed";
@@ -55,7 +73,7 @@ function parseSseBlock(block) {
55
73
  }
56
74
  /** Read an event stream until the handler says stop, the server ends it,
57
75
  * or the signal fires. Bounded, so a chatty server cannot fill memory. */
58
- async function readSse(response, signal, onEvent) {
76
+ async function readSse(response, signal, onEvent, maxBytes = MAX_BODY_BYTES) {
59
77
  const reader = response.body?.getReader();
60
78
  if (!reader)
61
79
  throw new McpHttpError("protocol", "empty event stream");
@@ -70,7 +88,7 @@ async function readSse(response, signal, onEvent) {
70
88
  if (done)
71
89
  return;
72
90
  bytes += value.byteLength;
73
- if (bytes > MAX_BODY_BYTES)
91
+ if (bytes > maxBytes)
74
92
  throw new McpHttpError("protocol", "event stream too large");
75
93
  buffer += decoder.decode(value, { stream: true }).replace(/\r\n?/g, "\n");
76
94
  let separator;
@@ -89,7 +107,7 @@ async function readSse(response, signal, onEvent) {
89
107
  signal.removeEventListener("abort", onAbort);
90
108
  }
91
109
  }
92
- async function readBounded(response) {
110
+ async function readBounded(response, maxBytes = MAX_BODY_BYTES) {
93
111
  const reader = response.body?.getReader();
94
112
  if (!reader)
95
113
  return "";
@@ -101,7 +119,7 @@ async function readBounded(response) {
101
119
  if (done)
102
120
  return text + decoder.decode();
103
121
  bytes += value.byteLength;
104
- if (bytes > MAX_BODY_BYTES) {
122
+ if (bytes > maxBytes) {
105
123
  await reader.cancel().catch(() => { });
106
124
  throw new McpHttpError("protocol", "response too large");
107
125
  }
@@ -114,6 +132,8 @@ function drain(response) {
114
132
  export class RemoteMcpClient {
115
133
  target;
116
134
  fetchImpl;
135
+ maxBytes;
136
+ onNotification;
117
137
  sessionId = null;
118
138
  protocolVersion = null;
119
139
  nextId = 1;
@@ -125,6 +145,8 @@ export class RemoteMcpClient {
125
145
  constructor(target, options = {}) {
126
146
  this.target = target;
127
147
  this.fetchImpl = options.fetch ?? fetch;
148
+ this.maxBytes = options.maxBytes ?? MAX_BODY_BYTES;
149
+ this.onNotification = options.onNotification;
128
150
  }
129
151
  /** The MCP handshake: initialize, then the initialized notification. */
130
152
  async initialize(clientName, signal) {
@@ -141,9 +163,16 @@ export class RemoteMcpClient {
141
163
  request(method, params, signal) {
142
164
  if (this.closed)
143
165
  return Promise.reject(new McpHttpError("protocol", "client closed"));
166
+ if (signal.aborted)
167
+ return Promise.reject(new McpHttpError("network", "aborted"));
144
168
  const id = this.nextId++;
145
169
  const frame = { jsonrpc: "2.0", id, method, ...(params === undefined ? {} : { params }) };
146
- return this.target.type === "sse" ? this.sseRequest(id, frame, signal) : this.streamableRequest(id, frame, signal);
170
+ const onAbort = () => {
171
+ void this.notify("notifications/cancelled", { requestId: id, reason: "Request cancelled" }, AbortSignal.timeout(2_000)).catch(() => { });
172
+ };
173
+ signal.addEventListener("abort", onAbort, { once: true });
174
+ const reply = this.target.type === "sse" ? this.sseRequest(id, frame, signal) : this.streamableRequest(id, frame, signal);
175
+ return reply.finally(() => signal.removeEventListener("abort", onAbort));
147
176
  }
148
177
  async notify(method, params, signal) {
149
178
  if (this.closed)
@@ -216,7 +245,8 @@ export class RemoteMcpClient {
216
245
  const response = await this.post(this.target.url, frame, signal);
217
246
  const type = (response.headers.get("content-type") ?? "").toLowerCase();
218
247
  if (type.startsWith("application/json")) {
219
- const parsed = parseMessage(await readBounded(response));
248
+ const parsed = parseMessage(await readBounded(response, this.maxBytes));
249
+ this.deliverNotifications(parsed);
220
250
  const message = (Array.isArray(parsed) ? parsed : parsed ? [parsed] : []).find((entry) => entry.id === id);
221
251
  if (!message)
222
252
  throw new McpHttpError("protocol", "response did not answer the request");
@@ -228,12 +258,13 @@ export class RemoteMcpClient {
228
258
  if (event.event !== "message")
229
259
  return undefined;
230
260
  const parsed = parseMessage(event.data);
261
+ this.deliverNotifications(parsed);
231
262
  const message = (Array.isArray(parsed) ? parsed : parsed ? [parsed] : []).find((entry) => entry.id === id);
232
263
  if (!message)
233
264
  return undefined;
234
265
  found = message;
235
266
  return "stop";
236
- });
267
+ }, this.maxBytes);
237
268
  if (signal.aborted)
238
269
  throw new McpHttpError("network", "aborted");
239
270
  if (!found)
@@ -286,6 +317,7 @@ export class RemoteMcpClient {
286
317
  if (event.event !== "message")
287
318
  return undefined;
288
319
  const parsed = parseMessage(event.data);
320
+ this.deliverNotifications(parsed);
289
321
  for (const message of Array.isArray(parsed) ? parsed : parsed ? [parsed] : []) {
290
322
  const entry = typeof message.id === "number" ? this.pending.get(message.id) : undefined;
291
323
  if (!entry)
@@ -299,7 +331,7 @@ export class RemoteMcpClient {
299
331
  }
300
332
  }
301
333
  return undefined;
302
- });
334
+ }, this.maxBytes);
303
335
  fail(new McpHttpError("network", "event stream ended"));
304
336
  })
305
337
  .catch((error) => {
@@ -337,4 +369,10 @@ export class RemoteMcpClient {
337
369
  });
338
370
  });
339
371
  }
372
+ deliverNotifications(parsed) {
373
+ for (const message of Array.isArray(parsed) ? parsed : parsed ? [parsed] : []) {
374
+ if (typeof message.method === "string" && message.id === undefined)
375
+ this.onNotification?.(message);
376
+ }
377
+ }
340
378
  }
@@ -0,0 +1,108 @@
1
+ // A stdio facade for selected HTTP/SSE MCP servers. Configuration stays in
2
+ // private environment data; stdout contains only protocol frames.
3
+ import { createInterface } from "node:readline";
4
+ import { RemoteMcpClient, remoteMcpSpec } from "./mcp-http.js";
5
+ function isRecord(value) {
6
+ return !!value && typeof value === "object" && !Array.isArray(value);
7
+ }
8
+ function requestKey(id) {
9
+ return typeof id === "string" || (typeof id === "number" && Number.isFinite(id)) ? `${typeof id}:${id}` : undefined;
10
+ }
11
+ function send(frame) { process.stdout.write(`${JSON.stringify(frame)}\n`); }
12
+ function error(id, code, message) {
13
+ send({ jsonrpc: "2.0", id: requestKey(id) ? id : null, error: { code, message } });
14
+ }
15
+ let spec;
16
+ try {
17
+ spec = remoteMcpSpec(JSON.parse(process.env.OMB_REMOTE_MCP_SERVER ?? ""));
18
+ }
19
+ catch { /* invalid private config */ }
20
+ delete process.env.OMB_REMOTE_MCP_SERVER;
21
+ if (!spec) {
22
+ process.stderr.write("mcp-remote-proxy: invalid remote MCP configuration\n");
23
+ process.exit(1);
24
+ }
25
+ const client = new RemoteMcpClient(spec, { maxBytes: 32 * 1024 * 1024, onNotification: send });
26
+ const pending = new Map();
27
+ const tasks = new Set();
28
+ let initialized = false;
29
+ let closing = false;
30
+ async function handle(message) {
31
+ const id = requestKey(message.id);
32
+ if (typeof message.method !== "string" || message.jsonrpc !== "2.0") {
33
+ error(message.id, -32600, "Invalid JSON-RPC request");
34
+ return;
35
+ }
36
+ if (message.method === "notifications/cancelled") {
37
+ const key = isRecord(message.params) ? requestKey(message.params.requestId) : undefined;
38
+ if (key)
39
+ pending.get(key)?.abort();
40
+ return;
41
+ }
42
+ if (!id) {
43
+ // initialize() has already sent this once with its negotiated version.
44
+ if (message.method === "notifications/initialized" && initialized)
45
+ return;
46
+ try {
47
+ await client.notify(message.method, message.params, AbortSignal.timeout(30_000));
48
+ }
49
+ catch { /* notifications have no reply and must not print remote details */ }
50
+ return;
51
+ }
52
+ if (pending.has(id)) {
53
+ error(message.id, -32600, "Duplicate request ID");
54
+ return;
55
+ }
56
+ const controller = new AbortController();
57
+ pending.set(id, controller);
58
+ const signal = AbortSignal.any([controller.signal, AbortSignal.timeout(message.method === "tools/call" ? 120_000 : 30_000)]);
59
+ try {
60
+ const result = message.method === "initialize"
61
+ ? await client.initialize("OpenMausBot tool proxy", signal)
62
+ : await client.request(message.method, message.params, signal);
63
+ if (message.method === "initialize")
64
+ initialized = true;
65
+ send({ jsonrpc: "2.0", id: message.id, result });
66
+ }
67
+ catch {
68
+ // Remote errors may contain URLs or header values. Do not relay their text.
69
+ error(message.id, signal.aborted ? -32800 : -32603, signal.aborted ? "MCP request cancelled" : "Remote MCP request failed");
70
+ }
71
+ finally {
72
+ pending.delete(id);
73
+ }
74
+ }
75
+ const input = createInterface({ input: process.stdin });
76
+ input.on("line", (line) => {
77
+ if (closing || !line.trim())
78
+ return;
79
+ let value;
80
+ try {
81
+ value = JSON.parse(line);
82
+ }
83
+ catch {
84
+ error(null, -32700, "Invalid JSON-RPC frame");
85
+ return;
86
+ }
87
+ if (!isRecord(value)) {
88
+ error(null, -32600, "Invalid JSON-RPC frame");
89
+ return;
90
+ }
91
+ const task = handle(value);
92
+ tasks.add(task);
93
+ void task.finally(() => tasks.delete(task));
94
+ });
95
+ async function shutdown() {
96
+ if (closing)
97
+ return;
98
+ closing = true;
99
+ input.close();
100
+ for (const controller of pending.values())
101
+ controller.abort();
102
+ await Promise.allSettled(tasks);
103
+ await client.close();
104
+ process.exit(0);
105
+ }
106
+ input.on("close", () => { void shutdown(); });
107
+ process.on("SIGTERM", () => { void shutdown(); });
108
+ process.on("SIGINT", () => { void shutdown(); });
@@ -2,12 +2,21 @@
2
2
  // `- YYYY-MM-DD · from <source> · text[ · until YYYY-MM-DD]`, with a
3
3
  // struck-through body for a superseded fact. Pure helpers shared by the
4
4
  // prompt loader (which hides expired lines), background capture (which
5
- // must not add a fact the notebook already holds) and the nightly tidy-up.
5
+ // must not add a fact the notebook already holds), the nightly tidy-up and
6
+ // the rule that keeps MEMORY.md within what loads (server/workspace.ts).
6
7
  export const UNTIL_DATE = /^\d{4}-\d{2}-\d{2}$/;
7
8
  const DATED = /^(- (\d{4}-\d{2}-\d{2}) · (?:from [^·\n]* · )?)(.*)$/;
8
9
  const UNTIL_MARK = / · until (\d{4}-\d{2}-\d{2})(?= · |$)/;
9
- const TRAILING_MARKS = /(?: · (?:updated|confirmed|until|expired|superseded) \d{4}-\d{2}-\d{2})+$/;
10
+ const TRAILING_MARKS = /(?: · (?:updated|confirmed|until|expired|superseded|moved) \d{4}-\d{2}-\d{2})+$/;
10
11
  const BULLET = /^\s*(?:[-*•]|\d+[.)])\s+/;
12
+ /** A safety or health fact must load into every turn: neither the organize
13
+ * step nor the size rule moves one out of MEMORY.md, whatever else happens.
14
+ * Seen live — a model moved "is vegetarian" into a food topic despite being
15
+ * told diet is core. */
16
+ const ALWAYS_CORE = /\b(?:allerg\w*|anaphyla\w*|intoleran\w*|vegetarian|vegan|halal|kosher|gluten|lactose|diet\w*|diabet\w*|asthma\w*|epilep\w*|pregnan\w*|medicat\w*|medicine|medical|disabilit\w*|wheelchair|blind|deaf|health)\b/i;
17
+ export function alwaysCore(body) {
18
+ return ALWAYS_CORE.test(body);
19
+ }
11
20
  /** Whether `value` is a real calendar date in YYYY-MM-DD form. */
12
21
  export function isMemoryDate(value) {
13
22
  if (typeof value !== "string" || !UNTIL_DATE.test(value))
@@ -18,20 +27,41 @@ export function isMemoryDate(value) {
18
27
  export function untilMark(until) {
19
28
  return until ? ` · until ${until}` : "";
20
29
  }
30
+ /** A line with an odd number of fences opens or closes a code block. */
31
+ const togglesFence = (line) => (line.match(/```/g)?.length ?? 0) % 2 === 1;
21
32
  /** Every dated entry in a MEMORY.md text, in file order. Hand-written lines
22
- * without a date are not entries and are never touched by upkeep. */
33
+ * without a date are not entries and are never touched by upkeep. An entry
34
+ * also owns the lines under it, up to the next dated line: indented ones
35
+ * (Markdown's list rule; how an entry with a code block is written) and a
36
+ * code block it opens, through its closing fence (how such entries were
37
+ * written before). A fence that never closes claims nothing. */
23
38
  export function parseMemoryEntries(text) {
24
39
  const out = [];
25
- text.split("\n").forEach((raw, line) => {
40
+ const lines = text.split("\n");
41
+ for (let line = 0; line < lines.length;) {
42
+ const raw = lines[line];
26
43
  const m = DATED.exec(raw);
27
- if (!m)
28
- return;
44
+ if (!m) {
45
+ line += 1;
46
+ continue;
47
+ }
48
+ let open = togglesFence(raw);
49
+ let end = line + 1;
50
+ for (let next = line + 1; next < lines.length && !DATED.test(lines[next]); next += 1) {
51
+ if (!open && !/^[ \t]/.test(lines[next]) && !lines[next].startsWith("```"))
52
+ break;
53
+ if (togglesFence(lines[next]))
54
+ open = !open;
55
+ if (!open)
56
+ end = next + 1;
57
+ }
29
58
  const rest = m[3];
30
59
  const struck = rest.startsWith("~~");
31
60
  const until = UNTIL_MARK.exec(rest)?.[1] ?? null;
32
61
  const body = rest.replace(TRAILING_MARKS, "").replace(/^~~/, "").replace(/~~$/, "").trim();
33
- out.push({ line, raw, prefix: m[1], body, date: m[2], until, struck });
34
- });
62
+ out.push({ line, end, raw, prefix: m[1], body, date: m[2], until, struck });
63
+ line = end;
64
+ }
35
65
  return out;
36
66
  }
37
67
  /** A live entry whose `until` day has passed. The until day itself still counts. */
@@ -8,19 +8,12 @@
8
8
  // aliases recall matches. An entry judged core is remembered and not asked
9
9
  // about again. Pure here: the caller reads, calls the model, writes and
10
10
  // journals.
11
- import { factIdentity, isExpired, parseMemoryEntries } from "./memory-entries.js";
11
+ import { alwaysCore, factIdentity, isExpired, parseMemoryEntries } from "./memory-entries.js";
12
12
  import { topicFileName } from "./memory-capture.js";
13
13
  export const ORGANIZE_MARKER = "You are the ORGANIZE step of a memory system";
14
14
  /** One pass moves at most this many entries. */
15
15
  export const MAX_MOVES = 20;
16
16
  const PROMPT_ENTRIES = 60;
17
- /** Lines the model may not move, whatever it answers: a safety or health
18
- * fact must load into every turn. Seen live — a model moved "is vegetarian"
19
- * into a food topic despite being told diet is core. */
20
- const ALWAYS_CORE = /\b(?:allerg\w*|anaphyla\w*|intoleran\w*|vegetarian|vegan|halal|kosher|gluten|lactose|diet\w*|diabet\w*|asthma\w*|epilep\w*|pregnan\w*|medicat\w*|medicine|medical|disabilit\w*|wheelchair|blind|deaf|health)\b/i;
21
- export function alwaysCore(body) {
22
- return ALWAYS_CORE.test(body);
23
- }
24
17
  /** Live, unexpired entries not already judged core: what the call is asked about. */
25
18
  export function organizeCandidates(text, today, core) {
26
19
  return parseMemoryEntries(text)
@@ -90,8 +90,8 @@ export function readTopicHead(path) {
90
90
  closeSync(fd);
91
91
  }
92
92
  }
93
- /** One line per topic, capped by count and length. The archive the tidy-up
94
- * keeps is listed last and labelled, so it is not mistaken for current notes. */
93
+ /** One line per topic, capped by count and length. The archive is listed
94
+ * last and labelled, so it is not mistaken for current notes. */
95
95
  export function renderTopicIndex(topics) {
96
96
  if (!topics.length)
97
97
  return "";
@@ -104,8 +104,8 @@ export function renderTopicIndex(topics) {
104
104
  omitted += 1;
105
105
  continue;
106
106
  }
107
- const about = name === "archive.md" && !header.title && !header.description
108
- ? "expired and replaced notes, kept for the record"
107
+ const about = name === "archive.md"
108
+ ? "older notes moved out of MEMORY.md, kept for the record"
109
109
  : [header.title, header.description].filter(Boolean).join(" — ");
110
110
  const also = header.aliases.length ? ` (also: ${header.aliases.join(", ")})` : "";
111
111
  const line = `- memory/${name}${about ? ` — ${about}` : ""}${also}`.replace(/\s+/g, " ");
@@ -25,13 +25,13 @@ import { mergeTopicText } from "./memory-topics.js";
25
25
  import { applyMoves, MAX_MOVES, organizeCandidates, organizePrompt, parseMoves } from "./memory-organize.js";
26
26
  import { recordMemoryChange } from "./memory-journal.js";
27
27
  import { applyTidy, contradictionCandidates, contradictionPrompt, parseContradictions, planChanges, planTidy } from "./memory-tidy.js";
28
- import { ensureWorkspace, listMemoryTopics, memoryDate, memoryEntry, memoryTopicIndex, readMemoryText, updateMemory, workspaceDir, writeMemoryFile, writeMemoryTopic } from "./workspace.js";
28
+ import { appendMemoryArchive, ARCHIVE_TOPIC, ensureWorkspace, listMemoryTopics, memoryDate, memoryEntry, memoryTopicIndex, readMemoryText, updateMemory, workspaceDir, writeMemoryFile, writeMemoryTopic } from "./workspace.js";
29
29
  export const CAPTURE_MAX_TURNS = 6;
30
30
  export const MODEL_TIMEOUT_MS = 60_000;
31
31
  export const TIDY_CHECK_MS = 10 * 60_000;
32
32
  /** Below this many live entries there is nothing a contradiction pass may change. */
33
33
  export const MIN_ENTRIES_FOR_CONTRADICTIONS = 5;
34
- export const ARCHIVE_TOPIC = "archive.md";
34
+ const ARCHIVE_PATH = `memory/${ARCHIVE_TOPIC}`;
35
35
  /** A topic file past this size gains nothing more from capture. */
36
36
  export const TOPIC_MAX_BYTES = 64 * 1024;
37
37
  /** On unless switched off: every bot keeps its memory in shape by default. */
@@ -139,25 +139,25 @@ export function createMemoryUpkeep(deps) {
139
139
  // From here to the journal rows there is no await, so no write interleaves.
140
140
  if (!upkeepEnabled(deps.bot(bot.id)))
141
141
  return off;
142
- const { parsed, added, topics, full } = writing(bot.id, () => {
142
+ const { parsed, added, topics } = writing(bot.id, () => {
143
143
  const before = readRaw(bot.id, "MEMORY.md");
144
+ const archiveBefore = readRaw(bot.id, ARCHIVE_PATH);
144
145
  const parsed = parseCandidates(answer, today);
145
146
  const fresh = newCandidates(parsed, before ?? "");
146
147
  const source = `${deps.sourceLabel(bot.id, batch.threadId)} (noticed)`;
147
148
  let added = 0;
148
- let full = false;
149
149
  const byTopic = new Map();
150
150
  for (const candidate of fresh) {
151
151
  if (candidate.topic) {
152
152
  byTopic.set(candidate.topic, [...(byTopic.get(candidate.topic) ?? []), candidate]);
153
153
  continue;
154
154
  }
155
- const result = updateMemory(bot.id, { action: "append", text: candidate.text, ...(candidate.until ? { until: candidate.until } : {}) }, { source, now: now() });
156
- if (result.ok)
155
+ if (updateMemory(bot.id, { action: "append", text: candidate.text, ...(candidate.until ? { until: candidate.until } : {}) }, { source, now: now() }).ok)
157
156
  added += 1;
158
- else if (result.code === "over-budget")
159
- full = true;
160
157
  }
158
+ // Older entries the appends moved out are journaled first, as the
159
+ // tidy-up does, so undoing the newest row (MEMORY.md) brings them back.
160
+ recordMemoryChange(bot.id, { path: ARCHIVE_PATH, actor: "upkeep", via: "capture", threadId: batch.threadId, before: archiveBefore, after: readRaw(bot.id, ARCHIVE_PATH) });
161
161
  if (added)
162
162
  recordMemoryChange(bot.id, { path: "MEMORY.md", actor: "upkeep", via: "capture", threadId: batch.threadId, before, after: readRaw(bot.id, "MEMORY.md") });
163
163
  // Detail goes to a topic file the bot keeps for that subject, created
@@ -188,7 +188,7 @@ export function createMemoryUpkeep(deps) {
188
188
  existing.set(name.toLowerCase(), name);
189
189
  topics += lines.length;
190
190
  }
191
- return { parsed, added, topics, full };
191
+ return { parsed, added, topics };
192
192
  });
193
193
  // Only the owner's own words reach About me: on a shared workspace
194
194
  // another person's facts are not the owner's profile. Every parsed fact
@@ -199,7 +199,7 @@ export function createMemoryUpkeep(deps) {
199
199
  // whoever wrote MEMORY.md — the bot, the person or this capture — detail
200
200
  // that is not core moves to its topic now, not only at night
201
201
  const organized = await organize(bot.id);
202
- const report = { at, added, topics, aboutMe, organized, ...(full ? { note: "MEMORY.md is full; the tidy-up or a person needs to make room." } : {}) };
202
+ const report = { at, added, topics, aboutMe, organized };
203
203
  record(bot.id, { lastCapture: report });
204
204
  if (added || topics || aboutMe)
205
205
  log(`memory upkeep: noticed ${added} fact(s) for MEMORY.md, ${topics} for topic files and ${aboutMe} for About me — ${bot.name} (${bot.id}) from ${batch.threadId}`);
@@ -284,11 +284,9 @@ export function createMemoryUpkeep(deps) {
284
284
  });
285
285
  }
286
286
  function appendArchive(botId, archived) {
287
- const archivePath = `memory/${ARCHIVE_TOPIC}`;
288
- const archiveBefore = readRaw(botId, archivePath);
289
- const head = archiveBefore ?? "---\ntitle: Archive\ndescription: expired notes moved out by the tidy-up\n---\n";
290
- writeMemoryTopic(botId, ARCHIVE_TOPIC, `${head}${head.endsWith("\n") ? "" : "\n"}${archived.join("\n")}\n`);
291
- recordMemoryChange(botId, { path: archivePath, actor: "upkeep", via: "tidy", before: archiveBefore, after: readRaw(botId, archivePath) });
287
+ const archiveBefore = readRaw(botId, ARCHIVE_PATH);
288
+ appendMemoryArchive(botId, archived);
289
+ recordMemoryChange(botId, { path: ARCHIVE_PATH, actor: "upkeep", via: "tidy", before: archiveBefore, after: readRaw(botId, ARCHIVE_PATH) });
292
290
  }
293
291
  async function runTidy(botId) {
294
292
  const bot = deps.bot(botId);
@@ -259,16 +259,12 @@ function safeDisplayName(value) {
259
259
  return boundedDisplayName(clean || "download");
260
260
  }
261
261
  /**
262
- * Confirm that a stored user message carries the requested attachment as an
263
- * exact standalone transport tag. Plain prose, inline examples, fenced code,
264
- * and near-matching paths do not grant access. The HTTP route pairs this
265
- * message capability with ATTACHMENTS_DIR containment, so this can never
266
- * become an arbitrary host-file reader.
262
+ * The user-facing name of the requested attachment when a stored user
263
+ * message carries it as an exact standalone transport tag, else null. Plain
264
+ * prose, inline examples, fenced code, and near-matching paths do not grant
265
+ * access. The HTTP route pairs this message capability with ATTACHMENTS_DIR
266
+ * containment, so this can never become an arbitrary host-file reader.
267
267
  */
268
- export function messageReferencesAttachment(text, requested) {
269
- return messageAttachmentName(text, requested) !== null;
270
- }
271
- /** Return the user-facing name carried by an authorised attachment tag. */
272
268
  export function messageAttachmentName(text, requested) {
273
269
  let wanted;
274
270
  try {
@@ -3,6 +3,7 @@ import { profilePatchSchema, fitsOnOneLine } from "./bot-profile.js";
3
3
  import { EFFORT_LEVELS } from "../shared/wire.js";
4
4
  import { isModelVariant } from "./contracts.js";
5
5
  import { normalizeCronSchedule } from "../shared/routine-schedule.js";
6
+ import { parseToolScope } from "../shared/tool-scope.js";
6
7
  const strings = (max, length) => z.array(z.string().max(length)).max(max);
7
8
  const clock = z.string().regex(/^(?:[01]\d|2[0-3]):[0-5]\d$/);
8
9
  const weekdays = z.array(z.number().int().min(0).max(6)).min(1).max(7);
@@ -35,6 +36,8 @@ export const botDefaultsProfileSchema = profilePatchSchema.extend({
35
36
  browser: z.boolean().optional(),
36
37
  browserProfile: z.string().max(100).nullable().optional(),
37
38
  mcpServers: strings(100, 100).nullable().optional(),
39
+ toolScope: z.custom((value) => value !== undefined && parseToolScope(value).ok, "Invalid tool selection")
40
+ .transform((value) => { const parsed = parseToolScope(value); return parsed.ok ? parsed.scope : value; }).nullable().optional(),
38
41
  parkDirectMessages: z.boolean().optional(),
39
42
  }).strict();
40
43
  const scheduleSchema = z.discriminatedUnion("type", [
@@ -0,0 +1,79 @@
1
+ // How many outbound calls each bot has made today. The daily cap in a
2
+ // bot's outbound policy is only as good as this number, so it lives on
3
+ // disk (0600, like every other record that names what a bot did) and is
4
+ // keyed by the local day, so it rolls over at midnight and not at the
5
+ // next restart.
6
+ //
7
+ // One day is kept per bot: the count exists to enforce today's cap, and
8
+ // the activity log already keeps the history.
9
+ import { readFileSync } from "node:fs";
10
+ import { writeFileAtomic } from "./atomic.js";
11
+ const dayKey = (date) => `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
12
+ export class OutboundCounts {
13
+ file;
14
+ stored;
15
+ readable = true;
16
+ constructor(file) {
17
+ this.file = file;
18
+ this.stored = this.load();
19
+ }
20
+ load() {
21
+ try {
22
+ const value = JSON.parse(readFileSync(this.file, "utf8"));
23
+ if (!value || typeof value !== "object" || Array.isArray(value))
24
+ throw new Error("invalid outbound counts");
25
+ const stored = {};
26
+ for (const [botId, entry] of Object.entries(value)) {
27
+ if (!entry || typeof entry !== "object")
28
+ throw new Error("invalid outbound count");
29
+ const { day, count } = entry;
30
+ if (typeof day === "string" && typeof count === "number" && Number.isInteger(count) && count >= 0) {
31
+ stored[botId] = { day, count };
32
+ }
33
+ else
34
+ throw new Error("invalid outbound count");
35
+ }
36
+ return stored;
37
+ }
38
+ catch (error) {
39
+ if (error.code !== "ENOENT")
40
+ this.readable = false;
41
+ return {};
42
+ }
43
+ }
44
+ save() {
45
+ writeFileAtomic(this.file, JSON.stringify(this.stored, null, 2), { mode: 0o600 });
46
+ }
47
+ /** How many outbound calls this bot has made on the day `now` falls in. */
48
+ today(botId, now = new Date()) {
49
+ if (!this.readable)
50
+ throw new Error("the daily allowance could not be read");
51
+ const entry = this.stored[botId];
52
+ return entry && entry.day === dayKey(now) ? entry.count : 0;
53
+ }
54
+ /** Count one more, and return the new total for the day. */
55
+ record(botId, now = new Date()) {
56
+ return this.reserve(botId, 1, Number.MAX_SAFE_INTEGER, now);
57
+ }
58
+ /** Reserve before any provider await. An uncertain send keeps its slot:
59
+ * refunding transport failures could repeat a send already delivered. */
60
+ reserve(botId, amount, cap, now = new Date()) {
61
+ const day = dayKey(now);
62
+ const entry = this.stored[botId];
63
+ const count = this.today(botId, now) + amount;
64
+ if (count > cap)
65
+ return null;
66
+ this.stored[botId] = { day, count };
67
+ try {
68
+ this.save();
69
+ }
70
+ catch (error) {
71
+ if (entry)
72
+ this.stored[botId] = entry;
73
+ else
74
+ delete this.stored[botId];
75
+ throw error;
76
+ }
77
+ return count;
78
+ }
79
+ }
@@ -0,0 +1,60 @@
1
+ // Outbound calls held for a person's answer.
2
+ //
3
+ // The connector relay opens one of these when a bot's tool call would send
4
+ // something and the bot's policy says ask. The relay then waits on
5
+ // `answer`; the respond route resolves it when the card is answered. The
6
+ // contract mirrors the other card services: a request is claimed exactly
7
+ // once, a second answer says so instead of doing anything, and nothing
8
+ // waits forever — the relay has its own deadline, so the hold has one too.
9
+ //
10
+ // In-memory on purpose. A restart drops every held relay along with the
11
+ // proxy process that was waiting on it; the card left on the thread then
12
+ // settles as unavailable, the same way an engine's own request does.
13
+ import { randomUUID } from "node:crypto";
14
+ export class OutboundRequestService {
15
+ held = new Map();
16
+ /** Hold one call. `answer` settles with the person's verdict or a timeout. */
17
+ open(input) {
18
+ const requestId = randomUUID();
19
+ let settle;
20
+ const answer = new Promise((resolve) => {
21
+ settle = resolve;
22
+ });
23
+ const timer = setTimeout(() => this.settle(requestId, "timeout"), input.timeoutMs);
24
+ timer.unref?.();
25
+ this.held.set(requestId, { botId: input.botId, threadId: input.threadId, tool: input.tool, settle, timer });
26
+ return { requestId, answer };
27
+ }
28
+ settle(requestId, answer) {
29
+ const entry = this.held.get(requestId);
30
+ if (!entry || entry.answered)
31
+ return;
32
+ entry.answered = answer;
33
+ clearTimeout(entry.timer);
34
+ entry.settle(answer);
35
+ }
36
+ /** The person's answer, from the respond route. */
37
+ resolve(input) {
38
+ const entry = this.held.get(input.requestId);
39
+ if (!entry || entry.threadId !== input.threadId)
40
+ return { claimed: false };
41
+ if (entry.answered)
42
+ return { claimed: true, state: "already_settled", behavior: entry.answered };
43
+ const answer = input.behavior === "allow" ? "allow" : "deny";
44
+ this.settle(input.requestId, answer);
45
+ return { claimed: true, state: answer === "allow" ? "allowed" : "denied" };
46
+ }
47
+ /** Requests still waiting in a thread, oldest first. */
48
+ pending(threadId) {
49
+ return [...this.held.entries()]
50
+ .filter(([, entry]) => entry.threadId === threadId && !entry.answered)
51
+ .map(([requestId]) => requestId);
52
+ }
53
+ /** Forget a settled request once its waiter has read the answer. */
54
+ forget(requestId) {
55
+ const entry = this.held.get(requestId);
56
+ if (entry)
57
+ clearTimeout(entry.timer);
58
+ this.held.delete(requestId);
59
+ }
60
+ }
@@ -41,7 +41,7 @@ export class PackageImportError extends Error {
41
41
  export function orgInstallId(adminOrigin, organizationId, packageId) {
42
42
  return createHash("sha256").update(`omb-install:v1\n${adminOrigin}\n${organizationId}\n${packageId}`, "utf8").digest("hex").slice(0, 32);
43
43
  }
44
- function memberFromAgent(agent) {
44
+ export function memberFromAgent(agent) {
45
45
  return {
46
46
  key: agent.key,
47
47
  name: agent.name,
@@ -43,6 +43,7 @@ export function peerDeliveryReceipt(receipt) {
43
43
  detail: clip(receipt.detail) || "no detail recorded",
44
44
  ...(receipt.taskId ? { taskId: receipt.taskId } : {}),
45
45
  ...(receipt.requestId ? { requestId: receipt.requestId } : {}),
46
+ ...(receipt.threadId ? { threadId: receipt.threadId } : {}),
46
47
  };
47
48
  }
48
49
  const named = (receipt) => peerName(receipt.botName ?? "") || peerName(receipt.botId) || "teammate";