@getpaseo/server 0.1.37 → 0.1.38

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 (295) hide show
  1. package/dist/scripts/dev-runner.js +1 -1
  2. package/dist/scripts/dev-runner.js.map +1 -1
  3. package/dist/scripts/{daemon-runner.js → supervisor-entrypoint.js} +7 -9
  4. package/dist/scripts/supervisor-entrypoint.js.map +1 -0
  5. package/dist/scripts/supervisor.js +17 -1
  6. package/dist/scripts/supervisor.js.map +1 -1
  7. package/dist/server/server/bootstrap.d.ts +0 -4
  8. package/dist/server/server/bootstrap.d.ts.map +1 -1
  9. package/dist/server/server/bootstrap.js +8 -22
  10. package/dist/server/server/bootstrap.js.map +1 -1
  11. package/dist/server/server/index.js +0 -4
  12. package/dist/server/server/index.js.map +1 -1
  13. package/dist/server/server/pid-lock.d.ts +7 -2
  14. package/dist/server/server/pid-lock.d.ts.map +1 -1
  15. package/dist/server/server/pid-lock.js +21 -0
  16. package/dist/server/server/pid-lock.js.map +1 -1
  17. package/dist/server/server/session.d.ts.map +1 -1
  18. package/dist/server/server/session.js +2 -2
  19. package/dist/server/server/session.js.map +1 -1
  20. package/dist/src/server/pid-lock.js +21 -0
  21. package/dist/src/server/pid-lock.js.map +1 -1
  22. package/package.json +3 -3
  23. package/dist/scripts/daemon-runner.js.map +0 -1
  24. package/dist/src/server/agent/activity-curator.js +0 -243
  25. package/dist/src/server/agent/activity-curator.js.map +0 -1
  26. package/dist/src/server/agent/agent-manager.js +0 -1855
  27. package/dist/src/server/agent/agent-manager.js.map +0 -1
  28. package/dist/src/server/agent/agent-metadata-generator.js +0 -161
  29. package/dist/src/server/agent/agent-metadata-generator.js.map +0 -1
  30. package/dist/src/server/agent/agent-projections.js +0 -254
  31. package/dist/src/server/agent/agent-projections.js.map +0 -1
  32. package/dist/src/server/agent/agent-response-loop.js +0 -304
  33. package/dist/src/server/agent/agent-response-loop.js.map +0 -1
  34. package/dist/src/server/agent/agent-sdk-types.js +0 -12
  35. package/dist/src/server/agent/agent-sdk-types.js.map +0 -1
  36. package/dist/src/server/agent/agent-storage.js +0 -302
  37. package/dist/src/server/agent/agent-storage.js.map +0 -1
  38. package/dist/src/server/agent/agent-title-limits.js +0 -3
  39. package/dist/src/server/agent/agent-title-limits.js.map +0 -1
  40. package/dist/src/server/agent/audio-utils.js +0 -19
  41. package/dist/src/server/agent/audio-utils.js.map +0 -1
  42. package/dist/src/server/agent/dictation-debug.js +0 -50
  43. package/dist/src/server/agent/dictation-debug.js.map +0 -1
  44. package/dist/src/server/agent/mcp-server.js +0 -754
  45. package/dist/src/server/agent/mcp-server.js.map +0 -1
  46. package/dist/src/server/agent/orchestrator-instructions.js +0 -51
  47. package/dist/src/server/agent/orchestrator-instructions.js.map +0 -1
  48. package/dist/src/server/agent/pcm16-resampler.js +0 -63
  49. package/dist/src/server/agent/pcm16-resampler.js.map +0 -1
  50. package/dist/src/server/agent/provider-launch-config.js +0 -213
  51. package/dist/src/server/agent/provider-launch-config.js.map +0 -1
  52. package/dist/src/server/agent/provider-manifest.js +0 -127
  53. package/dist/src/server/agent/provider-manifest.js.map +0 -1
  54. package/dist/src/server/agent/provider-registry.js +0 -45
  55. package/dist/src/server/agent/provider-registry.js.map +0 -1
  56. package/dist/src/server/agent/providers/claude/partial-json.js +0 -306
  57. package/dist/src/server/agent/providers/claude/partial-json.js.map +0 -1
  58. package/dist/src/server/agent/providers/claude/sdk-model-resolver.js +0 -104
  59. package/dist/src/server/agent/providers/claude/sdk-model-resolver.js.map +0 -1
  60. package/dist/src/server/agent/providers/claude/sidechain-tracker.js +0 -230
  61. package/dist/src/server/agent/providers/claude/sidechain-tracker.js.map +0 -1
  62. package/dist/src/server/agent/providers/claude/task-notification-tool-call.js +0 -267
  63. package/dist/src/server/agent/providers/claude/task-notification-tool-call.js.map +0 -1
  64. package/dist/src/server/agent/providers/claude/tool-call-detail-parser.js +0 -121
  65. package/dist/src/server/agent/providers/claude/tool-call-detail-parser.js.map +0 -1
  66. package/dist/src/server/agent/providers/claude/tool-call-mapper.js +0 -252
  67. package/dist/src/server/agent/providers/claude/tool-call-mapper.js.map +0 -1
  68. package/dist/src/server/agent/providers/claude-agent.js +0 -3166
  69. package/dist/src/server/agent/providers/claude-agent.js.map +0 -1
  70. package/dist/src/server/agent/providers/codex/tool-call-detail-parser.js +0 -104
  71. package/dist/src/server/agent/providers/codex/tool-call-detail-parser.js.map +0 -1
  72. package/dist/src/server/agent/providers/codex/tool-call-mapper.js +0 -758
  73. package/dist/src/server/agent/providers/codex/tool-call-mapper.js.map +0 -1
  74. package/dist/src/server/agent/providers/codex-app-server-agent.js +0 -2949
  75. package/dist/src/server/agent/providers/codex-app-server-agent.js.map +0 -1
  76. package/dist/src/server/agent/providers/codex-rollout-timeline.js +0 -544
  77. package/dist/src/server/agent/providers/codex-rollout-timeline.js.map +0 -1
  78. package/dist/src/server/agent/providers/opencode/tool-call-detail-parser.js +0 -39
  79. package/dist/src/server/agent/providers/opencode/tool-call-detail-parser.js.map +0 -1
  80. package/dist/src/server/agent/providers/opencode/tool-call-mapper.js +0 -144
  81. package/dist/src/server/agent/providers/opencode/tool-call-mapper.js.map +0 -1
  82. package/dist/src/server/agent/providers/opencode-agent.js +0 -1193
  83. package/dist/src/server/agent/providers/opencode-agent.js.map +0 -1
  84. package/dist/src/server/agent/providers/tool-call-detail-primitives.js +0 -686
  85. package/dist/src/server/agent/providers/tool-call-detail-primitives.js.map +0 -1
  86. package/dist/src/server/agent/providers/tool-call-mapper-utils.js +0 -115
  87. package/dist/src/server/agent/providers/tool-call-mapper-utils.js.map +0 -1
  88. package/dist/src/server/agent/recordings-debug.js +0 -19
  89. package/dist/src/server/agent/recordings-debug.js.map +0 -1
  90. package/dist/src/server/agent/stt-debug.js +0 -33
  91. package/dist/src/server/agent/stt-debug.js.map +0 -1
  92. package/dist/src/server/agent/stt-manager.js +0 -232
  93. package/dist/src/server/agent/stt-manager.js.map +0 -1
  94. package/dist/src/server/agent/timeline-append.js +0 -27
  95. package/dist/src/server/agent/timeline-append.js.map +0 -1
  96. package/dist/src/server/agent/timeline-projection.js +0 -215
  97. package/dist/src/server/agent/timeline-projection.js.map +0 -1
  98. package/dist/src/server/agent/tool-name-normalization.js +0 -45
  99. package/dist/src/server/agent/tool-name-normalization.js.map +0 -1
  100. package/dist/src/server/agent/tts-debug.js +0 -24
  101. package/dist/src/server/agent/tts-debug.js.map +0 -1
  102. package/dist/src/server/agent/tts-manager.js +0 -374
  103. package/dist/src/server/agent/tts-manager.js.map +0 -1
  104. package/dist/src/server/agent/wait-for-agent-tracker.js +0 -53
  105. package/dist/src/server/agent/wait-for-agent-tracker.js.map +0 -1
  106. package/dist/src/server/agent-attention-policy.js +0 -40
  107. package/dist/src/server/agent-attention-policy.js.map +0 -1
  108. package/dist/src/server/allowed-hosts.js +0 -94
  109. package/dist/src/server/allowed-hosts.js.map +0 -1
  110. package/dist/src/server/bootstrap.js +0 -620
  111. package/dist/src/server/bootstrap.js.map +0 -1
  112. package/dist/src/server/chat/chat-mentions.js +0 -71
  113. package/dist/src/server/chat/chat-mentions.js.map +0 -1
  114. package/dist/src/server/chat/chat-rpc-schemas.js +0 -103
  115. package/dist/src/server/chat/chat-rpc-schemas.js.map +0 -1
  116. package/dist/src/server/chat/chat-service.js +0 -330
  117. package/dist/src/server/chat/chat-service.js.map +0 -1
  118. package/dist/src/server/chat/chat-types.js +0 -22
  119. package/dist/src/server/chat/chat-types.js.map +0 -1
  120. package/dist/src/server/checkout-diff-manager.js +0 -272
  121. package/dist/src/server/checkout-diff-manager.js.map +0 -1
  122. package/dist/src/server/checkout-git-utils.js +0 -37
  123. package/dist/src/server/checkout-git-utils.js.map +0 -1
  124. package/dist/src/server/client-message-id.js +0 -12
  125. package/dist/src/server/client-message-id.js.map +0 -1
  126. package/dist/src/server/config.js +0 -73
  127. package/dist/src/server/config.js.map +0 -1
  128. package/dist/src/server/connection-offer.js +0 -59
  129. package/dist/src/server/connection-offer.js.map +0 -1
  130. package/dist/src/server/daemon-keypair.js +0 -40
  131. package/dist/src/server/daemon-keypair.js.map +0 -1
  132. package/dist/src/server/daemon-version.js +0 -22
  133. package/dist/src/server/daemon-version.js.map +0 -1
  134. package/dist/src/server/dictation/dictation-stream-manager.js +0 -571
  135. package/dist/src/server/dictation/dictation-stream-manager.js.map +0 -1
  136. package/dist/src/server/file-download/token-store.js +0 -40
  137. package/dist/src/server/file-download/token-store.js.map +0 -1
  138. package/dist/src/server/file-explorer/service.js +0 -180
  139. package/dist/src/server/file-explorer/service.js.map +0 -1
  140. package/dist/src/server/json-utils.js +0 -45
  141. package/dist/src/server/json-utils.js.map +0 -1
  142. package/dist/src/server/loop/rpc-schemas.js +0 -159
  143. package/dist/src/server/loop/rpc-schemas.js.map +0 -1
  144. package/dist/src/server/loop-service.js +0 -741
  145. package/dist/src/server/loop-service.js.map +0 -1
  146. package/dist/src/server/messages.js +0 -29
  147. package/dist/src/server/messages.js.map +0 -1
  148. package/dist/src/server/package-version.js +0 -46
  149. package/dist/src/server/package-version.js.map +0 -1
  150. package/dist/src/server/pairing-offer.js +0 -45
  151. package/dist/src/server/pairing-offer.js.map +0 -1
  152. package/dist/src/server/pairing-qr.js +0 -45
  153. package/dist/src/server/pairing-qr.js.map +0 -1
  154. package/dist/src/server/path-utils.js +0 -20
  155. package/dist/src/server/path-utils.js.map +0 -1
  156. package/dist/src/server/persisted-config.js +0 -265
  157. package/dist/src/server/persisted-config.js.map +0 -1
  158. package/dist/src/server/persistence-hooks.js +0 -60
  159. package/dist/src/server/persistence-hooks.js.map +0 -1
  160. package/dist/src/server/push/push-service.js +0 -68
  161. package/dist/src/server/push/push-service.js.map +0 -1
  162. package/dist/src/server/push/token-store.js +0 -70
  163. package/dist/src/server/push/token-store.js.map +0 -1
  164. package/dist/src/server/relay-transport.js +0 -461
  165. package/dist/src/server/relay-transport.js.map +0 -1
  166. package/dist/src/server/schedule/cron.js +0 -103
  167. package/dist/src/server/schedule/cron.js.map +0 -1
  168. package/dist/src/server/schedule/rpc-schemas.js +0 -112
  169. package/dist/src/server/schedule/rpc-schemas.js.map +0 -1
  170. package/dist/src/server/schedule/service.js +0 -397
  171. package/dist/src/server/schedule/service.js.map +0 -1
  172. package/dist/src/server/schedule/store.js +0 -56
  173. package/dist/src/server/schedule/store.js.map +0 -1
  174. package/dist/src/server/schedule/types.js +0 -73
  175. package/dist/src/server/schedule/types.js.map +0 -1
  176. package/dist/src/server/server-id.js +0 -63
  177. package/dist/src/server/server-id.js.map +0 -1
  178. package/dist/src/server/session.js +0 -6381
  179. package/dist/src/server/session.js.map +0 -1
  180. package/dist/src/server/speech/audio.js +0 -101
  181. package/dist/src/server/speech/audio.js.map +0 -1
  182. package/dist/src/server/speech/provider-resolver.js +0 -7
  183. package/dist/src/server/speech/provider-resolver.js.map +0 -1
  184. package/dist/src/server/speech/providers/local/config.js +0 -74
  185. package/dist/src/server/speech/providers/local/config.js.map +0 -1
  186. package/dist/src/server/speech/providers/local/models.js +0 -17
  187. package/dist/src/server/speech/providers/local/models.js.map +0 -1
  188. package/dist/src/server/speech/providers/local/pocket/pocket-tts-onnx.js +0 -436
  189. package/dist/src/server/speech/providers/local/pocket/pocket-tts-onnx.js.map +0 -1
  190. package/dist/src/server/speech/providers/local/runtime.js +0 -238
  191. package/dist/src/server/speech/providers/local/runtime.js.map +0 -1
  192. package/dist/src/server/speech/providers/local/sherpa/model-catalog.js +0 -166
  193. package/dist/src/server/speech/providers/local/sherpa/model-catalog.js.map +0 -1
  194. package/dist/src/server/speech/providers/local/sherpa/model-downloader.js +0 -165
  195. package/dist/src/server/speech/providers/local/sherpa/model-downloader.js.map +0 -1
  196. package/dist/src/server/speech/providers/local/sherpa/sherpa-offline-recognizer.js +0 -73
  197. package/dist/src/server/speech/providers/local/sherpa/sherpa-offline-recognizer.js.map +0 -1
  198. package/dist/src/server/speech/providers/local/sherpa/sherpa-online-recognizer.js +0 -84
  199. package/dist/src/server/speech/providers/local/sherpa/sherpa-online-recognizer.js.map +0 -1
  200. package/dist/src/server/speech/providers/local/sherpa/sherpa-onnx-loader.js +0 -11
  201. package/dist/src/server/speech/providers/local/sherpa/sherpa-onnx-loader.js.map +0 -1
  202. package/dist/src/server/speech/providers/local/sherpa/sherpa-onnx-node-loader.js +0 -102
  203. package/dist/src/server/speech/providers/local/sherpa/sherpa-onnx-node-loader.js.map +0 -1
  204. package/dist/src/server/speech/providers/local/sherpa/sherpa-parakeet-realtime-session.js +0 -135
  205. package/dist/src/server/speech/providers/local/sherpa/sherpa-parakeet-realtime-session.js.map +0 -1
  206. package/dist/src/server/speech/providers/local/sherpa/sherpa-parakeet-stt.js +0 -130
  207. package/dist/src/server/speech/providers/local/sherpa/sherpa-parakeet-stt.js.map +0 -1
  208. package/dist/src/server/speech/providers/local/sherpa/sherpa-realtime-session.js +0 -110
  209. package/dist/src/server/speech/providers/local/sherpa/sherpa-realtime-session.js.map +0 -1
  210. package/dist/src/server/speech/providers/local/sherpa/sherpa-stt.js +0 -138
  211. package/dist/src/server/speech/providers/local/sherpa/sherpa-stt.js.map +0 -1
  212. package/dist/src/server/speech/providers/local/sherpa/sherpa-tts.js +0 -98
  213. package/dist/src/server/speech/providers/local/sherpa/sherpa-tts.js.map +0 -1
  214. package/dist/src/server/speech/providers/local/sherpa/silero-vad-provider.js +0 -23
  215. package/dist/src/server/speech/providers/local/sherpa/silero-vad-provider.js.map +0 -1
  216. package/dist/src/server/speech/providers/local/sherpa/silero-vad-session.js +0 -107
  217. package/dist/src/server/speech/providers/local/sherpa/silero-vad-session.js.map +0 -1
  218. package/dist/src/server/speech/providers/openai/config.js +0 -80
  219. package/dist/src/server/speech/providers/openai/config.js.map +0 -1
  220. package/dist/src/server/speech/providers/openai/realtime-transcription-session.js +0 -168
  221. package/dist/src/server/speech/providers/openai/realtime-transcription-session.js.map +0 -1
  222. package/dist/src/server/speech/providers/openai/runtime.js +0 -112
  223. package/dist/src/server/speech/providers/openai/runtime.js.map +0 -1
  224. package/dist/src/server/speech/providers/openai/stt.js +0 -206
  225. package/dist/src/server/speech/providers/openai/stt.js.map +0 -1
  226. package/dist/src/server/speech/providers/openai/tts.js +0 -46
  227. package/dist/src/server/speech/providers/openai/tts.js.map +0 -1
  228. package/dist/src/server/speech/speech-config-resolver.js +0 -102
  229. package/dist/src/server/speech/speech-config-resolver.js.map +0 -1
  230. package/dist/src/server/speech/speech-provider.js +0 -2
  231. package/dist/src/server/speech/speech-provider.js.map +0 -1
  232. package/dist/src/server/speech/speech-runtime.js +0 -530
  233. package/dist/src/server/speech/speech-runtime.js.map +0 -1
  234. package/dist/src/server/speech/speech-types.js +0 -8
  235. package/dist/src/server/speech/speech-types.js.map +0 -1
  236. package/dist/src/server/speech/turn-detection-provider.js +0 -2
  237. package/dist/src/server/speech/turn-detection-provider.js.map +0 -1
  238. package/dist/src/server/utils/diff-highlighter.js +0 -257
  239. package/dist/src/server/utils/diff-highlighter.js.map +0 -1
  240. package/dist/src/server/voice/fixed-duration-pcm-ring-buffer.js +0 -35
  241. package/dist/src/server/voice/fixed-duration-pcm-ring-buffer.js.map +0 -1
  242. package/dist/src/server/voice/voice-turn-controller.js +0 -159
  243. package/dist/src/server/voice/voice-turn-controller.js.map +0 -1
  244. package/dist/src/server/voice-config.js +0 -51
  245. package/dist/src/server/voice-config.js.map +0 -1
  246. package/dist/src/server/voice-mcp-bridge-command.js +0 -31
  247. package/dist/src/server/voice-mcp-bridge-command.js.map +0 -1
  248. package/dist/src/server/voice-mcp-bridge.js +0 -109
  249. package/dist/src/server/voice-mcp-bridge.js.map +0 -1
  250. package/dist/src/server/voice-permission-policy.js +0 -13
  251. package/dist/src/server/voice-permission-policy.js.map +0 -1
  252. package/dist/src/server/voice-types.js +0 -2
  253. package/dist/src/server/voice-types.js.map +0 -1
  254. package/dist/src/server/websocket-server.js +0 -1048
  255. package/dist/src/server/websocket-server.js.map +0 -1
  256. package/dist/src/server/workspace-registry-bootstrap.js +0 -98
  257. package/dist/src/server/workspace-registry-bootstrap.js.map +0 -1
  258. package/dist/src/server/workspace-registry-model.js +0 -175
  259. package/dist/src/server/workspace-registry-model.js.map +0 -1
  260. package/dist/src/server/workspace-registry.js +0 -151
  261. package/dist/src/server/workspace-registry.js.map +0 -1
  262. package/dist/src/server/worktree-bootstrap.js +0 -508
  263. package/dist/src/server/worktree-bootstrap.js.map +0 -1
  264. package/dist/src/shared/agent-attention-notification.js +0 -130
  265. package/dist/src/shared/agent-attention-notification.js.map +0 -1
  266. package/dist/src/shared/agent-lifecycle.js +0 -8
  267. package/dist/src/shared/agent-lifecycle.js.map +0 -1
  268. package/dist/src/shared/connection-offer.js +0 -17
  269. package/dist/src/shared/connection-offer.js.map +0 -1
  270. package/dist/src/shared/daemon-endpoints.js +0 -122
  271. package/dist/src/shared/daemon-endpoints.js.map +0 -1
  272. package/dist/src/shared/messages.js +0 -2107
  273. package/dist/src/shared/messages.js.map +0 -1
  274. package/dist/src/shared/path-utils.js +0 -16
  275. package/dist/src/shared/path-utils.js.map +0 -1
  276. package/dist/src/shared/terminal-stream-protocol.js +0 -99
  277. package/dist/src/shared/terminal-stream-protocol.js.map +0 -1
  278. package/dist/src/shared/tool-call-display.js +0 -122
  279. package/dist/src/shared/tool-call-display.js.map +0 -1
  280. package/dist/src/terminal/terminal-manager.js +0 -136
  281. package/dist/src/terminal/terminal-manager.js.map +0 -1
  282. package/dist/src/terminal/terminal.js +0 -333
  283. package/dist/src/terminal/terminal.js.map +0 -1
  284. package/dist/src/utils/checkout-git.js +0 -1518
  285. package/dist/src/utils/checkout-git.js.map +0 -1
  286. package/dist/src/utils/directory-suggestions.js +0 -671
  287. package/dist/src/utils/directory-suggestions.js.map +0 -1
  288. package/dist/src/utils/path.js +0 -15
  289. package/dist/src/utils/path.js.map +0 -1
  290. package/dist/src/utils/project-icon.js +0 -389
  291. package/dist/src/utils/project-icon.js.map +0 -1
  292. package/dist/src/utils/worktree-metadata.js +0 -116
  293. package/dist/src/utils/worktree-metadata.js.map +0 -1
  294. package/dist/src/utils/worktree.js +0 -744
  295. package/dist/src/utils/worktree.js.map +0 -1
@@ -1,1048 +0,0 @@
1
- import { WebSocketServer } from "ws";
2
- import { join } from "path";
3
- import { hostname as getHostname } from "node:os";
4
- import { WSInboundMessageSchema, wrapSessionMessage, } from "./messages.js";
5
- import { asUint8Array, decodeTerminalStreamFrame, } from "../shared/terminal-stream-protocol.js";
6
- import { isHostAllowed } from "./allowed-hosts.js";
7
- import { Session } from "./session.js";
8
- import { PushTokenStore } from "./push/token-store.js";
9
- import { PushService } from "./push/push-service.js";
10
- import { computeShouldNotifyClient, computeShouldSendPush, } from "./agent-attention-policy.js";
11
- import { buildAgentAttentionNotificationPayload, findLatestAssistantMessageFromTimeline, findLatestPermissionRequest, } from "../shared/agent-attention-notification.js";
12
- function createNoopProjectRegistry() {
13
- return {
14
- initialize: async () => { },
15
- existsOnDisk: async () => true,
16
- list: async () => [],
17
- get: async () => null,
18
- upsert: async () => { },
19
- archive: async () => { },
20
- remove: async () => { },
21
- };
22
- }
23
- function createNoopWorkspaceRegistry() {
24
- return {
25
- initialize: async () => { },
26
- existsOnDisk: async () => true,
27
- list: async () => [],
28
- get: async () => null,
29
- upsert: async () => { },
30
- archive: async () => { },
31
- remove: async () => { },
32
- };
33
- }
34
- function toServerCapabilityState(params) {
35
- const { state, reason } = params;
36
- return {
37
- enabled: state.enabled,
38
- reason,
39
- };
40
- }
41
- function resolveCapabilityReason(params) {
42
- const { state, readiness } = params;
43
- if (state.available) {
44
- return "";
45
- }
46
- if (readiness.voiceFeature.reasonCode === "model_download_in_progress") {
47
- const baseMessage = readiness.voiceFeature.message.trim();
48
- if (baseMessage.includes("Try again in a few minutes")) {
49
- return baseMessage;
50
- }
51
- return `${baseMessage} Try again in a few minutes.`;
52
- }
53
- return state.message;
54
- }
55
- function buildServerCapabilities(params) {
56
- const readiness = params.readiness;
57
- if (!readiness) {
58
- return undefined;
59
- }
60
- return {
61
- voice: {
62
- dictation: toServerCapabilityState({
63
- state: readiness.dictation,
64
- reason: resolveCapabilityReason({
65
- state: readiness.dictation,
66
- readiness,
67
- }),
68
- }),
69
- voice: toServerCapabilityState({
70
- state: readiness.realtimeVoice,
71
- reason: resolveCapabilityReason({
72
- state: readiness.realtimeVoice,
73
- readiness,
74
- }),
75
- }),
76
- },
77
- };
78
- }
79
- function areServerCapabilitiesEqual(current, next) {
80
- return JSON.stringify(current ?? null) === JSON.stringify(next ?? null);
81
- }
82
- function bufferFromWsData(data) {
83
- if (typeof data === "string")
84
- return Buffer.from(data, "utf8");
85
- if (Array.isArray(data)) {
86
- return Buffer.concat(data.map((item) => (Buffer.isBuffer(item) ? item : Buffer.from(item))));
87
- }
88
- if (Buffer.isBuffer(data))
89
- return data;
90
- return Buffer.from(data);
91
- }
92
- const SLOW_REQUEST_THRESHOLD_MS = 500;
93
- const EXTERNAL_SESSION_DISCONNECT_GRACE_MS = 90000;
94
- const HELLO_TIMEOUT_MS = 15000;
95
- const WS_CLOSE_HELLO_TIMEOUT = 4001;
96
- const WS_CLOSE_INVALID_HELLO = 4002;
97
- const WS_CLOSE_INCOMPATIBLE_PROTOCOL = 4003;
98
- const WS_PROTOCOL_VERSION = 1;
99
- const WS_RUNTIME_METRICS_FLUSH_MS = 30000;
100
- export class MissingDaemonVersionError extends Error {
101
- constructor() {
102
- super("VoiceAssistantWebSocketServer requires a non-empty daemonVersion.");
103
- this.name = "MissingDaemonVersionError";
104
- }
105
- }
106
- /**
107
- * WebSocket server that only accepts sockets + parses/forwards messages to the session layer.
108
- */
109
- export class VoiceAssistantWebSocketServer {
110
- constructor(server, logger, serverId, agentManager, agentStorage, downloadTokenStore, paseoHome, createAgentMcpTransport, wsConfig, speech, terminalManager, voice, dictation, agentProviderRuntimeSettings, daemonVersion, onLifecycleIntent, projectRegistry, workspaceRegistry, chatService, loopService, scheduleService, checkoutDiffManager) {
111
- this.pendingConnections = new Map();
112
- this.sessions = new Map();
113
- this.externalSessionsByKey = new Map();
114
- this.voiceSpeakHandlers = new Map();
115
- this.voiceCallerContexts = new Map();
116
- this.runtimeWindowStartedAt = Date.now();
117
- this.runtimeCounters = {
118
- connectedAwaitingHello: 0,
119
- helloResumed: 0,
120
- helloNew: 0,
121
- pendingDisconnected: 0,
122
- sessionDisconnectedWaitingReconnect: 0,
123
- sessionSocketDisconnectedAttached: 0,
124
- sessionCleanup: 0,
125
- validationFailed: 0,
126
- binaryBeforeHelloRejected: 0,
127
- pendingMessageRejectedBeforeHello: 0,
128
- missingConnectionForMessage: 0,
129
- unexpectedHelloOnActiveConnection: 0,
130
- relayExternalSocketAttached: 0,
131
- originRejected: 0,
132
- hostRejected: 0,
133
- };
134
- this.inboundMessageCounts = new Map();
135
- this.inboundSessionRequestCounts = new Map();
136
- this.requestLatencies = new Map();
137
- this.runtimeMetricsInterval = null;
138
- this.ACTIVITY_THRESHOLD_MS = 120000;
139
- this.logger = logger.child({ module: "websocket-server" });
140
- this.serverId = serverId;
141
- if (typeof daemonVersion !== "string" || daemonVersion.trim().length === 0) {
142
- throw new MissingDaemonVersionError();
143
- }
144
- this.daemonVersion = daemonVersion.trim();
145
- this.agentManager = agentManager;
146
- this.agentStorage = agentStorage;
147
- this.projectRegistry = projectRegistry ?? createNoopProjectRegistry();
148
- this.workspaceRegistry = workspaceRegistry ?? createNoopWorkspaceRegistry();
149
- if (!chatService) {
150
- throw new Error("VoiceAssistantWebSocketServer requires a chat service.");
151
- }
152
- this.chatService = chatService;
153
- if (!loopService) {
154
- throw new Error("VoiceAssistantWebSocketServer requires a loop service.");
155
- }
156
- this.loopService = loopService;
157
- if (!scheduleService) {
158
- throw new Error("VoiceAssistantWebSocketServer requires a schedule service.");
159
- }
160
- this.scheduleService = scheduleService;
161
- if (!checkoutDiffManager) {
162
- throw new Error("VoiceAssistantWebSocketServer requires a checkout diff manager.");
163
- }
164
- this.checkoutDiffManager = checkoutDiffManager;
165
- this.downloadTokenStore = downloadTokenStore;
166
- this.paseoHome = paseoHome;
167
- this.createAgentMcpTransport = createAgentMcpTransport;
168
- this.turnDetection = speech?.turnDetection ?? null;
169
- this.stt = speech?.stt ?? null;
170
- this.tts = speech?.tts ?? null;
171
- this.terminalManager = terminalManager ?? null;
172
- this.voice = voice ?? null;
173
- this.dictation = dictation ?? null;
174
- this.agentProviderRuntimeSettings = agentProviderRuntimeSettings;
175
- this.onLifecycleIntent = onLifecycleIntent ?? null;
176
- this.serverCapabilities = buildServerCapabilities({
177
- readiness: this.dictation?.getSpeechReadiness?.() ?? null,
178
- });
179
- const pushLogger = this.logger.child({ module: "push" });
180
- this.pushTokenStore = new PushTokenStore(pushLogger, join(paseoHome, "push-tokens.json"));
181
- this.pushService = new PushService(pushLogger, this.pushTokenStore);
182
- this.agentManager.setAgentAttentionCallback((params) => {
183
- this.broadcastAgentAttention(params);
184
- });
185
- const { allowedOrigins, allowedHosts } = wsConfig;
186
- this.wss = new WebSocketServer({
187
- server,
188
- path: "/ws",
189
- verifyClient: ({ req }, callback) => {
190
- const requestMetadata = extractSocketRequestMetadata(req);
191
- const origin = requestMetadata.origin;
192
- const requestHost = requestMetadata.host ?? null;
193
- if (requestHost && !isHostAllowed(requestHost, allowedHosts)) {
194
- this.incrementRuntimeCounter("hostRejected");
195
- this.logger.warn({ ...requestMetadata, host: requestHost }, "Rejected connection from disallowed host");
196
- callback(false, 403, "Host not allowed");
197
- return;
198
- }
199
- const sameOrigin = !!origin &&
200
- !!requestHost &&
201
- (origin === `http://${requestHost}` || origin === `https://${requestHost}`);
202
- if (!origin || allowedOrigins.has(origin) || sameOrigin) {
203
- callback(true);
204
- }
205
- else {
206
- this.incrementRuntimeCounter("originRejected");
207
- this.logger.warn({ ...requestMetadata, origin }, "Rejected connection from origin");
208
- callback(false, 403, "Origin not allowed");
209
- }
210
- },
211
- });
212
- this.wss.on("connection", (ws, request) => {
213
- void this.attachSocket(ws, request);
214
- });
215
- const runtimeMetricsInterval = setInterval(() => {
216
- this.flushRuntimeMetrics();
217
- }, WS_RUNTIME_METRICS_FLUSH_MS);
218
- this.runtimeMetricsInterval = runtimeMetricsInterval;
219
- runtimeMetricsInterval.unref?.();
220
- this.logger.info("WebSocket server initialized on /ws");
221
- }
222
- broadcast(message) {
223
- const payload = JSON.stringify(message);
224
- for (const ws of this.sessions.keys()) {
225
- // WebSocket.OPEN = 1
226
- if (ws.readyState === 1) {
227
- ws.send(payload);
228
- }
229
- }
230
- }
231
- publishSpeechReadiness(readiness) {
232
- this.updateServerCapabilities(buildServerCapabilities({ readiness }));
233
- }
234
- updateServerCapabilities(capabilities) {
235
- const next = capabilities ?? undefined;
236
- if (areServerCapabilitiesEqual(this.serverCapabilities, next)) {
237
- return;
238
- }
239
- this.serverCapabilities = next;
240
- this.broadcastCapabilitiesUpdate();
241
- }
242
- async attachExternalSocket(ws, metadata) {
243
- if (metadata?.transport === "relay") {
244
- this.incrementRuntimeCounter("relayExternalSocketAttached");
245
- }
246
- await this.attachSocket(ws, undefined, metadata);
247
- }
248
- async close() {
249
- if (this.runtimeMetricsInterval) {
250
- clearInterval(this.runtimeMetricsInterval);
251
- this.runtimeMetricsInterval = null;
252
- }
253
- this.flushRuntimeMetrics({ final: true });
254
- const uniqueConnections = new Set([
255
- ...this.sessions.values(),
256
- ...this.externalSessionsByKey.values(),
257
- ]);
258
- const pendingSockets = new Set(this.pendingConnections.keys());
259
- for (const pending of this.pendingConnections.values()) {
260
- if (pending.helloTimeout) {
261
- clearTimeout(pending.helloTimeout);
262
- pending.helloTimeout = null;
263
- }
264
- }
265
- const cleanupPromises = [];
266
- for (const connection of uniqueConnections) {
267
- if (connection.externalDisconnectCleanupTimeout) {
268
- clearTimeout(connection.externalDisconnectCleanupTimeout);
269
- connection.externalDisconnectCleanupTimeout = null;
270
- }
271
- cleanupPromises.push(connection.session.cleanup());
272
- for (const ws of connection.sockets) {
273
- cleanupPromises.push(new Promise((resolve) => {
274
- // WebSocket.CLOSED = 3
275
- if (ws.readyState === 3) {
276
- resolve();
277
- return;
278
- }
279
- ws.once("close", () => resolve());
280
- ws.close();
281
- }));
282
- }
283
- }
284
- for (const ws of pendingSockets) {
285
- cleanupPromises.push(new Promise((resolve) => {
286
- if (ws.readyState === 3) {
287
- resolve();
288
- return;
289
- }
290
- ws.once("close", () => resolve());
291
- ws.close();
292
- }));
293
- }
294
- await Promise.all(cleanupPromises);
295
- this.checkoutDiffManager.dispose();
296
- this.pendingConnections.clear();
297
- this.sessions.clear();
298
- this.externalSessionsByKey.clear();
299
- this.wss.close();
300
- }
301
- sendToClient(ws, message) {
302
- // WebSocket.OPEN = 1
303
- if (ws.readyState === 1) {
304
- ws.send(JSON.stringify(message));
305
- }
306
- }
307
- sendBinaryToClient(ws, frame) {
308
- if (ws.readyState !== 1) {
309
- return;
310
- }
311
- ws.send(frame);
312
- }
313
- sendToConnection(connection, message) {
314
- for (const ws of connection.sockets) {
315
- this.sendToClient(ws, message);
316
- }
317
- }
318
- sendBinaryToConnection(connection, frame) {
319
- for (const ws of connection.sockets) {
320
- this.sendBinaryToClient(ws, frame);
321
- }
322
- }
323
- async attachSocket(ws, request, metadata) {
324
- const requestMetadata = extractSocketRequestMetadata(request);
325
- const connectionLoggerFields = {
326
- transport: metadata?.transport === "relay" ? "relay" : "direct",
327
- };
328
- if (requestMetadata.host) {
329
- connectionLoggerFields.host = requestMetadata.host;
330
- }
331
- if (requestMetadata.origin) {
332
- connectionLoggerFields.origin = requestMetadata.origin;
333
- }
334
- if (requestMetadata.userAgent) {
335
- connectionLoggerFields.userAgent = requestMetadata.userAgent;
336
- }
337
- if (requestMetadata.remoteAddress) {
338
- connectionLoggerFields.remoteAddress = requestMetadata.remoteAddress;
339
- }
340
- const connectionLogger = this.logger.child(connectionLoggerFields);
341
- const pending = {
342
- connectionLogger,
343
- helloTimeout: null,
344
- };
345
- const timeout = setTimeout(() => {
346
- if (this.pendingConnections.get(ws) !== pending) {
347
- return;
348
- }
349
- pending.helloTimeout = null;
350
- this.pendingConnections.delete(ws);
351
- pending.connectionLogger.warn({ timeoutMs: HELLO_TIMEOUT_MS }, "Closing connection due to missing hello");
352
- try {
353
- ws.close(WS_CLOSE_HELLO_TIMEOUT, "Hello timeout");
354
- }
355
- catch {
356
- // ignore close errors
357
- }
358
- }, HELLO_TIMEOUT_MS);
359
- pending.helloTimeout = timeout;
360
- timeout.unref?.();
361
- this.pendingConnections.set(ws, pending);
362
- this.incrementRuntimeCounter("connectedAwaitingHello");
363
- this.bindSocketHandlers(ws);
364
- pending.connectionLogger.trace({
365
- totalPendingConnections: this.pendingConnections.size,
366
- }, "Client connected; awaiting hello");
367
- }
368
- createSessionConnection(params) {
369
- const { ws, clientId, connectionLogger } = params;
370
- let connection = null;
371
- const session = new Session({
372
- clientId,
373
- onMessage: (msg) => {
374
- if (!connection) {
375
- return;
376
- }
377
- this.sendToConnection(connection, wrapSessionMessage(msg));
378
- },
379
- onBinaryMessage: (frame) => {
380
- if (!connection) {
381
- return;
382
- }
383
- this.sendBinaryToConnection(connection, frame);
384
- },
385
- getBinaryBufferedAmount: () => {
386
- if (!connection) {
387
- return 0;
388
- }
389
- let bufferedAmount = 0;
390
- for (const socket of connection.sockets) {
391
- bufferedAmount = Math.max(bufferedAmount, socket.bufferedAmount ?? 0);
392
- }
393
- return bufferedAmount;
394
- },
395
- onLifecycleIntent: (intent) => {
396
- this.onLifecycleIntent?.(intent);
397
- },
398
- logger: connectionLogger.child({ module: "session" }),
399
- downloadTokenStore: this.downloadTokenStore,
400
- pushTokenStore: this.pushTokenStore,
401
- paseoHome: this.paseoHome,
402
- agentManager: this.agentManager,
403
- agentStorage: this.agentStorage,
404
- projectRegistry: this.projectRegistry,
405
- workspaceRegistry: this.workspaceRegistry,
406
- chatService: this.chatService,
407
- loopService: this.loopService,
408
- scheduleService: this.scheduleService,
409
- checkoutDiffManager: this.checkoutDiffManager,
410
- createAgentMcpTransport: this.createAgentMcpTransport,
411
- stt: this.stt,
412
- tts: this.tts,
413
- terminalManager: this.terminalManager,
414
- voice: {
415
- ...(this.voice ?? {}),
416
- turnDetection: this.turnDetection,
417
- },
418
- voiceBridge: {
419
- registerVoiceSpeakHandler: (agentId, handler) => {
420
- this.voiceSpeakHandlers.set(agentId, handler);
421
- },
422
- unregisterVoiceSpeakHandler: (agentId) => {
423
- this.voiceSpeakHandlers.delete(agentId);
424
- },
425
- registerVoiceCallerContext: (agentId, context) => {
426
- this.voiceCallerContexts.set(agentId, context);
427
- },
428
- unregisterVoiceCallerContext: (agentId) => {
429
- this.voiceCallerContexts.delete(agentId);
430
- },
431
- ensureVoiceMcpSocketForAgent: this.voice?.ensureVoiceMcpSocketForAgent,
432
- removeVoiceMcpSocketForAgent: this.voice?.removeVoiceMcpSocketForAgent,
433
- },
434
- dictation: this.dictation ?? undefined,
435
- agentProviderRuntimeSettings: this.agentProviderRuntimeSettings,
436
- });
437
- connection = {
438
- session,
439
- clientId,
440
- connectionLogger,
441
- sockets: new Set([ws]),
442
- externalDisconnectCleanupTimeout: null,
443
- };
444
- return connection;
445
- }
446
- clearPendingConnection(ws) {
447
- const pending = this.pendingConnections.get(ws);
448
- if (!pending) {
449
- return null;
450
- }
451
- if (pending.helloTimeout) {
452
- clearTimeout(pending.helloTimeout);
453
- pending.helloTimeout = null;
454
- }
455
- this.pendingConnections.delete(ws);
456
- return pending;
457
- }
458
- handleHello(params) {
459
- const { ws, message, pending } = params;
460
- if (message.protocolVersion !== WS_PROTOCOL_VERSION) {
461
- this.clearPendingConnection(ws);
462
- pending.connectionLogger.warn({
463
- receivedProtocolVersion: message.protocolVersion,
464
- expectedProtocolVersion: WS_PROTOCOL_VERSION,
465
- }, "Rejected hello due to protocol version mismatch");
466
- try {
467
- ws.close(WS_CLOSE_INCOMPATIBLE_PROTOCOL, "Incompatible protocol version");
468
- }
469
- catch {
470
- // ignore close errors
471
- }
472
- return;
473
- }
474
- const clientId = message.clientId.trim();
475
- if (clientId.length === 0) {
476
- this.clearPendingConnection(ws);
477
- pending.connectionLogger.warn("Rejected hello with empty clientId");
478
- try {
479
- ws.close(WS_CLOSE_INVALID_HELLO, "Invalid hello");
480
- }
481
- catch {
482
- // ignore close errors
483
- }
484
- return;
485
- }
486
- this.clearPendingConnection(ws);
487
- const existing = this.externalSessionsByKey.get(clientId);
488
- if (existing) {
489
- this.incrementRuntimeCounter("helloResumed");
490
- if (existing.externalDisconnectCleanupTimeout) {
491
- clearTimeout(existing.externalDisconnectCleanupTimeout);
492
- existing.externalDisconnectCleanupTimeout = null;
493
- }
494
- existing.sockets.add(ws);
495
- this.sessions.set(ws, existing);
496
- this.sendToClient(ws, this.createServerInfoMessage());
497
- existing.connectionLogger.trace({
498
- clientId,
499
- resumed: true,
500
- totalSessions: this.sessions.size,
501
- }, "Client connected via hello");
502
- return;
503
- }
504
- const connectionLogger = pending.connectionLogger.child({ clientId });
505
- this.incrementRuntimeCounter("helloNew");
506
- const connection = this.createSessionConnection({
507
- ws,
508
- clientId,
509
- connectionLogger,
510
- });
511
- this.sessions.set(ws, connection);
512
- this.externalSessionsByKey.set(clientId, connection);
513
- this.sendToClient(ws, this.createServerInfoMessage());
514
- connection.connectionLogger.trace({
515
- clientId,
516
- resumed: false,
517
- totalSessions: this.sessions.size,
518
- }, "Client connected via hello");
519
- }
520
- buildServerInfoStatusPayload() {
521
- return {
522
- status: "server_info",
523
- serverId: this.serverId,
524
- hostname: getHostname(),
525
- version: this.daemonVersion,
526
- ...(this.serverCapabilities ? { capabilities: this.serverCapabilities } : {}),
527
- };
528
- }
529
- createServerInfoMessage() {
530
- return {
531
- type: "session",
532
- message: {
533
- type: "status",
534
- payload: this.buildServerInfoStatusPayload(),
535
- },
536
- };
537
- }
538
- broadcastCapabilitiesUpdate() {
539
- this.broadcast(this.createServerInfoMessage());
540
- }
541
- bindSocketHandlers(ws) {
542
- ws.on("message", (data) => {
543
- void this.handleRawMessage(ws, data);
544
- });
545
- ws.on("close", async (code, reason) => {
546
- await this.detachSocket(ws, {
547
- code: typeof code === "number" ? code : undefined,
548
- reason,
549
- });
550
- });
551
- ws.on("error", async (error) => {
552
- const err = error instanceof Error ? error : new Error(String(error));
553
- const active = this.sessions.get(ws);
554
- const pending = this.pendingConnections.get(ws);
555
- const log = active?.connectionLogger ?? pending?.connectionLogger ?? this.logger;
556
- log.error({ err }, "Client error");
557
- await this.detachSocket(ws, { error: err });
558
- });
559
- }
560
- resolveVoiceSpeakHandler(callerAgentId) {
561
- return this.voiceSpeakHandlers.get(callerAgentId) ?? null;
562
- }
563
- resolveVoiceCallerContext(callerAgentId) {
564
- return this.voiceCallerContexts.get(callerAgentId) ?? null;
565
- }
566
- async detachSocket(ws, details) {
567
- const pending = this.clearPendingConnection(ws);
568
- if (pending) {
569
- this.incrementRuntimeCounter("pendingDisconnected");
570
- pending.connectionLogger.trace({
571
- code: details.code,
572
- reason: stringifyCloseReason(details.reason),
573
- }, "Pending client disconnected");
574
- return;
575
- }
576
- const connection = this.sessions.get(ws);
577
- if (!connection) {
578
- return;
579
- }
580
- this.sessions.delete(ws);
581
- connection.sockets.delete(ws);
582
- if (connection.sockets.size === 0) {
583
- this.incrementRuntimeCounter("sessionDisconnectedWaitingReconnect");
584
- if (connection.externalDisconnectCleanupTimeout) {
585
- clearTimeout(connection.externalDisconnectCleanupTimeout);
586
- }
587
- const timeout = setTimeout(() => {
588
- if (connection.externalDisconnectCleanupTimeout !== timeout) {
589
- return;
590
- }
591
- connection.externalDisconnectCleanupTimeout = null;
592
- void this.cleanupConnection(connection, "Client disconnected (grace timeout)");
593
- }, EXTERNAL_SESSION_DISCONNECT_GRACE_MS);
594
- connection.externalDisconnectCleanupTimeout = timeout;
595
- connection.connectionLogger.trace({
596
- clientId: connection.clientId,
597
- code: details.code,
598
- reason: stringifyCloseReason(details.reason),
599
- reconnectGraceMs: EXTERNAL_SESSION_DISCONNECT_GRACE_MS,
600
- }, "Client disconnected; waiting for reconnect");
601
- return;
602
- }
603
- if (connection.sockets.size > 0) {
604
- this.incrementRuntimeCounter("sessionSocketDisconnectedAttached");
605
- connection.connectionLogger.trace({
606
- clientId: connection.clientId,
607
- remainingSockets: connection.sockets.size,
608
- code: details.code,
609
- reason: stringifyCloseReason(details.reason),
610
- }, "Client socket disconnected; session remains attached");
611
- return;
612
- }
613
- await this.cleanupConnection(connection, "Client disconnected");
614
- }
615
- async cleanupConnection(connection, logMessage) {
616
- this.incrementRuntimeCounter("sessionCleanup");
617
- if (connection.externalDisconnectCleanupTimeout) {
618
- clearTimeout(connection.externalDisconnectCleanupTimeout);
619
- connection.externalDisconnectCleanupTimeout = null;
620
- }
621
- for (const socket of connection.sockets) {
622
- this.sessions.delete(socket);
623
- }
624
- connection.sockets.clear();
625
- const existing = this.externalSessionsByKey.get(connection.clientId);
626
- if (existing === connection) {
627
- this.externalSessionsByKey.delete(connection.clientId);
628
- }
629
- connection.connectionLogger.trace({ clientId: connection.clientId, totalSessions: this.sessions.size }, logMessage);
630
- await connection.session.cleanup();
631
- }
632
- async handleRawMessage(ws, data) {
633
- const activeConnection = this.sessions.get(ws);
634
- const pendingConnection = this.pendingConnections.get(ws);
635
- const log = activeConnection?.connectionLogger ?? pendingConnection?.connectionLogger ?? this.logger;
636
- try {
637
- const buffer = bufferFromWsData(data);
638
- const asBytes = asUint8Array(buffer);
639
- if (asBytes) {
640
- const frame = decodeTerminalStreamFrame(asBytes);
641
- if (frame) {
642
- if (!activeConnection) {
643
- this.incrementRuntimeCounter("binaryBeforeHelloRejected");
644
- log.warn("Rejected binary frame before hello");
645
- this.clearPendingConnection(ws);
646
- try {
647
- ws.close(WS_CLOSE_INVALID_HELLO, "Session message before hello");
648
- }
649
- catch {
650
- // ignore close errors
651
- }
652
- return;
653
- }
654
- activeConnection.session.handleBinaryFrame(frame);
655
- return;
656
- }
657
- }
658
- const parsed = JSON.parse(buffer.toString());
659
- const parsedMessage = WSInboundMessageSchema.safeParse(parsed);
660
- if (!parsedMessage.success) {
661
- this.incrementRuntimeCounter("validationFailed");
662
- if (pendingConnection) {
663
- pendingConnection.connectionLogger.warn({
664
- error: parsedMessage.error.message,
665
- }, "Rejected pending message before hello");
666
- this.clearPendingConnection(ws);
667
- try {
668
- ws.close(WS_CLOSE_INVALID_HELLO, "Invalid hello");
669
- }
670
- catch {
671
- // ignore close errors
672
- }
673
- return;
674
- }
675
- const requestInfo = extractRequestInfoFromUnknownWsInbound(parsed);
676
- const isUnknownSchema = requestInfo?.requestId != null &&
677
- typeof parsed === "object" &&
678
- parsed != null &&
679
- "type" in parsed &&
680
- parsed.type === "session";
681
- log.warn({
682
- clientId: activeConnection?.clientId,
683
- requestId: requestInfo?.requestId,
684
- requestType: requestInfo?.requestType,
685
- error: parsedMessage.error.message,
686
- }, "WS inbound message validation failed");
687
- if (requestInfo) {
688
- this.sendToClient(ws, wrapSessionMessage({
689
- type: "rpc_error",
690
- payload: {
691
- requestId: requestInfo.requestId,
692
- requestType: requestInfo.requestType,
693
- error: isUnknownSchema ? "Unknown request schema" : "Invalid message",
694
- code: isUnknownSchema ? "unknown_schema" : "invalid_message",
695
- },
696
- }));
697
- return;
698
- }
699
- const errorMessage = `Invalid message: ${parsedMessage.error.message}`;
700
- this.sendToClient(ws, wrapSessionMessage({
701
- type: "status",
702
- payload: {
703
- status: "error",
704
- message: errorMessage,
705
- },
706
- }));
707
- return;
708
- }
709
- const message = parsedMessage.data;
710
- this.recordInboundMessageType(message.type);
711
- if (message.type === "ping") {
712
- this.sendToClient(ws, { type: "pong" });
713
- return;
714
- }
715
- if (message.type === "recording_state") {
716
- return;
717
- }
718
- if (pendingConnection) {
719
- if (message.type === "hello") {
720
- this.handleHello({
721
- ws,
722
- message,
723
- pending: pendingConnection,
724
- });
725
- return;
726
- }
727
- pendingConnection.connectionLogger.warn({
728
- messageType: message.type,
729
- }, "Rejected pending message before hello");
730
- this.incrementRuntimeCounter("pendingMessageRejectedBeforeHello");
731
- this.clearPendingConnection(ws);
732
- try {
733
- ws.close(WS_CLOSE_INVALID_HELLO, "Session message before hello");
734
- }
735
- catch {
736
- // ignore close errors
737
- }
738
- return;
739
- }
740
- if (!activeConnection) {
741
- this.incrementRuntimeCounter("missingConnectionForMessage");
742
- this.logger.error("No connection found for websocket");
743
- return;
744
- }
745
- if (message.type === "hello") {
746
- this.incrementRuntimeCounter("unexpectedHelloOnActiveConnection");
747
- activeConnection.connectionLogger.warn("Received hello on active connection");
748
- try {
749
- ws.close(WS_CLOSE_INVALID_HELLO, "Unexpected hello");
750
- }
751
- catch {
752
- // ignore close errors
753
- }
754
- return;
755
- }
756
- if (message.type === "session") {
757
- this.recordInboundSessionRequestType(message.message.type);
758
- const startMs = performance.now();
759
- await activeConnection.session.handleMessage(message.message);
760
- const durationMs = performance.now() - startMs;
761
- this.recordRequestLatency(message.message.type, durationMs);
762
- if (durationMs >= SLOW_REQUEST_THRESHOLD_MS) {
763
- activeConnection.connectionLogger.warn({
764
- requestType: message.message.type,
765
- durationMs: Math.round(durationMs),
766
- inflightRequests: activeConnection.session.getRuntimeMetrics().inflightRequests,
767
- }, "ws_slow_request");
768
- }
769
- }
770
- }
771
- catch (error) {
772
- const err = error instanceof Error ? error : new Error(String(error));
773
- let rawPayload = null;
774
- let parsedPayload = null;
775
- try {
776
- const buffer = bufferFromWsData(data);
777
- rawPayload = buffer.toString();
778
- parsedPayload = JSON.parse(rawPayload);
779
- }
780
- catch (payloadError) {
781
- rawPayload = rawPayload ?? "<unreadable>";
782
- parsedPayload = parsedPayload ?? rawPayload;
783
- const payloadErr = payloadError instanceof Error ? payloadError : new Error(String(payloadError));
784
- this.logger.error({ err: payloadErr }, "Failed to decode raw payload");
785
- }
786
- const trimmedRawPayload = typeof rawPayload === "string" && rawPayload.length > 2000
787
- ? `${rawPayload.slice(0, 2000)}... (truncated)`
788
- : rawPayload;
789
- log.error({
790
- err,
791
- rawPayload: trimmedRawPayload,
792
- parsedPayload,
793
- }, "Failed to parse/handle message");
794
- if (this.pendingConnections.has(ws)) {
795
- this.clearPendingConnection(ws);
796
- try {
797
- ws.close(WS_CLOSE_INVALID_HELLO, "Invalid hello");
798
- }
799
- catch {
800
- // ignore close errors
801
- }
802
- return;
803
- }
804
- const requestInfo = extractRequestInfoFromUnknownWsInbound(parsedPayload);
805
- if (requestInfo) {
806
- this.sendToClient(ws, wrapSessionMessage({
807
- type: "rpc_error",
808
- payload: {
809
- requestId: requestInfo.requestId,
810
- requestType: requestInfo.requestType,
811
- error: "Invalid message",
812
- code: "invalid_message",
813
- },
814
- }));
815
- return;
816
- }
817
- this.sendToClient(ws, wrapSessionMessage({
818
- type: "status",
819
- payload: {
820
- status: "error",
821
- message: `Invalid message: ${err.message}`,
822
- },
823
- }));
824
- }
825
- }
826
- incrementRuntimeCounter(counter) {
827
- this.runtimeCounters[counter] += 1;
828
- }
829
- incrementCount(map, key) {
830
- map.set(key, (map.get(key) ?? 0) + 1);
831
- }
832
- recordInboundMessageType(type) {
833
- this.incrementCount(this.inboundMessageCounts, type);
834
- }
835
- recordInboundSessionRequestType(type) {
836
- this.incrementCount(this.inboundSessionRequestCounts, type);
837
- }
838
- recordRequestLatency(type, durationMs) {
839
- let latencies = this.requestLatencies.get(type);
840
- if (!latencies) {
841
- latencies = [];
842
- this.requestLatencies.set(type, latencies);
843
- }
844
- latencies.push(durationMs);
845
- }
846
- getTopCounts(map, limit) {
847
- return [...map.entries()].sort((a, b) => b[1] - a[1]).slice(0, limit);
848
- }
849
- computeLatencyStats() {
850
- const stats = [];
851
- for (const [type, latencies] of this.requestLatencies) {
852
- if (latencies.length === 0)
853
- continue;
854
- latencies.sort((a, b) => a - b);
855
- const count = latencies.length;
856
- const minMs = Math.round(latencies[0]);
857
- const maxMs = Math.round(latencies[count - 1]);
858
- const p50Ms = Math.round(latencies[Math.floor(count / 2)]);
859
- const totalMs = Math.round(latencies.reduce((sum, v) => sum + v, 0));
860
- stats.push({ type, count, minMs, maxMs, p50Ms, totalMs });
861
- }
862
- stats.sort((a, b) => b.totalMs - a.totalMs);
863
- return stats.slice(0, 15);
864
- }
865
- collectSessionRuntimeMetrics() {
866
- const uniqueConnections = new Set(this.externalSessionsByKey.values());
867
- let terminalDirectorySubscriptionCount = 0;
868
- let terminalSubscriptionCount = 0;
869
- let inflightRequests = 0;
870
- let peakInflightRequests = 0;
871
- for (const connection of uniqueConnections) {
872
- const sessionMetrics = connection.session.getRuntimeMetrics();
873
- terminalDirectorySubscriptionCount += sessionMetrics.terminalDirectorySubscriptionCount;
874
- terminalSubscriptionCount += sessionMetrics.terminalSubscriptionCount;
875
- inflightRequests += sessionMetrics.inflightRequests;
876
- peakInflightRequests = Math.max(peakInflightRequests, sessionMetrics.peakInflightRequests);
877
- connection.session.resetPeakInflight();
878
- }
879
- return {
880
- ...this.checkoutDiffManager.getMetrics(),
881
- terminalDirectorySubscriptionCount,
882
- terminalSubscriptionCount,
883
- inflightRequests,
884
- peakInflightRequests,
885
- };
886
- }
887
- flushRuntimeMetrics(options) {
888
- const now = Date.now();
889
- const windowMs = Math.max(0, now - this.runtimeWindowStartedAt);
890
- const activeConnections = new Set(this.sessions.values()).size;
891
- const activeSockets = this.sessions.size;
892
- const pendingConnections = this.pendingConnections.size;
893
- const reconnectGraceSessions = [...this.externalSessionsByKey.values()].filter((connection) => connection.sockets.size === 0 && connection.externalDisconnectCleanupTimeout !== null).length;
894
- const sessionMetrics = this.collectSessionRuntimeMetrics();
895
- const latencyStats = this.computeLatencyStats();
896
- const agentSnapshot = this.agentManager.getMetricsSnapshot();
897
- this.logger.info({
898
- windowMs,
899
- final: Boolean(options?.final),
900
- sessions: {
901
- activeConnections,
902
- externalSessionKeys: this.externalSessionsByKey.size,
903
- reconnectGraceSessions,
904
- },
905
- sockets: {
906
- activeSockets,
907
- pendingConnections,
908
- },
909
- counters: { ...this.runtimeCounters },
910
- inboundMessageTypesTop: this.getTopCounts(this.inboundMessageCounts, 12),
911
- inboundSessionRequestTypesTop: this.getTopCounts(this.inboundSessionRequestCounts, 20),
912
- runtime: sessionMetrics,
913
- latency: latencyStats,
914
- agents: agentSnapshot,
915
- }, "ws_runtime_metrics");
916
- for (const counter of Object.keys(this.runtimeCounters)) {
917
- this.runtimeCounters[counter] = 0;
918
- }
919
- this.inboundMessageCounts.clear();
920
- this.inboundSessionRequestCounts.clear();
921
- this.requestLatencies.clear();
922
- this.runtimeWindowStartedAt = now;
923
- }
924
- getClientActivityState(session) {
925
- const activity = session.getClientActivity();
926
- if (!activity) {
927
- return { deviceType: null, focusedAgentId: null, isStale: true, appVisible: false };
928
- }
929
- const now = Date.now();
930
- const ageMs = now - activity.lastActivityAt.getTime();
931
- const isStale = ageMs >= this.ACTIVITY_THRESHOLD_MS;
932
- return {
933
- deviceType: activity.deviceType,
934
- focusedAgentId: activity.focusedAgentId,
935
- isStale,
936
- appVisible: activity.appVisible,
937
- };
938
- }
939
- broadcastAgentAttention(params) {
940
- const clientEntries = [];
941
- for (const [ws, connection] of this.sessions) {
942
- clientEntries.push({
943
- ws,
944
- state: this.getClientActivityState(connection.session),
945
- });
946
- }
947
- const allStates = clientEntries.map((e) => e.state);
948
- const agent = this.agentManager.getAgent(params.agentId);
949
- const notification = buildAgentAttentionNotificationPayload({
950
- reason: params.reason,
951
- serverId: this.serverId,
952
- agentId: params.agentId,
953
- assistantMessage: agent ? findLatestAssistantMessageFromTimeline(agent.timeline) : null,
954
- permissionRequest: agent ? findLatestPermissionRequest(agent.pendingPermissions) : null,
955
- });
956
- // Push is only a fallback when the user is away from desktop/web.
957
- // Also suppress push if they're actively using the mobile app.
958
- const shouldSendPush = computeShouldSendPush({
959
- reason: params.reason,
960
- allClientStates: allStates,
961
- });
962
- if (shouldSendPush) {
963
- const tokens = this.pushTokenStore.getAllTokens();
964
- this.logger.info({ tokenCount: tokens.length }, "Sending push notification");
965
- if (tokens.length > 0) {
966
- void this.pushService.sendPush(tokens, notification);
967
- }
968
- }
969
- for (const { ws, state } of clientEntries) {
970
- const shouldNotify = computeShouldNotifyClient({
971
- clientState: state,
972
- allClientStates: allStates,
973
- agentId: params.agentId,
974
- });
975
- const message = wrapSessionMessage({
976
- type: "agent_stream",
977
- payload: {
978
- agentId: params.agentId,
979
- event: {
980
- type: "attention_required",
981
- provider: params.provider,
982
- reason: params.reason,
983
- timestamp: new Date().toISOString(),
984
- shouldNotify,
985
- notification,
986
- },
987
- timestamp: new Date().toISOString(),
988
- },
989
- });
990
- this.sendToClient(ws, message);
991
- }
992
- }
993
- }
994
- function extractSocketRequestMetadata(request) {
995
- if (!request || typeof request !== "object") {
996
- return {};
997
- }
998
- const record = request;
999
- const host = typeof record.headers?.host === "string" ? record.headers.host : undefined;
1000
- const origin = typeof record.headers?.origin === "string" ? record.headers.origin : undefined;
1001
- const userAgent = typeof record.headers?.["user-agent"] === "string" ? record.headers["user-agent"] : undefined;
1002
- const remoteAddress = typeof record.socket?.remoteAddress === "string" ? record.socket.remoteAddress : undefined;
1003
- return {
1004
- ...(host ? { host } : {}),
1005
- ...(origin ? { origin } : {}),
1006
- ...(userAgent ? { userAgent } : {}),
1007
- ...(remoteAddress ? { remoteAddress } : {}),
1008
- };
1009
- }
1010
- function stringifyCloseReason(reason) {
1011
- if (typeof reason === "string") {
1012
- return reason.length > 0 ? reason : null;
1013
- }
1014
- if (Buffer.isBuffer(reason)) {
1015
- const text = reason.toString();
1016
- return text.length > 0 ? text : null;
1017
- }
1018
- if (reason == null) {
1019
- return null;
1020
- }
1021
- const text = String(reason);
1022
- return text.length > 0 ? text : null;
1023
- }
1024
- function extractRequestInfoFromUnknownWsInbound(payload) {
1025
- if (!payload || typeof payload !== "object") {
1026
- return null;
1027
- }
1028
- const record = payload;
1029
- // Session-wrapped messages
1030
- if (record.type === "session" && record.message && typeof record.message === "object") {
1031
- const msg = record.message;
1032
- if (typeof msg.requestId === "string") {
1033
- return {
1034
- requestId: msg.requestId,
1035
- ...(typeof msg.type === "string" ? { requestType: msg.type } : {}),
1036
- };
1037
- }
1038
- }
1039
- // Non-session messages (future-proof)
1040
- if (typeof record.requestId === "string") {
1041
- return {
1042
- requestId: record.requestId,
1043
- ...(typeof record.type === "string" ? { requestType: record.type } : {}),
1044
- };
1045
- }
1046
- return null;
1047
- }
1048
- //# sourceMappingURL=websocket-server.js.map