@mulmobridge/chat-service 1.2.0 → 1.3.1
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/dist/relay.d.ts +2 -0
- package/dist/relay.js +30 -8
- package/dist/reply-deadline.d.ts +2 -0
- package/dist/reply-deadline.js +9 -0
- package/package.json +3 -3
package/dist/relay.d.ts
CHANGED
|
@@ -34,6 +34,8 @@ export interface RelayDeps {
|
|
|
34
34
|
getRole: (roleId: string) => Role;
|
|
35
35
|
defaultRoleId: string;
|
|
36
36
|
logger: Logger;
|
|
37
|
+
/** Monotonic milliseconds for the reply limit. Defaults to `performance.now()`. */
|
|
38
|
+
now?: () => number;
|
|
37
39
|
}
|
|
38
40
|
export declare function createRelay(deps: RelayDeps): RelayFn;
|
|
39
41
|
export declare function resolveDefaultRole(bridgeOptions: Readonly<Record<string, string | number | boolean>> | undefined, getRole: (roleId: string) => Role, fallbackRoleId: string, logger: Logger, transportId: string): string;
|
package/dist/relay.js
CHANGED
|
@@ -6,10 +6,9 @@
|
|
|
6
6
|
// point, session events, role lookup, logger) arrive through
|
|
7
7
|
// `createRelay(deps)` so the module has no direct imports from the
|
|
8
8
|
// host.
|
|
9
|
-
import { EVENT_TYPES } from "@mulmobridge/protocol";
|
|
9
|
+
import { EVENT_TYPES, resolveReplyTimeoutMs } from "@mulmobridge/protocol";
|
|
10
10
|
import { createKeyedSerializer } from "./keyed-serializer.js";
|
|
11
|
-
|
|
12
|
-
const REPLY_TIMEOUT_MS = 5 * 60 * 1000;
|
|
11
|
+
import { remainingReplyMs } from "./reply-deadline.js";
|
|
13
12
|
// ── Factory ──────────────────────────────────────────────────
|
|
14
13
|
export function createRelay(deps) {
|
|
15
14
|
const serialize = createKeyedSerializer();
|
|
@@ -19,10 +18,19 @@ export function createRelay(deps) {
|
|
|
19
18
|
// sessions, and split the conversation across them (#1878). The
|
|
20
19
|
// JSON pair is an unambiguous composite key for the two ids.
|
|
21
20
|
const key = JSON.stringify([params.transportId, params.externalChatId]);
|
|
22
|
-
|
|
21
|
+
const budget = {
|
|
22
|
+
receivedAtMs: clockOf(deps)(),
|
|
23
|
+
replyTimeoutMs: resolveBridgeReplyTimeout(params.bridgeOptions, deps.logger, params.transportId),
|
|
24
|
+
};
|
|
25
|
+
return serialize.run(key, () => processRelayMessage(deps, params, budget));
|
|
23
26
|
};
|
|
24
27
|
}
|
|
25
|
-
|
|
28
|
+
// Neutral about the cause: a queue behind an earlier message, or slow setup on an idle chat.
|
|
29
|
+
const EXPIRED_BEFORE_START_REPLY = "The request timed out before the agent could start on it. Please send it again.";
|
|
30
|
+
// Monotonic by default, so a wall-clock step between receipt and processing cannot move the limit.
|
|
31
|
+
const clockOf = (deps) => deps.now ?? (() => performance.now());
|
|
32
|
+
const remainingOf = (budget, deps) => remainingReplyMs(budget.receivedAtMs, budget.replyTimeoutMs, clockOf(deps)());
|
|
33
|
+
async function processRelayMessage(deps, params, budget) {
|
|
26
34
|
const { store, handleCommand, startChat, onSessionEvent, getRole, defaultRoleId, logger } = deps;
|
|
27
35
|
const { transportId, externalChatId, attachments, bridgeOptions } = params;
|
|
28
36
|
let { text } = params;
|
|
@@ -62,6 +70,12 @@ async function processRelayMessage(deps, params) {
|
|
|
62
70
|
chatState = commandResult.nextState;
|
|
63
71
|
text = commandResult.forwardAs;
|
|
64
72
|
}
|
|
73
|
+
// Checked here, not on entry: a command above still runs and answers at once, but an agent turn
|
|
74
|
+
// started after the limit would produce a reply nobody is waiting for.
|
|
75
|
+
if (remainingOf(budget, deps) === 0) {
|
|
76
|
+
logger.info("chat-service", "message expired before its turn started", { transportId, externalChatId });
|
|
77
|
+
return { kind: "ok", reply: EXPIRED_BEFORE_START_REPLY };
|
|
78
|
+
}
|
|
65
79
|
const result = await startChat({
|
|
66
80
|
message: text,
|
|
67
81
|
roleId: chatState.roleId,
|
|
@@ -96,7 +110,7 @@ async function processRelayMessage(deps, params) {
|
|
|
96
110
|
};
|
|
97
111
|
}
|
|
98
112
|
try {
|
|
99
|
-
const reply = await collectAgentReply(onSessionEvent, chatState.sessionId, params.onChunk);
|
|
113
|
+
const reply = await collectAgentReply(onSessionEvent, chatState.sessionId, remainingOf(budget, deps), params.onChunk);
|
|
100
114
|
await store.setChatState(transportId, {
|
|
101
115
|
...chatState,
|
|
102
116
|
updatedAt: new Date().toISOString(),
|
|
@@ -139,15 +153,23 @@ export function resolveDefaultRole(bridgeOptions, getRole, fallbackRoleId, logge
|
|
|
139
153
|
}
|
|
140
154
|
return resolved.id;
|
|
141
155
|
}
|
|
156
|
+
// The bridge client reads the same option for its ack timer, so a bad value
|
|
157
|
+
// is warned about on both ends and both fall back to the same default.
|
|
158
|
+
function resolveBridgeReplyTimeout(bridgeOptions, logger, transportId) {
|
|
159
|
+
const { replyTimeoutMs, warning } = resolveReplyTimeoutMs(bridgeOptions?.replyTimeoutMs);
|
|
160
|
+
if (warning)
|
|
161
|
+
logger.warn("chat-service", "bridge reply timeout option ignored", { transportId, warning });
|
|
162
|
+
return replyTimeoutMs;
|
|
163
|
+
}
|
|
142
164
|
// Kept out of the factory closure so future packaging doesn't need
|
|
143
165
|
// to re-capture anything; `onSessionEvent` arrives as a plain param.
|
|
144
|
-
function collectAgentReply(onSessionEvent, chatSessionId, onChunk) {
|
|
166
|
+
function collectAgentReply(onSessionEvent, chatSessionId, replyTimeoutMs, onChunk) {
|
|
145
167
|
return new Promise((resolve) => {
|
|
146
168
|
const textChunks = [];
|
|
147
169
|
const timer = setTimeout(() => {
|
|
148
170
|
unsubscribe();
|
|
149
171
|
resolve(textChunks.join("") || "The request timed out before a reply was generated.");
|
|
150
|
-
},
|
|
172
|
+
}, replyTimeoutMs);
|
|
151
173
|
const unsubscribe = onSessionEvent(chatSessionId, (event) => {
|
|
152
174
|
const type = event.type;
|
|
153
175
|
if (type === EVENT_TYPES.text) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// A turn's reply limit is counted from when the relay RECEIVED the message,
|
|
2
|
+
// not from when the turn reached the front of its chat's queue. The bridge
|
|
3
|
+
// client starts its ack timer when it sends, so a limit counted from the start
|
|
4
|
+
// of collection let a queued or slow-to-start turn outlive the client's wait
|
|
5
|
+
// and have its reply dropped (#3312).
|
|
6
|
+
/** Milliseconds of the reply limit still left at `nowMs`; never negative. */
|
|
7
|
+
export function remainingReplyMs(receivedAtMs, replyTimeoutMs, nowMs) {
|
|
8
|
+
return Math.max(0, receivedAtMs + replyTimeoutMs - nowMs);
|
|
9
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mulmobridge/chat-service",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "Server-side chat service for MulmoBridge — socket.io + REST bridge to Claude Code agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -20,14 +20,14 @@
|
|
|
20
20
|
"scripts": {
|
|
21
21
|
"build": "tsc",
|
|
22
22
|
"prepack": "yarn build",
|
|
23
|
-
"typecheck": "tsc
|
|
23
|
+
"typecheck": "tsc -p tsconfig.typecheck.json",
|
|
24
24
|
"test": "tsx --test test/test_*.ts",
|
|
25
25
|
"lint": "eslint src test"
|
|
26
26
|
},
|
|
27
27
|
"license": "MIT",
|
|
28
28
|
"author": "Receptron Team",
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@mulmobridge/protocol": "^1.0
|
|
30
|
+
"@mulmobridge/protocol": "^1.1.0"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"express": "^5.0.0",
|