@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.
Files changed (175) 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 +130 -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 +54 -36
  26. package/dist/server/client/daemon-client.d.ts.map +1 -1
  27. package/dist/server/client/daemon-client.js +497 -800
  28. package/dist/server/client/daemon-client.js.map +1 -1
  29. package/dist/server/server/agent/agent-manager.d.ts +52 -0
  30. package/dist/server/server/agent/agent-manager.d.ts.map +1 -1
  31. package/dist/server/server/agent/agent-manager.js +238 -10
  32. package/dist/server/server/agent/agent-manager.js.map +1 -1
  33. package/dist/server/server/agent/agent-storage.d.ts +4 -4
  34. package/dist/server/server/agent/providers/claude/tool-call-detail-parser.d.ts.map +1 -1
  35. package/dist/server/server/agent/providers/claude/tool-call-detail-parser.js +62 -12
  36. package/dist/server/server/agent/providers/claude/tool-call-detail-parser.js.map +1 -1
  37. package/dist/server/server/agent/providers/claude/tool-call-mapper.d.ts +4 -4
  38. package/dist/server/server/agent/providers/claude/tool-call-mapper.d.ts.map +1 -1
  39. package/dist/server/server/agent/providers/claude/tool-call-mapper.js +213 -50
  40. package/dist/server/server/agent/providers/claude/tool-call-mapper.js.map +1 -1
  41. package/dist/server/server/agent/providers/claude-agent.d.ts +7 -0
  42. package/dist/server/server/agent/providers/claude-agent.d.ts.map +1 -1
  43. package/dist/server/server/agent/providers/claude-agent.js +100 -19
  44. package/dist/server/server/agent/providers/claude-agent.js.map +1 -1
  45. package/dist/server/server/agent/providers/codex/tool-call-detail-parser.d.ts +0 -1
  46. package/dist/server/server/agent/providers/codex/tool-call-detail-parser.d.ts.map +1 -1
  47. package/dist/server/server/agent/providers/codex/tool-call-detail-parser.js +67 -30
  48. package/dist/server/server/agent/providers/codex/tool-call-detail-parser.js.map +1 -1
  49. package/dist/server/server/agent/providers/codex/tool-call-mapper.d.ts +1 -1
  50. package/dist/server/server/agent/providers/codex/tool-call-mapper.d.ts.map +1 -1
  51. package/dist/server/server/agent/providers/codex/tool-call-mapper.js +293 -266
  52. package/dist/server/server/agent/providers/codex/tool-call-mapper.js.map +1 -1
  53. package/dist/server/server/agent/providers/codex-app-server-agent.d.ts +1 -1
  54. package/dist/server/server/agent/providers/codex-app-server-agent.d.ts.map +1 -1
  55. package/dist/server/server/agent/providers/codex-app-server-agent.js +49 -14
  56. package/dist/server/server/agent/providers/codex-app-server-agent.js.map +1 -1
  57. package/dist/server/server/agent/providers/codex-rollout-timeline.d.ts.map +1 -1
  58. package/dist/server/server/agent/providers/codex-rollout-timeline.js +5 -4
  59. package/dist/server/server/agent/providers/codex-rollout-timeline.js.map +1 -1
  60. package/dist/server/server/agent/providers/opencode/tool-call-detail-parser.d.ts.map +1 -1
  61. package/dist/server/server/agent/providers/opencode/tool-call-detail-parser.js +8 -2
  62. package/dist/server/server/agent/providers/opencode/tool-call-detail-parser.js.map +1 -1
  63. package/dist/server/server/agent/providers/opencode/tool-call-mapper.d.ts +1 -1
  64. package/dist/server/server/agent/providers/opencode/tool-call-mapper.d.ts.map +1 -1
  65. package/dist/server/server/agent/providers/opencode/tool-call-mapper.js +121 -45
  66. package/dist/server/server/agent/providers/opencode/tool-call-mapper.js.map +1 -1
  67. package/dist/server/server/agent/providers/opencode-agent.d.ts.map +1 -1
  68. package/dist/server/server/agent/providers/opencode-agent.js +87 -35
  69. package/dist/server/server/agent/providers/opencode-agent.js.map +1 -1
  70. package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts +2 -2
  71. package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts.map +1 -1
  72. package/dist/server/server/agent/providers/tool-call-detail-primitives.js +23 -6
  73. package/dist/server/server/agent/providers/tool-call-detail-primitives.js.map +1 -1
  74. package/dist/server/server/agent/providers/tool-call-mapper-utils.d.ts +0 -1
  75. package/dist/server/server/agent/providers/tool-call-mapper-utils.d.ts.map +1 -1
  76. package/dist/server/server/agent/providers/tool-call-mapper-utils.js +0 -10
  77. package/dist/server/server/agent/providers/tool-call-mapper-utils.js.map +1 -1
  78. package/dist/server/server/agent/stt-manager.d.ts +3 -2
  79. package/dist/server/server/agent/stt-manager.d.ts.map +1 -1
  80. package/dist/server/server/agent/stt-manager.js +5 -3
  81. package/dist/server/server/agent/stt-manager.js.map +1 -1
  82. package/dist/server/server/agent/timeline-projection.d.ts +19 -0
  83. package/dist/server/server/agent/timeline-projection.d.ts.map +1 -0
  84. package/dist/server/server/agent/timeline-projection.js +142 -0
  85. package/dist/server/server/agent/timeline-projection.js.map +1 -0
  86. package/dist/server/server/agent/tts-manager.d.ts +3 -2
  87. package/dist/server/server/agent/tts-manager.d.ts.map +1 -1
  88. package/dist/server/server/agent/tts-manager.js +5 -17
  89. package/dist/server/server/agent/tts-manager.js.map +1 -1
  90. package/dist/server/server/agent-attention-policy.d.ts +20 -0
  91. package/dist/server/server/agent-attention-policy.d.ts.map +1 -0
  92. package/dist/server/server/agent-attention-policy.js +40 -0
  93. package/dist/server/server/agent-attention-policy.js.map +1 -0
  94. package/dist/server/server/bootstrap.d.ts.map +1 -1
  95. package/dist/server/server/bootstrap.js +13 -20
  96. package/dist/server/server/bootstrap.js.map +1 -1
  97. package/dist/server/server/dictation/dictation-stream-manager.d.ts +10 -2
  98. package/dist/server/server/dictation/dictation-stream-manager.d.ts.map +1 -1
  99. package/dist/server/server/dictation/dictation-stream-manager.js +83 -27
  100. package/dist/server/server/dictation/dictation-stream-manager.js.map +1 -1
  101. package/dist/server/server/exports.d.ts +2 -1
  102. package/dist/server/server/exports.d.ts.map +1 -1
  103. package/dist/server/server/exports.js +1 -0
  104. package/dist/server/server/exports.js.map +1 -1
  105. package/dist/server/server/persisted-config.d.ts +36 -22
  106. package/dist/server/server/persisted-config.d.ts.map +1 -1
  107. package/dist/server/server/persisted-config.js +2 -0
  108. package/dist/server/server/persisted-config.js.map +1 -1
  109. package/dist/server/server/relay-transport.d.ts +3 -2
  110. package/dist/server/server/relay-transport.d.ts.map +1 -1
  111. package/dist/server/server/relay-transport.js +99 -16
  112. package/dist/server/server/relay-transport.js.map +1 -1
  113. package/dist/server/server/session.d.ts +34 -16
  114. package/dist/server/server/session.d.ts.map +1 -1
  115. package/dist/server/server/session.js +619 -328
  116. package/dist/server/server/session.js.map +1 -1
  117. package/dist/server/server/speech/provider-resolver.d.ts +3 -0
  118. package/dist/server/server/speech/provider-resolver.d.ts.map +1 -0
  119. package/dist/server/server/speech/provider-resolver.js +7 -0
  120. package/dist/server/server/speech/provider-resolver.js.map +1 -0
  121. package/dist/server/server/speech/providers/local/config.d.ts.map +1 -1
  122. package/dist/server/server/speech/providers/local/config.js +11 -8
  123. package/dist/server/server/speech/providers/local/config.js.map +1 -1
  124. package/dist/server/server/speech/providers/local/runtime.d.ts +1 -0
  125. package/dist/server/server/speech/providers/local/runtime.d.ts.map +1 -1
  126. package/dist/server/server/speech/providers/local/runtime.js +13 -9
  127. package/dist/server/server/speech/providers/local/runtime.js.map +1 -1
  128. package/dist/server/server/speech/providers/local/sherpa/model-downloader.d.ts.map +1 -1
  129. package/dist/server/server/speech/providers/local/sherpa/model-downloader.js +73 -54
  130. package/dist/server/server/speech/providers/local/sherpa/model-downloader.js.map +1 -1
  131. package/dist/server/server/speech/providers/openai/config.d.ts.map +1 -1
  132. package/dist/server/server/speech/providers/openai/config.js +10 -5
  133. package/dist/server/server/speech/providers/openai/config.js.map +1 -1
  134. package/dist/server/server/speech/providers/openai/runtime.d.ts.map +1 -1
  135. package/dist/server/server/speech/providers/openai/runtime.js +17 -6
  136. package/dist/server/server/speech/providers/openai/runtime.js.map +1 -1
  137. package/dist/server/server/speech/speech-config-resolver.d.ts.map +1 -1
  138. package/dist/server/server/speech/speech-config-resolver.js +25 -4
  139. package/dist/server/server/speech/speech-config-resolver.js.map +1 -1
  140. package/dist/server/server/speech/speech-runtime.d.ts +26 -3
  141. package/dist/server/server/speech/speech-runtime.d.ts.map +1 -1
  142. package/dist/server/server/speech/speech-runtime.js +468 -85
  143. package/dist/server/server/speech/speech-runtime.js.map +1 -1
  144. package/dist/server/server/speech/speech-types.d.ts +3 -0
  145. package/dist/server/server/speech/speech-types.d.ts.map +1 -1
  146. package/dist/server/server/speech/speech-types.js +1 -0
  147. package/dist/server/server/speech/speech-types.js.map +1 -1
  148. package/dist/server/server/websocket-server.d.ts +23 -7
  149. package/dist/server/server/websocket-server.d.ts.map +1 -1
  150. package/dist/server/server/websocket-server.js +288 -102
  151. package/dist/server/server/websocket-server.js.map +1 -1
  152. package/dist/server/shared/binary-mux.d.ts +31 -0
  153. package/dist/server/shared/binary-mux.d.ts.map +1 -0
  154. package/dist/server/shared/binary-mux.js +101 -0
  155. package/dist/server/shared/binary-mux.js.map +1 -0
  156. package/dist/server/shared/messages.d.ts +5206 -4814
  157. package/dist/server/shared/messages.d.ts.map +1 -1
  158. package/dist/server/shared/messages.js +225 -58
  159. package/dist/server/shared/messages.js.map +1 -1
  160. package/dist/server/shared/terminal-key-input.d.ts +9 -0
  161. package/dist/server/shared/terminal-key-input.d.ts.map +1 -0
  162. package/dist/server/shared/terminal-key-input.js +132 -0
  163. package/dist/server/shared/terminal-key-input.js.map +1 -0
  164. package/dist/server/terminal/terminal.d.ts +17 -0
  165. package/dist/server/terminal/terminal.d.ts.map +1 -1
  166. package/dist/server/terminal/terminal.js +89 -0
  167. package/dist/server/terminal/terminal.js.map +1 -1
  168. package/dist/server/utils/checkout-git.d.ts +9 -1
  169. package/dist/server/utils/checkout-git.d.ts.map +1 -1
  170. package/dist/server/utils/checkout-git.js +314 -75
  171. package/dist/server/utils/checkout-git.js.map +1 -1
  172. package/dist/server/utils/worktree.d.ts.map +1 -1
  173. package/dist/server/utils/worktree.js +33 -4
  174. package/dist/server/utils/worktree.js.map +1 -1
  175. 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":"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,EAIL,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EAEvB,MAAM,eAAe,CAAC;AAMvB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAI7D,OAAO,KAAK,EAAE,+BAA+B,EAAE,MAAM,mCAAmC,CAAC;AAGzF,OAAO,KAAK,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAC9F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAChE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAC;AAC1E,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AAC7E,OAAO,KAAK,EACV,kBAAkB,EAClB,mBAAmB,EACnB,iBAAiB,EAClB,MAAM,kBAAkB,CAAC;AAO1B,MAAM,MAAM,wBAAwB,GAAG,MAAM,OAAO,CAAC,SAAS,CAAC,CAAC;AAChE,MAAM,MAAM,sBAAsB,GAAG;IACnC,SAAS,EAAE,OAAO,CAAC;IACnB,kBAAkB,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,KAAK,qBAAqB,GAAG;IAC3B,cAAc,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC5B,YAAY,CAAC,EAAE,kBAAkB,CAAC;CACnC,CAAC;AAkFF,KAAK,aAAa,GAAG;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,WAAW,KAAK,IAAI,CAAC;IACxD,KAAK,EAAE,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD,EAAE,EAAE,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,GAAG,OAAO,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,KAAK,IAAI,CAAC;IACvF,IAAI,EAAE,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,KAAK,IAAI,CAAC;CAC9E,CAAC;AAaF;;GAEG;AACH,qBAAa,6BAA6B;IACxC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;IACrC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAkB;IACtC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAoD;IAC7E,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAA6C;IACnF,OAAO,CAAC,eAAe,CAAK;IAC5B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAe;IAC5C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAe;IAC5C,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAqB;IACxD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAiB;IAChD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAc;IAC1C,OAAO,CAAC,QAAQ,CAAC,uBAAuB,CAA2B;IACnE,OAAO,CAAC,QAAQ,CAAC,GAAG,CAA0C;IAC9D,OAAO,CAAC,QAAQ,CAAC,GAAG,CAA0C;IAC9D,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAyB;IACzD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAQjB;IACT,OAAO,CAAC,QAAQ,CAAC,KAAK,CAIb;IACT,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAG/B;IACJ,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAyC;IAC7E,OAAO,CAAC,QAAQ,CAAC,4BAA4B,CAA8C;IAC3F,OAAO,CAAC,kBAAkB,CAAiC;gBAGzD,MAAM,EAAE,UAAU,EAClB,MAAM,EAAE,IAAI,CAAC,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,YAAY,EAC1B,YAAY,EAAE,YAAY,EAC1B,kBAAkB,EAAE,kBAAkB,EACtC,SAAS,EAAE,MAAM,EACjB,uBAAuB,EAAE,wBAAwB,EACjD,QAAQ,EAAE,qBAAqB,EAC/B,MAAM,CAAC,EAAE;QACP,GAAG,EAAE,UAAU,CAAC,oBAAoB,GAAG,IAAI,CAAC,CAAC;QAC7C,GAAG,EAAE,UAAU,CAAC,oBAAoB,GAAG,IAAI,CAAC,CAAC;KAC9C,EACD,eAAe,CAAC,EAAE,eAAe,GAAG,IAAI,EACxC,KAAK,CAAC,EAAE;QACN,kBAAkB,CAAC,EAAE,mBAAmB,GAAG,IAAI,CAAC;QAChD,4BAA4B,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;QACpE,4BAA4B,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;KACnE,EACD,SAAS,CAAC,EAAE;QACV,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,GAAG,CAAC,EAAE,UAAU,CAAC,oBAAoB,GAAG,IAAI,CAAC,CAAC;QAC9C,WAAW,CAAC,EAAE;YACZ,SAAS,EAAE,MAAM,CAAC;YAClB,eAAe,EAAE,kBAAkB,EAAE,CAAC;SACvC,CAAC;QACF,kBAAkB,CAAC,EAAE,MAAM,uBAAuB,CAAC;KACpD,EACD,4BAA4B,CAAC,EAAE,+BAA+B;IAsEzD,SAAS,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI;IAU3C,sBAAsB,CAAC,SAAS,EAAE,uBAAuB,GAAG,IAAI,GAAG,IAAI;IAIvE,wBAAwB,CAC7B,YAAY,EAAE,kBAAkB,GAAG,IAAI,GAAG,SAAS,GAClD,IAAI;IASM,oBAAoB,CAC/B,EAAE,EAAE,aAAa,EACjB,QAAQ,CAAC,EAAE,sBAAsB,GAChC,OAAO,CAAC,IAAI,CAAC;IAIH,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAiCnC,OAAO,CAAC,YAAY;IAOpB,OAAO,CAAC,kBAAkB;YAUZ,YAAY;IA2H1B,OAAO,CAAC,4BAA4B;IASpC,OAAO,CAAC,mBAAmB;IAS3B,OAAO,CAAC,cAAc;IAWtB,OAAO,CAAC,kBAAkB;IAsBnB,wBAAwB,CAC7B,aAAa,EAAE,MAAM,GACpB,iBAAiB,GAAG,IAAI;IAIpB,yBAAyB,CAC9B,aAAa,EAAE,MAAM,GACpB,kBAAkB,GAAG,IAAI;YAId,YAAY;YA6CZ,iBAAiB;YAyBjB,gBAAgB;IAuJ9B,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAW;IAEjD,OAAO,CAAC,sBAAsB;IAgB9B,OAAO,CAAC,uBAAuB;CA4EhC"}
@@ -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.info({ 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.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.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,
@@ -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
- const session = this.sessions.get(ws);
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
- if (message.message.type === "create_agent_request") {
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, session] of this.sessions) {
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
- this.logger.debug({
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 = params.reason !== "error" &&
379
- !hasActiveWebClient &&
380
- !hasActiveMobileForegroundClient;
381
- this.logger.debug({ hasActiveWebClient, hasActiveMobileForegroundClient, shouldSendPush }, "Push gating check");
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 = this.computeShouldNotifyForClient(state, allStates, params.agentId);
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;