@getpaseo/server 0.1.29 → 0.1.32

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 (499) hide show
  1. package/dist/scripts/daemon-runner.js +1 -1
  2. package/dist/scripts/daemon-runner.js.map +1 -1
  3. package/dist/scripts/dev-runner.js +1 -1
  4. package/dist/scripts/dev-runner.js.map +1 -1
  5. package/dist/server/client/daemon-client-relay-e2ee-transport.d.ts.map +1 -1
  6. package/dist/server/client/daemon-client-relay-e2ee-transport.js.map +1 -1
  7. package/dist/server/client/daemon-client-websocket-transport.d.ts.map +1 -1
  8. package/dist/server/client/daemon-client-websocket-transport.js.map +1 -1
  9. package/dist/server/client/daemon-client.d.ts +108 -103
  10. package/dist/server/client/daemon-client.d.ts.map +1 -1
  11. package/dist/server/client/daemon-client.js +417 -407
  12. package/dist/server/client/daemon-client.js.map +1 -1
  13. package/dist/server/server/agent/activity-curator.d.ts.map +1 -1
  14. package/dist/server/server/agent/activity-curator.js +5 -4
  15. package/dist/server/server/agent/activity-curator.js.map +1 -1
  16. package/dist/server/server/agent/agent-management-mcp.d.ts.map +1 -1
  17. package/dist/server/server/agent/agent-management-mcp.js +13 -17
  18. package/dist/server/server/agent/agent-management-mcp.js.map +1 -1
  19. package/dist/server/server/agent/agent-manager.d.ts.map +1 -1
  20. package/dist/server/server/agent/agent-manager.js +26 -26
  21. package/dist/server/server/agent/agent-manager.js.map +1 -1
  22. package/dist/server/server/agent/agent-metadata-generator.d.ts.map +1 -1
  23. package/dist/server/server/agent/agent-metadata-generator.js +1 -3
  24. package/dist/server/server/agent/agent-metadata-generator.js.map +1 -1
  25. package/dist/server/server/agent/agent-projections.d.ts.map +1 -1
  26. package/dist/server/server/agent/agent-projections.js +4 -12
  27. package/dist/server/server/agent/agent-projections.js.map +1 -1
  28. package/dist/server/server/agent/agent-response-loop.d.ts.map +1 -1
  29. package/dist/server/server/agent/agent-response-loop.js +6 -6
  30. package/dist/server/server/agent/agent-response-loop.js.map +1 -1
  31. package/dist/server/server/agent/agent-sdk-types.d.ts +23 -0
  32. package/dist/server/server/agent/agent-sdk-types.d.ts.map +1 -1
  33. package/dist/server/server/agent/agent-sdk-types.js.map +1 -1
  34. package/dist/server/server/agent/agent-storage.d.ts.map +1 -1
  35. package/dist/server/server/agent/agent-storage.js +2 -4
  36. package/dist/server/server/agent/agent-storage.js.map +1 -1
  37. package/dist/server/server/agent/dictation-debug.d.ts.map +1 -1
  38. package/dist/server/server/agent/dictation-debug.js.map +1 -1
  39. package/dist/server/server/agent/mcp-server.d.ts.map +1 -1
  40. package/dist/server/server/agent/mcp-server.js +19 -27
  41. package/dist/server/server/agent/mcp-server.js.map +1 -1
  42. package/dist/server/server/agent/pcm16-resampler.d.ts.map +1 -1
  43. package/dist/server/server/agent/pcm16-resampler.js.map +1 -1
  44. package/dist/server/server/agent/provider-launch-config.d.ts.map +1 -1
  45. package/dist/server/server/agent/provider-launch-config.js +4 -2
  46. package/dist/server/server/agent/provider-launch-config.js.map +1 -1
  47. package/dist/server/server/agent/provider-manifest.d.ts +10 -1
  48. package/dist/server/server/agent/provider-manifest.d.ts.map +1 -1
  49. package/dist/server/server/agent/provider-manifest.js +25 -0
  50. package/dist/server/server/agent/provider-manifest.js.map +1 -1
  51. package/dist/server/server/agent/provider-registry.d.ts +2 -2
  52. package/dist/server/server/agent/provider-registry.d.ts.map +1 -1
  53. package/dist/server/server/agent/provider-registry.js +1 -1
  54. package/dist/server/server/agent/provider-registry.js.map +1 -1
  55. package/dist/server/server/agent/providers/claude/model-catalog.js +10 -10
  56. package/dist/server/server/agent/providers/claude/model-catalog.js.map +1 -1
  57. package/dist/server/server/agent/providers/claude/partial-json.d.ts +5 -0
  58. package/dist/server/server/agent/providers/claude/partial-json.d.ts.map +1 -0
  59. package/dist/server/server/agent/providers/claude/partial-json.js +306 -0
  60. package/dist/server/server/agent/providers/claude/partial-json.js.map +1 -0
  61. package/dist/server/server/agent/providers/claude/sidechain-tracker.d.ts +20 -0
  62. package/dist/server/server/agent/providers/claude/sidechain-tracker.d.ts.map +1 -0
  63. package/dist/server/server/agent/providers/claude/sidechain-tracker.js +230 -0
  64. package/dist/server/server/agent/providers/claude/sidechain-tracker.js.map +1 -0
  65. package/dist/server/server/agent/providers/claude/task-notification-tool-call.d.ts +11 -0
  66. package/dist/server/server/agent/providers/claude/task-notification-tool-call.d.ts.map +1 -1
  67. package/dist/server/server/agent/providers/claude/task-notification-tool-call.js +37 -20
  68. package/dist/server/server/agent/providers/claude/task-notification-tool-call.js.map +1 -1
  69. package/dist/server/server/agent/providers/claude/tool-call-detail-parser.d.ts.map +1 -1
  70. package/dist/server/server/agent/providers/claude/tool-call-detail-parser.js +21 -11
  71. package/dist/server/server/agent/providers/claude/tool-call-detail-parser.js.map +1 -1
  72. package/dist/server/server/agent/providers/claude/tool-call-mapper.d.ts.map +1 -1
  73. package/dist/server/server/agent/providers/claude/tool-call-mapper.js +23 -11
  74. package/dist/server/server/agent/providers/claude/tool-call-mapper.js.map +1 -1
  75. package/dist/server/server/agent/providers/claude-agent.d.ts.map +1 -1
  76. package/dist/server/server/agent/providers/claude-agent.js +508 -1141
  77. package/dist/server/server/agent/providers/claude-agent.js.map +1 -1
  78. package/dist/server/server/agent/providers/codex/tool-call-detail-parser.d.ts.map +1 -1
  79. package/dist/server/server/agent/providers/codex/tool-call-detail-parser.js +2 -2
  80. package/dist/server/server/agent/providers/codex/tool-call-detail-parser.js.map +1 -1
  81. package/dist/server/server/agent/providers/codex/tool-call-mapper.d.ts.map +1 -1
  82. package/dist/server/server/agent/providers/codex/tool-call-mapper.js +14 -11
  83. package/dist/server/server/agent/providers/codex/tool-call-mapper.js.map +1 -1
  84. package/dist/server/server/agent/providers/codex-app-server-agent.d.ts.map +1 -1
  85. package/dist/server/server/agent/providers/codex-app-server-agent.js +347 -163
  86. package/dist/server/server/agent/providers/codex-app-server-agent.js.map +1 -1
  87. package/dist/server/server/agent/providers/codex-rollout-timeline.d.ts.map +1 -1
  88. package/dist/server/server/agent/providers/codex-rollout-timeline.js +21 -32
  89. package/dist/server/server/agent/providers/codex-rollout-timeline.js.map +1 -1
  90. package/dist/server/server/agent/providers/opencode/tool-call-detail-parser.d.ts.map +1 -1
  91. package/dist/server/server/agent/providers/opencode/tool-call-detail-parser.js +2 -2
  92. package/dist/server/server/agent/providers/opencode/tool-call-detail-parser.js.map +1 -1
  93. package/dist/server/server/agent/providers/opencode/tool-call-mapper.d.ts.map +1 -1
  94. package/dist/server/server/agent/providers/opencode/tool-call-mapper.js +2 -9
  95. package/dist/server/server/agent/providers/opencode/tool-call-mapper.js.map +1 -1
  96. package/dist/server/server/agent/providers/opencode-agent.d.ts.map +1 -1
  97. package/dist/server/server/agent/providers/opencode-agent.js +5 -5
  98. package/dist/server/server/agent/providers/opencode-agent.js.map +1 -1
  99. package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts +277 -1
  100. package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts.map +1 -1
  101. package/dist/server/server/agent/providers/tool-call-detail-primitives.js +149 -15
  102. package/dist/server/server/agent/providers/tool-call-detail-primitives.js.map +1 -1
  103. package/dist/server/server/agent/providers/tool-call-mapper-utils.d.ts.map +1 -1
  104. package/dist/server/server/agent/providers/tool-call-mapper-utils.js +1 -3
  105. package/dist/server/server/agent/providers/tool-call-mapper-utils.js.map +1 -1
  106. package/dist/server/server/agent/stt-manager.d.ts.map +1 -1
  107. package/dist/server/server/agent/stt-manager.js +1 -2
  108. package/dist/server/server/agent/stt-manager.js.map +1 -1
  109. package/dist/server/server/agent/system-prompt.js +5 -5
  110. package/dist/server/server/agent/timeline-projection.d.ts.map +1 -1
  111. package/dist/server/server/agent/timeline-projection.js.map +1 -1
  112. package/dist/server/server/agent/tts-manager.d.ts.map +1 -1
  113. package/dist/server/server/agent/tts-manager.js +27 -9
  114. package/dist/server/server/agent/tts-manager.js.map +1 -1
  115. package/dist/server/server/agent/wait-for-agent-tracker.d.ts.map +1 -1
  116. package/dist/server/server/agent/wait-for-agent-tracker.js.map +1 -1
  117. package/dist/server/server/agent-attention-policy.d.ts.map +1 -1
  118. package/dist/server/server/agent-attention-policy.js.map +1 -1
  119. package/dist/server/server/allowed-hosts.d.ts.map +1 -1
  120. package/dist/server/server/allowed-hosts.js.map +1 -1
  121. package/dist/server/server/bootstrap.d.ts.map +1 -1
  122. package/dist/server/server/bootstrap.js +46 -5
  123. package/dist/server/server/bootstrap.js.map +1 -1
  124. package/dist/server/server/config.d.ts.map +1 -1
  125. package/dist/server/server/config.js +4 -11
  126. package/dist/server/server/config.js.map +1 -1
  127. package/dist/server/server/connection-offer.d.ts +1 -1
  128. package/dist/server/server/connection-offer.d.ts.map +1 -1
  129. package/dist/server/server/connection-offer.js +2 -3
  130. package/dist/server/server/connection-offer.js.map +1 -1
  131. package/dist/server/server/daemon-version.d.ts.map +1 -1
  132. package/dist/server/server/daemon-version.js +1 -1
  133. package/dist/server/server/daemon-version.js.map +1 -1
  134. package/dist/server/server/dictation/dictation-stream-manager.d.ts.map +1 -1
  135. package/dist/server/server/dictation/dictation-stream-manager.js +4 -1
  136. package/dist/server/server/dictation/dictation-stream-manager.js.map +1 -1
  137. package/dist/server/server/exports.d.ts +1 -1
  138. package/dist/server/server/exports.d.ts.map +1 -1
  139. package/dist/server/server/exports.js +1 -1
  140. package/dist/server/server/exports.js.map +1 -1
  141. package/dist/server/server/file-explorer/service.d.ts +1 -1
  142. package/dist/server/server/file-explorer/service.d.ts.map +1 -1
  143. package/dist/server/server/file-explorer/service.js +5 -8
  144. package/dist/server/server/file-explorer/service.js.map +1 -1
  145. package/dist/server/server/index.js +1 -1
  146. package/dist/server/server/index.js.map +1 -1
  147. package/dist/server/server/logger.d.ts.map +1 -1
  148. package/dist/server/server/logger.js.map +1 -1
  149. package/dist/server/server/messages.d.ts.map +1 -1
  150. package/dist/server/server/messages.js.map +1 -1
  151. package/dist/server/server/package-version.d.ts.map +1 -1
  152. package/dist/server/server/package-version.js +1 -2
  153. package/dist/server/server/package-version.js.map +1 -1
  154. package/dist/server/server/persisted-config.d.ts.map +1 -1
  155. package/dist/server/server/persisted-config.js.map +1 -1
  156. package/dist/server/server/persistence-hooks.d.ts.map +1 -1
  157. package/dist/server/server/persistence-hooks.js.map +1 -1
  158. package/dist/server/server/pid-lock.d.ts.map +1 -1
  159. package/dist/server/server/pid-lock.js.map +1 -1
  160. package/dist/server/server/push/push-service.d.ts.map +1 -1
  161. package/dist/server/server/push/push-service.js.map +1 -1
  162. package/dist/server/server/relay-transport.d.ts.map +1 -1
  163. package/dist/server/server/relay-transport.js +6 -2
  164. package/dist/server/server/relay-transport.js.map +1 -1
  165. package/dist/server/server/session.d.ts +49 -37
  166. package/dist/server/server/session.d.ts.map +1 -1
  167. package/dist/server/server/session.js +1234 -998
  168. package/dist/server/server/session.js.map +1 -1
  169. package/dist/server/server/speech/audio.d.ts.map +1 -1
  170. package/dist/server/server/speech/audio.js.map +1 -1
  171. package/dist/server/server/speech/providers/local/config.d.ts.map +1 -1
  172. package/dist/server/server/speech/providers/local/config.js +5 -14
  173. package/dist/server/server/speech/providers/local/config.js.map +1 -1
  174. package/dist/server/server/speech/providers/local/models.d.ts.map +1 -1
  175. package/dist/server/server/speech/providers/local/models.js +1 -1
  176. package/dist/server/server/speech/providers/local/models.js.map +1 -1
  177. package/dist/server/server/speech/providers/local/pocket/pocket-tts-onnx.d.ts.map +1 -1
  178. package/dist/server/server/speech/providers/local/pocket/pocket-tts-onnx.js +21 -7
  179. package/dist/server/server/speech/providers/local/pocket/pocket-tts-onnx.js.map +1 -1
  180. package/dist/server/server/speech/providers/local/runtime.d.ts.map +1 -1
  181. package/dist/server/server/speech/providers/local/runtime.js +1 -23
  182. package/dist/server/server/speech/providers/local/runtime.js.map +1 -1
  183. package/dist/server/server/speech/providers/local/sherpa/model-catalog.d.ts.map +1 -1
  184. package/dist/server/server/speech/providers/local/sherpa/model-catalog.js.map +1 -1
  185. package/dist/server/server/speech/providers/local/sherpa/model-downloader.d.ts.map +1 -1
  186. package/dist/server/server/speech/providers/local/sherpa/model-downloader.js.map +1 -1
  187. package/dist/server/server/speech/providers/local/sherpa/sherpa-offline-recognizer.d.ts.map +1 -1
  188. package/dist/server/server/speech/providers/local/sherpa/sherpa-offline-recognizer.js +9 -4
  189. package/dist/server/server/speech/providers/local/sherpa/sherpa-offline-recognizer.js.map +1 -1
  190. package/dist/server/server/speech/providers/local/sherpa/sherpa-online-recognizer.d.ts.map +1 -1
  191. package/dist/server/server/speech/providers/local/sherpa/sherpa-online-recognizer.js +7 -2
  192. package/dist/server/server/speech/providers/local/sherpa/sherpa-online-recognizer.js.map +1 -1
  193. package/dist/server/server/speech/providers/local/sherpa/sherpa-parakeet-realtime-session.d.ts.map +1 -1
  194. package/dist/server/server/speech/providers/local/sherpa/sherpa-parakeet-realtime-session.js +5 -1
  195. package/dist/server/server/speech/providers/local/sherpa/sherpa-parakeet-realtime-session.js.map +1 -1
  196. package/dist/server/server/speech/providers/local/sherpa/sherpa-parakeet-stt.d.ts.map +1 -1
  197. package/dist/server/server/speech/providers/local/sherpa/sherpa-parakeet-stt.js +2 -4
  198. package/dist/server/server/speech/providers/local/sherpa/sherpa-parakeet-stt.js.map +1 -1
  199. package/dist/server/server/speech/providers/local/sherpa/sherpa-realtime-session.d.ts.map +1 -1
  200. package/dist/server/server/speech/providers/local/sherpa/sherpa-realtime-session.js +1 -3
  201. package/dist/server/server/speech/providers/local/sherpa/sherpa-realtime-session.js.map +1 -1
  202. package/dist/server/server/speech/providers/local/sherpa/sherpa-stt.d.ts.map +1 -1
  203. package/dist/server/server/speech/providers/local/sherpa/sherpa-stt.js +2 -4
  204. package/dist/server/server/speech/providers/local/sherpa/sherpa-stt.js.map +1 -1
  205. package/dist/server/server/speech/providers/local/sherpa/sherpa-tts.d.ts.map +1 -1
  206. package/dist/server/server/speech/providers/local/sherpa/sherpa-tts.js +4 -1
  207. package/dist/server/server/speech/providers/local/sherpa/sherpa-tts.js.map +1 -1
  208. package/dist/server/server/speech/providers/local/sherpa/silero-vad-provider.d.ts.map +1 -1
  209. package/dist/server/server/speech/providers/local/sherpa/silero-vad-provider.js +1 -1
  210. package/dist/server/server/speech/providers/local/sherpa/silero-vad-provider.js.map +1 -1
  211. package/dist/server/server/speech/providers/local/sherpa/silero-vad-session.d.ts.map +1 -1
  212. package/dist/server/server/speech/providers/local/sherpa/silero-vad-session.js.map +1 -1
  213. package/dist/server/server/speech/providers/openai/config.d.ts.map +1 -1
  214. package/dist/server/server/speech/providers/openai/config.js +5 -24
  215. package/dist/server/server/speech/providers/openai/config.js.map +1 -1
  216. package/dist/server/server/speech/providers/openai/realtime-transcription-session.d.ts.map +1 -1
  217. package/dist/server/server/speech/providers/openai/realtime-transcription-session.js +6 -3
  218. package/dist/server/server/speech/providers/openai/realtime-transcription-session.js.map +1 -1
  219. package/dist/server/server/speech/providers/openai/runtime.d.ts.map +1 -1
  220. package/dist/server/server/speech/providers/openai/runtime.js +2 -6
  221. package/dist/server/server/speech/providers/openai/runtime.js.map +1 -1
  222. package/dist/server/server/speech/providers/openai/stt.d.ts.map +1 -1
  223. package/dist/server/server/speech/providers/openai/stt.js +1 -3
  224. package/dist/server/server/speech/providers/openai/stt.js.map +1 -1
  225. package/dist/server/server/speech/providers/openai/tts.d.ts.map +1 -1
  226. package/dist/server/server/speech/providers/openai/tts.js.map +1 -1
  227. package/dist/server/server/speech/speech-config-resolver.d.ts.map +1 -1
  228. package/dist/server/server/speech/speech-config-resolver.js +3 -7
  229. package/dist/server/server/speech/speech-config-resolver.js.map +1 -1
  230. package/dist/server/server/speech/speech-provider.d.ts.map +1 -1
  231. package/dist/server/server/speech/speech-runtime.d.ts.map +1 -1
  232. package/dist/server/server/speech/speech-runtime.js.map +1 -1
  233. package/dist/server/server/speech/turn-detection-provider.d.ts.map +1 -1
  234. package/dist/server/server/terminal-mcp/server.d.ts.map +1 -1
  235. package/dist/server/server/terminal-mcp/server.js +3 -11
  236. package/dist/server/server/terminal-mcp/server.js.map +1 -1
  237. package/dist/server/server/terminal-mcp/terminal-manager.d.ts.map +1 -1
  238. package/dist/server/server/terminal-mcp/terminal-manager.js +2 -14
  239. package/dist/server/server/terminal-mcp/terminal-manager.js.map +1 -1
  240. package/dist/server/server/terminal-mcp/tmux.d.ts +1 -1
  241. package/dist/server/server/terminal-mcp/tmux.d.ts.map +1 -1
  242. package/dist/server/server/terminal-mcp/tmux.js +20 -123
  243. package/dist/server/server/terminal-mcp/tmux.js.map +1 -1
  244. package/dist/server/server/utils/diff-highlighter.d.ts +11 -3
  245. package/dist/server/server/utils/diff-highlighter.d.ts.map +1 -1
  246. package/dist/server/server/utils/diff-highlighter.js +49 -36
  247. package/dist/server/server/utils/diff-highlighter.js.map +1 -1
  248. package/dist/server/server/voice/fixed-duration-pcm-ring-buffer.d.ts.map +1 -1
  249. package/dist/server/server/voice/fixed-duration-pcm-ring-buffer.js.map +1 -1
  250. package/dist/server/server/voice/voice-turn-controller.d.ts.map +1 -1
  251. package/dist/server/server/voice/voice-turn-controller.js +1 -3
  252. package/dist/server/server/voice/voice-turn-controller.js.map +1 -1
  253. package/dist/server/server/voice-config.d.ts.map +1 -1
  254. package/dist/server/server/voice-config.js.map +1 -1
  255. package/dist/server/server/voice-mcp-bridge.d.ts.map +1 -1
  256. package/dist/server/server/voice-mcp-bridge.js.map +1 -1
  257. package/dist/server/server/websocket-server.d.ts +1 -0
  258. package/dist/server/server/websocket-server.d.ts.map +1 -1
  259. package/dist/server/server/websocket-server.js +20 -22
  260. package/dist/server/server/websocket-server.js.map +1 -1
  261. package/dist/server/server/workspace-registry-bootstrap.d.ts +3 -3
  262. package/dist/server/server/workspace-registry-bootstrap.d.ts.map +1 -1
  263. package/dist/server/server/workspace-registry-bootstrap.js +6 -6
  264. package/dist/server/server/workspace-registry-bootstrap.js.map +1 -1
  265. package/dist/server/server/workspace-registry-model.d.ts +14 -3
  266. package/dist/server/server/workspace-registry-model.d.ts.map +1 -1
  267. package/dist/server/server/workspace-registry-model.js +40 -15
  268. package/dist/server/server/workspace-registry-model.js.map +1 -1
  269. package/dist/server/server/workspace-registry.d.ts +5 -5
  270. package/dist/server/server/workspace-registry.d.ts.map +1 -1
  271. package/dist/server/server/workspace-registry.js +16 -13
  272. package/dist/server/server/workspace-registry.js.map +1 -1
  273. package/dist/server/server/worktree-bootstrap.d.ts.map +1 -1
  274. package/dist/server/server/worktree-bootstrap.js +17 -6
  275. package/dist/server/server/worktree-bootstrap.js.map +1 -1
  276. package/dist/server/shared/agent-attention-notification.d.ts.map +1 -1
  277. package/dist/server/shared/agent-attention-notification.js.map +1 -1
  278. package/dist/server/shared/agent-lifecycle.d.ts.map +1 -1
  279. package/dist/server/shared/daemon-endpoints.d.ts +1 -0
  280. package/dist/server/shared/daemon-endpoints.d.ts.map +1 -1
  281. package/dist/server/shared/daemon-endpoints.js +11 -2
  282. package/dist/server/shared/daemon-endpoints.js.map +1 -1
  283. package/dist/server/shared/messages.d.ts +1228 -2982
  284. package/dist/server/shared/messages.d.ts.map +1 -1
  285. package/dist/server/shared/messages.js +330 -302
  286. package/dist/server/shared/messages.js.map +1 -1
  287. package/dist/server/shared/terminal-stream-protocol.d.ts +36 -0
  288. package/dist/server/shared/terminal-stream-protocol.d.ts.map +1 -0
  289. package/dist/server/shared/terminal-stream-protocol.js +99 -0
  290. package/dist/server/shared/terminal-stream-protocol.js.map +1 -0
  291. package/dist/server/shared/tool-call-display.d.ts.map +1 -1
  292. package/dist/server/shared/tool-call-display.js +6 -3
  293. package/dist/server/shared/tool-call-display.js.map +1 -1
  294. package/dist/server/terminal/terminal.d.ts +5 -48
  295. package/dist/server/terminal/terminal.d.ts.map +1 -1
  296. package/dist/server/terminal/terminal.js +44 -98
  297. package/dist/server/terminal/terminal.js.map +1 -1
  298. package/dist/server/utils/checkout-git.d.ts +1 -0
  299. package/dist/server/utils/checkout-git.d.ts.map +1 -1
  300. package/dist/server/utils/checkout-git.js +111 -120
  301. package/dist/server/utils/checkout-git.js.map +1 -1
  302. package/dist/server/utils/directory-suggestions.d.ts +1 -1
  303. package/dist/server/utils/directory-suggestions.d.ts.map +1 -1
  304. package/dist/server/utils/directory-suggestions.js +40 -40
  305. package/dist/server/utils/directory-suggestions.js.map +1 -1
  306. package/dist/server/utils/project-icon.d.ts.map +1 -1
  307. package/dist/server/utils/project-icon.js +2 -11
  308. package/dist/server/utils/project-icon.js.map +1 -1
  309. package/dist/server/utils/worktree.d.ts +2 -0
  310. package/dist/server/utils/worktree.d.ts.map +1 -1
  311. package/dist/server/utils/worktree.js +22 -19
  312. package/dist/server/utils/worktree.js.map +1 -1
  313. package/dist/src/server/agent/activity-curator.js +5 -4
  314. package/dist/src/server/agent/activity-curator.js.map +1 -1
  315. package/dist/src/server/agent/agent-manager.js +26 -26
  316. package/dist/src/server/agent/agent-manager.js.map +1 -1
  317. package/dist/src/server/agent/agent-metadata-generator.js +1 -3
  318. package/dist/src/server/agent/agent-metadata-generator.js.map +1 -1
  319. package/dist/src/server/agent/agent-projections.js +4 -12
  320. package/dist/src/server/agent/agent-projections.js.map +1 -1
  321. package/dist/src/server/agent/agent-response-loop.js +6 -6
  322. package/dist/src/server/agent/agent-response-loop.js.map +1 -1
  323. package/dist/src/server/agent/agent-sdk-types.js.map +1 -1
  324. package/dist/src/server/agent/agent-storage.js +2 -4
  325. package/dist/src/server/agent/agent-storage.js.map +1 -1
  326. package/dist/src/server/agent/dictation-debug.js.map +1 -1
  327. package/dist/src/server/agent/mcp-server.js +19 -27
  328. package/dist/src/server/agent/mcp-server.js.map +1 -1
  329. package/dist/src/server/agent/pcm16-resampler.js.map +1 -1
  330. package/dist/src/server/agent/provider-launch-config.js +4 -2
  331. package/dist/src/server/agent/provider-launch-config.js.map +1 -1
  332. package/dist/src/server/agent/provider-manifest.js +25 -0
  333. package/dist/src/server/agent/provider-manifest.js.map +1 -1
  334. package/dist/src/server/agent/provider-registry.js +1 -1
  335. package/dist/src/server/agent/provider-registry.js.map +1 -1
  336. package/dist/src/server/agent/providers/claude/model-catalog.js +10 -10
  337. package/dist/src/server/agent/providers/claude/model-catalog.js.map +1 -1
  338. package/dist/src/server/agent/providers/claude/partial-json.js +306 -0
  339. package/dist/src/server/agent/providers/claude/partial-json.js.map +1 -0
  340. package/dist/src/server/agent/providers/claude/sidechain-tracker.js +230 -0
  341. package/dist/src/server/agent/providers/claude/sidechain-tracker.js.map +1 -0
  342. package/dist/src/server/agent/providers/claude/task-notification-tool-call.js +37 -20
  343. package/dist/src/server/agent/providers/claude/task-notification-tool-call.js.map +1 -1
  344. package/dist/src/server/agent/providers/claude/tool-call-detail-parser.js +21 -11
  345. package/dist/src/server/agent/providers/claude/tool-call-detail-parser.js.map +1 -1
  346. package/dist/src/server/agent/providers/claude/tool-call-mapper.js +23 -11
  347. package/dist/src/server/agent/providers/claude/tool-call-mapper.js.map +1 -1
  348. package/dist/src/server/agent/providers/claude-agent.js +508 -1141
  349. package/dist/src/server/agent/providers/claude-agent.js.map +1 -1
  350. package/dist/src/server/agent/providers/codex/tool-call-detail-parser.js +2 -2
  351. package/dist/src/server/agent/providers/codex/tool-call-detail-parser.js.map +1 -1
  352. package/dist/src/server/agent/providers/codex/tool-call-mapper.js +14 -11
  353. package/dist/src/server/agent/providers/codex/tool-call-mapper.js.map +1 -1
  354. package/dist/src/server/agent/providers/codex-app-server-agent.js +347 -163
  355. package/dist/src/server/agent/providers/codex-app-server-agent.js.map +1 -1
  356. package/dist/src/server/agent/providers/codex-rollout-timeline.js +21 -32
  357. package/dist/src/server/agent/providers/codex-rollout-timeline.js.map +1 -1
  358. package/dist/src/server/agent/providers/opencode/tool-call-detail-parser.js +2 -2
  359. package/dist/src/server/agent/providers/opencode/tool-call-detail-parser.js.map +1 -1
  360. package/dist/src/server/agent/providers/opencode/tool-call-mapper.js +2 -9
  361. package/dist/src/server/agent/providers/opencode/tool-call-mapper.js.map +1 -1
  362. package/dist/src/server/agent/providers/opencode-agent.js +5 -5
  363. package/dist/src/server/agent/providers/opencode-agent.js.map +1 -1
  364. package/dist/src/server/agent/providers/tool-call-detail-primitives.js +149 -15
  365. package/dist/src/server/agent/providers/tool-call-detail-primitives.js.map +1 -1
  366. package/dist/src/server/agent/providers/tool-call-mapper-utils.js +1 -3
  367. package/dist/src/server/agent/providers/tool-call-mapper-utils.js.map +1 -1
  368. package/dist/src/server/agent/stt-manager.js +1 -2
  369. package/dist/src/server/agent/stt-manager.js.map +1 -1
  370. package/dist/src/server/agent/timeline-projection.js.map +1 -1
  371. package/dist/src/server/agent/tts-manager.js +27 -9
  372. package/dist/src/server/agent/tts-manager.js.map +1 -1
  373. package/dist/src/server/agent/wait-for-agent-tracker.js.map +1 -1
  374. package/dist/src/server/agent-attention-policy.js.map +1 -1
  375. package/dist/src/server/allowed-hosts.js.map +1 -1
  376. package/dist/src/server/bootstrap.js +46 -5
  377. package/dist/src/server/bootstrap.js.map +1 -1
  378. package/dist/src/server/config.js +4 -11
  379. package/dist/src/server/config.js.map +1 -1
  380. package/dist/src/server/connection-offer.js +2 -3
  381. package/dist/src/server/connection-offer.js.map +1 -1
  382. package/dist/src/server/daemon-version.js +1 -1
  383. package/dist/src/server/daemon-version.js.map +1 -1
  384. package/dist/src/server/dictation/dictation-stream-manager.js +4 -1
  385. package/dist/src/server/dictation/dictation-stream-manager.js.map +1 -1
  386. package/dist/src/server/file-explorer/service.js +5 -8
  387. package/dist/src/server/file-explorer/service.js.map +1 -1
  388. package/dist/src/server/messages.js.map +1 -1
  389. package/dist/src/server/package-version.js +1 -2
  390. package/dist/src/server/package-version.js.map +1 -1
  391. package/dist/src/server/pairing-offer.js +45 -0
  392. package/dist/src/server/pairing-offer.js.map +1 -0
  393. package/dist/src/server/pairing-qr.js +45 -0
  394. package/dist/src/server/pairing-qr.js.map +1 -0
  395. package/dist/src/server/persisted-config.js.map +1 -1
  396. package/dist/src/server/persistence-hooks.js.map +1 -1
  397. package/dist/src/server/pid-lock.js.map +1 -1
  398. package/dist/src/server/push/push-service.js.map +1 -1
  399. package/dist/src/server/relay-transport.js +6 -2
  400. package/dist/src/server/relay-transport.js.map +1 -1
  401. package/dist/src/server/session.js +1234 -998
  402. package/dist/src/server/session.js.map +1 -1
  403. package/dist/src/server/speech/audio.js.map +1 -1
  404. package/dist/src/server/speech/providers/local/config.js +5 -14
  405. package/dist/src/server/speech/providers/local/config.js.map +1 -1
  406. package/dist/src/server/speech/providers/local/models.js +1 -1
  407. package/dist/src/server/speech/providers/local/models.js.map +1 -1
  408. package/dist/src/server/speech/providers/local/pocket/pocket-tts-onnx.js +21 -7
  409. package/dist/src/server/speech/providers/local/pocket/pocket-tts-onnx.js.map +1 -1
  410. package/dist/src/server/speech/providers/local/runtime.js +1 -23
  411. package/dist/src/server/speech/providers/local/runtime.js.map +1 -1
  412. package/dist/src/server/speech/providers/local/sherpa/model-catalog.js.map +1 -1
  413. package/dist/src/server/speech/providers/local/sherpa/model-downloader.js.map +1 -1
  414. package/dist/src/server/speech/providers/local/sherpa/sherpa-offline-recognizer.js +9 -4
  415. package/dist/src/server/speech/providers/local/sherpa/sherpa-offline-recognizer.js.map +1 -1
  416. package/dist/src/server/speech/providers/local/sherpa/sherpa-online-recognizer.js +7 -2
  417. package/dist/src/server/speech/providers/local/sherpa/sherpa-online-recognizer.js.map +1 -1
  418. package/dist/src/server/speech/providers/local/sherpa/sherpa-parakeet-realtime-session.js +5 -1
  419. package/dist/src/server/speech/providers/local/sherpa/sherpa-parakeet-realtime-session.js.map +1 -1
  420. package/dist/src/server/speech/providers/local/sherpa/sherpa-parakeet-stt.js +2 -4
  421. package/dist/src/server/speech/providers/local/sherpa/sherpa-parakeet-stt.js.map +1 -1
  422. package/dist/src/server/speech/providers/local/sherpa/sherpa-realtime-session.js +1 -3
  423. package/dist/src/server/speech/providers/local/sherpa/sherpa-realtime-session.js.map +1 -1
  424. package/dist/src/server/speech/providers/local/sherpa/sherpa-stt.js +2 -4
  425. package/dist/src/server/speech/providers/local/sherpa/sherpa-stt.js.map +1 -1
  426. package/dist/src/server/speech/providers/local/sherpa/sherpa-tts.js +4 -1
  427. package/dist/src/server/speech/providers/local/sherpa/sherpa-tts.js.map +1 -1
  428. package/dist/src/server/speech/providers/local/sherpa/silero-vad-provider.js +1 -1
  429. package/dist/src/server/speech/providers/local/sherpa/silero-vad-provider.js.map +1 -1
  430. package/dist/src/server/speech/providers/local/sherpa/silero-vad-session.js.map +1 -1
  431. package/dist/src/server/speech/providers/openai/config.js +5 -24
  432. package/dist/src/server/speech/providers/openai/config.js.map +1 -1
  433. package/dist/src/server/speech/providers/openai/realtime-transcription-session.js +6 -3
  434. package/dist/src/server/speech/providers/openai/realtime-transcription-session.js.map +1 -1
  435. package/dist/src/server/speech/providers/openai/runtime.js +2 -6
  436. package/dist/src/server/speech/providers/openai/runtime.js.map +1 -1
  437. package/dist/src/server/speech/providers/openai/stt.js +1 -3
  438. package/dist/src/server/speech/providers/openai/stt.js.map +1 -1
  439. package/dist/src/server/speech/providers/openai/tts.js.map +1 -1
  440. package/dist/src/server/speech/speech-config-resolver.js +3 -7
  441. package/dist/src/server/speech/speech-config-resolver.js.map +1 -1
  442. package/dist/src/server/speech/speech-runtime.js.map +1 -1
  443. package/dist/src/server/utils/diff-highlighter.js +49 -36
  444. package/dist/src/server/utils/diff-highlighter.js.map +1 -1
  445. package/dist/src/server/voice/fixed-duration-pcm-ring-buffer.js.map +1 -1
  446. package/dist/src/server/voice/voice-turn-controller.js +1 -3
  447. package/dist/src/server/voice/voice-turn-controller.js.map +1 -1
  448. package/dist/src/server/voice-config.js.map +1 -1
  449. package/dist/src/server/voice-mcp-bridge.js.map +1 -1
  450. package/dist/src/server/websocket-server.js +20 -22
  451. package/dist/src/server/websocket-server.js.map +1 -1
  452. package/dist/src/server/workspace-registry-bootstrap.js +6 -6
  453. package/dist/src/server/workspace-registry-bootstrap.js.map +1 -1
  454. package/dist/src/server/workspace-registry-model.js +40 -15
  455. package/dist/src/server/workspace-registry-model.js.map +1 -1
  456. package/dist/src/server/workspace-registry.js +16 -13
  457. package/dist/src/server/workspace-registry.js.map +1 -1
  458. package/dist/src/server/worktree-bootstrap.js +17 -6
  459. package/dist/src/server/worktree-bootstrap.js.map +1 -1
  460. package/dist/src/shared/agent-attention-notification.js.map +1 -1
  461. package/dist/src/shared/daemon-endpoints.js +11 -2
  462. package/dist/src/shared/daemon-endpoints.js.map +1 -1
  463. package/dist/src/shared/messages.js +330 -302
  464. package/dist/src/shared/messages.js.map +1 -1
  465. package/dist/src/shared/terminal-stream-protocol.js +99 -0
  466. package/dist/src/shared/terminal-stream-protocol.js.map +1 -0
  467. package/dist/src/shared/tool-call-display.js +6 -3
  468. package/dist/src/shared/tool-call-display.js.map +1 -1
  469. package/dist/src/terminal/terminal.js +44 -98
  470. package/dist/src/terminal/terminal.js.map +1 -1
  471. package/dist/src/utils/checkout-git.js +111 -120
  472. package/dist/src/utils/checkout-git.js.map +1 -1
  473. package/dist/src/utils/directory-suggestions.js +40 -40
  474. package/dist/src/utils/directory-suggestions.js.map +1 -1
  475. package/dist/src/utils/project-icon.js +2 -11
  476. package/dist/src/utils/project-icon.js.map +1 -1
  477. package/dist/src/utils/worktree.js +22 -19
  478. package/dist/src/utils/worktree.js.map +1 -1
  479. package/package.json +3 -11
  480. package/dist/server/client/daemon-client-terminal-stream-manager.d.ts +0 -43
  481. package/dist/server/client/daemon-client-terminal-stream-manager.d.ts.map +0 -1
  482. package/dist/server/client/daemon-client-terminal-stream-manager.js +0 -134
  483. package/dist/server/client/daemon-client-terminal-stream-manager.js.map +0 -1
  484. package/dist/server/server/utils/syntax-highlighter.d.ts +0 -10
  485. package/dist/server/server/utils/syntax-highlighter.d.ts.map +0 -1
  486. package/dist/server/server/utils/syntax-highlighter.js +0 -145
  487. package/dist/server/server/utils/syntax-highlighter.js.map +0 -1
  488. package/dist/server/shared/binary-mux.d.ts +0 -31
  489. package/dist/server/shared/binary-mux.d.ts.map +0 -1
  490. package/dist/server/shared/binary-mux.js +0 -114
  491. package/dist/server/shared/binary-mux.js.map +0 -1
  492. package/dist/server/shared/terminal-key-input.d.ts +0 -9
  493. package/dist/server/shared/terminal-key-input.d.ts.map +0 -1
  494. package/dist/server/shared/terminal-key-input.js +0 -132
  495. package/dist/server/shared/terminal-key-input.js.map +0 -1
  496. package/dist/src/server/utils/syntax-highlighter.js +0 -145
  497. package/dist/src/server/utils/syntax-highlighter.js.map +0 -1
  498. package/dist/src/shared/binary-mux.js +0 -114
  499. package/dist/src/shared/binary-mux.js.map +0 -1
@@ -1,56 +1,58 @@
1
- import { v4 as uuidv4 } from 'uuid';
2
- import { watch } from 'node:fs';
3
- import { stat } from 'fs/promises';
4
- import { exec } from 'child_process';
5
- import { promisify } from 'util';
6
- import { join, resolve, sep } from 'path';
7
- import { homedir } from 'node:os';
8
- import { z } from 'zod';
9
- import { serializeAgentStreamEvent, } from './messages.js';
10
- import { BinaryMuxChannel, TerminalBinaryFlags, TerminalBinaryMessageType, } from '../shared/binary-mux.js';
11
- import { TTSManager } from './agent/tts-manager.js';
12
- import { STTManager } from './agent/stt-manager.js';
13
- import { maybePersistTtsDebugAudio } from './agent/tts-debug.js';
14
- import { isPaseoDictationDebugEnabled } from './agent/recordings-debug.js';
15
- import { DictationStreamManager, } from './dictation/dictation-stream-manager.js';
16
- import { createVoiceTurnController, } from './voice/voice-turn-controller.js';
17
- import { buildConfigOverrides, buildSessionConfig, extractTimestamps } from './persistence-hooks.js';
18
- import { experimental_createMCPClient } from 'ai';
19
- import { buildProviderRegistry } from './agent/provider-registry.js';
20
- import { scheduleAgentMetadataGeneration } from './agent/agent-metadata-generator.js';
21
- import { resolveEffectiveThinkingOptionId, toAgentPayload } from './agent/agent-projections.js';
22
- import { MAX_EXPLICIT_AGENT_TITLE_CHARS } from './agent/agent-title-limits.js';
23
- import { appendTimelineItemIfAgentKnown, emitLiveTimelineItemIfAgentKnown, } from './agent/timeline-append.js';
24
- import { projectTimelineRows, selectTimelineWindowByProjectedLimit, } from './agent/timeline-projection.js';
25
- import { DEFAULT_STRUCTURED_GENERATION_PROVIDERS, StructuredAgentFallbackError, StructuredAgentResponseError, generateStructuredAgentResponseWithFallback, } from './agent/agent-response-loop.js';
26
- import { isValidAgentProvider, AGENT_PROVIDER_IDS } from './agent/provider-manifest.js';
27
- import { buildProjectPlacementForCwd, deriveProjectKind, deriveProjectRootPath, deriveWorkspaceDisplayName, deriveWorkspaceKind, normalizeWorkspaceId as normalizePersistedWorkspaceId, } from './workspace-registry-model.js';
28
- import { createPersistedProjectRecord, createPersistedWorkspaceRecord, } from './workspace-registry.js';
29
- import { buildVoiceAgentMcpServerConfig, buildVoiceModeSystemPrompt, stripVoiceModeSystemPrompt, } from './voice-config.js';
30
- import { isVoicePermissionAllowed } from './voice-permission-policy.js';
31
- import { listDirectoryEntries, readExplorerFile, getDownloadableFileInfo, } from './file-explorer/service.js';
32
- import { slugify, validateBranchSlug, listPaseoWorktrees, deletePaseoWorktree, isPaseoOwnedWorktreeCwd, resolvePaseoWorktreeRootForCwd, } from '../utils/worktree.js';
33
- import { createAgentWorktree, runAsyncWorktreeBootstrap } from './worktree-bootstrap.js';
34
- import { getCheckoutDiff, getCheckoutShortstat, getCheckoutStatus, listBranchSuggestions, NotGitRepoError, MergeConflictError, MergeFromBaseConflictError, commitChanges, mergeToBase, mergeFromBase, pushCurrentBranch, createPullRequest, getPullRequestStatus, } from '../utils/checkout-git.js';
35
- import { getProjectIcon } from '../utils/project-icon.js';
36
- import { expandTilde } from '../utils/path.js';
37
- import { searchHomeDirectories, searchWorkspaceEntries } from '../utils/directory-suggestions.js';
38
- import { ensureLocalSpeechModels, getLocalSpeechModelDir, listLocalSpeechModels, } from './speech/providers/local/models.js';
39
- import { toResolver } from './speech/provider-resolver.js';
40
- import { resolveClientMessageId } from './client-message-id.js';
1
+ import { v4 as uuidv4 } from "uuid";
2
+ import { watch } from "node:fs";
3
+ import { readFile, stat } from "fs/promises";
4
+ import { exec } from "child_process";
5
+ import { promisify } from "util";
6
+ import { join, resolve, sep } from "path";
7
+ import { homedir } from "node:os";
8
+ import { z } from "zod";
9
+ import { serializeAgentStreamEvent, } from "./messages.js";
10
+ import { TerminalStreamOpcode, encodeTerminalSnapshotPayload, encodeTerminalStreamFrame, decodeTerminalResizePayload, } from "../shared/terminal-stream-protocol.js";
11
+ import { TTSManager } from "./agent/tts-manager.js";
12
+ import { STTManager } from "./agent/stt-manager.js";
13
+ import { maybePersistTtsDebugAudio } from "./agent/tts-debug.js";
14
+ import { isPaseoDictationDebugEnabled } from "./agent/recordings-debug.js";
15
+ import { DictationStreamManager, } from "./dictation/dictation-stream-manager.js";
16
+ import { createVoiceTurnController, } from "./voice/voice-turn-controller.js";
17
+ import { buildConfigOverrides, buildSessionConfig, extractTimestamps, } from "./persistence-hooks.js";
18
+ import { experimental_createMCPClient } from "ai";
19
+ import { buildProviderRegistry } from "./agent/provider-registry.js";
20
+ import { scheduleAgentMetadataGeneration } from "./agent/agent-metadata-generator.js";
21
+ import { resolveEffectiveThinkingOptionId, toAgentPayload } from "./agent/agent-projections.js";
22
+ import { MAX_EXPLICIT_AGENT_TITLE_CHARS } from "./agent/agent-title-limits.js";
23
+ import { appendTimelineItemIfAgentKnown, emitLiveTimelineItemIfAgentKnown, } from "./agent/timeline-append.js";
24
+ import { projectTimelineRows, selectTimelineWindowByProjectedLimit, } from "./agent/timeline-projection.js";
25
+ import { DEFAULT_STRUCTURED_GENERATION_PROVIDERS, StructuredAgentFallbackError, StructuredAgentResponseError, generateStructuredAgentResponseWithFallback, } from "./agent/agent-response-loop.js";
26
+ import { isValidAgentProvider, AGENT_PROVIDER_IDS } from "./agent/provider-manifest.js";
27
+ import { buildProjectPlacementForCwd, detectStaleWorkspaces, deriveProjectKind, deriveProjectRootPath, deriveWorkspaceDisplayName, deriveWorkspaceKind, normalizeWorkspaceId as normalizePersistedWorkspaceId, } from "./workspace-registry-model.js";
28
+ import { createPersistedProjectRecord, createPersistedWorkspaceRecord, } from "./workspace-registry.js";
29
+ import { buildVoiceAgentMcpServerConfig, buildVoiceModeSystemPrompt, stripVoiceModeSystemPrompt, } from "./voice-config.js";
30
+ import { isVoicePermissionAllowed } from "./voice-permission-policy.js";
31
+ import { listDirectoryEntries, readExplorerFile, getDownloadableFileInfo, } from "./file-explorer/service.js";
32
+ import { computeWorktreePath, getWorktreeSetupCommands, resolveWorktreeRuntimeEnv, slugify, validateBranchSlug, listPaseoWorktrees, deletePaseoWorktree, isPaseoOwnedWorktreeCwd, resolvePaseoWorktreeRootForCwd, } from "../utils/worktree.js";
33
+ import { createAgentWorktree, runAsyncWorktreeBootstrap } from "./worktree-bootstrap.js";
34
+ import { getCheckoutDiff, getCheckoutShortstat, getCheckoutStatus, getCheckoutStatusLite, listBranchSuggestions, NotGitRepoError, MergeConflictError, MergeFromBaseConflictError, commitChanges, mergeToBase, mergeFromBase, pushCurrentBranch, createPullRequest, getPullRequestStatus, resolveRepositoryDefaultBranch, } from "../utils/checkout-git.js";
35
+ import { getProjectIcon } from "../utils/project-icon.js";
36
+ import { expandTilde } from "../utils/path.js";
37
+ import { searchHomeDirectories, searchWorkspaceEntries } from "../utils/directory-suggestions.js";
38
+ import { ensureLocalSpeechModels, getLocalSpeechModelDir, listLocalSpeechModels, } from "./speech/providers/local/models.js";
39
+ import { toResolver } from "./speech/provider-resolver.js";
40
+ import { resolveClientMessageId } from "./client-message-id.js";
41
41
  const execAsync = promisify(exec);
42
42
  const MAX_INITIAL_AGENT_TITLE_CHARS = Math.min(60, MAX_EXPLICIT_AGENT_TITLE_CHARS);
43
43
  const READ_ONLY_GIT_ENV = {
44
44
  ...process.env,
45
- GIT_OPTIONAL_LOCKS: '0',
45
+ GIT_OPTIONAL_LOCKS: "0",
46
46
  };
47
47
  const pendingAgentInitializations = new Map();
48
48
  const DEFAULT_AGENT_PROVIDER = AGENT_PROVIDER_IDS[0];
49
49
  const CHECKOUT_DIFF_WATCH_DEBOUNCE_MS = 150;
50
50
  const CHECKOUT_DIFF_FALLBACK_REFRESH_MS = 5000;
51
- const TERMINAL_STREAM_WINDOW_BYTES = 256 * 1024;
52
- const TERMINAL_STREAM_MAX_PENDING_BYTES = 2 * 1024 * 1024;
53
- const TERMINAL_STREAM_MAX_PENDING_CHUNKS = 2048;
51
+ const WORKSPACE_GIT_WATCH_DEBOUNCE_MS = 500;
52
+ const WORKSPACE_GIT_WATCH_REMOVED_FINGERPRINT = "__removed__";
53
+ const TERMINAL_STREAM_HIGH_WATER_BYTES = 256 * 1024;
54
+ const TERMINAL_STREAM_LOW_WATER_BYTES = 16 * 1024;
55
+ const MAX_TERMINAL_STREAM_SLOTS = 256;
54
56
  function deriveInitialAgentTitle(prompt) {
55
57
  const firstContentLine = prompt
56
58
  .split(/\r?\n/)
@@ -59,7 +61,7 @@ function deriveInitialAgentTitle(prompt) {
59
61
  if (!firstContentLine) {
60
62
  return null;
61
63
  }
62
- const normalized = firstContentLine.replace(/\s+/g, ' ').trim();
64
+ const normalized = firstContentLine.replace(/\s+/g, " ").trim();
63
65
  if (!normalized) {
64
66
  return null;
65
67
  }
@@ -67,7 +69,7 @@ function deriveInitialAgentTitle(prompt) {
67
69
  return clamped.length > 0 ? clamped : null;
68
70
  }
69
71
  export function resolveCreateAgentTitles(options) {
70
- const explicitTitle = typeof options.configTitle === 'string' && options.configTitle.trim().length > 0
72
+ const explicitTitle = typeof options.configTitle === "string" && options.configTitle.trim().length > 0
71
73
  ? options.configTitle.trim()
72
74
  : null;
73
75
  const trimmedPrompt = options.initialPrompt?.trim();
@@ -81,7 +83,7 @@ class SessionRequestError extends Error {
81
83
  constructor(code, message) {
82
84
  super(message);
83
85
  this.code = code;
84
- this.name = 'SessionRequestError';
86
+ this.name = "SessionRequestError";
85
87
  }
86
88
  }
87
89
  const PCM_SAMPLE_RATE = 16000;
@@ -92,12 +94,12 @@ const MIN_STREAMING_SEGMENT_DURATION_MS = 1000;
92
94
  const MIN_STREAMING_SEGMENT_BYTES = Math.round(PCM_BYTES_PER_MS * MIN_STREAMING_SEGMENT_DURATION_MS);
93
95
  const SAFE_GIT_REF_PATTERN = /^[A-Za-z0-9._\/-]+$/;
94
96
  const AgentIdSchema = z.string().uuid();
95
- const VOICE_MCP_SERVER_NAME = 'paseo_voice';
97
+ const VOICE_MCP_SERVER_NAME = "paseo_voice";
96
98
  const VOICE_INTERRUPT_CONFIRMATION_MS = 500;
97
99
  class VoiceFeatureUnavailableError extends Error {
98
100
  constructor(context) {
99
101
  super(context.message);
100
- this.name = 'VoiceFeatureUnavailableError';
102
+ this.name = "VoiceFeatureUnavailableError";
101
103
  this.reasonCode = context.reasonCode;
102
104
  this.retryable = context.retryable;
103
105
  this.missingModelIds = [...context.missingModelIds];
@@ -108,10 +110,10 @@ function convertPCMToWavBuffer(pcmBuffer, sampleRate, channels, bitsPerSample) {
108
110
  const wavBuffer = Buffer.alloc(headerSize + pcmBuffer.length);
109
111
  const byteRate = (sampleRate * channels * bitsPerSample) / 8;
110
112
  const blockAlign = (channels * bitsPerSample) / 8;
111
- wavBuffer.write('RIFF', 0);
113
+ wavBuffer.write("RIFF", 0);
112
114
  wavBuffer.writeUInt32LE(36 + pcmBuffer.length, 4);
113
- wavBuffer.write('WAVE', 8);
114
- wavBuffer.write('fmt ', 12);
115
+ wavBuffer.write("WAVE", 8);
116
+ wavBuffer.write("fmt ", 12);
115
117
  wavBuffer.writeUInt32LE(16, 16);
116
118
  wavBuffer.writeUInt16LE(1, 20);
117
119
  wavBuffer.writeUInt16LE(channels, 22);
@@ -119,7 +121,7 @@ function convertPCMToWavBuffer(pcmBuffer, sampleRate, channels, bitsPerSample) {
119
121
  wavBuffer.writeUInt32LE(byteRate, 28);
120
122
  wavBuffer.writeUInt16LE(blockAlign, 32);
121
123
  wavBuffer.writeUInt16LE(bitsPerSample, 34);
122
- wavBuffer.write('data', 36);
124
+ wavBuffer.write("data", 36);
123
125
  wavBuffer.writeUInt32LE(pcmBuffer.length, 40);
124
126
  pcmBuffer.copy(wavBuffer, 44);
125
127
  return wavBuffer;
@@ -128,7 +130,7 @@ function coerceAgentProvider(logger, value, agentId) {
128
130
  if (isValidAgentProvider(value)) {
129
131
  return value;
130
132
  }
131
- logger.warn({ value, agentId, defaultProvider: DEFAULT_AGENT_PROVIDER }, `Unknown provider '${value}' for agent ${agentId ?? 'unknown'}; defaulting to '${DEFAULT_AGENT_PROVIDER}'`);
133
+ logger.warn({ value, agentId, defaultProvider: DEFAULT_AGENT_PROVIDER }, `Unknown provider '${value}' for agent ${agentId ?? "unknown"}; defaulting to '${DEFAULT_AGENT_PROVIDER}'`);
132
134
  return DEFAULT_AGENT_PROVIDER;
133
135
  }
134
136
  function toAgentPersistenceHandle(logger, handle) {
@@ -141,7 +143,7 @@ function toAgentPersistenceHandle(logger, handle) {
141
143
  return null;
142
144
  }
143
145
  if (!handle.sessionId) {
144
- logger.warn('Ignoring persistence handle missing sessionId');
146
+ logger.warn("Ignoring persistence handle missing sessionId");
145
147
  return null;
146
148
  }
147
149
  return {
@@ -158,7 +160,7 @@ function toAgentPersistenceHandle(logger, handle) {
158
160
  */
159
161
  export class Session {
160
162
  constructor(options) {
161
- this.processingPhase = 'idle';
163
+ this.processingPhase = "idle";
162
164
  // Voice mode state
163
165
  this.isVoiceMode = false;
164
166
  this.speechInProgress = false;
@@ -184,13 +186,13 @@ export class Session {
184
186
  this.MOBILE_BACKGROUND_STREAM_GRACE_MS = 60000;
185
187
  this.subscribedTerminalDirectories = new Set();
186
188
  this.unsubscribeTerminalsChanged = null;
187
- this.terminalSubscriptions = new Map();
188
189
  this.terminalExitSubscriptions = new Map();
189
- this.terminalStreams = new Map();
190
- this.terminalStreamByTerminalId = new Map();
191
- this.nextTerminalStreamId = 1;
190
+ this.activeTerminalStreams = new Map();
191
+ this.terminalIdToSlot = new Map();
192
+ this.nextTerminalSlot = 0;
192
193
  this.checkoutDiffSubscriptions = new Map();
193
194
  this.checkoutDiffTargets = new Map();
195
+ this.workspaceGitWatchTargets = new Map();
194
196
  this.voiceModeAgentId = null;
195
197
  this.voiceModeBaseConfig = null;
196
198
  this.workspaceStatePriority = {
@@ -200,11 +202,12 @@ export class Session {
200
202
  attention: 3,
201
203
  done: 4,
202
204
  };
203
- const { clientId, onMessage, onBinaryMessage, onLifecycleIntent, logger, downloadTokenStore, pushTokenStore, paseoHome, agentManager, agentStorage, projectRegistry, workspaceRegistry, createAgentMcpTransport, stt, tts, terminalManager, voice, voiceBridge, dictation, agentProviderRuntimeSettings, } = options;
205
+ const { clientId, onMessage, onBinaryMessage, getBinaryBufferedAmount, onLifecycleIntent, logger, downloadTokenStore, pushTokenStore, paseoHome, agentManager, agentStorage, projectRegistry, workspaceRegistry, createAgentMcpTransport, stt, tts, terminalManager, voice, voiceBridge, dictation, agentProviderRuntimeSettings, } = options;
204
206
  this.clientId = clientId;
205
207
  this.sessionId = uuidv4();
206
208
  this.onMessage = onMessage;
207
209
  this.onBinaryMessage = onBinaryMessage ?? null;
210
+ this.getBinaryBufferedAmount = getBinaryBufferedAmount ?? null;
208
211
  this.onLifecycleIntent = onLifecycleIntent ?? null;
209
212
  this.downloadTokenStore = downloadTokenStore;
210
213
  this.pushTokenStore = pushTokenStore;
@@ -224,11 +227,11 @@ export class Session {
224
227
  this.localSpeechModelsDir =
225
228
  configuredModelsDir && configuredModelsDir.length > 0
226
229
  ? configuredModelsDir
227
- : join(this.paseoHome, 'models', 'local-speech');
230
+ : join(this.paseoHome, "models", "local-speech");
228
231
  this.defaultLocalSpeechModelIds =
229
232
  dictation?.localModels?.defaultModelIds && dictation.localModels.defaultModelIds.length > 0
230
233
  ? [...new Set(dictation.localModels.defaultModelIds)]
231
- : ['parakeet-tdt-0.6b-v2-int8', 'kokoro-en-v0_19'];
234
+ : ["parakeet-tdt-0.6b-v2-int8", "kokoro-en-v0_19"];
232
235
  this.registerVoiceSpeakHandler = voiceBridge?.registerVoiceSpeakHandler;
233
236
  this.unregisterVoiceSpeakHandler = voiceBridge?.unregisterVoiceSpeakHandler;
234
237
  this.registerVoiceCallerContext = voiceBridge?.registerVoiceCallerContext;
@@ -239,7 +242,7 @@ export class Session {
239
242
  this.agentProviderRuntimeSettings = agentProviderRuntimeSettings;
240
243
  this.abortController = new AbortController();
241
244
  this.sessionLogger = logger.child({
242
- module: 'session',
245
+ module: "session",
243
246
  clientId: this.clientId,
244
247
  sessionId: this.sessionId,
245
248
  });
@@ -259,7 +262,7 @@ export class Session {
259
262
  // Initialize agent MCP client asynchronously
260
263
  void this.initializeAgentMcp();
261
264
  this.subscribeToAgentEvents();
262
- this.sessionLogger.trace('Session created');
265
+ this.sessionLogger.trace("Session created");
263
266
  }
264
267
  /**
265
268
  * Get the client's current activity state
@@ -282,8 +285,7 @@ export class Session {
282
285
  checkoutDiffWatcherCount,
283
286
  checkoutDiffFallbackRefreshTargetCount,
284
287
  terminalDirectorySubscriptionCount: this.subscribedTerminalDirectories.size,
285
- terminalSubscriptionCount: this.terminalSubscriptions.size,
286
- terminalStreamCount: this.terminalStreams.size,
288
+ terminalSubscriptionCount: this.activeTerminalStreams.size,
287
289
  };
288
290
  }
289
291
  /**
@@ -296,16 +298,16 @@ export class Session {
296
298
  * Normalize a user prompt (with optional image metadata) for AgentManager
297
299
  */
298
300
  buildAgentPrompt(text, images) {
299
- const normalized = text?.trim() ?? '';
301
+ const normalized = text?.trim() ?? "";
300
302
  if (!images || images.length === 0) {
301
303
  return normalized;
302
304
  }
303
305
  const blocks = [];
304
306
  if (normalized.length > 0) {
305
- blocks.push({ type: 'text', text: normalized });
307
+ blocks.push({ type: "text", text: normalized });
306
308
  }
307
309
  for (const image of images) {
308
- blocks.push({ type: 'image', data: image.data, mimeType: image.mimeType });
310
+ blocks.push({ type: "image", data: image.data, mimeType: image.mimeType });
309
311
  }
310
312
  return blocks;
311
313
  }
@@ -316,20 +318,20 @@ export class Session {
316
318
  async interruptAgentIfRunning(agentId) {
317
319
  const snapshot = this.agentManager.getAgent(agentId);
318
320
  if (!snapshot) {
319
- this.sessionLogger.trace({ agentId }, 'interruptAgentIfRunning: agent not found');
321
+ this.sessionLogger.trace({ agentId }, "interruptAgentIfRunning: agent not found");
320
322
  throw new Error(`Agent ${agentId} not found`);
321
323
  }
322
- if (snapshot.lifecycle !== 'running' && !snapshot.pendingRun) {
323
- this.sessionLogger.trace({ agentId, lifecycle: snapshot.lifecycle, pendingRun: Boolean(snapshot.pendingRun) }, 'interruptAgentIfRunning: skipping because agent is not running');
324
+ if (snapshot.lifecycle !== "running" && !snapshot.pendingRun) {
325
+ this.sessionLogger.trace({ agentId, lifecycle: snapshot.lifecycle, pendingRun: Boolean(snapshot.pendingRun) }, "interruptAgentIfRunning: skipping because agent is not running");
324
326
  return;
325
327
  }
326
- this.sessionLogger.debug({ agentId, lifecycle: snapshot.lifecycle, pendingRun: Boolean(snapshot.pendingRun) }, 'interruptAgentIfRunning: interrupting');
328
+ this.sessionLogger.debug({ agentId, lifecycle: snapshot.lifecycle, pendingRun: Boolean(snapshot.pendingRun) }, "interruptAgentIfRunning: interrupting");
327
329
  try {
328
330
  const t0 = Date.now();
329
331
  const cancelled = await this.agentManager.cancelAgentRun(agentId);
330
- this.sessionLogger.debug({ agentId, cancelled, durationMs: Date.now() - t0 }, 'interruptAgentIfRunning: cancelAgentRun completed');
332
+ this.sessionLogger.debug({ agentId, cancelled, durationMs: Date.now() - t0 }, "interruptAgentIfRunning: cancelAgentRun completed");
331
333
  if (!cancelled) {
332
- this.sessionLogger.warn({ agentId }, 'interruptAgentIfRunning: reported running but no active run was cancelled');
334
+ this.sessionLogger.warn({ agentId }, "interruptAgentIfRunning: reported running but no active run was cancelled");
333
335
  }
334
336
  }
335
337
  catch (error) {
@@ -344,7 +346,7 @@ export class Session {
344
346
  if (!snapshot) {
345
347
  return false;
346
348
  }
347
- return snapshot.lifecycle === 'running' || Boolean(snapshot.pendingRun);
349
+ return snapshot.lifecycle === "running" || Boolean(snapshot.pendingRun);
348
350
  }
349
351
  /**
350
352
  * Start streaming an agent run and forward results via the websocket broadcast
@@ -352,21 +354,25 @@ export class Session {
352
354
  startAgentStream(agentId, prompt, runOptions) {
353
355
  this.sessionLogger.trace({
354
356
  agentId,
355
- promptType: typeof prompt === 'string' ? 'string' : 'structured',
357
+ promptType: typeof prompt === "string" ? "string" : "structured",
356
358
  hasRunOptions: Boolean(runOptions),
357
- }, 'startAgentStream: requested');
359
+ }, "startAgentStream: requested");
358
360
  let iterator;
359
361
  try {
360
362
  const snapshot = this.agentManager.getAgent(agentId);
361
- const shouldReplace = Boolean(snapshot && (snapshot.lifecycle === 'running' || snapshot.pendingRun));
363
+ const shouldReplace = Boolean(snapshot && (snapshot.lifecycle === "running" || snapshot.pendingRun));
362
364
  iterator = shouldReplace
363
365
  ? this.agentManager.replaceAgentRun(agentId, prompt, runOptions)
364
366
  : this.agentManager.streamAgent(agentId, prompt, runOptions);
365
- this.sessionLogger.trace({ agentId, shouldReplace }, 'startAgentStream: agent iterator returned');
367
+ this.sessionLogger.trace({ agentId, shouldReplace }, "startAgentStream: agent iterator returned");
366
368
  }
367
369
  catch (error) {
368
- this.handleAgentRunError(agentId, error, 'Failed to start agent run');
369
- const message = error instanceof Error ? error.message : typeof error === 'string' ? error : 'Unknown error';
370
+ this.handleAgentRunError(agentId, error, "Failed to start agent run");
371
+ const message = error instanceof Error
372
+ ? error.message
373
+ : typeof error === "string"
374
+ ? error
375
+ : "Unknown error";
370
376
  return { ok: false, error: message };
371
377
  }
372
378
  void (async () => {
@@ -374,24 +380,24 @@ export class Session {
374
380
  for await (const _ of iterator) {
375
381
  // Events are forwarded via the session's AgentManager subscription.
376
382
  }
377
- this.sessionLogger.trace({ agentId }, 'startAgentStream: iterator drained');
383
+ this.sessionLogger.trace({ agentId }, "startAgentStream: iterator drained");
378
384
  }
379
385
  catch (error) {
380
- this.sessionLogger.trace({ agentId, err: error }, 'startAgentStream: iterator threw');
381
- this.handleAgentRunError(agentId, error, 'Agent stream failed');
386
+ this.sessionLogger.trace({ agentId, err: error }, "startAgentStream: iterator threw");
387
+ this.handleAgentRunError(agentId, error, "Agent stream failed");
382
388
  }
383
389
  })();
384
390
  return { ok: true };
385
391
  }
386
392
  handleAgentRunError(agentId, error, context) {
387
- const message = error instanceof Error ? error.message : typeof error === 'string' ? error : 'Unknown error';
393
+ const message = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
388
394
  this.sessionLogger.error({ err: error, agentId, context }, `${context} for agent ${agentId}`);
389
395
  this.emit({
390
- type: 'activity_log',
396
+ type: "activity_log",
391
397
  payload: {
392
398
  id: uuidv4(),
393
399
  timestamp: new Date(),
394
- type: 'error',
400
+ type: "error",
395
401
  content: `${context}: ${message}`,
396
402
  },
397
403
  });
@@ -411,7 +417,7 @@ export class Session {
411
417
  this.sessionLogger.trace({ agentToolCount }, `Agent MCP initialized with ${agentToolCount} tools`);
412
418
  }
413
419
  catch (error) {
414
- this.sessionLogger.error({ err: error }, 'Failed to initialize Agent MCP');
420
+ this.sessionLogger.error({ err: error }, "Failed to initialize Agent MCP");
415
421
  }
416
422
  }
417
423
  /**
@@ -422,32 +428,32 @@ export class Session {
422
428
  this.unsubscribeAgentEvents();
423
429
  }
424
430
  this.unsubscribeAgentEvents = this.agentManager.subscribe((event) => {
425
- if (event.type === 'agent_state') {
431
+ if (event.type === "agent_state") {
426
432
  void this.forwardAgentUpdate(event.agent);
427
433
  return;
428
434
  }
429
435
  if (this.isVoiceMode &&
430
436
  this.voiceModeAgentId === event.agentId &&
431
- event.event.type === 'permission_requested' &&
437
+ event.event.type === "permission_requested" &&
432
438
  isVoicePermissionAllowed(event.event.request)) {
433
439
  const requestId = event.event.request.id;
434
440
  void this.agentManager
435
441
  .respondToPermission(event.agentId, requestId, {
436
- behavior: 'allow',
442
+ behavior: "allow",
437
443
  })
438
444
  .catch((error) => {
439
445
  this.sessionLogger.warn({
440
446
  err: error,
441
447
  agentId: event.agentId,
442
448
  requestId,
443
- }, 'Failed to auto-allow speak tool permission in voice mode');
449
+ }, "Failed to auto-allow speak tool permission in voice mode");
444
450
  });
445
451
  }
446
452
  // Reduce bandwidth/CPU on mobile: only forward high-frequency agent stream events
447
453
  // for the focused agent, with a short grace window while backgrounded.
448
454
  // History catch-up is handled via pull-based `fetch_agent_timeline_request`.
449
455
  const activity = this.clientActivity;
450
- if (activity?.deviceType === 'mobile') {
456
+ if (activity?.deviceType === "mobile") {
451
457
  if (!activity.focusedAgentId) {
452
458
  return;
453
459
  }
@@ -469,25 +475,25 @@ export class Session {
469
475
  agentId: event.agentId,
470
476
  event: serializedEvent,
471
477
  timestamp: new Date().toISOString(),
472
- ...(typeof event.seq === 'number' ? { seq: event.seq } : {}),
473
- ...(typeof event.epoch === 'string' ? { epoch: event.epoch } : {}),
478
+ ...(typeof event.seq === "number" ? { seq: event.seq } : {}),
479
+ ...(typeof event.epoch === "string" ? { epoch: event.epoch } : {}),
474
480
  };
475
481
  this.emit({
476
- type: 'agent_stream',
482
+ type: "agent_stream",
477
483
  payload,
478
484
  });
479
- if (event.event.type === 'permission_requested') {
485
+ if (event.event.type === "permission_requested") {
480
486
  this.emit({
481
- type: 'agent_permission_request',
487
+ type: "agent_permission_request",
482
488
  payload: {
483
489
  agentId: event.agentId,
484
490
  request: event.event.request,
485
491
  },
486
492
  });
487
493
  }
488
- else if (event.event.type === 'permission_resolved') {
494
+ else if (event.event.type === "permission_resolved") {
489
495
  this.emit({
490
- type: 'agent_permission_resolved',
496
+ type: "agent_permission_resolved",
491
497
  payload: {
492
498
  agentId: event.agentId,
493
499
  requestId: event.event.requestId,
@@ -522,13 +528,13 @@ export class Session {
522
528
  ? {
523
529
  provider: coerceAgentProvider(this.sessionLogger, record.runtimeInfo.provider, record.id),
524
530
  sessionId: record.runtimeInfo.sessionId,
525
- ...(Object.prototype.hasOwnProperty.call(record.runtimeInfo, 'model')
531
+ ...(Object.prototype.hasOwnProperty.call(record.runtimeInfo, "model")
526
532
  ? { model: record.runtimeInfo.model ?? null }
527
533
  : {}),
528
- ...(Object.prototype.hasOwnProperty.call(record.runtimeInfo, 'thinkingOptionId')
534
+ ...(Object.prototype.hasOwnProperty.call(record.runtimeInfo, "thinkingOptionId")
529
535
  ? { thinkingOptionId: record.runtimeInfo.thinkingOptionId ?? null }
530
536
  : {}),
531
- ...(Object.prototype.hasOwnProperty.call(record.runtimeInfo, 'modeId')
537
+ ...(Object.prototype.hasOwnProperty.call(record.runtimeInfo, "modeId")
532
538
  ? { modeId: record.runtimeInfo.modeId ?? null }
533
539
  : {}),
534
540
  ...(record.runtimeInfo.extra ? { extra: record.runtimeInfo.extra } : {}),
@@ -582,12 +588,12 @@ export class Session {
582
588
  let snapshot;
583
589
  if (handle) {
584
590
  snapshot = await this.agentManager.resumeAgentFromPersistence(handle, buildConfigOverrides(record), agentId, extractTimestamps(record));
585
- this.sessionLogger.info({ agentId, provider: record.provider }, 'Agent resumed from persistence');
591
+ this.sessionLogger.info({ agentId, provider: record.provider }, "Agent resumed from persistence");
586
592
  }
587
593
  else {
588
594
  const config = buildSessionConfig(record);
589
595
  snapshot = await this.agentManager.createAgent(config, agentId, { labels: record.labels });
590
- this.sessionLogger.info({ agentId, provider: record.provider }, 'Agent created from stored config');
596
+ this.sessionLogger.info({ agentId, provider: record.provider }, "Agent created from stored config");
591
597
  }
592
598
  await this.agentManager.hydrateTimelineFromProvider(agentId);
593
599
  return this.agentManager.getAgent(agentId) ?? snapshot;
@@ -634,7 +640,7 @@ export class Session {
634
640
  return false;
635
641
  }
636
642
  }
637
- if (typeof filter?.requiresAttention === 'boolean') {
643
+ if (typeof filter?.requiresAttention === "boolean") {
638
644
  const requiresAttention = agent.requiresAttention ?? false;
639
645
  if (requiresAttention !== filter.requiresAttention) {
640
646
  return false;
@@ -649,7 +655,7 @@ export class Session {
649
655
  return true;
650
656
  }
651
657
  getAgentUpdateTargetId(update) {
652
- return update.kind === 'remove' ? update.agentId : update.agent.id;
658
+ return update.kind === "remove" ? update.agentId : update.agent.id;
653
659
  }
654
660
  bufferOrEmitAgentUpdate(subscription, payload) {
655
661
  if (subscription.isBootstrapping) {
@@ -657,7 +663,7 @@ export class Session {
657
663
  return;
658
664
  }
659
665
  this.emit({
660
- type: 'agent_update',
666
+ type: "agent_update",
661
667
  payload,
662
668
  });
663
669
  }
@@ -670,9 +676,9 @@ export class Session {
670
676
  const pending = Array.from(subscription.pendingUpdatesByAgentId.values());
671
677
  subscription.pendingUpdatesByAgentId.clear();
672
678
  for (const payload of pending) {
673
- if (payload.kind === 'upsert') {
679
+ if (payload.kind === "upsert") {
674
680
  const snapshotUpdatedAt = options?.snapshotUpdatedAtByAgentId?.get(payload.agent.id);
675
- if (typeof snapshotUpdatedAt === 'number') {
681
+ if (typeof snapshotUpdatedAt === "number") {
676
682
  const updateUpdatedAt = Date.parse(payload.agent.updatedAt);
677
683
  if (!Number.isNaN(updateUpdatedAt) && updateUpdatedAt <= snapshotUpdatedAt) {
678
684
  continue;
@@ -680,7 +686,7 @@ export class Session {
680
686
  }
681
687
  }
682
688
  this.emit({
683
- type: 'agent_update',
689
+ type: "agent_update",
684
690
  payload,
685
691
  });
686
692
  }
@@ -730,6 +736,9 @@ export class Session {
730
736
  const normalizedWorkspaceId = normalizePersistedWorkspaceId(workspaceId);
731
737
  const existing = await this.workspaceRegistry.get(normalizedWorkspaceId);
732
738
  const placement = await this.buildProjectPlacement(normalizedWorkspaceId);
739
+ await this.syncWorkspaceGitWatchTarget(normalizedWorkspaceId, {
740
+ isGit: placement.checkout.isGit,
741
+ });
733
742
  const now = new Date().toISOString();
734
743
  const nextProjectCreatedAt = existing?.createdAt ?? now;
735
744
  const nextWorkspaceCreatedAt = existing?.createdAt ?? now;
@@ -764,9 +773,7 @@ export class Session {
764
773
  }
765
774
  await this.projectRegistry.upsert(nextProjectRecord);
766
775
  await this.workspaceRegistry.upsert(nextWorkspaceRecord);
767
- if (existing &&
768
- !existing.archivedAt &&
769
- existing.projectId !== nextWorkspaceRecord.projectId) {
776
+ if (existing && !existing.archivedAt && existing.projectId !== nextWorkspaceRecord.projectId) {
770
777
  await this.archiveProjectRecordIfEmpty(existing.projectId, now);
771
778
  }
772
779
  return {
@@ -777,7 +784,30 @@ export class Session {
777
784
  async reconcileActiveWorkspaceRecords() {
778
785
  const changedWorkspaceIds = new Set();
779
786
  const activeWorkspaces = (await this.workspaceRegistry.list()).filter((workspace) => !workspace.archivedAt);
787
+ const staleWorkspaceIds = await detectStaleWorkspaces({
788
+ activeWorkspaces,
789
+ agentRecords: (await this.agentStorage.list()).map((agent) => ({
790
+ cwd: agent.cwd,
791
+ archivedAt: agent.archivedAt ?? null,
792
+ })),
793
+ checkDirectoryExists: async (cwd) => {
794
+ try {
795
+ await stat(cwd);
796
+ return true;
797
+ }
798
+ catch {
799
+ return false;
800
+ }
801
+ },
802
+ });
803
+ for (const workspaceId of staleWorkspaceIds) {
804
+ await this.archiveWorkspaceRecord(workspaceId);
805
+ changedWorkspaceIds.add(workspaceId);
806
+ }
780
807
  for (const workspace of activeWorkspaces) {
808
+ if (staleWorkspaceIds.has(workspace.workspaceId)) {
809
+ continue;
810
+ }
781
811
  const result = await this.reconcileWorkspaceRecord(workspace.workspaceId);
782
812
  if (result.changed) {
783
813
  changedWorkspaceIds.add(result.workspace.workspaceId);
@@ -799,14 +829,14 @@ export class Session {
799
829
  });
800
830
  if (matches) {
801
831
  this.bufferOrEmitAgentUpdate(subscription, {
802
- kind: 'upsert',
832
+ kind: "upsert",
803
833
  agent: payload,
804
834
  project,
805
835
  });
806
836
  }
807
837
  else {
808
838
  this.bufferOrEmitAgentUpdate(subscription, {
809
- kind: 'remove',
839
+ kind: "remove",
810
840
  agentId: payload.id,
811
841
  });
812
842
  }
@@ -814,7 +844,7 @@ export class Session {
814
844
  await this.emitWorkspaceUpdateForCwd(payload.cwd);
815
845
  }
816
846
  catch (error) {
817
- this.sessionLogger.error({ err: error }, 'Failed to emit agent update');
847
+ this.sessionLogger.error({ err: error }, "Failed to emit agent update");
818
848
  }
819
849
  }
820
850
  /**
@@ -823,48 +853,48 @@ export class Session {
823
853
  async handleMessage(msg) {
824
854
  try {
825
855
  switch (msg.type) {
826
- case 'voice_audio_chunk':
856
+ case "voice_audio_chunk":
827
857
  await this.handleAudioChunk(msg);
828
858
  break;
829
- case 'abort_request':
859
+ case "abort_request":
830
860
  await this.handleAbort();
831
861
  break;
832
- case 'audio_played':
862
+ case "audio_played":
833
863
  this.handleAudioPlayed(msg.id);
834
864
  break;
835
- case 'fetch_agents_request':
865
+ case "fetch_agents_request":
836
866
  await this.handleFetchAgents(msg);
837
867
  break;
838
- case 'fetch_workspaces_request':
868
+ case "fetch_workspaces_request":
839
869
  await this.handleFetchWorkspacesRequest(msg);
840
870
  break;
841
- case 'fetch_agent_request':
871
+ case "fetch_agent_request":
842
872
  await this.handleFetchAgent(msg.agentId, msg.requestId);
843
873
  break;
844
- case 'delete_agent_request':
874
+ case "delete_agent_request":
845
875
  await this.handleDeleteAgentRequest(msg.agentId, msg.requestId);
846
876
  break;
847
- case 'archive_agent_request':
877
+ case "archive_agent_request":
848
878
  await this.handleArchiveAgentRequest(msg.agentId, msg.requestId);
849
879
  break;
850
- case 'update_agent_request':
880
+ case "update_agent_request":
851
881
  await this.handleUpdateAgentRequest(msg.agentId, msg.name, msg.labels, msg.requestId);
852
882
  break;
853
- case 'set_voice_mode':
883
+ case "set_voice_mode":
854
884
  await this.handleSetVoiceMode(msg.enabled, msg.agentId, msg.requestId);
855
885
  break;
856
- case 'send_agent_message_request':
886
+ case "send_agent_message_request":
857
887
  await this.handleSendAgentMessageRequest(msg);
858
888
  break;
859
- case 'wait_for_finish_request':
889
+ case "wait_for_finish_request":
860
890
  await this.handleWaitForFinish(msg.agentId, msg.requestId, msg.timeoutMs);
861
891
  break;
862
- case 'dictation_stream_start':
892
+ case "dictation_stream_start":
863
893
  {
864
- const unavailable = this.resolveVoiceFeatureUnavailableContext('dictation');
894
+ const unavailable = this.resolveVoiceFeatureUnavailableContext("dictation");
865
895
  if (unavailable) {
866
896
  this.emit({
867
- type: 'dictation_stream_error',
897
+ type: "dictation_stream_error",
868
898
  payload: {
869
899
  dictationId: msg.dictationId,
870
900
  error: unavailable.message,
@@ -878,7 +908,7 @@ export class Session {
878
908
  }
879
909
  await this.dictationStreamManager.handleStart(msg.dictationId, msg.format);
880
910
  break;
881
- case 'dictation_stream_chunk':
911
+ case "dictation_stream_chunk":
882
912
  await this.dictationStreamManager.handleChunk({
883
913
  dictationId: msg.dictationId,
884
914
  seq: msg.seq,
@@ -886,124 +916,127 @@ export class Session {
886
916
  format: msg.format,
887
917
  });
888
918
  break;
889
- case 'dictation_stream_finish':
919
+ case "dictation_stream_finish":
890
920
  await this.dictationStreamManager.handleFinish(msg.dictationId, msg.finalSeq);
891
921
  break;
892
- case 'dictation_stream_cancel':
922
+ case "dictation_stream_cancel":
893
923
  this.dictationStreamManager.handleCancel(msg.dictationId);
894
924
  break;
895
- case 'create_agent_request':
925
+ case "create_agent_request":
896
926
  await this.handleCreateAgentRequest(msg);
897
927
  break;
898
- case 'resume_agent_request':
928
+ case "resume_agent_request":
899
929
  await this.handleResumeAgentRequest(msg);
900
930
  break;
901
- case 'refresh_agent_request':
931
+ case "refresh_agent_request":
902
932
  await this.handleRefreshAgentRequest(msg);
903
933
  break;
904
- case 'cancel_agent_request':
934
+ case "cancel_agent_request":
905
935
  await this.handleCancelAgentRequest(msg.agentId);
906
936
  break;
907
- case 'restart_server_request':
937
+ case "restart_server_request":
908
938
  await this.handleRestartServerRequest(msg.requestId, msg.reason);
909
939
  break;
910
- case 'shutdown_server_request':
940
+ case "shutdown_server_request":
911
941
  await this.handleShutdownServerRequest(msg.requestId);
912
942
  break;
913
- case 'fetch_agent_timeline_request':
943
+ case "fetch_agent_timeline_request":
914
944
  await this.handleFetchAgentTimelineRequest(msg);
915
945
  break;
916
- case 'set_agent_mode_request':
946
+ case "set_agent_mode_request":
917
947
  await this.handleSetAgentModeRequest(msg.agentId, msg.modeId, msg.requestId);
918
948
  break;
919
- case 'set_agent_model_request':
949
+ case "set_agent_model_request":
920
950
  await this.handleSetAgentModelRequest(msg.agentId, msg.modelId, msg.requestId);
921
951
  break;
922
- case 'set_agent_thinking_request':
952
+ case "set_agent_thinking_request":
923
953
  await this.handleSetAgentThinkingRequest(msg.agentId, msg.thinkingOptionId, msg.requestId);
924
954
  break;
925
- case 'agent_permission_response':
955
+ case "agent_permission_response":
926
956
  await this.handleAgentPermissionResponse(msg.agentId, msg.requestId, msg.response);
927
957
  break;
928
- case 'checkout_status_request':
958
+ case "checkout_status_request":
929
959
  await this.handleCheckoutStatusRequest(msg);
930
960
  break;
931
- case 'validate_branch_request':
961
+ case "validate_branch_request":
932
962
  await this.handleValidateBranchRequest(msg);
933
963
  break;
934
- case 'branch_suggestions_request':
964
+ case "branch_suggestions_request":
935
965
  await this.handleBranchSuggestionsRequest(msg);
936
966
  break;
937
- case 'directory_suggestions_request':
967
+ case "directory_suggestions_request":
938
968
  await this.handleDirectorySuggestionsRequest(msg);
939
969
  break;
940
- case 'subscribe_checkout_diff_request':
970
+ case "subscribe_checkout_diff_request":
941
971
  await this.handleSubscribeCheckoutDiffRequest(msg);
942
972
  break;
943
- case 'unsubscribe_checkout_diff_request':
973
+ case "unsubscribe_checkout_diff_request":
944
974
  this.handleUnsubscribeCheckoutDiffRequest(msg);
945
975
  break;
946
- case 'checkout_commit_request':
976
+ case "checkout_commit_request":
947
977
  await this.handleCheckoutCommitRequest(msg);
948
978
  break;
949
- case 'checkout_merge_request':
979
+ case "checkout_merge_request":
950
980
  await this.handleCheckoutMergeRequest(msg);
951
981
  break;
952
- case 'checkout_merge_from_base_request':
982
+ case "checkout_merge_from_base_request":
953
983
  await this.handleCheckoutMergeFromBaseRequest(msg);
954
984
  break;
955
- case 'checkout_push_request':
985
+ case "checkout_push_request":
956
986
  await this.handleCheckoutPushRequest(msg);
957
987
  break;
958
- case 'checkout_pr_create_request':
988
+ case "checkout_pr_create_request":
959
989
  await this.handleCheckoutPrCreateRequest(msg);
960
990
  break;
961
- case 'checkout_pr_status_request':
991
+ case "checkout_pr_status_request":
962
992
  await this.handleCheckoutPrStatusRequest(msg);
963
993
  break;
964
- case 'paseo_worktree_list_request':
994
+ case "paseo_worktree_list_request":
965
995
  await this.handlePaseoWorktreeListRequest(msg);
966
996
  break;
967
- case 'paseo_worktree_archive_request':
997
+ case "paseo_worktree_archive_request":
968
998
  await this.handlePaseoWorktreeArchiveRequest(msg);
969
999
  break;
970
- case 'open_project_request':
1000
+ case "create_paseo_worktree_request":
1001
+ await this.handleCreatePaseoWorktreeRequest(msg);
1002
+ break;
1003
+ case "open_project_request":
971
1004
  await this.handleOpenProjectRequest(msg);
972
1005
  break;
973
- case 'archive_workspace_request':
1006
+ case "archive_workspace_request":
974
1007
  await this.handleArchiveWorkspaceRequest(msg);
975
1008
  break;
976
- case 'file_explorer_request':
1009
+ case "file_explorer_request":
977
1010
  await this.handleFileExplorerRequest(msg);
978
1011
  break;
979
- case 'project_icon_request':
1012
+ case "project_icon_request":
980
1013
  await this.handleProjectIconRequest(msg);
981
1014
  break;
982
- case 'file_download_token_request':
1015
+ case "file_download_token_request":
983
1016
  await this.handleFileDownloadTokenRequest(msg);
984
1017
  break;
985
- case 'list_provider_models_request':
1018
+ case "list_provider_models_request":
986
1019
  await this.handleListProviderModelsRequest(msg);
987
1020
  break;
988
- case 'list_available_providers_request':
1021
+ case "list_available_providers_request":
989
1022
  await this.handleListAvailableProvidersRequest(msg);
990
1023
  break;
991
- case 'speech_models_list_request':
1024
+ case "speech_models_list_request":
992
1025
  await this.handleSpeechModelsListRequest(msg);
993
1026
  break;
994
- case 'speech_models_download_request':
1027
+ case "speech_models_download_request":
995
1028
  await this.handleSpeechModelsDownloadRequest(msg);
996
1029
  break;
997
- case 'clear_agent_attention':
1030
+ case "clear_agent_attention":
998
1031
  await this.handleClearAgentAttention(msg.agentId);
999
1032
  break;
1000
- case 'client_heartbeat':
1033
+ case "client_heartbeat":
1001
1034
  this.handleClientHeartbeat(msg);
1002
1035
  break;
1003
- case 'ping': {
1036
+ case "ping": {
1004
1037
  const now = Date.now();
1005
1038
  this.emit({
1006
- type: 'pong',
1039
+ type: "pong",
1007
1040
  payload: {
1008
1041
  requestId: msg.requestId,
1009
1042
  clientSentAt: msg.clientSentAt,
@@ -1013,160 +1046,137 @@ export class Session {
1013
1046
  });
1014
1047
  break;
1015
1048
  }
1016
- case 'list_commands_request':
1049
+ case "list_commands_request":
1017
1050
  await this.handleListCommandsRequest(msg);
1018
1051
  break;
1019
- case 'register_push_token':
1052
+ case "register_push_token":
1020
1053
  this.handleRegisterPushToken(msg.token);
1021
1054
  break;
1022
- case 'subscribe_terminals_request':
1055
+ case "subscribe_terminals_request":
1023
1056
  this.handleSubscribeTerminalsRequest(msg);
1024
1057
  break;
1025
- case 'unsubscribe_terminals_request':
1058
+ case "unsubscribe_terminals_request":
1026
1059
  this.handleUnsubscribeTerminalsRequest(msg);
1027
1060
  break;
1028
- case 'list_terminals_request':
1061
+ case "list_terminals_request":
1029
1062
  await this.handleListTerminalsRequest(msg);
1030
1063
  break;
1031
- case 'create_terminal_request':
1064
+ case "create_terminal_request":
1032
1065
  await this.handleCreateTerminalRequest(msg);
1033
1066
  break;
1034
- case 'subscribe_terminal_request':
1067
+ case "subscribe_terminal_request":
1035
1068
  await this.handleSubscribeTerminalRequest(msg);
1036
1069
  break;
1037
- case 'unsubscribe_terminal_request':
1070
+ case "unsubscribe_terminal_request":
1038
1071
  this.handleUnsubscribeTerminalRequest(msg);
1039
1072
  break;
1040
- case 'terminal_input':
1073
+ case "terminal_input":
1041
1074
  this.handleTerminalInput(msg);
1042
1075
  break;
1043
- case 'kill_terminal_request':
1076
+ case "kill_terminal_request":
1044
1077
  await this.handleKillTerminalRequest(msg);
1045
1078
  break;
1046
- case 'attach_terminal_stream_request':
1047
- await this.handleAttachTerminalStreamRequest(msg);
1048
- break;
1049
- case 'detach_terminal_stream_request':
1050
- this.handleDetachTerminalStreamRequest(msg);
1051
- break;
1052
1079
  }
1053
1080
  }
1054
1081
  catch (error) {
1055
1082
  const err = error instanceof Error ? error : new Error(String(error));
1056
- this.sessionLogger.error({ err }, 'Error handling message');
1083
+ this.sessionLogger.error({ err }, "Error handling message");
1057
1084
  const requestId = msg.requestId;
1058
- if (typeof requestId === 'string') {
1085
+ if (typeof requestId === "string") {
1059
1086
  try {
1060
1087
  this.emit({
1061
- type: 'rpc_error',
1088
+ type: "rpc_error",
1062
1089
  payload: {
1063
1090
  requestId,
1064
1091
  requestType: msg.type,
1065
- error: 'Request failed',
1066
- code: 'handler_error',
1092
+ error: `Request failed: ${err.message}`,
1093
+ code: "handler_error",
1067
1094
  },
1068
1095
  });
1069
1096
  }
1070
1097
  catch (emitError) {
1071
- this.sessionLogger.error({ err: emitError }, 'Failed to emit rpc_error');
1098
+ this.sessionLogger.error({ err: emitError }, "Failed to emit rpc_error");
1072
1099
  }
1073
1100
  }
1074
1101
  this.emit({
1075
- type: 'activity_log',
1102
+ type: "activity_log",
1076
1103
  payload: {
1077
1104
  id: uuidv4(),
1078
1105
  timestamp: new Date(),
1079
- type: 'error',
1106
+ type: "error",
1080
1107
  content: `Error: ${err.message}`,
1081
1108
  },
1082
1109
  });
1083
1110
  }
1084
1111
  }
1085
1112
  handleBinaryFrame(frame) {
1086
- switch (frame.channel) {
1087
- case BinaryMuxChannel.Terminal:
1088
- this.handleTerminalBinaryFrame(frame);
1089
- break;
1090
- default:
1091
- this.sessionLogger.warn({ channel: frame.channel, messageType: frame.messageType }, 'Unhandled binary mux channel');
1092
- break;
1113
+ const activeStream = this.activeTerminalStreams.get(frame.slot);
1114
+ if (!activeStream || !this.terminalManager) {
1115
+ return;
1093
1116
  }
1094
- }
1095
- handleTerminalBinaryFrame(frame) {
1096
- if (frame.messageType === TerminalBinaryMessageType.InputUtf8) {
1097
- const binding = this.terminalStreams.get(frame.streamId);
1098
- if (!binding) {
1099
- this.sessionLogger.warn({ streamId: frame.streamId }, 'Terminal stream not found for input');
1100
- return;
1101
- }
1102
- if (!this.terminalManager) {
1103
- return;
1104
- }
1105
- const session = this.terminalManager.getTerminal(binding.terminalId);
1106
- if (!session) {
1107
- this.detachTerminalStream(frame.streamId, { emitExit: true });
1108
- return;
1109
- }
1110
- const payload = frame.payload ?? new Uint8Array(0);
1111
- if (payload.byteLength === 0) {
1112
- return;
1113
- }
1114
- const text = Buffer.from(payload).toString('utf8');
1115
- if (!text) {
1116
- return;
1117
- }
1118
- session.send({ type: 'input', data: text });
1117
+ const terminal = this.terminalManager.getTerminal(activeStream.terminalId);
1118
+ if (!terminal) {
1119
+ this.detachTerminalStream(activeStream.terminalId, { emitExit: true });
1119
1120
  return;
1120
1121
  }
1121
- if (frame.messageType === TerminalBinaryMessageType.Ack) {
1122
- const binding = this.terminalStreams.get(frame.streamId);
1123
- if (binding) {
1124
- if (!Number.isFinite(frame.offset) || frame.offset < 0) {
1122
+ switch (frame.opcode) {
1123
+ case TerminalStreamOpcode.Input: {
1124
+ if (frame.payload.byteLength === 0) {
1125
1125
  return;
1126
1126
  }
1127
- const nextAckOffset = Math.max(binding.lastAckOffset, Math.min(Math.floor(frame.offset), binding.lastOutputOffset));
1128
- if (nextAckOffset > binding.lastAckOffset) {
1129
- binding.lastAckOffset = nextAckOffset;
1130
- this.flushPendingTerminalStreamChunks(frame.streamId, binding);
1127
+ const text = Buffer.from(frame.payload).toString("utf8");
1128
+ if (!text) {
1129
+ return;
1131
1130
  }
1131
+ terminal.send({ type: "input", data: text });
1132
+ return;
1132
1133
  }
1133
- return;
1134
+ case TerminalStreamOpcode.Resize: {
1135
+ const resize = decodeTerminalResizePayload(frame.payload);
1136
+ if (!resize) {
1137
+ return;
1138
+ }
1139
+ activeStream.needsSnapshot = true;
1140
+ terminal.send({ type: "resize", rows: resize.rows, cols: resize.cols });
1141
+ return;
1142
+ }
1143
+ default:
1144
+ return;
1134
1145
  }
1135
- this.sessionLogger.warn({ streamId: frame.streamId, messageType: frame.messageType }, 'Unhandled terminal binary frame');
1136
1146
  }
1137
1147
  async handleRestartServerRequest(requestId, reason) {
1138
1148
  const payload = {
1139
- status: 'restart_requested',
1149
+ status: "restart_requested",
1140
1150
  clientId: this.clientId,
1141
1151
  };
1142
1152
  if (reason && reason.trim().length > 0) {
1143
1153
  payload.reason = reason;
1144
1154
  }
1145
1155
  payload.requestId = requestId;
1146
- this.sessionLogger.warn({ reason }, 'Restart requested via websocket');
1156
+ this.sessionLogger.warn({ reason }, "Restart requested via websocket");
1147
1157
  this.emit({
1148
- type: 'status',
1158
+ type: "status",
1149
1159
  payload,
1150
1160
  });
1151
1161
  this.emitLifecycleIntent({
1152
- type: 'restart',
1162
+ type: "restart",
1153
1163
  clientId: this.clientId,
1154
1164
  requestId,
1155
1165
  ...(reason ? { reason } : {}),
1156
1166
  });
1157
1167
  }
1158
1168
  async handleShutdownServerRequest(requestId) {
1159
- this.sessionLogger.warn('Shutdown requested via websocket');
1169
+ this.sessionLogger.warn("Shutdown requested via websocket");
1160
1170
  this.emit({
1161
- type: 'status',
1171
+ type: "status",
1162
1172
  payload: {
1163
- status: 'shutdown_requested',
1173
+ status: "shutdown_requested",
1164
1174
  clientId: this.clientId,
1165
1175
  requestId,
1166
1176
  },
1167
1177
  });
1168
1178
  this.emitLifecycleIntent({
1169
- type: 'shutdown',
1179
+ type: "shutdown",
1170
1180
  clientId: this.clientId,
1171
1181
  requestId,
1172
1182
  });
@@ -1179,7 +1189,7 @@ export class Session {
1179
1189
  this.onLifecycleIntent(intent);
1180
1190
  }
1181
1191
  catch (error) {
1182
- this.sessionLogger.error({ err: error, intent }, 'Lifecycle intent handler failed');
1192
+ this.sessionLogger.error({ err: error, intent }, "Lifecycle intent handler failed");
1183
1193
  }
1184
1194
  }
1185
1195
  async handleDeleteAgentRequest(agentId, requestId) {
@@ -1202,7 +1212,7 @@ export class Session {
1202
1212
  this.sessionLogger.error({ err: error, agentId }, `Failed to remove agent ${agentId} from registry`);
1203
1213
  }
1204
1214
  this.emit({
1205
- type: 'agent_deleted',
1215
+ type: "agent_deleted",
1206
1216
  payload: {
1207
1217
  agentId,
1208
1218
  requestId,
@@ -1210,7 +1220,7 @@ export class Session {
1210
1220
  });
1211
1221
  if (this.agentUpdatesSubscription) {
1212
1222
  this.bufferOrEmitAgentUpdate(this.agentUpdatesSubscription, {
1213
- kind: 'remove',
1223
+ kind: "remove",
1214
1224
  agentId,
1215
1225
  });
1216
1226
  }
@@ -1222,7 +1232,7 @@ export class Session {
1222
1232
  this.sessionLogger.info({ agentId }, `Archiving agent ${agentId}`);
1223
1233
  const { archivedAt } = await this.archiveAgentState(agentId);
1224
1234
  this.emit({
1225
- type: 'agent_archived',
1235
+ type: "agent_archived",
1226
1236
  payload: {
1227
1237
  agentId,
1228
1238
  archivedAt,
@@ -1251,8 +1261,8 @@ export class Session {
1251
1261
  throw new Error(`Agent not found in storage after snapshot: ${agentId}`);
1252
1262
  }
1253
1263
  }
1254
- const normalizedStatus = archivedRecord.lastStatus === 'running' || archivedRecord.lastStatus === 'initializing'
1255
- ? 'idle'
1264
+ const normalizedStatus = archivedRecord.lastStatus === "running" || archivedRecord.lastStatus === "initializing"
1265
+ ? "idle"
1256
1266
  : archivedRecord.lastStatus;
1257
1267
  const nextRecord = {
1258
1268
  ...archivedRecord,
@@ -1263,7 +1273,17 @@ export class Session {
1263
1273
  attentionTimestamp: null,
1264
1274
  };
1265
1275
  await this.agentStorage.upsert(nextRecord);
1266
- this.agentManager.notifyAgentState(agentId);
1276
+ // Unload the agent from memory — the storage record is the source of truth now.
1277
+ // This tears down the provider session and drops the hydrated timeline,
1278
+ // freeing memory. ensureAgentLoaded will re-initialize if needed later.
1279
+ if (this.agentManager.getAgent(agentId)) {
1280
+ try {
1281
+ await this.agentManager.closeAgent(agentId);
1282
+ }
1283
+ catch (error) {
1284
+ this.sessionLogger.warn({ err: error, agentId }, "Failed to close agent during archive");
1285
+ }
1286
+ }
1267
1287
  return { archivedAt, archivedRecord: nextRecord };
1268
1288
  }
1269
1289
  async unarchiveAgentState(agentId) {
@@ -1291,19 +1311,19 @@ export class Session {
1291
1311
  this.sessionLogger.info({
1292
1312
  agentId,
1293
1313
  requestId,
1294
- hasName: typeof name === 'string',
1314
+ hasName: typeof name === "string",
1295
1315
  labelCount: labels ? Object.keys(labels).length : 0,
1296
- }, 'session: update_agent_request');
1316
+ }, "session: update_agent_request");
1297
1317
  const normalizedName = name?.trim();
1298
1318
  const normalizedLabels = labels && Object.keys(labels).length > 0 ? labels : undefined;
1299
1319
  if (!normalizedName && !normalizedLabels) {
1300
1320
  this.emit({
1301
- type: 'update_agent_response',
1321
+ type: "update_agent_response",
1302
1322
  payload: {
1303
1323
  requestId,
1304
1324
  agentId,
1305
1325
  accepted: false,
1306
- error: 'Nothing to update (provide name and/or labels)',
1326
+ error: "Nothing to update (provide name and/or labels)",
1307
1327
  },
1308
1328
  });
1309
1329
  return;
@@ -1330,28 +1350,28 @@ export class Session {
1330
1350
  });
1331
1351
  }
1332
1352
  this.emit({
1333
- type: 'update_agent_response',
1353
+ type: "update_agent_response",
1334
1354
  payload: { requestId, agentId, accepted: true, error: null },
1335
1355
  });
1336
1356
  }
1337
1357
  catch (error) {
1338
- this.sessionLogger.error({ err: error, agentId, requestId }, 'session: update_agent_request error');
1358
+ this.sessionLogger.error({ err: error, agentId, requestId }, "session: update_agent_request error");
1339
1359
  this.emit({
1340
- type: 'activity_log',
1360
+ type: "activity_log",
1341
1361
  payload: {
1342
1362
  id: uuidv4(),
1343
1363
  timestamp: new Date(),
1344
- type: 'error',
1364
+ type: "error",
1345
1365
  content: `Failed to update agent: ${error.message}`,
1346
1366
  },
1347
1367
  });
1348
1368
  this.emit({
1349
- type: 'update_agent_response',
1369
+ type: "update_agent_response",
1350
1370
  payload: {
1351
1371
  requestId,
1352
1372
  agentId,
1353
1373
  accepted: false,
1354
- error: error?.message ? String(error.message) : 'Failed to update agent',
1374
+ error: error?.message ? String(error.message) : "Failed to update agent",
1355
1375
  },
1356
1376
  });
1357
1377
  }
@@ -1365,7 +1385,7 @@ export class Session {
1365
1385
  };
1366
1386
  }
1367
1387
  resolveModeReadinessState(readiness, mode) {
1368
- if (mode === 'voice_mode') {
1388
+ if (mode === "voice_mode") {
1369
1389
  return readiness.realtimeVoice;
1370
1390
  }
1371
1391
  return readiness.dictation;
@@ -1403,13 +1423,13 @@ export class Session {
1403
1423
  async handleSetVoiceMode(enabled, agentId, requestId) {
1404
1424
  const startedAt = Date.now();
1405
1425
  try {
1406
- this.sessionLogger.info({ enabled, requestedAgentId: agentId ?? null, requestId: requestId ?? null }, 'set_voice_mode started');
1426
+ this.sessionLogger.info({ enabled, requestedAgentId: agentId ?? null, requestId: requestId ?? null }, "set_voice_mode started");
1407
1427
  if (enabled) {
1408
- const unavailable = this.resolveVoiceFeatureUnavailableContext('voice_mode');
1428
+ const unavailable = this.resolveVoiceFeatureUnavailableContext("voice_mode");
1409
1429
  if (unavailable) {
1410
1430
  throw new VoiceFeatureUnavailableError(unavailable);
1411
1431
  }
1412
- const normalizedAgentId = this.parseVoiceTargetAgentId(agentId ?? '', 'set_voice_mode');
1432
+ const normalizedAgentId = this.parseVoiceTargetAgentId(agentId ?? "", "set_voice_mode");
1413
1433
  if (this.isVoiceMode &&
1414
1434
  this.voiceModeAgentId &&
1415
1435
  this.voiceModeAgentId !== normalizedAgentId) {
@@ -1417,26 +1437,26 @@ export class Session {
1417
1437
  previousAgentId: this.voiceModeAgentId,
1418
1438
  nextAgentId: normalizedAgentId,
1419
1439
  elapsedMs: Date.now() - startedAt,
1420
- }, 'set_voice_mode disabling previous active voice agent');
1440
+ }, "set_voice_mode disabling previous active voice agent");
1421
1441
  await this.disableVoiceModeForActiveAgent(true);
1422
1442
  }
1423
1443
  if (!this.isVoiceMode || this.voiceModeAgentId !== normalizedAgentId) {
1424
- this.sessionLogger.info({ agentId: normalizedAgentId, elapsedMs: Date.now() - startedAt }, 'set_voice_mode enabling voice for agent');
1444
+ this.sessionLogger.info({ agentId: normalizedAgentId, elapsedMs: Date.now() - startedAt }, "set_voice_mode enabling voice for agent");
1425
1445
  const refreshedAgentId = await this.enableVoiceModeForAgent(normalizedAgentId);
1426
1446
  this.voiceModeAgentId = refreshedAgentId;
1427
- this.sessionLogger.info({ agentId: refreshedAgentId, elapsedMs: Date.now() - startedAt }, 'set_voice_mode agent enable complete');
1447
+ this.sessionLogger.info({ agentId: refreshedAgentId, elapsedMs: Date.now() - startedAt }, "set_voice_mode agent enable complete");
1428
1448
  }
1429
- this.sessionLogger.info({ agentId: this.voiceModeAgentId, elapsedMs: Date.now() - startedAt }, 'set_voice_mode starting voice turn controller');
1449
+ this.sessionLogger.info({ agentId: this.voiceModeAgentId, elapsedMs: Date.now() - startedAt }, "set_voice_mode starting voice turn controller");
1430
1450
  await this.startVoiceTurnController();
1431
- this.sessionLogger.info({ agentId: this.voiceModeAgentId, elapsedMs: Date.now() - startedAt }, 'set_voice_mode voice turn controller started');
1451
+ this.sessionLogger.info({ agentId: this.voiceModeAgentId, elapsedMs: Date.now() - startedAt }, "set_voice_mode voice turn controller started");
1432
1452
  this.isVoiceMode = true;
1433
1453
  this.sessionLogger.info({
1434
1454
  agentId: this.voiceModeAgentId,
1435
1455
  elapsedMs: Date.now() - startedAt,
1436
- }, 'Voice mode enabled for existing agent');
1456
+ }, "Voice mode enabled for existing agent");
1437
1457
  if (requestId) {
1438
1458
  this.emit({
1439
- type: 'set_voice_mode_response',
1459
+ type: "set_voice_mode_response",
1440
1460
  payload: {
1441
1461
  requestId,
1442
1462
  enabled: true,
@@ -1448,13 +1468,13 @@ export class Session {
1448
1468
  }
1449
1469
  return;
1450
1470
  }
1451
- this.sessionLogger.info({ agentId: this.voiceModeAgentId, elapsedMs: Date.now() - startedAt }, 'set_voice_mode disabling active voice mode');
1471
+ this.sessionLogger.info({ agentId: this.voiceModeAgentId, elapsedMs: Date.now() - startedAt }, "set_voice_mode disabling active voice mode");
1452
1472
  await this.disableVoiceModeForActiveAgent(true);
1453
1473
  this.isVoiceMode = false;
1454
- this.sessionLogger.info({ elapsedMs: Date.now() - startedAt }, 'Voice mode disabled');
1474
+ this.sessionLogger.info({ elapsedMs: Date.now() - startedAt }, "Voice mode disabled");
1455
1475
  if (requestId) {
1456
1476
  this.emit({
1457
- type: 'set_voice_mode_response',
1477
+ type: "set_voice_mode_response",
1458
1478
  payload: {
1459
1479
  requestId,
1460
1480
  enabled: false,
@@ -1466,17 +1486,17 @@ export class Session {
1466
1486
  }
1467
1487
  }
1468
1488
  catch (error) {
1469
- const errorMessage = error instanceof Error ? error.message : 'Failed to set voice mode';
1489
+ const errorMessage = error instanceof Error ? error.message : "Failed to set voice mode";
1470
1490
  const unavailable = this.getVoiceFeatureUnavailableResponseMetadata(error);
1471
1491
  this.sessionLogger.error({
1472
1492
  err: error,
1473
1493
  enabled,
1474
1494
  requestedAgentId: agentId ?? null,
1475
1495
  elapsedMs: Date.now() - startedAt,
1476
- }, 'set_voice_mode failed');
1496
+ }, "set_voice_mode failed");
1477
1497
  if (requestId) {
1478
1498
  this.emit({
1479
- type: 'set_voice_mode_response',
1499
+ type: "set_voice_mode_response",
1480
1500
  payload: {
1481
1501
  requestId,
1482
1502
  enabled: this.isVoiceMode,
@@ -1507,7 +1527,7 @@ export class Session {
1507
1527
  buildVoiceModeMcpServers(existing, socketPath) {
1508
1528
  const mcpStdio = this.voiceAgentMcpStdio;
1509
1529
  if (!mcpStdio) {
1510
- throw new Error('Voice MCP stdio bridge is not configured');
1530
+ throw new Error("Voice MCP stdio bridge is not configured");
1511
1531
  }
1512
1532
  return {
1513
1533
  ...(existing ?? {}),
@@ -1523,14 +1543,14 @@ export class Session {
1523
1543
  const startedAt = Date.now();
1524
1544
  const ensureVoiceSocket = this.ensureVoiceMcpSocketForAgent;
1525
1545
  if (!ensureVoiceSocket) {
1526
- throw new Error('Voice MCP socket bridge is not configured');
1546
+ throw new Error("Voice MCP socket bridge is not configured");
1527
1547
  }
1528
- this.sessionLogger.info({ agentId }, 'enableVoiceModeForAgent.ensureAgentLoaded.start');
1548
+ this.sessionLogger.info({ agentId }, "enableVoiceModeForAgent.ensureAgentLoaded.start");
1529
1549
  const existing = await this.ensureAgentLoaded(agentId);
1530
- this.sessionLogger.info({ agentId, elapsedMs: Date.now() - startedAt }, 'enableVoiceModeForAgent.ensureAgentLoaded.done');
1531
- this.sessionLogger.info({ agentId }, 'enableVoiceModeForAgent.ensureVoiceSocket.start');
1550
+ this.sessionLogger.info({ agentId, elapsedMs: Date.now() - startedAt }, "enableVoiceModeForAgent.ensureAgentLoaded.done");
1551
+ this.sessionLogger.info({ agentId }, "enableVoiceModeForAgent.ensureVoiceSocket.start");
1532
1552
  const socketPath = await ensureVoiceSocket(agentId);
1533
- this.sessionLogger.info({ agentId, socketPath, elapsedMs: Date.now() - startedAt }, 'enableVoiceModeForAgent.ensureVoiceSocket.done');
1553
+ this.sessionLogger.info({ agentId, socketPath, elapsedMs: Date.now() - startedAt }, "enableVoiceModeForAgent.ensureVoiceSocket.done");
1534
1554
  this.registerVoiceBridgeForAgent(agentId);
1535
1555
  const baseConfig = {
1536
1556
  systemPrompt: stripVoiceModeSystemPrompt(existing.config.systemPrompt),
@@ -1542,9 +1562,9 @@ export class Session {
1542
1562
  mcpServers: this.buildVoiceModeMcpServers(baseConfig.mcpServers, socketPath),
1543
1563
  };
1544
1564
  try {
1545
- this.sessionLogger.info({ agentId, elapsedMs: Date.now() - startedAt }, 'enableVoiceModeForAgent.reloadAgentSession.start');
1565
+ this.sessionLogger.info({ agentId, elapsedMs: Date.now() - startedAt }, "enableVoiceModeForAgent.reloadAgentSession.start");
1546
1566
  const refreshed = await this.agentManager.reloadAgentSession(agentId, refreshOverrides);
1547
- this.sessionLogger.info({ agentId, refreshedAgentId: refreshed.id, elapsedMs: Date.now() - startedAt }, 'enableVoiceModeForAgent.reloadAgentSession.done');
1567
+ this.sessionLogger.info({ agentId, refreshedAgentId: refreshed.id, elapsedMs: Date.now() - startedAt }, "enableVoiceModeForAgent.reloadAgentSession.done");
1548
1568
  return refreshed.id;
1549
1569
  }
1550
1570
  catch (error) {
@@ -1565,7 +1585,7 @@ export class Session {
1565
1585
  this.unregisterVoiceSpeakHandler?.(agentId);
1566
1586
  this.unregisterVoiceCallerContext?.(agentId);
1567
1587
  await this.removeVoiceMcpSocketForAgent?.(agentId).catch((error) => {
1568
- this.sessionLogger.warn({ err: error, agentId }, 'Failed to remove voice MCP socket bridge on disable');
1588
+ this.sessionLogger.warn({ err: error, agentId }, "Failed to remove voice MCP socket bridge on disable");
1569
1589
  });
1570
1590
  if (restoreAgentConfig && this.voiceModeBaseConfig) {
1571
1591
  const baseConfig = this.voiceModeBaseConfig;
@@ -1576,7 +1596,7 @@ export class Session {
1576
1596
  });
1577
1597
  }
1578
1598
  catch (error) {
1579
- this.sessionLogger.warn({ err: error, agentId }, 'Failed to restore agent config while disabling voice mode');
1599
+ this.sessionLogger.warn({ err: error, agentId }, "Failed to restore agent config while disabling voice mode");
1580
1600
  }
1581
1601
  }
1582
1602
  this.voiceModeBaseConfig = null;
@@ -1587,16 +1607,16 @@ export class Session {
1587
1607
  }
1588
1608
  async startVoiceTurnController() {
1589
1609
  if (this.voiceTurnController) {
1590
- this.sessionLogger.info('startVoiceTurnController skipped: already running');
1610
+ this.sessionLogger.info("startVoiceTurnController skipped: already running");
1591
1611
  return;
1592
1612
  }
1593
1613
  const turnDetection = this.resolveVoiceTurnDetection();
1594
1614
  if (!turnDetection) {
1595
- throw new Error('Voice turn detection is not configured');
1615
+ throw new Error("Voice turn detection is not configured");
1596
1616
  }
1597
- this.sessionLogger.info({ providerId: turnDetection.id }, 'startVoiceTurnController creating controller');
1617
+ this.sessionLogger.info({ providerId: turnDetection.id }, "startVoiceTurnController creating controller");
1598
1618
  const controller = createVoiceTurnController({
1599
- logger: this.sessionLogger.child({ component: 'voice-turn-controller' }),
1619
+ logger: this.sessionLogger.child({ component: "voice-turn-controller" }),
1600
1620
  turnDetection,
1601
1621
  utteranceSink: {
1602
1622
  submitUtterance: async ({ pcm16, format, sampleRate, startedAt, endedAt }) => {
@@ -1606,7 +1626,7 @@ export class Session {
1606
1626
  startedAt,
1607
1627
  endedAt,
1608
1628
  durationMs: Math.max(0, endedAt - startedAt),
1609
- }, 'Submitting detected voice utterance');
1629
+ }, "Submitting detected voice utterance");
1610
1630
  await this.processCompletedAudio(pcm16, format);
1611
1631
  },
1612
1632
  },
@@ -1618,20 +1638,20 @@ export class Session {
1618
1638
  this.handleVoiceSpeechStopped();
1619
1639
  },
1620
1640
  onError: (error) => {
1621
- this.sessionLogger.error({ err: error }, 'Voice turn controller failed');
1641
+ this.sessionLogger.error({ err: error }, "Voice turn controller failed");
1622
1642
  },
1623
1643
  },
1624
1644
  });
1625
- this.sessionLogger.info('startVoiceTurnController connecting controller');
1645
+ this.sessionLogger.info("startVoiceTurnController connecting controller");
1626
1646
  await controller.start();
1627
1647
  this.voiceTurnController = controller;
1628
- this.sessionLogger.info('startVoiceTurnController connected');
1648
+ this.sessionLogger.info("startVoiceTurnController connected");
1629
1649
  }
1630
1650
  async stopVoiceTurnController() {
1631
1651
  if (!this.voiceTurnController) {
1632
1652
  return;
1633
1653
  }
1634
- this.clearPendingVoiceSpeechStart('turn-controller-stop');
1654
+ this.clearPendingVoiceSpeechStart("turn-controller-stop");
1635
1655
  const controller = this.voiceTurnController;
1636
1656
  this.voiceTurnController = null;
1637
1657
  await controller.stop();
@@ -1642,7 +1662,7 @@ export class Session {
1642
1662
  this.pendingVoiceSpeechTimer = null;
1643
1663
  }
1644
1664
  if (this.pendingVoiceSpeechStartAt !== null) {
1645
- this.sessionLogger.debug({ reason }, 'Clearing provisional voice speech start');
1665
+ this.sessionLogger.debug({ reason }, "Clearing provisional voice speech start");
1646
1666
  this.pendingVoiceSpeechStartAt = null;
1647
1667
  }
1648
1668
  }
@@ -1652,16 +1672,16 @@ export class Session {
1652
1672
  }
1653
1673
  const startedAt = Date.now();
1654
1674
  this.pendingVoiceSpeechStartAt = startedAt;
1655
- this.sessionLogger.info({ confirmationMs: VOICE_INTERRUPT_CONFIRMATION_MS }, 'Silero VAD provisional speech_started');
1675
+ this.sessionLogger.info({ confirmationMs: VOICE_INTERRUPT_CONFIRMATION_MS }, "Silero VAD provisional speech_started");
1656
1676
  this.pendingVoiceSpeechTimer = setTimeout(() => {
1657
1677
  this.pendingVoiceSpeechTimer = null;
1658
1678
  if (this.pendingVoiceSpeechStartAt !== startedAt || this.speechInProgress) {
1659
1679
  return;
1660
1680
  }
1661
1681
  this.pendingVoiceSpeechStartAt = null;
1662
- this.sessionLogger.info('voice_input_state emitting isSpeaking=true');
1682
+ this.sessionLogger.info("voice_input_state emitting isSpeaking=true");
1663
1683
  this.emit({
1664
- type: 'voice_input_state',
1684
+ type: "voice_input_state",
1665
1685
  payload: {
1666
1686
  isSpeaking: true,
1667
1687
  },
@@ -1672,13 +1692,13 @@ export class Session {
1672
1692
  handleVoiceSpeechStopped() {
1673
1693
  if (this.pendingVoiceSpeechStartAt !== null) {
1674
1694
  const durationMs = Date.now() - this.pendingVoiceSpeechStartAt;
1675
- this.clearPendingVoiceSpeechStart('speech-stopped-before-confirmation');
1676
- this.sessionLogger.info({ durationMs, confirmationMs: VOICE_INTERRUPT_CONFIRMATION_MS }, 'Ignoring provisional voice speech start that ended before confirmation');
1695
+ this.clearPendingVoiceSpeechStart("speech-stopped-before-confirmation");
1696
+ this.sessionLogger.info({ durationMs, confirmationMs: VOICE_INTERRUPT_CONFIRMATION_MS }, "Ignoring provisional voice speech start that ended before confirmation");
1677
1697
  return;
1678
1698
  }
1679
- this.sessionLogger.info('voice_input_state emitting isSpeaking=false');
1699
+ this.sessionLogger.info("voice_input_state emitting isSpeaking=false");
1680
1700
  this.emit({
1681
- type: 'voice_input_state',
1701
+ type: "voice_input_state",
1682
1702
  payload: {
1683
1703
  isSpeaking: false,
1684
1704
  },
@@ -1688,13 +1708,13 @@ export class Session {
1688
1708
  * Handle text message to agent (with optional image attachments)
1689
1709
  */
1690
1710
  async handleSendAgentMessage(agentId, text, messageId, images, runOptions) {
1691
- this.sessionLogger.info({ agentId, textPreview: text.substring(0, 50), imageCount: images?.length ?? 0 }, `Sending text to agent ${agentId}${images && images.length > 0 ? ` with ${images.length} image attachment(s)` : ''}`);
1711
+ this.sessionLogger.info({ agentId, textPreview: text.substring(0, 50), imageCount: images?.length ?? 0 }, `Sending text to agent ${agentId}${images && images.length > 0 ? ` with ${images.length} image attachment(s)` : ""}`);
1692
1712
  await this.unarchiveAgentState(agentId);
1693
1713
  try {
1694
1714
  await this.ensureAgentLoaded(agentId);
1695
1715
  }
1696
1716
  catch (error) {
1697
- this.handleAgentRunError(agentId, error, 'Failed to initialize agent before sending prompt');
1717
+ this.handleAgentRunError(agentId, error, "Failed to initialize agent before sending prompt");
1698
1718
  return;
1699
1719
  }
1700
1720
  try {
@@ -1714,7 +1734,7 @@ export class Session {
1714
1734
  */
1715
1735
  async handleCreateAgentRequest(msg) {
1716
1736
  const { config, worktreeName, requestId, initialPrompt, clientMessageId, outputSchema, git, images, labels, } = msg;
1717
- this.sessionLogger.info({ cwd: config.cwd, provider: config.provider, worktreeName }, `Creating agent in ${config.cwd} (${config.provider})${worktreeName ? ` with worktree ${worktreeName}` : ''}`);
1737
+ this.sessionLogger.info({ cwd: config.cwd, provider: config.provider, worktreeName }, `Creating agent in ${config.cwd} (${config.provider})${worktreeName ? ` with worktree ${worktreeName}` : ""}`);
1718
1738
  try {
1719
1739
  const trimmedPrompt = initialPrompt?.trim();
1720
1740
  const { explicitTitle, provisionalTitle } = resolveCreateAgentTitles({
@@ -1735,9 +1755,9 @@ export class Session {
1735
1755
  throw new Error(`Agent ${snapshot.id} not found after creation`);
1736
1756
  }
1737
1757
  this.emit({
1738
- type: 'status',
1758
+ type: "status",
1739
1759
  payload: {
1740
- status: 'agent_created',
1760
+ status: "agent_created",
1741
1761
  agentId: snapshot.id,
1742
1762
  requestId,
1743
1763
  agent: agentPayload,
@@ -1757,11 +1777,11 @@ export class Session {
1757
1777
  void this.handleSendAgentMessage(snapshot.id, trimmedPrompt, resolveClientMessageId(clientMessageId), images, outputSchema ? { outputSchema } : undefined).catch((promptError) => {
1758
1778
  this.sessionLogger.error({ err: promptError, agentId: snapshot.id }, `Failed to run initial prompt for agent ${snapshot.id}`);
1759
1779
  this.emit({
1760
- type: 'activity_log',
1780
+ type: "activity_log",
1761
1781
  payload: {
1762
1782
  id: uuidv4(),
1763
1783
  timestamp: new Date(),
1764
- type: 'error',
1784
+ type: "error",
1765
1785
  content: `Initial prompt failed: ${promptError?.message ?? promptError}`,
1766
1786
  },
1767
1787
  });
@@ -1788,23 +1808,23 @@ export class Session {
1788
1808
  this.sessionLogger.info({ agentId: snapshot.id, provider: snapshot.provider }, `Created agent ${snapshot.id} (${snapshot.provider})`);
1789
1809
  }
1790
1810
  catch (error) {
1791
- this.sessionLogger.error({ err: error }, 'Failed to create agent');
1811
+ this.sessionLogger.error({ err: error }, "Failed to create agent");
1792
1812
  if (requestId) {
1793
1813
  this.emit({
1794
- type: 'status',
1814
+ type: "status",
1795
1815
  payload: {
1796
- status: 'agent_create_failed',
1816
+ status: "agent_create_failed",
1797
1817
  requestId,
1798
1818
  error: error?.message ?? String(error),
1799
1819
  },
1800
1820
  });
1801
1821
  }
1802
1822
  this.emit({
1803
- type: 'activity_log',
1823
+ type: "activity_log",
1804
1824
  payload: {
1805
1825
  id: uuidv4(),
1806
1826
  timestamp: new Date(),
1807
- type: 'error',
1827
+ type: "error",
1808
1828
  content: `Failed to create agent: ${error.message}`,
1809
1829
  },
1810
1830
  });
@@ -1813,14 +1833,14 @@ export class Session {
1813
1833
  async handleResumeAgentRequest(msg) {
1814
1834
  const { handle, overrides, requestId } = msg;
1815
1835
  if (!handle) {
1816
- this.sessionLogger.warn('Resume request missing persistence handle');
1836
+ this.sessionLogger.warn("Resume request missing persistence handle");
1817
1837
  this.emit({
1818
- type: 'activity_log',
1838
+ type: "activity_log",
1819
1839
  payload: {
1820
1840
  id: uuidv4(),
1821
1841
  timestamp: new Date(),
1822
- type: 'error',
1823
- content: 'Unable to resume agent: missing persistence handle',
1842
+ type: "error",
1843
+ content: "Unable to resume agent: missing persistence handle",
1824
1844
  },
1825
1845
  });
1826
1846
  return;
@@ -1839,9 +1859,9 @@ export class Session {
1839
1859
  throw new Error(`Agent ${snapshot.id} not found after resume`);
1840
1860
  }
1841
1861
  this.emit({
1842
- type: 'status',
1862
+ type: "status",
1843
1863
  payload: {
1844
- status: 'agent_resumed',
1864
+ status: "agent_resumed",
1845
1865
  agentId: snapshot.id,
1846
1866
  requestId,
1847
1867
  timelineSize,
@@ -1851,13 +1871,13 @@ export class Session {
1851
1871
  }
1852
1872
  }
1853
1873
  catch (error) {
1854
- this.sessionLogger.error({ err: error }, 'Failed to resume agent');
1874
+ this.sessionLogger.error({ err: error }, "Failed to resume agent");
1855
1875
  this.emit({
1856
- type: 'activity_log',
1876
+ type: "activity_log",
1857
1877
  payload: {
1858
1878
  id: uuidv4(),
1859
1879
  timestamp: new Date(),
1860
- type: 'error',
1880
+ type: "error",
1861
1881
  content: `Failed to resume agent: ${error.message}`,
1862
1882
  },
1863
1883
  });
@@ -1895,9 +1915,9 @@ export class Session {
1895
1915
  const timelineSize = this.agentManager.getTimeline(agentId).length;
1896
1916
  if (requestId) {
1897
1917
  this.emit({
1898
- type: 'status',
1918
+ type: "status",
1899
1919
  payload: {
1900
- status: 'agent_refreshed',
1920
+ status: "agent_refreshed",
1901
1921
  agentId,
1902
1922
  requestId,
1903
1923
  timelineSize,
@@ -1908,11 +1928,11 @@ export class Session {
1908
1928
  catch (error) {
1909
1929
  this.sessionLogger.error({ err: error, agentId }, `Failed to refresh agent ${agentId}`);
1910
1930
  this.emit({
1911
- type: 'activity_log',
1931
+ type: "activity_log",
1912
1932
  payload: {
1913
1933
  id: uuidv4(),
1914
1934
  timestamp: new Date(),
1915
- type: 'error',
1935
+ type: "error",
1916
1936
  content: `Failed to refresh agent: ${error.message}`,
1917
1937
  },
1918
1938
  });
@@ -1924,7 +1944,7 @@ export class Session {
1924
1944
  await this.interruptAgentIfRunning(agentId);
1925
1945
  }
1926
1946
  catch (error) {
1927
- this.handleAgentRunError(agentId, error, 'Failed to cancel running agent on request');
1947
+ this.handleAgentRunError(agentId, error, "Failed to cancel running agent on request");
1928
1948
  }
1929
1949
  }
1930
1950
  async buildAgentSessionConfig(config, gitOptions, legacyWorktreeName, _labels) {
@@ -1946,20 +1966,21 @@ export class Session {
1946
1966
  }
1947
1967
  else {
1948
1968
  // Resolve current branch name from HEAD
1949
- const { stdout } = await execAsync('git rev-parse --abbrev-ref HEAD', {
1969
+ const { stdout } = await execAsync("git rev-parse --abbrev-ref HEAD", {
1950
1970
  cwd,
1951
1971
  env: READ_ONLY_GIT_ENV,
1952
1972
  });
1953
1973
  targetBranch = stdout.trim();
1954
1974
  }
1955
1975
  if (!targetBranch) {
1956
- throw new Error('A branch name is required when creating a worktree.');
1976
+ throw new Error("A branch name is required when creating a worktree.");
1957
1977
  }
1958
1978
  this.sessionLogger.info({ worktreeSlug: normalized.worktreeSlug ?? targetBranch, branch: targetBranch }, `Creating worktree '${normalized.worktreeSlug ?? targetBranch}' for branch ${targetBranch}`);
1979
+ const baseBranch = normalized.baseBranch ?? (await this.resolveGitCreateBaseBranch(cwd));
1959
1980
  const createdWorktree = await createAgentWorktree({
1960
1981
  branchName: targetBranch,
1961
1982
  cwd,
1962
- baseBranch: normalized.baseBranch,
1983
+ baseBranch,
1963
1984
  worktreeSlug: normalized.worktreeSlug ?? targetBranch,
1964
1985
  paseoHome: this.paseoHome,
1965
1986
  });
@@ -1967,9 +1988,10 @@ export class Session {
1967
1988
  worktreeConfig = createdWorktree;
1968
1989
  }
1969
1990
  else if (normalized.createNewBranch) {
1991
+ const baseBranch = normalized.baseBranch ?? (await this.resolveGitCreateBaseBranch(cwd));
1970
1992
  await this.createBranchFromBase({
1971
1993
  cwd,
1972
- baseBranch: normalized.baseBranch,
1994
+ baseBranch,
1973
1995
  newBranchName: normalized.newBranchName,
1974
1996
  });
1975
1997
  }
@@ -1991,7 +2013,7 @@ export class Session {
1991
2013
  cwd: msg.cwd ? expandTilde(msg.cwd) : undefined,
1992
2014
  });
1993
2015
  this.emit({
1994
- type: 'list_provider_models_response',
2016
+ type: "list_provider_models_response",
1995
2017
  payload: {
1996
2018
  provider: msg.provider,
1997
2019
  models,
@@ -2004,7 +2026,7 @@ export class Session {
2004
2026
  catch (error) {
2005
2027
  this.sessionLogger.error({ err: error, provider: msg.provider }, `Failed to list models for ${msg.provider}`);
2006
2028
  this.emit({
2007
- type: 'list_provider_models_response',
2029
+ type: "list_provider_models_response",
2008
2030
  payload: {
2009
2031
  provider: msg.provider,
2010
2032
  error: error?.message ?? String(error),
@@ -2019,7 +2041,7 @@ export class Session {
2019
2041
  try {
2020
2042
  const providers = await this.agentManager.listProviderAvailability();
2021
2043
  this.emit({
2022
- type: 'list_available_providers_response',
2044
+ type: "list_available_providers_response",
2023
2045
  payload: {
2024
2046
  providers,
2025
2047
  error: null,
@@ -2029,9 +2051,9 @@ export class Session {
2029
2051
  });
2030
2052
  }
2031
2053
  catch (error) {
2032
- this.sessionLogger.error({ err: error }, 'Failed to list provider availability');
2054
+ this.sessionLogger.error({ err: error }, "Failed to list provider availability");
2033
2055
  this.emit({
2034
- type: 'list_available_providers_response',
2056
+ type: "list_available_providers_response",
2035
2057
  payload: {
2036
2058
  providers: [],
2037
2059
  error: error?.message ?? String(error),
@@ -2071,7 +2093,7 @@ export class Session {
2071
2093
  };
2072
2094
  }));
2073
2095
  this.emit({
2074
- type: 'speech_models_list_response',
2096
+ type: "speech_models_list_response",
2075
2097
  payload: {
2076
2098
  modelsDir,
2077
2099
  models,
@@ -2086,11 +2108,11 @@ export class Session {
2086
2108
  const invalid = modelIdsRaw.filter((id) => !allModelIds.has(id));
2087
2109
  if (invalid.length > 0) {
2088
2110
  this.emit({
2089
- type: 'speech_models_download_response',
2111
+ type: "speech_models_download_response",
2090
2112
  payload: {
2091
2113
  modelsDir,
2092
2114
  downloadedModelIds: [],
2093
- error: `Unknown speech model id(s): ${invalid.join(', ')}`,
2115
+ error: `Unknown speech model id(s): ${invalid.join(", ")}`,
2094
2116
  requestId: msg.requestId,
2095
2117
  },
2096
2118
  });
@@ -2104,7 +2126,7 @@ export class Session {
2104
2126
  logger: this.sessionLogger,
2105
2127
  });
2106
2128
  this.emit({
2107
- type: 'speech_models_download_response',
2129
+ type: "speech_models_download_response",
2108
2130
  payload: {
2109
2131
  modelsDir,
2110
2132
  downloadedModelIds: modelIds,
@@ -2114,9 +2136,9 @@ export class Session {
2114
2136
  });
2115
2137
  }
2116
2138
  catch (error) {
2117
- this.sessionLogger.error({ err: error, modelIds }, 'Failed to download speech models');
2139
+ this.sessionLogger.error({ err: error, modelIds }, "Failed to download speech models");
2118
2140
  this.emit({
2119
- type: 'speech_models_download_response',
2141
+ type: "speech_models_download_response",
2120
2142
  payload: {
2121
2143
  modelsDir,
2122
2144
  downloadedModelIds: [],
@@ -2150,17 +2172,11 @@ export class Session {
2150
2172
  return null;
2151
2173
  }
2152
2174
  if (baseBranch) {
2153
- this.assertSafeGitRef(baseBranch, 'base branch');
2154
- }
2155
- if (createWorktree && !baseBranch) {
2156
- throw new Error('Base branch is required when creating a worktree');
2157
- }
2158
- if (createNewBranch && !baseBranch) {
2159
- throw new Error('Base branch is required when creating a new branch');
2175
+ this.assertSafeGitRef(baseBranch, "base branch");
2160
2176
  }
2161
2177
  if (createNewBranch) {
2162
2178
  if (!normalizedBranchName) {
2163
- throw new Error('New branch name is required');
2179
+ throw new Error("New branch name is required");
2164
2180
  }
2165
2181
  const validation = validateBranchSlug(normalizedBranchName);
2166
2182
  if (!validation.valid) {
@@ -2182,24 +2198,36 @@ export class Session {
2182
2198
  };
2183
2199
  }
2184
2200
  assertSafeGitRef(ref, label) {
2185
- if (!SAFE_GIT_REF_PATTERN.test(ref) || ref.includes('..') || ref.includes('@{')) {
2201
+ if (!SAFE_GIT_REF_PATTERN.test(ref) || ref.includes("..") || ref.includes("@{")) {
2186
2202
  throw new Error(`Invalid ${label}: ${ref}`);
2187
2203
  }
2188
2204
  }
2205
+ async resolveGitCreateBaseBranch(cwd) {
2206
+ const checkout = await getCheckoutStatusLite(cwd, { paseoHome: this.paseoHome });
2207
+ if (!checkout.isGit) {
2208
+ throw new Error("Cannot create a worktree outside a git repository");
2209
+ }
2210
+ const repoRoot = checkout.isPaseoOwnedWorktree ? checkout.mainRepoRoot : cwd;
2211
+ const baseBranch = await resolveRepositoryDefaultBranch(repoRoot);
2212
+ if (!baseBranch) {
2213
+ throw new Error("Unable to resolve repository default branch");
2214
+ }
2215
+ return baseBranch;
2216
+ }
2189
2217
  toCheckoutError(error) {
2190
2218
  if (error instanceof NotGitRepoError) {
2191
- return { code: 'NOT_GIT_REPO', message: error.message };
2219
+ return { code: "NOT_GIT_REPO", message: error.message };
2192
2220
  }
2193
2221
  if (error instanceof MergeConflictError) {
2194
- return { code: 'MERGE_CONFLICT', message: error.message };
2222
+ return { code: "MERGE_CONFLICT", message: error.message };
2195
2223
  }
2196
2224
  if (error instanceof MergeFromBaseConflictError) {
2197
- return { code: 'MERGE_CONFLICT', message: error.message };
2225
+ return { code: "MERGE_CONFLICT", message: error.message };
2198
2226
  }
2199
2227
  if (error instanceof Error) {
2200
- return { code: 'UNKNOWN', message: error.message };
2228
+ return { code: "UNKNOWN", message: error.message };
2201
2229
  }
2202
- return { code: 'UNKNOWN', message: String(error) };
2230
+ return { code: "UNKNOWN", message: String(error) };
2203
2231
  }
2204
2232
  isPathWithinRoot(rootPath, candidatePath) {
2205
2233
  const resolvedRoot = resolve(rootPath);
@@ -2210,47 +2238,47 @@ export class Session {
2210
2238
  return resolvedCandidate.startsWith(resolvedRoot + sep);
2211
2239
  }
2212
2240
  async generateCommitMessage(cwd) {
2213
- const diff = await getCheckoutDiff(cwd, { mode: 'uncommitted', includeStructured: true }, { paseoHome: this.paseoHome });
2241
+ const diff = await getCheckoutDiff(cwd, { mode: "uncommitted", includeStructured: true }, { paseoHome: this.paseoHome });
2214
2242
  const schema = z.object({
2215
2243
  message: z
2216
2244
  .string()
2217
2245
  .min(1)
2218
2246
  .max(72)
2219
- .describe('Concise git commit message, imperative mood, no trailing period.'),
2247
+ .describe("Concise git commit message, imperative mood, no trailing period."),
2220
2248
  });
2221
2249
  const fileList = diff.structured && diff.structured.length > 0
2222
2250
  ? [
2223
- 'Files changed:',
2251
+ "Files changed:",
2224
2252
  ...diff.structured.map((file) => {
2225
- const changeType = file.isNew ? 'A' : file.isDeleted ? 'D' : 'M';
2226
- const status = file.status && file.status !== 'ok' ? ` [${file.status}]` : '';
2253
+ const changeType = file.isNew ? "A" : file.isDeleted ? "D" : "M";
2254
+ const status = file.status && file.status !== "ok" ? ` [${file.status}]` : "";
2227
2255
  return `${changeType}\t${file.path}\t(+${file.additions} -${file.deletions})${status}`;
2228
2256
  }),
2229
- ].join('\n')
2230
- : 'Files changed: (unknown)';
2257
+ ].join("\n")
2258
+ : "Files changed: (unknown)";
2231
2259
  const maxPatchChars = 120000;
2232
2260
  const patch = diff.diff.length > maxPatchChars
2233
2261
  ? `${diff.diff.slice(0, maxPatchChars)}\n\n... (diff truncated to ${maxPatchChars} chars)\n`
2234
2262
  : diff.diff;
2235
2263
  const prompt = [
2236
- 'Write a concise git commit message for the changes below.',
2264
+ "Write a concise git commit message for the changes below.",
2237
2265
  "Return JSON only with a single field 'message'.",
2238
- '',
2266
+ "",
2239
2267
  fileList,
2240
- '',
2241
- patch.length > 0 ? patch : '(No diff available)',
2242
- ].join('\n');
2268
+ "",
2269
+ patch.length > 0 ? patch : "(No diff available)",
2270
+ ].join("\n");
2243
2271
  try {
2244
2272
  const result = await generateStructuredAgentResponseWithFallback({
2245
2273
  manager: this.agentManager,
2246
2274
  cwd,
2247
2275
  prompt,
2248
2276
  schema,
2249
- schemaName: 'CommitMessage',
2277
+ schemaName: "CommitMessage",
2250
2278
  maxRetries: 2,
2251
2279
  providers: DEFAULT_STRUCTURED_GENERATION_PROVIDERS,
2252
2280
  agentConfigOverrides: {
2253
- title: 'Commit generator',
2281
+ title: "Commit generator",
2254
2282
  internal: true,
2255
2283
  },
2256
2284
  });
@@ -2259,14 +2287,14 @@ export class Session {
2259
2287
  catch (error) {
2260
2288
  if (error instanceof StructuredAgentResponseError ||
2261
2289
  error instanceof StructuredAgentFallbackError) {
2262
- return 'Update files';
2290
+ return "Update files";
2263
2291
  }
2264
2292
  throw error;
2265
2293
  }
2266
2294
  }
2267
2295
  async generatePullRequestText(cwd, baseRef) {
2268
2296
  const diff = await getCheckoutDiff(cwd, {
2269
- mode: 'base',
2297
+ mode: "base",
2270
2298
  baseRef,
2271
2299
  includeStructured: true,
2272
2300
  }, { paseoHome: this.paseoHome });
@@ -2276,37 +2304,37 @@ export class Session {
2276
2304
  });
2277
2305
  const fileList = diff.structured && diff.structured.length > 0
2278
2306
  ? [
2279
- 'Files changed:',
2307
+ "Files changed:",
2280
2308
  ...diff.structured.map((file) => {
2281
- const changeType = file.isNew ? 'A' : file.isDeleted ? 'D' : 'M';
2282
- const status = file.status && file.status !== 'ok' ? ` [${file.status}]` : '';
2309
+ const changeType = file.isNew ? "A" : file.isDeleted ? "D" : "M";
2310
+ const status = file.status && file.status !== "ok" ? ` [${file.status}]` : "";
2283
2311
  return `${changeType}\t${file.path}\t(+${file.additions} -${file.deletions})${status}`;
2284
2312
  }),
2285
- ].join('\n')
2286
- : 'Files changed: (unknown)';
2313
+ ].join("\n")
2314
+ : "Files changed: (unknown)";
2287
2315
  const maxPatchChars = 200000;
2288
2316
  const patch = diff.diff.length > maxPatchChars
2289
2317
  ? `${diff.diff.slice(0, maxPatchChars)}\n\n... (diff truncated to ${maxPatchChars} chars)\n`
2290
2318
  : diff.diff;
2291
2319
  const prompt = [
2292
- 'Write a pull request title and body for the changes below.',
2320
+ "Write a pull request title and body for the changes below.",
2293
2321
  "Return JSON only with fields 'title' and 'body'.",
2294
- '',
2322
+ "",
2295
2323
  fileList,
2296
- '',
2297
- patch.length > 0 ? patch : '(No diff available)',
2298
- ].join('\n');
2324
+ "",
2325
+ patch.length > 0 ? patch : "(No diff available)",
2326
+ ].join("\n");
2299
2327
  try {
2300
2328
  return await generateStructuredAgentResponseWithFallback({
2301
2329
  manager: this.agentManager,
2302
2330
  cwd,
2303
2331
  prompt,
2304
2332
  schema,
2305
- schemaName: 'PullRequest',
2333
+ schemaName: "PullRequest",
2306
2334
  maxRetries: 2,
2307
2335
  providers: DEFAULT_STRUCTURED_GENERATION_PROVIDERS,
2308
2336
  agentConfigOverrides: {
2309
- title: 'PR generator',
2337
+ title: "PR generator",
2310
2338
  internal: true,
2311
2339
  },
2312
2340
  });
@@ -2315,8 +2343,8 @@ export class Session {
2315
2343
  if (error instanceof StructuredAgentResponseError ||
2316
2344
  error instanceof StructuredAgentFallbackError) {
2317
2345
  return {
2318
- title: 'Update changes',
2319
- body: 'Automated PR generated by Paseo.',
2346
+ title: "Update changes",
2347
+ body: "Automated PR generated by Paseo.",
2320
2348
  };
2321
2349
  }
2322
2350
  throw error;
@@ -2325,12 +2353,12 @@ export class Session {
2325
2353
  async ensureCleanWorkingTree(cwd) {
2326
2354
  const dirty = await this.isWorkingTreeDirty(cwd);
2327
2355
  if (dirty) {
2328
- throw new Error('Working directory has uncommitted changes. Commit or stash before switching branches.');
2356
+ throw new Error("Working directory has uncommitted changes. Commit or stash before switching branches.");
2329
2357
  }
2330
2358
  }
2331
2359
  async isWorkingTreeDirty(cwd) {
2332
2360
  try {
2333
- const { stdout } = await execAsync('git status --porcelain', {
2361
+ const { stdout } = await execAsync("git status --porcelain", {
2334
2362
  cwd,
2335
2363
  env: READ_ONLY_GIT_ENV,
2336
2364
  });
@@ -2341,14 +2369,14 @@ export class Session {
2341
2369
  }
2342
2370
  }
2343
2371
  async checkoutExistingBranch(cwd, branch) {
2344
- this.assertSafeGitRef(branch, 'branch');
2372
+ this.assertSafeGitRef(branch, "branch");
2345
2373
  try {
2346
2374
  await execAsync(`git rev-parse --verify ${branch}`, { cwd });
2347
2375
  }
2348
2376
  catch (error) {
2349
2377
  throw new Error(`Branch not found: ${branch}`);
2350
2378
  }
2351
- const { stdout } = await execAsync('git rev-parse --abbrev-ref HEAD', {
2379
+ const { stdout } = await execAsync("git rev-parse --abbrev-ref HEAD", {
2352
2380
  cwd,
2353
2381
  });
2354
2382
  const current = stdout.trim();
@@ -2360,7 +2388,7 @@ export class Session {
2360
2388
  }
2361
2389
  async createBranchFromBase(params) {
2362
2390
  const { cwd, baseBranch, newBranchName } = params;
2363
- this.assertSafeGitRef(baseBranch, 'base branch');
2391
+ this.assertSafeGitRef(baseBranch, "base branch");
2364
2392
  try {
2365
2393
  await execAsync(`git rev-parse --verify ${baseBranch}`, { cwd });
2366
2394
  }
@@ -2391,97 +2419,97 @@ export class Session {
2391
2419
  * Handle set agent mode request
2392
2420
  */
2393
2421
  async handleSetAgentModeRequest(agentId, modeId, requestId) {
2394
- this.sessionLogger.info({ agentId, modeId, requestId }, 'session: set_agent_mode_request');
2422
+ this.sessionLogger.info({ agentId, modeId, requestId }, "session: set_agent_mode_request");
2395
2423
  try {
2396
2424
  await this.agentManager.setAgentMode(agentId, modeId);
2397
- this.sessionLogger.info({ agentId, modeId, requestId }, 'session: set_agent_mode_request success');
2425
+ this.sessionLogger.info({ agentId, modeId, requestId }, "session: set_agent_mode_request success");
2398
2426
  this.emit({
2399
- type: 'set_agent_mode_response',
2427
+ type: "set_agent_mode_response",
2400
2428
  payload: { requestId, agentId, accepted: true, error: null },
2401
2429
  });
2402
2430
  }
2403
2431
  catch (error) {
2404
- this.sessionLogger.error({ err: error, agentId, modeId, requestId }, 'session: set_agent_mode_request error');
2432
+ this.sessionLogger.error({ err: error, agentId, modeId, requestId }, "session: set_agent_mode_request error");
2405
2433
  this.emit({
2406
- type: 'activity_log',
2434
+ type: "activity_log",
2407
2435
  payload: {
2408
2436
  id: uuidv4(),
2409
2437
  timestamp: new Date(),
2410
- type: 'error',
2438
+ type: "error",
2411
2439
  content: `Failed to set agent mode: ${error.message}`,
2412
2440
  },
2413
2441
  });
2414
2442
  this.emit({
2415
- type: 'set_agent_mode_response',
2443
+ type: "set_agent_mode_response",
2416
2444
  payload: {
2417
2445
  requestId,
2418
2446
  agentId,
2419
2447
  accepted: false,
2420
- error: error?.message ? String(error.message) : 'Failed to set agent mode',
2448
+ error: error?.message ? String(error.message) : "Failed to set agent mode",
2421
2449
  },
2422
2450
  });
2423
2451
  }
2424
2452
  }
2425
2453
  async handleSetAgentModelRequest(agentId, modelId, requestId) {
2426
- this.sessionLogger.info({ agentId, modelId, requestId }, 'session: set_agent_model_request');
2454
+ this.sessionLogger.info({ agentId, modelId, requestId }, "session: set_agent_model_request");
2427
2455
  try {
2428
2456
  await this.agentManager.setAgentModel(agentId, modelId);
2429
- this.sessionLogger.info({ agentId, modelId, requestId }, 'session: set_agent_model_request success');
2457
+ this.sessionLogger.info({ agentId, modelId, requestId }, "session: set_agent_model_request success");
2430
2458
  this.emit({
2431
- type: 'set_agent_model_response',
2459
+ type: "set_agent_model_response",
2432
2460
  payload: { requestId, agentId, accepted: true, error: null },
2433
2461
  });
2434
2462
  }
2435
2463
  catch (error) {
2436
- this.sessionLogger.error({ err: error, agentId, modelId, requestId }, 'session: set_agent_model_request error');
2464
+ this.sessionLogger.error({ err: error, agentId, modelId, requestId }, "session: set_agent_model_request error");
2437
2465
  this.emit({
2438
- type: 'activity_log',
2466
+ type: "activity_log",
2439
2467
  payload: {
2440
2468
  id: uuidv4(),
2441
2469
  timestamp: new Date(),
2442
- type: 'error',
2470
+ type: "error",
2443
2471
  content: `Failed to set agent model: ${error.message}`,
2444
2472
  },
2445
2473
  });
2446
2474
  this.emit({
2447
- type: 'set_agent_model_response',
2475
+ type: "set_agent_model_response",
2448
2476
  payload: {
2449
2477
  requestId,
2450
2478
  agentId,
2451
2479
  accepted: false,
2452
- error: error?.message ? String(error.message) : 'Failed to set agent model',
2480
+ error: error?.message ? String(error.message) : "Failed to set agent model",
2453
2481
  },
2454
2482
  });
2455
2483
  }
2456
2484
  }
2457
2485
  async handleSetAgentThinkingRequest(agentId, thinkingOptionId, requestId) {
2458
- this.sessionLogger.info({ agentId, thinkingOptionId, requestId }, 'session: set_agent_thinking_request');
2486
+ this.sessionLogger.info({ agentId, thinkingOptionId, requestId }, "session: set_agent_thinking_request");
2459
2487
  try {
2460
2488
  await this.agentManager.setAgentThinkingOption(agentId, thinkingOptionId);
2461
- this.sessionLogger.info({ agentId, thinkingOptionId, requestId }, 'session: set_agent_thinking_request success');
2489
+ this.sessionLogger.info({ agentId, thinkingOptionId, requestId }, "session: set_agent_thinking_request success");
2462
2490
  this.emit({
2463
- type: 'set_agent_thinking_response',
2491
+ type: "set_agent_thinking_response",
2464
2492
  payload: { requestId, agentId, accepted: true, error: null },
2465
2493
  });
2466
2494
  }
2467
2495
  catch (error) {
2468
- this.sessionLogger.error({ err: error, agentId, thinkingOptionId, requestId }, 'session: set_agent_thinking_request error');
2496
+ this.sessionLogger.error({ err: error, agentId, thinkingOptionId, requestId }, "session: set_agent_thinking_request error");
2469
2497
  this.emit({
2470
- type: 'activity_log',
2498
+ type: "activity_log",
2471
2499
  payload: {
2472
2500
  id: uuidv4(),
2473
2501
  timestamp: new Date(),
2474
- type: 'error',
2502
+ type: "error",
2475
2503
  content: `Failed to set agent thinking option: ${error.message}`,
2476
2504
  },
2477
2505
  });
2478
2506
  this.emit({
2479
- type: 'set_agent_thinking_response',
2507
+ type: "set_agent_thinking_response",
2480
2508
  payload: {
2481
2509
  requestId,
2482
2510
  agentId,
2483
2511
  accepted: false,
2484
- error: error?.message ? String(error.message) : 'Failed to set agent thinking option',
2512
+ error: error?.message ? String(error.message) : "Failed to set agent thinking option",
2485
2513
  },
2486
2514
  });
2487
2515
  }
@@ -2495,7 +2523,7 @@ export class Session {
2495
2523
  await Promise.all(agentIds.map((id) => this.agentManager.clearAgentAttention(id)));
2496
2524
  }
2497
2525
  catch (error) {
2498
- this.sessionLogger.error({ err: error, agentIds }, 'Failed to clear agent attention');
2526
+ this.sessionLogger.error({ err: error, agentIds }, "Failed to clear agent attention");
2499
2527
  // Don't throw - this is not critical
2500
2528
  }
2501
2529
  }
@@ -2519,7 +2547,7 @@ export class Session {
2519
2547
  */
2520
2548
  handleRegisterPushToken(token) {
2521
2549
  this.pushTokenStore.addToken(token);
2522
- this.sessionLogger.info('Registered push token');
2550
+ this.sessionLogger.info("Registered push token");
2523
2551
  }
2524
2552
  /**
2525
2553
  * Handle list commands request for an agent
@@ -2533,7 +2561,7 @@ export class Session {
2533
2561
  if (agent?.session?.listCommands) {
2534
2562
  const commands = await agent.session.listCommands();
2535
2563
  this.emit({
2536
- type: 'list_commands_response',
2564
+ type: "list_commands_response",
2537
2565
  payload: {
2538
2566
  agentId,
2539
2567
  commands,
@@ -2555,7 +2583,7 @@ export class Session {
2555
2583
  };
2556
2584
  const commands = await this.agentManager.listDraftCommands(sessionConfig);
2557
2585
  this.emit({
2558
- type: 'list_commands_response',
2586
+ type: "list_commands_response",
2559
2587
  payload: {
2560
2588
  agentId,
2561
2589
  commands,
@@ -2566,7 +2594,7 @@ export class Session {
2566
2594
  return;
2567
2595
  }
2568
2596
  this.emit({
2569
- type: 'list_commands_response',
2597
+ type: "list_commands_response",
2570
2598
  payload: {
2571
2599
  agentId,
2572
2600
  commands: [],
@@ -2576,9 +2604,9 @@ export class Session {
2576
2604
  });
2577
2605
  }
2578
2606
  catch (error) {
2579
- this.sessionLogger.error({ err: error, agentId, draftConfig }, 'Failed to list commands');
2607
+ this.sessionLogger.error({ err: error, agentId, draftConfig }, "Failed to list commands");
2580
2608
  this.emit({
2581
- type: 'list_commands_response',
2609
+ type: "list_commands_response",
2582
2610
  payload: {
2583
2611
  agentId,
2584
2612
  commands: [],
@@ -2598,13 +2626,13 @@ export class Session {
2598
2626
  this.sessionLogger.debug({ agentId }, `Permission response forwarded to agent ${agentId}`);
2599
2627
  }
2600
2628
  catch (error) {
2601
- this.sessionLogger.error({ err: error, agentId, requestId }, 'Failed to respond to permission');
2629
+ this.sessionLogger.error({ err: error, agentId, requestId }, "Failed to respond to permission");
2602
2630
  this.emit({
2603
- type: 'activity_log',
2631
+ type: "activity_log",
2604
2632
  payload: {
2605
2633
  id: uuidv4(),
2606
2634
  timestamp: new Date(),
2607
- type: 'error',
2635
+ type: "error",
2608
2636
  content: `Failed to respond to permission: ${error.message}`,
2609
2637
  },
2610
2638
  });
@@ -2618,7 +2646,7 @@ export class Session {
2618
2646
  const status = await getCheckoutStatus(resolvedCwd, { paseoHome: this.paseoHome });
2619
2647
  if (!status.isGit) {
2620
2648
  this.emit({
2621
- type: 'checkout_status_response',
2649
+ type: "checkout_status_response",
2622
2650
  payload: {
2623
2651
  cwd,
2624
2652
  isGit: false,
@@ -2640,7 +2668,7 @@ export class Session {
2640
2668
  }
2641
2669
  if (status.isPaseoOwnedWorktree) {
2642
2670
  this.emit({
2643
- type: 'checkout_status_response',
2671
+ type: "checkout_status_response",
2644
2672
  payload: {
2645
2673
  cwd,
2646
2674
  isGit: true,
@@ -2662,7 +2690,7 @@ export class Session {
2662
2690
  return;
2663
2691
  }
2664
2692
  this.emit({
2665
- type: 'checkout_status_response',
2693
+ type: "checkout_status_response",
2666
2694
  payload: {
2667
2695
  cwd,
2668
2696
  isGit: true,
@@ -2683,7 +2711,7 @@ export class Session {
2683
2711
  }
2684
2712
  catch (error) {
2685
2713
  this.emit({
2686
- type: 'checkout_status_response',
2714
+ type: "checkout_status_response",
2687
2715
  payload: {
2688
2716
  cwd,
2689
2717
  isGit: false,
@@ -2714,7 +2742,7 @@ export class Session {
2714
2742
  env: READ_ONLY_GIT_ENV,
2715
2743
  });
2716
2744
  this.emit({
2717
- type: 'validate_branch_response',
2745
+ type: "validate_branch_response",
2718
2746
  payload: {
2719
2747
  exists: true,
2720
2748
  resolvedRef: branchName,
@@ -2735,7 +2763,7 @@ export class Session {
2735
2763
  env: READ_ONLY_GIT_ENV,
2736
2764
  });
2737
2765
  this.emit({
2738
- type: 'validate_branch_response',
2766
+ type: "validate_branch_response",
2739
2767
  payload: {
2740
2768
  exists: true,
2741
2769
  resolvedRef: `origin/${branchName}`,
@@ -2751,7 +2779,7 @@ export class Session {
2751
2779
  }
2752
2780
  // Branch not found anywhere
2753
2781
  this.emit({
2754
- type: 'validate_branch_response',
2782
+ type: "validate_branch_response",
2755
2783
  payload: {
2756
2784
  exists: false,
2757
2785
  resolvedRef: null,
@@ -2763,7 +2791,7 @@ export class Session {
2763
2791
  }
2764
2792
  catch (error) {
2765
2793
  this.emit({
2766
- type: 'validate_branch_response',
2794
+ type: "validate_branch_response",
2767
2795
  payload: {
2768
2796
  exists: false,
2769
2797
  resolvedRef: null,
@@ -2780,7 +2808,7 @@ export class Session {
2780
2808
  const resolvedCwd = expandTilde(cwd);
2781
2809
  const branches = await listBranchSuggestions(resolvedCwd, { query, limit });
2782
2810
  this.emit({
2783
- type: 'branch_suggestions_response',
2811
+ type: "branch_suggestions_response",
2784
2812
  payload: {
2785
2813
  branches,
2786
2814
  error: null,
@@ -2790,7 +2818,7 @@ export class Session {
2790
2818
  }
2791
2819
  catch (error) {
2792
2820
  this.emit({
2793
- type: 'branch_suggestions_response',
2821
+ type: "branch_suggestions_response",
2794
2822
  payload: {
2795
2823
  branches: [],
2796
2824
  error: error instanceof Error ? error.message : String(error),
@@ -2815,12 +2843,12 @@ export class Session {
2815
2843
  homeDir: process.env.HOME ?? homedir(),
2816
2844
  query,
2817
2845
  limit,
2818
- })).map((path) => ({ path, kind: 'directory' }));
2846
+ })).map((path) => ({ path, kind: "directory" }));
2819
2847
  const directories = entries
2820
- .filter((entry) => entry.kind === 'directory')
2848
+ .filter((entry) => entry.kind === "directory")
2821
2849
  .map((entry) => entry.path);
2822
2850
  this.emit({
2823
- type: 'directory_suggestions_response',
2851
+ type: "directory_suggestions_response",
2824
2852
  payload: {
2825
2853
  directories,
2826
2854
  entries,
@@ -2831,7 +2859,7 @@ export class Session {
2831
2859
  }
2832
2860
  catch (error) {
2833
2861
  this.emit({
2834
- type: 'directory_suggestions_response',
2862
+ type: "directory_suggestions_response",
2835
2863
  payload: {
2836
2864
  directories: [],
2837
2865
  entries: [],
@@ -2842,17 +2870,17 @@ export class Session {
2842
2870
  }
2843
2871
  }
2844
2872
  normalizeCheckoutDiffCompare(compare) {
2845
- if (compare.mode === 'uncommitted') {
2846
- return { mode: 'uncommitted' };
2873
+ if (compare.mode === "uncommitted") {
2874
+ return { mode: "uncommitted" };
2847
2875
  }
2848
2876
  const trimmedBaseRef = compare.baseRef?.trim();
2849
- return trimmedBaseRef ? { mode: 'base', baseRef: trimmedBaseRef } : { mode: 'base' };
2877
+ return trimmedBaseRef ? { mode: "base", baseRef: trimmedBaseRef } : { mode: "base" };
2850
2878
  }
2851
2879
  buildCheckoutDiffTargetKey(cwd, compare) {
2852
2880
  return JSON.stringify([
2853
2881
  cwd,
2854
2882
  compare.mode,
2855
- compare.mode === 'base' ? (compare.baseRef ?? '') : '',
2883
+ compare.mode === "base" ? (compare.baseRef ?? "") : "",
2856
2884
  ]);
2857
2885
  }
2858
2886
  closeCheckoutDiffWatchTarget(target) {
@@ -2887,7 +2915,7 @@ export class Session {
2887
2915
  }
2888
2916
  async resolveCheckoutGitDir(cwd) {
2889
2917
  try {
2890
- const { stdout } = await execAsync('git rev-parse --absolute-git-dir', {
2918
+ const { stdout } = await execAsync("git rev-parse --absolute-git-dir", {
2891
2919
  cwd,
2892
2920
  env: READ_ONLY_GIT_ENV,
2893
2921
  });
@@ -2898,9 +2926,150 @@ export class Session {
2898
2926
  return null;
2899
2927
  }
2900
2928
  }
2929
+ async resolveWorkspaceGitRefsRoot(gitDir) {
2930
+ try {
2931
+ const commonDir = (await readFile(join(gitDir, "commondir"), "utf8")).trim();
2932
+ if (commonDir.length > 0) {
2933
+ return resolve(gitDir, commonDir);
2934
+ }
2935
+ }
2936
+ catch {
2937
+ // Regular repos do not have a commondir file.
2938
+ }
2939
+ return gitDir;
2940
+ }
2941
+ closeWorkspaceGitWatchTarget(target) {
2942
+ if (target.debounceTimer) {
2943
+ clearTimeout(target.debounceTimer);
2944
+ target.debounceTimer = null;
2945
+ }
2946
+ for (const watcher of target.watchers) {
2947
+ watcher.close();
2948
+ }
2949
+ target.watchers = [];
2950
+ }
2951
+ removeWorkspaceGitWatchTarget(cwd) {
2952
+ const workspaceId = normalizePersistedWorkspaceId(cwd);
2953
+ const target = this.workspaceGitWatchTargets.get(workspaceId);
2954
+ if (!target) {
2955
+ return;
2956
+ }
2957
+ this.closeWorkspaceGitWatchTarget(target);
2958
+ this.workspaceGitWatchTargets.delete(workspaceId);
2959
+ }
2960
+ workspaceGitDescriptorFingerprint(workspace) {
2961
+ if (!workspace) {
2962
+ return WORKSPACE_GIT_WATCH_REMOVED_FINGERPRINT;
2963
+ }
2964
+ return JSON.stringify([
2965
+ workspace.name,
2966
+ workspace.diffStat ? [workspace.diffStat.additions, workspace.diffStat.deletions] : null,
2967
+ ]);
2968
+ }
2969
+ shouldSkipWorkspaceGitWatchUpdate(workspaceId, workspace) {
2970
+ const target = this.workspaceGitWatchTargets.get(workspaceId);
2971
+ if (!target) {
2972
+ return false;
2973
+ }
2974
+ const nextFingerprint = this.workspaceGitDescriptorFingerprint(workspace);
2975
+ if (target.latestFingerprint === nextFingerprint) {
2976
+ return true;
2977
+ }
2978
+ target.latestFingerprint = nextFingerprint;
2979
+ return false;
2980
+ }
2981
+ rememberWorkspaceGitWatchFingerprint(workspaceId, workspace) {
2982
+ const target = this.workspaceGitWatchTargets.get(workspaceId);
2983
+ if (!target) {
2984
+ return;
2985
+ }
2986
+ target.latestFingerprint = this.workspaceGitDescriptorFingerprint(workspace);
2987
+ }
2988
+ primeWorkspaceGitWatchFingerprints(workspaces) {
2989
+ for (const workspace of workspaces) {
2990
+ this.rememberWorkspaceGitWatchFingerprint(workspace.id, workspace);
2991
+ }
2992
+ }
2993
+ scheduleWorkspaceGitWatchRefresh(target) {
2994
+ if (target.debounceTimer) {
2995
+ clearTimeout(target.debounceTimer);
2996
+ }
2997
+ target.debounceTimer = setTimeout(() => {
2998
+ target.debounceTimer = null;
2999
+ void this.refreshWorkspaceGitWatchTarget(target);
3000
+ }, WORKSPACE_GIT_WATCH_DEBOUNCE_MS);
3001
+ }
3002
+ async refreshWorkspaceGitWatchTarget(target) {
3003
+ if (target.refreshPromise) {
3004
+ target.refreshQueued = true;
3005
+ return;
3006
+ }
3007
+ target.refreshPromise = (async () => {
3008
+ do {
3009
+ target.refreshQueued = false;
3010
+ await this.emitWorkspaceUpdateForCwd(target.cwd, {
3011
+ dedupeGitState: true,
3012
+ });
3013
+ } while (target.refreshQueued);
3014
+ })();
3015
+ try {
3016
+ await target.refreshPromise;
3017
+ }
3018
+ finally {
3019
+ target.refreshPromise = null;
3020
+ }
3021
+ }
3022
+ async ensureWorkspaceGitWatchTarget(cwd) {
3023
+ const workspaceId = normalizePersistedWorkspaceId(cwd);
3024
+ if (this.workspaceGitWatchTargets.has(workspaceId)) {
3025
+ return;
3026
+ }
3027
+ const gitDir = await this.resolveCheckoutGitDir(cwd);
3028
+ if (!gitDir) {
3029
+ return;
3030
+ }
3031
+ const refsRoot = await this.resolveWorkspaceGitRefsRoot(gitDir);
3032
+ const target = {
3033
+ cwd: workspaceId,
3034
+ watchers: [],
3035
+ debounceTimer: null,
3036
+ refreshPromise: null,
3037
+ refreshQueued: false,
3038
+ latestFingerprint: null,
3039
+ };
3040
+ for (const watchPath of new Set([join(gitDir, "HEAD"), join(refsRoot, "refs", "heads")])) {
3041
+ let watcher = null;
3042
+ try {
3043
+ watcher = watch(watchPath, { recursive: false }, () => {
3044
+ this.scheduleWorkspaceGitWatchRefresh(target);
3045
+ });
3046
+ }
3047
+ catch (error) {
3048
+ this.sessionLogger.warn({ err: error, cwd, watchPath }, "Failed to start workspace git watcher");
3049
+ }
3050
+ if (!watcher) {
3051
+ continue;
3052
+ }
3053
+ watcher.on("error", (error) => {
3054
+ this.sessionLogger.warn({ err: error, cwd, watchPath }, "Workspace git watcher error");
3055
+ });
3056
+ target.watchers.push(watcher);
3057
+ }
3058
+ if (target.watchers.length === 0) {
3059
+ return;
3060
+ }
3061
+ this.workspaceGitWatchTargets.set(workspaceId, target);
3062
+ }
3063
+ async syncWorkspaceGitWatchTarget(cwd, options) {
3064
+ if (!options.isGit) {
3065
+ this.removeWorkspaceGitWatchTarget(cwd);
3066
+ return;
3067
+ }
3068
+ await this.ensureWorkspaceGitWatchTarget(cwd);
3069
+ }
2901
3070
  async resolveCheckoutWatchRoot(cwd) {
2902
3071
  try {
2903
- const { stdout } = await execAsync('git rev-parse --path-format=absolute --show-toplevel', {
3072
+ const { stdout } = await execAsync("git rev-parse --path-format=absolute --show-toplevel", {
2904
3073
  cwd,
2905
3074
  env: READ_ONLY_GIT_ENV,
2906
3075
  });
@@ -2926,7 +3095,7 @@ export class Session {
2926
3095
  }
2927
3096
  for (const subscriptionId of target.subscriptions) {
2928
3097
  this.emit({
2929
- type: 'checkout_diff_update',
3098
+ type: "checkout_diff_update",
2930
3099
  payload: {
2931
3100
  subscriptionId,
2932
3101
  ...snapshot,
@@ -3019,7 +3188,7 @@ export class Session {
3019
3188
  watchPaths.add(gitDir);
3020
3189
  }
3021
3190
  let hasRecursiveRepoCoverage = false;
3022
- const allowRecursiveRepoWatch = process.platform !== 'linux';
3191
+ const allowRecursiveRepoWatch = process.platform !== "linux";
3023
3192
  for (const watchPath of watchPaths) {
3024
3193
  const shouldTryRecursive = watchPath === repoWatchPath && allowRecursiveRepoWatch;
3025
3194
  const createWatcher = (recursive) => watch(watchPath, { recursive }, () => {
@@ -3040,21 +3209,21 @@ export class Session {
3040
3209
  if (shouldTryRecursive) {
3041
3210
  try {
3042
3211
  watcher = createWatcher(false);
3043
- this.sessionLogger.warn({ err: error, watchPath, cwd, compare }, 'Checkout diff recursive watch unavailable; using non-recursive fallback');
3212
+ this.sessionLogger.warn({ err: error, watchPath, cwd, compare }, "Checkout diff recursive watch unavailable; using non-recursive fallback");
3044
3213
  }
3045
3214
  catch (fallbackError) {
3046
- this.sessionLogger.warn({ err: fallbackError, watchPath, cwd, compare }, 'Failed to start checkout diff watcher');
3215
+ this.sessionLogger.warn({ err: fallbackError, watchPath, cwd, compare }, "Failed to start checkout diff watcher");
3047
3216
  }
3048
3217
  }
3049
3218
  else {
3050
- this.sessionLogger.warn({ err: error, watchPath, cwd, compare }, 'Failed to start checkout diff watcher');
3219
+ this.sessionLogger.warn({ err: error, watchPath, cwd, compare }, "Failed to start checkout diff watcher");
3051
3220
  }
3052
3221
  }
3053
3222
  if (!watcher) {
3054
3223
  continue;
3055
3224
  }
3056
- watcher.on('error', (error) => {
3057
- this.sessionLogger.warn({ err: error, watchPath, cwd, compare }, 'Checkout diff watcher error');
3225
+ watcher.on("error", (error) => {
3226
+ this.sessionLogger.warn({ err: error, watchPath, cwd, compare }, "Checkout diff watcher error");
3058
3227
  });
3059
3228
  target.watchers.push(watcher);
3060
3229
  if (watchPath === repoWatchPath && watcherIsRecursive) {
@@ -3070,8 +3239,8 @@ export class Session {
3070
3239
  cwd,
3071
3240
  compare,
3072
3241
  intervalMs: CHECKOUT_DIFF_FALLBACK_REFRESH_MS,
3073
- reason: target.watchers.length === 0 ? 'no_watchers' : 'missing_recursive_repo_root_coverage',
3074
- }, 'Checkout diff watchers unavailable; using timed refresh fallback');
3242
+ reason: target.watchers.length === 0 ? "no_watchers" : "missing_recursive_repo_root_coverage",
3243
+ }, "Checkout diff watchers unavailable; using timed refresh fallback");
3075
3244
  }
3076
3245
  this.checkoutDiffTargets.set(targetKey, target);
3077
3246
  return target;
@@ -3092,7 +3261,7 @@ export class Session {
3092
3261
  target.latestPayload = snapshot;
3093
3262
  target.latestFingerprint = this.checkoutDiffSnapshotFingerprint(snapshot);
3094
3263
  this.emit({
3095
- type: 'subscribe_checkout_diff_response',
3264
+ type: "subscribe_checkout_diff_response",
3096
3265
  payload: {
3097
3266
  subscriptionId: msg.subscriptionId,
3098
3267
  ...snapshot,
@@ -3115,12 +3284,12 @@ export class Session {
3115
3284
  async handleCheckoutCommitRequest(msg) {
3116
3285
  const { cwd, requestId } = msg;
3117
3286
  try {
3118
- let message = msg.message?.trim() ?? '';
3287
+ let message = msg.message?.trim() ?? "";
3119
3288
  if (!message) {
3120
3289
  message = await this.generateCommitMessage(cwd);
3121
3290
  }
3122
3291
  if (!message) {
3123
- throw new Error('Commit message is required');
3292
+ throw new Error("Commit message is required");
3124
3293
  }
3125
3294
  await commitChanges(cwd, {
3126
3295
  message,
@@ -3128,7 +3297,7 @@ export class Session {
3128
3297
  });
3129
3298
  this.scheduleCheckoutDiffRefreshForCwd(cwd);
3130
3299
  this.emit({
3131
- type: 'checkout_commit_response',
3300
+ type: "checkout_commit_response",
3132
3301
  payload: {
3133
3302
  cwd,
3134
3303
  success: true,
@@ -3139,7 +3308,7 @@ export class Session {
3139
3308
  }
3140
3309
  catch (error) {
3141
3310
  this.emit({
3142
- type: 'checkout_commit_response',
3311
+ type: "checkout_commit_response",
3143
3312
  payload: {
3144
3313
  cwd,
3145
3314
  success: false,
@@ -3155,13 +3324,13 @@ export class Session {
3155
3324
  const status = await getCheckoutStatus(cwd, { paseoHome: this.paseoHome });
3156
3325
  if (!status.isGit) {
3157
3326
  try {
3158
- await execAsync('git rev-parse --is-inside-work-tree', {
3327
+ await execAsync("git rev-parse --is-inside-work-tree", {
3159
3328
  cwd,
3160
3329
  env: READ_ONLY_GIT_ENV,
3161
3330
  });
3162
3331
  }
3163
3332
  catch (error) {
3164
- const details = typeof error?.stderr === 'string'
3333
+ const details = typeof error?.stderr === "string"
3165
3334
  ? String(error.stderr).trim()
3166
3335
  : error instanceof Error
3167
3336
  ? error.message
@@ -3170,28 +3339,28 @@ export class Session {
3170
3339
  }
3171
3340
  }
3172
3341
  if (msg.requireCleanTarget) {
3173
- const { stdout } = await execAsync('git status --porcelain', {
3342
+ const { stdout } = await execAsync("git status --porcelain", {
3174
3343
  cwd,
3175
3344
  env: READ_ONLY_GIT_ENV,
3176
3345
  });
3177
3346
  if (stdout.trim().length > 0) {
3178
- throw new Error('Working directory has uncommitted changes.');
3347
+ throw new Error("Working directory has uncommitted changes.");
3179
3348
  }
3180
3349
  }
3181
3350
  let baseRef = msg.baseRef ?? (status.isGit ? status.baseRef : null);
3182
3351
  if (!baseRef) {
3183
- throw new Error('Base branch is required for merge');
3352
+ throw new Error("Base branch is required for merge");
3184
3353
  }
3185
- if (baseRef.startsWith('origin/')) {
3186
- baseRef = baseRef.slice('origin/'.length);
3354
+ if (baseRef.startsWith("origin/")) {
3355
+ baseRef = baseRef.slice("origin/".length);
3187
3356
  }
3188
3357
  await mergeToBase(cwd, {
3189
3358
  baseRef,
3190
- mode: msg.strategy === 'squash' ? 'squash' : 'merge',
3359
+ mode: msg.strategy === "squash" ? "squash" : "merge",
3191
3360
  }, { paseoHome: this.paseoHome });
3192
3361
  this.scheduleCheckoutDiffRefreshForCwd(cwd);
3193
3362
  this.emit({
3194
- type: 'checkout_merge_response',
3363
+ type: "checkout_merge_response",
3195
3364
  payload: {
3196
3365
  cwd,
3197
3366
  success: true,
@@ -3202,7 +3371,7 @@ export class Session {
3202
3371
  }
3203
3372
  catch (error) {
3204
3373
  this.emit({
3205
- type: 'checkout_merge_response',
3374
+ type: "checkout_merge_response",
3206
3375
  payload: {
3207
3376
  cwd,
3208
3377
  success: false,
@@ -3216,12 +3385,12 @@ export class Session {
3216
3385
  const { cwd, requestId } = msg;
3217
3386
  try {
3218
3387
  if (msg.requireCleanTarget ?? true) {
3219
- const { stdout } = await execAsync('git status --porcelain', {
3388
+ const { stdout } = await execAsync("git status --porcelain", {
3220
3389
  cwd,
3221
3390
  env: READ_ONLY_GIT_ENV,
3222
3391
  });
3223
3392
  if (stdout.trim().length > 0) {
3224
- throw new Error('Working directory has uncommitted changes.');
3393
+ throw new Error("Working directory has uncommitted changes.");
3225
3394
  }
3226
3395
  }
3227
3396
  await mergeFromBase(cwd, {
@@ -3230,7 +3399,7 @@ export class Session {
3230
3399
  });
3231
3400
  this.scheduleCheckoutDiffRefreshForCwd(cwd);
3232
3401
  this.emit({
3233
- type: 'checkout_merge_from_base_response',
3402
+ type: "checkout_merge_from_base_response",
3234
3403
  payload: {
3235
3404
  cwd,
3236
3405
  success: true,
@@ -3241,7 +3410,7 @@ export class Session {
3241
3410
  }
3242
3411
  catch (error) {
3243
3412
  this.emit({
3244
- type: 'checkout_merge_from_base_response',
3413
+ type: "checkout_merge_from_base_response",
3245
3414
  payload: {
3246
3415
  cwd,
3247
3416
  success: false,
@@ -3256,7 +3425,7 @@ export class Session {
3256
3425
  try {
3257
3426
  await pushCurrentBranch(cwd);
3258
3427
  this.emit({
3259
- type: 'checkout_push_response',
3428
+ type: "checkout_push_response",
3260
3429
  payload: {
3261
3430
  cwd,
3262
3431
  success: true,
@@ -3267,7 +3436,7 @@ export class Session {
3267
3436
  }
3268
3437
  catch (error) {
3269
3438
  this.emit({
3270
- type: 'checkout_push_response',
3439
+ type: "checkout_push_response",
3271
3440
  payload: {
3272
3441
  cwd,
3273
3442
  success: false,
@@ -3280,8 +3449,8 @@ export class Session {
3280
3449
  async handleCheckoutPrCreateRequest(msg) {
3281
3450
  const { cwd, requestId } = msg;
3282
3451
  try {
3283
- let title = msg.title?.trim() ?? '';
3284
- let body = msg.body?.trim() ?? '';
3452
+ let title = msg.title?.trim() ?? "";
3453
+ let body = msg.body?.trim() ?? "";
3285
3454
  if (!title || !body) {
3286
3455
  const generated = await this.generatePullRequestText(cwd, msg.baseRef);
3287
3456
  if (!title)
@@ -3295,7 +3464,7 @@ export class Session {
3295
3464
  base: msg.baseRef,
3296
3465
  });
3297
3466
  this.emit({
3298
- type: 'checkout_pr_create_response',
3467
+ type: "checkout_pr_create_response",
3299
3468
  payload: {
3300
3469
  cwd,
3301
3470
  url: result.url ?? null,
@@ -3307,7 +3476,7 @@ export class Session {
3307
3476
  }
3308
3477
  catch (error) {
3309
3478
  this.emit({
3310
- type: 'checkout_pr_create_response',
3479
+ type: "checkout_pr_create_response",
3311
3480
  payload: {
3312
3481
  cwd,
3313
3482
  url: null,
@@ -3323,7 +3492,7 @@ export class Session {
3323
3492
  try {
3324
3493
  const prStatus = await getPullRequestStatus(cwd);
3325
3494
  this.emit({
3326
- type: 'checkout_pr_status_response',
3495
+ type: "checkout_pr_status_response",
3327
3496
  payload: {
3328
3497
  cwd,
3329
3498
  status: prStatus.status,
@@ -3335,7 +3504,7 @@ export class Session {
3335
3504
  }
3336
3505
  catch (error) {
3337
3506
  this.emit({
3338
- type: 'checkout_pr_status_response',
3507
+ type: "checkout_pr_status_response",
3339
3508
  payload: {
3340
3509
  cwd,
3341
3510
  status: null,
@@ -3351,10 +3520,10 @@ export class Session {
3351
3520
  const cwd = msg.repoRoot ?? msg.cwd;
3352
3521
  if (!cwd) {
3353
3522
  this.emit({
3354
- type: 'paseo_worktree_list_response',
3523
+ type: "paseo_worktree_list_response",
3355
3524
  payload: {
3356
3525
  worktrees: [],
3357
- error: { code: 'UNKNOWN', message: 'cwd or repoRoot is required' },
3526
+ error: { code: "UNKNOWN", message: "cwd or repoRoot is required" },
3358
3527
  requestId,
3359
3528
  },
3360
3529
  });
@@ -3363,7 +3532,7 @@ export class Session {
3363
3532
  try {
3364
3533
  const worktrees = await listPaseoWorktrees({ cwd, paseoHome: this.paseoHome });
3365
3534
  this.emit({
3366
- type: 'paseo_worktree_list_response',
3535
+ type: "paseo_worktree_list_response",
3367
3536
  payload: {
3368
3537
  worktrees: worktrees.map((entry) => ({
3369
3538
  worktreePath: entry.path,
@@ -3378,7 +3547,7 @@ export class Session {
3378
3547
  }
3379
3548
  catch (error) {
3380
3549
  this.emit({
3381
- type: 'paseo_worktree_list_response',
3550
+ type: "paseo_worktree_list_response",
3382
3551
  payload: {
3383
3552
  worktrees: [],
3384
3553
  error: this.toCheckoutError(error),
@@ -3443,7 +3612,7 @@ export class Session {
3443
3612
  }
3444
3613
  for (const agentId of removedAgents) {
3445
3614
  this.emit({
3446
- type: 'agent_deleted',
3615
+ type: "agent_deleted",
3447
3616
  payload: {
3448
3617
  agentId,
3449
3618
  requestId: options.requestId,
@@ -3460,7 +3629,7 @@ export class Session {
3460
3629
  try {
3461
3630
  if (!targetPath) {
3462
3631
  if (!repoRoot || !msg.branchName) {
3463
- throw new Error('worktreePath or repoRoot+branchName is required');
3632
+ throw new Error("worktreePath or repoRoot+branchName is required");
3464
3633
  }
3465
3634
  const worktrees = await listPaseoWorktrees({ cwd: repoRoot, paseoHome: this.paseoHome });
3466
3635
  const match = worktrees.find((entry) => entry.branchName === msg.branchName);
@@ -3474,13 +3643,13 @@ export class Session {
3474
3643
  });
3475
3644
  if (!ownership.allowed) {
3476
3645
  this.emit({
3477
- type: 'paseo_worktree_archive_response',
3646
+ type: "paseo_worktree_archive_response",
3478
3647
  payload: {
3479
3648
  success: false,
3480
3649
  removedAgents: [],
3481
3650
  error: {
3482
- code: 'NOT_ALLOWED',
3483
- message: 'Worktree is not a Paseo-owned worktree',
3651
+ code: "NOT_ALLOWED",
3652
+ message: "Worktree is not a Paseo-owned worktree",
3484
3653
  },
3485
3654
  requestId,
3486
3655
  },
@@ -3489,7 +3658,7 @@ export class Session {
3489
3658
  }
3490
3659
  repoRoot = ownership.repoRoot ?? repoRoot ?? null;
3491
3660
  if (!repoRoot) {
3492
- throw new Error('Unable to resolve repo root for worktree');
3661
+ throw new Error("Unable to resolve repo root for worktree");
3493
3662
  }
3494
3663
  const removedAgents = await this.archivePaseoWorktree({
3495
3664
  targetPath,
@@ -3497,7 +3666,7 @@ export class Session {
3497
3666
  requestId,
3498
3667
  });
3499
3668
  this.emit({
3500
- type: 'paseo_worktree_archive_response',
3669
+ type: "paseo_worktree_archive_response",
3501
3670
  payload: {
3502
3671
  success: true,
3503
3672
  removedAgents,
@@ -3508,7 +3677,7 @@ export class Session {
3508
3677
  }
3509
3678
  catch (error) {
3510
3679
  this.emit({
3511
- type: 'paseo_worktree_archive_response',
3680
+ type: "paseo_worktree_archive_response",
3512
3681
  payload: {
3513
3682
  success: false,
3514
3683
  removedAgents: [],
@@ -3522,31 +3691,31 @@ export class Session {
3522
3691
  * Handle read-only file explorer requests scoped to a workspace cwd
3523
3692
  */
3524
3693
  async handleFileExplorerRequest(request) {
3525
- const { cwd: workspaceCwd, path: requestedPath = '.', mode, requestId } = request;
3694
+ const { cwd: workspaceCwd, path: requestedPath = ".", mode, requestId } = request;
3526
3695
  const cwd = workspaceCwd.trim();
3527
3696
  if (!cwd) {
3528
3697
  this.emit({
3529
- type: 'file_explorer_response',
3698
+ type: "file_explorer_response",
3530
3699
  payload: {
3531
3700
  cwd: workspaceCwd,
3532
3701
  path: requestedPath,
3533
3702
  mode,
3534
3703
  directory: null,
3535
3704
  file: null,
3536
- error: 'cwd is required',
3705
+ error: "cwd is required",
3537
3706
  requestId,
3538
3707
  },
3539
3708
  });
3540
3709
  return;
3541
3710
  }
3542
3711
  try {
3543
- if (mode === 'list') {
3712
+ if (mode === "list") {
3544
3713
  const directory = await listDirectoryEntries({
3545
3714
  root: cwd,
3546
3715
  relativePath: requestedPath,
3547
3716
  });
3548
3717
  this.emit({
3549
- type: 'file_explorer_response',
3718
+ type: "file_explorer_response",
3550
3719
  payload: {
3551
3720
  cwd,
3552
3721
  path: directory.path,
@@ -3564,7 +3733,7 @@ export class Session {
3564
3733
  relativePath: requestedPath,
3565
3734
  });
3566
3735
  this.emit({
3567
- type: 'file_explorer_response',
3736
+ type: "file_explorer_response",
3568
3737
  payload: {
3569
3738
  cwd,
3570
3739
  path: file.path,
@@ -3580,7 +3749,7 @@ export class Session {
3580
3749
  catch (error) {
3581
3750
  this.sessionLogger.error({ err: error, cwd, path: requestedPath }, `Failed to fulfill file explorer request for workspace ${cwd}`);
3582
3751
  this.emit({
3583
- type: 'file_explorer_response',
3752
+ type: "file_explorer_response",
3584
3753
  payload: {
3585
3754
  cwd,
3586
3755
  path: requestedPath,
@@ -3601,7 +3770,7 @@ export class Session {
3601
3770
  try {
3602
3771
  const icon = await getProjectIcon(cwd);
3603
3772
  this.emit({
3604
- type: 'project_icon_response',
3773
+ type: "project_icon_response",
3605
3774
  payload: {
3606
3775
  cwd,
3607
3776
  icon,
@@ -3612,7 +3781,7 @@ export class Session {
3612
3781
  }
3613
3782
  catch (error) {
3614
3783
  this.emit({
3615
- type: 'project_icon_response',
3784
+ type: "project_icon_response",
3616
3785
  payload: {
3617
3786
  cwd,
3618
3787
  icon: null,
@@ -3630,7 +3799,7 @@ export class Session {
3630
3799
  const cwd = workspaceCwd.trim();
3631
3800
  if (!cwd) {
3632
3801
  this.emit({
3633
- type: 'file_download_token_response',
3802
+ type: "file_download_token_response",
3634
3803
  payload: {
3635
3804
  cwd: workspaceCwd,
3636
3805
  path: requestedPath,
@@ -3638,7 +3807,7 @@ export class Session {
3638
3807
  fileName: null,
3639
3808
  mimeType: null,
3640
3809
  size: null,
3641
- error: 'cwd is required',
3810
+ error: "cwd is required",
3642
3811
  requestId,
3643
3812
  },
3644
3813
  });
@@ -3658,7 +3827,7 @@ export class Session {
3658
3827
  size: info.size,
3659
3828
  });
3660
3829
  this.emit({
3661
- type: 'file_download_token_response',
3830
+ type: "file_download_token_response",
3662
3831
  payload: {
3663
3832
  cwd,
3664
3833
  path: info.path,
@@ -3674,7 +3843,7 @@ export class Session {
3674
3843
  catch (error) {
3675
3844
  this.sessionLogger.error({ err: error, cwd, path: requestedPath }, `Failed to issue download token for workspace ${cwd}`);
3676
3845
  this.emit({
3677
- type: 'file_download_token_response',
3846
+ type: "file_download_token_response",
3678
3847
  payload: {
3679
3848
  cwd,
3680
3849
  path: requestedPath,
@@ -3713,7 +3882,7 @@ export class Session {
3713
3882
  async resolveAgentIdentifier(identifier) {
3714
3883
  const trimmed = identifier.trim();
3715
3884
  if (!trimmed) {
3716
- return { ok: false, error: 'Agent identifier cannot be empty' };
3885
+ return { ok: false, error: "Agent identifier cannot be empty" };
3717
3886
  }
3718
3887
  const stored = await this.agentStorage.list();
3719
3888
  const storedRecords = stored.filter((record) => !record.internal);
@@ -3737,7 +3906,7 @@ export class Session {
3737
3906
  error: `Agent identifier "${trimmed}" is ambiguous (${prefixMatches
3738
3907
  .slice(0, 5)
3739
3908
  .map((id) => id.slice(0, 8))
3740
- .join(', ')}${prefixMatches.length > 5 ? ', …' : ''})`,
3909
+ .join(", ")}${prefixMatches.length > 5 ? ", …" : ""})`,
3741
3910
  };
3742
3911
  }
3743
3912
  const titleMatches = storedRecords.filter((record) => record.title === trimmed);
@@ -3750,7 +3919,7 @@ export class Session {
3750
3919
  error: `Agent title "${trimmed}" is ambiguous (${titleMatches
3751
3920
  .slice(0, 5)
3752
3921
  .map((r) => r.id.slice(0, 8))
3753
- .join(', ')}${titleMatches.length > 5 ? ', …' : ''})`,
3922
+ .join(", ")}${titleMatches.length > 5 ? ", …" : ""})`,
3754
3923
  };
3755
3924
  }
3756
3925
  return { ok: false, error: `Agent not found: ${trimmed}` };
@@ -3767,7 +3936,7 @@ export class Session {
3767
3936
  return this.buildStoredAgentPayload(record);
3768
3937
  }
3769
3938
  normalizeFetchAgentsSort(sort) {
3770
- const fallback = [{ key: 'updated_at', direction: 'desc' }];
3939
+ const fallback = [{ key: "updated_at", direction: "desc" }];
3771
3940
  if (!sort || sort.length === 0) {
3772
3941
  return fallback;
3773
3942
  }
@@ -3785,42 +3954,42 @@ export class Session {
3785
3954
  getStatusPriority(agent) {
3786
3955
  const attentionReason = agent.attentionReason ?? null;
3787
3956
  const hasPendingPermission = (agent.pendingPermissions?.length ?? 0) > 0;
3788
- if (hasPendingPermission || attentionReason === 'permission') {
3957
+ if (hasPendingPermission || attentionReason === "permission") {
3789
3958
  return 0;
3790
3959
  }
3791
- if (agent.status === 'error' || attentionReason === 'error') {
3960
+ if (agent.status === "error" || attentionReason === "error") {
3792
3961
  return 1;
3793
3962
  }
3794
- if (agent.status === 'running') {
3963
+ if (agent.status === "running") {
3795
3964
  return 2;
3796
3965
  }
3797
- if (agent.status === 'initializing') {
3966
+ if (agent.status === "initializing") {
3798
3967
  return 3;
3799
3968
  }
3800
3969
  return 4;
3801
3970
  }
3802
3971
  getFetchAgentsSortValue(entry, key) {
3803
3972
  switch (key) {
3804
- case 'status_priority':
3973
+ case "status_priority":
3805
3974
  return this.getStatusPriority(entry.agent);
3806
- case 'created_at':
3975
+ case "created_at":
3807
3976
  return Date.parse(entry.agent.createdAt);
3808
- case 'updated_at':
3977
+ case "updated_at":
3809
3978
  return Date.parse(entry.agent.updatedAt);
3810
- case 'title':
3811
- return entry.agent.title?.toLocaleLowerCase() ?? '';
3979
+ case "title":
3980
+ return entry.agent.title?.toLocaleLowerCase() ?? "";
3812
3981
  }
3813
3982
  }
3814
3983
  getFetchAgentsSortValueFromAgent(agent, key) {
3815
3984
  switch (key) {
3816
- case 'status_priority':
3985
+ case "status_priority":
3817
3986
  return this.getStatusPriority(agent);
3818
- case 'created_at':
3987
+ case "created_at":
3819
3988
  return Date.parse(agent.createdAt);
3820
- case 'updated_at':
3989
+ case "updated_at":
3821
3990
  return Date.parse(agent.updatedAt);
3822
- case 'title':
3823
- return agent.title?.toLocaleLowerCase() ?? '';
3991
+ case "title":
3992
+ return agent.title?.toLocaleLowerCase() ?? "";
3824
3993
  }
3825
3994
  }
3826
3995
  compareSortValues(left, right) {
@@ -3833,7 +4002,7 @@ export class Session {
3833
4002
  if (right === null) {
3834
4003
  return 1;
3835
4004
  }
3836
- if (typeof left === 'number' && typeof right === 'number') {
4005
+ if (typeof left === "number" && typeof right === "number") {
3837
4006
  return left < right ? -1 : 1;
3838
4007
  }
3839
4008
  return String(left).localeCompare(String(right));
@@ -3846,7 +4015,7 @@ export class Session {
3846
4015
  if (base === 0) {
3847
4016
  continue;
3848
4017
  }
3849
- return spec.direction === 'asc' ? base : -base;
4018
+ return spec.direction === "asc" ? base : -base;
3850
4019
  }
3851
4020
  return left.id.localeCompare(right.id);
3852
4021
  }
@@ -3859,48 +4028,48 @@ export class Session {
3859
4028
  sort,
3860
4029
  values,
3861
4030
  id: entry.agent.id,
3862
- }), 'utf8').toString('base64url');
4031
+ }), "utf8").toString("base64url");
3863
4032
  }
3864
4033
  decodeFetchAgentsCursor(cursor, sort) {
3865
4034
  let parsed;
3866
4035
  try {
3867
- parsed = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
4036
+ parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
3868
4037
  }
3869
4038
  catch {
3870
- throw new SessionRequestError('invalid_cursor', 'Invalid fetch_agents cursor');
4039
+ throw new SessionRequestError("invalid_cursor", "Invalid fetch_agents cursor");
3871
4040
  }
3872
- if (!parsed || typeof parsed !== 'object') {
3873
- throw new SessionRequestError('invalid_cursor', 'Invalid fetch_agents cursor');
4041
+ if (!parsed || typeof parsed !== "object") {
4042
+ throw new SessionRequestError("invalid_cursor", "Invalid fetch_agents cursor");
3874
4043
  }
3875
4044
  const payload = parsed;
3876
- if (!Array.isArray(payload.sort) || typeof payload.id !== 'string') {
3877
- throw new SessionRequestError('invalid_cursor', 'Invalid fetch_agents cursor');
4045
+ if (!Array.isArray(payload.sort) || typeof payload.id !== "string") {
4046
+ throw new SessionRequestError("invalid_cursor", "Invalid fetch_agents cursor");
3878
4047
  }
3879
- if (!payload.values || typeof payload.values !== 'object') {
3880
- throw new SessionRequestError('invalid_cursor', 'Invalid fetch_agents cursor');
4048
+ if (!payload.values || typeof payload.values !== "object") {
4049
+ throw new SessionRequestError("invalid_cursor", "Invalid fetch_agents cursor");
3881
4050
  }
3882
4051
  const cursorSort = [];
3883
4052
  for (const item of payload.sort) {
3884
4053
  if (!item ||
3885
- typeof item !== 'object' ||
3886
- typeof item.key !== 'string' ||
3887
- typeof item.direction !== 'string') {
3888
- throw new SessionRequestError('invalid_cursor', 'Invalid fetch_agents cursor');
4054
+ typeof item !== "object" ||
4055
+ typeof item.key !== "string" ||
4056
+ typeof item.direction !== "string") {
4057
+ throw new SessionRequestError("invalid_cursor", "Invalid fetch_agents cursor");
3889
4058
  }
3890
4059
  const key = item.key;
3891
4060
  const direction = item.direction;
3892
- if ((key !== 'status_priority' &&
3893
- key !== 'created_at' &&
3894
- key !== 'updated_at' &&
3895
- key !== 'title') ||
3896
- (direction !== 'asc' && direction !== 'desc')) {
3897
- throw new SessionRequestError('invalid_cursor', 'Invalid fetch_agents cursor');
4061
+ if ((key !== "status_priority" &&
4062
+ key !== "created_at" &&
4063
+ key !== "updated_at" &&
4064
+ key !== "title") ||
4065
+ (direction !== "asc" && direction !== "desc")) {
4066
+ throw new SessionRequestError("invalid_cursor", "Invalid fetch_agents cursor");
3898
4067
  }
3899
4068
  cursorSort.push({ key, direction });
3900
4069
  }
3901
4070
  if (cursorSort.length !== sort.length ||
3902
4071
  cursorSort.some((entry, index) => entry.key !== sort[index]?.key || entry.direction !== sort[index]?.direction)) {
3903
- throw new SessionRequestError('invalid_cursor', 'fetch_agents cursor does not match current sort');
4072
+ throw new SessionRequestError("invalid_cursor", "fetch_agents cursor does not match current sort");
3904
4073
  }
3905
4074
  return {
3906
4075
  sort: cursorSort,
@@ -3916,7 +4085,7 @@ export class Session {
3916
4085
  if (base === 0) {
3917
4086
  continue;
3918
4087
  }
3919
- return spec.direction === 'asc' ? base : -base;
4088
+ return spec.direction === "asc" ? base : -base;
3920
4089
  }
3921
4090
  return agent.id.localeCompare(cursor.id);
3922
4091
  }
@@ -3982,19 +4151,19 @@ export class Session {
3982
4151
  }
3983
4152
  deriveWorkspaceStateBucket(agent) {
3984
4153
  const pendingPermissionCount = agent.pendingPermissions?.length ?? 0;
3985
- if (pendingPermissionCount > 0 || agent.attentionReason === 'permission') {
3986
- return 'needs_input';
4154
+ if (pendingPermissionCount > 0 || agent.attentionReason === "permission") {
4155
+ return "needs_input";
3987
4156
  }
3988
- if (agent.status === 'error' || agent.attentionReason === 'error') {
3989
- return 'failed';
4157
+ if (agent.status === "error" || agent.attentionReason === "error") {
4158
+ return "failed";
3990
4159
  }
3991
- if (agent.status === 'running' || agent.status === 'initializing') {
3992
- return 'running';
4160
+ if (agent.status === "running" || agent.status === "initializing") {
4161
+ return "running";
3993
4162
  }
3994
4163
  if (agent.requiresAttention) {
3995
- return 'attention';
4164
+ return "attention";
3996
4165
  }
3997
- return 'done';
4166
+ return "done";
3998
4167
  }
3999
4168
  accumulateLatestActivityAt(current, agent) {
4000
4169
  const candidateRaw = agent.lastUserMessageAt ?? agent.updatedAt;
@@ -4036,10 +4205,10 @@ export class Session {
4036
4205
  projectId: workspace.projectId,
4037
4206
  projectDisplayName: resolvedProjectRecord?.displayName ?? workspace.projectId,
4038
4207
  projectRootPath: resolvedProjectRecord?.rootPath ?? workspace.cwd,
4039
- projectKind: resolvedProjectRecord?.kind ?? 'non_git',
4208
+ projectKind: resolvedProjectRecord?.kind ?? "non_git",
4040
4209
  workspaceKind: workspace.kind,
4041
4210
  name: displayName,
4042
- status: 'done',
4211
+ status: "done",
4043
4212
  activityAt: null,
4044
4213
  diffStat,
4045
4214
  };
@@ -4080,7 +4249,7 @@ export class Session {
4080
4249
  return this.listWorkspaceDescriptorsSnapshot();
4081
4250
  }
4082
4251
  normalizeFetchWorkspacesSort(sort) {
4083
- const fallback = [{ key: 'activity_at', direction: 'desc' }];
4252
+ const fallback = [{ key: "activity_at", direction: "desc" }];
4084
4253
  if (!sort || sort.length === 0) {
4085
4254
  return fallback;
4086
4255
  }
@@ -4097,13 +4266,13 @@ export class Session {
4097
4266
  }
4098
4267
  getFetchWorkspacesSortValue(workspace, key) {
4099
4268
  switch (key) {
4100
- case 'status_priority':
4269
+ case "status_priority":
4101
4270
  return this.workspaceStatePriority[workspace.status];
4102
- case 'activity_at':
4271
+ case "activity_at":
4103
4272
  return workspace.activityAt ? Date.parse(workspace.activityAt) : null;
4104
- case 'name':
4273
+ case "name":
4105
4274
  return workspace.name.toLocaleLowerCase();
4106
- case 'project_id':
4275
+ case "project_id":
4107
4276
  return workspace.projectId.toLocaleLowerCase();
4108
4277
  }
4109
4278
  }
@@ -4115,7 +4284,7 @@ export class Session {
4115
4284
  if (base === 0) {
4116
4285
  continue;
4117
4286
  }
4118
- return spec.direction === 'asc' ? base : -base;
4287
+ return spec.direction === "asc" ? base : -base;
4119
4288
  }
4120
4289
  return left.id.localeCompare(right.id);
4121
4290
  }
@@ -4128,48 +4297,48 @@ export class Session {
4128
4297
  sort,
4129
4298
  values,
4130
4299
  id: entry.id,
4131
- }), 'utf8').toString('base64url');
4300
+ }), "utf8").toString("base64url");
4132
4301
  }
4133
4302
  decodeFetchWorkspacesCursor(cursor, sort) {
4134
4303
  let parsed;
4135
4304
  try {
4136
- parsed = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
4305
+ parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
4137
4306
  }
4138
4307
  catch {
4139
- throw new SessionRequestError('invalid_cursor', 'Invalid fetch_workspaces cursor');
4308
+ throw new SessionRequestError("invalid_cursor", "Invalid fetch_workspaces cursor");
4140
4309
  }
4141
- if (!parsed || typeof parsed !== 'object') {
4142
- throw new SessionRequestError('invalid_cursor', 'Invalid fetch_workspaces cursor');
4310
+ if (!parsed || typeof parsed !== "object") {
4311
+ throw new SessionRequestError("invalid_cursor", "Invalid fetch_workspaces cursor");
4143
4312
  }
4144
4313
  const payload = parsed;
4145
- if (!Array.isArray(payload.sort) || typeof payload.id !== 'string') {
4146
- throw new SessionRequestError('invalid_cursor', 'Invalid fetch_workspaces cursor');
4314
+ if (!Array.isArray(payload.sort) || typeof payload.id !== "string") {
4315
+ throw new SessionRequestError("invalid_cursor", "Invalid fetch_workspaces cursor");
4147
4316
  }
4148
- if (!payload.values || typeof payload.values !== 'object') {
4149
- throw new SessionRequestError('invalid_cursor', 'Invalid fetch_workspaces cursor');
4317
+ if (!payload.values || typeof payload.values !== "object") {
4318
+ throw new SessionRequestError("invalid_cursor", "Invalid fetch_workspaces cursor");
4150
4319
  }
4151
4320
  const cursorSort = [];
4152
4321
  for (const item of payload.sort) {
4153
4322
  if (!item ||
4154
- typeof item !== 'object' ||
4155
- typeof item.key !== 'string' ||
4156
- typeof item.direction !== 'string') {
4157
- throw new SessionRequestError('invalid_cursor', 'Invalid fetch_workspaces cursor');
4323
+ typeof item !== "object" ||
4324
+ typeof item.key !== "string" ||
4325
+ typeof item.direction !== "string") {
4326
+ throw new SessionRequestError("invalid_cursor", "Invalid fetch_workspaces cursor");
4158
4327
  }
4159
4328
  const key = item.key;
4160
4329
  const direction = item.direction;
4161
- if ((key !== 'status_priority' &&
4162
- key !== 'activity_at' &&
4163
- key !== 'name' &&
4164
- key !== 'project_id') ||
4165
- (direction !== 'asc' && direction !== 'desc')) {
4166
- throw new SessionRequestError('invalid_cursor', 'Invalid fetch_workspaces cursor');
4330
+ if ((key !== "status_priority" &&
4331
+ key !== "activity_at" &&
4332
+ key !== "name" &&
4333
+ key !== "project_id") ||
4334
+ (direction !== "asc" && direction !== "desc")) {
4335
+ throw new SessionRequestError("invalid_cursor", "Invalid fetch_workspaces cursor");
4167
4336
  }
4168
4337
  cursorSort.push({ key, direction });
4169
4338
  }
4170
4339
  if (cursorSort.length !== sort.length ||
4171
4340
  cursorSort.some((entry, index) => entry.key !== sort[index]?.key || entry.direction !== sort[index]?.direction)) {
4172
- throw new SessionRequestError('invalid_cursor', 'fetch_workspaces cursor does not match current sort');
4341
+ throw new SessionRequestError("invalid_cursor", "fetch_workspaces cursor does not match current sort");
4173
4342
  }
4174
4343
  return {
4175
4344
  sort: cursorSort,
@@ -4185,7 +4354,7 @@ export class Session {
4185
4354
  if (base === 0) {
4186
4355
  continue;
4187
4356
  }
4188
- return spec.direction === 'asc' ? base : -base;
4357
+ return spec.direction === "asc" ? base : -base;
4189
4358
  }
4190
4359
  return workspace.id.localeCompare(cursor.id);
4191
4360
  }
@@ -4241,12 +4410,12 @@ export class Session {
4241
4410
  }
4242
4411
  bufferOrEmitWorkspaceUpdate(subscription, payload) {
4243
4412
  if (subscription.isBootstrapping) {
4244
- const workspaceId = payload.kind === 'upsert' ? payload.workspace.id : payload.id;
4413
+ const workspaceId = payload.kind === "upsert" ? payload.workspace.id : payload.id;
4245
4414
  subscription.pendingUpdatesByWorkspaceId.set(workspaceId, payload);
4246
4415
  return;
4247
4416
  }
4248
4417
  this.emit({
4249
- type: 'workspace_update',
4418
+ type: "workspace_update",
4250
4419
  payload,
4251
4420
  });
4252
4421
  }
@@ -4259,19 +4428,20 @@ export class Session {
4259
4428
  const pending = Array.from(subscription.pendingUpdatesByWorkspaceId.values());
4260
4429
  subscription.pendingUpdatesByWorkspaceId.clear();
4261
4430
  for (const payload of pending) {
4262
- if (payload.kind === 'upsert') {
4431
+ if (payload.kind === "upsert") {
4263
4432
  const snapshotLatestActivity = options?.snapshotLatestActivityByWorkspaceId?.get(payload.workspace.id);
4264
- if (typeof snapshotLatestActivity === 'number') {
4433
+ if (typeof snapshotLatestActivity === "number") {
4265
4434
  const updateLatestActivity = payload.workspace.activityAt
4266
4435
  ? Date.parse(payload.workspace.activityAt)
4267
4436
  : Number.NEGATIVE_INFINITY;
4268
- if (!Number.isNaN(updateLatestActivity) && updateLatestActivity <= snapshotLatestActivity) {
4437
+ if (!Number.isNaN(updateLatestActivity) &&
4438
+ updateLatestActivity <= snapshotLatestActivity) {
4269
4439
  continue;
4270
4440
  }
4271
4441
  }
4272
4442
  }
4273
4443
  this.emit({
4274
- type: 'workspace_update',
4444
+ type: "workspace_update",
4275
4445
  payload,
4276
4446
  });
4277
4447
  }
@@ -4280,19 +4450,62 @@ export class Session {
4280
4450
  const workspaceId = normalizePersistedWorkspaceId(cwd);
4281
4451
  return (await this.reconcileWorkspaceRecord(workspaceId)).workspace;
4282
4452
  }
4453
+ async registerPendingWorktreeWorkspace(options) {
4454
+ const workspaceId = normalizePersistedWorkspaceId(options.worktreePath);
4455
+ const basePlacement = await this.buildProjectPlacement(options.repoRoot);
4456
+ const placement = {
4457
+ ...basePlacement,
4458
+ checkout: {
4459
+ cwd: workspaceId,
4460
+ isGit: true,
4461
+ currentBranch: options.branchName,
4462
+ remoteUrl: basePlacement.checkout.remoteUrl,
4463
+ isPaseoOwnedWorktree: true,
4464
+ mainRepoRoot: options.repoRoot,
4465
+ },
4466
+ };
4467
+ const now = new Date().toISOString();
4468
+ const existingWorkspace = await this.workspaceRegistry.get(workspaceId);
4469
+ const existingProject = await this.projectRegistry.get(placement.projectKey);
4470
+ const nextProjectRecord = this.buildPersistedProjectRecord({
4471
+ workspaceId,
4472
+ placement,
4473
+ createdAt: existingProject?.createdAt ?? now,
4474
+ updatedAt: now,
4475
+ });
4476
+ const nextWorkspaceRecord = this.buildPersistedWorkspaceRecord({
4477
+ workspaceId,
4478
+ placement,
4479
+ createdAt: existingWorkspace?.createdAt ?? now,
4480
+ updatedAt: now,
4481
+ });
4482
+ await this.projectRegistry.upsert(nextProjectRecord);
4483
+ await this.workspaceRegistry.upsert(nextWorkspaceRecord);
4484
+ await this.syncWorkspaceGitWatchTarget(workspaceId, {
4485
+ isGit: placement.checkout.isGit,
4486
+ });
4487
+ if (existingWorkspace &&
4488
+ !existingWorkspace.archivedAt &&
4489
+ existingWorkspace.projectId !== nextWorkspaceRecord.projectId) {
4490
+ await this.archiveProjectRecordIfEmpty(existingWorkspace.projectId, now);
4491
+ }
4492
+ return nextWorkspaceRecord;
4493
+ }
4283
4494
  async archiveWorkspaceRecord(workspaceId, archivedAt) {
4284
4495
  const existing = await this.workspaceRegistry.get(workspaceId);
4285
4496
  if (!existing || existing.archivedAt) {
4497
+ this.removeWorkspaceGitWatchTarget(workspaceId);
4286
4498
  return;
4287
4499
  }
4288
4500
  const nextArchivedAt = archivedAt ?? new Date().toISOString();
4289
4501
  await this.workspaceRegistry.archive(workspaceId, nextArchivedAt);
4502
+ this.removeWorkspaceGitWatchTarget(workspaceId);
4290
4503
  const siblingWorkspaces = (await this.workspaceRegistry.list()).filter((workspace) => workspace.projectId === existing.projectId && !workspace.archivedAt);
4291
4504
  if (siblingWorkspaces.length === 0) {
4292
4505
  await this.projectRegistry.archive(existing.projectId, nextArchivedAt);
4293
4506
  }
4294
4507
  }
4295
- async emitWorkspaceUpdateForCwd(cwd) {
4508
+ async emitWorkspaceUpdateForCwd(cwd, options) {
4296
4509
  const subscription = this.workspaceUpdatesSubscription;
4297
4510
  if (!subscription) {
4298
4511
  return;
@@ -4304,16 +4517,24 @@ export class Session {
4304
4517
  const workspaceIdsToEmit = new Set([workspaceId, ...changedWorkspaceIds]);
4305
4518
  for (const nextWorkspaceId of workspaceIdsToEmit) {
4306
4519
  const workspace = descriptorsByWorkspaceId.get(nextWorkspaceId);
4307
- if (!workspace || !this.matchesWorkspaceFilter({ workspace, filter: subscription.filter })) {
4520
+ const nextWorkspace = workspace && this.matchesWorkspaceFilter({ workspace, filter: subscription.filter })
4521
+ ? workspace
4522
+ : null;
4523
+ if (options?.dedupeGitState &&
4524
+ this.shouldSkipWorkspaceGitWatchUpdate(nextWorkspaceId, nextWorkspace)) {
4525
+ continue;
4526
+ }
4527
+ this.rememberWorkspaceGitWatchFingerprint(nextWorkspaceId, nextWorkspace);
4528
+ if (!nextWorkspace) {
4308
4529
  this.bufferOrEmitWorkspaceUpdate(subscription, {
4309
- kind: 'remove',
4530
+ kind: "remove",
4310
4531
  id: nextWorkspaceId,
4311
4532
  });
4312
4533
  continue;
4313
4534
  }
4314
4535
  this.bufferOrEmitWorkspaceUpdate(subscription, {
4315
- kind: 'upsert',
4316
- workspace,
4536
+ kind: "upsert",
4537
+ workspace: nextWorkspace,
4317
4538
  });
4318
4539
  }
4319
4540
  }
@@ -4335,16 +4556,20 @@ export class Session {
4335
4556
  const descriptorsByWorkspaceId = new Map(all.map((entry) => [entry.id, entry]));
4336
4557
  for (const workspaceId of uniqueWorkspaceCwds) {
4337
4558
  const workspace = descriptorsByWorkspaceId.get(workspaceId);
4338
- if (!workspace || !this.matchesWorkspaceFilter({ workspace, filter: subscription.filter })) {
4559
+ const nextWorkspace = workspace && this.matchesWorkspaceFilter({ workspace, filter: subscription.filter })
4560
+ ? workspace
4561
+ : null;
4562
+ this.rememberWorkspaceGitWatchFingerprint(workspaceId, nextWorkspace);
4563
+ if (!nextWorkspace) {
4339
4564
  this.bufferOrEmitWorkspaceUpdate(subscription, {
4340
- kind: 'remove',
4565
+ kind: "remove",
4341
4566
  id: workspaceId,
4342
4567
  });
4343
4568
  continue;
4344
4569
  }
4345
4570
  this.bufferOrEmitWorkspaceUpdate(subscription, {
4346
- kind: 'upsert',
4347
- workspace,
4571
+ kind: "upsert",
4572
+ workspace: nextWorkspace,
4348
4573
  });
4349
4574
  }
4350
4575
  }
@@ -4373,7 +4598,7 @@ export class Session {
4373
4598
  }
4374
4599
  }
4375
4600
  this.emit({
4376
- type: 'fetch_agents_response',
4601
+ type: "fetch_agents_response",
4377
4602
  payload: {
4378
4603
  requestId: request.requestId,
4379
4604
  ...(subscriptionId ? { subscriptionId } : {}),
@@ -4388,11 +4613,11 @@ export class Session {
4388
4613
  if (subscriptionId && this.agentUpdatesSubscription?.subscriptionId === subscriptionId) {
4389
4614
  this.agentUpdatesSubscription = null;
4390
4615
  }
4391
- const code = error instanceof SessionRequestError ? error.code : 'fetch_agents_failed';
4392
- const message = error instanceof Error ? error.message : 'Failed to fetch agents';
4393
- this.sessionLogger.error({ err: error }, 'Failed to handle fetch_agents_request');
4616
+ const code = error instanceof SessionRequestError ? error.code : "fetch_agents_failed";
4617
+ const message = error instanceof Error ? error.message : "Failed to fetch agents";
4618
+ this.sessionLogger.error({ err: error }, "Failed to handle fetch_agents_request");
4394
4619
  this.emit({
4395
- type: 'rpc_error',
4620
+ type: "rpc_error",
4396
4621
  payload: {
4397
4622
  requestId: request.requestId,
4398
4623
  requestType: request.type,
@@ -4419,6 +4644,7 @@ export class Session {
4419
4644
  };
4420
4645
  }
4421
4646
  const payload = await this.listFetchWorkspacesEntries(request);
4647
+ this.primeWorkspaceGitWatchFingerprints(payload.entries);
4422
4648
  const snapshotLatestActivityByWorkspaceId = new Map();
4423
4649
  for (const entry of payload.entries) {
4424
4650
  const parsedLatestActivity = entry.activityAt
@@ -4429,7 +4655,7 @@ export class Session {
4429
4655
  }
4430
4656
  }
4431
4657
  this.emit({
4432
- type: 'fetch_workspaces_response',
4658
+ type: "fetch_workspaces_response",
4433
4659
  payload: {
4434
4660
  requestId: request.requestId,
4435
4661
  ...(subscriptionId ? { subscriptionId } : {}),
@@ -4444,11 +4670,11 @@ export class Session {
4444
4670
  if (subscriptionId && this.workspaceUpdatesSubscription?.subscriptionId === subscriptionId) {
4445
4671
  this.workspaceUpdatesSubscription = null;
4446
4672
  }
4447
- const code = error instanceof SessionRequestError ? error.code : 'fetch_workspaces_failed';
4448
- const message = error instanceof Error ? error.message : 'Failed to fetch workspaces';
4449
- this.sessionLogger.error({ err: error }, 'Failed to handle fetch_workspaces_request');
4673
+ const code = error instanceof SessionRequestError ? error.code : "fetch_workspaces_failed";
4674
+ const message = error instanceof Error ? error.message : "Failed to fetch workspaces";
4675
+ this.sessionLogger.error({ err: error }, "Failed to handle fetch_workspaces_request");
4450
4676
  this.emit({
4451
- type: 'rpc_error',
4677
+ type: "rpc_error",
4452
4678
  payload: {
4453
4679
  requestId: request.requestId,
4454
4680
  requestType: request.type,
@@ -4464,7 +4690,7 @@ export class Session {
4464
4690
  await this.emitWorkspaceUpdateForCwd(workspace.cwd);
4465
4691
  const descriptor = await this.describeWorkspaceRecord(workspace);
4466
4692
  this.emit({
4467
- type: 'open_project_response',
4693
+ type: "open_project_response",
4468
4694
  payload: {
4469
4695
  requestId: request.requestId,
4470
4696
  workspace: descriptor,
@@ -4473,10 +4699,10 @@ export class Session {
4473
4699
  });
4474
4700
  }
4475
4701
  catch (error) {
4476
- const message = error instanceof Error ? error.message : 'Failed to open project';
4477
- this.sessionLogger.error({ err: error, cwd: request.cwd }, 'Failed to open project');
4702
+ const message = error instanceof Error ? error.message : "Failed to open project";
4703
+ this.sessionLogger.error({ err: error, cwd: request.cwd }, "Failed to open project");
4478
4704
  this.emit({
4479
- type: 'open_project_response',
4705
+ type: "open_project_response",
4480
4706
  payload: {
4481
4707
  requestId: request.requestId,
4482
4708
  workspace: null,
@@ -4485,20 +4711,123 @@ export class Session {
4485
4711
  });
4486
4712
  }
4487
4713
  }
4714
+ async handleCreatePaseoWorktreeRequest(request) {
4715
+ try {
4716
+ const checkout = await getCheckoutStatusLite(request.cwd, { paseoHome: this.paseoHome });
4717
+ if (!checkout.isGit) {
4718
+ throw new Error("Create worktree requires a git repository");
4719
+ }
4720
+ const repoRoot = checkout.isPaseoOwnedWorktree ? checkout.mainRepoRoot : request.cwd;
4721
+ const baseBranch = await resolveRepositoryDefaultBranch(repoRoot);
4722
+ if (!baseBranch) {
4723
+ throw new Error("Unable to resolve repository default branch");
4724
+ }
4725
+ const normalizedSlug = request.worktreeSlug ? slugify(request.worktreeSlug) : uuidv4();
4726
+ const validation = validateBranchSlug(normalizedSlug);
4727
+ if (!validation.valid) {
4728
+ throw new Error(`Invalid worktree name: ${validation.error}`);
4729
+ }
4730
+ const worktreePath = await computeWorktreePath(repoRoot, normalizedSlug, this.paseoHome);
4731
+ const workspace = await this.registerPendingWorktreeWorkspace({
4732
+ repoRoot,
4733
+ worktreePath,
4734
+ branchName: normalizedSlug,
4735
+ });
4736
+ const descriptor = await this.describeWorkspaceRecord(workspace);
4737
+ this.emit({
4738
+ type: "create_paseo_worktree_response",
4739
+ payload: {
4740
+ workspace: descriptor,
4741
+ error: null,
4742
+ setupTerminalId: null,
4743
+ requestId: request.requestId,
4744
+ },
4745
+ });
4746
+ void this.createPaseoWorktreeInBackground({
4747
+ requestCwd: request.cwd,
4748
+ repoRoot,
4749
+ baseBranch,
4750
+ slug: normalizedSlug,
4751
+ worktreePath,
4752
+ });
4753
+ }
4754
+ catch (error) {
4755
+ const message = error instanceof Error ? error.message : "Failed to create worktree";
4756
+ this.sessionLogger.error({ err: error, cwd: request.cwd, worktreeSlug: request.worktreeSlug }, "Failed to create worktree");
4757
+ this.emit({
4758
+ type: "create_paseo_worktree_response",
4759
+ payload: {
4760
+ workspace: null,
4761
+ error: message,
4762
+ setupTerminalId: null,
4763
+ requestId: request.requestId,
4764
+ },
4765
+ });
4766
+ }
4767
+ }
4768
+ async createPaseoWorktreeInBackground(options) {
4769
+ let setupTerminalId = null;
4770
+ try {
4771
+ await createAgentWorktree({
4772
+ cwd: options.repoRoot,
4773
+ branchName: options.slug,
4774
+ baseBranch: options.baseBranch,
4775
+ worktreeSlug: options.slug,
4776
+ paseoHome: this.paseoHome,
4777
+ });
4778
+ const setupCommands = getWorktreeSetupCommands(options.worktreePath);
4779
+ if (setupCommands.length > 0 && this.terminalManager) {
4780
+ const runtimeEnv = await resolveWorktreeRuntimeEnv({
4781
+ worktreePath: options.worktreePath,
4782
+ branchName: options.slug,
4783
+ repoRootPath: options.repoRoot,
4784
+ });
4785
+ this.terminalManager.registerCwdEnv({
4786
+ cwd: options.worktreePath,
4787
+ env: runtimeEnv,
4788
+ });
4789
+ const terminal = await this.terminalManager.createTerminal({
4790
+ cwd: options.worktreePath,
4791
+ name: `setup-${options.slug}`,
4792
+ env: runtimeEnv,
4793
+ });
4794
+ setupTerminalId = terminal.id;
4795
+ for (const command of setupCommands) {
4796
+ terminal.send({
4797
+ type: "input",
4798
+ data: `${command}\r`,
4799
+ });
4800
+ }
4801
+ }
4802
+ }
4803
+ catch (error) {
4804
+ this.sessionLogger.error({
4805
+ err: error,
4806
+ cwd: options.requestCwd,
4807
+ repoRoot: options.repoRoot,
4808
+ worktreeSlug: options.slug,
4809
+ worktreePath: options.worktreePath,
4810
+ setupTerminalId,
4811
+ }, "Background worktree creation failed");
4812
+ }
4813
+ finally {
4814
+ await this.emitWorkspaceUpdateForCwd(options.worktreePath);
4815
+ }
4816
+ }
4488
4817
  async handleArchiveWorkspaceRequest(request) {
4489
4818
  try {
4490
4819
  const existing = await this.workspaceRegistry.get(request.workspaceId);
4491
4820
  if (!existing) {
4492
4821
  throw new Error(`Workspace not found: ${request.workspaceId}`);
4493
4822
  }
4494
- if (existing.kind === 'worktree') {
4495
- throw new Error('Use worktree archive for Paseo worktrees');
4823
+ if (existing.kind === "worktree") {
4824
+ throw new Error("Use worktree archive for Paseo worktrees");
4496
4825
  }
4497
4826
  const archivedAt = new Date().toISOString();
4498
4827
  await this.archiveWorkspaceRecord(request.workspaceId, archivedAt);
4499
4828
  await this.emitWorkspaceUpdateForCwd(existing.cwd);
4500
4829
  this.emit({
4501
- type: 'archive_workspace_response',
4830
+ type: "archive_workspace_response",
4502
4831
  payload: {
4503
4832
  requestId: request.requestId,
4504
4833
  workspaceId: request.workspaceId,
@@ -4508,10 +4837,10 @@ export class Session {
4508
4837
  });
4509
4838
  }
4510
4839
  catch (error) {
4511
- const message = error instanceof Error ? error.message : 'Failed to archive workspace';
4512
- this.sessionLogger.error({ err: error, workspaceId: request.workspaceId }, 'Failed to archive workspace');
4840
+ const message = error instanceof Error ? error.message : "Failed to archive workspace";
4841
+ this.sessionLogger.error({ err: error, workspaceId: request.workspaceId }, "Failed to archive workspace");
4513
4842
  this.emit({
4514
- type: 'archive_workspace_response',
4843
+ type: "archive_workspace_response",
4515
4844
  payload: {
4516
4845
  requestId: request.requestId,
4517
4846
  workspaceId: request.workspaceId,
@@ -4525,7 +4854,7 @@ export class Session {
4525
4854
  const resolved = await this.resolveAgentIdentifier(agentIdOrIdentifier);
4526
4855
  if (!resolved.ok) {
4527
4856
  this.emit({
4528
- type: 'fetch_agent_response',
4857
+ type: "fetch_agent_response",
4529
4858
  payload: { requestId, agent: null, project: null, error: resolved.error },
4530
4859
  });
4531
4860
  return;
@@ -4533,7 +4862,7 @@ export class Session {
4533
4862
  const agent = await this.getAgentPayloadById(resolved.agentId);
4534
4863
  if (!agent) {
4535
4864
  this.emit({
4536
- type: 'fetch_agent_response',
4865
+ type: "fetch_agent_response",
4537
4866
  payload: {
4538
4867
  requestId,
4539
4868
  agent: null,
@@ -4545,18 +4874,18 @@ export class Session {
4545
4874
  }
4546
4875
  const project = await this.buildProjectPlacement(agent.cwd);
4547
4876
  this.emit({
4548
- type: 'fetch_agent_response',
4877
+ type: "fetch_agent_response",
4549
4878
  payload: { requestId, agent, project, error: null },
4550
4879
  });
4551
4880
  }
4552
4881
  async handleFetchAgentTimelineRequest(msg) {
4553
- const direction = msg.direction ?? (msg.cursor ? 'after' : 'tail');
4554
- const projection = msg.projection ?? 'projected';
4882
+ const direction = msg.direction ?? (msg.cursor ? "after" : "tail");
4883
+ const projection = msg.projection ?? "projected";
4555
4884
  const requestedLimit = msg.limit;
4556
- const limit = requestedLimit ?? (direction === 'after' ? 0 : undefined);
4557
- const shouldLimitByProjectedWindow = projection === 'canonical' &&
4558
- direction === 'tail' &&
4559
- typeof requestedLimit === 'number' &&
4885
+ const limit = requestedLimit ?? (direction === "after" ? 0 : undefined);
4886
+ const shouldLimitByProjectedWindow = projection === "canonical" &&
4887
+ direction === "tail" &&
4888
+ typeof requestedLimit === "number" &&
4560
4889
  requestedLimit > 0;
4561
4890
  const cursor = msg.cursor
4562
4891
  ? {
@@ -4570,7 +4899,7 @@ export class Session {
4570
4899
  let timeline = this.agentManager.fetchTimeline(msg.agentId, {
4571
4900
  direction,
4572
4901
  cursor,
4573
- limit: shouldLimitByProjectedWindow && typeof requestedLimit === 'number'
4902
+ limit: shouldLimitByProjectedWindow && typeof requestedLimit === "number"
4574
4903
  ? Math.max(1, Math.floor(requestedLimit))
4575
4904
  : limit,
4576
4905
  });
@@ -4596,7 +4925,7 @@ export class Session {
4596
4925
  const startsAtLoadedBoundary = firstLoadedRow != null &&
4597
4926
  firstSelectedRow != null &&
4598
4927
  firstSelectedRow.seq === firstLoadedRow.seq;
4599
- const boundaryIsAssistantChunk = startsAtLoadedBoundary && firstLoadedRow.item.type === 'assistant_message';
4928
+ const boundaryIsAssistantChunk = startsAtLoadedBoundary && firstLoadedRow.item.type === "assistant_message";
4600
4929
  if (!needsMoreProjectedEntries && !boundaryIsAssistantChunk) {
4601
4930
  break;
4602
4931
  }
@@ -4636,7 +4965,7 @@ export class Session {
4636
4965
  entries = projectTimelineRows(timeline.rows, snapshot.provider, projection);
4637
4966
  }
4638
4967
  this.emit({
4639
- type: 'fetch_agent_timeline_response',
4968
+ type: "fetch_agent_timeline_response",
4640
4969
  payload: {
4641
4970
  requestId: msg.requestId,
4642
4971
  agentId: msg.agentId,
@@ -4658,16 +4987,16 @@ export class Session {
4658
4987
  });
4659
4988
  }
4660
4989
  catch (error) {
4661
- this.sessionLogger.error({ err: error, agentId: msg.agentId }, 'Failed to handle fetch_agent_timeline_request');
4990
+ this.sessionLogger.error({ err: error, agentId: msg.agentId }, "Failed to handle fetch_agent_timeline_request");
4662
4991
  this.emit({
4663
- type: 'fetch_agent_timeline_response',
4992
+ type: "fetch_agent_timeline_response",
4664
4993
  payload: {
4665
4994
  requestId: msg.requestId,
4666
4995
  agentId: msg.agentId,
4667
4996
  agent: null,
4668
4997
  direction,
4669
4998
  projection,
4670
- epoch: '',
4999
+ epoch: "",
4671
5000
  reset: false,
4672
5001
  staleCursor: false,
4673
5002
  gap: false,
@@ -4686,7 +5015,7 @@ export class Session {
4686
5015
  const resolved = await this.resolveAgentIdentifier(msg.agentId);
4687
5016
  if (!resolved.ok) {
4688
5017
  this.emit({
4689
- type: 'send_agent_message_response',
5018
+ type: "send_agent_message_response",
4690
5019
  payload: {
4691
5020
  requestId: msg.requestId,
4692
5021
  agentId: msg.agentId,
@@ -4707,13 +5036,13 @@ export class Session {
4707
5036
  });
4708
5037
  }
4709
5038
  catch (error) {
4710
- this.sessionLogger.error({ err: error, agentId }, 'Failed to record user message for send_agent_message_request');
5039
+ this.sessionLogger.error({ err: error, agentId }, "Failed to record user message for send_agent_message_request");
4711
5040
  }
4712
5041
  const prompt = this.buildAgentPrompt(msg.text, msg.images);
4713
5042
  const started = this.startAgentStream(agentId, prompt);
4714
5043
  if (!started.ok) {
4715
5044
  this.emit({
4716
- type: 'send_agent_message_response',
5045
+ type: "send_agent_message_response",
4717
5046
  payload: {
4718
5047
  requestId: msg.requestId,
4719
5048
  agentId,
@@ -4725,18 +5054,18 @@ export class Session {
4725
5054
  }
4726
5055
  const startAbort = new AbortController();
4727
5056
  const startTimeoutMs = 15000;
4728
- const startTimeout = setTimeout(() => startAbort.abort('timeout'), startTimeoutMs);
5057
+ const startTimeout = setTimeout(() => startAbort.abort("timeout"), startTimeoutMs);
4729
5058
  try {
4730
5059
  await this.agentManager.waitForAgentRunStart(agentId, { signal: startAbort.signal });
4731
5060
  }
4732
5061
  catch (error) {
4733
5062
  const message = error instanceof Error
4734
5063
  ? error.message
4735
- : typeof error === 'string'
5064
+ : typeof error === "string"
4736
5065
  ? error
4737
- : 'Unknown error';
5066
+ : "Unknown error";
4738
5067
  this.emit({
4739
- type: 'send_agent_message_response',
5068
+ type: "send_agent_message_response",
4740
5069
  payload: {
4741
5070
  requestId: msg.requestId,
4742
5071
  agentId,
@@ -4750,7 +5079,7 @@ export class Session {
4750
5079
  clearTimeout(startTimeout);
4751
5080
  }
4752
5081
  this.emit({
4753
- type: 'send_agent_message_response',
5082
+ type: "send_agent_message_response",
4754
5083
  payload: {
4755
5084
  requestId: msg.requestId,
4756
5085
  agentId,
@@ -4760,9 +5089,13 @@ export class Session {
4760
5089
  });
4761
5090
  }
4762
5091
  catch (error) {
4763
- const message = error instanceof Error ? error.message : typeof error === 'string' ? error : 'Unknown error';
5092
+ const message = error instanceof Error
5093
+ ? error.message
5094
+ : typeof error === "string"
5095
+ ? error
5096
+ : "Unknown error";
4764
5097
  this.emit({
4765
- type: 'send_agent_message_response',
5098
+ type: "send_agent_message_response",
4766
5099
  payload: {
4767
5100
  requestId: msg.requestId,
4768
5101
  agentId: resolved.agentId,
@@ -4776,10 +5109,10 @@ export class Session {
4776
5109
  const resolved = await this.resolveAgentIdentifier(agentIdOrIdentifier);
4777
5110
  if (!resolved.ok) {
4778
5111
  this.emit({
4779
- type: 'wait_for_finish_response',
5112
+ type: "wait_for_finish_response",
4780
5113
  payload: {
4781
5114
  requestId,
4782
- status: 'error',
5115
+ status: "error",
4783
5116
  final: null,
4784
5117
  error: resolved.error,
4785
5118
  lastMessage: null,
@@ -4793,10 +5126,10 @@ export class Session {
4793
5126
  const record = await this.agentStorage.get(agentId);
4794
5127
  if (!record || record.internal) {
4795
5128
  this.emit({
4796
- type: 'wait_for_finish_response',
5129
+ type: "wait_for_finish_response",
4797
5130
  payload: {
4798
5131
  requestId,
4799
- status: 'error',
5132
+ status: "error",
4800
5133
  final: null,
4801
5134
  error: `Agent not found: ${agentId}`,
4802
5135
  lastMessage: null,
@@ -4805,22 +5138,22 @@ export class Session {
4805
5138
  return;
4806
5139
  }
4807
5140
  const final = this.buildStoredAgentPayload(record);
4808
- const status = record.attentionReason === 'permission'
4809
- ? 'permission'
4810
- : record.lastStatus === 'error'
4811
- ? 'error'
4812
- : 'idle';
5141
+ const status = record.attentionReason === "permission"
5142
+ ? "permission"
5143
+ : record.lastStatus === "error"
5144
+ ? "error"
5145
+ : "idle";
4813
5146
  this.emit({
4814
- type: 'wait_for_finish_response',
5147
+ type: "wait_for_finish_response",
4815
5148
  payload: { requestId, status, final, error: null, lastMessage: null },
4816
5149
  });
4817
5150
  return;
4818
5151
  }
4819
5152
  const abortController = new AbortController();
4820
- const hasTimeout = typeof timeoutMs === 'number' && timeoutMs > 0;
5153
+ const hasTimeout = typeof timeoutMs === "number" && timeoutMs > 0;
4821
5154
  const timeoutHandle = hasTimeout
4822
5155
  ? setTimeout(() => {
4823
- abortController.abort('timeout');
5156
+ abortController.abort("timeout");
4824
5157
  }, timeoutMs)
4825
5158
  : null;
4826
5159
  try {
@@ -4831,28 +5164,32 @@ export class Session {
4831
5164
  if (!final) {
4832
5165
  throw new Error(`Agent ${agentId} disappeared while waiting`);
4833
5166
  }
4834
- let status = result.permission ? 'permission' : result.status === 'error' ? 'error' : 'idle';
5167
+ let status = result.permission
5168
+ ? "permission"
5169
+ : result.status === "error"
5170
+ ? "error"
5171
+ : "idle";
4835
5172
  this.emit({
4836
- type: 'wait_for_finish_response',
5173
+ type: "wait_for_finish_response",
4837
5174
  payload: { requestId, status, final, error: null, lastMessage: result.lastMessage },
4838
5175
  });
4839
5176
  }
4840
5177
  catch (error) {
4841
5178
  const isAbort = error instanceof Error &&
4842
- (error.name === 'AbortError' || error.message.toLowerCase().includes('aborted'));
5179
+ (error.name === "AbortError" || error.message.toLowerCase().includes("aborted"));
4843
5180
  if (!isAbort) {
4844
5181
  const message = error instanceof Error
4845
5182
  ? error.message
4846
- : typeof error === 'string'
5183
+ : typeof error === "string"
4847
5184
  ? error
4848
- : 'Unknown error';
4849
- this.sessionLogger.error({ err: error, agentId }, 'wait_for_finish_request failed');
5185
+ : "Unknown error";
5186
+ this.sessionLogger.error({ err: error, agentId }, "wait_for_finish_request failed");
4850
5187
  const final = await this.getAgentPayloadById(agentId);
4851
5188
  this.emit({
4852
- type: 'wait_for_finish_response',
5189
+ type: "wait_for_finish_response",
4853
5190
  payload: {
4854
5191
  requestId,
4855
- status: 'error',
5192
+ status: "error",
4856
5193
  final,
4857
5194
  error: message,
4858
5195
  lastMessage: null,
@@ -4865,8 +5202,8 @@ export class Session {
4865
5202
  throw new Error(`Agent ${agentId} disappeared while waiting`);
4866
5203
  }
4867
5204
  this.emit({
4868
- type: 'wait_for_finish_response',
4869
- payload: { requestId, status: 'timeout', final, error: null, lastMessage: null },
5205
+ type: "wait_for_finish_response",
5206
+ payload: { requestId, status: "timeout", final, error: null, lastMessage: null },
4870
5207
  });
4871
5208
  }
4872
5209
  finally {
@@ -4880,31 +5217,30 @@ export class Session {
4880
5217
  */
4881
5218
  async handleAudioChunk(msg) {
4882
5219
  if (!this.isVoiceMode) {
4883
- this.sessionLogger.warn('Received voice_audio_chunk while voice mode is disabled; transcript will be emitted but voice assistant turn is skipped');
5220
+ this.sessionLogger.warn("Received voice_audio_chunk while voice mode is disabled; transcript will be emitted but voice assistant turn is skipped");
4884
5221
  }
4885
- const chunkFormat = msg.format || 'audio/wav';
5222
+ const chunkFormat = msg.format || "audio/wav";
4886
5223
  if (this.isVoiceMode) {
4887
5224
  if (!this.voiceTurnController) {
4888
- throw new Error('Voice mode is enabled but the voice turn controller is not running');
5225
+ throw new Error("Voice mode is enabled but the voice turn controller is not running");
4889
5226
  }
4890
- const chunkBytes = Buffer.byteLength(msg.audio, 'base64');
5227
+ const chunkBytes = Buffer.byteLength(msg.audio, "base64");
4891
5228
  this.voiceInputChunkCount += 1;
4892
5229
  this.voiceInputBytes += chunkBytes;
4893
5230
  if (this.voiceInputChunkCount === 1) {
4894
5231
  this.sessionLogger.info({
4895
5232
  format: chunkFormat,
4896
5233
  audioBytes: chunkBytes,
4897
- }, 'Received first voice_audio_chunk for active voice mode');
5234
+ }, "Received first voice_audio_chunk for active voice mode");
4898
5235
  }
4899
5236
  const now = Date.now();
4900
- if (this.voiceInputChunkCount % 50 === 0 ||
4901
- now - this.voiceInputWindowStartedAt >= 1000) {
5237
+ if (this.voiceInputChunkCount % 50 === 0 || now - this.voiceInputWindowStartedAt >= 1000) {
4902
5238
  this.sessionLogger.info({
4903
5239
  chunkCount: this.voiceInputChunkCount,
4904
5240
  audioBytes: this.voiceInputBytes,
4905
5241
  windowMs: now - this.voiceInputWindowStartedAt,
4906
5242
  format: chunkFormat,
4907
- }, 'Voice input chunk summary');
5243
+ }, "Voice input chunk summary");
4908
5244
  this.voiceInputWindowStartedAt = now;
4909
5245
  this.voiceInputChunkCount = 0;
4910
5246
  this.voiceInputBytes = 0;
@@ -4915,8 +5251,8 @@ export class Session {
4915
5251
  });
4916
5252
  return;
4917
5253
  }
4918
- const chunkBuffer = Buffer.from(msg.audio, 'base64');
4919
- const isPCMChunk = chunkFormat.toLowerCase().includes('pcm');
5254
+ const chunkBuffer = Buffer.from(msg.audio, "base64");
5255
+ const isPCMChunk = chunkFormat.toLowerCase().includes("pcm");
4920
5256
  if (!this.audioBuffer) {
4921
5257
  this.audioBuffer = {
4922
5258
  chunks: [],
@@ -4928,7 +5264,7 @@ export class Session {
4928
5264
  // If the format changes mid-stream, flush what we have first
4929
5265
  if (this.audioBuffer.isPCM !== isPCMChunk) {
4930
5266
  this.sessionLogger.debug({
4931
- oldFormat: this.audioBuffer.isPCM ? 'pcm' : this.audioBuffer.format,
5267
+ oldFormat: this.audioBuffer.isPCM ? "pcm" : this.audioBuffer.format,
4932
5268
  newFormat: chunkFormat,
4933
5269
  }, `Audio format changed mid-stream, flushing current buffer`);
4934
5270
  const finalized = this.finalizeBufferedAudio();
@@ -4984,7 +5320,7 @@ export class Session {
4984
5320
  const wavBuffer = convertPCMToWavBuffer(pcmBuffer, PCM_SAMPLE_RATE, PCM_CHANNELS, PCM_BITS_PER_SAMPLE);
4985
5321
  return {
4986
5322
  audio: wavBuffer,
4987
- format: 'audio/wav',
5323
+ format: "audio/wav",
4988
5324
  };
4989
5325
  }
4990
5326
  return {
@@ -4993,7 +5329,7 @@ export class Session {
4993
5329
  };
4994
5330
  }
4995
5331
  async processCompletedAudio(audio, format) {
4996
- if (this.processingPhase === 'transcribing') {
5332
+ if (this.processingPhase === "transcribing") {
4997
5333
  this.sessionLogger.debug({ phase: this.processingPhase, segmentCount: this.pendingAudioSegments.length + 1 }, `Buffering audio segment (phase: ${this.processingPhase})`);
4998
5334
  this.pendingAudioSegments.push({
4999
5335
  audio,
@@ -5019,7 +5355,7 @@ export class Session {
5019
5355
  await this.processAudio(audio, format);
5020
5356
  }
5021
5357
  async flushPendingAudioSegments(reason) {
5022
- if (this.processingPhase === 'transcribing' || this.pendingAudioSegments.length === 0) {
5358
+ if (this.processingPhase === "transcribing" || this.pendingAudioSegments.length === 0) {
5023
5359
  return;
5024
5360
  }
5025
5361
  const pendingSegments = [...this.pendingAudioSegments];
@@ -5034,21 +5370,21 @@ export class Session {
5034
5370
  * Process audio through STT and then LLM
5035
5371
  */
5036
5372
  async processAudio(audio, format) {
5037
- this.setPhase('transcribing');
5373
+ this.setPhase("transcribing");
5038
5374
  this.emit({
5039
- type: 'activity_log',
5375
+ type: "activity_log",
5040
5376
  payload: {
5041
5377
  id: uuidv4(),
5042
5378
  timestamp: new Date(),
5043
- type: 'system',
5044
- content: 'Transcribing audio...',
5379
+ type: "system",
5380
+ content: "Transcribing audio...",
5045
5381
  },
5046
5382
  });
5047
5383
  try {
5048
5384
  const requestId = uuidv4();
5049
5385
  const result = await this.sttManager.transcribe(audio, format, {
5050
5386
  requestId,
5051
- label: this.isVoiceMode ? 'voice' : 'buffered',
5387
+ label: this.isVoiceMode ? "voice" : "buffered",
5052
5388
  });
5053
5389
  const transcriptText = result.text.trim();
5054
5390
  this.sessionLogger.info({
@@ -5056,7 +5392,7 @@ export class Session {
5056
5392
  isVoiceMode: this.isVoiceMode,
5057
5393
  transcriptLength: transcriptText.length,
5058
5394
  transcript: transcriptText,
5059
- }, 'Transcription result');
5395
+ }, "Transcription result");
5060
5396
  await this.handleTranscriptionResultPayload({
5061
5397
  text: result.text,
5062
5398
  language: result.language,
@@ -5070,15 +5406,15 @@ export class Session {
5070
5406
  });
5071
5407
  }
5072
5408
  catch (error) {
5073
- this.setPhase('idle');
5074
- this.clearSpeechInProgress('transcription error');
5075
- await this.flushPendingAudioSegments('transcription error');
5409
+ this.setPhase("idle");
5410
+ this.clearSpeechInProgress("transcription error");
5411
+ await this.flushPendingAudioSegments("transcription error");
5076
5412
  this.emit({
5077
- type: 'activity_log',
5413
+ type: "activity_log",
5078
5414
  payload: {
5079
5415
  id: uuidv4(),
5080
5416
  timestamp: new Date(),
5081
- type: 'error',
5417
+ type: "error",
5082
5418
  content: `Transcription error: ${error.message}`,
5083
5419
  },
5084
5420
  });
@@ -5088,7 +5424,7 @@ export class Session {
5088
5424
  async handleTranscriptionResultPayload(result) {
5089
5425
  const transcriptText = result.text.trim();
5090
5426
  this.emit({
5091
- type: 'transcription_result',
5427
+ type: "transcription_result",
5092
5428
  payload: {
5093
5429
  text: result.text,
5094
5430
  ...(result.language ? { language: result.language } : {}),
@@ -5104,21 +5440,21 @@ export class Session {
5104
5440
  },
5105
5441
  });
5106
5442
  if (!transcriptText) {
5107
- this.sessionLogger.debug('Empty transcription (false positive), not aborting');
5108
- this.setPhase('idle');
5109
- this.clearSpeechInProgress('empty transcription');
5110
- await this.flushPendingAudioSegments('empty transcription');
5443
+ this.sessionLogger.debug("Empty transcription (false positive), not aborting");
5444
+ this.setPhase("idle");
5445
+ this.clearSpeechInProgress("empty transcription");
5446
+ await this.flushPendingAudioSegments("empty transcription");
5111
5447
  return;
5112
5448
  }
5113
5449
  // Has content - abort any in-progress stream now
5114
5450
  this.createAbortController();
5115
5451
  if (result.debugRecordingPath) {
5116
5452
  this.emit({
5117
- type: 'activity_log',
5453
+ type: "activity_log",
5118
5454
  payload: {
5119
5455
  id: uuidv4(),
5120
5456
  timestamp: new Date(),
5121
- type: 'system',
5457
+ type: "system",
5122
5458
  content: `Saved input audio: ${result.debugRecordingPath}`,
5123
5459
  metadata: {
5124
5460
  recordingPath: result.debugRecordingPath,
@@ -5129,11 +5465,11 @@ export class Session {
5129
5465
  });
5130
5466
  }
5131
5467
  this.emit({
5132
- type: 'activity_log',
5468
+ type: "activity_log",
5133
5469
  payload: {
5134
5470
  id: uuidv4(),
5135
5471
  timestamp: new Date(),
5136
- type: 'transcript',
5472
+ type: "transcript",
5137
5473
  content: result.text,
5138
5474
  metadata: {
5139
5475
  ...(result.language ? { language: result.language } : {}),
@@ -5141,23 +5477,23 @@ export class Session {
5141
5477
  },
5142
5478
  },
5143
5479
  });
5144
- this.clearSpeechInProgress('transcription complete');
5145
- this.setPhase('idle');
5480
+ this.clearSpeechInProgress("transcription complete");
5481
+ this.setPhase("idle");
5146
5482
  if (!this.isVoiceMode) {
5147
- this.sessionLogger.debug({ requestId: result.requestId }, 'Skipping voice agent processing because voice mode is disabled');
5148
- await this.flushPendingAudioSegments('voice mode disabled');
5483
+ this.sessionLogger.debug({ requestId: result.requestId }, "Skipping voice agent processing because voice mode is disabled");
5484
+ await this.flushPendingAudioSegments("voice mode disabled");
5149
5485
  return;
5150
5486
  }
5151
5487
  const agentId = this.voiceModeAgentId;
5152
5488
  if (!agentId) {
5153
- this.sessionLogger.warn({ requestId: result.requestId }, 'Skipping voice agent processing because no agent is currently voice-enabled');
5154
- await this.flushPendingAudioSegments('no active voice agent');
5489
+ this.sessionLogger.warn({ requestId: result.requestId }, "Skipping voice agent processing because no agent is currently voice-enabled");
5490
+ await this.flushPendingAudioSegments("no active voice agent");
5155
5491
  return;
5156
5492
  }
5157
5493
  // Route voice utterances through the same send path as regular text input:
5158
5494
  // interrupt-if-running, record message, then start a new stream.
5159
5495
  await this.handleSendAgentMessage(agentId, result.text);
5160
- await this.flushPendingAudioSegments('transcription complete');
5496
+ await this.flushPendingAudioSegments("transcription complete");
5161
5497
  }
5162
5498
  registerVoiceBridgeForAgent(agentId) {
5163
5499
  this.registerVoiceSpeakHandler?.(agentId, async ({ text, signal }) => {
@@ -5165,16 +5501,16 @@ export class Session {
5165
5501
  agentId,
5166
5502
  textLength: text.length,
5167
5503
  preview: text.slice(0, 160),
5168
- }, 'Voice speak tool call received by session handler');
5504
+ }, "Voice speak tool call received by session handler");
5169
5505
  const abortSignal = signal ?? this.abortController.signal;
5170
5506
  await this.ttsManager.generateAndWaitForPlayback(text, (msg) => this.emit(msg), abortSignal, true);
5171
- this.sessionLogger.info({ agentId, textLength: text.length }, 'Voice speak tool call finished playback');
5507
+ this.sessionLogger.info({ agentId, textLength: text.length }, "Voice speak tool call finished playback");
5172
5508
  this.emit({
5173
- type: 'activity_log',
5509
+ type: "activity_log",
5174
5510
  payload: {
5175
5511
  id: uuidv4(),
5176
5512
  timestamp: new Date(),
5177
- type: 'assistant',
5513
+ type: "assistant",
5178
5514
  content: text,
5179
5515
  },
5180
5516
  });
@@ -5191,24 +5527,24 @@ export class Session {
5191
5527
  async handleAbort() {
5192
5528
  this.sessionLogger.info({ phase: this.processingPhase }, `Abort request, phase: ${this.processingPhase}`);
5193
5529
  this.abortController.abort();
5194
- this.ttsManager.cancelPendingPlaybacks('abort request');
5530
+ this.ttsManager.cancelPendingPlaybacks("abort request");
5195
5531
  // Voice abort should always interrupt active agent output immediately.
5196
5532
  if (this.isVoiceMode && this.voiceModeAgentId) {
5197
5533
  try {
5198
5534
  await this.interruptAgentIfRunning(this.voiceModeAgentId);
5199
5535
  }
5200
5536
  catch (error) {
5201
- this.sessionLogger.warn({ err: error, agentId: this.voiceModeAgentId }, 'Failed to interrupt active voice-mode agent on abort');
5537
+ this.sessionLogger.warn({ err: error, agentId: this.voiceModeAgentId }, "Failed to interrupt active voice-mode agent on abort");
5202
5538
  }
5203
5539
  }
5204
- if (this.processingPhase === 'transcribing') {
5540
+ if (this.processingPhase === "transcribing") {
5205
5541
  // Still in STT phase - we'll buffer the next audio
5206
- this.sessionLogger.debug('Will buffer next audio (currently transcribing)');
5542
+ this.sessionLogger.debug("Will buffer next audio (currently transcribing)");
5207
5543
  // Phase stays as 'transcribing', handleAudioChunk will handle buffering
5208
5544
  return;
5209
5545
  }
5210
5546
  // Reset phase to idle and clear pending non-voice buffers.
5211
- this.setPhase('idle');
5547
+ this.setPhase("idle");
5212
5548
  this.pendingAudioSegments = [];
5213
5549
  this.clearBufferTimeout();
5214
5550
  }
@@ -5229,20 +5565,20 @@ export class Session {
5229
5565
  const phaseBeforeAbort = this.processingPhase;
5230
5566
  const hadActiveStream = this.hasActiveAgentRun(this.voiceModeAgentId);
5231
5567
  this.speechInProgress = true;
5232
- this.sessionLogger.debug('Voice speech detected – aborting playback and active agent run');
5568
+ this.sessionLogger.debug("Voice speech detected – aborting playback and active agent run");
5233
5569
  if (this.pendingAudioSegments.length > 0) {
5234
5570
  this.sessionLogger.debug({ segmentCount: this.pendingAudioSegments.length }, `Dropping ${this.pendingAudioSegments.length} buffered audio segment(s) due to voice speech`);
5235
5571
  this.pendingAudioSegments = [];
5236
5572
  }
5237
5573
  if (this.audioBuffer) {
5238
- this.sessionLogger.debug({ chunks: this.audioBuffer.chunks.length, pcmBytes: this.audioBuffer.totalPCMBytes }, `Clearing partial audio buffer (${this.audioBuffer.chunks.length} chunk(s)${this.audioBuffer.isPCM ? `, ${this.audioBuffer.totalPCMBytes} PCM bytes` : ''})`);
5574
+ this.sessionLogger.debug({ chunks: this.audioBuffer.chunks.length, pcmBytes: this.audioBuffer.totalPCMBytes }, `Clearing partial audio buffer (${this.audioBuffer.chunks.length} chunk(s)${this.audioBuffer.isPCM ? `, ${this.audioBuffer.totalPCMBytes} PCM bytes` : ""})`);
5239
5575
  this.audioBuffer = null;
5240
5576
  }
5241
5577
  this.clearBufferTimeout();
5242
5578
  this.abortController.abort();
5243
5579
  await this.handleAbort();
5244
5580
  const latencyMs = Date.now() - chunkReceivedAt;
5245
- this.sessionLogger.debug({ latencyMs, phaseBeforeAbort, hadActiveStream }, '[Telemetry] barge_in.llm_abort_latency');
5581
+ this.sessionLogger.debug({ latencyMs, phaseBeforeAbort, hadActiveStream }, "[Telemetry] barge_in.llm_abort_latency");
5246
5582
  }
5247
5583
  /**
5248
5584
  * Clear speech-in-progress flag once the user turn has completed
@@ -5277,9 +5613,9 @@ export class Session {
5277
5613
  setBufferTimeout() {
5278
5614
  this.clearBufferTimeout();
5279
5615
  this.bufferTimeout = setTimeout(async () => {
5280
- this.sessionLogger.debug('Buffer timeout reached, processing pending segments');
5281
- if (this.processingPhase === 'transcribing') {
5282
- this.sessionLogger.debug({ segmentCount: this.pendingAudioSegments.length }, 'Buffer timeout deferred because transcription is still in progress');
5616
+ this.sessionLogger.debug("Buffer timeout reached, processing pending segments");
5617
+ if (this.processingPhase === "transcribing") {
5618
+ this.sessionLogger.debug({ segmentCount: this.pendingAudioSegments.length }, "Buffer timeout deferred because transcription is still in progress");
5283
5619
  this.setBufferTimeout();
5284
5620
  return;
5285
5621
  }
@@ -5305,15 +5641,15 @@ export class Session {
5305
5641
  * Emit a message to the client
5306
5642
  */
5307
5643
  emit(msg) {
5308
- if (msg.type === 'audio_output' &&
5644
+ if (msg.type === "audio_output" &&
5309
5645
  (process.env.TTS_DEBUG_AUDIO_DIR || isPaseoDictationDebugEnabled()) &&
5310
5646
  msg.payload.groupId &&
5311
- typeof msg.payload.audio === 'string') {
5647
+ typeof msg.payload.audio === "string") {
5312
5648
  const groupId = msg.payload.groupId;
5313
5649
  const existing = this.ttsDebugStreams.get(groupId) ??
5314
5650
  { format: msg.payload.format, chunks: [] };
5315
5651
  try {
5316
- existing.chunks.push(Buffer.from(msg.payload.audio, 'base64'));
5652
+ existing.chunks.push(Buffer.from(msg.payload.audio, "base64"));
5317
5653
  existing.format = msg.payload.format;
5318
5654
  this.ttsDebugStreams.set(groupId, existing);
5319
5655
  }
@@ -5328,11 +5664,11 @@ export class Session {
5328
5664
  const recordingPath = await maybePersistTtsDebugAudio(Buffer.concat(final.chunks), { sessionId: this.sessionId, groupId, format: final.format }, this.sessionLogger);
5329
5665
  if (recordingPath) {
5330
5666
  this.onMessage({
5331
- type: 'activity_log',
5667
+ type: "activity_log",
5332
5668
  payload: {
5333
5669
  id: uuidv4(),
5334
5670
  timestamp: new Date(),
5335
- type: 'system',
5671
+ type: "system",
5336
5672
  content: `Saved TTS audio: ${recordingPath}`,
5337
5673
  metadata: { recordingPath, format: final.format, groupId },
5338
5674
  },
@@ -5352,14 +5688,14 @@ export class Session {
5352
5688
  this.onBinaryMessage(frame);
5353
5689
  }
5354
5690
  catch (error) {
5355
- this.sessionLogger.error({ err: error }, 'Failed to emit binary frame');
5691
+ this.sessionLogger.error({ err: error }, "Failed to emit binary frame");
5356
5692
  }
5357
5693
  }
5358
5694
  /**
5359
5695
  * Clean up session resources
5360
5696
  */
5361
5697
  async cleanup() {
5362
- this.sessionLogger.trace('Cleaning up');
5698
+ this.sessionLogger.trace("Cleaning up");
5363
5699
  if (this.unsubscribeAgentEvents) {
5364
5700
  this.unsubscribeAgentEvents();
5365
5701
  this.unsubscribeAgentEvents = null;
@@ -5382,7 +5718,7 @@ export class Session {
5382
5718
  await this.agentMcpClient.close();
5383
5719
  }
5384
5720
  catch (error) {
5385
- this.sessionLogger.error({ err: error }, 'Failed to close Agent MCP client');
5721
+ this.sessionLogger.error({ err: error }, "Failed to close Agent MCP client");
5386
5722
  }
5387
5723
  this.agentMcpClient = null;
5388
5724
  this.agentTools = null;
@@ -5395,20 +5731,20 @@ export class Session {
5395
5731
  this.unsubscribeTerminalsChanged = null;
5396
5732
  }
5397
5733
  this.subscribedTerminalDirectories.clear();
5398
- for (const unsubscribe of this.terminalSubscriptions.values()) {
5399
- unsubscribe();
5400
- }
5401
- this.terminalSubscriptions.clear();
5402
5734
  for (const unsubscribeExit of this.terminalExitSubscriptions.values()) {
5403
5735
  unsubscribeExit();
5404
5736
  }
5405
5737
  this.terminalExitSubscriptions.clear();
5406
- this.detachAllTerminalStreams({ emitExit: false });
5738
+ this.disposeTerminalSubscriptions();
5407
5739
  for (const target of this.checkoutDiffTargets.values()) {
5408
5740
  this.closeCheckoutDiffWatchTarget(target);
5409
5741
  }
5410
5742
  this.checkoutDiffTargets.clear();
5411
5743
  this.checkoutDiffSubscriptions.clear();
5744
+ for (const target of this.workspaceGitWatchTargets.values()) {
5745
+ this.closeWorkspaceGitWatchTarget(target);
5746
+ }
5747
+ this.workspaceGitWatchTargets.clear();
5412
5748
  }
5413
5749
  // ============================================================================
5414
5750
  // Terminal Handlers
@@ -5428,24 +5764,11 @@ export class Session {
5428
5764
  unsubscribeExit();
5429
5765
  this.terminalExitSubscriptions.delete(terminalId);
5430
5766
  }
5431
- const unsubscribe = this.terminalSubscriptions.get(terminalId);
5432
- if (unsubscribe) {
5433
- try {
5434
- unsubscribe();
5435
- }
5436
- catch (error) {
5437
- this.sessionLogger.warn({ err: error, terminalId }, 'Failed to unsubscribe terminal after process exit');
5438
- }
5439
- this.terminalSubscriptions.delete(terminalId);
5440
- }
5441
- const streamId = this.terminalStreamByTerminalId.get(terminalId);
5442
- if (typeof streamId === 'number') {
5443
- this.detachTerminalStream(streamId, { emitExit: true });
5444
- }
5767
+ this.detachTerminalStream(terminalId, { emitExit: true });
5445
5768
  }
5446
5769
  emitTerminalsChangedSnapshot(input) {
5447
5770
  this.emit({
5448
- type: 'terminals_changed',
5771
+ type: "terminals_changed",
5449
5772
  payload: {
5450
5773
  cwd: input.cwd,
5451
5774
  terminals: input.terminals,
@@ -5492,13 +5815,13 @@ export class Session {
5492
5815
  });
5493
5816
  }
5494
5817
  catch (error) {
5495
- this.sessionLogger.warn({ err: error, cwd }, 'Failed to emit initial terminal snapshot');
5818
+ this.sessionLogger.warn({ err: error, cwd }, "Failed to emit initial terminal snapshot");
5496
5819
  }
5497
5820
  }
5498
5821
  async handleListTerminalsRequest(msg) {
5499
5822
  if (!this.terminalManager) {
5500
5823
  this.emit({
5501
- type: 'list_terminals_response',
5824
+ type: "list_terminals_response",
5502
5825
  payload: {
5503
5826
  cwd: msg.cwd,
5504
5827
  terminals: [],
@@ -5513,7 +5836,7 @@ export class Session {
5513
5836
  this.ensureTerminalExitSubscription(terminal);
5514
5837
  }
5515
5838
  this.emit({
5516
- type: 'list_terminals_response',
5839
+ type: "list_terminals_response",
5517
5840
  payload: {
5518
5841
  cwd: msg.cwd,
5519
5842
  terminals: terminals.map((t) => ({ id: t.id, name: t.name })),
@@ -5522,9 +5845,9 @@ export class Session {
5522
5845
  });
5523
5846
  }
5524
5847
  catch (error) {
5525
- this.sessionLogger.error({ err: error, cwd: msg.cwd }, 'Failed to list terminals');
5848
+ this.sessionLogger.error({ err: error, cwd: msg.cwd }, "Failed to list terminals");
5526
5849
  this.emit({
5527
- type: 'list_terminals_response',
5850
+ type: "list_terminals_response",
5528
5851
  payload: {
5529
5852
  cwd: msg.cwd,
5530
5853
  terminals: [],
@@ -5536,10 +5859,10 @@ export class Session {
5536
5859
  async handleCreateTerminalRequest(msg) {
5537
5860
  if (!this.terminalManager) {
5538
5861
  this.emit({
5539
- type: 'create_terminal_response',
5862
+ type: "create_terminal_response",
5540
5863
  payload: {
5541
5864
  terminal: null,
5542
- error: 'Terminal manager not available',
5865
+ error: "Terminal manager not available",
5543
5866
  requestId: msg.requestId,
5544
5867
  },
5545
5868
  });
@@ -5552,7 +5875,7 @@ export class Session {
5552
5875
  });
5553
5876
  this.ensureTerminalExitSubscription(session);
5554
5877
  this.emit({
5555
- type: 'create_terminal_response',
5878
+ type: "create_terminal_response",
5556
5879
  payload: {
5557
5880
  terminal: { id: session.id, name: session.name, cwd: session.cwd },
5558
5881
  error: null,
@@ -5561,9 +5884,9 @@ export class Session {
5561
5884
  });
5562
5885
  }
5563
5886
  catch (error) {
5564
- this.sessionLogger.error({ err: error, cwd: msg.cwd }, 'Failed to create terminal');
5887
+ this.sessionLogger.error({ err: error, cwd: msg.cwd }, "Failed to create terminal");
5565
5888
  this.emit({
5566
- type: 'create_terminal_response',
5889
+ type: "create_terminal_response",
5567
5890
  payload: {
5568
5891
  terminal: null,
5569
5892
  error: error.message,
@@ -5575,11 +5898,10 @@ export class Session {
5575
5898
  async handleSubscribeTerminalRequest(msg) {
5576
5899
  if (!this.terminalManager) {
5577
5900
  this.emit({
5578
- type: 'subscribe_terminal_response',
5901
+ type: "subscribe_terminal_response",
5579
5902
  payload: {
5580
5903
  terminalId: msg.terminalId,
5581
- state: null,
5582
- error: 'Terminal manager not available',
5904
+ error: "Terminal manager not available",
5583
5905
  requestId: msg.requestId,
5584
5906
  },
5585
5907
  });
@@ -5588,52 +5910,44 @@ export class Session {
5588
5910
  const session = this.terminalManager.getTerminal(msg.terminalId);
5589
5911
  if (!session) {
5590
5912
  this.emit({
5591
- type: 'subscribe_terminal_response',
5913
+ type: "subscribe_terminal_response",
5592
5914
  payload: {
5593
5915
  terminalId: msg.terminalId,
5594
- state: null,
5595
- error: 'Terminal not found',
5916
+ error: "Terminal not found",
5596
5917
  requestId: msg.requestId,
5597
5918
  },
5598
5919
  });
5599
5920
  return;
5600
5921
  }
5601
5922
  this.ensureTerminalExitSubscription(session);
5602
- // Unsubscribe from previous subscription if any
5603
- const existing = this.terminalSubscriptions.get(msg.terminalId);
5604
- if (existing) {
5605
- existing();
5923
+ const slot = this.bindActiveTerminalStream(session);
5924
+ if (slot === null) {
5925
+ this.sessionLogger.warn({
5926
+ terminalId: msg.terminalId,
5927
+ activeTerminalStreamCount: this.activeTerminalStreams.size,
5928
+ }, "Terminal stream slot exhaustion");
5929
+ this.emit({
5930
+ type: "subscribe_terminal_response",
5931
+ payload: {
5932
+ terminalId: msg.terminalId,
5933
+ error: "No terminal stream slots available",
5934
+ requestId: msg.requestId,
5935
+ },
5936
+ });
5937
+ return;
5606
5938
  }
5607
- // Subscribe to terminal updates
5608
- const unsubscribe = session.subscribe((serverMsg) => {
5609
- if (serverMsg.type === 'full') {
5610
- this.emit({
5611
- type: 'terminal_output',
5612
- payload: {
5613
- terminalId: msg.terminalId,
5614
- state: serverMsg.state,
5615
- },
5616
- });
5617
- }
5618
- });
5619
- this.terminalSubscriptions.set(msg.terminalId, unsubscribe);
5620
- // Send initial state
5621
5939
  this.emit({
5622
- type: 'subscribe_terminal_response',
5940
+ type: "subscribe_terminal_response",
5623
5941
  payload: {
5624
5942
  terminalId: msg.terminalId,
5625
- state: session.getState(),
5943
+ slot,
5626
5944
  error: null,
5627
5945
  requestId: msg.requestId,
5628
5946
  },
5629
5947
  });
5630
5948
  }
5631
5949
  handleUnsubscribeTerminalRequest(msg) {
5632
- const unsubscribe = this.terminalSubscriptions.get(msg.terminalId);
5633
- if (unsubscribe) {
5634
- unsubscribe();
5635
- this.terminalSubscriptions.delete(msg.terminalId);
5636
- }
5950
+ this.detachTerminalStream(msg.terminalId, { emitExit: false });
5637
5951
  }
5638
5952
  handleTerminalInput(msg) {
5639
5953
  if (!this.terminalManager) {
@@ -5641,22 +5955,14 @@ export class Session {
5641
5955
  }
5642
5956
  const session = this.terminalManager.getTerminal(msg.terminalId);
5643
5957
  if (!session) {
5644
- this.sessionLogger.warn({ terminalId: msg.terminalId }, 'Terminal not found for input');
5958
+ this.sessionLogger.warn({ terminalId: msg.terminalId }, "Terminal not found for input");
5645
5959
  return;
5646
5960
  }
5647
5961
  this.ensureTerminalExitSubscription(session);
5648
5962
  session.send(msg.message);
5649
5963
  }
5650
5964
  killTrackedTerminal(terminalId, options) {
5651
- const unsubscribe = this.terminalSubscriptions.get(terminalId);
5652
- if (unsubscribe) {
5653
- unsubscribe();
5654
- this.terminalSubscriptions.delete(terminalId);
5655
- }
5656
- const streamId = this.terminalStreamByTerminalId.get(terminalId);
5657
- if (typeof streamId === 'number') {
5658
- this.detachTerminalStream(streamId, { emitExit: options?.emitExit ?? true });
5659
- }
5965
+ this.detachTerminalStream(terminalId, { emitExit: options?.emitExit ?? true });
5660
5966
  this.terminalManager?.killTerminal(terminalId);
5661
5967
  }
5662
5968
  async killTerminalsUnderPath(rootPath) {
@@ -5678,18 +5984,18 @@ export class Session {
5678
5984
  catch (error) {
5679
5985
  const message = error instanceof Error ? error.message : String(error);
5680
5986
  cleanupErrors.push({ cwd: terminalCwd, message });
5681
- this.sessionLogger.warn({ err: error, cwd: terminalCwd }, 'Failed to clean up worktree terminals during archive');
5987
+ this.sessionLogger.warn({ err: error, cwd: terminalCwd }, "Failed to clean up worktree terminals during archive");
5682
5988
  }
5683
5989
  }
5684
5990
  if (cleanupErrors.length > 0) {
5685
- const details = cleanupErrors.map((entry) => `${entry.cwd}: ${entry.message}`).join('; ');
5991
+ const details = cleanupErrors.map((entry) => `${entry.cwd}: ${entry.message}`).join("; ");
5686
5992
  throw new Error(`Failed to clean up worktree terminals during archive (${details})`);
5687
5993
  }
5688
5994
  }
5689
5995
  async handleKillTerminalRequest(msg) {
5690
5996
  if (!this.terminalManager) {
5691
5997
  this.emit({
5692
- type: 'kill_terminal_response',
5998
+ type: "kill_terminal_response",
5693
5999
  payload: {
5694
6000
  terminalId: msg.terminalId,
5695
6001
  success: false,
@@ -5700,7 +6006,7 @@ export class Session {
5700
6006
  }
5701
6007
  this.killTrackedTerminal(msg.terminalId, { emitExit: true });
5702
6008
  this.emit({
5703
- type: 'kill_terminal_response',
6009
+ type: "kill_terminal_response",
5704
6010
  payload: {
5705
6011
  terminalId: msg.terminalId,
5706
6012
  success: true,
@@ -5708,219 +6014,149 @@ export class Session {
5708
6014
  },
5709
6015
  });
5710
6016
  }
5711
- async handleAttachTerminalStreamRequest(msg) {
5712
- if (!this.terminalManager || !this.onBinaryMessage) {
5713
- this.emit({
5714
- type: 'attach_terminal_stream_response',
5715
- payload: {
5716
- terminalId: msg.terminalId,
5717
- streamId: null,
5718
- replayedFrom: 0,
5719
- currentOffset: 0,
5720
- earliestAvailableOffset: 0,
5721
- reset: true,
5722
- error: 'Terminal streaming not available',
5723
- requestId: msg.requestId,
5724
- },
5725
- });
5726
- return;
6017
+ bindActiveTerminalStream(terminal) {
6018
+ if (!this.onBinaryMessage) {
6019
+ return null;
5727
6020
  }
5728
- const session = this.terminalManager.getTerminal(msg.terminalId);
5729
- if (!session) {
5730
- this.emit({
5731
- type: 'attach_terminal_stream_response',
5732
- payload: {
5733
- terminalId: msg.terminalId,
5734
- streamId: null,
5735
- replayedFrom: 0,
5736
- currentOffset: 0,
5737
- earliestAvailableOffset: 0,
5738
- reset: true,
5739
- error: 'Terminal not found',
5740
- requestId: msg.requestId,
5741
- },
5742
- });
5743
- return;
6021
+ const existingSlot = this.terminalIdToSlot.get(terminal.id);
6022
+ if (typeof existingSlot === "number") {
6023
+ const existingStream = this.activeTerminalStreams.get(existingSlot);
6024
+ if (existingStream) {
6025
+ existingStream.needsSnapshot = true;
6026
+ this.trySendTerminalSnapshot(existingStream);
6027
+ return existingSlot;
6028
+ }
6029
+ this.terminalIdToSlot.delete(terminal.id);
6030
+ }
6031
+ const slot = this.allocateTerminalSlot();
6032
+ if (slot === null) {
6033
+ return null;
5744
6034
  }
5745
- if (msg.rows || msg.cols) {
5746
- const state = session.getState();
5747
- session.send({
5748
- type: 'resize',
5749
- rows: msg.rows ?? state.rows,
5750
- cols: msg.cols ?? state.cols,
5751
- });
5752
- }
5753
- const existingStreamId = this.terminalStreamByTerminalId.get(msg.terminalId);
5754
- if (typeof existingStreamId === 'number') {
5755
- // Replacing an active stream can happen when multiple UI surfaces attach to the
5756
- // same terminal. Emit exit for the replaced stream so stale listeners reconnect
5757
- // instead of continuing to send input to an invalid stream id.
5758
- this.detachTerminalStream(existingStreamId, { emitExit: true });
5759
- }
5760
- const streamId = this.allocateTerminalStreamId();
5761
- const requestedResumeOffset = typeof msg.resumeOffset === 'number'
5762
- ? msg.resumeOffset
5763
- : 0;
5764
- const initialOffset = Math.max(0, Math.floor(requestedResumeOffset));
5765
- const binding = {
5766
- terminalId: msg.terminalId,
6035
+ const activeStream = {
6036
+ terminalId: terminal.id,
6037
+ slot,
5767
6038
  unsubscribe: () => { },
5768
- lastOutputOffset: initialOffset,
5769
- lastAckOffset: initialOffset,
5770
- pendingChunks: [],
5771
- pendingBytes: 0,
6039
+ needsSnapshot: true,
6040
+ snapshotRetryTimer: null,
5772
6041
  };
5773
- this.terminalStreams.set(streamId, binding);
5774
- this.terminalStreamByTerminalId.set(msg.terminalId, streamId);
5775
- let rawSub;
5776
- try {
5777
- rawSub = session.subscribeRaw((chunk) => {
5778
- const currentBinding = this.terminalStreams.get(streamId);
5779
- if (!currentBinding) {
5780
- return;
5781
- }
5782
- this.enqueueOrEmitTerminalStreamChunk(streamId, currentBinding, {
5783
- data: chunk.data,
5784
- startOffset: chunk.startOffset,
5785
- endOffset: chunk.endOffset,
5786
- replay: chunk.replay,
5787
- });
5788
- }, { fromOffset: requestedResumeOffset });
5789
- }
5790
- catch (error) {
5791
- this.terminalStreams.delete(streamId);
5792
- this.terminalStreamByTerminalId.delete(msg.terminalId);
5793
- throw error;
5794
- }
5795
- binding.unsubscribe = rawSub.unsubscribe;
5796
- binding.lastAckOffset = rawSub.replayedFrom;
5797
- if (binding.lastOutputOffset < rawSub.replayedFrom) {
5798
- binding.lastOutputOffset = rawSub.replayedFrom;
5799
- }
5800
- this.flushPendingTerminalStreamChunks(streamId, binding);
5801
- this.emit({
5802
- type: 'attach_terminal_stream_response',
5803
- payload: {
5804
- terminalId: msg.terminalId,
5805
- streamId,
5806
- replayedFrom: rawSub.replayedFrom,
5807
- currentOffset: rawSub.currentOffset,
5808
- earliestAvailableOffset: rawSub.earliestAvailableOffset,
5809
- reset: rawSub.reset,
5810
- error: null,
5811
- requestId: msg.requestId,
5812
- },
5813
- });
5814
- }
5815
- getTerminalStreamChunkByteLength(chunk) {
5816
- return Math.max(0, chunk.endOffset - chunk.startOffset);
5817
- }
5818
- canEmitTerminalStreamChunk(binding, chunk) {
5819
- return chunk.startOffset < binding.lastAckOffset + TERMINAL_STREAM_WINDOW_BYTES;
5820
- }
5821
- emitTerminalStreamChunk(streamId, binding, chunk) {
5822
- const payload = new Uint8Array(Buffer.from(chunk.data, 'utf8'));
5823
- this.emitBinary({
5824
- channel: BinaryMuxChannel.Terminal,
5825
- messageType: TerminalBinaryMessageType.OutputUtf8,
5826
- streamId,
5827
- offset: chunk.startOffset,
5828
- flags: chunk.replay ? TerminalBinaryFlags.Replay : 0,
5829
- payload,
5830
- });
5831
- binding.lastOutputOffset = chunk.endOffset;
5832
- }
5833
- enqueueOrEmitTerminalStreamChunk(streamId, binding, chunk) {
5834
- const chunkBytes = this.getTerminalStreamChunkByteLength(chunk);
5835
- if (binding.pendingChunks.length > 0 || !this.canEmitTerminalStreamChunk(binding, chunk)) {
5836
- if (binding.pendingChunks.length >= TERMINAL_STREAM_MAX_PENDING_CHUNKS ||
5837
- binding.pendingBytes + chunkBytes > TERMINAL_STREAM_MAX_PENDING_BYTES) {
5838
- this.sessionLogger.warn({
5839
- streamId,
5840
- pendingChunks: binding.pendingChunks.length,
5841
- pendingBytes: binding.pendingBytes,
5842
- chunkBytes,
5843
- }, 'Terminal stream pending buffer overflow; closing stream');
5844
- this.detachTerminalStream(streamId, { emitExit: true });
6042
+ this.activeTerminalStreams.set(slot, activeStream);
6043
+ this.terminalIdToSlot.set(terminal.id, slot);
6044
+ activeStream.unsubscribe = terminal.subscribe((message) => {
6045
+ if (this.activeTerminalStreams.get(slot) !== activeStream) {
6046
+ return;
6047
+ }
6048
+ if (message.type === "snapshot") {
6049
+ this.trySendTerminalSnapshot(activeStream);
6050
+ return;
6051
+ }
6052
+ if (activeStream.needsSnapshot || message.data.length === 0) {
6053
+ return;
6054
+ }
6055
+ if (this.getCurrentBinaryBufferedAmount() >= TERMINAL_STREAM_HIGH_WATER_BYTES) {
6056
+ this.markAllActiveTerminalStreamsForSnapshot();
5845
6057
  return;
5846
6058
  }
5847
- binding.pendingChunks.push(chunk);
5848
- binding.pendingBytes += chunkBytes;
6059
+ this.emitBinary(encodeTerminalStreamFrame({
6060
+ opcode: TerminalStreamOpcode.Output,
6061
+ slot,
6062
+ payload: new Uint8Array(Buffer.from(message.data, "utf8")),
6063
+ }));
6064
+ if (this.getCurrentBinaryBufferedAmount() >= TERMINAL_STREAM_HIGH_WATER_BYTES) {
6065
+ this.markAllActiveTerminalStreamsForSnapshot();
6066
+ }
6067
+ });
6068
+ return slot;
6069
+ }
6070
+ trySendTerminalSnapshot(activeStream) {
6071
+ if (this.activeTerminalStreams.get(activeStream.slot) !== activeStream ||
6072
+ !activeStream.needsSnapshot) {
5849
6073
  return;
5850
6074
  }
5851
- this.emitTerminalStreamChunk(streamId, binding, chunk);
5852
- }
5853
- flushPendingTerminalStreamChunks(streamId, binding) {
5854
- while (binding.pendingChunks.length > 0) {
5855
- const next = binding.pendingChunks[0];
5856
- if (!next || !this.canEmitTerminalStreamChunk(binding, next)) {
5857
- break;
6075
+ if (this.getCurrentBinaryBufferedAmount() > TERMINAL_STREAM_LOW_WATER_BYTES) {
6076
+ if (!activeStream.snapshotRetryTimer) {
6077
+ activeStream.snapshotRetryTimer = setTimeout(() => {
6078
+ activeStream.snapshotRetryTimer = null;
6079
+ this.trySendTerminalSnapshot(activeStream);
6080
+ }, 33);
5858
6081
  }
5859
- binding.pendingChunks.shift();
5860
- binding.pendingBytes -= this.getTerminalStreamChunkByteLength(next);
5861
- if (binding.pendingBytes < 0) {
5862
- binding.pendingBytes = 0;
5863
- }
5864
- this.emitTerminalStreamChunk(streamId, binding, next);
6082
+ return;
5865
6083
  }
6084
+ if (activeStream.snapshotRetryTimer) {
6085
+ clearTimeout(activeStream.snapshotRetryTimer);
6086
+ activeStream.snapshotRetryTimer = null;
6087
+ }
6088
+ const terminal = this.terminalManager?.getTerminal(activeStream.terminalId);
6089
+ if (!terminal) {
6090
+ this.detachTerminalStream(activeStream.terminalId, { emitExit: true });
6091
+ return;
6092
+ }
6093
+ activeStream.needsSnapshot = false;
6094
+ this.emitBinary(encodeTerminalStreamFrame({
6095
+ opcode: TerminalStreamOpcode.Snapshot,
6096
+ slot: activeStream.slot,
6097
+ payload: encodeTerminalSnapshotPayload(terminal.getState()),
6098
+ }));
5866
6099
  }
5867
- handleDetachTerminalStreamRequest(msg) {
5868
- const success = this.detachTerminalStream(msg.streamId, { emitExit: false });
5869
- this.emit({
5870
- type: 'detach_terminal_stream_response',
5871
- payload: {
5872
- streamId: msg.streamId,
5873
- success,
5874
- requestId: msg.requestId,
5875
- },
5876
- });
6100
+ markAllActiveTerminalStreamsForSnapshot() {
6101
+ for (const activeStream of this.activeTerminalStreams.values()) {
6102
+ activeStream.needsSnapshot = true;
6103
+ this.trySendTerminalSnapshot(activeStream);
6104
+ }
5877
6105
  }
5878
- detachAllTerminalStreams(options) {
5879
- for (const streamId of Array.from(this.terminalStreams.keys())) {
5880
- this.detachTerminalStream(streamId, options);
6106
+ allocateTerminalSlot() {
6107
+ for (let attempt = 0; attempt < MAX_TERMINAL_STREAM_SLOTS; attempt += 1) {
6108
+ const slot = (this.nextTerminalSlot + attempt) % MAX_TERMINAL_STREAM_SLOTS;
6109
+ if (this.activeTerminalStreams.has(slot)) {
6110
+ continue;
6111
+ }
6112
+ this.nextTerminalSlot = (slot + 1) % MAX_TERMINAL_STREAM_SLOTS;
6113
+ return slot;
5881
6114
  }
6115
+ return null;
5882
6116
  }
5883
- detachTerminalStream(streamId, options) {
5884
- const binding = this.terminalStreams.get(streamId);
5885
- if (!binding) {
6117
+ detachTerminalStream(terminalId, options) {
6118
+ const slot = this.terminalIdToSlot.get(terminalId);
6119
+ if (typeof slot !== "number") {
5886
6120
  return false;
5887
6121
  }
6122
+ const activeStream = this.activeTerminalStreams.get(slot);
6123
+ if (!activeStream) {
6124
+ this.terminalIdToSlot.delete(terminalId);
6125
+ return false;
6126
+ }
6127
+ this.activeTerminalStreams.delete(slot);
6128
+ this.terminalIdToSlot.delete(terminalId);
6129
+ if (activeStream.snapshotRetryTimer) {
6130
+ clearTimeout(activeStream.snapshotRetryTimer);
6131
+ activeStream.snapshotRetryTimer = null;
6132
+ }
5888
6133
  try {
5889
- binding.unsubscribe();
6134
+ activeStream.unsubscribe();
5890
6135
  }
5891
6136
  catch (error) {
5892
- this.sessionLogger.warn({ err: error, streamId }, 'Failed to unsubscribe terminal stream');
5893
- }
5894
- this.terminalStreams.delete(streamId);
5895
- if (this.terminalStreamByTerminalId.get(binding.terminalId) === streamId) {
5896
- this.terminalStreamByTerminalId.delete(binding.terminalId);
6137
+ this.sessionLogger.warn({ err: error }, "Failed to unsubscribe terminal stream");
5897
6138
  }
5898
6139
  if (options?.emitExit) {
5899
6140
  this.emit({
5900
- type: 'terminal_stream_exit',
6141
+ type: "terminal_stream_exit",
5901
6142
  payload: {
5902
- streamId,
5903
- terminalId: binding.terminalId,
6143
+ terminalId: activeStream.terminalId,
5904
6144
  },
5905
6145
  });
5906
6146
  }
5907
6147
  return true;
5908
6148
  }
5909
- allocateTerminalStreamId() {
5910
- let attempts = 0;
5911
- while (attempts < 0xffffffff) {
5912
- const candidate = this.nextTerminalStreamId >>> 0;
5913
- this.nextTerminalStreamId = ((this.nextTerminalStreamId + 1) & 0xffffffff) >>> 0;
5914
- if (candidate === 0) {
5915
- attempts += 1;
5916
- continue;
5917
- }
5918
- if (!this.terminalStreams.has(candidate)) {
5919
- return candidate;
5920
- }
5921
- attempts += 1;
6149
+ disposeTerminalSubscriptions() {
6150
+ for (const terminalId of [...this.terminalIdToSlot.keys()]) {
6151
+ this.detachTerminalStream(terminalId, { emitExit: false });
6152
+ }
6153
+ }
6154
+ getCurrentBinaryBufferedAmount() {
6155
+ const bufferedAmount = this.getBinaryBufferedAmount?.() ?? 0;
6156
+ if (!Number.isFinite(bufferedAmount) || bufferedAmount < 0) {
6157
+ return 0;
5922
6158
  }
5923
- throw new Error('Unable to allocate terminal stream id');
6159
+ return Math.floor(bufferedAmount);
5924
6160
  }
5925
6161
  }
5926
6162
  //# sourceMappingURL=session.js.map