@getpaseo/server 0.1.15 → 0.1.16
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/server/client/daemon-client.d.ts +41 -4
- package/dist/server/client/daemon-client.d.ts.map +1 -1
- package/dist/server/client/daemon-client.js +355 -84
- package/dist/server/client/daemon-client.js.map +1 -1
- package/dist/server/server/agent/agent-manager.d.ts +10 -0
- package/dist/server/server/agent/agent-manager.d.ts.map +1 -1
- package/dist/server/server/agent/agent-manager.js +261 -18
- package/dist/server/server/agent/agent-manager.js.map +1 -1
- package/dist/server/server/agent/agent-projections.d.ts +5 -0
- package/dist/server/server/agent/agent-projections.d.ts.map +1 -1
- package/dist/server/server/agent/agent-projections.js +24 -0
- package/dist/server/server/agent/agent-projections.js.map +1 -1
- package/dist/server/server/agent/agent-sdk-types.d.ts +11 -0
- package/dist/server/server/agent/agent-sdk-types.d.ts.map +1 -1
- package/dist/server/server/agent/agent-storage.d.ts +15 -5
- package/dist/server/server/agent/agent-storage.d.ts.map +1 -1
- package/dist/server/server/agent/agent-storage.js +2 -0
- package/dist/server/server/agent/agent-storage.js.map +1 -1
- package/dist/server/server/agent/providers/claude/tool-call-detail-parser.d.ts.map +1 -1
- package/dist/server/server/agent/providers/claude/tool-call-detail-parser.js +2 -0
- package/dist/server/server/agent/providers/claude/tool-call-detail-parser.js.map +1 -1
- package/dist/server/server/agent/providers/claude/tool-call-mapper.d.ts.map +1 -1
- package/dist/server/server/agent/providers/claude/tool-call-mapper.js +2 -0
- package/dist/server/server/agent/providers/claude/tool-call-mapper.js.map +1 -1
- package/dist/server/server/agent/providers/claude-agent.d.ts +7 -1
- package/dist/server/server/agent/providers/claude-agent.d.ts.map +1 -1
- package/dist/server/server/agent/providers/claude-agent.js +1470 -232
- package/dist/server/server/agent/providers/claude-agent.js.map +1 -1
- package/dist/server/server/agent/providers/codex-app-server-agent.d.ts.map +1 -1
- package/dist/server/server/agent/providers/codex-app-server-agent.js +19 -4
- package/dist/server/server/agent/providers/codex-app-server-agent.js.map +1 -1
- package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts +40 -0
- package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts.map +1 -1
- package/dist/server/server/agent/providers/tool-call-detail-primitives.js +1 -0
- package/dist/server/server/agent/providers/tool-call-detail-primitives.js.map +1 -1
- package/dist/server/server/client-message-id.d.ts +3 -0
- package/dist/server/server/client-message-id.d.ts.map +1 -0
- package/dist/server/server/client-message-id.js +12 -0
- package/dist/server/server/client-message-id.js.map +1 -0
- package/dist/server/server/persisted-config.d.ts +8 -8
- package/dist/server/server/persistence-hooks.js +1 -1
- package/dist/server/server/persistence-hooks.js.map +1 -1
- package/dist/server/server/relay-transport.d.ts.map +1 -1
- package/dist/server/server/relay-transport.js +27 -28
- package/dist/server/server/relay-transport.js.map +1 -1
- package/dist/server/server/session.d.ts +4 -2
- package/dist/server/server/session.d.ts.map +1 -1
- package/dist/server/server/session.js +122 -31
- package/dist/server/server/session.js.map +1 -1
- package/dist/server/server/websocket-server.d.ts +8 -4
- package/dist/server/server/websocket-server.d.ts.map +1 -1
- package/dist/server/server/websocket-server.js +272 -75
- package/dist/server/server/websocket-server.js.map +1 -1
- package/dist/server/shared/daemon-endpoints.d.ts +9 -1
- package/dist/server/shared/daemon-endpoints.d.ts.map +1 -1
- package/dist/server/shared/daemon-endpoints.js +18 -3
- package/dist/server/shared/daemon-endpoints.js.map +1 -1
- package/dist/server/shared/messages.d.ts +2065 -313
- package/dist/server/shared/messages.d.ts.map +1 -1
- package/dist/server/shared/messages.js +40 -1
- package/dist/server/shared/messages.js.map +1 -1
- package/dist/server/shared/tool-call-display.d.ts.map +1 -1
- package/dist/server/shared/tool-call-display.js +4 -0
- package/dist/server/shared/tool-call-display.js.map +1 -1
- package/package.json +3 -3
|
@@ -16,7 +16,6 @@ import type { VoiceCallerContext, VoiceMcpStdioConfig, VoiceSpeakHandler } from
|
|
|
16
16
|
export type AgentMcpTransportFactory = () => Promise<Transport>;
|
|
17
17
|
export type ExternalSocketMetadata = {
|
|
18
18
|
transport: "relay";
|
|
19
|
-
externalSessionKey: string;
|
|
20
19
|
};
|
|
21
20
|
type WebSocketServerConfig = {
|
|
22
21
|
allowedOrigins: Set<string>;
|
|
@@ -38,9 +37,9 @@ export declare class MissingDaemonVersionError extends Error {
|
|
|
38
37
|
export declare class VoiceAssistantWebSocketServer {
|
|
39
38
|
private readonly logger;
|
|
40
39
|
private readonly wss;
|
|
40
|
+
private readonly pendingConnections;
|
|
41
41
|
private readonly sessions;
|
|
42
42
|
private readonly externalSessionsByKey;
|
|
43
|
-
private clientIdCounter;
|
|
44
43
|
private readonly serverId;
|
|
45
44
|
private readonly daemonVersion;
|
|
46
45
|
private readonly agentManager;
|
|
@@ -82,10 +81,15 @@ export declare class VoiceAssistantWebSocketServer {
|
|
|
82
81
|
close(): Promise<void>;
|
|
83
82
|
private sendToClient;
|
|
84
83
|
private sendBinaryToClient;
|
|
84
|
+
private sendToConnection;
|
|
85
|
+
private sendBinaryToConnection;
|
|
85
86
|
private attachSocket;
|
|
87
|
+
private createSessionConnection;
|
|
88
|
+
private clearPendingConnection;
|
|
89
|
+
private buildWelcomeMessage;
|
|
90
|
+
private handleHello;
|
|
86
91
|
private buildServerInfoStatusPayload;
|
|
87
|
-
private
|
|
88
|
-
private sendServerInfo;
|
|
92
|
+
private broadcastCapabilitiesUpdate;
|
|
89
93
|
private bindSocketHandlers;
|
|
90
94
|
resolveVoiceSpeakHandler(callerAgentId: string): VoiceSpeakHandler | null;
|
|
91
95
|
resolveVoiceCallerContext(callerAgentId: string): VoiceCallerContext | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"websocket-server.d.ts","sourceRoot":"","sources":["../../../src/server/websocket-server.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,MAAM,CAAC;AACjD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+CAA+C,CAAC;AAG/E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AACzE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AACvE,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,
|
|
1
|
+
{"version":3,"file":"websocket-server.d.ts","sourceRoot":"","sources":["../../../src/server/websocket-server.ts"],"names":[],"mappings":"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"}
|
|
@@ -68,6 +68,11 @@ function bufferFromWsData(data) {
|
|
|
68
68
|
return Buffer.from(data);
|
|
69
69
|
}
|
|
70
70
|
const EXTERNAL_SESSION_DISCONNECT_GRACE_MS = 90000;
|
|
71
|
+
const HELLO_TIMEOUT_MS = 15000;
|
|
72
|
+
const WS_CLOSE_HELLO_TIMEOUT = 4001;
|
|
73
|
+
const WS_CLOSE_INVALID_HELLO = 4002;
|
|
74
|
+
const WS_CLOSE_INCOMPATIBLE_PROTOCOL = 4003;
|
|
75
|
+
const WS_PROTOCOL_VERSION = 1;
|
|
71
76
|
export class MissingDaemonVersionError extends Error {
|
|
72
77
|
constructor() {
|
|
73
78
|
super("VoiceAssistantWebSocketServer requires a non-empty daemonVersion.");
|
|
@@ -79,9 +84,9 @@ export class MissingDaemonVersionError extends Error {
|
|
|
79
84
|
*/
|
|
80
85
|
export class VoiceAssistantWebSocketServer {
|
|
81
86
|
constructor(server, logger, serverId, agentManager, agentStorage, downloadTokenStore, paseoHome, createAgentMcpTransport, wsConfig, speech, terminalManager, voice, dictation, agentProviderRuntimeSettings, daemonVersion) {
|
|
87
|
+
this.pendingConnections = new Map();
|
|
82
88
|
this.sessions = new Map();
|
|
83
89
|
this.externalSessionsByKey = new Map();
|
|
84
|
-
this.clientIdCounter = 0;
|
|
85
90
|
this.voiceSpeakHandlers = new Map();
|
|
86
91
|
this.voiceCallerContexts = new Map();
|
|
87
92
|
this.ACTIVITY_THRESHOLD_MS = 120000;
|
|
@@ -159,7 +164,7 @@ export class VoiceAssistantWebSocketServer {
|
|
|
159
164
|
return;
|
|
160
165
|
}
|
|
161
166
|
this.serverCapabilities = next;
|
|
162
|
-
this.
|
|
167
|
+
this.broadcastCapabilitiesUpdate();
|
|
163
168
|
}
|
|
164
169
|
async attachExternalSocket(ws, metadata) {
|
|
165
170
|
await this.attachSocket(ws, undefined, metadata);
|
|
@@ -169,16 +174,34 @@ export class VoiceAssistantWebSocketServer {
|
|
|
169
174
|
...this.sessions.values(),
|
|
170
175
|
...this.externalSessionsByKey.values(),
|
|
171
176
|
]);
|
|
177
|
+
const pendingSockets = new Set(this.pendingConnections.keys());
|
|
178
|
+
for (const pending of this.pendingConnections.values()) {
|
|
179
|
+
if (pending.helloTimeout) {
|
|
180
|
+
clearTimeout(pending.helloTimeout);
|
|
181
|
+
pending.helloTimeout = null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
172
184
|
const cleanupPromises = [];
|
|
173
185
|
for (const connection of uniqueConnections) {
|
|
174
186
|
if (connection.externalDisconnectCleanupTimeout) {
|
|
175
187
|
clearTimeout(connection.externalDisconnectCleanupTimeout);
|
|
176
188
|
connection.externalDisconnectCleanupTimeout = null;
|
|
177
189
|
}
|
|
178
|
-
const ws = connection.socketRef.current;
|
|
179
190
|
cleanupPromises.push(connection.session.cleanup());
|
|
191
|
+
for (const ws of connection.sockets) {
|
|
192
|
+
cleanupPromises.push(new Promise((resolve) => {
|
|
193
|
+
// WebSocket.CLOSED = 3
|
|
194
|
+
if (ws.readyState === 3) {
|
|
195
|
+
resolve();
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
ws.once("close", () => resolve());
|
|
199
|
+
ws.close();
|
|
200
|
+
}));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
for (const ws of pendingSockets) {
|
|
180
204
|
cleanupPromises.push(new Promise((resolve) => {
|
|
181
|
-
// WebSocket.CLOSED = 3
|
|
182
205
|
if (ws.readyState === 3) {
|
|
183
206
|
resolve();
|
|
184
207
|
return;
|
|
@@ -188,6 +211,7 @@ export class VoiceAssistantWebSocketServer {
|
|
|
188
211
|
}));
|
|
189
212
|
}
|
|
190
213
|
await Promise.all(cleanupPromises);
|
|
214
|
+
this.pendingConnections.clear();
|
|
191
215
|
this.sessions.clear();
|
|
192
216
|
this.externalSessionsByKey.clear();
|
|
193
217
|
this.wss.close();
|
|
@@ -204,38 +228,20 @@ export class VoiceAssistantWebSocketServer {
|
|
|
204
228
|
}
|
|
205
229
|
ws.send(encodeBinaryMuxFrame(frame));
|
|
206
230
|
}
|
|
207
|
-
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
: null;
|
|
211
|
-
if (externalSessionKey) {
|
|
212
|
-
const existing = this.externalSessionsByKey.get(externalSessionKey);
|
|
213
|
-
if (existing) {
|
|
214
|
-
if (existing.externalDisconnectCleanupTimeout) {
|
|
215
|
-
clearTimeout(existing.externalDisconnectCleanupTimeout);
|
|
216
|
-
existing.externalDisconnectCleanupTimeout = null;
|
|
217
|
-
}
|
|
218
|
-
const previousSocket = existing.socketRef.current;
|
|
219
|
-
if (previousSocket !== ws) {
|
|
220
|
-
this.sessions.delete(previousSocket);
|
|
221
|
-
existing.socketRef.current = ws;
|
|
222
|
-
}
|
|
223
|
-
this.sessions.set(ws, existing);
|
|
224
|
-
this.sendServerInfo(ws);
|
|
225
|
-
existing.connectionLogger.trace({
|
|
226
|
-
clientId: existing.clientId,
|
|
227
|
-
externalSessionKey,
|
|
228
|
-
totalSessions: this.sessions.size,
|
|
229
|
-
}, "Client reconnected");
|
|
230
|
-
this.bindSocketHandlers(ws, existing);
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
231
|
+
sendToConnection(connection, message) {
|
|
232
|
+
for (const ws of connection.sockets) {
|
|
233
|
+
this.sendToClient(ws, message);
|
|
233
234
|
}
|
|
234
|
-
|
|
235
|
+
}
|
|
236
|
+
sendBinaryToConnection(connection, frame) {
|
|
237
|
+
for (const ws of connection.sockets) {
|
|
238
|
+
this.sendBinaryToClient(ws, frame);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
async attachSocket(ws, request, metadata) {
|
|
235
242
|
const requestMetadata = extractSocketRequestMetadata(request);
|
|
236
243
|
const connectionLoggerFields = {
|
|
237
|
-
|
|
238
|
-
transport: externalSessionKey ? "relay" : "direct",
|
|
244
|
+
transport: metadata?.transport === "relay" ? "relay" : "direct",
|
|
239
245
|
};
|
|
240
246
|
if (requestMetadata.host) {
|
|
241
247
|
connectionLoggerFields.host = requestMetadata.host;
|
|
@@ -250,14 +256,48 @@ export class VoiceAssistantWebSocketServer {
|
|
|
250
256
|
connectionLoggerFields.remoteAddress = requestMetadata.remoteAddress;
|
|
251
257
|
}
|
|
252
258
|
const connectionLogger = this.logger.child(connectionLoggerFields);
|
|
253
|
-
const
|
|
259
|
+
const pending = {
|
|
260
|
+
connectionLogger,
|
|
261
|
+
helloTimeout: null,
|
|
262
|
+
};
|
|
263
|
+
const timeout = setTimeout(() => {
|
|
264
|
+
if (this.pendingConnections.get(ws) !== pending) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
pending.helloTimeout = null;
|
|
268
|
+
this.pendingConnections.delete(ws);
|
|
269
|
+
pending.connectionLogger.warn({ timeoutMs: HELLO_TIMEOUT_MS }, "Closing connection due to missing hello");
|
|
270
|
+
try {
|
|
271
|
+
ws.close(WS_CLOSE_HELLO_TIMEOUT, "Hello timeout");
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
// ignore close errors
|
|
275
|
+
}
|
|
276
|
+
}, HELLO_TIMEOUT_MS);
|
|
277
|
+
pending.helloTimeout = timeout;
|
|
278
|
+
timeout.unref?.();
|
|
279
|
+
this.pendingConnections.set(ws, pending);
|
|
280
|
+
this.bindSocketHandlers(ws);
|
|
281
|
+
pending.connectionLogger.trace({
|
|
282
|
+
totalPendingConnections: this.pendingConnections.size,
|
|
283
|
+
}, "Client connected; awaiting hello");
|
|
284
|
+
}
|
|
285
|
+
createSessionConnection(params) {
|
|
286
|
+
const { ws, clientId, connectionLogger } = params;
|
|
287
|
+
let connection = null;
|
|
254
288
|
const session = new Session({
|
|
255
289
|
clientId,
|
|
256
290
|
onMessage: (msg) => {
|
|
257
|
-
|
|
291
|
+
if (!connection) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
this.sendToConnection(connection, wrapSessionMessage(msg));
|
|
258
295
|
},
|
|
259
296
|
onBinaryMessage: (frame) => {
|
|
260
|
-
|
|
297
|
+
if (!connection) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
this.sendBinaryToConnection(connection, frame);
|
|
261
301
|
},
|
|
262
302
|
logger: connectionLogger.child({ module: "session" }),
|
|
263
303
|
downloadTokenStore: this.downloadTokenStore,
|
|
@@ -289,21 +329,96 @@ export class VoiceAssistantWebSocketServer {
|
|
|
289
329
|
dictation: this.dictation ?? undefined,
|
|
290
330
|
agentProviderRuntimeSettings: this.agentProviderRuntimeSettings,
|
|
291
331
|
});
|
|
292
|
-
|
|
332
|
+
connection = {
|
|
293
333
|
session,
|
|
294
334
|
clientId,
|
|
295
335
|
connectionLogger,
|
|
296
|
-
|
|
297
|
-
externalSessionKey,
|
|
336
|
+
sockets: new Set([ws]),
|
|
298
337
|
externalDisconnectCleanupTimeout: null,
|
|
299
338
|
};
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
339
|
+
return connection;
|
|
340
|
+
}
|
|
341
|
+
clearPendingConnection(ws) {
|
|
342
|
+
const pending = this.pendingConnections.get(ws);
|
|
343
|
+
if (!pending) {
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
if (pending.helloTimeout) {
|
|
347
|
+
clearTimeout(pending.helloTimeout);
|
|
348
|
+
pending.helloTimeout = null;
|
|
349
|
+
}
|
|
350
|
+
this.pendingConnections.delete(ws);
|
|
351
|
+
return pending;
|
|
352
|
+
}
|
|
353
|
+
buildWelcomeMessage(params) {
|
|
354
|
+
return {
|
|
355
|
+
type: "welcome",
|
|
356
|
+
serverId: this.serverId,
|
|
357
|
+
hostname: getHostname(),
|
|
358
|
+
version: this.daemonVersion,
|
|
359
|
+
resumed: params.resumed,
|
|
360
|
+
...(this.serverCapabilities ? { capabilities: this.serverCapabilities } : {}),
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
handleHello(params) {
|
|
364
|
+
const { ws, message, pending } = params;
|
|
365
|
+
if (message.protocolVersion !== WS_PROTOCOL_VERSION) {
|
|
366
|
+
this.clearPendingConnection(ws);
|
|
367
|
+
pending.connectionLogger.warn({
|
|
368
|
+
receivedProtocolVersion: message.protocolVersion,
|
|
369
|
+
expectedProtocolVersion: WS_PROTOCOL_VERSION,
|
|
370
|
+
}, "Rejected hello due to protocol version mismatch");
|
|
371
|
+
try {
|
|
372
|
+
ws.close(WS_CLOSE_INCOMPATIBLE_PROTOCOL, "Incompatible protocol version");
|
|
373
|
+
}
|
|
374
|
+
catch {
|
|
375
|
+
// ignore close errors
|
|
376
|
+
}
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
const clientId = message.clientId.trim();
|
|
380
|
+
if (clientId.length === 0) {
|
|
381
|
+
this.clearPendingConnection(ws);
|
|
382
|
+
pending.connectionLogger.warn("Rejected hello with empty clientId");
|
|
383
|
+
try {
|
|
384
|
+
ws.close(WS_CLOSE_INVALID_HELLO, "Invalid hello");
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
// ignore close errors
|
|
388
|
+
}
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
this.clearPendingConnection(ws);
|
|
392
|
+
const existing = this.externalSessionsByKey.get(clientId);
|
|
393
|
+
if (existing) {
|
|
394
|
+
if (existing.externalDisconnectCleanupTimeout) {
|
|
395
|
+
clearTimeout(existing.externalDisconnectCleanupTimeout);
|
|
396
|
+
existing.externalDisconnectCleanupTimeout = null;
|
|
397
|
+
}
|
|
398
|
+
existing.sockets.add(ws);
|
|
399
|
+
this.sessions.set(ws, existing);
|
|
400
|
+
this.sendToClient(ws, this.buildWelcomeMessage({ resumed: true }));
|
|
401
|
+
existing.connectionLogger.trace({
|
|
402
|
+
clientId,
|
|
403
|
+
resumed: true,
|
|
404
|
+
totalSessions: this.sessions.size,
|
|
405
|
+
}, "Client connected via hello");
|
|
406
|
+
return;
|
|
303
407
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
408
|
+
const connectionLogger = pending.connectionLogger.child({ clientId });
|
|
409
|
+
const connection = this.createSessionConnection({
|
|
410
|
+
ws,
|
|
411
|
+
clientId,
|
|
412
|
+
connectionLogger,
|
|
413
|
+
});
|
|
414
|
+
this.sessions.set(ws, connection);
|
|
415
|
+
this.externalSessionsByKey.set(clientId, connection);
|
|
416
|
+
this.sendToClient(ws, this.buildWelcomeMessage({ resumed: false }));
|
|
417
|
+
connection.connectionLogger.trace({
|
|
418
|
+
clientId,
|
|
419
|
+
resumed: false,
|
|
420
|
+
totalSessions: this.sessions.size,
|
|
421
|
+
}, "Client connected via hello");
|
|
307
422
|
}
|
|
308
423
|
buildServerInfoStatusPayload() {
|
|
309
424
|
return {
|
|
@@ -314,33 +429,29 @@ export class VoiceAssistantWebSocketServer {
|
|
|
314
429
|
...(this.serverCapabilities ? { capabilities: this.serverCapabilities } : {}),
|
|
315
430
|
};
|
|
316
431
|
}
|
|
317
|
-
|
|
432
|
+
broadcastCapabilitiesUpdate() {
|
|
318
433
|
this.broadcast(wrapSessionMessage({
|
|
319
434
|
type: "status",
|
|
320
435
|
payload: this.buildServerInfoStatusPayload(),
|
|
321
436
|
}));
|
|
322
437
|
}
|
|
323
|
-
|
|
324
|
-
// Advertise stable server identity immediately on connect (used for URL/shareable IDs).
|
|
325
|
-
this.sendToClient(ws, wrapSessionMessage({
|
|
326
|
-
type: "status",
|
|
327
|
-
payload: this.buildServerInfoStatusPayload(),
|
|
328
|
-
}));
|
|
329
|
-
}
|
|
330
|
-
bindSocketHandlers(ws, connection) {
|
|
438
|
+
bindSocketHandlers(ws) {
|
|
331
439
|
ws.on("message", (data) => {
|
|
332
440
|
void this.handleRawMessage(ws, data);
|
|
333
441
|
});
|
|
334
442
|
ws.on("close", async (code, reason) => {
|
|
335
|
-
await this.detachSocket(ws,
|
|
443
|
+
await this.detachSocket(ws, {
|
|
336
444
|
code: typeof code === "number" ? code : undefined,
|
|
337
445
|
reason,
|
|
338
446
|
});
|
|
339
447
|
});
|
|
340
448
|
ws.on("error", async (error) => {
|
|
341
449
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
342
|
-
|
|
343
|
-
|
|
450
|
+
const active = this.sessions.get(ws);
|
|
451
|
+
const pending = this.pendingConnections.get(ws);
|
|
452
|
+
const log = active?.connectionLogger ?? pending?.connectionLogger ?? this.logger;
|
|
453
|
+
log.error({ err }, "Client error");
|
|
454
|
+
await this.detachSocket(ws, { error: err });
|
|
344
455
|
});
|
|
345
456
|
}
|
|
346
457
|
resolveVoiceSpeakHandler(callerAgentId) {
|
|
@@ -349,13 +460,22 @@ export class VoiceAssistantWebSocketServer {
|
|
|
349
460
|
resolveVoiceCallerContext(callerAgentId) {
|
|
350
461
|
return this.voiceCallerContexts.get(callerAgentId) ?? null;
|
|
351
462
|
}
|
|
352
|
-
async detachSocket(ws,
|
|
353
|
-
const
|
|
354
|
-
if (
|
|
463
|
+
async detachSocket(ws, details) {
|
|
464
|
+
const pending = this.clearPendingConnection(ws);
|
|
465
|
+
if (pending) {
|
|
466
|
+
pending.connectionLogger.trace({
|
|
467
|
+
code: details.code,
|
|
468
|
+
reason: stringifyCloseReason(details.reason),
|
|
469
|
+
}, "Pending client disconnected");
|
|
355
470
|
return;
|
|
471
|
+
}
|
|
472
|
+
const connection = this.sessions.get(ws);
|
|
473
|
+
if (!connection) {
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
356
476
|
this.sessions.delete(ws);
|
|
357
|
-
|
|
358
|
-
|
|
477
|
+
connection.sockets.delete(ws);
|
|
478
|
+
if (connection.sockets.size === 0) {
|
|
359
479
|
if (connection.externalDisconnectCleanupTimeout) {
|
|
360
480
|
clearTimeout(connection.externalDisconnectCleanupTimeout);
|
|
361
481
|
}
|
|
@@ -369,13 +489,21 @@ export class VoiceAssistantWebSocketServer {
|
|
|
369
489
|
connection.externalDisconnectCleanupTimeout = timeout;
|
|
370
490
|
connection.connectionLogger.trace({
|
|
371
491
|
clientId: connection.clientId,
|
|
372
|
-
externalSessionKey: connection.externalSessionKey,
|
|
373
492
|
code: details.code,
|
|
374
493
|
reason: stringifyCloseReason(details.reason),
|
|
375
494
|
reconnectGraceMs: EXTERNAL_SESSION_DISCONNECT_GRACE_MS,
|
|
376
495
|
}, "Client disconnected; waiting for reconnect");
|
|
377
496
|
return;
|
|
378
497
|
}
|
|
498
|
+
if (connection.sockets.size > 0) {
|
|
499
|
+
connection.connectionLogger.trace({
|
|
500
|
+
clientId: connection.clientId,
|
|
501
|
+
remainingSockets: connection.sockets.size,
|
|
502
|
+
code: details.code,
|
|
503
|
+
reason: stringifyCloseReason(details.reason),
|
|
504
|
+
}, "Client socket disconnected; session remains attached");
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
379
507
|
await this.cleanupConnection(connection, "Client disconnected");
|
|
380
508
|
}
|
|
381
509
|
async cleanupConnection(connection, logMessage) {
|
|
@@ -383,27 +511,36 @@ export class VoiceAssistantWebSocketServer {
|
|
|
383
511
|
clearTimeout(connection.externalDisconnectCleanupTimeout);
|
|
384
512
|
connection.externalDisconnectCleanupTimeout = null;
|
|
385
513
|
}
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
514
|
+
for (const socket of connection.sockets) {
|
|
515
|
+
this.sessions.delete(socket);
|
|
516
|
+
}
|
|
517
|
+
connection.sockets.clear();
|
|
518
|
+
const existing = this.externalSessionsByKey.get(connection.clientId);
|
|
519
|
+
if (existing === connection) {
|
|
520
|
+
this.externalSessionsByKey.delete(connection.clientId);
|
|
393
521
|
}
|
|
394
522
|
connection.connectionLogger.trace({ clientId: connection.clientId, totalSessions: this.sessions.size }, logMessage);
|
|
395
523
|
await connection.session.cleanup();
|
|
396
524
|
}
|
|
397
525
|
async handleRawMessage(ws, data) {
|
|
526
|
+
const activeConnection = this.sessions.get(ws);
|
|
527
|
+
const pendingConnection = this.pendingConnections.get(ws);
|
|
528
|
+
const log = activeConnection?.connectionLogger ?? pendingConnection?.connectionLogger ?? this.logger;
|
|
398
529
|
try {
|
|
399
|
-
const activeConnection = this.sessions.get(ws);
|
|
400
530
|
const buffer = bufferFromWsData(data);
|
|
401
531
|
const asBytes = asUint8Array(buffer);
|
|
402
532
|
if (asBytes) {
|
|
403
533
|
const frame = decodeBinaryMuxFrame(asBytes);
|
|
404
534
|
if (frame) {
|
|
405
535
|
if (!activeConnection) {
|
|
406
|
-
|
|
536
|
+
log.warn("Rejected binary frame before hello");
|
|
537
|
+
this.clearPendingConnection(ws);
|
|
538
|
+
try {
|
|
539
|
+
ws.close(WS_CLOSE_INVALID_HELLO, "Session message before hello");
|
|
540
|
+
}
|
|
541
|
+
catch {
|
|
542
|
+
// ignore close errors
|
|
543
|
+
}
|
|
407
544
|
return;
|
|
408
545
|
}
|
|
409
546
|
activeConnection.session.handleBinaryFrame(frame);
|
|
@@ -413,13 +550,25 @@ export class VoiceAssistantWebSocketServer {
|
|
|
413
550
|
const parsed = JSON.parse(buffer.toString());
|
|
414
551
|
const parsedMessage = WSInboundMessageSchema.safeParse(parsed);
|
|
415
552
|
if (!parsedMessage.success) {
|
|
553
|
+
if (pendingConnection) {
|
|
554
|
+
pendingConnection.connectionLogger.warn({
|
|
555
|
+
error: parsedMessage.error.message,
|
|
556
|
+
}, "Rejected pending message before hello");
|
|
557
|
+
this.clearPendingConnection(ws);
|
|
558
|
+
try {
|
|
559
|
+
ws.close(WS_CLOSE_INVALID_HELLO, "Invalid hello");
|
|
560
|
+
}
|
|
561
|
+
catch {
|
|
562
|
+
// ignore close errors
|
|
563
|
+
}
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
416
566
|
const requestInfo = extractRequestInfoFromUnknownWsInbound(parsed);
|
|
417
567
|
const isUnknownSchema = requestInfo?.requestId != null &&
|
|
418
568
|
typeof parsed === "object" &&
|
|
419
569
|
parsed != null &&
|
|
420
570
|
"type" in parsed &&
|
|
421
571
|
parsed.type === "session";
|
|
422
|
-
const log = activeConnection?.connectionLogger ?? this.logger;
|
|
423
572
|
log.warn({
|
|
424
573
|
clientId: activeConnection?.clientId,
|
|
425
574
|
requestId: requestInfo?.requestId,
|
|
@@ -456,8 +605,39 @@ export class VoiceAssistantWebSocketServer {
|
|
|
456
605
|
if (message.type === "recording_state") {
|
|
457
606
|
return;
|
|
458
607
|
}
|
|
608
|
+
if (pendingConnection) {
|
|
609
|
+
if (message.type === "hello") {
|
|
610
|
+
this.handleHello({
|
|
611
|
+
ws,
|
|
612
|
+
message,
|
|
613
|
+
pending: pendingConnection,
|
|
614
|
+
});
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
pendingConnection.connectionLogger.warn({
|
|
618
|
+
messageType: message.type,
|
|
619
|
+
}, "Rejected pending message before hello");
|
|
620
|
+
this.clearPendingConnection(ws);
|
|
621
|
+
try {
|
|
622
|
+
ws.close(WS_CLOSE_INVALID_HELLO, "Session message before hello");
|
|
623
|
+
}
|
|
624
|
+
catch {
|
|
625
|
+
// ignore close errors
|
|
626
|
+
}
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
459
629
|
if (!activeConnection) {
|
|
460
|
-
this.logger.error("No
|
|
630
|
+
this.logger.error("No connection found for websocket");
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
if (message.type === "hello") {
|
|
634
|
+
activeConnection.connectionLogger.warn("Received hello on active connection");
|
|
635
|
+
try {
|
|
636
|
+
ws.close(WS_CLOSE_INVALID_HELLO, "Unexpected hello");
|
|
637
|
+
}
|
|
638
|
+
catch {
|
|
639
|
+
// ignore close errors
|
|
640
|
+
}
|
|
461
641
|
return;
|
|
462
642
|
}
|
|
463
643
|
if (message.type === "session") {
|
|
@@ -482,11 +662,21 @@ export class VoiceAssistantWebSocketServer {
|
|
|
482
662
|
const trimmedRawPayload = typeof rawPayload === "string" && rawPayload.length > 2000
|
|
483
663
|
? `${rawPayload.slice(0, 2000)}... (truncated)`
|
|
484
664
|
: rawPayload;
|
|
485
|
-
|
|
665
|
+
log.error({
|
|
486
666
|
err,
|
|
487
667
|
rawPayload: trimmedRawPayload,
|
|
488
668
|
parsedPayload,
|
|
489
669
|
}, "Failed to parse/handle message");
|
|
670
|
+
if (this.pendingConnections.has(ws)) {
|
|
671
|
+
this.clearPendingConnection(ws);
|
|
672
|
+
try {
|
|
673
|
+
ws.close(WS_CLOSE_INVALID_HELLO, "Invalid hello");
|
|
674
|
+
}
|
|
675
|
+
catch {
|
|
676
|
+
// ignore close errors
|
|
677
|
+
}
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
490
680
|
const requestInfo = extractRequestInfoFromUnknownWsInbound(parsedPayload);
|
|
491
681
|
if (requestInfo) {
|
|
492
682
|
this.sendToClient(ws, wrapSessionMessage({
|
|
@@ -534,6 +724,13 @@ export class VoiceAssistantWebSocketServer {
|
|
|
534
724
|
}
|
|
535
725
|
const allStates = clientEntries.map((e) => e.state);
|
|
536
726
|
const agent = this.agentManager.getAgent(params.agentId);
|
|
727
|
+
if (agent?.labels?.ui !== "true") {
|
|
728
|
+
this.logger.debug({
|
|
729
|
+
agentId: params.agentId,
|
|
730
|
+
labels: agent?.labels ?? null,
|
|
731
|
+
}, "Skipping attention notification for non-UI agent");
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
537
734
|
const notification = buildAgentAttentionNotificationPayload({
|
|
538
735
|
reason: params.reason,
|
|
539
736
|
serverId: this.serverId,
|