@mulmobridge/client 1.3.0 → 1.4.0

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.
package/README.md CHANGED
@@ -108,6 +108,30 @@ One case is outside this: a server-initiated disconnect (`io server disconnect`)
108
108
  is the one reason socket.io does not retry, so no connection failure follows it.
109
109
  The chat-service never issues one, so there is nothing to recover from today.
110
110
 
111
+ ## How long `send()` waits
112
+
113
+ The server gives the agent **5 minutes** per turn, then replies with whatever
114
+ text has streamed so far — anything the agent produces after that is dropped.
115
+ `send()` waits one minute longer than that, so the server's reply always wins
116
+ over a client-side timeout.
117
+
118
+ To give long turns more time, set the limit in milliseconds on the bridge:
119
+
120
+ ```bash
121
+ BRIDGE_REPLY_TIMEOUT_MS=1800000 # every bridge: 30 minutes
122
+ DISCORD_BRIDGE_REPLY_TIMEOUT_MS=1800000 # this bridge only (wins over the shared form)
123
+ ```
124
+
125
+ It travels to the server in the handshake options, so the server and `send()`
126
+ always use the same value — nothing to keep in step by hand. A value that is not
127
+ a positive whole number is ignored with a warning (the default applies); one
128
+ past Node's timer ceiling (about 24.8 days) is clamped with a warning.
129
+
130
+ While a turn is running, the next message in the same chat waits for it, so a
131
+ longer limit can also mean a longer wait for the message after it. Upgrade the
132
+ bridge together with the server: an older client keeps its fixed 6-minute wait
133
+ and gives up before a longer server limit ends.
134
+
111
135
  ## Ecosystem
112
136
 
113
137
  Part of the [`@mulmobridge/*`](https://www.npmjs.com/~mulmobridge) package family.
@@ -0,0 +1,5 @@
1
+ import { type BridgeOptions } from "@mulmobridge/protocol";
2
+ /** How long `send()` waits for the ack. Read from the same option the handshake
3
+ * sends the server, so it always outlasts the server's reply limit and the
4
+ * server's timeout surfaces as a reply, not a client-side cancellation. */
5
+ export declare function resolveAckTimeoutMs(options: BridgeOptions, warn: (message: string) => void): number;
@@ -0,0 +1,10 @@
1
+ import { ackTimeoutMsFor, resolveReplyTimeoutMs } from "@mulmobridge/protocol";
2
+ /** How long `send()` waits for the ack. Read from the same option the handshake
3
+ * sends the server, so it always outlasts the server's reply limit and the
4
+ * server's timeout surfaces as a reply, not a client-side cancellation. */
5
+ export function resolveAckTimeoutMs(options, warn) {
6
+ const { replyTimeoutMs, warning } = resolveReplyTimeoutMs(options.replyTimeoutMs);
7
+ if (warning)
8
+ warn(`[bridge] ${warning}`);
9
+ return ackTimeoutMsFor(replyTimeoutMs);
10
+ }
package/dist/client.js CHANGED
@@ -13,13 +13,11 @@
13
13
  // minimal non-Node equivalent.
14
14
  import { io } from "socket.io-client";
15
15
  import { CHAT_SOCKET_EVENTS, CHAT_SOCKET_PATH } from "@mulmobridge/protocol";
16
+ import { resolveAckTimeoutMs } from "./ackTimeout.js";
16
17
  import { readBridgeToken, tokenFilePath } from "./token.js";
17
18
  import { readBridgeEnvOptions } from "./options.js";
18
19
  import { DEFAULT_API_URL, resolvePublishedApiUrl } from "./apiUrl.js";
19
20
  import { backoffMs, credentialsChanged } from "./supervisor.js";
20
- // 6 min > the server's REPLY_TIMEOUT_MS (5 min) so the server's
21
- // timeout surfaces as a reply, not a client-side cancellation.
22
- const REPLY_TIMEOUT_MS = 6 * 60 * 1000;
23
21
  /**
24
22
  * Resolve the bearer token from the workspace / env var, exit with
25
23
  * a clear error if absent. Kept separate so bridges that want to
@@ -69,7 +67,10 @@ export function createBridgeClient(opts) {
69
67
  const token = requireBearerToken();
70
68
  // `opts.options === undefined` → scrape env automatically.
71
69
  // `opts.options === {}` → opt out of the scrape explicitly.
72
- const options = opts.options ?? readBridgeEnvOptions(opts.transportId, process.env);
70
+ // A copy: every reconnect re-sends it, and the ack limit below is fixed from it
71
+ // once, so a caller mutating their object later must not move one without the other.
72
+ const options = { ...(opts.options ?? readBridgeEnvOptions(opts.transportId, process.env)) };
73
+ const ackTimeoutMs = resolveAckTimeoutMs(options, console.error);
73
74
  const subscriptions = emptySubscriptions();
74
75
  const pending = new Set();
75
76
  const published = resolvePublishedApiUrl(opts.apiUrl);
@@ -191,7 +192,7 @@ export function createBridgeClient(opts) {
191
192
  live.retry = setTimeout(reresolve, backoffMs(live.attempt));
192
193
  }
193
194
  return {
194
- send: (externalChatId, text, attachments) => sendMessage(live.socket, pending, externalChatId, text, attachments),
195
+ send: (externalChatId, text, attachments) => sendMessage({ socket: live.socket, pending, ackTimeoutMs }, externalChatId, text, attachments),
195
196
  onPush: (handler) => {
196
197
  subscriptions.push.push(handler);
197
198
  live.socket.on(CHAT_SOCKET_EVENTS.push, handler);
@@ -222,7 +223,8 @@ export function createBridgeClient(opts) {
222
223
  },
223
224
  };
224
225
  }
225
- function sendMessage(socket, pending, externalChatId, text, attachments) {
226
+ function sendMessage(channel, externalChatId, text, attachments) {
227
+ const { socket, pending, ackTimeoutMs } = channel;
226
228
  const payload = { externalChatId, text };
227
229
  if (attachments && attachments.length > 0)
228
230
  payload.attachments = attachments;
@@ -231,7 +233,7 @@ function sendMessage(socket, pending, externalChatId, text, attachments) {
231
233
  // CANCELLABLE. socket.io arms its ack timer at emit time and keeps it armed
232
234
  // on a socket that is closed underneath it, so a send abandoned by a rebuild
233
235
  // left a six-minute timer behind per send — measured: the test process exited
234
- // at 6:00.45, exactly REPLY_TIMEOUT_MS, long after every assertion had passed
236
+ // at 6:00.45, exactly the ack timeout, long after every assertion had passed
235
237
  // (Codex, #3078). `settle` clears it, so `abandon` clears it too.
236
238
  const state = {};
237
239
  const settle = (ack) => {
@@ -240,7 +242,7 @@ function sendMessage(socket, pending, externalChatId, text, attachments) {
240
242
  clearTimeout(state.timer);
241
243
  resolve(ack);
242
244
  };
243
- state.timer = setTimeout(() => settle({ ok: false, error: `timeout: no ack within ${REPLY_TIMEOUT_MS}ms` }), REPLY_TIMEOUT_MS);
245
+ state.timer = setTimeout(() => settle({ ok: false, error: `timeout: no ack within ${ackTimeoutMs}ms` }), ackTimeoutMs);
244
246
  pending.add(settle);
245
247
  socket.emit(CHAT_SOCKET_EVENTS.message, payload, (ack) => {
246
248
  settle(ack ?? { ok: false, error: "no ack from server" });
@@ -253,8 +255,8 @@ function sendMessage(socket, pending, externalChatId, text, attachments) {
253
255
  * socket.io settles an IN-FLIGHT ack immediately when its socket closes, but a
254
256
  * send issued while the socket was already disconnected is queued for a
255
257
  * reconnection that will never happen here — the socket is being replaced, not
256
- * reconnected — so its callback would sit for the full 6-minute ack timeout
257
- * (measured, Codex). The bridge's user would wait six minutes for a message the
258
+ * reconnected — so its callback would sit for the full ack timeout
259
+ * (measured, Codex). The bridge's user would wait that whole time for a message the
258
260
  * client already knows it cannot deliver.
259
261
  */
260
262
  function abandon(pending, reason) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mulmobridge/client",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Socket.io client library for MulmoBridge — shared by all bridge implementations",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -38,19 +38,19 @@
38
38
  "scripts": {
39
39
  "build": "tsc",
40
40
  "prepack": "yarn build",
41
- "typecheck": "tsc --noEmit",
41
+ "typecheck": "tsc -p tsconfig.typecheck.json",
42
42
  "test": "tsx --test test/test_*.ts",
43
43
  "lint": "eslint src test"
44
44
  },
45
45
  "license": "MIT",
46
46
  "author": "Receptron Team",
47
47
  "dependencies": {
48
- "@mulmobridge/protocol": "^1.0.1",
48
+ "@mulmobridge/protocol": "^1.1.0",
49
49
  "@mulmoclaude/common": "^1.3.0",
50
50
  "socket.io-client": "^4.0.0"
51
51
  },
52
52
  "devDependencies": {
53
- "@types/node": "^26.5.1",
53
+ "@types/node": "^26.6.3",
54
54
  "typescript": "^6.0.3"
55
55
  },
56
56
  "homepage": "https://github.com/receptron/mulmoclaude/tree/main/packages/client#readme",