@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 +24 -0
- package/dist/ackTimeout.d.ts +5 -0
- package/dist/ackTimeout.js +10 -0
- package/dist/client.js +12 -10
- package/package.json +4 -4
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
|
-
|
|
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(
|
|
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
|
|
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 ${
|
|
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
|
|
257
|
-
* (measured, Codex). The bridge's user would wait
|
|
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
|
+
"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
|
|
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
|
|
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.
|
|
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",
|