@getpaseo/server 0.1.2 → 0.1.4
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-relay-e2ee-transport.d.ts +8 -0
- package/dist/server/client/daemon-client-relay-e2ee-transport.d.ts.map +1 -0
- package/dist/server/client/daemon-client-relay-e2ee-transport.js +161 -0
- package/dist/server/client/daemon-client-relay-e2ee-transport.js.map +1 -0
- package/dist/server/client/daemon-client-terminal-stream-manager.d.ts +43 -0
- package/dist/server/client/daemon-client-terminal-stream-manager.d.ts.map +1 -0
- package/dist/server/client/daemon-client-terminal-stream-manager.js +130 -0
- package/dist/server/client/daemon-client-terminal-stream-manager.js.map +1 -0
- package/dist/server/client/daemon-client-transport-types.d.ts +34 -0
- package/dist/server/client/daemon-client-transport-types.d.ts.map +1 -0
- package/dist/server/client/daemon-client-transport-types.js +2 -0
- package/dist/server/client/daemon-client-transport-types.js.map +1 -0
- package/dist/server/client/daemon-client-transport-utils.d.ts +9 -0
- package/dist/server/client/daemon-client-transport-utils.d.ts.map +1 -0
- package/dist/server/client/daemon-client-transport-utils.js +121 -0
- package/dist/server/client/daemon-client-transport-utils.js.map +1 -0
- package/dist/server/client/daemon-client-transport.d.ts +5 -0
- package/dist/server/client/daemon-client-transport.d.ts.map +1 -0
- package/dist/server/client/daemon-client-transport.js +4 -0
- package/dist/server/client/daemon-client-transport.js.map +1 -0
- package/dist/server/client/daemon-client-websocket-transport.d.ts +7 -0
- package/dist/server/client/daemon-client-websocket-transport.d.ts.map +1 -0
- package/dist/server/client/daemon-client-websocket-transport.js +64 -0
- package/dist/server/client/daemon-client-websocket-transport.js.map +1 -0
- package/dist/server/client/daemon-client.d.ts +54 -36
- package/dist/server/client/daemon-client.d.ts.map +1 -1
- package/dist/server/client/daemon-client.js +497 -800
- package/dist/server/client/daemon-client.js.map +1 -1
- package/dist/server/server/agent/agent-manager.d.ts +52 -0
- package/dist/server/server/agent/agent-manager.d.ts.map +1 -1
- package/dist/server/server/agent/agent-manager.js +238 -10
- package/dist/server/server/agent/agent-manager.js.map +1 -1
- package/dist/server/server/agent/agent-storage.d.ts +4 -4
- 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 +62 -12
- 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 +4 -4
- 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 +213 -50
- 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 -0
- package/dist/server/server/agent/providers/claude-agent.d.ts.map +1 -1
- package/dist/server/server/agent/providers/claude-agent.js +100 -19
- package/dist/server/server/agent/providers/claude-agent.js.map +1 -1
- package/dist/server/server/agent/providers/codex/tool-call-detail-parser.d.ts +0 -1
- package/dist/server/server/agent/providers/codex/tool-call-detail-parser.d.ts.map +1 -1
- package/dist/server/server/agent/providers/codex/tool-call-detail-parser.js +67 -30
- package/dist/server/server/agent/providers/codex/tool-call-detail-parser.js.map +1 -1
- package/dist/server/server/agent/providers/codex/tool-call-mapper.d.ts +1 -1
- package/dist/server/server/agent/providers/codex/tool-call-mapper.d.ts.map +1 -1
- package/dist/server/server/agent/providers/codex/tool-call-mapper.js +293 -266
- package/dist/server/server/agent/providers/codex/tool-call-mapper.js.map +1 -1
- package/dist/server/server/agent/providers/codex-app-server-agent.d.ts +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 +49 -14
- package/dist/server/server/agent/providers/codex-app-server-agent.js.map +1 -1
- package/dist/server/server/agent/providers/codex-rollout-timeline.d.ts.map +1 -1
- package/dist/server/server/agent/providers/codex-rollout-timeline.js +5 -4
- package/dist/server/server/agent/providers/codex-rollout-timeline.js.map +1 -1
- package/dist/server/server/agent/providers/opencode/tool-call-detail-parser.d.ts.map +1 -1
- package/dist/server/server/agent/providers/opencode/tool-call-detail-parser.js +8 -2
- package/dist/server/server/agent/providers/opencode/tool-call-detail-parser.js.map +1 -1
- package/dist/server/server/agent/providers/opencode/tool-call-mapper.d.ts +1 -1
- package/dist/server/server/agent/providers/opencode/tool-call-mapper.d.ts.map +1 -1
- package/dist/server/server/agent/providers/opencode/tool-call-mapper.js +121 -45
- package/dist/server/server/agent/providers/opencode/tool-call-mapper.js.map +1 -1
- package/dist/server/server/agent/providers/opencode-agent.d.ts.map +1 -1
- package/dist/server/server/agent/providers/opencode-agent.js +87 -35
- package/dist/server/server/agent/providers/opencode-agent.js.map +1 -1
- package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts +2 -2
- 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 +23 -6
- package/dist/server/server/agent/providers/tool-call-detail-primitives.js.map +1 -1
- package/dist/server/server/agent/providers/tool-call-mapper-utils.d.ts +0 -1
- package/dist/server/server/agent/providers/tool-call-mapper-utils.d.ts.map +1 -1
- package/dist/server/server/agent/providers/tool-call-mapper-utils.js +0 -10
- package/dist/server/server/agent/providers/tool-call-mapper-utils.js.map +1 -1
- package/dist/server/server/agent/stt-manager.d.ts +3 -2
- package/dist/server/server/agent/stt-manager.d.ts.map +1 -1
- package/dist/server/server/agent/stt-manager.js +5 -3
- package/dist/server/server/agent/stt-manager.js.map +1 -1
- package/dist/server/server/agent/timeline-projection.d.ts +19 -0
- package/dist/server/server/agent/timeline-projection.d.ts.map +1 -0
- package/dist/server/server/agent/timeline-projection.js +142 -0
- package/dist/server/server/agent/timeline-projection.js.map +1 -0
- package/dist/server/server/agent/tts-manager.d.ts +3 -2
- package/dist/server/server/agent/tts-manager.d.ts.map +1 -1
- package/dist/server/server/agent/tts-manager.js +5 -17
- package/dist/server/server/agent/tts-manager.js.map +1 -1
- package/dist/server/server/agent-attention-policy.d.ts +20 -0
- package/dist/server/server/agent-attention-policy.d.ts.map +1 -0
- package/dist/server/server/agent-attention-policy.js +40 -0
- package/dist/server/server/agent-attention-policy.js.map +1 -0
- package/dist/server/server/bootstrap.d.ts.map +1 -1
- package/dist/server/server/bootstrap.js +13 -20
- package/dist/server/server/bootstrap.js.map +1 -1
- package/dist/server/server/dictation/dictation-stream-manager.d.ts +10 -2
- package/dist/server/server/dictation/dictation-stream-manager.d.ts.map +1 -1
- package/dist/server/server/dictation/dictation-stream-manager.js +83 -27
- package/dist/server/server/dictation/dictation-stream-manager.js.map +1 -1
- package/dist/server/server/exports.d.ts +2 -1
- package/dist/server/server/exports.d.ts.map +1 -1
- package/dist/server/server/exports.js +1 -0
- package/dist/server/server/exports.js.map +1 -1
- package/dist/server/server/persisted-config.d.ts +36 -22
- package/dist/server/server/persisted-config.d.ts.map +1 -1
- package/dist/server/server/persisted-config.js +2 -0
- package/dist/server/server/persisted-config.js.map +1 -1
- package/dist/server/server/relay-transport.d.ts +3 -2
- package/dist/server/server/relay-transport.d.ts.map +1 -1
- package/dist/server/server/relay-transport.js +99 -16
- package/dist/server/server/relay-transport.js.map +1 -1
- package/dist/server/server/session.d.ts +34 -16
- package/dist/server/server/session.d.ts.map +1 -1
- package/dist/server/server/session.js +619 -328
- package/dist/server/server/session.js.map +1 -1
- package/dist/server/server/speech/provider-resolver.d.ts +3 -0
- package/dist/server/server/speech/provider-resolver.d.ts.map +1 -0
- package/dist/server/server/speech/provider-resolver.js +7 -0
- package/dist/server/server/speech/provider-resolver.js.map +1 -0
- package/dist/server/server/speech/providers/local/config.d.ts.map +1 -1
- package/dist/server/server/speech/providers/local/config.js +11 -8
- package/dist/server/server/speech/providers/local/config.js.map +1 -1
- package/dist/server/server/speech/providers/local/runtime.d.ts +1 -0
- package/dist/server/server/speech/providers/local/runtime.d.ts.map +1 -1
- package/dist/server/server/speech/providers/local/runtime.js +13 -9
- package/dist/server/server/speech/providers/local/runtime.js.map +1 -1
- package/dist/server/server/speech/providers/local/sherpa/model-downloader.d.ts.map +1 -1
- package/dist/server/server/speech/providers/local/sherpa/model-downloader.js +73 -54
- package/dist/server/server/speech/providers/local/sherpa/model-downloader.js.map +1 -1
- package/dist/server/server/speech/providers/openai/config.d.ts.map +1 -1
- package/dist/server/server/speech/providers/openai/config.js +10 -5
- package/dist/server/server/speech/providers/openai/config.js.map +1 -1
- package/dist/server/server/speech/providers/openai/runtime.d.ts.map +1 -1
- package/dist/server/server/speech/providers/openai/runtime.js +17 -6
- package/dist/server/server/speech/providers/openai/runtime.js.map +1 -1
- package/dist/server/server/speech/speech-config-resolver.d.ts.map +1 -1
- package/dist/server/server/speech/speech-config-resolver.js +25 -4
- package/dist/server/server/speech/speech-config-resolver.js.map +1 -1
- package/dist/server/server/speech/speech-runtime.d.ts +26 -3
- package/dist/server/server/speech/speech-runtime.d.ts.map +1 -1
- package/dist/server/server/speech/speech-runtime.js +468 -85
- package/dist/server/server/speech/speech-runtime.js.map +1 -1
- package/dist/server/server/speech/speech-types.d.ts +3 -0
- package/dist/server/server/speech/speech-types.d.ts.map +1 -1
- package/dist/server/server/speech/speech-types.js +1 -0
- package/dist/server/server/speech/speech-types.js.map +1 -1
- package/dist/server/server/websocket-server.d.ts +23 -7
- package/dist/server/server/websocket-server.d.ts.map +1 -1
- package/dist/server/server/websocket-server.js +288 -102
- package/dist/server/server/websocket-server.js.map +1 -1
- package/dist/server/shared/binary-mux.d.ts +31 -0
- package/dist/server/shared/binary-mux.d.ts.map +1 -0
- package/dist/server/shared/binary-mux.js +101 -0
- package/dist/server/shared/binary-mux.js.map +1 -0
- package/dist/server/shared/messages.d.ts +5206 -4814
- package/dist/server/shared/messages.d.ts.map +1 -1
- package/dist/server/shared/messages.js +225 -58
- package/dist/server/shared/messages.js.map +1 -1
- package/dist/server/shared/terminal-key-input.d.ts +9 -0
- package/dist/server/shared/terminal-key-input.d.ts.map +1 -0
- package/dist/server/shared/terminal-key-input.js +132 -0
- package/dist/server/shared/terminal-key-input.js.map +1 -0
- package/dist/server/terminal/terminal.d.ts +17 -0
- package/dist/server/terminal/terminal.d.ts.map +1 -1
- package/dist/server/terminal/terminal.js +89 -0
- package/dist/server/terminal/terminal.js.map +1 -1
- package/dist/server/utils/checkout-git.d.ts +9 -1
- package/dist/server/utils/checkout-git.d.ts.map +1 -1
- package/dist/server/utils/checkout-git.js +314 -75
- package/dist/server/utils/checkout-git.js.map +1 -1
- package/dist/server/utils/worktree.d.ts.map +1 -1
- package/dist/server/utils/worktree.js +33 -4
- package/dist/server/utils/worktree.js.map +1 -1
- package/package.json +2 -2
|
@@ -5,20 +5,26 @@ import type { AgentStorage } from "./agent/agent-storage.js";
|
|
|
5
5
|
import type { DownloadTokenStore } from "./file-download/token-store.js";
|
|
6
6
|
import type { TerminalManager } from "../terminal/terminal-manager.js";
|
|
7
7
|
import type pino from "pino";
|
|
8
|
-
import { type WSOutboundMessage } from "./messages.js";
|
|
8
|
+
import { type ServerCapabilities, type WSOutboundMessage } from "./messages.js";
|
|
9
9
|
import type { AllowedHostsConfig } from "./allowed-hosts.js";
|
|
10
10
|
import type { AgentProviderRuntimeSettingsMap } from "./agent/provider-launch-config.js";
|
|
11
11
|
import type { SpeechToTextProvider, TextToSpeechProvider } from "./speech/speech-provider.js";
|
|
12
|
+
import type { Resolvable } from "./speech/provider-resolver.js";
|
|
13
|
+
import type { SpeechReadinessSnapshot } from "./speech/speech-runtime.js";
|
|
12
14
|
import type { LocalSpeechModelId } from "./speech/providers/local/models.js";
|
|
13
15
|
import type { VoiceCallerContext, VoiceMcpStdioConfig, VoiceSpeakHandler } from "./voice-types.js";
|
|
14
16
|
export type AgentMcpTransportFactory = () => Promise<Transport>;
|
|
17
|
+
export type ExternalSocketMetadata = {
|
|
18
|
+
transport: "relay";
|
|
19
|
+
externalSessionKey: string;
|
|
20
|
+
};
|
|
15
21
|
type WebSocketServerConfig = {
|
|
16
22
|
allowedOrigins: Set<string>;
|
|
17
23
|
allowedHosts?: AllowedHostsConfig;
|
|
18
24
|
};
|
|
19
25
|
type WebSocketLike = {
|
|
20
26
|
readyState: number;
|
|
21
|
-
send: (data: string) => void;
|
|
27
|
+
send: (data: string | Uint8Array | ArrayBuffer) => void;
|
|
22
28
|
close: (code?: number, reason?: string) => void;
|
|
23
29
|
on: (event: "message" | "close" | "error", listener: (...args: any[]) => void) => void;
|
|
24
30
|
once: (event: "close" | "error", listener: (...args: any[]) => void) => void;
|
|
@@ -30,6 +36,7 @@ export declare class VoiceAssistantWebSocketServer {
|
|
|
30
36
|
private readonly logger;
|
|
31
37
|
private readonly wss;
|
|
32
38
|
private readonly sessions;
|
|
39
|
+
private readonly externalSessionsByKey;
|
|
33
40
|
private clientIdCounter;
|
|
34
41
|
private readonly serverId;
|
|
35
42
|
private readonly agentManager;
|
|
@@ -47,33 +54,42 @@ export declare class VoiceAssistantWebSocketServer {
|
|
|
47
54
|
private readonly voiceSpeakHandlers;
|
|
48
55
|
private readonly voiceCallerContexts;
|
|
49
56
|
private readonly agentProviderRuntimeSettings;
|
|
57
|
+
private serverCapabilities;
|
|
50
58
|
constructor(server: HTTPServer, logger: pino.Logger, serverId: string, agentManager: AgentManager, agentStorage: AgentStorage, downloadTokenStore: DownloadTokenStore, paseoHome: string, createAgentMcpTransport: AgentMcpTransportFactory, wsConfig: WebSocketServerConfig, speech?: {
|
|
51
|
-
stt: SpeechToTextProvider | null
|
|
52
|
-
tts: TextToSpeechProvider | null
|
|
59
|
+
stt: Resolvable<SpeechToTextProvider | null>;
|
|
60
|
+
tts: Resolvable<TextToSpeechProvider | null>;
|
|
53
61
|
}, terminalManager?: TerminalManager | null, voice?: {
|
|
54
62
|
voiceAgentMcpStdio?: VoiceMcpStdioConfig | null;
|
|
55
63
|
ensureVoiceMcpSocketForAgent?: (agentId: string) => Promise<string>;
|
|
56
64
|
removeVoiceMcpSocketForAgent?: (agentId: string) => Promise<void>;
|
|
57
65
|
}, dictation?: {
|
|
58
66
|
finalTimeoutMs?: number;
|
|
59
|
-
stt?: SpeechToTextProvider | null
|
|
67
|
+
stt?: Resolvable<SpeechToTextProvider | null>;
|
|
60
68
|
localModels?: {
|
|
61
69
|
modelsDir: string;
|
|
62
70
|
defaultModelIds: LocalSpeechModelId[];
|
|
63
71
|
};
|
|
72
|
+
getSpeechReadiness?: () => SpeechReadinessSnapshot;
|
|
64
73
|
}, agentProviderRuntimeSettings?: AgentProviderRuntimeSettingsMap);
|
|
65
74
|
broadcast(message: WSOutboundMessage): void;
|
|
66
|
-
|
|
75
|
+
publishSpeechReadiness(readiness: SpeechReadinessSnapshot | null): void;
|
|
76
|
+
updateServerCapabilities(capabilities: ServerCapabilities | null | undefined): void;
|
|
77
|
+
attachExternalSocket(ws: WebSocketLike, metadata?: ExternalSocketMetadata): Promise<void>;
|
|
67
78
|
close(): Promise<void>;
|
|
68
79
|
private sendToClient;
|
|
80
|
+
private sendBinaryToClient;
|
|
69
81
|
private attachSocket;
|
|
82
|
+
private buildServerInfoStatusPayload;
|
|
83
|
+
private broadcastServerInfo;
|
|
84
|
+
private sendServerInfo;
|
|
85
|
+
private bindSocketHandlers;
|
|
70
86
|
resolveVoiceSpeakHandler(callerAgentId: string): VoiceSpeakHandler | null;
|
|
71
87
|
resolveVoiceCallerContext(callerAgentId: string): VoiceCallerContext | null;
|
|
72
88
|
private detachSocket;
|
|
89
|
+
private cleanupConnection;
|
|
73
90
|
private handleRawMessage;
|
|
74
91
|
private readonly ACTIVITY_THRESHOLD_MS;
|
|
75
92
|
private getClientActivityState;
|
|
76
|
-
private computeShouldNotifyForClient;
|
|
77
93
|
private broadcastAgentAttention;
|
|
78
94
|
}
|
|
79
95
|
export {};
|
|
@@ -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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"}
|
|
@@ -2,10 +2,60 @@ import { WebSocketServer } from "ws";
|
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
import { hostname as getHostname } from "node:os";
|
|
4
4
|
import { WSInboundMessageSchema, wrapSessionMessage, } from "./messages.js";
|
|
5
|
+
import { asUint8Array, decodeBinaryMuxFrame, encodeBinaryMuxFrame, } from "../shared/binary-mux.js";
|
|
5
6
|
import { isHostAllowed } from "./allowed-hosts.js";
|
|
6
7
|
import { Session } from "./session.js";
|
|
7
8
|
import { PushTokenStore } from "./push/token-store.js";
|
|
8
9
|
import { PushService } from "./push/push-service.js";
|
|
10
|
+
import { computeShouldNotifyClient, computeShouldSendPush, } from "./agent-attention-policy.js";
|
|
11
|
+
function toServerCapabilityState(params) {
|
|
12
|
+
const { state, reason } = params;
|
|
13
|
+
return {
|
|
14
|
+
enabled: state.enabled,
|
|
15
|
+
reason,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function resolveCapabilityReason(params) {
|
|
19
|
+
const { state, readiness } = params;
|
|
20
|
+
if (state.available) {
|
|
21
|
+
return "";
|
|
22
|
+
}
|
|
23
|
+
if (readiness.voiceFeature.reasonCode === "model_download_in_progress") {
|
|
24
|
+
const baseMessage = readiness.voiceFeature.message.trim();
|
|
25
|
+
if (baseMessage.includes("Try again in a few minutes")) {
|
|
26
|
+
return baseMessage;
|
|
27
|
+
}
|
|
28
|
+
return `${baseMessage} Try again in a few minutes.`;
|
|
29
|
+
}
|
|
30
|
+
return state.message;
|
|
31
|
+
}
|
|
32
|
+
function buildServerCapabilities(params) {
|
|
33
|
+
const readiness = params.readiness;
|
|
34
|
+
if (!readiness) {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
voice: {
|
|
39
|
+
dictation: toServerCapabilityState({
|
|
40
|
+
state: readiness.dictation,
|
|
41
|
+
reason: resolveCapabilityReason({
|
|
42
|
+
state: readiness.dictation,
|
|
43
|
+
readiness,
|
|
44
|
+
}),
|
|
45
|
+
}),
|
|
46
|
+
voice: toServerCapabilityState({
|
|
47
|
+
state: readiness.realtimeVoice,
|
|
48
|
+
reason: resolveCapabilityReason({
|
|
49
|
+
state: readiness.realtimeVoice,
|
|
50
|
+
readiness,
|
|
51
|
+
}),
|
|
52
|
+
}),
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function areServerCapabilitiesEqual(current, next) {
|
|
57
|
+
return JSON.stringify(current ?? null) === JSON.stringify(next ?? null);
|
|
58
|
+
}
|
|
9
59
|
function bufferFromWsData(data) {
|
|
10
60
|
if (typeof data === "string")
|
|
11
61
|
return Buffer.from(data, "utf8");
|
|
@@ -16,12 +66,14 @@ function bufferFromWsData(data) {
|
|
|
16
66
|
return data;
|
|
17
67
|
return Buffer.from(data);
|
|
18
68
|
}
|
|
69
|
+
const EXTERNAL_SESSION_DISCONNECT_GRACE_MS = 90000;
|
|
19
70
|
/**
|
|
20
71
|
* WebSocket server that only accepts sockets + parses/forwards messages to the session layer.
|
|
21
72
|
*/
|
|
22
73
|
export class VoiceAssistantWebSocketServer {
|
|
23
74
|
constructor(server, logger, serverId, agentManager, agentStorage, downloadTokenStore, paseoHome, createAgentMcpTransport, wsConfig, speech, terminalManager, voice, dictation, agentProviderRuntimeSettings) {
|
|
24
75
|
this.sessions = new Map();
|
|
76
|
+
this.externalSessionsByKey = new Map();
|
|
25
77
|
this.clientIdCounter = 0;
|
|
26
78
|
this.voiceSpeakHandlers = new Map();
|
|
27
79
|
this.voiceCallerContexts = new Map();
|
|
@@ -39,6 +91,9 @@ export class VoiceAssistantWebSocketServer {
|
|
|
39
91
|
this.voice = voice ?? null;
|
|
40
92
|
this.dictation = dictation ?? null;
|
|
41
93
|
this.agentProviderRuntimeSettings = agentProviderRuntimeSettings;
|
|
94
|
+
this.serverCapabilities = buildServerCapabilities({
|
|
95
|
+
readiness: this.dictation?.getSpeechReadiness?.() ?? null,
|
|
96
|
+
});
|
|
42
97
|
const pushLogger = this.logger.child({ module: "push" });
|
|
43
98
|
this.pushTokenStore = new PushTokenStore(pushLogger, join(paseoHome, "push-tokens.json"));
|
|
44
99
|
this.pushService = new PushService(pushLogger, this.pushTokenStore);
|
|
@@ -50,10 +105,11 @@ export class VoiceAssistantWebSocketServer {
|
|
|
50
105
|
server,
|
|
51
106
|
path: "/ws",
|
|
52
107
|
verifyClient: ({ req }, callback) => {
|
|
53
|
-
const
|
|
54
|
-
const
|
|
108
|
+
const requestMetadata = extractSocketRequestMetadata(req);
|
|
109
|
+
const origin = requestMetadata.origin;
|
|
110
|
+
const requestHost = requestMetadata.host ?? null;
|
|
55
111
|
if (requestHost && !isHostAllowed(requestHost, allowedHosts)) {
|
|
56
|
-
this.logger.warn({ host: requestHost }, "Rejected connection from disallowed host");
|
|
112
|
+
this.logger.warn({ ...requestMetadata, host: requestHost }, "Rejected connection from disallowed host");
|
|
57
113
|
callback(false, 403, "Host not allowed");
|
|
58
114
|
return;
|
|
59
115
|
}
|
|
@@ -64,7 +120,7 @@ export class VoiceAssistantWebSocketServer {
|
|
|
64
120
|
callback(true);
|
|
65
121
|
}
|
|
66
122
|
else {
|
|
67
|
-
this.logger.warn({ origin }, "Rejected connection from origin");
|
|
123
|
+
this.logger.warn({ ...requestMetadata, origin }, "Rejected connection from origin");
|
|
68
124
|
callback(false, 403, "Origin not allowed");
|
|
69
125
|
}
|
|
70
126
|
},
|
|
@@ -83,13 +139,33 @@ export class VoiceAssistantWebSocketServer {
|
|
|
83
139
|
}
|
|
84
140
|
}
|
|
85
141
|
}
|
|
86
|
-
|
|
87
|
-
|
|
142
|
+
publishSpeechReadiness(readiness) {
|
|
143
|
+
this.updateServerCapabilities(buildServerCapabilities({ readiness }));
|
|
144
|
+
}
|
|
145
|
+
updateServerCapabilities(capabilities) {
|
|
146
|
+
const next = capabilities ?? undefined;
|
|
147
|
+
if (areServerCapabilitiesEqual(this.serverCapabilities, next)) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
this.serverCapabilities = next;
|
|
151
|
+
this.broadcastServerInfo();
|
|
152
|
+
}
|
|
153
|
+
async attachExternalSocket(ws, metadata) {
|
|
154
|
+
await this.attachSocket(ws, undefined, metadata);
|
|
88
155
|
}
|
|
89
156
|
async close() {
|
|
157
|
+
const uniqueConnections = new Set([
|
|
158
|
+
...this.sessions.values(),
|
|
159
|
+
...this.externalSessionsByKey.values(),
|
|
160
|
+
]);
|
|
90
161
|
const cleanupPromises = [];
|
|
91
|
-
for (const
|
|
92
|
-
|
|
162
|
+
for (const connection of uniqueConnections) {
|
|
163
|
+
if (connection.externalDisconnectCleanupTimeout) {
|
|
164
|
+
clearTimeout(connection.externalDisconnectCleanupTimeout);
|
|
165
|
+
connection.externalDisconnectCleanupTimeout = null;
|
|
166
|
+
}
|
|
167
|
+
const ws = connection.socketRef.current;
|
|
168
|
+
cleanupPromises.push(connection.session.cleanup());
|
|
93
169
|
cleanupPromises.push(new Promise((resolve) => {
|
|
94
170
|
// WebSocket.CLOSED = 3
|
|
95
171
|
if (ws.readyState === 3) {
|
|
@@ -102,6 +178,7 @@ export class VoiceAssistantWebSocketServer {
|
|
|
102
178
|
}
|
|
103
179
|
await Promise.all(cleanupPromises);
|
|
104
180
|
this.sessions.clear();
|
|
181
|
+
this.externalSessionsByKey.clear();
|
|
105
182
|
this.wss.close();
|
|
106
183
|
}
|
|
107
184
|
sendToClient(ws, message) {
|
|
@@ -110,13 +187,66 @@ export class VoiceAssistantWebSocketServer {
|
|
|
110
187
|
ws.send(JSON.stringify(message));
|
|
111
188
|
}
|
|
112
189
|
}
|
|
113
|
-
|
|
190
|
+
sendBinaryToClient(ws, frame) {
|
|
191
|
+
if (ws.readyState !== 1) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
ws.send(encodeBinaryMuxFrame(frame));
|
|
195
|
+
}
|
|
196
|
+
async attachSocket(ws, request, metadata) {
|
|
197
|
+
const externalSessionKey = metadata?.transport === "relay" && metadata.externalSessionKey.trim().length > 0
|
|
198
|
+
? metadata.externalSessionKey
|
|
199
|
+
: null;
|
|
200
|
+
if (externalSessionKey) {
|
|
201
|
+
const existing = this.externalSessionsByKey.get(externalSessionKey);
|
|
202
|
+
if (existing) {
|
|
203
|
+
if (existing.externalDisconnectCleanupTimeout) {
|
|
204
|
+
clearTimeout(existing.externalDisconnectCleanupTimeout);
|
|
205
|
+
existing.externalDisconnectCleanupTimeout = null;
|
|
206
|
+
}
|
|
207
|
+
const previousSocket = existing.socketRef.current;
|
|
208
|
+
if (previousSocket !== ws) {
|
|
209
|
+
this.sessions.delete(previousSocket);
|
|
210
|
+
existing.socketRef.current = ws;
|
|
211
|
+
}
|
|
212
|
+
this.sessions.set(ws, existing);
|
|
213
|
+
this.sendServerInfo(ws);
|
|
214
|
+
existing.connectionLogger.trace({
|
|
215
|
+
clientId: existing.clientId,
|
|
216
|
+
externalSessionKey,
|
|
217
|
+
totalSessions: this.sessions.size,
|
|
218
|
+
}, "Client reconnected");
|
|
219
|
+
this.bindSocketHandlers(ws, existing);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
114
223
|
const clientId = `client-${++this.clientIdCounter}`;
|
|
115
|
-
const
|
|
224
|
+
const requestMetadata = extractSocketRequestMetadata(request);
|
|
225
|
+
const connectionLoggerFields = {
|
|
226
|
+
clientId,
|
|
227
|
+
transport: externalSessionKey ? "relay" : "direct",
|
|
228
|
+
};
|
|
229
|
+
if (requestMetadata.host) {
|
|
230
|
+
connectionLoggerFields.host = requestMetadata.host;
|
|
231
|
+
}
|
|
232
|
+
if (requestMetadata.origin) {
|
|
233
|
+
connectionLoggerFields.origin = requestMetadata.origin;
|
|
234
|
+
}
|
|
235
|
+
if (requestMetadata.userAgent) {
|
|
236
|
+
connectionLoggerFields.userAgent = requestMetadata.userAgent;
|
|
237
|
+
}
|
|
238
|
+
if (requestMetadata.remoteAddress) {
|
|
239
|
+
connectionLoggerFields.remoteAddress = requestMetadata.remoteAddress;
|
|
240
|
+
}
|
|
241
|
+
const connectionLogger = this.logger.child(connectionLoggerFields);
|
|
242
|
+
const socketRef = { current: ws };
|
|
116
243
|
const session = new Session({
|
|
117
244
|
clientId,
|
|
118
245
|
onMessage: (msg) => {
|
|
119
|
-
this.sendToClient(
|
|
246
|
+
this.sendToClient(socketRef.current, wrapSessionMessage(msg));
|
|
247
|
+
},
|
|
248
|
+
onBinaryMessage: (frame) => {
|
|
249
|
+
this.sendBinaryToClient(socketRef.current, frame);
|
|
120
250
|
},
|
|
121
251
|
logger: connectionLogger.child({ module: "session" }),
|
|
122
252
|
downloadTokenStore: this.downloadTokenStore,
|
|
@@ -148,27 +278,57 @@ export class VoiceAssistantWebSocketServer {
|
|
|
148
278
|
dictation: this.dictation ?? undefined,
|
|
149
279
|
agentProviderRuntimeSettings: this.agentProviderRuntimeSettings,
|
|
150
280
|
});
|
|
151
|
-
|
|
281
|
+
const connection = {
|
|
282
|
+
session,
|
|
283
|
+
clientId,
|
|
284
|
+
connectionLogger,
|
|
285
|
+
socketRef,
|
|
286
|
+
externalSessionKey,
|
|
287
|
+
externalDisconnectCleanupTimeout: null,
|
|
288
|
+
};
|
|
289
|
+
this.sessions.set(ws, connection);
|
|
290
|
+
if (externalSessionKey) {
|
|
291
|
+
this.externalSessionsByKey.set(externalSessionKey, connection);
|
|
292
|
+
}
|
|
293
|
+
this.sendServerInfo(ws);
|
|
294
|
+
connectionLogger.trace({ clientId, externalSessionKey, totalSessions: this.sessions.size }, "Client connected");
|
|
295
|
+
this.bindSocketHandlers(ws, connection);
|
|
296
|
+
}
|
|
297
|
+
buildServerInfoStatusPayload() {
|
|
298
|
+
return {
|
|
299
|
+
status: "server_info",
|
|
300
|
+
serverId: this.serverId,
|
|
301
|
+
hostname: getHostname(),
|
|
302
|
+
...(this.serverCapabilities ? { capabilities: this.serverCapabilities } : {}),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
broadcastServerInfo() {
|
|
306
|
+
this.broadcast(wrapSessionMessage({
|
|
307
|
+
type: "status",
|
|
308
|
+
payload: this.buildServerInfoStatusPayload(),
|
|
309
|
+
}));
|
|
310
|
+
}
|
|
311
|
+
sendServerInfo(ws) {
|
|
152
312
|
// Advertise stable server identity immediately on connect (used for URL/shareable IDs).
|
|
153
313
|
this.sendToClient(ws, wrapSessionMessage({
|
|
154
314
|
type: "status",
|
|
155
|
-
payload:
|
|
156
|
-
status: "server_info",
|
|
157
|
-
serverId: this.serverId,
|
|
158
|
-
hostname: getHostname(),
|
|
159
|
-
},
|
|
315
|
+
payload: this.buildServerInfoStatusPayload(),
|
|
160
316
|
}));
|
|
161
|
-
|
|
317
|
+
}
|
|
318
|
+
bindSocketHandlers(ws, connection) {
|
|
162
319
|
ws.on("message", (data) => {
|
|
163
320
|
void this.handleRawMessage(ws, data);
|
|
164
321
|
});
|
|
165
|
-
ws.on("close", async () => {
|
|
166
|
-
await this.detachSocket(ws,
|
|
322
|
+
ws.on("close", async (code, reason) => {
|
|
323
|
+
await this.detachSocket(ws, connection, {
|
|
324
|
+
code: typeof code === "number" ? code : undefined,
|
|
325
|
+
reason,
|
|
326
|
+
});
|
|
167
327
|
});
|
|
168
328
|
ws.on("error", async (error) => {
|
|
169
329
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
170
|
-
connectionLogger.error({ err }, "Client error");
|
|
171
|
-
await this.detachSocket(ws,
|
|
330
|
+
connection.connectionLogger.error({ err }, "Client error");
|
|
331
|
+
await this.detachSocket(ws, connection, { error: err });
|
|
172
332
|
});
|
|
173
333
|
}
|
|
174
334
|
resolveVoiceSpeakHandler(callerAgentId) {
|
|
@@ -177,17 +337,67 @@ export class VoiceAssistantWebSocketServer {
|
|
|
177
337
|
resolveVoiceCallerContext(callerAgentId) {
|
|
178
338
|
return this.voiceCallerContexts.get(callerAgentId) ?? null;
|
|
179
339
|
}
|
|
180
|
-
async detachSocket(ws,
|
|
181
|
-
const
|
|
182
|
-
if (
|
|
340
|
+
async detachSocket(ws, connection, details) {
|
|
341
|
+
const activeConnection = this.sessions.get(ws);
|
|
342
|
+
if (activeConnection !== connection)
|
|
183
343
|
return;
|
|
184
|
-
connectionLogger.info({ clientId, totalSessions: this.sessions.size - 1 }, "Client disconnected");
|
|
185
|
-
await session.cleanup();
|
|
186
344
|
this.sessions.delete(ws);
|
|
345
|
+
if (connection.externalSessionKey &&
|
|
346
|
+
connection.socketRef.current === ws) {
|
|
347
|
+
if (connection.externalDisconnectCleanupTimeout) {
|
|
348
|
+
clearTimeout(connection.externalDisconnectCleanupTimeout);
|
|
349
|
+
}
|
|
350
|
+
const timeout = setTimeout(() => {
|
|
351
|
+
if (connection.externalDisconnectCleanupTimeout !== timeout) {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
connection.externalDisconnectCleanupTimeout = null;
|
|
355
|
+
void this.cleanupConnection(connection, "Client disconnected (grace timeout)");
|
|
356
|
+
}, EXTERNAL_SESSION_DISCONNECT_GRACE_MS);
|
|
357
|
+
connection.externalDisconnectCleanupTimeout = timeout;
|
|
358
|
+
connection.connectionLogger.trace({
|
|
359
|
+
clientId: connection.clientId,
|
|
360
|
+
externalSessionKey: connection.externalSessionKey,
|
|
361
|
+
code: details.code,
|
|
362
|
+
reason: stringifyCloseReason(details.reason),
|
|
363
|
+
reconnectGraceMs: EXTERNAL_SESSION_DISCONNECT_GRACE_MS,
|
|
364
|
+
}, "Client disconnected; waiting for reconnect");
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
await this.cleanupConnection(connection, "Client disconnected");
|
|
368
|
+
}
|
|
369
|
+
async cleanupConnection(connection, logMessage) {
|
|
370
|
+
if (connection.externalDisconnectCleanupTimeout) {
|
|
371
|
+
clearTimeout(connection.externalDisconnectCleanupTimeout);
|
|
372
|
+
connection.externalDisconnectCleanupTimeout = null;
|
|
373
|
+
}
|
|
374
|
+
const currentSocket = connection.socketRef.current;
|
|
375
|
+
this.sessions.delete(currentSocket);
|
|
376
|
+
if (connection.externalSessionKey) {
|
|
377
|
+
const existing = this.externalSessionsByKey.get(connection.externalSessionKey);
|
|
378
|
+
if (existing === connection) {
|
|
379
|
+
this.externalSessionsByKey.delete(connection.externalSessionKey);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
connection.connectionLogger.trace({ clientId: connection.clientId, totalSessions: this.sessions.size }, logMessage);
|
|
383
|
+
await connection.session.cleanup();
|
|
187
384
|
}
|
|
188
385
|
async handleRawMessage(ws, data) {
|
|
189
386
|
try {
|
|
387
|
+
const activeConnection = this.sessions.get(ws);
|
|
190
388
|
const buffer = bufferFromWsData(data);
|
|
389
|
+
const asBytes = asUint8Array(buffer);
|
|
390
|
+
if (asBytes) {
|
|
391
|
+
const frame = decodeBinaryMuxFrame(asBytes);
|
|
392
|
+
if (frame) {
|
|
393
|
+
if (!activeConnection) {
|
|
394
|
+
this.logger.error("No session found for client");
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
activeConnection.session.handleBinaryFrame(frame);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
191
401
|
const parsed = JSON.parse(buffer.toString());
|
|
192
402
|
const parsedMessage = WSInboundMessageSchema.safeParse(parsed);
|
|
193
403
|
if (!parsedMessage.success) {
|
|
@@ -197,7 +407,9 @@ export class VoiceAssistantWebSocketServer {
|
|
|
197
407
|
parsed != null &&
|
|
198
408
|
"type" in parsed &&
|
|
199
409
|
parsed.type === "session";
|
|
200
|
-
this.logger
|
|
410
|
+
const log = activeConnection?.connectionLogger ?? this.logger;
|
|
411
|
+
log.warn({
|
|
412
|
+
clientId: activeConnection?.clientId,
|
|
201
413
|
requestId: requestInfo?.requestId,
|
|
202
414
|
requestType: requestInfo?.requestType,
|
|
203
415
|
error: parsedMessage.error.message,
|
|
@@ -225,42 +437,19 @@ export class VoiceAssistantWebSocketServer {
|
|
|
225
437
|
return;
|
|
226
438
|
}
|
|
227
439
|
const message = parsedMessage.data;
|
|
228
|
-
const messageSummary = {
|
|
229
|
-
type: message.type,
|
|
230
|
-
...(message.type === "session" && message.message
|
|
231
|
-
? { sessionMessageType: message.message.type }
|
|
232
|
-
: {}),
|
|
233
|
-
};
|
|
234
|
-
const isSessionNoise = message.type === "session" &&
|
|
235
|
-
(message.message.type === "client_heartbeat" ||
|
|
236
|
-
message.message.type === "voice_audio_chunk" ||
|
|
237
|
-
message.message.type === "dictation_stream_chunk");
|
|
238
|
-
if (!isSessionNoise) {
|
|
239
|
-
this.logger.debug(messageSummary, "Received message");
|
|
240
|
-
}
|
|
241
440
|
if (message.type === "ping") {
|
|
242
441
|
this.sendToClient(ws, { type: "pong" });
|
|
243
442
|
return;
|
|
244
443
|
}
|
|
245
444
|
if (message.type === "recording_state") {
|
|
246
|
-
this.logger.debug({ isRecording: message.isRecording }, "Recording state");
|
|
247
445
|
return;
|
|
248
446
|
}
|
|
249
|
-
|
|
250
|
-
if (!session) {
|
|
447
|
+
if (!activeConnection) {
|
|
251
448
|
this.logger.error("No session found for client");
|
|
252
449
|
return;
|
|
253
450
|
}
|
|
254
451
|
if (message.type === "session") {
|
|
255
|
-
|
|
256
|
-
this.logger.debug({
|
|
257
|
-
cwd: message.message.config.cwd,
|
|
258
|
-
initialMode: message.message.config.modeId,
|
|
259
|
-
worktreeName: message.message.worktreeName,
|
|
260
|
-
requestId: message.message.requestId,
|
|
261
|
-
}, "create_agent_request details");
|
|
262
|
-
}
|
|
263
|
-
await session.handleMessage(message.message);
|
|
452
|
+
await activeConnection.session.handleMessage(message.message);
|
|
264
453
|
}
|
|
265
454
|
}
|
|
266
455
|
catch (error) {
|
|
@@ -311,20 +500,11 @@ export class VoiceAssistantWebSocketServer {
|
|
|
311
500
|
getClientActivityState(session) {
|
|
312
501
|
const activity = session.getClientActivity();
|
|
313
502
|
if (!activity) {
|
|
314
|
-
this.logger.debug("getClientActivityState: no activity for session");
|
|
315
503
|
return { deviceType: null, focusedAgentId: null, isStale: true, appVisible: false };
|
|
316
504
|
}
|
|
317
505
|
const now = Date.now();
|
|
318
506
|
const ageMs = now - activity.lastActivityAt.getTime();
|
|
319
507
|
const isStale = ageMs >= this.ACTIVITY_THRESHOLD_MS;
|
|
320
|
-
this.logger.debug({
|
|
321
|
-
deviceType: activity.deviceType,
|
|
322
|
-
focusedAgentId: activity.focusedAgentId,
|
|
323
|
-
lastActivityAt: activity.lastActivityAt.toISOString(),
|
|
324
|
-
ageMs,
|
|
325
|
-
isStale,
|
|
326
|
-
appVisible: activity.appVisible,
|
|
327
|
-
}, "getClientActivityState");
|
|
328
508
|
return {
|
|
329
509
|
deviceType: activity.deviceType,
|
|
330
510
|
focusedAgentId: activity.focusedAgentId,
|
|
@@ -332,53 +512,21 @@ export class VoiceAssistantWebSocketServer {
|
|
|
332
512
|
appVisible: activity.appVisible,
|
|
333
513
|
};
|
|
334
514
|
}
|
|
335
|
-
computeShouldNotifyForClient(clientState, allClientStates, agentId) {
|
|
336
|
-
const isAnyoneActiveOnAgent = allClientStates.some((state) => state.focusedAgentId === agentId && state.appVisible && !state.isStale);
|
|
337
|
-
if (isAnyoneActiveOnAgent) {
|
|
338
|
-
return false;
|
|
339
|
-
}
|
|
340
|
-
if (clientState.deviceType === null) {
|
|
341
|
-
return true;
|
|
342
|
-
}
|
|
343
|
-
if (!clientState.isStale && clientState.appVisible && clientState.focusedAgentId !== null) {
|
|
344
|
-
return true;
|
|
345
|
-
}
|
|
346
|
-
if (!clientState.isStale) {
|
|
347
|
-
return false;
|
|
348
|
-
}
|
|
349
|
-
const hasActiveWebClient = allClientStates.some((state) => state.deviceType === "web" && !state.isStale);
|
|
350
|
-
if (clientState.deviceType === "mobile") {
|
|
351
|
-
return !hasActiveWebClient;
|
|
352
|
-
}
|
|
353
|
-
if (clientState.deviceType === "web") {
|
|
354
|
-
const hasOtherClient = allClientStates.some((state) => state !== clientState && (state.deviceType === "mobile" || state.deviceType === null));
|
|
355
|
-
return !hasOtherClient;
|
|
356
|
-
}
|
|
357
|
-
return true;
|
|
358
|
-
}
|
|
359
515
|
broadcastAgentAttention(params) {
|
|
360
516
|
const clientEntries = [];
|
|
361
|
-
for (const [ws,
|
|
517
|
+
for (const [ws, connection] of this.sessions) {
|
|
362
518
|
clientEntries.push({
|
|
363
519
|
ws,
|
|
364
|
-
state: this.getClientActivityState(session),
|
|
520
|
+
state: this.getClientActivityState(connection.session),
|
|
365
521
|
});
|
|
366
522
|
}
|
|
367
523
|
const allStates = clientEntries.map((e) => e.state);
|
|
368
|
-
|
|
369
|
-
agentId: params.agentId,
|
|
370
|
-
reason: params.reason,
|
|
371
|
-
clientCount: clientEntries.length,
|
|
372
|
-
allStates,
|
|
373
|
-
}, "broadcastAgentAttention");
|
|
374
|
-
const hasActiveWebClient = allStates.some((state) => state.deviceType === "web" && !state.isStale);
|
|
375
|
-
const hasActiveMobileForegroundClient = allStates.some((state) => state.deviceType === "mobile" && state.appVisible && !state.isStale);
|
|
376
|
-
// Push is only a fallback when the user is away from their desktop/web.
|
|
524
|
+
// Push is only a fallback when the user is away from desktop/web.
|
|
377
525
|
// Also suppress push if they're actively using the mobile app.
|
|
378
|
-
const shouldSendPush =
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
526
|
+
const shouldSendPush = computeShouldSendPush({
|
|
527
|
+
reason: params.reason,
|
|
528
|
+
allClientStates: allStates,
|
|
529
|
+
});
|
|
382
530
|
if (shouldSendPush) {
|
|
383
531
|
const tokens = this.pushTokenStore.getAllTokens();
|
|
384
532
|
this.logger.info({ tokenCount: tokens.length }, "Sending push notification");
|
|
@@ -401,7 +549,11 @@ export class VoiceAssistantWebSocketServer {
|
|
|
401
549
|
}
|
|
402
550
|
}
|
|
403
551
|
for (const { ws, state } of clientEntries) {
|
|
404
|
-
const shouldNotify =
|
|
552
|
+
const shouldNotify = computeShouldNotifyClient({
|
|
553
|
+
clientState: state,
|
|
554
|
+
allClientStates: allStates,
|
|
555
|
+
agentId: params.agentId,
|
|
556
|
+
});
|
|
405
557
|
const message = wrapSessionMessage({
|
|
406
558
|
type: "agent_stream",
|
|
407
559
|
payload: {
|
|
@@ -420,6 +572,40 @@ export class VoiceAssistantWebSocketServer {
|
|
|
420
572
|
}
|
|
421
573
|
}
|
|
422
574
|
}
|
|
575
|
+
function extractSocketRequestMetadata(request) {
|
|
576
|
+
if (!request || typeof request !== "object") {
|
|
577
|
+
return {};
|
|
578
|
+
}
|
|
579
|
+
const record = request;
|
|
580
|
+
const host = typeof record.headers?.host === "string" ? record.headers.host : undefined;
|
|
581
|
+
const origin = typeof record.headers?.origin === "string" ? record.headers.origin : undefined;
|
|
582
|
+
const userAgent = typeof record.headers?.["user-agent"] === "string"
|
|
583
|
+
? record.headers["user-agent"]
|
|
584
|
+
: undefined;
|
|
585
|
+
const remoteAddress = typeof record.socket?.remoteAddress === "string"
|
|
586
|
+
? record.socket.remoteAddress
|
|
587
|
+
: undefined;
|
|
588
|
+
return {
|
|
589
|
+
...(host ? { host } : {}),
|
|
590
|
+
...(origin ? { origin } : {}),
|
|
591
|
+
...(userAgent ? { userAgent } : {}),
|
|
592
|
+
...(remoteAddress ? { remoteAddress } : {}),
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
function stringifyCloseReason(reason) {
|
|
596
|
+
if (typeof reason === "string") {
|
|
597
|
+
return reason.length > 0 ? reason : null;
|
|
598
|
+
}
|
|
599
|
+
if (Buffer.isBuffer(reason)) {
|
|
600
|
+
const text = reason.toString();
|
|
601
|
+
return text.length > 0 ? text : null;
|
|
602
|
+
}
|
|
603
|
+
if (reason == null) {
|
|
604
|
+
return null;
|
|
605
|
+
}
|
|
606
|
+
const text = String(reason);
|
|
607
|
+
return text.length > 0 ? text : null;
|
|
608
|
+
}
|
|
423
609
|
function extractRequestInfoFromUnknownWsInbound(payload) {
|
|
424
610
|
if (!payload || typeof payload !== "object") {
|
|
425
611
|
return null;
|