@getpaseo/server 0.1.30 → 0.1.33

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 +2 -2
  48. package/dist/server/server/agent/provider-manifest.d.ts.map +1 -1
  49. package/dist/server/server/agent/provider-manifest.js +63 -9
  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.map +1 -1
  58. package/dist/server/server/agent/providers/claude/partial-json.js +4 -4
  59. package/dist/server/server/agent/providers/claude/partial-json.js.map +1 -1
  60. package/dist/server/server/agent/providers/claude/sidechain-tracker.d.ts +20 -0
  61. package/dist/server/server/agent/providers/claude/sidechain-tracker.d.ts.map +1 -0
  62. package/dist/server/server/agent/providers/claude/sidechain-tracker.js +230 -0
  63. package/dist/server/server/agent/providers/claude/sidechain-tracker.js.map +1 -0
  64. package/dist/server/server/agent/providers/claude/task-notification-tool-call.d.ts +11 -0
  65. package/dist/server/server/agent/providers/claude/task-notification-tool-call.d.ts.map +1 -1
  66. package/dist/server/server/agent/providers/claude/task-notification-tool-call.js +37 -20
  67. package/dist/server/server/agent/providers/claude/task-notification-tool-call.js.map +1 -1
  68. package/dist/server/server/agent/providers/claude/tool-call-detail-parser.d.ts.map +1 -1
  69. package/dist/server/server/agent/providers/claude/tool-call-detail-parser.js +21 -11
  70. package/dist/server/server/agent/providers/claude/tool-call-detail-parser.js.map +1 -1
  71. package/dist/server/server/agent/providers/claude/tool-call-mapper.d.ts.map +1 -1
  72. package/dist/server/server/agent/providers/claude/tool-call-mapper.js +23 -11
  73. package/dist/server/server/agent/providers/claude/tool-call-mapper.js.map +1 -1
  74. package/dist/server/server/agent/providers/claude-agent.d.ts.map +1 -1
  75. package/dist/server/server/agent/providers/claude-agent.js +488 -1134
  76. package/dist/server/server/agent/providers/claude-agent.js.map +1 -1
  77. package/dist/server/server/agent/providers/codex/tool-call-detail-parser.d.ts.map +1 -1
  78. package/dist/server/server/agent/providers/codex/tool-call-detail-parser.js +2 -2
  79. package/dist/server/server/agent/providers/codex/tool-call-detail-parser.js.map +1 -1
  80. package/dist/server/server/agent/providers/codex/tool-call-mapper.d.ts.map +1 -1
  81. package/dist/server/server/agent/providers/codex/tool-call-mapper.js +14 -11
  82. package/dist/server/server/agent/providers/codex/tool-call-mapper.js.map +1 -1
  83. package/dist/server/server/agent/providers/codex-app-server-agent.d.ts.map +1 -1
  84. package/dist/server/server/agent/providers/codex-app-server-agent.js +347 -163
  85. package/dist/server/server/agent/providers/codex-app-server-agent.js.map +1 -1
  86. package/dist/server/server/agent/providers/codex-rollout-timeline.d.ts.map +1 -1
  87. package/dist/server/server/agent/providers/codex-rollout-timeline.js +21 -32
  88. package/dist/server/server/agent/providers/codex-rollout-timeline.js.map +1 -1
  89. package/dist/server/server/agent/providers/opencode/tool-call-detail-parser.d.ts.map +1 -1
  90. package/dist/server/server/agent/providers/opencode/tool-call-detail-parser.js +2 -2
  91. package/dist/server/server/agent/providers/opencode/tool-call-detail-parser.js.map +1 -1
  92. package/dist/server/server/agent/providers/opencode/tool-call-mapper.d.ts.map +1 -1
  93. package/dist/server/server/agent/providers/opencode/tool-call-mapper.js +2 -9
  94. package/dist/server/server/agent/providers/opencode/tool-call-mapper.js.map +1 -1
  95. package/dist/server/server/agent/providers/opencode-agent.d.ts.map +1 -1
  96. package/dist/server/server/agent/providers/opencode-agent.js +5 -5
  97. package/dist/server/server/agent/providers/opencode-agent.js.map +1 -1
  98. package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts +277 -1
  99. package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts.map +1 -1
  100. package/dist/server/server/agent/providers/tool-call-detail-primitives.js +149 -15
  101. package/dist/server/server/agent/providers/tool-call-detail-primitives.js.map +1 -1
  102. package/dist/server/server/agent/providers/tool-call-mapper-utils.d.ts.map +1 -1
  103. package/dist/server/server/agent/providers/tool-call-mapper-utils.js +1 -3
  104. package/dist/server/server/agent/providers/tool-call-mapper-utils.js.map +1 -1
  105. package/dist/server/server/agent/stt-manager.d.ts.map +1 -1
  106. package/dist/server/server/agent/stt-manager.js +1 -2
  107. package/dist/server/server/agent/stt-manager.js.map +1 -1
  108. package/dist/server/server/agent/system-prompt.js +5 -5
  109. package/dist/server/server/agent/timeline-projection.d.ts.map +1 -1
  110. package/dist/server/server/agent/timeline-projection.js.map +1 -1
  111. package/dist/server/server/agent/tts-manager.d.ts.map +1 -1
  112. package/dist/server/server/agent/tts-manager.js +27 -9
  113. package/dist/server/server/agent/tts-manager.js.map +1 -1
  114. package/dist/server/server/agent/wait-for-agent-tracker.d.ts.map +1 -1
  115. package/dist/server/server/agent/wait-for-agent-tracker.js.map +1 -1
  116. package/dist/server/server/agent-attention-policy.d.ts.map +1 -1
  117. package/dist/server/server/agent-attention-policy.js.map +1 -1
  118. package/dist/server/server/allowed-hosts.d.ts.map +1 -1
  119. package/dist/server/server/allowed-hosts.js.map +1 -1
  120. package/dist/server/server/bootstrap.d.ts.map +1 -1
  121. package/dist/server/server/bootstrap.js +46 -5
  122. package/dist/server/server/bootstrap.js.map +1 -1
  123. package/dist/server/server/config.d.ts.map +1 -1
  124. package/dist/server/server/config.js +4 -11
  125. package/dist/server/server/config.js.map +1 -1
  126. package/dist/server/server/connection-offer.d.ts +1 -1
  127. package/dist/server/server/connection-offer.d.ts.map +1 -1
  128. package/dist/server/server/connection-offer.js +2 -3
  129. package/dist/server/server/connection-offer.js.map +1 -1
  130. package/dist/server/server/daemon-version.d.ts.map +1 -1
  131. package/dist/server/server/daemon-version.js +1 -1
  132. package/dist/server/server/daemon-version.js.map +1 -1
  133. package/dist/server/server/dictation/dictation-stream-manager.d.ts.map +1 -1
  134. package/dist/server/server/dictation/dictation-stream-manager.js +4 -1
  135. package/dist/server/server/dictation/dictation-stream-manager.js.map +1 -1
  136. package/dist/server/server/exports.d.ts +1 -1
  137. package/dist/server/server/exports.d.ts.map +1 -1
  138. package/dist/server/server/exports.js +1 -1
  139. package/dist/server/server/exports.js.map +1 -1
  140. package/dist/server/server/file-explorer/service.d.ts +1 -1
  141. package/dist/server/server/file-explorer/service.d.ts.map +1 -1
  142. package/dist/server/server/file-explorer/service.js +5 -8
  143. package/dist/server/server/file-explorer/service.js.map +1 -1
  144. package/dist/server/server/index.js +1 -1
  145. package/dist/server/server/index.js.map +1 -1
  146. package/dist/server/server/logger.d.ts.map +1 -1
  147. package/dist/server/server/logger.js.map +1 -1
  148. package/dist/server/server/messages.d.ts.map +1 -1
  149. package/dist/server/server/messages.js.map +1 -1
  150. package/dist/server/server/package-version.d.ts.map +1 -1
  151. package/dist/server/server/package-version.js +1 -2
  152. package/dist/server/server/package-version.js.map +1 -1
  153. package/dist/server/server/persisted-config.d.ts +10 -10
  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 +1240 -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 +9 -48
  295. package/dist/server/terminal/terminal.d.ts.map +1 -1
  296. package/dist/server/terminal/terminal.js +49 -126
  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 +63 -9
  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 +4 -4
  339. package/dist/src/server/agent/providers/claude/partial-json.js.map +1 -1
  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 +488 -1134
  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 +1240 -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 +49 -126
  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,136 @@ 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
+ terminal.send({ type: "resize", rows: resize.rows, cols: resize.cols });
1140
+ return;
1141
+ }
1142
+ default:
1143
+ return;
1134
1144
  }
1135
- this.sessionLogger.warn({ streamId: frame.streamId, messageType: frame.messageType }, 'Unhandled terminal binary frame');
1136
1145
  }
1137
1146
  async handleRestartServerRequest(requestId, reason) {
1138
1147
  const payload = {
1139
- status: 'restart_requested',
1148
+ status: "restart_requested",
1140
1149
  clientId: this.clientId,
1141
1150
  };
1142
1151
  if (reason && reason.trim().length > 0) {
1143
1152
  payload.reason = reason;
1144
1153
  }
1145
1154
  payload.requestId = requestId;
1146
- this.sessionLogger.warn({ reason }, 'Restart requested via websocket');
1155
+ this.sessionLogger.warn({ reason }, "Restart requested via websocket");
1147
1156
  this.emit({
1148
- type: 'status',
1157
+ type: "status",
1149
1158
  payload,
1150
1159
  });
1151
1160
  this.emitLifecycleIntent({
1152
- type: 'restart',
1161
+ type: "restart",
1153
1162
  clientId: this.clientId,
1154
1163
  requestId,
1155
1164
  ...(reason ? { reason } : {}),
1156
1165
  });
1157
1166
  }
1158
1167
  async handleShutdownServerRequest(requestId) {
1159
- this.sessionLogger.warn('Shutdown requested via websocket');
1168
+ this.sessionLogger.warn("Shutdown requested via websocket");
1160
1169
  this.emit({
1161
- type: 'status',
1170
+ type: "status",
1162
1171
  payload: {
1163
- status: 'shutdown_requested',
1172
+ status: "shutdown_requested",
1164
1173
  clientId: this.clientId,
1165
1174
  requestId,
1166
1175
  },
1167
1176
  });
1168
1177
  this.emitLifecycleIntent({
1169
- type: 'shutdown',
1178
+ type: "shutdown",
1170
1179
  clientId: this.clientId,
1171
1180
  requestId,
1172
1181
  });
@@ -1179,7 +1188,7 @@ export class Session {
1179
1188
  this.onLifecycleIntent(intent);
1180
1189
  }
1181
1190
  catch (error) {
1182
- this.sessionLogger.error({ err: error, intent }, 'Lifecycle intent handler failed');
1191
+ this.sessionLogger.error({ err: error, intent }, "Lifecycle intent handler failed");
1183
1192
  }
1184
1193
  }
1185
1194
  async handleDeleteAgentRequest(agentId, requestId) {
@@ -1202,7 +1211,7 @@ export class Session {
1202
1211
  this.sessionLogger.error({ err: error, agentId }, `Failed to remove agent ${agentId} from registry`);
1203
1212
  }
1204
1213
  this.emit({
1205
- type: 'agent_deleted',
1214
+ type: "agent_deleted",
1206
1215
  payload: {
1207
1216
  agentId,
1208
1217
  requestId,
@@ -1210,7 +1219,7 @@ export class Session {
1210
1219
  });
1211
1220
  if (this.agentUpdatesSubscription) {
1212
1221
  this.bufferOrEmitAgentUpdate(this.agentUpdatesSubscription, {
1213
- kind: 'remove',
1222
+ kind: "remove",
1214
1223
  agentId,
1215
1224
  });
1216
1225
  }
@@ -1222,7 +1231,7 @@ export class Session {
1222
1231
  this.sessionLogger.info({ agentId }, `Archiving agent ${agentId}`);
1223
1232
  const { archivedAt } = await this.archiveAgentState(agentId);
1224
1233
  this.emit({
1225
- type: 'agent_archived',
1234
+ type: "agent_archived",
1226
1235
  payload: {
1227
1236
  agentId,
1228
1237
  archivedAt,
@@ -1251,8 +1260,8 @@ export class Session {
1251
1260
  throw new Error(`Agent not found in storage after snapshot: ${agentId}`);
1252
1261
  }
1253
1262
  }
1254
- const normalizedStatus = archivedRecord.lastStatus === 'running' || archivedRecord.lastStatus === 'initializing'
1255
- ? 'idle'
1263
+ const normalizedStatus = archivedRecord.lastStatus === "running" || archivedRecord.lastStatus === "initializing"
1264
+ ? "idle"
1256
1265
  : archivedRecord.lastStatus;
1257
1266
  const nextRecord = {
1258
1267
  ...archivedRecord,
@@ -1263,7 +1272,17 @@ export class Session {
1263
1272
  attentionTimestamp: null,
1264
1273
  };
1265
1274
  await this.agentStorage.upsert(nextRecord);
1266
- this.agentManager.notifyAgentState(agentId);
1275
+ // Unload the agent from memory — the storage record is the source of truth now.
1276
+ // This tears down the provider session and drops the hydrated timeline,
1277
+ // freeing memory. ensureAgentLoaded will re-initialize if needed later.
1278
+ if (this.agentManager.getAgent(agentId)) {
1279
+ try {
1280
+ await this.agentManager.closeAgent(agentId);
1281
+ }
1282
+ catch (error) {
1283
+ this.sessionLogger.warn({ err: error, agentId }, "Failed to close agent during archive");
1284
+ }
1285
+ }
1267
1286
  return { archivedAt, archivedRecord: nextRecord };
1268
1287
  }
1269
1288
  async unarchiveAgentState(agentId) {
@@ -1291,19 +1310,19 @@ export class Session {
1291
1310
  this.sessionLogger.info({
1292
1311
  agentId,
1293
1312
  requestId,
1294
- hasName: typeof name === 'string',
1313
+ hasName: typeof name === "string",
1295
1314
  labelCount: labels ? Object.keys(labels).length : 0,
1296
- }, 'session: update_agent_request');
1315
+ }, "session: update_agent_request");
1297
1316
  const normalizedName = name?.trim();
1298
1317
  const normalizedLabels = labels && Object.keys(labels).length > 0 ? labels : undefined;
1299
1318
  if (!normalizedName && !normalizedLabels) {
1300
1319
  this.emit({
1301
- type: 'update_agent_response',
1320
+ type: "update_agent_response",
1302
1321
  payload: {
1303
1322
  requestId,
1304
1323
  agentId,
1305
1324
  accepted: false,
1306
- error: 'Nothing to update (provide name and/or labels)',
1325
+ error: "Nothing to update (provide name and/or labels)",
1307
1326
  },
1308
1327
  });
1309
1328
  return;
@@ -1330,28 +1349,28 @@ export class Session {
1330
1349
  });
1331
1350
  }
1332
1351
  this.emit({
1333
- type: 'update_agent_response',
1352
+ type: "update_agent_response",
1334
1353
  payload: { requestId, agentId, accepted: true, error: null },
1335
1354
  });
1336
1355
  }
1337
1356
  catch (error) {
1338
- this.sessionLogger.error({ err: error, agentId, requestId }, 'session: update_agent_request error');
1357
+ this.sessionLogger.error({ err: error, agentId, requestId }, "session: update_agent_request error");
1339
1358
  this.emit({
1340
- type: 'activity_log',
1359
+ type: "activity_log",
1341
1360
  payload: {
1342
1361
  id: uuidv4(),
1343
1362
  timestamp: new Date(),
1344
- type: 'error',
1363
+ type: "error",
1345
1364
  content: `Failed to update agent: ${error.message}`,
1346
1365
  },
1347
1366
  });
1348
1367
  this.emit({
1349
- type: 'update_agent_response',
1368
+ type: "update_agent_response",
1350
1369
  payload: {
1351
1370
  requestId,
1352
1371
  agentId,
1353
1372
  accepted: false,
1354
- error: error?.message ? String(error.message) : 'Failed to update agent',
1373
+ error: error?.message ? String(error.message) : "Failed to update agent",
1355
1374
  },
1356
1375
  });
1357
1376
  }
@@ -1365,7 +1384,7 @@ export class Session {
1365
1384
  };
1366
1385
  }
1367
1386
  resolveModeReadinessState(readiness, mode) {
1368
- if (mode === 'voice_mode') {
1387
+ if (mode === "voice_mode") {
1369
1388
  return readiness.realtimeVoice;
1370
1389
  }
1371
1390
  return readiness.dictation;
@@ -1403,13 +1422,13 @@ export class Session {
1403
1422
  async handleSetVoiceMode(enabled, agentId, requestId) {
1404
1423
  const startedAt = Date.now();
1405
1424
  try {
1406
- this.sessionLogger.info({ enabled, requestedAgentId: agentId ?? null, requestId: requestId ?? null }, 'set_voice_mode started');
1425
+ this.sessionLogger.info({ enabled, requestedAgentId: agentId ?? null, requestId: requestId ?? null }, "set_voice_mode started");
1407
1426
  if (enabled) {
1408
- const unavailable = this.resolveVoiceFeatureUnavailableContext('voice_mode');
1427
+ const unavailable = this.resolveVoiceFeatureUnavailableContext("voice_mode");
1409
1428
  if (unavailable) {
1410
1429
  throw new VoiceFeatureUnavailableError(unavailable);
1411
1430
  }
1412
- const normalizedAgentId = this.parseVoiceTargetAgentId(agentId ?? '', 'set_voice_mode');
1431
+ const normalizedAgentId = this.parseVoiceTargetAgentId(agentId ?? "", "set_voice_mode");
1413
1432
  if (this.isVoiceMode &&
1414
1433
  this.voiceModeAgentId &&
1415
1434
  this.voiceModeAgentId !== normalizedAgentId) {
@@ -1417,26 +1436,26 @@ export class Session {
1417
1436
  previousAgentId: this.voiceModeAgentId,
1418
1437
  nextAgentId: normalizedAgentId,
1419
1438
  elapsedMs: Date.now() - startedAt,
1420
- }, 'set_voice_mode disabling previous active voice agent');
1439
+ }, "set_voice_mode disabling previous active voice agent");
1421
1440
  await this.disableVoiceModeForActiveAgent(true);
1422
1441
  }
1423
1442
  if (!this.isVoiceMode || this.voiceModeAgentId !== normalizedAgentId) {
1424
- this.sessionLogger.info({ agentId: normalizedAgentId, elapsedMs: Date.now() - startedAt }, 'set_voice_mode enabling voice for agent');
1443
+ this.sessionLogger.info({ agentId: normalizedAgentId, elapsedMs: Date.now() - startedAt }, "set_voice_mode enabling voice for agent");
1425
1444
  const refreshedAgentId = await this.enableVoiceModeForAgent(normalizedAgentId);
1426
1445
  this.voiceModeAgentId = refreshedAgentId;
1427
- this.sessionLogger.info({ agentId: refreshedAgentId, elapsedMs: Date.now() - startedAt }, 'set_voice_mode agent enable complete');
1446
+ this.sessionLogger.info({ agentId: refreshedAgentId, elapsedMs: Date.now() - startedAt }, "set_voice_mode agent enable complete");
1428
1447
  }
1429
- this.sessionLogger.info({ agentId: this.voiceModeAgentId, elapsedMs: Date.now() - startedAt }, 'set_voice_mode starting voice turn controller');
1448
+ this.sessionLogger.info({ agentId: this.voiceModeAgentId, elapsedMs: Date.now() - startedAt }, "set_voice_mode starting voice turn controller");
1430
1449
  await this.startVoiceTurnController();
1431
- this.sessionLogger.info({ agentId: this.voiceModeAgentId, elapsedMs: Date.now() - startedAt }, 'set_voice_mode voice turn controller started');
1450
+ this.sessionLogger.info({ agentId: this.voiceModeAgentId, elapsedMs: Date.now() - startedAt }, "set_voice_mode voice turn controller started");
1432
1451
  this.isVoiceMode = true;
1433
1452
  this.sessionLogger.info({
1434
1453
  agentId: this.voiceModeAgentId,
1435
1454
  elapsedMs: Date.now() - startedAt,
1436
- }, 'Voice mode enabled for existing agent');
1455
+ }, "Voice mode enabled for existing agent");
1437
1456
  if (requestId) {
1438
1457
  this.emit({
1439
- type: 'set_voice_mode_response',
1458
+ type: "set_voice_mode_response",
1440
1459
  payload: {
1441
1460
  requestId,
1442
1461
  enabled: true,
@@ -1448,13 +1467,13 @@ export class Session {
1448
1467
  }
1449
1468
  return;
1450
1469
  }
1451
- this.sessionLogger.info({ agentId: this.voiceModeAgentId, elapsedMs: Date.now() - startedAt }, 'set_voice_mode disabling active voice mode');
1470
+ this.sessionLogger.info({ agentId: this.voiceModeAgentId, elapsedMs: Date.now() - startedAt }, "set_voice_mode disabling active voice mode");
1452
1471
  await this.disableVoiceModeForActiveAgent(true);
1453
1472
  this.isVoiceMode = false;
1454
- this.sessionLogger.info({ elapsedMs: Date.now() - startedAt }, 'Voice mode disabled');
1473
+ this.sessionLogger.info({ elapsedMs: Date.now() - startedAt }, "Voice mode disabled");
1455
1474
  if (requestId) {
1456
1475
  this.emit({
1457
- type: 'set_voice_mode_response',
1476
+ type: "set_voice_mode_response",
1458
1477
  payload: {
1459
1478
  requestId,
1460
1479
  enabled: false,
@@ -1466,17 +1485,17 @@ export class Session {
1466
1485
  }
1467
1486
  }
1468
1487
  catch (error) {
1469
- const errorMessage = error instanceof Error ? error.message : 'Failed to set voice mode';
1488
+ const errorMessage = error instanceof Error ? error.message : "Failed to set voice mode";
1470
1489
  const unavailable = this.getVoiceFeatureUnavailableResponseMetadata(error);
1471
1490
  this.sessionLogger.error({
1472
1491
  err: error,
1473
1492
  enabled,
1474
1493
  requestedAgentId: agentId ?? null,
1475
1494
  elapsedMs: Date.now() - startedAt,
1476
- }, 'set_voice_mode failed');
1495
+ }, "set_voice_mode failed");
1477
1496
  if (requestId) {
1478
1497
  this.emit({
1479
- type: 'set_voice_mode_response',
1498
+ type: "set_voice_mode_response",
1480
1499
  payload: {
1481
1500
  requestId,
1482
1501
  enabled: this.isVoiceMode,
@@ -1507,7 +1526,7 @@ export class Session {
1507
1526
  buildVoiceModeMcpServers(existing, socketPath) {
1508
1527
  const mcpStdio = this.voiceAgentMcpStdio;
1509
1528
  if (!mcpStdio) {
1510
- throw new Error('Voice MCP stdio bridge is not configured');
1529
+ throw new Error("Voice MCP stdio bridge is not configured");
1511
1530
  }
1512
1531
  return {
1513
1532
  ...(existing ?? {}),
@@ -1523,14 +1542,14 @@ export class Session {
1523
1542
  const startedAt = Date.now();
1524
1543
  const ensureVoiceSocket = this.ensureVoiceMcpSocketForAgent;
1525
1544
  if (!ensureVoiceSocket) {
1526
- throw new Error('Voice MCP socket bridge is not configured');
1545
+ throw new Error("Voice MCP socket bridge is not configured");
1527
1546
  }
1528
- this.sessionLogger.info({ agentId }, 'enableVoiceModeForAgent.ensureAgentLoaded.start');
1547
+ this.sessionLogger.info({ agentId }, "enableVoiceModeForAgent.ensureAgentLoaded.start");
1529
1548
  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');
1549
+ this.sessionLogger.info({ agentId, elapsedMs: Date.now() - startedAt }, "enableVoiceModeForAgent.ensureAgentLoaded.done");
1550
+ this.sessionLogger.info({ agentId }, "enableVoiceModeForAgent.ensureVoiceSocket.start");
1532
1551
  const socketPath = await ensureVoiceSocket(agentId);
1533
- this.sessionLogger.info({ agentId, socketPath, elapsedMs: Date.now() - startedAt }, 'enableVoiceModeForAgent.ensureVoiceSocket.done');
1552
+ this.sessionLogger.info({ agentId, socketPath, elapsedMs: Date.now() - startedAt }, "enableVoiceModeForAgent.ensureVoiceSocket.done");
1534
1553
  this.registerVoiceBridgeForAgent(agentId);
1535
1554
  const baseConfig = {
1536
1555
  systemPrompt: stripVoiceModeSystemPrompt(existing.config.systemPrompt),
@@ -1542,9 +1561,9 @@ export class Session {
1542
1561
  mcpServers: this.buildVoiceModeMcpServers(baseConfig.mcpServers, socketPath),
1543
1562
  };
1544
1563
  try {
1545
- this.sessionLogger.info({ agentId, elapsedMs: Date.now() - startedAt }, 'enableVoiceModeForAgent.reloadAgentSession.start');
1564
+ this.sessionLogger.info({ agentId, elapsedMs: Date.now() - startedAt }, "enableVoiceModeForAgent.reloadAgentSession.start");
1546
1565
  const refreshed = await this.agentManager.reloadAgentSession(agentId, refreshOverrides);
1547
- this.sessionLogger.info({ agentId, refreshedAgentId: refreshed.id, elapsedMs: Date.now() - startedAt }, 'enableVoiceModeForAgent.reloadAgentSession.done');
1566
+ this.sessionLogger.info({ agentId, refreshedAgentId: refreshed.id, elapsedMs: Date.now() - startedAt }, "enableVoiceModeForAgent.reloadAgentSession.done");
1548
1567
  return refreshed.id;
1549
1568
  }
1550
1569
  catch (error) {
@@ -1565,7 +1584,7 @@ export class Session {
1565
1584
  this.unregisterVoiceSpeakHandler?.(agentId);
1566
1585
  this.unregisterVoiceCallerContext?.(agentId);
1567
1586
  await this.removeVoiceMcpSocketForAgent?.(agentId).catch((error) => {
1568
- this.sessionLogger.warn({ err: error, agentId }, 'Failed to remove voice MCP socket bridge on disable');
1587
+ this.sessionLogger.warn({ err: error, agentId }, "Failed to remove voice MCP socket bridge on disable");
1569
1588
  });
1570
1589
  if (restoreAgentConfig && this.voiceModeBaseConfig) {
1571
1590
  const baseConfig = this.voiceModeBaseConfig;
@@ -1576,7 +1595,7 @@ export class Session {
1576
1595
  });
1577
1596
  }
1578
1597
  catch (error) {
1579
- this.sessionLogger.warn({ err: error, agentId }, 'Failed to restore agent config while disabling voice mode');
1598
+ this.sessionLogger.warn({ err: error, agentId }, "Failed to restore agent config while disabling voice mode");
1580
1599
  }
1581
1600
  }
1582
1601
  this.voiceModeBaseConfig = null;
@@ -1587,16 +1606,16 @@ export class Session {
1587
1606
  }
1588
1607
  async startVoiceTurnController() {
1589
1608
  if (this.voiceTurnController) {
1590
- this.sessionLogger.info('startVoiceTurnController skipped: already running');
1609
+ this.sessionLogger.info("startVoiceTurnController skipped: already running");
1591
1610
  return;
1592
1611
  }
1593
1612
  const turnDetection = this.resolveVoiceTurnDetection();
1594
1613
  if (!turnDetection) {
1595
- throw new Error('Voice turn detection is not configured');
1614
+ throw new Error("Voice turn detection is not configured");
1596
1615
  }
1597
- this.sessionLogger.info({ providerId: turnDetection.id }, 'startVoiceTurnController creating controller');
1616
+ this.sessionLogger.info({ providerId: turnDetection.id }, "startVoiceTurnController creating controller");
1598
1617
  const controller = createVoiceTurnController({
1599
- logger: this.sessionLogger.child({ component: 'voice-turn-controller' }),
1618
+ logger: this.sessionLogger.child({ component: "voice-turn-controller" }),
1600
1619
  turnDetection,
1601
1620
  utteranceSink: {
1602
1621
  submitUtterance: async ({ pcm16, format, sampleRate, startedAt, endedAt }) => {
@@ -1606,7 +1625,7 @@ export class Session {
1606
1625
  startedAt,
1607
1626
  endedAt,
1608
1627
  durationMs: Math.max(0, endedAt - startedAt),
1609
- }, 'Submitting detected voice utterance');
1628
+ }, "Submitting detected voice utterance");
1610
1629
  await this.processCompletedAudio(pcm16, format);
1611
1630
  },
1612
1631
  },
@@ -1618,20 +1637,20 @@ export class Session {
1618
1637
  this.handleVoiceSpeechStopped();
1619
1638
  },
1620
1639
  onError: (error) => {
1621
- this.sessionLogger.error({ err: error }, 'Voice turn controller failed');
1640
+ this.sessionLogger.error({ err: error }, "Voice turn controller failed");
1622
1641
  },
1623
1642
  },
1624
1643
  });
1625
- this.sessionLogger.info('startVoiceTurnController connecting controller');
1644
+ this.sessionLogger.info("startVoiceTurnController connecting controller");
1626
1645
  await controller.start();
1627
1646
  this.voiceTurnController = controller;
1628
- this.sessionLogger.info('startVoiceTurnController connected');
1647
+ this.sessionLogger.info("startVoiceTurnController connected");
1629
1648
  }
1630
1649
  async stopVoiceTurnController() {
1631
1650
  if (!this.voiceTurnController) {
1632
1651
  return;
1633
1652
  }
1634
- this.clearPendingVoiceSpeechStart('turn-controller-stop');
1653
+ this.clearPendingVoiceSpeechStart("turn-controller-stop");
1635
1654
  const controller = this.voiceTurnController;
1636
1655
  this.voiceTurnController = null;
1637
1656
  await controller.stop();
@@ -1642,7 +1661,7 @@ export class Session {
1642
1661
  this.pendingVoiceSpeechTimer = null;
1643
1662
  }
1644
1663
  if (this.pendingVoiceSpeechStartAt !== null) {
1645
- this.sessionLogger.debug({ reason }, 'Clearing provisional voice speech start');
1664
+ this.sessionLogger.debug({ reason }, "Clearing provisional voice speech start");
1646
1665
  this.pendingVoiceSpeechStartAt = null;
1647
1666
  }
1648
1667
  }
@@ -1652,16 +1671,16 @@ export class Session {
1652
1671
  }
1653
1672
  const startedAt = Date.now();
1654
1673
  this.pendingVoiceSpeechStartAt = startedAt;
1655
- this.sessionLogger.info({ confirmationMs: VOICE_INTERRUPT_CONFIRMATION_MS }, 'Silero VAD provisional speech_started');
1674
+ this.sessionLogger.info({ confirmationMs: VOICE_INTERRUPT_CONFIRMATION_MS }, "Silero VAD provisional speech_started");
1656
1675
  this.pendingVoiceSpeechTimer = setTimeout(() => {
1657
1676
  this.pendingVoiceSpeechTimer = null;
1658
1677
  if (this.pendingVoiceSpeechStartAt !== startedAt || this.speechInProgress) {
1659
1678
  return;
1660
1679
  }
1661
1680
  this.pendingVoiceSpeechStartAt = null;
1662
- this.sessionLogger.info('voice_input_state emitting isSpeaking=true');
1681
+ this.sessionLogger.info("voice_input_state emitting isSpeaking=true");
1663
1682
  this.emit({
1664
- type: 'voice_input_state',
1683
+ type: "voice_input_state",
1665
1684
  payload: {
1666
1685
  isSpeaking: true,
1667
1686
  },
@@ -1672,13 +1691,13 @@ export class Session {
1672
1691
  handleVoiceSpeechStopped() {
1673
1692
  if (this.pendingVoiceSpeechStartAt !== null) {
1674
1693
  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');
1694
+ this.clearPendingVoiceSpeechStart("speech-stopped-before-confirmation");
1695
+ this.sessionLogger.info({ durationMs, confirmationMs: VOICE_INTERRUPT_CONFIRMATION_MS }, "Ignoring provisional voice speech start that ended before confirmation");
1677
1696
  return;
1678
1697
  }
1679
- this.sessionLogger.info('voice_input_state emitting isSpeaking=false');
1698
+ this.sessionLogger.info("voice_input_state emitting isSpeaking=false");
1680
1699
  this.emit({
1681
- type: 'voice_input_state',
1700
+ type: "voice_input_state",
1682
1701
  payload: {
1683
1702
  isSpeaking: false,
1684
1703
  },
@@ -1688,13 +1707,13 @@ export class Session {
1688
1707
  * Handle text message to agent (with optional image attachments)
1689
1708
  */
1690
1709
  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)` : ''}`);
1710
+ 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
1711
  await this.unarchiveAgentState(agentId);
1693
1712
  try {
1694
1713
  await this.ensureAgentLoaded(agentId);
1695
1714
  }
1696
1715
  catch (error) {
1697
- this.handleAgentRunError(agentId, error, 'Failed to initialize agent before sending prompt');
1716
+ this.handleAgentRunError(agentId, error, "Failed to initialize agent before sending prompt");
1698
1717
  return;
1699
1718
  }
1700
1719
  try {
@@ -1714,7 +1733,7 @@ export class Session {
1714
1733
  */
1715
1734
  async handleCreateAgentRequest(msg) {
1716
1735
  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}` : ''}`);
1736
+ this.sessionLogger.info({ cwd: config.cwd, provider: config.provider, worktreeName }, `Creating agent in ${config.cwd} (${config.provider})${worktreeName ? ` with worktree ${worktreeName}` : ""}`);
1718
1737
  try {
1719
1738
  const trimmedPrompt = initialPrompt?.trim();
1720
1739
  const { explicitTitle, provisionalTitle } = resolveCreateAgentTitles({
@@ -1735,9 +1754,9 @@ export class Session {
1735
1754
  throw new Error(`Agent ${snapshot.id} not found after creation`);
1736
1755
  }
1737
1756
  this.emit({
1738
- type: 'status',
1757
+ type: "status",
1739
1758
  payload: {
1740
- status: 'agent_created',
1759
+ status: "agent_created",
1741
1760
  agentId: snapshot.id,
1742
1761
  requestId,
1743
1762
  agent: agentPayload,
@@ -1757,11 +1776,11 @@ export class Session {
1757
1776
  void this.handleSendAgentMessage(snapshot.id, trimmedPrompt, resolveClientMessageId(clientMessageId), images, outputSchema ? { outputSchema } : undefined).catch((promptError) => {
1758
1777
  this.sessionLogger.error({ err: promptError, agentId: snapshot.id }, `Failed to run initial prompt for agent ${snapshot.id}`);
1759
1778
  this.emit({
1760
- type: 'activity_log',
1779
+ type: "activity_log",
1761
1780
  payload: {
1762
1781
  id: uuidv4(),
1763
1782
  timestamp: new Date(),
1764
- type: 'error',
1783
+ type: "error",
1765
1784
  content: `Initial prompt failed: ${promptError?.message ?? promptError}`,
1766
1785
  },
1767
1786
  });
@@ -1788,23 +1807,23 @@ export class Session {
1788
1807
  this.sessionLogger.info({ agentId: snapshot.id, provider: snapshot.provider }, `Created agent ${snapshot.id} (${snapshot.provider})`);
1789
1808
  }
1790
1809
  catch (error) {
1791
- this.sessionLogger.error({ err: error }, 'Failed to create agent');
1810
+ this.sessionLogger.error({ err: error }, "Failed to create agent");
1792
1811
  if (requestId) {
1793
1812
  this.emit({
1794
- type: 'status',
1813
+ type: "status",
1795
1814
  payload: {
1796
- status: 'agent_create_failed',
1815
+ status: "agent_create_failed",
1797
1816
  requestId,
1798
1817
  error: error?.message ?? String(error),
1799
1818
  },
1800
1819
  });
1801
1820
  }
1802
1821
  this.emit({
1803
- type: 'activity_log',
1822
+ type: "activity_log",
1804
1823
  payload: {
1805
1824
  id: uuidv4(),
1806
1825
  timestamp: new Date(),
1807
- type: 'error',
1826
+ type: "error",
1808
1827
  content: `Failed to create agent: ${error.message}`,
1809
1828
  },
1810
1829
  });
@@ -1813,14 +1832,14 @@ export class Session {
1813
1832
  async handleResumeAgentRequest(msg) {
1814
1833
  const { handle, overrides, requestId } = msg;
1815
1834
  if (!handle) {
1816
- this.sessionLogger.warn('Resume request missing persistence handle');
1835
+ this.sessionLogger.warn("Resume request missing persistence handle");
1817
1836
  this.emit({
1818
- type: 'activity_log',
1837
+ type: "activity_log",
1819
1838
  payload: {
1820
1839
  id: uuidv4(),
1821
1840
  timestamp: new Date(),
1822
- type: 'error',
1823
- content: 'Unable to resume agent: missing persistence handle',
1841
+ type: "error",
1842
+ content: "Unable to resume agent: missing persistence handle",
1824
1843
  },
1825
1844
  });
1826
1845
  return;
@@ -1839,9 +1858,9 @@ export class Session {
1839
1858
  throw new Error(`Agent ${snapshot.id} not found after resume`);
1840
1859
  }
1841
1860
  this.emit({
1842
- type: 'status',
1861
+ type: "status",
1843
1862
  payload: {
1844
- status: 'agent_resumed',
1863
+ status: "agent_resumed",
1845
1864
  agentId: snapshot.id,
1846
1865
  requestId,
1847
1866
  timelineSize,
@@ -1851,13 +1870,13 @@ export class Session {
1851
1870
  }
1852
1871
  }
1853
1872
  catch (error) {
1854
- this.sessionLogger.error({ err: error }, 'Failed to resume agent');
1873
+ this.sessionLogger.error({ err: error }, "Failed to resume agent");
1855
1874
  this.emit({
1856
- type: 'activity_log',
1875
+ type: "activity_log",
1857
1876
  payload: {
1858
1877
  id: uuidv4(),
1859
1878
  timestamp: new Date(),
1860
- type: 'error',
1879
+ type: "error",
1861
1880
  content: `Failed to resume agent: ${error.message}`,
1862
1881
  },
1863
1882
  });
@@ -1895,9 +1914,9 @@ export class Session {
1895
1914
  const timelineSize = this.agentManager.getTimeline(agentId).length;
1896
1915
  if (requestId) {
1897
1916
  this.emit({
1898
- type: 'status',
1917
+ type: "status",
1899
1918
  payload: {
1900
- status: 'agent_refreshed',
1919
+ status: "agent_refreshed",
1901
1920
  agentId,
1902
1921
  requestId,
1903
1922
  timelineSize,
@@ -1908,11 +1927,11 @@ export class Session {
1908
1927
  catch (error) {
1909
1928
  this.sessionLogger.error({ err: error, agentId }, `Failed to refresh agent ${agentId}`);
1910
1929
  this.emit({
1911
- type: 'activity_log',
1930
+ type: "activity_log",
1912
1931
  payload: {
1913
1932
  id: uuidv4(),
1914
1933
  timestamp: new Date(),
1915
- type: 'error',
1934
+ type: "error",
1916
1935
  content: `Failed to refresh agent: ${error.message}`,
1917
1936
  },
1918
1937
  });
@@ -1924,7 +1943,7 @@ export class Session {
1924
1943
  await this.interruptAgentIfRunning(agentId);
1925
1944
  }
1926
1945
  catch (error) {
1927
- this.handleAgentRunError(agentId, error, 'Failed to cancel running agent on request');
1946
+ this.handleAgentRunError(agentId, error, "Failed to cancel running agent on request");
1928
1947
  }
1929
1948
  }
1930
1949
  async buildAgentSessionConfig(config, gitOptions, legacyWorktreeName, _labels) {
@@ -1946,20 +1965,21 @@ export class Session {
1946
1965
  }
1947
1966
  else {
1948
1967
  // Resolve current branch name from HEAD
1949
- const { stdout } = await execAsync('git rev-parse --abbrev-ref HEAD', {
1968
+ const { stdout } = await execAsync("git rev-parse --abbrev-ref HEAD", {
1950
1969
  cwd,
1951
1970
  env: READ_ONLY_GIT_ENV,
1952
1971
  });
1953
1972
  targetBranch = stdout.trim();
1954
1973
  }
1955
1974
  if (!targetBranch) {
1956
- throw new Error('A branch name is required when creating a worktree.');
1975
+ throw new Error("A branch name is required when creating a worktree.");
1957
1976
  }
1958
1977
  this.sessionLogger.info({ worktreeSlug: normalized.worktreeSlug ?? targetBranch, branch: targetBranch }, `Creating worktree '${normalized.worktreeSlug ?? targetBranch}' for branch ${targetBranch}`);
1978
+ const baseBranch = normalized.baseBranch ?? (await this.resolveGitCreateBaseBranch(cwd));
1959
1979
  const createdWorktree = await createAgentWorktree({
1960
1980
  branchName: targetBranch,
1961
1981
  cwd,
1962
- baseBranch: normalized.baseBranch,
1982
+ baseBranch,
1963
1983
  worktreeSlug: normalized.worktreeSlug ?? targetBranch,
1964
1984
  paseoHome: this.paseoHome,
1965
1985
  });
@@ -1967,9 +1987,10 @@ export class Session {
1967
1987
  worktreeConfig = createdWorktree;
1968
1988
  }
1969
1989
  else if (normalized.createNewBranch) {
1990
+ const baseBranch = normalized.baseBranch ?? (await this.resolveGitCreateBaseBranch(cwd));
1970
1991
  await this.createBranchFromBase({
1971
1992
  cwd,
1972
- baseBranch: normalized.baseBranch,
1993
+ baseBranch,
1973
1994
  newBranchName: normalized.newBranchName,
1974
1995
  });
1975
1996
  }
@@ -1991,7 +2012,7 @@ export class Session {
1991
2012
  cwd: msg.cwd ? expandTilde(msg.cwd) : undefined,
1992
2013
  });
1993
2014
  this.emit({
1994
- type: 'list_provider_models_response',
2015
+ type: "list_provider_models_response",
1995
2016
  payload: {
1996
2017
  provider: msg.provider,
1997
2018
  models,
@@ -2004,7 +2025,7 @@ export class Session {
2004
2025
  catch (error) {
2005
2026
  this.sessionLogger.error({ err: error, provider: msg.provider }, `Failed to list models for ${msg.provider}`);
2006
2027
  this.emit({
2007
- type: 'list_provider_models_response',
2028
+ type: "list_provider_models_response",
2008
2029
  payload: {
2009
2030
  provider: msg.provider,
2010
2031
  error: error?.message ?? String(error),
@@ -2019,7 +2040,7 @@ export class Session {
2019
2040
  try {
2020
2041
  const providers = await this.agentManager.listProviderAvailability();
2021
2042
  this.emit({
2022
- type: 'list_available_providers_response',
2043
+ type: "list_available_providers_response",
2023
2044
  payload: {
2024
2045
  providers,
2025
2046
  error: null,
@@ -2029,9 +2050,9 @@ export class Session {
2029
2050
  });
2030
2051
  }
2031
2052
  catch (error) {
2032
- this.sessionLogger.error({ err: error }, 'Failed to list provider availability');
2053
+ this.sessionLogger.error({ err: error }, "Failed to list provider availability");
2033
2054
  this.emit({
2034
- type: 'list_available_providers_response',
2055
+ type: "list_available_providers_response",
2035
2056
  payload: {
2036
2057
  providers: [],
2037
2058
  error: error?.message ?? String(error),
@@ -2071,7 +2092,7 @@ export class Session {
2071
2092
  };
2072
2093
  }));
2073
2094
  this.emit({
2074
- type: 'speech_models_list_response',
2095
+ type: "speech_models_list_response",
2075
2096
  payload: {
2076
2097
  modelsDir,
2077
2098
  models,
@@ -2086,11 +2107,11 @@ export class Session {
2086
2107
  const invalid = modelIdsRaw.filter((id) => !allModelIds.has(id));
2087
2108
  if (invalid.length > 0) {
2088
2109
  this.emit({
2089
- type: 'speech_models_download_response',
2110
+ type: "speech_models_download_response",
2090
2111
  payload: {
2091
2112
  modelsDir,
2092
2113
  downloadedModelIds: [],
2093
- error: `Unknown speech model id(s): ${invalid.join(', ')}`,
2114
+ error: `Unknown speech model id(s): ${invalid.join(", ")}`,
2094
2115
  requestId: msg.requestId,
2095
2116
  },
2096
2117
  });
@@ -2104,7 +2125,7 @@ export class Session {
2104
2125
  logger: this.sessionLogger,
2105
2126
  });
2106
2127
  this.emit({
2107
- type: 'speech_models_download_response',
2128
+ type: "speech_models_download_response",
2108
2129
  payload: {
2109
2130
  modelsDir,
2110
2131
  downloadedModelIds: modelIds,
@@ -2114,9 +2135,9 @@ export class Session {
2114
2135
  });
2115
2136
  }
2116
2137
  catch (error) {
2117
- this.sessionLogger.error({ err: error, modelIds }, 'Failed to download speech models');
2138
+ this.sessionLogger.error({ err: error, modelIds }, "Failed to download speech models");
2118
2139
  this.emit({
2119
- type: 'speech_models_download_response',
2140
+ type: "speech_models_download_response",
2120
2141
  payload: {
2121
2142
  modelsDir,
2122
2143
  downloadedModelIds: [],
@@ -2150,17 +2171,11 @@ export class Session {
2150
2171
  return null;
2151
2172
  }
2152
2173
  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');
2174
+ this.assertSafeGitRef(baseBranch, "base branch");
2160
2175
  }
2161
2176
  if (createNewBranch) {
2162
2177
  if (!normalizedBranchName) {
2163
- throw new Error('New branch name is required');
2178
+ throw new Error("New branch name is required");
2164
2179
  }
2165
2180
  const validation = validateBranchSlug(normalizedBranchName);
2166
2181
  if (!validation.valid) {
@@ -2182,24 +2197,36 @@ export class Session {
2182
2197
  };
2183
2198
  }
2184
2199
  assertSafeGitRef(ref, label) {
2185
- if (!SAFE_GIT_REF_PATTERN.test(ref) || ref.includes('..') || ref.includes('@{')) {
2200
+ if (!SAFE_GIT_REF_PATTERN.test(ref) || ref.includes("..") || ref.includes("@{")) {
2186
2201
  throw new Error(`Invalid ${label}: ${ref}`);
2187
2202
  }
2188
2203
  }
2204
+ async resolveGitCreateBaseBranch(cwd) {
2205
+ const checkout = await getCheckoutStatusLite(cwd, { paseoHome: this.paseoHome });
2206
+ if (!checkout.isGit) {
2207
+ throw new Error("Cannot create a worktree outside a git repository");
2208
+ }
2209
+ const repoRoot = checkout.isPaseoOwnedWorktree ? checkout.mainRepoRoot : cwd;
2210
+ const baseBranch = await resolveRepositoryDefaultBranch(repoRoot);
2211
+ if (!baseBranch) {
2212
+ throw new Error("Unable to resolve repository default branch");
2213
+ }
2214
+ return baseBranch;
2215
+ }
2189
2216
  toCheckoutError(error) {
2190
2217
  if (error instanceof NotGitRepoError) {
2191
- return { code: 'NOT_GIT_REPO', message: error.message };
2218
+ return { code: "NOT_GIT_REPO", message: error.message };
2192
2219
  }
2193
2220
  if (error instanceof MergeConflictError) {
2194
- return { code: 'MERGE_CONFLICT', message: error.message };
2221
+ return { code: "MERGE_CONFLICT", message: error.message };
2195
2222
  }
2196
2223
  if (error instanceof MergeFromBaseConflictError) {
2197
- return { code: 'MERGE_CONFLICT', message: error.message };
2224
+ return { code: "MERGE_CONFLICT", message: error.message };
2198
2225
  }
2199
2226
  if (error instanceof Error) {
2200
- return { code: 'UNKNOWN', message: error.message };
2227
+ return { code: "UNKNOWN", message: error.message };
2201
2228
  }
2202
- return { code: 'UNKNOWN', message: String(error) };
2229
+ return { code: "UNKNOWN", message: String(error) };
2203
2230
  }
2204
2231
  isPathWithinRoot(rootPath, candidatePath) {
2205
2232
  const resolvedRoot = resolve(rootPath);
@@ -2210,47 +2237,47 @@ export class Session {
2210
2237
  return resolvedCandidate.startsWith(resolvedRoot + sep);
2211
2238
  }
2212
2239
  async generateCommitMessage(cwd) {
2213
- const diff = await getCheckoutDiff(cwd, { mode: 'uncommitted', includeStructured: true }, { paseoHome: this.paseoHome });
2240
+ const diff = await getCheckoutDiff(cwd, { mode: "uncommitted", includeStructured: true }, { paseoHome: this.paseoHome });
2214
2241
  const schema = z.object({
2215
2242
  message: z
2216
2243
  .string()
2217
2244
  .min(1)
2218
2245
  .max(72)
2219
- .describe('Concise git commit message, imperative mood, no trailing period.'),
2246
+ .describe("Concise git commit message, imperative mood, no trailing period."),
2220
2247
  });
2221
2248
  const fileList = diff.structured && diff.structured.length > 0
2222
2249
  ? [
2223
- 'Files changed:',
2250
+ "Files changed:",
2224
2251
  ...diff.structured.map((file) => {
2225
- const changeType = file.isNew ? 'A' : file.isDeleted ? 'D' : 'M';
2226
- const status = file.status && file.status !== 'ok' ? ` [${file.status}]` : '';
2252
+ const changeType = file.isNew ? "A" : file.isDeleted ? "D" : "M";
2253
+ const status = file.status && file.status !== "ok" ? ` [${file.status}]` : "";
2227
2254
  return `${changeType}\t${file.path}\t(+${file.additions} -${file.deletions})${status}`;
2228
2255
  }),
2229
- ].join('\n')
2230
- : 'Files changed: (unknown)';
2256
+ ].join("\n")
2257
+ : "Files changed: (unknown)";
2231
2258
  const maxPatchChars = 120000;
2232
2259
  const patch = diff.diff.length > maxPatchChars
2233
2260
  ? `${diff.diff.slice(0, maxPatchChars)}\n\n... (diff truncated to ${maxPatchChars} chars)\n`
2234
2261
  : diff.diff;
2235
2262
  const prompt = [
2236
- 'Write a concise git commit message for the changes below.',
2263
+ "Write a concise git commit message for the changes below.",
2237
2264
  "Return JSON only with a single field 'message'.",
2238
- '',
2265
+ "",
2239
2266
  fileList,
2240
- '',
2241
- patch.length > 0 ? patch : '(No diff available)',
2242
- ].join('\n');
2267
+ "",
2268
+ patch.length > 0 ? patch : "(No diff available)",
2269
+ ].join("\n");
2243
2270
  try {
2244
2271
  const result = await generateStructuredAgentResponseWithFallback({
2245
2272
  manager: this.agentManager,
2246
2273
  cwd,
2247
2274
  prompt,
2248
2275
  schema,
2249
- schemaName: 'CommitMessage',
2276
+ schemaName: "CommitMessage",
2250
2277
  maxRetries: 2,
2251
2278
  providers: DEFAULT_STRUCTURED_GENERATION_PROVIDERS,
2252
2279
  agentConfigOverrides: {
2253
- title: 'Commit generator',
2280
+ title: "Commit generator",
2254
2281
  internal: true,
2255
2282
  },
2256
2283
  });
@@ -2259,14 +2286,14 @@ export class Session {
2259
2286
  catch (error) {
2260
2287
  if (error instanceof StructuredAgentResponseError ||
2261
2288
  error instanceof StructuredAgentFallbackError) {
2262
- return 'Update files';
2289
+ return "Update files";
2263
2290
  }
2264
2291
  throw error;
2265
2292
  }
2266
2293
  }
2267
2294
  async generatePullRequestText(cwd, baseRef) {
2268
2295
  const diff = await getCheckoutDiff(cwd, {
2269
- mode: 'base',
2296
+ mode: "base",
2270
2297
  baseRef,
2271
2298
  includeStructured: true,
2272
2299
  }, { paseoHome: this.paseoHome });
@@ -2276,37 +2303,37 @@ export class Session {
2276
2303
  });
2277
2304
  const fileList = diff.structured && diff.structured.length > 0
2278
2305
  ? [
2279
- 'Files changed:',
2306
+ "Files changed:",
2280
2307
  ...diff.structured.map((file) => {
2281
- const changeType = file.isNew ? 'A' : file.isDeleted ? 'D' : 'M';
2282
- const status = file.status && file.status !== 'ok' ? ` [${file.status}]` : '';
2308
+ const changeType = file.isNew ? "A" : file.isDeleted ? "D" : "M";
2309
+ const status = file.status && file.status !== "ok" ? ` [${file.status}]` : "";
2283
2310
  return `${changeType}\t${file.path}\t(+${file.additions} -${file.deletions})${status}`;
2284
2311
  }),
2285
- ].join('\n')
2286
- : 'Files changed: (unknown)';
2312
+ ].join("\n")
2313
+ : "Files changed: (unknown)";
2287
2314
  const maxPatchChars = 200000;
2288
2315
  const patch = diff.diff.length > maxPatchChars
2289
2316
  ? `${diff.diff.slice(0, maxPatchChars)}\n\n... (diff truncated to ${maxPatchChars} chars)\n`
2290
2317
  : diff.diff;
2291
2318
  const prompt = [
2292
- 'Write a pull request title and body for the changes below.',
2319
+ "Write a pull request title and body for the changes below.",
2293
2320
  "Return JSON only with fields 'title' and 'body'.",
2294
- '',
2321
+ "",
2295
2322
  fileList,
2296
- '',
2297
- patch.length > 0 ? patch : '(No diff available)',
2298
- ].join('\n');
2323
+ "",
2324
+ patch.length > 0 ? patch : "(No diff available)",
2325
+ ].join("\n");
2299
2326
  try {
2300
2327
  return await generateStructuredAgentResponseWithFallback({
2301
2328
  manager: this.agentManager,
2302
2329
  cwd,
2303
2330
  prompt,
2304
2331
  schema,
2305
- schemaName: 'PullRequest',
2332
+ schemaName: "PullRequest",
2306
2333
  maxRetries: 2,
2307
2334
  providers: DEFAULT_STRUCTURED_GENERATION_PROVIDERS,
2308
2335
  agentConfigOverrides: {
2309
- title: 'PR generator',
2336
+ title: "PR generator",
2310
2337
  internal: true,
2311
2338
  },
2312
2339
  });
@@ -2315,8 +2342,8 @@ export class Session {
2315
2342
  if (error instanceof StructuredAgentResponseError ||
2316
2343
  error instanceof StructuredAgentFallbackError) {
2317
2344
  return {
2318
- title: 'Update changes',
2319
- body: 'Automated PR generated by Paseo.',
2345
+ title: "Update changes",
2346
+ body: "Automated PR generated by Paseo.",
2320
2347
  };
2321
2348
  }
2322
2349
  throw error;
@@ -2325,12 +2352,12 @@ export class Session {
2325
2352
  async ensureCleanWorkingTree(cwd) {
2326
2353
  const dirty = await this.isWorkingTreeDirty(cwd);
2327
2354
  if (dirty) {
2328
- throw new Error('Working directory has uncommitted changes. Commit or stash before switching branches.');
2355
+ throw new Error("Working directory has uncommitted changes. Commit or stash before switching branches.");
2329
2356
  }
2330
2357
  }
2331
2358
  async isWorkingTreeDirty(cwd) {
2332
2359
  try {
2333
- const { stdout } = await execAsync('git status --porcelain', {
2360
+ const { stdout } = await execAsync("git status --porcelain", {
2334
2361
  cwd,
2335
2362
  env: READ_ONLY_GIT_ENV,
2336
2363
  });
@@ -2341,14 +2368,14 @@ export class Session {
2341
2368
  }
2342
2369
  }
2343
2370
  async checkoutExistingBranch(cwd, branch) {
2344
- this.assertSafeGitRef(branch, 'branch');
2371
+ this.assertSafeGitRef(branch, "branch");
2345
2372
  try {
2346
2373
  await execAsync(`git rev-parse --verify ${branch}`, { cwd });
2347
2374
  }
2348
2375
  catch (error) {
2349
2376
  throw new Error(`Branch not found: ${branch}`);
2350
2377
  }
2351
- const { stdout } = await execAsync('git rev-parse --abbrev-ref HEAD', {
2378
+ const { stdout } = await execAsync("git rev-parse --abbrev-ref HEAD", {
2352
2379
  cwd,
2353
2380
  });
2354
2381
  const current = stdout.trim();
@@ -2360,7 +2387,7 @@ export class Session {
2360
2387
  }
2361
2388
  async createBranchFromBase(params) {
2362
2389
  const { cwd, baseBranch, newBranchName } = params;
2363
- this.assertSafeGitRef(baseBranch, 'base branch');
2390
+ this.assertSafeGitRef(baseBranch, "base branch");
2364
2391
  try {
2365
2392
  await execAsync(`git rev-parse --verify ${baseBranch}`, { cwd });
2366
2393
  }
@@ -2391,97 +2418,97 @@ export class Session {
2391
2418
  * Handle set agent mode request
2392
2419
  */
2393
2420
  async handleSetAgentModeRequest(agentId, modeId, requestId) {
2394
- this.sessionLogger.info({ agentId, modeId, requestId }, 'session: set_agent_mode_request');
2421
+ this.sessionLogger.info({ agentId, modeId, requestId }, "session: set_agent_mode_request");
2395
2422
  try {
2396
2423
  await this.agentManager.setAgentMode(agentId, modeId);
2397
- this.sessionLogger.info({ agentId, modeId, requestId }, 'session: set_agent_mode_request success');
2424
+ this.sessionLogger.info({ agentId, modeId, requestId }, "session: set_agent_mode_request success");
2398
2425
  this.emit({
2399
- type: 'set_agent_mode_response',
2426
+ type: "set_agent_mode_response",
2400
2427
  payload: { requestId, agentId, accepted: true, error: null },
2401
2428
  });
2402
2429
  }
2403
2430
  catch (error) {
2404
- this.sessionLogger.error({ err: error, agentId, modeId, requestId }, 'session: set_agent_mode_request error');
2431
+ this.sessionLogger.error({ err: error, agentId, modeId, requestId }, "session: set_agent_mode_request error");
2405
2432
  this.emit({
2406
- type: 'activity_log',
2433
+ type: "activity_log",
2407
2434
  payload: {
2408
2435
  id: uuidv4(),
2409
2436
  timestamp: new Date(),
2410
- type: 'error',
2437
+ type: "error",
2411
2438
  content: `Failed to set agent mode: ${error.message}`,
2412
2439
  },
2413
2440
  });
2414
2441
  this.emit({
2415
- type: 'set_agent_mode_response',
2442
+ type: "set_agent_mode_response",
2416
2443
  payload: {
2417
2444
  requestId,
2418
2445
  agentId,
2419
2446
  accepted: false,
2420
- error: error?.message ? String(error.message) : 'Failed to set agent mode',
2447
+ error: error?.message ? String(error.message) : "Failed to set agent mode",
2421
2448
  },
2422
2449
  });
2423
2450
  }
2424
2451
  }
2425
2452
  async handleSetAgentModelRequest(agentId, modelId, requestId) {
2426
- this.sessionLogger.info({ agentId, modelId, requestId }, 'session: set_agent_model_request');
2453
+ this.sessionLogger.info({ agentId, modelId, requestId }, "session: set_agent_model_request");
2427
2454
  try {
2428
2455
  await this.agentManager.setAgentModel(agentId, modelId);
2429
- this.sessionLogger.info({ agentId, modelId, requestId }, 'session: set_agent_model_request success');
2456
+ this.sessionLogger.info({ agentId, modelId, requestId }, "session: set_agent_model_request success");
2430
2457
  this.emit({
2431
- type: 'set_agent_model_response',
2458
+ type: "set_agent_model_response",
2432
2459
  payload: { requestId, agentId, accepted: true, error: null },
2433
2460
  });
2434
2461
  }
2435
2462
  catch (error) {
2436
- this.sessionLogger.error({ err: error, agentId, modelId, requestId }, 'session: set_agent_model_request error');
2463
+ this.sessionLogger.error({ err: error, agentId, modelId, requestId }, "session: set_agent_model_request error");
2437
2464
  this.emit({
2438
- type: 'activity_log',
2465
+ type: "activity_log",
2439
2466
  payload: {
2440
2467
  id: uuidv4(),
2441
2468
  timestamp: new Date(),
2442
- type: 'error',
2469
+ type: "error",
2443
2470
  content: `Failed to set agent model: ${error.message}`,
2444
2471
  },
2445
2472
  });
2446
2473
  this.emit({
2447
- type: 'set_agent_model_response',
2474
+ type: "set_agent_model_response",
2448
2475
  payload: {
2449
2476
  requestId,
2450
2477
  agentId,
2451
2478
  accepted: false,
2452
- error: error?.message ? String(error.message) : 'Failed to set agent model',
2479
+ error: error?.message ? String(error.message) : "Failed to set agent model",
2453
2480
  },
2454
2481
  });
2455
2482
  }
2456
2483
  }
2457
2484
  async handleSetAgentThinkingRequest(agentId, thinkingOptionId, requestId) {
2458
- this.sessionLogger.info({ agentId, thinkingOptionId, requestId }, 'session: set_agent_thinking_request');
2485
+ this.sessionLogger.info({ agentId, thinkingOptionId, requestId }, "session: set_agent_thinking_request");
2459
2486
  try {
2460
2487
  await this.agentManager.setAgentThinkingOption(agentId, thinkingOptionId);
2461
- this.sessionLogger.info({ agentId, thinkingOptionId, requestId }, 'session: set_agent_thinking_request success');
2488
+ this.sessionLogger.info({ agentId, thinkingOptionId, requestId }, "session: set_agent_thinking_request success");
2462
2489
  this.emit({
2463
- type: 'set_agent_thinking_response',
2490
+ type: "set_agent_thinking_response",
2464
2491
  payload: { requestId, agentId, accepted: true, error: null },
2465
2492
  });
2466
2493
  }
2467
2494
  catch (error) {
2468
- this.sessionLogger.error({ err: error, agentId, thinkingOptionId, requestId }, 'session: set_agent_thinking_request error');
2495
+ this.sessionLogger.error({ err: error, agentId, thinkingOptionId, requestId }, "session: set_agent_thinking_request error");
2469
2496
  this.emit({
2470
- type: 'activity_log',
2497
+ type: "activity_log",
2471
2498
  payload: {
2472
2499
  id: uuidv4(),
2473
2500
  timestamp: new Date(),
2474
- type: 'error',
2501
+ type: "error",
2475
2502
  content: `Failed to set agent thinking option: ${error.message}`,
2476
2503
  },
2477
2504
  });
2478
2505
  this.emit({
2479
- type: 'set_agent_thinking_response',
2506
+ type: "set_agent_thinking_response",
2480
2507
  payload: {
2481
2508
  requestId,
2482
2509
  agentId,
2483
2510
  accepted: false,
2484
- error: error?.message ? String(error.message) : 'Failed to set agent thinking option',
2511
+ error: error?.message ? String(error.message) : "Failed to set agent thinking option",
2485
2512
  },
2486
2513
  });
2487
2514
  }
@@ -2495,7 +2522,7 @@ export class Session {
2495
2522
  await Promise.all(agentIds.map((id) => this.agentManager.clearAgentAttention(id)));
2496
2523
  }
2497
2524
  catch (error) {
2498
- this.sessionLogger.error({ err: error, agentIds }, 'Failed to clear agent attention');
2525
+ this.sessionLogger.error({ err: error, agentIds }, "Failed to clear agent attention");
2499
2526
  // Don't throw - this is not critical
2500
2527
  }
2501
2528
  }
@@ -2519,7 +2546,7 @@ export class Session {
2519
2546
  */
2520
2547
  handleRegisterPushToken(token) {
2521
2548
  this.pushTokenStore.addToken(token);
2522
- this.sessionLogger.info('Registered push token');
2549
+ this.sessionLogger.info("Registered push token");
2523
2550
  }
2524
2551
  /**
2525
2552
  * Handle list commands request for an agent
@@ -2533,7 +2560,7 @@ export class Session {
2533
2560
  if (agent?.session?.listCommands) {
2534
2561
  const commands = await agent.session.listCommands();
2535
2562
  this.emit({
2536
- type: 'list_commands_response',
2563
+ type: "list_commands_response",
2537
2564
  payload: {
2538
2565
  agentId,
2539
2566
  commands,
@@ -2555,7 +2582,7 @@ export class Session {
2555
2582
  };
2556
2583
  const commands = await this.agentManager.listDraftCommands(sessionConfig);
2557
2584
  this.emit({
2558
- type: 'list_commands_response',
2585
+ type: "list_commands_response",
2559
2586
  payload: {
2560
2587
  agentId,
2561
2588
  commands,
@@ -2566,7 +2593,7 @@ export class Session {
2566
2593
  return;
2567
2594
  }
2568
2595
  this.emit({
2569
- type: 'list_commands_response',
2596
+ type: "list_commands_response",
2570
2597
  payload: {
2571
2598
  agentId,
2572
2599
  commands: [],
@@ -2576,9 +2603,9 @@ export class Session {
2576
2603
  });
2577
2604
  }
2578
2605
  catch (error) {
2579
- this.sessionLogger.error({ err: error, agentId, draftConfig }, 'Failed to list commands');
2606
+ this.sessionLogger.error({ err: error, agentId, draftConfig }, "Failed to list commands");
2580
2607
  this.emit({
2581
- type: 'list_commands_response',
2608
+ type: "list_commands_response",
2582
2609
  payload: {
2583
2610
  agentId,
2584
2611
  commands: [],
@@ -2598,13 +2625,13 @@ export class Session {
2598
2625
  this.sessionLogger.debug({ agentId }, `Permission response forwarded to agent ${agentId}`);
2599
2626
  }
2600
2627
  catch (error) {
2601
- this.sessionLogger.error({ err: error, agentId, requestId }, 'Failed to respond to permission');
2628
+ this.sessionLogger.error({ err: error, agentId, requestId }, "Failed to respond to permission");
2602
2629
  this.emit({
2603
- type: 'activity_log',
2630
+ type: "activity_log",
2604
2631
  payload: {
2605
2632
  id: uuidv4(),
2606
2633
  timestamp: new Date(),
2607
- type: 'error',
2634
+ type: "error",
2608
2635
  content: `Failed to respond to permission: ${error.message}`,
2609
2636
  },
2610
2637
  });
@@ -2618,7 +2645,7 @@ export class Session {
2618
2645
  const status = await getCheckoutStatus(resolvedCwd, { paseoHome: this.paseoHome });
2619
2646
  if (!status.isGit) {
2620
2647
  this.emit({
2621
- type: 'checkout_status_response',
2648
+ type: "checkout_status_response",
2622
2649
  payload: {
2623
2650
  cwd,
2624
2651
  isGit: false,
@@ -2640,7 +2667,7 @@ export class Session {
2640
2667
  }
2641
2668
  if (status.isPaseoOwnedWorktree) {
2642
2669
  this.emit({
2643
- type: 'checkout_status_response',
2670
+ type: "checkout_status_response",
2644
2671
  payload: {
2645
2672
  cwd,
2646
2673
  isGit: true,
@@ -2662,7 +2689,7 @@ export class Session {
2662
2689
  return;
2663
2690
  }
2664
2691
  this.emit({
2665
- type: 'checkout_status_response',
2692
+ type: "checkout_status_response",
2666
2693
  payload: {
2667
2694
  cwd,
2668
2695
  isGit: true,
@@ -2683,7 +2710,7 @@ export class Session {
2683
2710
  }
2684
2711
  catch (error) {
2685
2712
  this.emit({
2686
- type: 'checkout_status_response',
2713
+ type: "checkout_status_response",
2687
2714
  payload: {
2688
2715
  cwd,
2689
2716
  isGit: false,
@@ -2714,7 +2741,7 @@ export class Session {
2714
2741
  env: READ_ONLY_GIT_ENV,
2715
2742
  });
2716
2743
  this.emit({
2717
- type: 'validate_branch_response',
2744
+ type: "validate_branch_response",
2718
2745
  payload: {
2719
2746
  exists: true,
2720
2747
  resolvedRef: branchName,
@@ -2735,7 +2762,7 @@ export class Session {
2735
2762
  env: READ_ONLY_GIT_ENV,
2736
2763
  });
2737
2764
  this.emit({
2738
- type: 'validate_branch_response',
2765
+ type: "validate_branch_response",
2739
2766
  payload: {
2740
2767
  exists: true,
2741
2768
  resolvedRef: `origin/${branchName}`,
@@ -2751,7 +2778,7 @@ export class Session {
2751
2778
  }
2752
2779
  // Branch not found anywhere
2753
2780
  this.emit({
2754
- type: 'validate_branch_response',
2781
+ type: "validate_branch_response",
2755
2782
  payload: {
2756
2783
  exists: false,
2757
2784
  resolvedRef: null,
@@ -2763,7 +2790,7 @@ export class Session {
2763
2790
  }
2764
2791
  catch (error) {
2765
2792
  this.emit({
2766
- type: 'validate_branch_response',
2793
+ type: "validate_branch_response",
2767
2794
  payload: {
2768
2795
  exists: false,
2769
2796
  resolvedRef: null,
@@ -2780,7 +2807,7 @@ export class Session {
2780
2807
  const resolvedCwd = expandTilde(cwd);
2781
2808
  const branches = await listBranchSuggestions(resolvedCwd, { query, limit });
2782
2809
  this.emit({
2783
- type: 'branch_suggestions_response',
2810
+ type: "branch_suggestions_response",
2784
2811
  payload: {
2785
2812
  branches,
2786
2813
  error: null,
@@ -2790,7 +2817,7 @@ export class Session {
2790
2817
  }
2791
2818
  catch (error) {
2792
2819
  this.emit({
2793
- type: 'branch_suggestions_response',
2820
+ type: "branch_suggestions_response",
2794
2821
  payload: {
2795
2822
  branches: [],
2796
2823
  error: error instanceof Error ? error.message : String(error),
@@ -2815,12 +2842,12 @@ export class Session {
2815
2842
  homeDir: process.env.HOME ?? homedir(),
2816
2843
  query,
2817
2844
  limit,
2818
- })).map((path) => ({ path, kind: 'directory' }));
2845
+ })).map((path) => ({ path, kind: "directory" }));
2819
2846
  const directories = entries
2820
- .filter((entry) => entry.kind === 'directory')
2847
+ .filter((entry) => entry.kind === "directory")
2821
2848
  .map((entry) => entry.path);
2822
2849
  this.emit({
2823
- type: 'directory_suggestions_response',
2850
+ type: "directory_suggestions_response",
2824
2851
  payload: {
2825
2852
  directories,
2826
2853
  entries,
@@ -2831,7 +2858,7 @@ export class Session {
2831
2858
  }
2832
2859
  catch (error) {
2833
2860
  this.emit({
2834
- type: 'directory_suggestions_response',
2861
+ type: "directory_suggestions_response",
2835
2862
  payload: {
2836
2863
  directories: [],
2837
2864
  entries: [],
@@ -2842,17 +2869,17 @@ export class Session {
2842
2869
  }
2843
2870
  }
2844
2871
  normalizeCheckoutDiffCompare(compare) {
2845
- if (compare.mode === 'uncommitted') {
2846
- return { mode: 'uncommitted' };
2872
+ if (compare.mode === "uncommitted") {
2873
+ return { mode: "uncommitted" };
2847
2874
  }
2848
2875
  const trimmedBaseRef = compare.baseRef?.trim();
2849
- return trimmedBaseRef ? { mode: 'base', baseRef: trimmedBaseRef } : { mode: 'base' };
2876
+ return trimmedBaseRef ? { mode: "base", baseRef: trimmedBaseRef } : { mode: "base" };
2850
2877
  }
2851
2878
  buildCheckoutDiffTargetKey(cwd, compare) {
2852
2879
  return JSON.stringify([
2853
2880
  cwd,
2854
2881
  compare.mode,
2855
- compare.mode === 'base' ? (compare.baseRef ?? '') : '',
2882
+ compare.mode === "base" ? (compare.baseRef ?? "") : "",
2856
2883
  ]);
2857
2884
  }
2858
2885
  closeCheckoutDiffWatchTarget(target) {
@@ -2887,7 +2914,7 @@ export class Session {
2887
2914
  }
2888
2915
  async resolveCheckoutGitDir(cwd) {
2889
2916
  try {
2890
- const { stdout } = await execAsync('git rev-parse --absolute-git-dir', {
2917
+ const { stdout } = await execAsync("git rev-parse --absolute-git-dir", {
2891
2918
  cwd,
2892
2919
  env: READ_ONLY_GIT_ENV,
2893
2920
  });
@@ -2898,9 +2925,150 @@ export class Session {
2898
2925
  return null;
2899
2926
  }
2900
2927
  }
2928
+ async resolveWorkspaceGitRefsRoot(gitDir) {
2929
+ try {
2930
+ const commonDir = (await readFile(join(gitDir, "commondir"), "utf8")).trim();
2931
+ if (commonDir.length > 0) {
2932
+ return resolve(gitDir, commonDir);
2933
+ }
2934
+ }
2935
+ catch {
2936
+ // Regular repos do not have a commondir file.
2937
+ }
2938
+ return gitDir;
2939
+ }
2940
+ closeWorkspaceGitWatchTarget(target) {
2941
+ if (target.debounceTimer) {
2942
+ clearTimeout(target.debounceTimer);
2943
+ target.debounceTimer = null;
2944
+ }
2945
+ for (const watcher of target.watchers) {
2946
+ watcher.close();
2947
+ }
2948
+ target.watchers = [];
2949
+ }
2950
+ removeWorkspaceGitWatchTarget(cwd) {
2951
+ const workspaceId = normalizePersistedWorkspaceId(cwd);
2952
+ const target = this.workspaceGitWatchTargets.get(workspaceId);
2953
+ if (!target) {
2954
+ return;
2955
+ }
2956
+ this.closeWorkspaceGitWatchTarget(target);
2957
+ this.workspaceGitWatchTargets.delete(workspaceId);
2958
+ }
2959
+ workspaceGitDescriptorFingerprint(workspace) {
2960
+ if (!workspace) {
2961
+ return WORKSPACE_GIT_WATCH_REMOVED_FINGERPRINT;
2962
+ }
2963
+ return JSON.stringify([
2964
+ workspace.name,
2965
+ workspace.diffStat ? [workspace.diffStat.additions, workspace.diffStat.deletions] : null,
2966
+ ]);
2967
+ }
2968
+ shouldSkipWorkspaceGitWatchUpdate(workspaceId, workspace) {
2969
+ const target = this.workspaceGitWatchTargets.get(workspaceId);
2970
+ if (!target) {
2971
+ return false;
2972
+ }
2973
+ const nextFingerprint = this.workspaceGitDescriptorFingerprint(workspace);
2974
+ if (target.latestFingerprint === nextFingerprint) {
2975
+ return true;
2976
+ }
2977
+ target.latestFingerprint = nextFingerprint;
2978
+ return false;
2979
+ }
2980
+ rememberWorkspaceGitWatchFingerprint(workspaceId, workspace) {
2981
+ const target = this.workspaceGitWatchTargets.get(workspaceId);
2982
+ if (!target) {
2983
+ return;
2984
+ }
2985
+ target.latestFingerprint = this.workspaceGitDescriptorFingerprint(workspace);
2986
+ }
2987
+ primeWorkspaceGitWatchFingerprints(workspaces) {
2988
+ for (const workspace of workspaces) {
2989
+ this.rememberWorkspaceGitWatchFingerprint(workspace.id, workspace);
2990
+ }
2991
+ }
2992
+ scheduleWorkspaceGitWatchRefresh(target) {
2993
+ if (target.debounceTimer) {
2994
+ clearTimeout(target.debounceTimer);
2995
+ }
2996
+ target.debounceTimer = setTimeout(() => {
2997
+ target.debounceTimer = null;
2998
+ void this.refreshWorkspaceGitWatchTarget(target);
2999
+ }, WORKSPACE_GIT_WATCH_DEBOUNCE_MS);
3000
+ }
3001
+ async refreshWorkspaceGitWatchTarget(target) {
3002
+ if (target.refreshPromise) {
3003
+ target.refreshQueued = true;
3004
+ return;
3005
+ }
3006
+ target.refreshPromise = (async () => {
3007
+ do {
3008
+ target.refreshQueued = false;
3009
+ await this.emitWorkspaceUpdateForCwd(target.cwd, {
3010
+ dedupeGitState: true,
3011
+ });
3012
+ } while (target.refreshQueued);
3013
+ })();
3014
+ try {
3015
+ await target.refreshPromise;
3016
+ }
3017
+ finally {
3018
+ target.refreshPromise = null;
3019
+ }
3020
+ }
3021
+ async ensureWorkspaceGitWatchTarget(cwd) {
3022
+ const workspaceId = normalizePersistedWorkspaceId(cwd);
3023
+ if (this.workspaceGitWatchTargets.has(workspaceId)) {
3024
+ return;
3025
+ }
3026
+ const gitDir = await this.resolveCheckoutGitDir(cwd);
3027
+ if (!gitDir) {
3028
+ return;
3029
+ }
3030
+ const refsRoot = await this.resolveWorkspaceGitRefsRoot(gitDir);
3031
+ const target = {
3032
+ cwd: workspaceId,
3033
+ watchers: [],
3034
+ debounceTimer: null,
3035
+ refreshPromise: null,
3036
+ refreshQueued: false,
3037
+ latestFingerprint: null,
3038
+ };
3039
+ for (const watchPath of new Set([join(gitDir, "HEAD"), join(refsRoot, "refs", "heads")])) {
3040
+ let watcher = null;
3041
+ try {
3042
+ watcher = watch(watchPath, { recursive: false }, () => {
3043
+ this.scheduleWorkspaceGitWatchRefresh(target);
3044
+ });
3045
+ }
3046
+ catch (error) {
3047
+ this.sessionLogger.warn({ err: error, cwd, watchPath }, "Failed to start workspace git watcher");
3048
+ }
3049
+ if (!watcher) {
3050
+ continue;
3051
+ }
3052
+ watcher.on("error", (error) => {
3053
+ this.sessionLogger.warn({ err: error, cwd, watchPath }, "Workspace git watcher error");
3054
+ });
3055
+ target.watchers.push(watcher);
3056
+ }
3057
+ if (target.watchers.length === 0) {
3058
+ return;
3059
+ }
3060
+ this.workspaceGitWatchTargets.set(workspaceId, target);
3061
+ }
3062
+ async syncWorkspaceGitWatchTarget(cwd, options) {
3063
+ if (!options.isGit) {
3064
+ this.removeWorkspaceGitWatchTarget(cwd);
3065
+ return;
3066
+ }
3067
+ await this.ensureWorkspaceGitWatchTarget(cwd);
3068
+ }
2901
3069
  async resolveCheckoutWatchRoot(cwd) {
2902
3070
  try {
2903
- const { stdout } = await execAsync('git rev-parse --path-format=absolute --show-toplevel', {
3071
+ const { stdout } = await execAsync("git rev-parse --path-format=absolute --show-toplevel", {
2904
3072
  cwd,
2905
3073
  env: READ_ONLY_GIT_ENV,
2906
3074
  });
@@ -2926,7 +3094,7 @@ export class Session {
2926
3094
  }
2927
3095
  for (const subscriptionId of target.subscriptions) {
2928
3096
  this.emit({
2929
- type: 'checkout_diff_update',
3097
+ type: "checkout_diff_update",
2930
3098
  payload: {
2931
3099
  subscriptionId,
2932
3100
  ...snapshot,
@@ -3019,7 +3187,7 @@ export class Session {
3019
3187
  watchPaths.add(gitDir);
3020
3188
  }
3021
3189
  let hasRecursiveRepoCoverage = false;
3022
- const allowRecursiveRepoWatch = process.platform !== 'linux';
3190
+ const allowRecursiveRepoWatch = process.platform !== "linux";
3023
3191
  for (const watchPath of watchPaths) {
3024
3192
  const shouldTryRecursive = watchPath === repoWatchPath && allowRecursiveRepoWatch;
3025
3193
  const createWatcher = (recursive) => watch(watchPath, { recursive }, () => {
@@ -3040,21 +3208,21 @@ export class Session {
3040
3208
  if (shouldTryRecursive) {
3041
3209
  try {
3042
3210
  watcher = createWatcher(false);
3043
- this.sessionLogger.warn({ err: error, watchPath, cwd, compare }, 'Checkout diff recursive watch unavailable; using non-recursive fallback');
3211
+ this.sessionLogger.warn({ err: error, watchPath, cwd, compare }, "Checkout diff recursive watch unavailable; using non-recursive fallback");
3044
3212
  }
3045
3213
  catch (fallbackError) {
3046
- this.sessionLogger.warn({ err: fallbackError, watchPath, cwd, compare }, 'Failed to start checkout diff watcher');
3214
+ this.sessionLogger.warn({ err: fallbackError, watchPath, cwd, compare }, "Failed to start checkout diff watcher");
3047
3215
  }
3048
3216
  }
3049
3217
  else {
3050
- this.sessionLogger.warn({ err: error, watchPath, cwd, compare }, 'Failed to start checkout diff watcher');
3218
+ this.sessionLogger.warn({ err: error, watchPath, cwd, compare }, "Failed to start checkout diff watcher");
3051
3219
  }
3052
3220
  }
3053
3221
  if (!watcher) {
3054
3222
  continue;
3055
3223
  }
3056
- watcher.on('error', (error) => {
3057
- this.sessionLogger.warn({ err: error, watchPath, cwd, compare }, 'Checkout diff watcher error');
3224
+ watcher.on("error", (error) => {
3225
+ this.sessionLogger.warn({ err: error, watchPath, cwd, compare }, "Checkout diff watcher error");
3058
3226
  });
3059
3227
  target.watchers.push(watcher);
3060
3228
  if (watchPath === repoWatchPath && watcherIsRecursive) {
@@ -3070,8 +3238,8 @@ export class Session {
3070
3238
  cwd,
3071
3239
  compare,
3072
3240
  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');
3241
+ reason: target.watchers.length === 0 ? "no_watchers" : "missing_recursive_repo_root_coverage",
3242
+ }, "Checkout diff watchers unavailable; using timed refresh fallback");
3075
3243
  }
3076
3244
  this.checkoutDiffTargets.set(targetKey, target);
3077
3245
  return target;
@@ -3092,7 +3260,7 @@ export class Session {
3092
3260
  target.latestPayload = snapshot;
3093
3261
  target.latestFingerprint = this.checkoutDiffSnapshotFingerprint(snapshot);
3094
3262
  this.emit({
3095
- type: 'subscribe_checkout_diff_response',
3263
+ type: "subscribe_checkout_diff_response",
3096
3264
  payload: {
3097
3265
  subscriptionId: msg.subscriptionId,
3098
3266
  ...snapshot,
@@ -3115,12 +3283,12 @@ export class Session {
3115
3283
  async handleCheckoutCommitRequest(msg) {
3116
3284
  const { cwd, requestId } = msg;
3117
3285
  try {
3118
- let message = msg.message?.trim() ?? '';
3286
+ let message = msg.message?.trim() ?? "";
3119
3287
  if (!message) {
3120
3288
  message = await this.generateCommitMessage(cwd);
3121
3289
  }
3122
3290
  if (!message) {
3123
- throw new Error('Commit message is required');
3291
+ throw new Error("Commit message is required");
3124
3292
  }
3125
3293
  await commitChanges(cwd, {
3126
3294
  message,
@@ -3128,7 +3296,7 @@ export class Session {
3128
3296
  });
3129
3297
  this.scheduleCheckoutDiffRefreshForCwd(cwd);
3130
3298
  this.emit({
3131
- type: 'checkout_commit_response',
3299
+ type: "checkout_commit_response",
3132
3300
  payload: {
3133
3301
  cwd,
3134
3302
  success: true,
@@ -3139,7 +3307,7 @@ export class Session {
3139
3307
  }
3140
3308
  catch (error) {
3141
3309
  this.emit({
3142
- type: 'checkout_commit_response',
3310
+ type: "checkout_commit_response",
3143
3311
  payload: {
3144
3312
  cwd,
3145
3313
  success: false,
@@ -3155,13 +3323,13 @@ export class Session {
3155
3323
  const status = await getCheckoutStatus(cwd, { paseoHome: this.paseoHome });
3156
3324
  if (!status.isGit) {
3157
3325
  try {
3158
- await execAsync('git rev-parse --is-inside-work-tree', {
3326
+ await execAsync("git rev-parse --is-inside-work-tree", {
3159
3327
  cwd,
3160
3328
  env: READ_ONLY_GIT_ENV,
3161
3329
  });
3162
3330
  }
3163
3331
  catch (error) {
3164
- const details = typeof error?.stderr === 'string'
3332
+ const details = typeof error?.stderr === "string"
3165
3333
  ? String(error.stderr).trim()
3166
3334
  : error instanceof Error
3167
3335
  ? error.message
@@ -3170,28 +3338,28 @@ export class Session {
3170
3338
  }
3171
3339
  }
3172
3340
  if (msg.requireCleanTarget) {
3173
- const { stdout } = await execAsync('git status --porcelain', {
3341
+ const { stdout } = await execAsync("git status --porcelain", {
3174
3342
  cwd,
3175
3343
  env: READ_ONLY_GIT_ENV,
3176
3344
  });
3177
3345
  if (stdout.trim().length > 0) {
3178
- throw new Error('Working directory has uncommitted changes.');
3346
+ throw new Error("Working directory has uncommitted changes.");
3179
3347
  }
3180
3348
  }
3181
3349
  let baseRef = msg.baseRef ?? (status.isGit ? status.baseRef : null);
3182
3350
  if (!baseRef) {
3183
- throw new Error('Base branch is required for merge');
3351
+ throw new Error("Base branch is required for merge");
3184
3352
  }
3185
- if (baseRef.startsWith('origin/')) {
3186
- baseRef = baseRef.slice('origin/'.length);
3353
+ if (baseRef.startsWith("origin/")) {
3354
+ baseRef = baseRef.slice("origin/".length);
3187
3355
  }
3188
3356
  await mergeToBase(cwd, {
3189
3357
  baseRef,
3190
- mode: msg.strategy === 'squash' ? 'squash' : 'merge',
3358
+ mode: msg.strategy === "squash" ? "squash" : "merge",
3191
3359
  }, { paseoHome: this.paseoHome });
3192
3360
  this.scheduleCheckoutDiffRefreshForCwd(cwd);
3193
3361
  this.emit({
3194
- type: 'checkout_merge_response',
3362
+ type: "checkout_merge_response",
3195
3363
  payload: {
3196
3364
  cwd,
3197
3365
  success: true,
@@ -3202,7 +3370,7 @@ export class Session {
3202
3370
  }
3203
3371
  catch (error) {
3204
3372
  this.emit({
3205
- type: 'checkout_merge_response',
3373
+ type: "checkout_merge_response",
3206
3374
  payload: {
3207
3375
  cwd,
3208
3376
  success: false,
@@ -3216,12 +3384,12 @@ export class Session {
3216
3384
  const { cwd, requestId } = msg;
3217
3385
  try {
3218
3386
  if (msg.requireCleanTarget ?? true) {
3219
- const { stdout } = await execAsync('git status --porcelain', {
3387
+ const { stdout } = await execAsync("git status --porcelain", {
3220
3388
  cwd,
3221
3389
  env: READ_ONLY_GIT_ENV,
3222
3390
  });
3223
3391
  if (stdout.trim().length > 0) {
3224
- throw new Error('Working directory has uncommitted changes.');
3392
+ throw new Error("Working directory has uncommitted changes.");
3225
3393
  }
3226
3394
  }
3227
3395
  await mergeFromBase(cwd, {
@@ -3230,7 +3398,7 @@ export class Session {
3230
3398
  });
3231
3399
  this.scheduleCheckoutDiffRefreshForCwd(cwd);
3232
3400
  this.emit({
3233
- type: 'checkout_merge_from_base_response',
3401
+ type: "checkout_merge_from_base_response",
3234
3402
  payload: {
3235
3403
  cwd,
3236
3404
  success: true,
@@ -3241,7 +3409,7 @@ export class Session {
3241
3409
  }
3242
3410
  catch (error) {
3243
3411
  this.emit({
3244
- type: 'checkout_merge_from_base_response',
3412
+ type: "checkout_merge_from_base_response",
3245
3413
  payload: {
3246
3414
  cwd,
3247
3415
  success: false,
@@ -3256,7 +3424,7 @@ export class Session {
3256
3424
  try {
3257
3425
  await pushCurrentBranch(cwd);
3258
3426
  this.emit({
3259
- type: 'checkout_push_response',
3427
+ type: "checkout_push_response",
3260
3428
  payload: {
3261
3429
  cwd,
3262
3430
  success: true,
@@ -3267,7 +3435,7 @@ export class Session {
3267
3435
  }
3268
3436
  catch (error) {
3269
3437
  this.emit({
3270
- type: 'checkout_push_response',
3438
+ type: "checkout_push_response",
3271
3439
  payload: {
3272
3440
  cwd,
3273
3441
  success: false,
@@ -3280,8 +3448,8 @@ export class Session {
3280
3448
  async handleCheckoutPrCreateRequest(msg) {
3281
3449
  const { cwd, requestId } = msg;
3282
3450
  try {
3283
- let title = msg.title?.trim() ?? '';
3284
- let body = msg.body?.trim() ?? '';
3451
+ let title = msg.title?.trim() ?? "";
3452
+ let body = msg.body?.trim() ?? "";
3285
3453
  if (!title || !body) {
3286
3454
  const generated = await this.generatePullRequestText(cwd, msg.baseRef);
3287
3455
  if (!title)
@@ -3295,7 +3463,7 @@ export class Session {
3295
3463
  base: msg.baseRef,
3296
3464
  });
3297
3465
  this.emit({
3298
- type: 'checkout_pr_create_response',
3466
+ type: "checkout_pr_create_response",
3299
3467
  payload: {
3300
3468
  cwd,
3301
3469
  url: result.url ?? null,
@@ -3307,7 +3475,7 @@ export class Session {
3307
3475
  }
3308
3476
  catch (error) {
3309
3477
  this.emit({
3310
- type: 'checkout_pr_create_response',
3478
+ type: "checkout_pr_create_response",
3311
3479
  payload: {
3312
3480
  cwd,
3313
3481
  url: null,
@@ -3323,7 +3491,7 @@ export class Session {
3323
3491
  try {
3324
3492
  const prStatus = await getPullRequestStatus(cwd);
3325
3493
  this.emit({
3326
- type: 'checkout_pr_status_response',
3494
+ type: "checkout_pr_status_response",
3327
3495
  payload: {
3328
3496
  cwd,
3329
3497
  status: prStatus.status,
@@ -3335,7 +3503,7 @@ export class Session {
3335
3503
  }
3336
3504
  catch (error) {
3337
3505
  this.emit({
3338
- type: 'checkout_pr_status_response',
3506
+ type: "checkout_pr_status_response",
3339
3507
  payload: {
3340
3508
  cwd,
3341
3509
  status: null,
@@ -3351,10 +3519,10 @@ export class Session {
3351
3519
  const cwd = msg.repoRoot ?? msg.cwd;
3352
3520
  if (!cwd) {
3353
3521
  this.emit({
3354
- type: 'paseo_worktree_list_response',
3522
+ type: "paseo_worktree_list_response",
3355
3523
  payload: {
3356
3524
  worktrees: [],
3357
- error: { code: 'UNKNOWN', message: 'cwd or repoRoot is required' },
3525
+ error: { code: "UNKNOWN", message: "cwd or repoRoot is required" },
3358
3526
  requestId,
3359
3527
  },
3360
3528
  });
@@ -3363,7 +3531,7 @@ export class Session {
3363
3531
  try {
3364
3532
  const worktrees = await listPaseoWorktrees({ cwd, paseoHome: this.paseoHome });
3365
3533
  this.emit({
3366
- type: 'paseo_worktree_list_response',
3534
+ type: "paseo_worktree_list_response",
3367
3535
  payload: {
3368
3536
  worktrees: worktrees.map((entry) => ({
3369
3537
  worktreePath: entry.path,
@@ -3378,7 +3546,7 @@ export class Session {
3378
3546
  }
3379
3547
  catch (error) {
3380
3548
  this.emit({
3381
- type: 'paseo_worktree_list_response',
3549
+ type: "paseo_worktree_list_response",
3382
3550
  payload: {
3383
3551
  worktrees: [],
3384
3552
  error: this.toCheckoutError(error),
@@ -3443,7 +3611,7 @@ export class Session {
3443
3611
  }
3444
3612
  for (const agentId of removedAgents) {
3445
3613
  this.emit({
3446
- type: 'agent_deleted',
3614
+ type: "agent_deleted",
3447
3615
  payload: {
3448
3616
  agentId,
3449
3617
  requestId: options.requestId,
@@ -3460,7 +3628,7 @@ export class Session {
3460
3628
  try {
3461
3629
  if (!targetPath) {
3462
3630
  if (!repoRoot || !msg.branchName) {
3463
- throw new Error('worktreePath or repoRoot+branchName is required');
3631
+ throw new Error("worktreePath or repoRoot+branchName is required");
3464
3632
  }
3465
3633
  const worktrees = await listPaseoWorktrees({ cwd: repoRoot, paseoHome: this.paseoHome });
3466
3634
  const match = worktrees.find((entry) => entry.branchName === msg.branchName);
@@ -3474,13 +3642,13 @@ export class Session {
3474
3642
  });
3475
3643
  if (!ownership.allowed) {
3476
3644
  this.emit({
3477
- type: 'paseo_worktree_archive_response',
3645
+ type: "paseo_worktree_archive_response",
3478
3646
  payload: {
3479
3647
  success: false,
3480
3648
  removedAgents: [],
3481
3649
  error: {
3482
- code: 'NOT_ALLOWED',
3483
- message: 'Worktree is not a Paseo-owned worktree',
3650
+ code: "NOT_ALLOWED",
3651
+ message: "Worktree is not a Paseo-owned worktree",
3484
3652
  },
3485
3653
  requestId,
3486
3654
  },
@@ -3489,7 +3657,7 @@ export class Session {
3489
3657
  }
3490
3658
  repoRoot = ownership.repoRoot ?? repoRoot ?? null;
3491
3659
  if (!repoRoot) {
3492
- throw new Error('Unable to resolve repo root for worktree');
3660
+ throw new Error("Unable to resolve repo root for worktree");
3493
3661
  }
3494
3662
  const removedAgents = await this.archivePaseoWorktree({
3495
3663
  targetPath,
@@ -3497,7 +3665,7 @@ export class Session {
3497
3665
  requestId,
3498
3666
  });
3499
3667
  this.emit({
3500
- type: 'paseo_worktree_archive_response',
3668
+ type: "paseo_worktree_archive_response",
3501
3669
  payload: {
3502
3670
  success: true,
3503
3671
  removedAgents,
@@ -3508,7 +3676,7 @@ export class Session {
3508
3676
  }
3509
3677
  catch (error) {
3510
3678
  this.emit({
3511
- type: 'paseo_worktree_archive_response',
3679
+ type: "paseo_worktree_archive_response",
3512
3680
  payload: {
3513
3681
  success: false,
3514
3682
  removedAgents: [],
@@ -3522,31 +3690,31 @@ export class Session {
3522
3690
  * Handle read-only file explorer requests scoped to a workspace cwd
3523
3691
  */
3524
3692
  async handleFileExplorerRequest(request) {
3525
- const { cwd: workspaceCwd, path: requestedPath = '.', mode, requestId } = request;
3693
+ const { cwd: workspaceCwd, path: requestedPath = ".", mode, requestId } = request;
3526
3694
  const cwd = workspaceCwd.trim();
3527
3695
  if (!cwd) {
3528
3696
  this.emit({
3529
- type: 'file_explorer_response',
3697
+ type: "file_explorer_response",
3530
3698
  payload: {
3531
3699
  cwd: workspaceCwd,
3532
3700
  path: requestedPath,
3533
3701
  mode,
3534
3702
  directory: null,
3535
3703
  file: null,
3536
- error: 'cwd is required',
3704
+ error: "cwd is required",
3537
3705
  requestId,
3538
3706
  },
3539
3707
  });
3540
3708
  return;
3541
3709
  }
3542
3710
  try {
3543
- if (mode === 'list') {
3711
+ if (mode === "list") {
3544
3712
  const directory = await listDirectoryEntries({
3545
3713
  root: cwd,
3546
3714
  relativePath: requestedPath,
3547
3715
  });
3548
3716
  this.emit({
3549
- type: 'file_explorer_response',
3717
+ type: "file_explorer_response",
3550
3718
  payload: {
3551
3719
  cwd,
3552
3720
  path: directory.path,
@@ -3564,7 +3732,7 @@ export class Session {
3564
3732
  relativePath: requestedPath,
3565
3733
  });
3566
3734
  this.emit({
3567
- type: 'file_explorer_response',
3735
+ type: "file_explorer_response",
3568
3736
  payload: {
3569
3737
  cwd,
3570
3738
  path: file.path,
@@ -3580,7 +3748,7 @@ export class Session {
3580
3748
  catch (error) {
3581
3749
  this.sessionLogger.error({ err: error, cwd, path: requestedPath }, `Failed to fulfill file explorer request for workspace ${cwd}`);
3582
3750
  this.emit({
3583
- type: 'file_explorer_response',
3751
+ type: "file_explorer_response",
3584
3752
  payload: {
3585
3753
  cwd,
3586
3754
  path: requestedPath,
@@ -3601,7 +3769,7 @@ export class Session {
3601
3769
  try {
3602
3770
  const icon = await getProjectIcon(cwd);
3603
3771
  this.emit({
3604
- type: 'project_icon_response',
3772
+ type: "project_icon_response",
3605
3773
  payload: {
3606
3774
  cwd,
3607
3775
  icon,
@@ -3612,7 +3780,7 @@ export class Session {
3612
3780
  }
3613
3781
  catch (error) {
3614
3782
  this.emit({
3615
- type: 'project_icon_response',
3783
+ type: "project_icon_response",
3616
3784
  payload: {
3617
3785
  cwd,
3618
3786
  icon: null,
@@ -3630,7 +3798,7 @@ export class Session {
3630
3798
  const cwd = workspaceCwd.trim();
3631
3799
  if (!cwd) {
3632
3800
  this.emit({
3633
- type: 'file_download_token_response',
3801
+ type: "file_download_token_response",
3634
3802
  payload: {
3635
3803
  cwd: workspaceCwd,
3636
3804
  path: requestedPath,
@@ -3638,7 +3806,7 @@ export class Session {
3638
3806
  fileName: null,
3639
3807
  mimeType: null,
3640
3808
  size: null,
3641
- error: 'cwd is required',
3809
+ error: "cwd is required",
3642
3810
  requestId,
3643
3811
  },
3644
3812
  });
@@ -3658,7 +3826,7 @@ export class Session {
3658
3826
  size: info.size,
3659
3827
  });
3660
3828
  this.emit({
3661
- type: 'file_download_token_response',
3829
+ type: "file_download_token_response",
3662
3830
  payload: {
3663
3831
  cwd,
3664
3832
  path: info.path,
@@ -3674,7 +3842,7 @@ export class Session {
3674
3842
  catch (error) {
3675
3843
  this.sessionLogger.error({ err: error, cwd, path: requestedPath }, `Failed to issue download token for workspace ${cwd}`);
3676
3844
  this.emit({
3677
- type: 'file_download_token_response',
3845
+ type: "file_download_token_response",
3678
3846
  payload: {
3679
3847
  cwd,
3680
3848
  path: requestedPath,
@@ -3713,7 +3881,7 @@ export class Session {
3713
3881
  async resolveAgentIdentifier(identifier) {
3714
3882
  const trimmed = identifier.trim();
3715
3883
  if (!trimmed) {
3716
- return { ok: false, error: 'Agent identifier cannot be empty' };
3884
+ return { ok: false, error: "Agent identifier cannot be empty" };
3717
3885
  }
3718
3886
  const stored = await this.agentStorage.list();
3719
3887
  const storedRecords = stored.filter((record) => !record.internal);
@@ -3737,7 +3905,7 @@ export class Session {
3737
3905
  error: `Agent identifier "${trimmed}" is ambiguous (${prefixMatches
3738
3906
  .slice(0, 5)
3739
3907
  .map((id) => id.slice(0, 8))
3740
- .join(', ')}${prefixMatches.length > 5 ? ', …' : ''})`,
3908
+ .join(", ")}${prefixMatches.length > 5 ? ", …" : ""})`,
3741
3909
  };
3742
3910
  }
3743
3911
  const titleMatches = storedRecords.filter((record) => record.title === trimmed);
@@ -3750,7 +3918,7 @@ export class Session {
3750
3918
  error: `Agent title "${trimmed}" is ambiguous (${titleMatches
3751
3919
  .slice(0, 5)
3752
3920
  .map((r) => r.id.slice(0, 8))
3753
- .join(', ')}${titleMatches.length > 5 ? ', …' : ''})`,
3921
+ .join(", ")}${titleMatches.length > 5 ? ", …" : ""})`,
3754
3922
  };
3755
3923
  }
3756
3924
  return { ok: false, error: `Agent not found: ${trimmed}` };
@@ -3767,7 +3935,7 @@ export class Session {
3767
3935
  return this.buildStoredAgentPayload(record);
3768
3936
  }
3769
3937
  normalizeFetchAgentsSort(sort) {
3770
- const fallback = [{ key: 'updated_at', direction: 'desc' }];
3938
+ const fallback = [{ key: "updated_at", direction: "desc" }];
3771
3939
  if (!sort || sort.length === 0) {
3772
3940
  return fallback;
3773
3941
  }
@@ -3785,42 +3953,42 @@ export class Session {
3785
3953
  getStatusPriority(agent) {
3786
3954
  const attentionReason = agent.attentionReason ?? null;
3787
3955
  const hasPendingPermission = (agent.pendingPermissions?.length ?? 0) > 0;
3788
- if (hasPendingPermission || attentionReason === 'permission') {
3956
+ if (hasPendingPermission || attentionReason === "permission") {
3789
3957
  return 0;
3790
3958
  }
3791
- if (agent.status === 'error' || attentionReason === 'error') {
3959
+ if (agent.status === "error" || attentionReason === "error") {
3792
3960
  return 1;
3793
3961
  }
3794
- if (agent.status === 'running') {
3962
+ if (agent.status === "running") {
3795
3963
  return 2;
3796
3964
  }
3797
- if (agent.status === 'initializing') {
3965
+ if (agent.status === "initializing") {
3798
3966
  return 3;
3799
3967
  }
3800
3968
  return 4;
3801
3969
  }
3802
3970
  getFetchAgentsSortValue(entry, key) {
3803
3971
  switch (key) {
3804
- case 'status_priority':
3972
+ case "status_priority":
3805
3973
  return this.getStatusPriority(entry.agent);
3806
- case 'created_at':
3974
+ case "created_at":
3807
3975
  return Date.parse(entry.agent.createdAt);
3808
- case 'updated_at':
3976
+ case "updated_at":
3809
3977
  return Date.parse(entry.agent.updatedAt);
3810
- case 'title':
3811
- return entry.agent.title?.toLocaleLowerCase() ?? '';
3978
+ case "title":
3979
+ return entry.agent.title?.toLocaleLowerCase() ?? "";
3812
3980
  }
3813
3981
  }
3814
3982
  getFetchAgentsSortValueFromAgent(agent, key) {
3815
3983
  switch (key) {
3816
- case 'status_priority':
3984
+ case "status_priority":
3817
3985
  return this.getStatusPriority(agent);
3818
- case 'created_at':
3986
+ case "created_at":
3819
3987
  return Date.parse(agent.createdAt);
3820
- case 'updated_at':
3988
+ case "updated_at":
3821
3989
  return Date.parse(agent.updatedAt);
3822
- case 'title':
3823
- return agent.title?.toLocaleLowerCase() ?? '';
3990
+ case "title":
3991
+ return agent.title?.toLocaleLowerCase() ?? "";
3824
3992
  }
3825
3993
  }
3826
3994
  compareSortValues(left, right) {
@@ -3833,7 +4001,7 @@ export class Session {
3833
4001
  if (right === null) {
3834
4002
  return 1;
3835
4003
  }
3836
- if (typeof left === 'number' && typeof right === 'number') {
4004
+ if (typeof left === "number" && typeof right === "number") {
3837
4005
  return left < right ? -1 : 1;
3838
4006
  }
3839
4007
  return String(left).localeCompare(String(right));
@@ -3846,7 +4014,7 @@ export class Session {
3846
4014
  if (base === 0) {
3847
4015
  continue;
3848
4016
  }
3849
- return spec.direction === 'asc' ? base : -base;
4017
+ return spec.direction === "asc" ? base : -base;
3850
4018
  }
3851
4019
  return left.id.localeCompare(right.id);
3852
4020
  }
@@ -3859,48 +4027,48 @@ export class Session {
3859
4027
  sort,
3860
4028
  values,
3861
4029
  id: entry.agent.id,
3862
- }), 'utf8').toString('base64url');
4030
+ }), "utf8").toString("base64url");
3863
4031
  }
3864
4032
  decodeFetchAgentsCursor(cursor, sort) {
3865
4033
  let parsed;
3866
4034
  try {
3867
- parsed = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
4035
+ parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
3868
4036
  }
3869
4037
  catch {
3870
- throw new SessionRequestError('invalid_cursor', 'Invalid fetch_agents cursor');
4038
+ throw new SessionRequestError("invalid_cursor", "Invalid fetch_agents cursor");
3871
4039
  }
3872
- if (!parsed || typeof parsed !== 'object') {
3873
- throw new SessionRequestError('invalid_cursor', 'Invalid fetch_agents cursor');
4040
+ if (!parsed || typeof parsed !== "object") {
4041
+ throw new SessionRequestError("invalid_cursor", "Invalid fetch_agents cursor");
3874
4042
  }
3875
4043
  const payload = parsed;
3876
- if (!Array.isArray(payload.sort) || typeof payload.id !== 'string') {
3877
- throw new SessionRequestError('invalid_cursor', 'Invalid fetch_agents cursor');
4044
+ if (!Array.isArray(payload.sort) || typeof payload.id !== "string") {
4045
+ throw new SessionRequestError("invalid_cursor", "Invalid fetch_agents cursor");
3878
4046
  }
3879
- if (!payload.values || typeof payload.values !== 'object') {
3880
- throw new SessionRequestError('invalid_cursor', 'Invalid fetch_agents cursor');
4047
+ if (!payload.values || typeof payload.values !== "object") {
4048
+ throw new SessionRequestError("invalid_cursor", "Invalid fetch_agents cursor");
3881
4049
  }
3882
4050
  const cursorSort = [];
3883
4051
  for (const item of payload.sort) {
3884
4052
  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');
4053
+ typeof item !== "object" ||
4054
+ typeof item.key !== "string" ||
4055
+ typeof item.direction !== "string") {
4056
+ throw new SessionRequestError("invalid_cursor", "Invalid fetch_agents cursor");
3889
4057
  }
3890
4058
  const key = item.key;
3891
4059
  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');
4060
+ if ((key !== "status_priority" &&
4061
+ key !== "created_at" &&
4062
+ key !== "updated_at" &&
4063
+ key !== "title") ||
4064
+ (direction !== "asc" && direction !== "desc")) {
4065
+ throw new SessionRequestError("invalid_cursor", "Invalid fetch_agents cursor");
3898
4066
  }
3899
4067
  cursorSort.push({ key, direction });
3900
4068
  }
3901
4069
  if (cursorSort.length !== sort.length ||
3902
4070
  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');
4071
+ throw new SessionRequestError("invalid_cursor", "fetch_agents cursor does not match current sort");
3904
4072
  }
3905
4073
  return {
3906
4074
  sort: cursorSort,
@@ -3916,7 +4084,7 @@ export class Session {
3916
4084
  if (base === 0) {
3917
4085
  continue;
3918
4086
  }
3919
- return spec.direction === 'asc' ? base : -base;
4087
+ return spec.direction === "asc" ? base : -base;
3920
4088
  }
3921
4089
  return agent.id.localeCompare(cursor.id);
3922
4090
  }
@@ -3982,19 +4150,19 @@ export class Session {
3982
4150
  }
3983
4151
  deriveWorkspaceStateBucket(agent) {
3984
4152
  const pendingPermissionCount = agent.pendingPermissions?.length ?? 0;
3985
- if (pendingPermissionCount > 0 || agent.attentionReason === 'permission') {
3986
- return 'needs_input';
4153
+ if (pendingPermissionCount > 0 || agent.attentionReason === "permission") {
4154
+ return "needs_input";
3987
4155
  }
3988
- if (agent.status === 'error' || agent.attentionReason === 'error') {
3989
- return 'failed';
4156
+ if (agent.status === "error" || agent.attentionReason === "error") {
4157
+ return "failed";
3990
4158
  }
3991
- if (agent.status === 'running' || agent.status === 'initializing') {
3992
- return 'running';
4159
+ if (agent.status === "running" || agent.status === "initializing") {
4160
+ return "running";
3993
4161
  }
3994
4162
  if (agent.requiresAttention) {
3995
- return 'attention';
4163
+ return "attention";
3996
4164
  }
3997
- return 'done';
4165
+ return "done";
3998
4166
  }
3999
4167
  accumulateLatestActivityAt(current, agent) {
4000
4168
  const candidateRaw = agent.lastUserMessageAt ?? agent.updatedAt;
@@ -4036,10 +4204,10 @@ export class Session {
4036
4204
  projectId: workspace.projectId,
4037
4205
  projectDisplayName: resolvedProjectRecord?.displayName ?? workspace.projectId,
4038
4206
  projectRootPath: resolvedProjectRecord?.rootPath ?? workspace.cwd,
4039
- projectKind: resolvedProjectRecord?.kind ?? 'non_git',
4207
+ projectKind: resolvedProjectRecord?.kind ?? "non_git",
4040
4208
  workspaceKind: workspace.kind,
4041
4209
  name: displayName,
4042
- status: 'done',
4210
+ status: "done",
4043
4211
  activityAt: null,
4044
4212
  diffStat,
4045
4213
  };
@@ -4080,7 +4248,7 @@ export class Session {
4080
4248
  return this.listWorkspaceDescriptorsSnapshot();
4081
4249
  }
4082
4250
  normalizeFetchWorkspacesSort(sort) {
4083
- const fallback = [{ key: 'activity_at', direction: 'desc' }];
4251
+ const fallback = [{ key: "activity_at", direction: "desc" }];
4084
4252
  if (!sort || sort.length === 0) {
4085
4253
  return fallback;
4086
4254
  }
@@ -4097,13 +4265,13 @@ export class Session {
4097
4265
  }
4098
4266
  getFetchWorkspacesSortValue(workspace, key) {
4099
4267
  switch (key) {
4100
- case 'status_priority':
4268
+ case "status_priority":
4101
4269
  return this.workspaceStatePriority[workspace.status];
4102
- case 'activity_at':
4270
+ case "activity_at":
4103
4271
  return workspace.activityAt ? Date.parse(workspace.activityAt) : null;
4104
- case 'name':
4272
+ case "name":
4105
4273
  return workspace.name.toLocaleLowerCase();
4106
- case 'project_id':
4274
+ case "project_id":
4107
4275
  return workspace.projectId.toLocaleLowerCase();
4108
4276
  }
4109
4277
  }
@@ -4115,7 +4283,7 @@ export class Session {
4115
4283
  if (base === 0) {
4116
4284
  continue;
4117
4285
  }
4118
- return spec.direction === 'asc' ? base : -base;
4286
+ return spec.direction === "asc" ? base : -base;
4119
4287
  }
4120
4288
  return left.id.localeCompare(right.id);
4121
4289
  }
@@ -4128,48 +4296,48 @@ export class Session {
4128
4296
  sort,
4129
4297
  values,
4130
4298
  id: entry.id,
4131
- }), 'utf8').toString('base64url');
4299
+ }), "utf8").toString("base64url");
4132
4300
  }
4133
4301
  decodeFetchWorkspacesCursor(cursor, sort) {
4134
4302
  let parsed;
4135
4303
  try {
4136
- parsed = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
4304
+ parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
4137
4305
  }
4138
4306
  catch {
4139
- throw new SessionRequestError('invalid_cursor', 'Invalid fetch_workspaces cursor');
4307
+ throw new SessionRequestError("invalid_cursor", "Invalid fetch_workspaces cursor");
4140
4308
  }
4141
- if (!parsed || typeof parsed !== 'object') {
4142
- throw new SessionRequestError('invalid_cursor', 'Invalid fetch_workspaces cursor');
4309
+ if (!parsed || typeof parsed !== "object") {
4310
+ throw new SessionRequestError("invalid_cursor", "Invalid fetch_workspaces cursor");
4143
4311
  }
4144
4312
  const payload = parsed;
4145
- if (!Array.isArray(payload.sort) || typeof payload.id !== 'string') {
4146
- throw new SessionRequestError('invalid_cursor', 'Invalid fetch_workspaces cursor');
4313
+ if (!Array.isArray(payload.sort) || typeof payload.id !== "string") {
4314
+ throw new SessionRequestError("invalid_cursor", "Invalid fetch_workspaces cursor");
4147
4315
  }
4148
- if (!payload.values || typeof payload.values !== 'object') {
4149
- throw new SessionRequestError('invalid_cursor', 'Invalid fetch_workspaces cursor');
4316
+ if (!payload.values || typeof payload.values !== "object") {
4317
+ throw new SessionRequestError("invalid_cursor", "Invalid fetch_workspaces cursor");
4150
4318
  }
4151
4319
  const cursorSort = [];
4152
4320
  for (const item of payload.sort) {
4153
4321
  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');
4322
+ typeof item !== "object" ||
4323
+ typeof item.key !== "string" ||
4324
+ typeof item.direction !== "string") {
4325
+ throw new SessionRequestError("invalid_cursor", "Invalid fetch_workspaces cursor");
4158
4326
  }
4159
4327
  const key = item.key;
4160
4328
  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');
4329
+ if ((key !== "status_priority" &&
4330
+ key !== "activity_at" &&
4331
+ key !== "name" &&
4332
+ key !== "project_id") ||
4333
+ (direction !== "asc" && direction !== "desc")) {
4334
+ throw new SessionRequestError("invalid_cursor", "Invalid fetch_workspaces cursor");
4167
4335
  }
4168
4336
  cursorSort.push({ key, direction });
4169
4337
  }
4170
4338
  if (cursorSort.length !== sort.length ||
4171
4339
  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');
4340
+ throw new SessionRequestError("invalid_cursor", "fetch_workspaces cursor does not match current sort");
4173
4341
  }
4174
4342
  return {
4175
4343
  sort: cursorSort,
@@ -4185,7 +4353,7 @@ export class Session {
4185
4353
  if (base === 0) {
4186
4354
  continue;
4187
4355
  }
4188
- return spec.direction === 'asc' ? base : -base;
4356
+ return spec.direction === "asc" ? base : -base;
4189
4357
  }
4190
4358
  return workspace.id.localeCompare(cursor.id);
4191
4359
  }
@@ -4241,12 +4409,12 @@ export class Session {
4241
4409
  }
4242
4410
  bufferOrEmitWorkspaceUpdate(subscription, payload) {
4243
4411
  if (subscription.isBootstrapping) {
4244
- const workspaceId = payload.kind === 'upsert' ? payload.workspace.id : payload.id;
4412
+ const workspaceId = payload.kind === "upsert" ? payload.workspace.id : payload.id;
4245
4413
  subscription.pendingUpdatesByWorkspaceId.set(workspaceId, payload);
4246
4414
  return;
4247
4415
  }
4248
4416
  this.emit({
4249
- type: 'workspace_update',
4417
+ type: "workspace_update",
4250
4418
  payload,
4251
4419
  });
4252
4420
  }
@@ -4259,19 +4427,20 @@ export class Session {
4259
4427
  const pending = Array.from(subscription.pendingUpdatesByWorkspaceId.values());
4260
4428
  subscription.pendingUpdatesByWorkspaceId.clear();
4261
4429
  for (const payload of pending) {
4262
- if (payload.kind === 'upsert') {
4430
+ if (payload.kind === "upsert") {
4263
4431
  const snapshotLatestActivity = options?.snapshotLatestActivityByWorkspaceId?.get(payload.workspace.id);
4264
- if (typeof snapshotLatestActivity === 'number') {
4432
+ if (typeof snapshotLatestActivity === "number") {
4265
4433
  const updateLatestActivity = payload.workspace.activityAt
4266
4434
  ? Date.parse(payload.workspace.activityAt)
4267
4435
  : Number.NEGATIVE_INFINITY;
4268
- if (!Number.isNaN(updateLatestActivity) && updateLatestActivity <= snapshotLatestActivity) {
4436
+ if (!Number.isNaN(updateLatestActivity) &&
4437
+ updateLatestActivity <= snapshotLatestActivity) {
4269
4438
  continue;
4270
4439
  }
4271
4440
  }
4272
4441
  }
4273
4442
  this.emit({
4274
- type: 'workspace_update',
4443
+ type: "workspace_update",
4275
4444
  payload,
4276
4445
  });
4277
4446
  }
@@ -4280,19 +4449,62 @@ export class Session {
4280
4449
  const workspaceId = normalizePersistedWorkspaceId(cwd);
4281
4450
  return (await this.reconcileWorkspaceRecord(workspaceId)).workspace;
4282
4451
  }
4452
+ async registerPendingWorktreeWorkspace(options) {
4453
+ const workspaceId = normalizePersistedWorkspaceId(options.worktreePath);
4454
+ const basePlacement = await this.buildProjectPlacement(options.repoRoot);
4455
+ const placement = {
4456
+ ...basePlacement,
4457
+ checkout: {
4458
+ cwd: workspaceId,
4459
+ isGit: true,
4460
+ currentBranch: options.branchName,
4461
+ remoteUrl: basePlacement.checkout.remoteUrl,
4462
+ isPaseoOwnedWorktree: true,
4463
+ mainRepoRoot: options.repoRoot,
4464
+ },
4465
+ };
4466
+ const now = new Date().toISOString();
4467
+ const existingWorkspace = await this.workspaceRegistry.get(workspaceId);
4468
+ const existingProject = await this.projectRegistry.get(placement.projectKey);
4469
+ const nextProjectRecord = this.buildPersistedProjectRecord({
4470
+ workspaceId,
4471
+ placement,
4472
+ createdAt: existingProject?.createdAt ?? now,
4473
+ updatedAt: now,
4474
+ });
4475
+ const nextWorkspaceRecord = this.buildPersistedWorkspaceRecord({
4476
+ workspaceId,
4477
+ placement,
4478
+ createdAt: existingWorkspace?.createdAt ?? now,
4479
+ updatedAt: now,
4480
+ });
4481
+ await this.projectRegistry.upsert(nextProjectRecord);
4482
+ await this.workspaceRegistry.upsert(nextWorkspaceRecord);
4483
+ await this.syncWorkspaceGitWatchTarget(workspaceId, {
4484
+ isGit: placement.checkout.isGit,
4485
+ });
4486
+ if (existingWorkspace &&
4487
+ !existingWorkspace.archivedAt &&
4488
+ existingWorkspace.projectId !== nextWorkspaceRecord.projectId) {
4489
+ await this.archiveProjectRecordIfEmpty(existingWorkspace.projectId, now);
4490
+ }
4491
+ return nextWorkspaceRecord;
4492
+ }
4283
4493
  async archiveWorkspaceRecord(workspaceId, archivedAt) {
4284
4494
  const existing = await this.workspaceRegistry.get(workspaceId);
4285
4495
  if (!existing || existing.archivedAt) {
4496
+ this.removeWorkspaceGitWatchTarget(workspaceId);
4286
4497
  return;
4287
4498
  }
4288
4499
  const nextArchivedAt = archivedAt ?? new Date().toISOString();
4289
4500
  await this.workspaceRegistry.archive(workspaceId, nextArchivedAt);
4501
+ this.removeWorkspaceGitWatchTarget(workspaceId);
4290
4502
  const siblingWorkspaces = (await this.workspaceRegistry.list()).filter((workspace) => workspace.projectId === existing.projectId && !workspace.archivedAt);
4291
4503
  if (siblingWorkspaces.length === 0) {
4292
4504
  await this.projectRegistry.archive(existing.projectId, nextArchivedAt);
4293
4505
  }
4294
4506
  }
4295
- async emitWorkspaceUpdateForCwd(cwd) {
4507
+ async emitWorkspaceUpdateForCwd(cwd, options) {
4296
4508
  const subscription = this.workspaceUpdatesSubscription;
4297
4509
  if (!subscription) {
4298
4510
  return;
@@ -4304,16 +4516,24 @@ export class Session {
4304
4516
  const workspaceIdsToEmit = new Set([workspaceId, ...changedWorkspaceIds]);
4305
4517
  for (const nextWorkspaceId of workspaceIdsToEmit) {
4306
4518
  const workspace = descriptorsByWorkspaceId.get(nextWorkspaceId);
4307
- if (!workspace || !this.matchesWorkspaceFilter({ workspace, filter: subscription.filter })) {
4519
+ const nextWorkspace = workspace && this.matchesWorkspaceFilter({ workspace, filter: subscription.filter })
4520
+ ? workspace
4521
+ : null;
4522
+ if (options?.dedupeGitState &&
4523
+ this.shouldSkipWorkspaceGitWatchUpdate(nextWorkspaceId, nextWorkspace)) {
4524
+ continue;
4525
+ }
4526
+ this.rememberWorkspaceGitWatchFingerprint(nextWorkspaceId, nextWorkspace);
4527
+ if (!nextWorkspace) {
4308
4528
  this.bufferOrEmitWorkspaceUpdate(subscription, {
4309
- kind: 'remove',
4529
+ kind: "remove",
4310
4530
  id: nextWorkspaceId,
4311
4531
  });
4312
4532
  continue;
4313
4533
  }
4314
4534
  this.bufferOrEmitWorkspaceUpdate(subscription, {
4315
- kind: 'upsert',
4316
- workspace,
4535
+ kind: "upsert",
4536
+ workspace: nextWorkspace,
4317
4537
  });
4318
4538
  }
4319
4539
  }
@@ -4335,16 +4555,20 @@ export class Session {
4335
4555
  const descriptorsByWorkspaceId = new Map(all.map((entry) => [entry.id, entry]));
4336
4556
  for (const workspaceId of uniqueWorkspaceCwds) {
4337
4557
  const workspace = descriptorsByWorkspaceId.get(workspaceId);
4338
- if (!workspace || !this.matchesWorkspaceFilter({ workspace, filter: subscription.filter })) {
4558
+ const nextWorkspace = workspace && this.matchesWorkspaceFilter({ workspace, filter: subscription.filter })
4559
+ ? workspace
4560
+ : null;
4561
+ this.rememberWorkspaceGitWatchFingerprint(workspaceId, nextWorkspace);
4562
+ if (!nextWorkspace) {
4339
4563
  this.bufferOrEmitWorkspaceUpdate(subscription, {
4340
- kind: 'remove',
4564
+ kind: "remove",
4341
4565
  id: workspaceId,
4342
4566
  });
4343
4567
  continue;
4344
4568
  }
4345
4569
  this.bufferOrEmitWorkspaceUpdate(subscription, {
4346
- kind: 'upsert',
4347
- workspace,
4570
+ kind: "upsert",
4571
+ workspace: nextWorkspace,
4348
4572
  });
4349
4573
  }
4350
4574
  }
@@ -4373,7 +4597,7 @@ export class Session {
4373
4597
  }
4374
4598
  }
4375
4599
  this.emit({
4376
- type: 'fetch_agents_response',
4600
+ type: "fetch_agents_response",
4377
4601
  payload: {
4378
4602
  requestId: request.requestId,
4379
4603
  ...(subscriptionId ? { subscriptionId } : {}),
@@ -4388,11 +4612,11 @@ export class Session {
4388
4612
  if (subscriptionId && this.agentUpdatesSubscription?.subscriptionId === subscriptionId) {
4389
4613
  this.agentUpdatesSubscription = null;
4390
4614
  }
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');
4615
+ const code = error instanceof SessionRequestError ? error.code : "fetch_agents_failed";
4616
+ const message = error instanceof Error ? error.message : "Failed to fetch agents";
4617
+ this.sessionLogger.error({ err: error }, "Failed to handle fetch_agents_request");
4394
4618
  this.emit({
4395
- type: 'rpc_error',
4619
+ type: "rpc_error",
4396
4620
  payload: {
4397
4621
  requestId: request.requestId,
4398
4622
  requestType: request.type,
@@ -4419,6 +4643,7 @@ export class Session {
4419
4643
  };
4420
4644
  }
4421
4645
  const payload = await this.listFetchWorkspacesEntries(request);
4646
+ this.primeWorkspaceGitWatchFingerprints(payload.entries);
4422
4647
  const snapshotLatestActivityByWorkspaceId = new Map();
4423
4648
  for (const entry of payload.entries) {
4424
4649
  const parsedLatestActivity = entry.activityAt
@@ -4429,7 +4654,7 @@ export class Session {
4429
4654
  }
4430
4655
  }
4431
4656
  this.emit({
4432
- type: 'fetch_workspaces_response',
4657
+ type: "fetch_workspaces_response",
4433
4658
  payload: {
4434
4659
  requestId: request.requestId,
4435
4660
  ...(subscriptionId ? { subscriptionId } : {}),
@@ -4444,11 +4669,11 @@ export class Session {
4444
4669
  if (subscriptionId && this.workspaceUpdatesSubscription?.subscriptionId === subscriptionId) {
4445
4670
  this.workspaceUpdatesSubscription = null;
4446
4671
  }
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');
4672
+ const code = error instanceof SessionRequestError ? error.code : "fetch_workspaces_failed";
4673
+ const message = error instanceof Error ? error.message : "Failed to fetch workspaces";
4674
+ this.sessionLogger.error({ err: error }, "Failed to handle fetch_workspaces_request");
4450
4675
  this.emit({
4451
- type: 'rpc_error',
4676
+ type: "rpc_error",
4452
4677
  payload: {
4453
4678
  requestId: request.requestId,
4454
4679
  requestType: request.type,
@@ -4464,7 +4689,7 @@ export class Session {
4464
4689
  await this.emitWorkspaceUpdateForCwd(workspace.cwd);
4465
4690
  const descriptor = await this.describeWorkspaceRecord(workspace);
4466
4691
  this.emit({
4467
- type: 'open_project_response',
4692
+ type: "open_project_response",
4468
4693
  payload: {
4469
4694
  requestId: request.requestId,
4470
4695
  workspace: descriptor,
@@ -4473,10 +4698,10 @@ export class Session {
4473
4698
  });
4474
4699
  }
4475
4700
  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');
4701
+ const message = error instanceof Error ? error.message : "Failed to open project";
4702
+ this.sessionLogger.error({ err: error, cwd: request.cwd }, "Failed to open project");
4478
4703
  this.emit({
4479
- type: 'open_project_response',
4704
+ type: "open_project_response",
4480
4705
  payload: {
4481
4706
  requestId: request.requestId,
4482
4707
  workspace: null,
@@ -4485,20 +4710,123 @@ export class Session {
4485
4710
  });
4486
4711
  }
4487
4712
  }
4713
+ async handleCreatePaseoWorktreeRequest(request) {
4714
+ try {
4715
+ const checkout = await getCheckoutStatusLite(request.cwd, { paseoHome: this.paseoHome });
4716
+ if (!checkout.isGit) {
4717
+ throw new Error("Create worktree requires a git repository");
4718
+ }
4719
+ const repoRoot = checkout.isPaseoOwnedWorktree ? checkout.mainRepoRoot : request.cwd;
4720
+ const baseBranch = await resolveRepositoryDefaultBranch(repoRoot);
4721
+ if (!baseBranch) {
4722
+ throw new Error("Unable to resolve repository default branch");
4723
+ }
4724
+ const normalizedSlug = request.worktreeSlug ? slugify(request.worktreeSlug) : uuidv4();
4725
+ const validation = validateBranchSlug(normalizedSlug);
4726
+ if (!validation.valid) {
4727
+ throw new Error(`Invalid worktree name: ${validation.error}`);
4728
+ }
4729
+ const worktreePath = await computeWorktreePath(repoRoot, normalizedSlug, this.paseoHome);
4730
+ const workspace = await this.registerPendingWorktreeWorkspace({
4731
+ repoRoot,
4732
+ worktreePath,
4733
+ branchName: normalizedSlug,
4734
+ });
4735
+ const descriptor = await this.describeWorkspaceRecord(workspace);
4736
+ this.emit({
4737
+ type: "create_paseo_worktree_response",
4738
+ payload: {
4739
+ workspace: descriptor,
4740
+ error: null,
4741
+ setupTerminalId: null,
4742
+ requestId: request.requestId,
4743
+ },
4744
+ });
4745
+ void this.createPaseoWorktreeInBackground({
4746
+ requestCwd: request.cwd,
4747
+ repoRoot,
4748
+ baseBranch,
4749
+ slug: normalizedSlug,
4750
+ worktreePath,
4751
+ });
4752
+ }
4753
+ catch (error) {
4754
+ const message = error instanceof Error ? error.message : "Failed to create worktree";
4755
+ this.sessionLogger.error({ err: error, cwd: request.cwd, worktreeSlug: request.worktreeSlug }, "Failed to create worktree");
4756
+ this.emit({
4757
+ type: "create_paseo_worktree_response",
4758
+ payload: {
4759
+ workspace: null,
4760
+ error: message,
4761
+ setupTerminalId: null,
4762
+ requestId: request.requestId,
4763
+ },
4764
+ });
4765
+ }
4766
+ }
4767
+ async createPaseoWorktreeInBackground(options) {
4768
+ let setupTerminalId = null;
4769
+ try {
4770
+ await createAgentWorktree({
4771
+ cwd: options.repoRoot,
4772
+ branchName: options.slug,
4773
+ baseBranch: options.baseBranch,
4774
+ worktreeSlug: options.slug,
4775
+ paseoHome: this.paseoHome,
4776
+ });
4777
+ const setupCommands = getWorktreeSetupCommands(options.worktreePath);
4778
+ if (setupCommands.length > 0 && this.terminalManager) {
4779
+ const runtimeEnv = await resolveWorktreeRuntimeEnv({
4780
+ worktreePath: options.worktreePath,
4781
+ branchName: options.slug,
4782
+ repoRootPath: options.repoRoot,
4783
+ });
4784
+ this.terminalManager.registerCwdEnv({
4785
+ cwd: options.worktreePath,
4786
+ env: runtimeEnv,
4787
+ });
4788
+ const terminal = await this.terminalManager.createTerminal({
4789
+ cwd: options.worktreePath,
4790
+ name: `setup-${options.slug}`,
4791
+ env: runtimeEnv,
4792
+ });
4793
+ setupTerminalId = terminal.id;
4794
+ for (const command of setupCommands) {
4795
+ terminal.send({
4796
+ type: "input",
4797
+ data: `${command}\r`,
4798
+ });
4799
+ }
4800
+ }
4801
+ }
4802
+ catch (error) {
4803
+ this.sessionLogger.error({
4804
+ err: error,
4805
+ cwd: options.requestCwd,
4806
+ repoRoot: options.repoRoot,
4807
+ worktreeSlug: options.slug,
4808
+ worktreePath: options.worktreePath,
4809
+ setupTerminalId,
4810
+ }, "Background worktree creation failed");
4811
+ }
4812
+ finally {
4813
+ await this.emitWorkspaceUpdateForCwd(options.worktreePath);
4814
+ }
4815
+ }
4488
4816
  async handleArchiveWorkspaceRequest(request) {
4489
4817
  try {
4490
4818
  const existing = await this.workspaceRegistry.get(request.workspaceId);
4491
4819
  if (!existing) {
4492
4820
  throw new Error(`Workspace not found: ${request.workspaceId}`);
4493
4821
  }
4494
- if (existing.kind === 'worktree') {
4495
- throw new Error('Use worktree archive for Paseo worktrees');
4822
+ if (existing.kind === "worktree") {
4823
+ throw new Error("Use worktree archive for Paseo worktrees");
4496
4824
  }
4497
4825
  const archivedAt = new Date().toISOString();
4498
4826
  await this.archiveWorkspaceRecord(request.workspaceId, archivedAt);
4499
4827
  await this.emitWorkspaceUpdateForCwd(existing.cwd);
4500
4828
  this.emit({
4501
- type: 'archive_workspace_response',
4829
+ type: "archive_workspace_response",
4502
4830
  payload: {
4503
4831
  requestId: request.requestId,
4504
4832
  workspaceId: request.workspaceId,
@@ -4508,10 +4836,10 @@ export class Session {
4508
4836
  });
4509
4837
  }
4510
4838
  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');
4839
+ const message = error instanceof Error ? error.message : "Failed to archive workspace";
4840
+ this.sessionLogger.error({ err: error, workspaceId: request.workspaceId }, "Failed to archive workspace");
4513
4841
  this.emit({
4514
- type: 'archive_workspace_response',
4842
+ type: "archive_workspace_response",
4515
4843
  payload: {
4516
4844
  requestId: request.requestId,
4517
4845
  workspaceId: request.workspaceId,
@@ -4525,7 +4853,7 @@ export class Session {
4525
4853
  const resolved = await this.resolveAgentIdentifier(agentIdOrIdentifier);
4526
4854
  if (!resolved.ok) {
4527
4855
  this.emit({
4528
- type: 'fetch_agent_response',
4856
+ type: "fetch_agent_response",
4529
4857
  payload: { requestId, agent: null, project: null, error: resolved.error },
4530
4858
  });
4531
4859
  return;
@@ -4533,7 +4861,7 @@ export class Session {
4533
4861
  const agent = await this.getAgentPayloadById(resolved.agentId);
4534
4862
  if (!agent) {
4535
4863
  this.emit({
4536
- type: 'fetch_agent_response',
4864
+ type: "fetch_agent_response",
4537
4865
  payload: {
4538
4866
  requestId,
4539
4867
  agent: null,
@@ -4545,18 +4873,18 @@ export class Session {
4545
4873
  }
4546
4874
  const project = await this.buildProjectPlacement(agent.cwd);
4547
4875
  this.emit({
4548
- type: 'fetch_agent_response',
4876
+ type: "fetch_agent_response",
4549
4877
  payload: { requestId, agent, project, error: null },
4550
4878
  });
4551
4879
  }
4552
4880
  async handleFetchAgentTimelineRequest(msg) {
4553
- const direction = msg.direction ?? (msg.cursor ? 'after' : 'tail');
4554
- const projection = msg.projection ?? 'projected';
4881
+ const direction = msg.direction ?? (msg.cursor ? "after" : "tail");
4882
+ const projection = msg.projection ?? "projected";
4555
4883
  const requestedLimit = msg.limit;
4556
- const limit = requestedLimit ?? (direction === 'after' ? 0 : undefined);
4557
- const shouldLimitByProjectedWindow = projection === 'canonical' &&
4558
- direction === 'tail' &&
4559
- typeof requestedLimit === 'number' &&
4884
+ const limit = requestedLimit ?? (direction === "after" ? 0 : undefined);
4885
+ const shouldLimitByProjectedWindow = projection === "canonical" &&
4886
+ direction === "tail" &&
4887
+ typeof requestedLimit === "number" &&
4560
4888
  requestedLimit > 0;
4561
4889
  const cursor = msg.cursor
4562
4890
  ? {
@@ -4570,7 +4898,7 @@ export class Session {
4570
4898
  let timeline = this.agentManager.fetchTimeline(msg.agentId, {
4571
4899
  direction,
4572
4900
  cursor,
4573
- limit: shouldLimitByProjectedWindow && typeof requestedLimit === 'number'
4901
+ limit: shouldLimitByProjectedWindow && typeof requestedLimit === "number"
4574
4902
  ? Math.max(1, Math.floor(requestedLimit))
4575
4903
  : limit,
4576
4904
  });
@@ -4596,7 +4924,7 @@ export class Session {
4596
4924
  const startsAtLoadedBoundary = firstLoadedRow != null &&
4597
4925
  firstSelectedRow != null &&
4598
4926
  firstSelectedRow.seq === firstLoadedRow.seq;
4599
- const boundaryIsAssistantChunk = startsAtLoadedBoundary && firstLoadedRow.item.type === 'assistant_message';
4927
+ const boundaryIsAssistantChunk = startsAtLoadedBoundary && firstLoadedRow.item.type === "assistant_message";
4600
4928
  if (!needsMoreProjectedEntries && !boundaryIsAssistantChunk) {
4601
4929
  break;
4602
4930
  }
@@ -4636,7 +4964,7 @@ export class Session {
4636
4964
  entries = projectTimelineRows(timeline.rows, snapshot.provider, projection);
4637
4965
  }
4638
4966
  this.emit({
4639
- type: 'fetch_agent_timeline_response',
4967
+ type: "fetch_agent_timeline_response",
4640
4968
  payload: {
4641
4969
  requestId: msg.requestId,
4642
4970
  agentId: msg.agentId,
@@ -4658,16 +4986,16 @@ export class Session {
4658
4986
  });
4659
4987
  }
4660
4988
  catch (error) {
4661
- this.sessionLogger.error({ err: error, agentId: msg.agentId }, 'Failed to handle fetch_agent_timeline_request');
4989
+ this.sessionLogger.error({ err: error, agentId: msg.agentId }, "Failed to handle fetch_agent_timeline_request");
4662
4990
  this.emit({
4663
- type: 'fetch_agent_timeline_response',
4991
+ type: "fetch_agent_timeline_response",
4664
4992
  payload: {
4665
4993
  requestId: msg.requestId,
4666
4994
  agentId: msg.agentId,
4667
4995
  agent: null,
4668
4996
  direction,
4669
4997
  projection,
4670
- epoch: '',
4998
+ epoch: "",
4671
4999
  reset: false,
4672
5000
  staleCursor: false,
4673
5001
  gap: false,
@@ -4686,7 +5014,7 @@ export class Session {
4686
5014
  const resolved = await this.resolveAgentIdentifier(msg.agentId);
4687
5015
  if (!resolved.ok) {
4688
5016
  this.emit({
4689
- type: 'send_agent_message_response',
5017
+ type: "send_agent_message_response",
4690
5018
  payload: {
4691
5019
  requestId: msg.requestId,
4692
5020
  agentId: msg.agentId,
@@ -4707,13 +5035,13 @@ export class Session {
4707
5035
  });
4708
5036
  }
4709
5037
  catch (error) {
4710
- this.sessionLogger.error({ err: error, agentId }, 'Failed to record user message for send_agent_message_request');
5038
+ this.sessionLogger.error({ err: error, agentId }, "Failed to record user message for send_agent_message_request");
4711
5039
  }
4712
5040
  const prompt = this.buildAgentPrompt(msg.text, msg.images);
4713
5041
  const started = this.startAgentStream(agentId, prompt);
4714
5042
  if (!started.ok) {
4715
5043
  this.emit({
4716
- type: 'send_agent_message_response',
5044
+ type: "send_agent_message_response",
4717
5045
  payload: {
4718
5046
  requestId: msg.requestId,
4719
5047
  agentId,
@@ -4725,18 +5053,18 @@ export class Session {
4725
5053
  }
4726
5054
  const startAbort = new AbortController();
4727
5055
  const startTimeoutMs = 15000;
4728
- const startTimeout = setTimeout(() => startAbort.abort('timeout'), startTimeoutMs);
5056
+ const startTimeout = setTimeout(() => startAbort.abort("timeout"), startTimeoutMs);
4729
5057
  try {
4730
5058
  await this.agentManager.waitForAgentRunStart(agentId, { signal: startAbort.signal });
4731
5059
  }
4732
5060
  catch (error) {
4733
5061
  const message = error instanceof Error
4734
5062
  ? error.message
4735
- : typeof error === 'string'
5063
+ : typeof error === "string"
4736
5064
  ? error
4737
- : 'Unknown error';
5065
+ : "Unknown error";
4738
5066
  this.emit({
4739
- type: 'send_agent_message_response',
5067
+ type: "send_agent_message_response",
4740
5068
  payload: {
4741
5069
  requestId: msg.requestId,
4742
5070
  agentId,
@@ -4750,7 +5078,7 @@ export class Session {
4750
5078
  clearTimeout(startTimeout);
4751
5079
  }
4752
5080
  this.emit({
4753
- type: 'send_agent_message_response',
5081
+ type: "send_agent_message_response",
4754
5082
  payload: {
4755
5083
  requestId: msg.requestId,
4756
5084
  agentId,
@@ -4760,9 +5088,13 @@ export class Session {
4760
5088
  });
4761
5089
  }
4762
5090
  catch (error) {
4763
- const message = error instanceof Error ? error.message : typeof error === 'string' ? error : 'Unknown error';
5091
+ const message = error instanceof Error
5092
+ ? error.message
5093
+ : typeof error === "string"
5094
+ ? error
5095
+ : "Unknown error";
4764
5096
  this.emit({
4765
- type: 'send_agent_message_response',
5097
+ type: "send_agent_message_response",
4766
5098
  payload: {
4767
5099
  requestId: msg.requestId,
4768
5100
  agentId: resolved.agentId,
@@ -4776,10 +5108,10 @@ export class Session {
4776
5108
  const resolved = await this.resolveAgentIdentifier(agentIdOrIdentifier);
4777
5109
  if (!resolved.ok) {
4778
5110
  this.emit({
4779
- type: 'wait_for_finish_response',
5111
+ type: "wait_for_finish_response",
4780
5112
  payload: {
4781
5113
  requestId,
4782
- status: 'error',
5114
+ status: "error",
4783
5115
  final: null,
4784
5116
  error: resolved.error,
4785
5117
  lastMessage: null,
@@ -4793,10 +5125,10 @@ export class Session {
4793
5125
  const record = await this.agentStorage.get(agentId);
4794
5126
  if (!record || record.internal) {
4795
5127
  this.emit({
4796
- type: 'wait_for_finish_response',
5128
+ type: "wait_for_finish_response",
4797
5129
  payload: {
4798
5130
  requestId,
4799
- status: 'error',
5131
+ status: "error",
4800
5132
  final: null,
4801
5133
  error: `Agent not found: ${agentId}`,
4802
5134
  lastMessage: null,
@@ -4805,22 +5137,22 @@ export class Session {
4805
5137
  return;
4806
5138
  }
4807
5139
  const final = this.buildStoredAgentPayload(record);
4808
- const status = record.attentionReason === 'permission'
4809
- ? 'permission'
4810
- : record.lastStatus === 'error'
4811
- ? 'error'
4812
- : 'idle';
5140
+ const status = record.attentionReason === "permission"
5141
+ ? "permission"
5142
+ : record.lastStatus === "error"
5143
+ ? "error"
5144
+ : "idle";
4813
5145
  this.emit({
4814
- type: 'wait_for_finish_response',
5146
+ type: "wait_for_finish_response",
4815
5147
  payload: { requestId, status, final, error: null, lastMessage: null },
4816
5148
  });
4817
5149
  return;
4818
5150
  }
4819
5151
  const abortController = new AbortController();
4820
- const hasTimeout = typeof timeoutMs === 'number' && timeoutMs > 0;
5152
+ const hasTimeout = typeof timeoutMs === "number" && timeoutMs > 0;
4821
5153
  const timeoutHandle = hasTimeout
4822
5154
  ? setTimeout(() => {
4823
- abortController.abort('timeout');
5155
+ abortController.abort("timeout");
4824
5156
  }, timeoutMs)
4825
5157
  : null;
4826
5158
  try {
@@ -4831,28 +5163,32 @@ export class Session {
4831
5163
  if (!final) {
4832
5164
  throw new Error(`Agent ${agentId} disappeared while waiting`);
4833
5165
  }
4834
- let status = result.permission ? 'permission' : result.status === 'error' ? 'error' : 'idle';
5166
+ let status = result.permission
5167
+ ? "permission"
5168
+ : result.status === "error"
5169
+ ? "error"
5170
+ : "idle";
4835
5171
  this.emit({
4836
- type: 'wait_for_finish_response',
5172
+ type: "wait_for_finish_response",
4837
5173
  payload: { requestId, status, final, error: null, lastMessage: result.lastMessage },
4838
5174
  });
4839
5175
  }
4840
5176
  catch (error) {
4841
5177
  const isAbort = error instanceof Error &&
4842
- (error.name === 'AbortError' || error.message.toLowerCase().includes('aborted'));
5178
+ (error.name === "AbortError" || error.message.toLowerCase().includes("aborted"));
4843
5179
  if (!isAbort) {
4844
5180
  const message = error instanceof Error
4845
5181
  ? error.message
4846
- : typeof error === 'string'
5182
+ : typeof error === "string"
4847
5183
  ? error
4848
- : 'Unknown error';
4849
- this.sessionLogger.error({ err: error, agentId }, 'wait_for_finish_request failed');
5184
+ : "Unknown error";
5185
+ this.sessionLogger.error({ err: error, agentId }, "wait_for_finish_request failed");
4850
5186
  const final = await this.getAgentPayloadById(agentId);
4851
5187
  this.emit({
4852
- type: 'wait_for_finish_response',
5188
+ type: "wait_for_finish_response",
4853
5189
  payload: {
4854
5190
  requestId,
4855
- status: 'error',
5191
+ status: "error",
4856
5192
  final,
4857
5193
  error: message,
4858
5194
  lastMessage: null,
@@ -4865,8 +5201,8 @@ export class Session {
4865
5201
  throw new Error(`Agent ${agentId} disappeared while waiting`);
4866
5202
  }
4867
5203
  this.emit({
4868
- type: 'wait_for_finish_response',
4869
- payload: { requestId, status: 'timeout', final, error: null, lastMessage: null },
5204
+ type: "wait_for_finish_response",
5205
+ payload: { requestId, status: "timeout", final, error: null, lastMessage: null },
4870
5206
  });
4871
5207
  }
4872
5208
  finally {
@@ -4880,31 +5216,30 @@ export class Session {
4880
5216
  */
4881
5217
  async handleAudioChunk(msg) {
4882
5218
  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');
5219
+ this.sessionLogger.warn("Received voice_audio_chunk while voice mode is disabled; transcript will be emitted but voice assistant turn is skipped");
4884
5220
  }
4885
- const chunkFormat = msg.format || 'audio/wav';
5221
+ const chunkFormat = msg.format || "audio/wav";
4886
5222
  if (this.isVoiceMode) {
4887
5223
  if (!this.voiceTurnController) {
4888
- throw new Error('Voice mode is enabled but the voice turn controller is not running');
5224
+ throw new Error("Voice mode is enabled but the voice turn controller is not running");
4889
5225
  }
4890
- const chunkBytes = Buffer.byteLength(msg.audio, 'base64');
5226
+ const chunkBytes = Buffer.byteLength(msg.audio, "base64");
4891
5227
  this.voiceInputChunkCount += 1;
4892
5228
  this.voiceInputBytes += chunkBytes;
4893
5229
  if (this.voiceInputChunkCount === 1) {
4894
5230
  this.sessionLogger.info({
4895
5231
  format: chunkFormat,
4896
5232
  audioBytes: chunkBytes,
4897
- }, 'Received first voice_audio_chunk for active voice mode');
5233
+ }, "Received first voice_audio_chunk for active voice mode");
4898
5234
  }
4899
5235
  const now = Date.now();
4900
- if (this.voiceInputChunkCount % 50 === 0 ||
4901
- now - this.voiceInputWindowStartedAt >= 1000) {
5236
+ if (this.voiceInputChunkCount % 50 === 0 || now - this.voiceInputWindowStartedAt >= 1000) {
4902
5237
  this.sessionLogger.info({
4903
5238
  chunkCount: this.voiceInputChunkCount,
4904
5239
  audioBytes: this.voiceInputBytes,
4905
5240
  windowMs: now - this.voiceInputWindowStartedAt,
4906
5241
  format: chunkFormat,
4907
- }, 'Voice input chunk summary');
5242
+ }, "Voice input chunk summary");
4908
5243
  this.voiceInputWindowStartedAt = now;
4909
5244
  this.voiceInputChunkCount = 0;
4910
5245
  this.voiceInputBytes = 0;
@@ -4915,8 +5250,8 @@ export class Session {
4915
5250
  });
4916
5251
  return;
4917
5252
  }
4918
- const chunkBuffer = Buffer.from(msg.audio, 'base64');
4919
- const isPCMChunk = chunkFormat.toLowerCase().includes('pcm');
5253
+ const chunkBuffer = Buffer.from(msg.audio, "base64");
5254
+ const isPCMChunk = chunkFormat.toLowerCase().includes("pcm");
4920
5255
  if (!this.audioBuffer) {
4921
5256
  this.audioBuffer = {
4922
5257
  chunks: [],
@@ -4928,7 +5263,7 @@ export class Session {
4928
5263
  // If the format changes mid-stream, flush what we have first
4929
5264
  if (this.audioBuffer.isPCM !== isPCMChunk) {
4930
5265
  this.sessionLogger.debug({
4931
- oldFormat: this.audioBuffer.isPCM ? 'pcm' : this.audioBuffer.format,
5266
+ oldFormat: this.audioBuffer.isPCM ? "pcm" : this.audioBuffer.format,
4932
5267
  newFormat: chunkFormat,
4933
5268
  }, `Audio format changed mid-stream, flushing current buffer`);
4934
5269
  const finalized = this.finalizeBufferedAudio();
@@ -4984,7 +5319,7 @@ export class Session {
4984
5319
  const wavBuffer = convertPCMToWavBuffer(pcmBuffer, PCM_SAMPLE_RATE, PCM_CHANNELS, PCM_BITS_PER_SAMPLE);
4985
5320
  return {
4986
5321
  audio: wavBuffer,
4987
- format: 'audio/wav',
5322
+ format: "audio/wav",
4988
5323
  };
4989
5324
  }
4990
5325
  return {
@@ -4993,7 +5328,7 @@ export class Session {
4993
5328
  };
4994
5329
  }
4995
5330
  async processCompletedAudio(audio, format) {
4996
- if (this.processingPhase === 'transcribing') {
5331
+ if (this.processingPhase === "transcribing") {
4997
5332
  this.sessionLogger.debug({ phase: this.processingPhase, segmentCount: this.pendingAudioSegments.length + 1 }, `Buffering audio segment (phase: ${this.processingPhase})`);
4998
5333
  this.pendingAudioSegments.push({
4999
5334
  audio,
@@ -5019,7 +5354,7 @@ export class Session {
5019
5354
  await this.processAudio(audio, format);
5020
5355
  }
5021
5356
  async flushPendingAudioSegments(reason) {
5022
- if (this.processingPhase === 'transcribing' || this.pendingAudioSegments.length === 0) {
5357
+ if (this.processingPhase === "transcribing" || this.pendingAudioSegments.length === 0) {
5023
5358
  return;
5024
5359
  }
5025
5360
  const pendingSegments = [...this.pendingAudioSegments];
@@ -5034,21 +5369,21 @@ export class Session {
5034
5369
  * Process audio through STT and then LLM
5035
5370
  */
5036
5371
  async processAudio(audio, format) {
5037
- this.setPhase('transcribing');
5372
+ this.setPhase("transcribing");
5038
5373
  this.emit({
5039
- type: 'activity_log',
5374
+ type: "activity_log",
5040
5375
  payload: {
5041
5376
  id: uuidv4(),
5042
5377
  timestamp: new Date(),
5043
- type: 'system',
5044
- content: 'Transcribing audio...',
5378
+ type: "system",
5379
+ content: "Transcribing audio...",
5045
5380
  },
5046
5381
  });
5047
5382
  try {
5048
5383
  const requestId = uuidv4();
5049
5384
  const result = await this.sttManager.transcribe(audio, format, {
5050
5385
  requestId,
5051
- label: this.isVoiceMode ? 'voice' : 'buffered',
5386
+ label: this.isVoiceMode ? "voice" : "buffered",
5052
5387
  });
5053
5388
  const transcriptText = result.text.trim();
5054
5389
  this.sessionLogger.info({
@@ -5056,7 +5391,7 @@ export class Session {
5056
5391
  isVoiceMode: this.isVoiceMode,
5057
5392
  transcriptLength: transcriptText.length,
5058
5393
  transcript: transcriptText,
5059
- }, 'Transcription result');
5394
+ }, "Transcription result");
5060
5395
  await this.handleTranscriptionResultPayload({
5061
5396
  text: result.text,
5062
5397
  language: result.language,
@@ -5070,15 +5405,15 @@ export class Session {
5070
5405
  });
5071
5406
  }
5072
5407
  catch (error) {
5073
- this.setPhase('idle');
5074
- this.clearSpeechInProgress('transcription error');
5075
- await this.flushPendingAudioSegments('transcription error');
5408
+ this.setPhase("idle");
5409
+ this.clearSpeechInProgress("transcription error");
5410
+ await this.flushPendingAudioSegments("transcription error");
5076
5411
  this.emit({
5077
- type: 'activity_log',
5412
+ type: "activity_log",
5078
5413
  payload: {
5079
5414
  id: uuidv4(),
5080
5415
  timestamp: new Date(),
5081
- type: 'error',
5416
+ type: "error",
5082
5417
  content: `Transcription error: ${error.message}`,
5083
5418
  },
5084
5419
  });
@@ -5088,7 +5423,7 @@ export class Session {
5088
5423
  async handleTranscriptionResultPayload(result) {
5089
5424
  const transcriptText = result.text.trim();
5090
5425
  this.emit({
5091
- type: 'transcription_result',
5426
+ type: "transcription_result",
5092
5427
  payload: {
5093
5428
  text: result.text,
5094
5429
  ...(result.language ? { language: result.language } : {}),
@@ -5104,21 +5439,21 @@ export class Session {
5104
5439
  },
5105
5440
  });
5106
5441
  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');
5442
+ this.sessionLogger.debug("Empty transcription (false positive), not aborting");
5443
+ this.setPhase("idle");
5444
+ this.clearSpeechInProgress("empty transcription");
5445
+ await this.flushPendingAudioSegments("empty transcription");
5111
5446
  return;
5112
5447
  }
5113
5448
  // Has content - abort any in-progress stream now
5114
5449
  this.createAbortController();
5115
5450
  if (result.debugRecordingPath) {
5116
5451
  this.emit({
5117
- type: 'activity_log',
5452
+ type: "activity_log",
5118
5453
  payload: {
5119
5454
  id: uuidv4(),
5120
5455
  timestamp: new Date(),
5121
- type: 'system',
5456
+ type: "system",
5122
5457
  content: `Saved input audio: ${result.debugRecordingPath}`,
5123
5458
  metadata: {
5124
5459
  recordingPath: result.debugRecordingPath,
@@ -5129,11 +5464,11 @@ export class Session {
5129
5464
  });
5130
5465
  }
5131
5466
  this.emit({
5132
- type: 'activity_log',
5467
+ type: "activity_log",
5133
5468
  payload: {
5134
5469
  id: uuidv4(),
5135
5470
  timestamp: new Date(),
5136
- type: 'transcript',
5471
+ type: "transcript",
5137
5472
  content: result.text,
5138
5473
  metadata: {
5139
5474
  ...(result.language ? { language: result.language } : {}),
@@ -5141,23 +5476,23 @@ export class Session {
5141
5476
  },
5142
5477
  },
5143
5478
  });
5144
- this.clearSpeechInProgress('transcription complete');
5145
- this.setPhase('idle');
5479
+ this.clearSpeechInProgress("transcription complete");
5480
+ this.setPhase("idle");
5146
5481
  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');
5482
+ this.sessionLogger.debug({ requestId: result.requestId }, "Skipping voice agent processing because voice mode is disabled");
5483
+ await this.flushPendingAudioSegments("voice mode disabled");
5149
5484
  return;
5150
5485
  }
5151
5486
  const agentId = this.voiceModeAgentId;
5152
5487
  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');
5488
+ this.sessionLogger.warn({ requestId: result.requestId }, "Skipping voice agent processing because no agent is currently voice-enabled");
5489
+ await this.flushPendingAudioSegments("no active voice agent");
5155
5490
  return;
5156
5491
  }
5157
5492
  // Route voice utterances through the same send path as regular text input:
5158
5493
  // interrupt-if-running, record message, then start a new stream.
5159
5494
  await this.handleSendAgentMessage(agentId, result.text);
5160
- await this.flushPendingAudioSegments('transcription complete');
5495
+ await this.flushPendingAudioSegments("transcription complete");
5161
5496
  }
5162
5497
  registerVoiceBridgeForAgent(agentId) {
5163
5498
  this.registerVoiceSpeakHandler?.(agentId, async ({ text, signal }) => {
@@ -5165,16 +5500,16 @@ export class Session {
5165
5500
  agentId,
5166
5501
  textLength: text.length,
5167
5502
  preview: text.slice(0, 160),
5168
- }, 'Voice speak tool call received by session handler');
5503
+ }, "Voice speak tool call received by session handler");
5169
5504
  const abortSignal = signal ?? this.abortController.signal;
5170
5505
  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');
5506
+ this.sessionLogger.info({ agentId, textLength: text.length }, "Voice speak tool call finished playback");
5172
5507
  this.emit({
5173
- type: 'activity_log',
5508
+ type: "activity_log",
5174
5509
  payload: {
5175
5510
  id: uuidv4(),
5176
5511
  timestamp: new Date(),
5177
- type: 'assistant',
5512
+ type: "assistant",
5178
5513
  content: text,
5179
5514
  },
5180
5515
  });
@@ -5191,24 +5526,24 @@ export class Session {
5191
5526
  async handleAbort() {
5192
5527
  this.sessionLogger.info({ phase: this.processingPhase }, `Abort request, phase: ${this.processingPhase}`);
5193
5528
  this.abortController.abort();
5194
- this.ttsManager.cancelPendingPlaybacks('abort request');
5529
+ this.ttsManager.cancelPendingPlaybacks("abort request");
5195
5530
  // Voice abort should always interrupt active agent output immediately.
5196
5531
  if (this.isVoiceMode && this.voiceModeAgentId) {
5197
5532
  try {
5198
5533
  await this.interruptAgentIfRunning(this.voiceModeAgentId);
5199
5534
  }
5200
5535
  catch (error) {
5201
- this.sessionLogger.warn({ err: error, agentId: this.voiceModeAgentId }, 'Failed to interrupt active voice-mode agent on abort');
5536
+ this.sessionLogger.warn({ err: error, agentId: this.voiceModeAgentId }, "Failed to interrupt active voice-mode agent on abort");
5202
5537
  }
5203
5538
  }
5204
- if (this.processingPhase === 'transcribing') {
5539
+ if (this.processingPhase === "transcribing") {
5205
5540
  // Still in STT phase - we'll buffer the next audio
5206
- this.sessionLogger.debug('Will buffer next audio (currently transcribing)');
5541
+ this.sessionLogger.debug("Will buffer next audio (currently transcribing)");
5207
5542
  // Phase stays as 'transcribing', handleAudioChunk will handle buffering
5208
5543
  return;
5209
5544
  }
5210
5545
  // Reset phase to idle and clear pending non-voice buffers.
5211
- this.setPhase('idle');
5546
+ this.setPhase("idle");
5212
5547
  this.pendingAudioSegments = [];
5213
5548
  this.clearBufferTimeout();
5214
5549
  }
@@ -5229,20 +5564,20 @@ export class Session {
5229
5564
  const phaseBeforeAbort = this.processingPhase;
5230
5565
  const hadActiveStream = this.hasActiveAgentRun(this.voiceModeAgentId);
5231
5566
  this.speechInProgress = true;
5232
- this.sessionLogger.debug('Voice speech detected – aborting playback and active agent run');
5567
+ this.sessionLogger.debug("Voice speech detected – aborting playback and active agent run");
5233
5568
  if (this.pendingAudioSegments.length > 0) {
5234
5569
  this.sessionLogger.debug({ segmentCount: this.pendingAudioSegments.length }, `Dropping ${this.pendingAudioSegments.length} buffered audio segment(s) due to voice speech`);
5235
5570
  this.pendingAudioSegments = [];
5236
5571
  }
5237
5572
  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` : ''})`);
5573
+ 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
5574
  this.audioBuffer = null;
5240
5575
  }
5241
5576
  this.clearBufferTimeout();
5242
5577
  this.abortController.abort();
5243
5578
  await this.handleAbort();
5244
5579
  const latencyMs = Date.now() - chunkReceivedAt;
5245
- this.sessionLogger.debug({ latencyMs, phaseBeforeAbort, hadActiveStream }, '[Telemetry] barge_in.llm_abort_latency');
5580
+ this.sessionLogger.debug({ latencyMs, phaseBeforeAbort, hadActiveStream }, "[Telemetry] barge_in.llm_abort_latency");
5246
5581
  }
5247
5582
  /**
5248
5583
  * Clear speech-in-progress flag once the user turn has completed
@@ -5277,9 +5612,9 @@ export class Session {
5277
5612
  setBufferTimeout() {
5278
5613
  this.clearBufferTimeout();
5279
5614
  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');
5615
+ this.sessionLogger.debug("Buffer timeout reached, processing pending segments");
5616
+ if (this.processingPhase === "transcribing") {
5617
+ this.sessionLogger.debug({ segmentCount: this.pendingAudioSegments.length }, "Buffer timeout deferred because transcription is still in progress");
5283
5618
  this.setBufferTimeout();
5284
5619
  return;
5285
5620
  }
@@ -5305,15 +5640,15 @@ export class Session {
5305
5640
  * Emit a message to the client
5306
5641
  */
5307
5642
  emit(msg) {
5308
- if (msg.type === 'audio_output' &&
5643
+ if (msg.type === "audio_output" &&
5309
5644
  (process.env.TTS_DEBUG_AUDIO_DIR || isPaseoDictationDebugEnabled()) &&
5310
5645
  msg.payload.groupId &&
5311
- typeof msg.payload.audio === 'string') {
5646
+ typeof msg.payload.audio === "string") {
5312
5647
  const groupId = msg.payload.groupId;
5313
5648
  const existing = this.ttsDebugStreams.get(groupId) ??
5314
5649
  { format: msg.payload.format, chunks: [] };
5315
5650
  try {
5316
- existing.chunks.push(Buffer.from(msg.payload.audio, 'base64'));
5651
+ existing.chunks.push(Buffer.from(msg.payload.audio, "base64"));
5317
5652
  existing.format = msg.payload.format;
5318
5653
  this.ttsDebugStreams.set(groupId, existing);
5319
5654
  }
@@ -5328,11 +5663,11 @@ export class Session {
5328
5663
  const recordingPath = await maybePersistTtsDebugAudio(Buffer.concat(final.chunks), { sessionId: this.sessionId, groupId, format: final.format }, this.sessionLogger);
5329
5664
  if (recordingPath) {
5330
5665
  this.onMessage({
5331
- type: 'activity_log',
5666
+ type: "activity_log",
5332
5667
  payload: {
5333
5668
  id: uuidv4(),
5334
5669
  timestamp: new Date(),
5335
- type: 'system',
5670
+ type: "system",
5336
5671
  content: `Saved TTS audio: ${recordingPath}`,
5337
5672
  metadata: { recordingPath, format: final.format, groupId },
5338
5673
  },
@@ -5352,14 +5687,14 @@ export class Session {
5352
5687
  this.onBinaryMessage(frame);
5353
5688
  }
5354
5689
  catch (error) {
5355
- this.sessionLogger.error({ err: error }, 'Failed to emit binary frame');
5690
+ this.sessionLogger.error({ err: error }, "Failed to emit binary frame");
5356
5691
  }
5357
5692
  }
5358
5693
  /**
5359
5694
  * Clean up session resources
5360
5695
  */
5361
5696
  async cleanup() {
5362
- this.sessionLogger.trace('Cleaning up');
5697
+ this.sessionLogger.trace("Cleaning up");
5363
5698
  if (this.unsubscribeAgentEvents) {
5364
5699
  this.unsubscribeAgentEvents();
5365
5700
  this.unsubscribeAgentEvents = null;
@@ -5382,7 +5717,7 @@ export class Session {
5382
5717
  await this.agentMcpClient.close();
5383
5718
  }
5384
5719
  catch (error) {
5385
- this.sessionLogger.error({ err: error }, 'Failed to close Agent MCP client');
5720
+ this.sessionLogger.error({ err: error }, "Failed to close Agent MCP client");
5386
5721
  }
5387
5722
  this.agentMcpClient = null;
5388
5723
  this.agentTools = null;
@@ -5395,20 +5730,20 @@ export class Session {
5395
5730
  this.unsubscribeTerminalsChanged = null;
5396
5731
  }
5397
5732
  this.subscribedTerminalDirectories.clear();
5398
- for (const unsubscribe of this.terminalSubscriptions.values()) {
5399
- unsubscribe();
5400
- }
5401
- this.terminalSubscriptions.clear();
5402
5733
  for (const unsubscribeExit of this.terminalExitSubscriptions.values()) {
5403
5734
  unsubscribeExit();
5404
5735
  }
5405
5736
  this.terminalExitSubscriptions.clear();
5406
- this.detachAllTerminalStreams({ emitExit: false });
5737
+ this.disposeTerminalSubscriptions();
5407
5738
  for (const target of this.checkoutDiffTargets.values()) {
5408
5739
  this.closeCheckoutDiffWatchTarget(target);
5409
5740
  }
5410
5741
  this.checkoutDiffTargets.clear();
5411
5742
  this.checkoutDiffSubscriptions.clear();
5743
+ for (const target of this.workspaceGitWatchTargets.values()) {
5744
+ this.closeWorkspaceGitWatchTarget(target);
5745
+ }
5746
+ this.workspaceGitWatchTargets.clear();
5412
5747
  }
5413
5748
  // ============================================================================
5414
5749
  // Terminal Handlers
@@ -5428,24 +5763,11 @@ export class Session {
5428
5763
  unsubscribeExit();
5429
5764
  this.terminalExitSubscriptions.delete(terminalId);
5430
5765
  }
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
- }
5766
+ this.detachTerminalStream(terminalId, { emitExit: true });
5445
5767
  }
5446
5768
  emitTerminalsChangedSnapshot(input) {
5447
5769
  this.emit({
5448
- type: 'terminals_changed',
5770
+ type: "terminals_changed",
5449
5771
  payload: {
5450
5772
  cwd: input.cwd,
5451
5773
  terminals: input.terminals,
@@ -5492,13 +5814,13 @@ export class Session {
5492
5814
  });
5493
5815
  }
5494
5816
  catch (error) {
5495
- this.sessionLogger.warn({ err: error, cwd }, 'Failed to emit initial terminal snapshot');
5817
+ this.sessionLogger.warn({ err: error, cwd }, "Failed to emit initial terminal snapshot");
5496
5818
  }
5497
5819
  }
5498
5820
  async handleListTerminalsRequest(msg) {
5499
5821
  if (!this.terminalManager) {
5500
5822
  this.emit({
5501
- type: 'list_terminals_response',
5823
+ type: "list_terminals_response",
5502
5824
  payload: {
5503
5825
  cwd: msg.cwd,
5504
5826
  terminals: [],
@@ -5513,7 +5835,7 @@ export class Session {
5513
5835
  this.ensureTerminalExitSubscription(terminal);
5514
5836
  }
5515
5837
  this.emit({
5516
- type: 'list_terminals_response',
5838
+ type: "list_terminals_response",
5517
5839
  payload: {
5518
5840
  cwd: msg.cwd,
5519
5841
  terminals: terminals.map((t) => ({ id: t.id, name: t.name })),
@@ -5522,9 +5844,9 @@ export class Session {
5522
5844
  });
5523
5845
  }
5524
5846
  catch (error) {
5525
- this.sessionLogger.error({ err: error, cwd: msg.cwd }, 'Failed to list terminals');
5847
+ this.sessionLogger.error({ err: error, cwd: msg.cwd }, "Failed to list terminals");
5526
5848
  this.emit({
5527
- type: 'list_terminals_response',
5849
+ type: "list_terminals_response",
5528
5850
  payload: {
5529
5851
  cwd: msg.cwd,
5530
5852
  terminals: [],
@@ -5536,10 +5858,10 @@ export class Session {
5536
5858
  async handleCreateTerminalRequest(msg) {
5537
5859
  if (!this.terminalManager) {
5538
5860
  this.emit({
5539
- type: 'create_terminal_response',
5861
+ type: "create_terminal_response",
5540
5862
  payload: {
5541
5863
  terminal: null,
5542
- error: 'Terminal manager not available',
5864
+ error: "Terminal manager not available",
5543
5865
  requestId: msg.requestId,
5544
5866
  },
5545
5867
  });
@@ -5552,7 +5874,7 @@ export class Session {
5552
5874
  });
5553
5875
  this.ensureTerminalExitSubscription(session);
5554
5876
  this.emit({
5555
- type: 'create_terminal_response',
5877
+ type: "create_terminal_response",
5556
5878
  payload: {
5557
5879
  terminal: { id: session.id, name: session.name, cwd: session.cwd },
5558
5880
  error: null,
@@ -5561,9 +5883,9 @@ export class Session {
5561
5883
  });
5562
5884
  }
5563
5885
  catch (error) {
5564
- this.sessionLogger.error({ err: error, cwd: msg.cwd }, 'Failed to create terminal');
5886
+ this.sessionLogger.error({ err: error, cwd: msg.cwd }, "Failed to create terminal");
5565
5887
  this.emit({
5566
- type: 'create_terminal_response',
5888
+ type: "create_terminal_response",
5567
5889
  payload: {
5568
5890
  terminal: null,
5569
5891
  error: error.message,
@@ -5575,11 +5897,10 @@ export class Session {
5575
5897
  async handleSubscribeTerminalRequest(msg) {
5576
5898
  if (!this.terminalManager) {
5577
5899
  this.emit({
5578
- type: 'subscribe_terminal_response',
5900
+ type: "subscribe_terminal_response",
5579
5901
  payload: {
5580
5902
  terminalId: msg.terminalId,
5581
- state: null,
5582
- error: 'Terminal manager not available',
5903
+ error: "Terminal manager not available",
5583
5904
  requestId: msg.requestId,
5584
5905
  },
5585
5906
  });
@@ -5588,52 +5909,44 @@ export class Session {
5588
5909
  const session = this.terminalManager.getTerminal(msg.terminalId);
5589
5910
  if (!session) {
5590
5911
  this.emit({
5591
- type: 'subscribe_terminal_response',
5912
+ type: "subscribe_terminal_response",
5592
5913
  payload: {
5593
5914
  terminalId: msg.terminalId,
5594
- state: null,
5595
- error: 'Terminal not found',
5915
+ error: "Terminal not found",
5596
5916
  requestId: msg.requestId,
5597
5917
  },
5598
5918
  });
5599
5919
  return;
5600
5920
  }
5601
5921
  this.ensureTerminalExitSubscription(session);
5602
- // Unsubscribe from previous subscription if any
5603
- const existing = this.terminalSubscriptions.get(msg.terminalId);
5604
- if (existing) {
5605
- existing();
5922
+ const slot = this.bindActiveTerminalStream(session);
5923
+ if (slot === null) {
5924
+ this.sessionLogger.warn({
5925
+ terminalId: msg.terminalId,
5926
+ activeTerminalStreamCount: this.activeTerminalStreams.size,
5927
+ }, "Terminal stream slot exhaustion");
5928
+ this.emit({
5929
+ type: "subscribe_terminal_response",
5930
+ payload: {
5931
+ terminalId: msg.terminalId,
5932
+ error: "No terminal stream slots available",
5933
+ requestId: msg.requestId,
5934
+ },
5935
+ });
5936
+ return;
5606
5937
  }
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
5938
  this.emit({
5622
- type: 'subscribe_terminal_response',
5939
+ type: "subscribe_terminal_response",
5623
5940
  payload: {
5624
5941
  terminalId: msg.terminalId,
5625
- state: session.getState(),
5942
+ slot,
5626
5943
  error: null,
5627
5944
  requestId: msg.requestId,
5628
5945
  },
5629
5946
  });
5630
5947
  }
5631
5948
  handleUnsubscribeTerminalRequest(msg) {
5632
- const unsubscribe = this.terminalSubscriptions.get(msg.terminalId);
5633
- if (unsubscribe) {
5634
- unsubscribe();
5635
- this.terminalSubscriptions.delete(msg.terminalId);
5636
- }
5949
+ this.detachTerminalStream(msg.terminalId, { emitExit: false });
5637
5950
  }
5638
5951
  handleTerminalInput(msg) {
5639
5952
  if (!this.terminalManager) {
@@ -5641,22 +5954,20 @@ export class Session {
5641
5954
  }
5642
5955
  const session = this.terminalManager.getTerminal(msg.terminalId);
5643
5956
  if (!session) {
5644
- this.sessionLogger.warn({ terminalId: msg.terminalId }, 'Terminal not found for input');
5957
+ this.sessionLogger.warn({ terminalId: msg.terminalId }, "Terminal not found for input");
5645
5958
  return;
5646
5959
  }
5647
5960
  this.ensureTerminalExitSubscription(session);
5961
+ if (msg.message.type === "resize") {
5962
+ const currentSize = session.getSize();
5963
+ if (currentSize.rows === msg.message.rows && currentSize.cols === msg.message.cols) {
5964
+ return;
5965
+ }
5966
+ }
5648
5967
  session.send(msg.message);
5649
5968
  }
5650
5969
  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
- }
5970
+ this.detachTerminalStream(terminalId, { emitExit: options?.emitExit ?? true });
5660
5971
  this.terminalManager?.killTerminal(terminalId);
5661
5972
  }
5662
5973
  async killTerminalsUnderPath(rootPath) {
@@ -5678,18 +5989,18 @@ export class Session {
5678
5989
  catch (error) {
5679
5990
  const message = error instanceof Error ? error.message : String(error);
5680
5991
  cleanupErrors.push({ cwd: terminalCwd, message });
5681
- this.sessionLogger.warn({ err: error, cwd: terminalCwd }, 'Failed to clean up worktree terminals during archive');
5992
+ this.sessionLogger.warn({ err: error, cwd: terminalCwd }, "Failed to clean up worktree terminals during archive");
5682
5993
  }
5683
5994
  }
5684
5995
  if (cleanupErrors.length > 0) {
5685
- const details = cleanupErrors.map((entry) => `${entry.cwd}: ${entry.message}`).join('; ');
5996
+ const details = cleanupErrors.map((entry) => `${entry.cwd}: ${entry.message}`).join("; ");
5686
5997
  throw new Error(`Failed to clean up worktree terminals during archive (${details})`);
5687
5998
  }
5688
5999
  }
5689
6000
  async handleKillTerminalRequest(msg) {
5690
6001
  if (!this.terminalManager) {
5691
6002
  this.emit({
5692
- type: 'kill_terminal_response',
6003
+ type: "kill_terminal_response",
5693
6004
  payload: {
5694
6005
  terminalId: msg.terminalId,
5695
6006
  success: false,
@@ -5700,7 +6011,7 @@ export class Session {
5700
6011
  }
5701
6012
  this.killTrackedTerminal(msg.terminalId, { emitExit: true });
5702
6013
  this.emit({
5703
- type: 'kill_terminal_response',
6014
+ type: "kill_terminal_response",
5704
6015
  payload: {
5705
6016
  terminalId: msg.terminalId,
5706
6017
  success: true,
@@ -5708,219 +6019,150 @@ export class Session {
5708
6019
  },
5709
6020
  });
5710
6021
  }
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;
6022
+ bindActiveTerminalStream(terminal) {
6023
+ if (!this.onBinaryMessage) {
6024
+ return null;
5727
6025
  }
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;
6026
+ const existingSlot = this.terminalIdToSlot.get(terminal.id);
6027
+ if (typeof existingSlot === "number") {
6028
+ const existingStream = this.activeTerminalStreams.get(existingSlot);
6029
+ if (existingStream) {
6030
+ existingStream.needsSnapshot = true;
6031
+ this.trySendTerminalSnapshot(existingStream);
6032
+ return existingSlot;
6033
+ }
6034
+ this.terminalIdToSlot.delete(terminal.id);
6035
+ }
6036
+ const slot = this.allocateTerminalSlot();
6037
+ if (slot === null) {
6038
+ return null;
5744
6039
  }
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,
6040
+ const activeStream = {
6041
+ terminalId: terminal.id,
6042
+ slot,
5767
6043
  unsubscribe: () => { },
5768
- lastOutputOffset: initialOffset,
5769
- lastAckOffset: initialOffset,
5770
- pendingChunks: [],
5771
- pendingBytes: 0,
6044
+ needsSnapshot: true,
6045
+ snapshotRetryTimer: null,
5772
6046
  };
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 });
6047
+ this.activeTerminalStreams.set(slot, activeStream);
6048
+ this.terminalIdToSlot.set(terminal.id, slot);
6049
+ activeStream.unsubscribe = terminal.subscribe((message) => {
6050
+ if (this.activeTerminalStreams.get(slot) !== activeStream) {
6051
+ return;
6052
+ }
6053
+ if (message.type === "snapshot") {
6054
+ this.trySendTerminalSnapshot(activeStream);
6055
+ return;
6056
+ }
6057
+ if (activeStream.needsSnapshot || message.data.length === 0) {
6058
+ return;
6059
+ }
6060
+ if (this.getCurrentBinaryBufferedAmount() >= TERMINAL_STREAM_HIGH_WATER_BYTES) {
6061
+ this.markAllActiveTerminalStreamsForSnapshot();
5845
6062
  return;
5846
6063
  }
5847
- binding.pendingChunks.push(chunk);
5848
- binding.pendingBytes += chunkBytes;
6064
+ this.emitBinary(encodeTerminalStreamFrame({
6065
+ opcode: TerminalStreamOpcode.Output,
6066
+ slot,
6067
+ payload: new Uint8Array(Buffer.from(message.data, "utf8")),
6068
+ }));
6069
+ if (this.getCurrentBinaryBufferedAmount() >= TERMINAL_STREAM_HIGH_WATER_BYTES) {
6070
+ this.markAllActiveTerminalStreamsForSnapshot();
6071
+ }
6072
+ });
6073
+ this.trySendTerminalSnapshot(activeStream);
6074
+ return slot;
6075
+ }
6076
+ trySendTerminalSnapshot(activeStream) {
6077
+ if (this.activeTerminalStreams.get(activeStream.slot) !== activeStream ||
6078
+ !activeStream.needsSnapshot) {
5849
6079
  return;
5850
6080
  }
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;
6081
+ if (this.getCurrentBinaryBufferedAmount() > TERMINAL_STREAM_LOW_WATER_BYTES) {
6082
+ if (!activeStream.snapshotRetryTimer) {
6083
+ activeStream.snapshotRetryTimer = setTimeout(() => {
6084
+ activeStream.snapshotRetryTimer = null;
6085
+ this.trySendTerminalSnapshot(activeStream);
6086
+ }, 33);
5858
6087
  }
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);
6088
+ return;
5865
6089
  }
6090
+ if (activeStream.snapshotRetryTimer) {
6091
+ clearTimeout(activeStream.snapshotRetryTimer);
6092
+ activeStream.snapshotRetryTimer = null;
6093
+ }
6094
+ const terminal = this.terminalManager?.getTerminal(activeStream.terminalId);
6095
+ if (!terminal) {
6096
+ this.detachTerminalStream(activeStream.terminalId, { emitExit: true });
6097
+ return;
6098
+ }
6099
+ activeStream.needsSnapshot = false;
6100
+ this.emitBinary(encodeTerminalStreamFrame({
6101
+ opcode: TerminalStreamOpcode.Snapshot,
6102
+ slot: activeStream.slot,
6103
+ payload: encodeTerminalSnapshotPayload(terminal.getState()),
6104
+ }));
5866
6105
  }
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
- });
6106
+ markAllActiveTerminalStreamsForSnapshot() {
6107
+ for (const activeStream of this.activeTerminalStreams.values()) {
6108
+ activeStream.needsSnapshot = true;
6109
+ this.trySendTerminalSnapshot(activeStream);
6110
+ }
5877
6111
  }
5878
- detachAllTerminalStreams(options) {
5879
- for (const streamId of Array.from(this.terminalStreams.keys())) {
5880
- this.detachTerminalStream(streamId, options);
6112
+ allocateTerminalSlot() {
6113
+ for (let attempt = 0; attempt < MAX_TERMINAL_STREAM_SLOTS; attempt += 1) {
6114
+ const slot = (this.nextTerminalSlot + attempt) % MAX_TERMINAL_STREAM_SLOTS;
6115
+ if (this.activeTerminalStreams.has(slot)) {
6116
+ continue;
6117
+ }
6118
+ this.nextTerminalSlot = (slot + 1) % MAX_TERMINAL_STREAM_SLOTS;
6119
+ return slot;
5881
6120
  }
6121
+ return null;
5882
6122
  }
5883
- detachTerminalStream(streamId, options) {
5884
- const binding = this.terminalStreams.get(streamId);
5885
- if (!binding) {
6123
+ detachTerminalStream(terminalId, options) {
6124
+ const slot = this.terminalIdToSlot.get(terminalId);
6125
+ if (typeof slot !== "number") {
5886
6126
  return false;
5887
6127
  }
6128
+ const activeStream = this.activeTerminalStreams.get(slot);
6129
+ if (!activeStream) {
6130
+ this.terminalIdToSlot.delete(terminalId);
6131
+ return false;
6132
+ }
6133
+ this.activeTerminalStreams.delete(slot);
6134
+ this.terminalIdToSlot.delete(terminalId);
6135
+ if (activeStream.snapshotRetryTimer) {
6136
+ clearTimeout(activeStream.snapshotRetryTimer);
6137
+ activeStream.snapshotRetryTimer = null;
6138
+ }
5888
6139
  try {
5889
- binding.unsubscribe();
6140
+ activeStream.unsubscribe();
5890
6141
  }
5891
6142
  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);
6143
+ this.sessionLogger.warn({ err: error }, "Failed to unsubscribe terminal stream");
5897
6144
  }
5898
6145
  if (options?.emitExit) {
5899
6146
  this.emit({
5900
- type: 'terminal_stream_exit',
6147
+ type: "terminal_stream_exit",
5901
6148
  payload: {
5902
- streamId,
5903
- terminalId: binding.terminalId,
6149
+ terminalId: activeStream.terminalId,
5904
6150
  },
5905
6151
  });
5906
6152
  }
5907
6153
  return true;
5908
6154
  }
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;
6155
+ disposeTerminalSubscriptions() {
6156
+ for (const terminalId of [...this.terminalIdToSlot.keys()]) {
6157
+ this.detachTerminalStream(terminalId, { emitExit: false });
6158
+ }
6159
+ }
6160
+ getCurrentBinaryBufferedAmount() {
6161
+ const bufferedAmount = this.getBinaryBufferedAmount?.() ?? 0;
6162
+ if (!Number.isFinite(bufferedAmount) || bufferedAmount < 0) {
6163
+ return 0;
5922
6164
  }
5923
- throw new Error('Unable to allocate terminal stream id');
6165
+ return Math.floor(bufferedAmount);
5924
6166
  }
5925
6167
  }
5926
6168
  //# sourceMappingURL=session.js.map