@getpaseo/server 0.1.3 → 0.1.6

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.
Files changed (148) hide show
  1. package/dist/server/client/daemon-client-relay-e2ee-transport.d.ts +8 -0
  2. package/dist/server/client/daemon-client-relay-e2ee-transport.d.ts.map +1 -0
  3. package/dist/server/client/daemon-client-relay-e2ee-transport.js +161 -0
  4. package/dist/server/client/daemon-client-relay-e2ee-transport.js.map +1 -0
  5. package/dist/server/client/daemon-client-terminal-stream-manager.d.ts +43 -0
  6. package/dist/server/client/daemon-client-terminal-stream-manager.d.ts.map +1 -0
  7. package/dist/server/client/daemon-client-terminal-stream-manager.js +134 -0
  8. package/dist/server/client/daemon-client-terminal-stream-manager.js.map +1 -0
  9. package/dist/server/client/daemon-client-transport-types.d.ts +34 -0
  10. package/dist/server/client/daemon-client-transport-types.d.ts.map +1 -0
  11. package/dist/server/client/daemon-client-transport-types.js +2 -0
  12. package/dist/server/client/daemon-client-transport-types.js.map +1 -0
  13. package/dist/server/client/daemon-client-transport-utils.d.ts +9 -0
  14. package/dist/server/client/daemon-client-transport-utils.d.ts.map +1 -0
  15. package/dist/server/client/daemon-client-transport-utils.js +121 -0
  16. package/dist/server/client/daemon-client-transport-utils.js.map +1 -0
  17. package/dist/server/client/daemon-client-transport.d.ts +5 -0
  18. package/dist/server/client/daemon-client-transport.d.ts.map +1 -0
  19. package/dist/server/client/daemon-client-transport.js +4 -0
  20. package/dist/server/client/daemon-client-transport.js.map +1 -0
  21. package/dist/server/client/daemon-client-websocket-transport.d.ts +7 -0
  22. package/dist/server/client/daemon-client-websocket-transport.d.ts.map +1 -0
  23. package/dist/server/client/daemon-client-websocket-transport.js +64 -0
  24. package/dist/server/client/daemon-client-websocket-transport.js.map +1 -0
  25. package/dist/server/client/daemon-client.d.ts +63 -46
  26. package/dist/server/client/daemon-client.d.ts.map +1 -1
  27. package/dist/server/client/daemon-client.js +497 -796
  28. package/dist/server/client/daemon-client.js.map +1 -1
  29. package/dist/server/server/agent/agent-management-mcp.d.ts +2 -0
  30. package/dist/server/server/agent/agent-management-mcp.d.ts.map +1 -1
  31. package/dist/server/server/agent/agent-management-mcp.js +29 -4
  32. package/dist/server/server/agent/agent-management-mcp.js.map +1 -1
  33. package/dist/server/server/agent/agent-manager.d.ts +48 -0
  34. package/dist/server/server/agent/agent-manager.d.ts.map +1 -1
  35. package/dist/server/server/agent/agent-manager.js +224 -14
  36. package/dist/server/server/agent/agent-manager.js.map +1 -1
  37. package/dist/server/server/agent/agent-sdk-types.d.ts +14 -0
  38. package/dist/server/server/agent/agent-sdk-types.d.ts.map +1 -1
  39. package/dist/server/server/agent/agent-storage.d.ts +4 -4
  40. package/dist/server/server/agent/mcp-server.d.ts +2 -0
  41. package/dist/server/server/agent/mcp-server.d.ts.map +1 -1
  42. package/dist/server/server/agent/mcp-server.js +30 -5
  43. package/dist/server/server/agent/mcp-server.js.map +1 -1
  44. package/dist/server/server/agent/providers/claude/tool-call-mapper.d.ts.map +1 -1
  45. package/dist/server/server/agent/providers/claude/tool-call-mapper.js +120 -6
  46. package/dist/server/server/agent/providers/claude/tool-call-mapper.js.map +1 -1
  47. package/dist/server/server/agent/providers/claude-agent.d.ts +7 -0
  48. package/dist/server/server/agent/providers/claude-agent.d.ts.map +1 -1
  49. package/dist/server/server/agent/providers/claude-agent.js +83 -9
  50. package/dist/server/server/agent/providers/claude-agent.js.map +1 -1
  51. package/dist/server/server/agent/providers/codex-app-server-agent.d.ts.map +1 -1
  52. package/dist/server/server/agent/providers/codex-app-server-agent.js +25 -0
  53. package/dist/server/server/agent/providers/codex-app-server-agent.js.map +1 -1
  54. package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts +42 -0
  55. package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts.map +1 -1
  56. package/dist/server/server/agent/stt-manager.d.ts +3 -2
  57. package/dist/server/server/agent/stt-manager.d.ts.map +1 -1
  58. package/dist/server/server/agent/stt-manager.js +5 -3
  59. package/dist/server/server/agent/stt-manager.js.map +1 -1
  60. package/dist/server/server/agent/timeline-append.d.ts +10 -0
  61. package/dist/server/server/agent/timeline-append.d.ts.map +1 -0
  62. package/dist/server/server/agent/timeline-append.js +27 -0
  63. package/dist/server/server/agent/timeline-append.js.map +1 -0
  64. package/dist/server/server/agent/timeline-projection.d.ts +19 -0
  65. package/dist/server/server/agent/timeline-projection.d.ts.map +1 -0
  66. package/dist/server/server/agent/timeline-projection.js +142 -0
  67. package/dist/server/server/agent/timeline-projection.js.map +1 -0
  68. package/dist/server/server/agent/tts-manager.d.ts +3 -2
  69. package/dist/server/server/agent/tts-manager.d.ts.map +1 -1
  70. package/dist/server/server/agent/tts-manager.js +5 -3
  71. package/dist/server/server/agent/tts-manager.js.map +1 -1
  72. package/dist/server/server/agent-attention-policy.d.ts +20 -0
  73. package/dist/server/server/agent-attention-policy.d.ts.map +1 -0
  74. package/dist/server/server/agent-attention-policy.js +40 -0
  75. package/dist/server/server/agent-attention-policy.js.map +1 -0
  76. package/dist/server/server/bootstrap.d.ts.map +1 -1
  77. package/dist/server/server/bootstrap.js +16 -18
  78. package/dist/server/server/bootstrap.js.map +1 -1
  79. package/dist/server/server/dictation/dictation-stream-manager.d.ts +10 -2
  80. package/dist/server/server/dictation/dictation-stream-manager.d.ts.map +1 -1
  81. package/dist/server/server/dictation/dictation-stream-manager.js +81 -14
  82. package/dist/server/server/dictation/dictation-stream-manager.js.map +1 -1
  83. package/dist/server/server/exports.d.ts +1 -1
  84. package/dist/server/server/exports.d.ts.map +1 -1
  85. package/dist/server/server/persisted-config.d.ts +12 -12
  86. package/dist/server/server/relay-transport.d.ts +3 -2
  87. package/dist/server/server/relay-transport.d.ts.map +1 -1
  88. package/dist/server/server/relay-transport.js +21 -5
  89. package/dist/server/server/relay-transport.js.map +1 -1
  90. package/dist/server/server/session.d.ts +51 -14
  91. package/dist/server/server/session.d.ts.map +1 -1
  92. package/dist/server/server/session.js +872 -250
  93. package/dist/server/server/session.js.map +1 -1
  94. package/dist/server/server/speech/provider-resolver.d.ts +3 -0
  95. package/dist/server/server/speech/provider-resolver.d.ts.map +1 -0
  96. package/dist/server/server/speech/provider-resolver.js +7 -0
  97. package/dist/server/server/speech/provider-resolver.js.map +1 -0
  98. package/dist/server/server/speech/providers/local/runtime.d.ts +1 -0
  99. package/dist/server/server/speech/providers/local/runtime.d.ts.map +1 -1
  100. package/dist/server/server/speech/providers/local/runtime.js +4 -3
  101. package/dist/server/server/speech/providers/local/runtime.js.map +1 -1
  102. package/dist/server/server/speech/providers/local/sherpa/model-downloader.d.ts.map +1 -1
  103. package/dist/server/server/speech/providers/local/sherpa/model-downloader.js +3 -66
  104. package/dist/server/server/speech/providers/local/sherpa/model-downloader.js.map +1 -1
  105. package/dist/server/server/speech/speech-runtime.d.ts +26 -3
  106. package/dist/server/server/speech/speech-runtime.d.ts.map +1 -1
  107. package/dist/server/server/speech/speech-runtime.js +466 -112
  108. package/dist/server/server/speech/speech-runtime.js.map +1 -1
  109. package/dist/server/server/websocket-server.d.ts +23 -7
  110. package/dist/server/server/websocket-server.d.ts.map +1 -1
  111. package/dist/server/server/websocket-server.js +288 -64
  112. package/dist/server/server/websocket-server.js.map +1 -1
  113. package/dist/server/server/worktree-bootstrap.d.ts +29 -0
  114. package/dist/server/server/worktree-bootstrap.d.ts.map +1 -0
  115. package/dist/server/server/worktree-bootstrap.js +407 -0
  116. package/dist/server/server/worktree-bootstrap.js.map +1 -0
  117. package/dist/server/shared/binary-mux.d.ts +31 -0
  118. package/dist/server/shared/binary-mux.d.ts.map +1 -0
  119. package/dist/server/shared/binary-mux.js +114 -0
  120. package/dist/server/shared/binary-mux.js.map +1 -0
  121. package/dist/server/shared/messages.d.ts +6437 -5839
  122. package/dist/server/shared/messages.d.ts.map +1 -1
  123. package/dist/server/shared/messages.js +238 -50
  124. package/dist/server/shared/messages.js.map +1 -1
  125. package/dist/server/shared/terminal-key-input.d.ts +9 -0
  126. package/dist/server/shared/terminal-key-input.d.ts.map +1 -0
  127. package/dist/server/shared/terminal-key-input.js +132 -0
  128. package/dist/server/shared/terminal-key-input.js.map +1 -0
  129. package/dist/server/shared/tool-call-display.d.ts.map +1 -1
  130. package/dist/server/shared/tool-call-display.js +4 -0
  131. package/dist/server/shared/tool-call-display.js.map +1 -1
  132. package/dist/server/terminal/terminal-manager.d.ts +11 -0
  133. package/dist/server/terminal/terminal-manager.d.ts.map +1 -1
  134. package/dist/server/terminal/terminal-manager.js +75 -24
  135. package/dist/server/terminal/terminal-manager.js.map +1 -1
  136. package/dist/server/terminal/terminal.d.ts +18 -0
  137. package/dist/server/terminal/terminal.d.ts.map +1 -1
  138. package/dist/server/terminal/terminal.js +142 -5
  139. package/dist/server/terminal/terminal.js.map +1 -1
  140. package/dist/server/utils/checkout-git.d.ts +4 -0
  141. package/dist/server/utils/checkout-git.d.ts.map +1 -1
  142. package/dist/server/utils/checkout-git.js +92 -0
  143. package/dist/server/utils/checkout-git.js.map +1 -1
  144. package/dist/server/utils/worktree.d.ts +32 -0
  145. package/dist/server/utils/worktree.d.ts.map +1 -1
  146. package/dist/server/utils/worktree.js +160 -10
  147. package/dist/server/utils/worktree.js.map +1 -1
  148. 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
- attachExternalSocket(ws: WebSocketLike): Promise<void>;
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":"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"}
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 origin = req.headers.origin;
54
- const requestHost = typeof req.headers.host === "string" ? req.headers.host : null;
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
- async attachExternalSocket(ws) {
87
- await this.attachSocket(ws);
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 [ws, session] of this.sessions) {
92
- cleanupPromises.push(session.cleanup());
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
- async attachSocket(ws, _request) {
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 connectionLogger = this.logger.child({ clientId });
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(ws, wrapSessionMessage(msg));
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
- this.sessions.set(ws, session);
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
- connectionLogger.trace({ clientId, totalSessions: this.sessions.size }, "Client connected");
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, connectionLogger, clientId);
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, connectionLogger, clientId);
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, connectionLogger, clientId) {
181
- const session = this.sessions.get(ws);
182
- if (!session)
340
+ async detachSocket(ws, connection, details) {
341
+ const activeConnection = this.sessions.get(ws);
342
+ if (activeConnection !== connection)
183
343
  return;
184
- connectionLogger.trace({ 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.warn({
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,
@@ -232,13 +444,12 @@ export class VoiceAssistantWebSocketServer {
232
444
  if (message.type === "recording_state") {
233
445
  return;
234
446
  }
235
- const session = this.sessions.get(ws);
236
- if (!session) {
447
+ if (!activeConnection) {
237
448
  this.logger.error("No session found for client");
238
449
  return;
239
450
  }
240
451
  if (message.type === "session") {
241
- await session.handleMessage(message.message);
452
+ await activeConnection.session.handleMessage(message.message);
242
453
  }
243
454
  }
244
455
  catch (error) {
@@ -301,46 +512,21 @@ export class VoiceAssistantWebSocketServer {
301
512
  appVisible: activity.appVisible,
302
513
  };
303
514
  }
304
- computeShouldNotifyForClient(clientState, allClientStates, agentId) {
305
- const isAnyoneActiveOnAgent = allClientStates.some((state) => state.focusedAgentId === agentId && state.appVisible && !state.isStale);
306
- if (isAnyoneActiveOnAgent) {
307
- return false;
308
- }
309
- if (clientState.deviceType === null) {
310
- return true;
311
- }
312
- if (!clientState.isStale && clientState.appVisible && clientState.focusedAgentId !== null) {
313
- return true;
314
- }
315
- if (!clientState.isStale) {
316
- return false;
317
- }
318
- const hasActiveWebClient = allClientStates.some((state) => state.deviceType === "web" && !state.isStale);
319
- if (clientState.deviceType === "mobile") {
320
- return !hasActiveWebClient;
321
- }
322
- if (clientState.deviceType === "web") {
323
- const hasOtherClient = allClientStates.some((state) => state !== clientState && (state.deviceType === "mobile" || state.deviceType === null));
324
- return !hasOtherClient;
325
- }
326
- return true;
327
- }
328
515
  broadcastAgentAttention(params) {
329
516
  const clientEntries = [];
330
- for (const [ws, session] of this.sessions) {
517
+ for (const [ws, connection] of this.sessions) {
331
518
  clientEntries.push({
332
519
  ws,
333
- state: this.getClientActivityState(session),
520
+ state: this.getClientActivityState(connection.session),
334
521
  });
335
522
  }
336
523
  const allStates = clientEntries.map((e) => e.state);
337
- const hasActiveWebClient = allStates.some((state) => state.deviceType === "web" && !state.isStale);
338
- const hasActiveMobileForegroundClient = allStates.some((state) => state.deviceType === "mobile" && state.appVisible && !state.isStale);
339
- // 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.
340
525
  // Also suppress push if they're actively using the mobile app.
341
- const shouldSendPush = params.reason !== "error" &&
342
- !hasActiveWebClient &&
343
- !hasActiveMobileForegroundClient;
526
+ const shouldSendPush = computeShouldSendPush({
527
+ reason: params.reason,
528
+ allClientStates: allStates,
529
+ });
344
530
  if (shouldSendPush) {
345
531
  const tokens = this.pushTokenStore.getAllTokens();
346
532
  this.logger.info({ tokenCount: tokens.length }, "Sending push notification");
@@ -363,7 +549,11 @@ export class VoiceAssistantWebSocketServer {
363
549
  }
364
550
  }
365
551
  for (const { ws, state } of clientEntries) {
366
- const shouldNotify = this.computeShouldNotifyForClient(state, allStates, params.agentId);
552
+ const shouldNotify = computeShouldNotifyClient({
553
+ clientState: state,
554
+ allClientStates: allStates,
555
+ agentId: params.agentId,
556
+ });
367
557
  const message = wrapSessionMessage({
368
558
  type: "agent_stream",
369
559
  payload: {
@@ -382,6 +572,40 @@ export class VoiceAssistantWebSocketServer {
382
572
  }
383
573
  }
384
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
+ }
385
609
  function extractRequestInfoFromUnknownWsInbound(payload) {
386
610
  if (!payload || typeof payload !== "object") {
387
611
  return null;