@mulmobridge/chat-service 1.1.0 → 1.3.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/dist/index.d.ts +7 -0
- package/dist/index.js +36 -0
- package/dist/relay.js +13 -6
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -14,6 +14,13 @@ export interface ChatService {
|
|
|
14
14
|
* before `attachSocket`: the message is queued and flushes on
|
|
15
15
|
* the next bridge connection for that transport. */
|
|
16
16
|
pushToBridge: PushFn;
|
|
17
|
+
/** Subscribe an IN-PROCESS bridge to this transport's pushes (#3080).
|
|
18
|
+
* Returns the unsubscribe function. An in-process bridge is always live, so
|
|
19
|
+
* its pushes are never queued. */
|
|
20
|
+
registerInProcessBridge(transportId: string, handler: (event: {
|
|
21
|
+
chatId: string;
|
|
22
|
+
message: string;
|
|
23
|
+
}) => void): () => void;
|
|
17
24
|
}
|
|
18
25
|
export declare function createChatService(deps: ChatServiceDeps): ChatService;
|
|
19
26
|
export type { Attachment, ChatServiceDeps, StartChatFn, StartChatParams, OnSessionEventFn } from "./types.js";
|
package/dist/index.js
CHANGED
|
@@ -60,7 +60,42 @@ export function createChatService(deps) {
|
|
|
60
60
|
// The queue is shared with the socket layer so any pushes
|
|
61
61
|
// enqueued during the pre-attach window flush on first connect.
|
|
62
62
|
let livePush = null;
|
|
63
|
+
// In-process bridges (#3080). Keyed by transportId, at most one each: two
|
|
64
|
+
// bridges claiming one transportId is what #3079 exists to stop, and the
|
|
65
|
+
// registry refuses the second rather than silently fanning a reply out twice.
|
|
66
|
+
const inProcessBridges = new Map();
|
|
67
|
+
const registerInProcessBridge = (transportId, handler) => {
|
|
68
|
+
if (inProcessBridges.has(transportId)) {
|
|
69
|
+
throw new Error(`an in-process bridge is already registered for transport "${transportId}"`);
|
|
70
|
+
}
|
|
71
|
+
inProcessBridges.set(transportId, handler);
|
|
72
|
+
return () => {
|
|
73
|
+
if (inProcessBridges.get(transportId) === handler)
|
|
74
|
+
inProcessBridges.delete(transportId);
|
|
75
|
+
};
|
|
76
|
+
};
|
|
63
77
|
const pushToBridge = (transportId, chatId, message) => {
|
|
78
|
+
// An in-process bridge is in this very process, so it is live by
|
|
79
|
+
// construction — deliver and return rather than consulting the socket
|
|
80
|
+
// layer, whose miss path would queue a message that has been delivered.
|
|
81
|
+
const inProcess = inProcessBridges.get(transportId);
|
|
82
|
+
if (inProcess) {
|
|
83
|
+
try {
|
|
84
|
+
// Runs on WHATEVER stack called `pushToBridge` — a route handler, a
|
|
85
|
+
// scheduler tick. A socket bridge cannot reach us here because its
|
|
86
|
+
// handler runs in its own process; an in-process one can, so the
|
|
87
|
+
// isolation has to be written rather than assumed.
|
|
88
|
+
inProcess({ chatId, message });
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
logger.error("chat-service", "in-process bridge push handler threw", {
|
|
92
|
+
transportId,
|
|
93
|
+
chatId,
|
|
94
|
+
error: err instanceof Error ? err.message : String(err),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
64
99
|
if (livePush) {
|
|
65
100
|
livePush(transportId, chatId, message);
|
|
66
101
|
return;
|
|
@@ -152,6 +187,7 @@ export function createChatService(deps) {
|
|
|
152
187
|
livePush = handle.pushToBridge;
|
|
153
188
|
},
|
|
154
189
|
pushToBridge,
|
|
190
|
+
registerInProcessBridge,
|
|
155
191
|
};
|
|
156
192
|
}
|
|
157
193
|
export { writeFileAtomic } from "./atomic-write.js";
|
package/dist/relay.js
CHANGED
|
@@ -6,10 +6,8 @@
|
|
|
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
|
-
// ── Constants ────────────────────────────────────────────────
|
|
12
|
-
const REPLY_TIMEOUT_MS = 5 * 60 * 1000;
|
|
13
11
|
// ── Factory ──────────────────────────────────────────────────
|
|
14
12
|
export function createRelay(deps) {
|
|
15
13
|
const serialize = createKeyedSerializer();
|
|
@@ -96,7 +94,8 @@ async function processRelayMessage(deps, params) {
|
|
|
96
94
|
};
|
|
97
95
|
}
|
|
98
96
|
try {
|
|
99
|
-
const
|
|
97
|
+
const replyTimeoutMs = resolveBridgeReplyTimeout(bridgeOptions, logger, transportId);
|
|
98
|
+
const reply = await collectAgentReply(onSessionEvent, chatState.sessionId, replyTimeoutMs, params.onChunk);
|
|
100
99
|
await store.setChatState(transportId, {
|
|
101
100
|
...chatState,
|
|
102
101
|
updatedAt: new Date().toISOString(),
|
|
@@ -139,15 +138,23 @@ export function resolveDefaultRole(bridgeOptions, getRole, fallbackRoleId, logge
|
|
|
139
138
|
}
|
|
140
139
|
return resolved.id;
|
|
141
140
|
}
|
|
141
|
+
// The bridge client reads the same option for its ack timer, so a bad value
|
|
142
|
+
// is warned about on both ends and both fall back to the same default.
|
|
143
|
+
function resolveBridgeReplyTimeout(bridgeOptions, logger, transportId) {
|
|
144
|
+
const { replyTimeoutMs, warning } = resolveReplyTimeoutMs(bridgeOptions?.replyTimeoutMs);
|
|
145
|
+
if (warning)
|
|
146
|
+
logger.warn("chat-service", "bridge reply timeout option ignored", { transportId, warning });
|
|
147
|
+
return replyTimeoutMs;
|
|
148
|
+
}
|
|
142
149
|
// Kept out of the factory closure so future packaging doesn't need
|
|
143
150
|
// to re-capture anything; `onSessionEvent` arrives as a plain param.
|
|
144
|
-
function collectAgentReply(onSessionEvent, chatSessionId, onChunk) {
|
|
151
|
+
function collectAgentReply(onSessionEvent, chatSessionId, replyTimeoutMs, onChunk) {
|
|
145
152
|
return new Promise((resolve) => {
|
|
146
153
|
const textChunks = [];
|
|
147
154
|
const timer = setTimeout(() => {
|
|
148
155
|
unsubscribe();
|
|
149
156
|
resolve(textChunks.join("") || "The request timed out before a reply was generated.");
|
|
150
|
-
},
|
|
157
|
+
}, replyTimeoutMs);
|
|
151
158
|
const unsubscribe = onSessionEvent(chatSessionId, (event) => {
|
|
152
159
|
const type = event.type;
|
|
153
160
|
if (type === EVENT_TYPES.text) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mulmobridge/chat-service",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
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",
|