@mulmobridge/chat-service 1.3.1 → 1.3.2
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.js +7 -11
- package/dist/start-when-idle.d.ts +11 -0
- package/dist/start-when-idle.js +52 -0
- package/package.json +1 -1
package/dist/relay.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { EVENT_TYPES, resolveReplyTimeoutMs } from "@mulmobridge/protocol";
|
|
10
10
|
import { createKeyedSerializer } from "./keyed-serializer.js";
|
|
11
11
|
import { remainingReplyMs } from "./reply-deadline.js";
|
|
12
|
+
import { startChatWhenIdle } from "./start-when-idle.js";
|
|
12
13
|
// ── Factory ──────────────────────────────────────────────────
|
|
13
14
|
export function createRelay(deps) {
|
|
14
15
|
const serialize = createKeyedSerializer();
|
|
@@ -76,7 +77,8 @@ async function processRelayMessage(deps, params, budget) {
|
|
|
76
77
|
logger.info("chat-service", "message expired before its turn started", { transportId, externalChatId });
|
|
77
78
|
return { kind: "ok", reply: EXPIRED_BEFORE_START_REPLY };
|
|
78
79
|
}
|
|
79
|
-
const
|
|
80
|
+
const idleStartDeps = { startChat, onSessionEvent, remainingMs: () => remainingOf(budget, deps) };
|
|
81
|
+
const result = await startChatWhenIdle(idleStartDeps, {
|
|
80
82
|
message: text,
|
|
81
83
|
roleId: chatState.roleId,
|
|
82
84
|
chatSessionId: chatState.sessionId,
|
|
@@ -86,18 +88,12 @@ async function processRelayMessage(deps, params, budget) {
|
|
|
86
88
|
// we forward the whole bag untouched.
|
|
87
89
|
bridgeOptions,
|
|
88
90
|
});
|
|
91
|
+
if (result.kind === "expired") {
|
|
92
|
+
logger.info("chat-service", "message expired waiting for the session to finish", { transportId, externalChatId });
|
|
93
|
+
return { kind: "ok", reply: EXPIRED_BEFORE_START_REPLY };
|
|
94
|
+
}
|
|
89
95
|
if (result.kind === "error") {
|
|
90
96
|
const status = result.status ?? 500;
|
|
91
|
-
if (status === 409) {
|
|
92
|
-
// Session busy — tell the bridge to retry. Keep the HTTP
|
|
93
|
-
// response shape the old handler returned (status 409 on
|
|
94
|
-
// the HTTP side, "ok" reply text on the socket side — both
|
|
95
|
-
// layers decide how to serialise).
|
|
96
|
-
return {
|
|
97
|
-
kind: "ok",
|
|
98
|
-
reply: "A previous message is still being processed. Please wait.",
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
97
|
logger.error("chat-service", "startChat failed", {
|
|
102
98
|
transportId,
|
|
103
99
|
externalChatId,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { OnSessionEventFn, StartChatFn, StartChatParams, StartChatResult } from "./types.js";
|
|
2
|
+
export type IdleStartResult = StartChatResult | {
|
|
3
|
+
kind: "expired";
|
|
4
|
+
};
|
|
5
|
+
export interface IdleStartDeps {
|
|
6
|
+
startChat: StartChatFn;
|
|
7
|
+
onSessionEvent: OnSessionEventFn;
|
|
8
|
+
/** Milliseconds this message may still wait; 0 once its limit has passed. */
|
|
9
|
+
remainingMs: () => number;
|
|
10
|
+
}
|
|
11
|
+
export declare function startChatWhenIdle(deps: IdleStartDeps, params: StartChatParams): Promise<IdleStartResult>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Start a bridge turn even when the session is still busy with an earlier run.
|
|
2
|
+
//
|
|
3
|
+
// A turn cut off at its reply limit leaves its agent running, so the next
|
|
4
|
+
// turn's `startChat()` used to get 409 and the message was dropped with
|
|
5
|
+
// "please wait" (#3320). Instead, wait for that run to finish — within the
|
|
6
|
+
// message's own remaining time — and start then.
|
|
7
|
+
import { EVENT_TYPES } from "@mulmobridge/protocol";
|
|
8
|
+
// `startChat()` answers 409 before it saves or broadcasts anything, so trying again is safe.
|
|
9
|
+
const isBusy = (result) => result.kind === "error" && result.status === 409;
|
|
10
|
+
function waitForSessionFinished(onSessionEvent, sessionId, timeoutMs) {
|
|
11
|
+
const handles = {};
|
|
12
|
+
const finished = new Promise((resolve) => {
|
|
13
|
+
handles.settle = (value) => {
|
|
14
|
+
handles.done = true;
|
|
15
|
+
clearTimeout(handles.timer);
|
|
16
|
+
handles.unsubscribe?.();
|
|
17
|
+
resolve(value);
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
const settle = (value) => handles.settle?.(value);
|
|
21
|
+
handles.timer = setTimeout(() => settle(false), timeoutMs);
|
|
22
|
+
handles.unsubscribe = onSessionEvent(sessionId, (event) => {
|
|
23
|
+
if (event.type === EVENT_TYPES.sessionFinished)
|
|
24
|
+
settle(true);
|
|
25
|
+
});
|
|
26
|
+
// A listener fired during subscription settled before `unsubscribe` existed.
|
|
27
|
+
if (handles.done)
|
|
28
|
+
handles.unsubscribe();
|
|
29
|
+
// A cancelled wait still settles, so nothing is left pending behind it.
|
|
30
|
+
return { finished, cancel: () => settle(false) };
|
|
31
|
+
}
|
|
32
|
+
async function retryWhenFinished(deps, params) {
|
|
33
|
+
const remainingMs = deps.remainingMs();
|
|
34
|
+
if (remainingMs === 0)
|
|
35
|
+
return { kind: "expired" };
|
|
36
|
+
// Subscribe BEFORE trying again: a run that ends between the 409 and the
|
|
37
|
+
// subscription would otherwise never be seen, and this would wait out the limit.
|
|
38
|
+
const wait = waitForSessionFinished(deps.onSessionEvent, params.chatSessionId, remainingMs);
|
|
39
|
+
const retried = await deps.startChat(params).catch((err) => {
|
|
40
|
+
wait.cancel();
|
|
41
|
+
throw err;
|
|
42
|
+
});
|
|
43
|
+
if (!isBusy(retried)) {
|
|
44
|
+
wait.cancel();
|
|
45
|
+
return retried;
|
|
46
|
+
}
|
|
47
|
+
return (await wait.finished) ? retryWhenFinished(deps, params) : { kind: "expired" };
|
|
48
|
+
}
|
|
49
|
+
export async function startChatWhenIdle(deps, params) {
|
|
50
|
+
const first = await deps.startChat(params);
|
|
51
|
+
return isBusy(first) ? retryWhenFinished(deps, params) : first;
|
|
52
|
+
}
|
package/package.json
CHANGED