@junctionpanel/server 0.1.16

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 (400) hide show
  1. package/.env.example +10 -0
  2. package/LICENSE +671 -0
  3. package/README.md +118 -0
  4. package/agent-prompt.md +339 -0
  5. package/dist/scripts/daemon-runner.js +141 -0
  6. package/dist/scripts/daemon-runner.js.map +1 -0
  7. package/dist/scripts/dev-runner.js +17 -0
  8. package/dist/scripts/dev-runner.js.map +1 -0
  9. package/dist/scripts/mcp-stdio-socket-bridge-cli.mjs +62 -0
  10. package/dist/scripts/supervisor.js +122 -0
  11. package/dist/scripts/supervisor.js.map +1 -0
  12. package/dist/server/client/daemon-client-relay-e2ee-transport.d.ts +8 -0
  13. package/dist/server/client/daemon-client-relay-e2ee-transport.d.ts.map +1 -0
  14. package/dist/server/client/daemon-client-relay-e2ee-transport.js +161 -0
  15. package/dist/server/client/daemon-client-relay-e2ee-transport.js.map +1 -0
  16. package/dist/server/client/daemon-client-terminal-stream-manager.d.ts +43 -0
  17. package/dist/server/client/daemon-client-terminal-stream-manager.d.ts.map +1 -0
  18. package/dist/server/client/daemon-client-terminal-stream-manager.js +134 -0
  19. package/dist/server/client/daemon-client-terminal-stream-manager.js.map +1 -0
  20. package/dist/server/client/daemon-client-transport-types.d.ts +34 -0
  21. package/dist/server/client/daemon-client-transport-types.d.ts.map +1 -0
  22. package/dist/server/client/daemon-client-transport-types.js +2 -0
  23. package/dist/server/client/daemon-client-transport-types.js.map +1 -0
  24. package/dist/server/client/daemon-client-transport-utils.d.ts +9 -0
  25. package/dist/server/client/daemon-client-transport-utils.d.ts.map +1 -0
  26. package/dist/server/client/daemon-client-transport-utils.js +121 -0
  27. package/dist/server/client/daemon-client-transport-utils.js.map +1 -0
  28. package/dist/server/client/daemon-client-transport.d.ts +5 -0
  29. package/dist/server/client/daemon-client-transport.d.ts.map +1 -0
  30. package/dist/server/client/daemon-client-transport.js +4 -0
  31. package/dist/server/client/daemon-client-transport.js.map +1 -0
  32. package/dist/server/client/daemon-client-websocket-transport.d.ts +7 -0
  33. package/dist/server/client/daemon-client-websocket-transport.d.ts.map +1 -0
  34. package/dist/server/client/daemon-client-websocket-transport.js +64 -0
  35. package/dist/server/client/daemon-client-websocket-transport.js.map +1 -0
  36. package/dist/server/client/daemon-client.d.ts +443 -0
  37. package/dist/server/client/daemon-client.d.ts.map +1 -0
  38. package/dist/server/client/daemon-client.js +2223 -0
  39. package/dist/server/client/daemon-client.js.map +1 -0
  40. package/dist/server/server/agent/activity-curator.d.ts +8 -0
  41. package/dist/server/server/agent/activity-curator.d.ts.map +1 -0
  42. package/dist/server/server/agent/activity-curator.js +228 -0
  43. package/dist/server/server/agent/activity-curator.js.map +1 -0
  44. package/dist/server/server/agent/agent-management-mcp.d.ts +36 -0
  45. package/dist/server/server/agent/agent-management-mcp.d.ts.map +1 -0
  46. package/dist/server/server/agent/agent-management-mcp.js +644 -0
  47. package/dist/server/server/agent/agent-management-mcp.js.map +1 -0
  48. package/dist/server/server/agent/agent-manager.d.ts +252 -0
  49. package/dist/server/server/agent/agent-manager.d.ts.map +1 -0
  50. package/dist/server/server/agent/agent-manager.js +1651 -0
  51. package/dist/server/server/agent/agent-manager.js.map +1 -0
  52. package/dist/server/server/agent/agent-metadata-generator.d.ts +29 -0
  53. package/dist/server/server/agent/agent-metadata-generator.d.ts.map +1 -0
  54. package/dist/server/server/agent/agent-metadata-generator.js +163 -0
  55. package/dist/server/server/agent/agent-metadata-generator.js.map +1 -0
  56. package/dist/server/server/agent/agent-projections.d.ts +17 -0
  57. package/dist/server/server/agent/agent-projections.d.ts.map +1 -0
  58. package/dist/server/server/agent/agent-projections.js +270 -0
  59. package/dist/server/server/agent/agent-projections.js.map +1 -0
  60. package/dist/server/server/agent/agent-response-loop.d.ts +60 -0
  61. package/dist/server/server/agent/agent-response-loop.d.ts.map +1 -0
  62. package/dist/server/server/agent/agent-response-loop.js +304 -0
  63. package/dist/server/server/agent/agent-response-loop.js.map +1 -0
  64. package/dist/server/server/agent/agent-sdk-types.d.ts +377 -0
  65. package/dist/server/server/agent/agent-sdk-types.d.ts.map +1 -0
  66. package/dist/server/server/agent/agent-sdk-types.js +12 -0
  67. package/dist/server/server/agent/agent-sdk-types.js.map +1 -0
  68. package/dist/server/server/agent/agent-storage.d.ts +230 -0
  69. package/dist/server/server/agent/agent-storage.d.ts.map +1 -0
  70. package/dist/server/server/agent/agent-storage.js +346 -0
  71. package/dist/server/server/agent/agent-storage.js.map +1 -0
  72. package/dist/server/server/agent/agent-title-limits.d.ts +3 -0
  73. package/dist/server/server/agent/agent-title-limits.d.ts.map +1 -0
  74. package/dist/server/server/agent/agent-title-limits.js +3 -0
  75. package/dist/server/server/agent/agent-title-limits.js.map +1 -0
  76. package/dist/server/server/agent/mcp-server.d.ts +19 -0
  77. package/dist/server/server/agent/mcp-server.d.ts.map +1 -0
  78. package/dist/server/server/agent/mcp-server.js +742 -0
  79. package/dist/server/server/agent/mcp-server.js.map +1 -0
  80. package/dist/server/server/agent/model-resolver.d.ts +11 -0
  81. package/dist/server/server/agent/model-resolver.d.ts.map +1 -0
  82. package/dist/server/server/agent/model-resolver.js +21 -0
  83. package/dist/server/server/agent/model-resolver.js.map +1 -0
  84. package/dist/server/server/agent/orchestrator-instructions.d.ts +7 -0
  85. package/dist/server/server/agent/orchestrator-instructions.d.ts.map +1 -0
  86. package/dist/server/server/agent/orchestrator-instructions.js +51 -0
  87. package/dist/server/server/agent/orchestrator-instructions.js.map +1 -0
  88. package/dist/server/server/agent/orchestrator.d.ts +12 -0
  89. package/dist/server/server/agent/orchestrator.d.ts.map +1 -0
  90. package/dist/server/server/agent/orchestrator.js +12 -0
  91. package/dist/server/server/agent/orchestrator.js.map +1 -0
  92. package/dist/server/server/agent/pcm16-resampler.d.ts +14 -0
  93. package/dist/server/server/agent/pcm16-resampler.d.ts.map +1 -0
  94. package/dist/server/server/agent/pcm16-resampler.js +63 -0
  95. package/dist/server/server/agent/pcm16-resampler.js.map +1 -0
  96. package/dist/server/server/agent/provider-launch-config.d.ts +139 -0
  97. package/dist/server/server/agent/provider-launch-config.d.ts.map +1 -0
  98. package/dist/server/server/agent/provider-launch-config.js +83 -0
  99. package/dist/server/server/agent/provider-launch-config.js.map +1 -0
  100. package/dist/server/server/agent/provider-manifest.d.ts +15 -0
  101. package/dist/server/server/agent/provider-manifest.d.ts.map +1 -0
  102. package/dist/server/server/agent/provider-manifest.js +83 -0
  103. package/dist/server/server/agent/provider-manifest.js.map +1 -0
  104. package/dist/server/server/agent/provider-registry.d.ts +18 -0
  105. package/dist/server/server/agent/provider-registry.d.ts.map +1 -0
  106. package/dist/server/server/agent/provider-registry.js +45 -0
  107. package/dist/server/server/agent/provider-registry.js.map +1 -0
  108. package/dist/server/server/agent/providers/claude/model-catalog.d.ts +29 -0
  109. package/dist/server/server/agent/providers/claude/model-catalog.d.ts.map +1 -0
  110. package/dist/server/server/agent/providers/claude/model-catalog.js +64 -0
  111. package/dist/server/server/agent/providers/claude/model-catalog.js.map +1 -0
  112. package/dist/server/server/agent/providers/claude/task-notification-tool-call.d.ts +44 -0
  113. package/dist/server/server/agent/providers/claude/task-notification-tool-call.d.ts.map +1 -0
  114. package/dist/server/server/agent/providers/claude/task-notification-tool-call.js +250 -0
  115. package/dist/server/server/agent/providers/claude/task-notification-tool-call.js.map +1 -0
  116. package/dist/server/server/agent/providers/claude/tool-call-detail-parser.d.ts +3 -0
  117. package/dist/server/server/agent/providers/claude/tool-call-detail-parser.d.ts.map +1 -0
  118. package/dist/server/server/agent/providers/claude/tool-call-detail-parser.js +109 -0
  119. package/dist/server/server/agent/providers/claude/tool-call-detail-parser.js.map +1 -0
  120. package/dist/server/server/agent/providers/claude/tool-call-mapper.d.ts +16 -0
  121. package/dist/server/server/agent/providers/claude/tool-call-mapper.d.ts.map +1 -0
  122. package/dist/server/server/agent/providers/claude/tool-call-mapper.js +238 -0
  123. package/dist/server/server/agent/providers/claude/tool-call-mapper.js.map +1 -0
  124. package/dist/server/server/agent/providers/claude-agent.d.ts +49 -0
  125. package/dist/server/server/agent/providers/claude-agent.d.ts.map +1 -0
  126. package/dist/server/server/agent/providers/claude-agent.js +3701 -0
  127. package/dist/server/server/agent/providers/claude-agent.js.map +1 -0
  128. package/dist/server/server/agent/providers/codex/tool-call-detail-parser.d.ts +12 -0
  129. package/dist/server/server/agent/providers/codex/tool-call-detail-parser.d.ts.map +1 -0
  130. package/dist/server/server/agent/providers/codex/tool-call-detail-parser.js +104 -0
  131. package/dist/server/server/agent/providers/codex/tool-call-detail-parser.js.map +1 -0
  132. package/dist/server/server/agent/providers/codex/tool-call-mapper.d.ts +15 -0
  133. package/dist/server/server/agent/providers/codex/tool-call-mapper.d.ts.map +1 -0
  134. package/dist/server/server/agent/providers/codex/tool-call-mapper.js +720 -0
  135. package/dist/server/server/agent/providers/codex/tool-call-mapper.js.map +1 -0
  136. package/dist/server/server/agent/providers/codex-app-server-agent.d.ts +34 -0
  137. package/dist/server/server/agent/providers/codex-app-server-agent.d.ts.map +1 -0
  138. package/dist/server/server/agent/providers/codex-app-server-agent.js +2660 -0
  139. package/dist/server/server/agent/providers/codex-app-server-agent.js.map +1 -0
  140. package/dist/server/server/agent/providers/codex-rollout-timeline.d.ts +9 -0
  141. package/dist/server/server/agent/providers/codex-rollout-timeline.d.ts.map +1 -0
  142. package/dist/server/server/agent/providers/codex-rollout-timeline.js +487 -0
  143. package/dist/server/server/agent/providers/codex-rollout-timeline.js.map +1 -0
  144. package/dist/server/server/agent/providers/opencode/tool-call-detail-parser.d.ts +3 -0
  145. package/dist/server/server/agent/providers/opencode/tool-call-detail-parser.d.ts.map +1 -0
  146. package/dist/server/server/agent/providers/opencode/tool-call-detail-parser.js +39 -0
  147. package/dist/server/server/agent/providers/opencode/tool-call-detail-parser.js.map +1 -0
  148. package/dist/server/server/agent/providers/opencode/tool-call-mapper.d.ts +13 -0
  149. package/dist/server/server/agent/providers/opencode/tool-call-mapper.d.ts.map +1 -0
  150. package/dist/server/server/agent/providers/opencode/tool-call-mapper.js +151 -0
  151. package/dist/server/server/agent/providers/opencode/tool-call-mapper.js.map +1 -0
  152. package/dist/server/server/agent/providers/opencode-agent.d.ts +37 -0
  153. package/dist/server/server/agent/providers/opencode-agent.d.ts.map +1 -0
  154. package/dist/server/server/agent/providers/opencode-agent.js +874 -0
  155. package/dist/server/server/agent/providers/opencode-agent.js.map +1 -0
  156. package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts +1460 -0
  157. package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts.map +1 -0
  158. package/dist/server/server/agent/providers/tool-call-detail-primitives.js +552 -0
  159. package/dist/server/server/agent/providers/tool-call-detail-primitives.js.map +1 -0
  160. package/dist/server/server/agent/providers/tool-call-mapper-utils.d.ts +17 -0
  161. package/dist/server/server/agent/providers/tool-call-mapper-utils.d.ts.map +1 -0
  162. package/dist/server/server/agent/providers/tool-call-mapper-utils.js +109 -0
  163. package/dist/server/server/agent/providers/tool-call-mapper-utils.js.map +1 -0
  164. package/dist/server/server/agent/system-prompt.d.ts +3 -0
  165. package/dist/server/server/agent/system-prompt.d.ts.map +1 -0
  166. package/dist/server/server/agent/system-prompt.js +19 -0
  167. package/dist/server/server/agent/system-prompt.js.map +1 -0
  168. package/dist/server/server/agent/timeline-append.d.ts +10 -0
  169. package/dist/server/server/agent/timeline-append.d.ts.map +1 -0
  170. package/dist/server/server/agent/timeline-append.js +27 -0
  171. package/dist/server/server/agent/timeline-append.js.map +1 -0
  172. package/dist/server/server/agent/timeline-projection.d.ts +39 -0
  173. package/dist/server/server/agent/timeline-projection.d.ts.map +1 -0
  174. package/dist/server/server/agent/timeline-projection.js +215 -0
  175. package/dist/server/server/agent/timeline-projection.js.map +1 -0
  176. package/dist/server/server/agent/tool-name-normalization.d.ts +7 -0
  177. package/dist/server/server/agent/tool-name-normalization.d.ts.map +1 -0
  178. package/dist/server/server/agent/tool-name-normalization.js +45 -0
  179. package/dist/server/server/agent/tool-name-normalization.js.map +1 -0
  180. package/dist/server/server/agent/wait-for-agent-tracker.d.ts +15 -0
  181. package/dist/server/server/agent/wait-for-agent-tracker.d.ts.map +1 -0
  182. package/dist/server/server/agent/wait-for-agent-tracker.js +53 -0
  183. package/dist/server/server/agent/wait-for-agent-tracker.js.map +1 -0
  184. package/dist/server/server/agent-attention-policy.d.ts +20 -0
  185. package/dist/server/server/agent-attention-policy.d.ts.map +1 -0
  186. package/dist/server/server/agent-attention-policy.js +40 -0
  187. package/dist/server/server/agent-attention-policy.js.map +1 -0
  188. package/dist/server/server/allowed-hosts.d.ts +13 -0
  189. package/dist/server/server/allowed-hosts.d.ts.map +1 -0
  190. package/dist/server/server/allowed-hosts.js +94 -0
  191. package/dist/server/server/allowed-hosts.js.map +1 -0
  192. package/dist/server/server/bootstrap.d.ts +49 -0
  193. package/dist/server/server/bootstrap.d.ts.map +1 -0
  194. package/dist/server/server/bootstrap.js +422 -0
  195. package/dist/server/server/bootstrap.js.map +1 -0
  196. package/dist/server/server/client-message-id.d.ts +3 -0
  197. package/dist/server/server/client-message-id.d.ts.map +1 -0
  198. package/dist/server/server/client-message-id.js +12 -0
  199. package/dist/server/server/client-message-id.js.map +1 -0
  200. package/dist/server/server/config.d.ts +13 -0
  201. package/dist/server/server/config.d.ts.map +1 -0
  202. package/dist/server/server/config.js +58 -0
  203. package/dist/server/server/config.js.map +1 -0
  204. package/dist/server/server/connection-offer.d.ts +19 -0
  205. package/dist/server/server/connection-offer.d.ts.map +1 -0
  206. package/dist/server/server/connection-offer.js +60 -0
  207. package/dist/server/server/connection-offer.js.map +1 -0
  208. package/dist/server/server/daemon-keypair.d.ts +8 -0
  209. package/dist/server/server/daemon-keypair.d.ts.map +1 -0
  210. package/dist/server/server/daemon-keypair.js +40 -0
  211. package/dist/server/server/daemon-keypair.js.map +1 -0
  212. package/dist/server/server/daemon-version.d.ts +5 -0
  213. package/dist/server/server/daemon-version.d.ts.map +1 -0
  214. package/dist/server/server/daemon-version.js +22 -0
  215. package/dist/server/server/daemon-version.js.map +1 -0
  216. package/dist/server/server/exports.d.ts +16 -0
  217. package/dist/server/server/exports.d.ts.map +1 -0
  218. package/dist/server/server/exports.js +16 -0
  219. package/dist/server/server/exports.js.map +1 -0
  220. package/dist/server/server/file-download/token-store.d.ts +25 -0
  221. package/dist/server/server/file-download/token-store.d.ts.map +1 -0
  222. package/dist/server/server/file-download/token-store.js +40 -0
  223. package/dist/server/server/file-download/token-store.js.map +1 -0
  224. package/dist/server/server/file-explorer/service.d.ts +41 -0
  225. package/dist/server/server/file-explorer/service.d.ts.map +1 -0
  226. package/dist/server/server/file-explorer/service.js +226 -0
  227. package/dist/server/server/file-explorer/service.js.map +1 -0
  228. package/dist/server/server/index.d.ts +2 -0
  229. package/dist/server/server/index.d.ts.map +1 -0
  230. package/dist/server/server/index.js +141 -0
  231. package/dist/server/server/index.js.map +1 -0
  232. package/dist/server/server/json-utils.d.ts +11 -0
  233. package/dist/server/server/json-utils.d.ts.map +1 -0
  234. package/dist/server/server/json-utils.js +45 -0
  235. package/dist/server/server/json-utils.js.map +1 -0
  236. package/dist/server/server/junction-home.d.ts +2 -0
  237. package/dist/server/server/junction-home.d.ts.map +1 -0
  238. package/dist/server/server/junction-home.js +19 -0
  239. package/dist/server/server/junction-home.js.map +1 -0
  240. package/dist/server/server/logger.d.ts +12 -0
  241. package/dist/server/server/logger.d.ts.map +1 -0
  242. package/dist/server/server/logger.js +29 -0
  243. package/dist/server/server/logger.js.map +1 -0
  244. package/dist/server/server/messages.d.ts +9 -0
  245. package/dist/server/server/messages.d.ts.map +1 -0
  246. package/dist/server/server/messages.js +29 -0
  247. package/dist/server/server/messages.js.map +1 -0
  248. package/dist/server/server/package-version.d.ts +13 -0
  249. package/dist/server/server/package-version.d.ts.map +1 -0
  250. package/dist/server/server/package-version.js +47 -0
  251. package/dist/server/server/package-version.js.map +1 -0
  252. package/dist/server/server/path-utils.d.ts +3 -0
  253. package/dist/server/server/path-utils.d.ts.map +1 -0
  254. package/dist/server/server/path-utils.js +20 -0
  255. package/dist/server/server/path-utils.js.map +1 -0
  256. package/dist/server/server/persisted-config.d.ts +270 -0
  257. package/dist/server/server/persisted-config.d.ts.map +1 -0
  258. package/dist/server/server/persisted-config.js +152 -0
  259. package/dist/server/server/persisted-config.js.map +1 -0
  260. package/dist/server/server/persistence-hooks.d.ts +30 -0
  261. package/dist/server/server/persistence-hooks.d.ts.map +1 -0
  262. package/dist/server/server/persistence-hooks.js +68 -0
  263. package/dist/server/server/persistence-hooks.js.map +1 -0
  264. package/dist/server/server/pid-lock.d.ts +26 -0
  265. package/dist/server/server/pid-lock.d.ts.map +1 -0
  266. package/dist/server/server/pid-lock.js +280 -0
  267. package/dist/server/server/pid-lock.js.map +1 -0
  268. package/dist/server/server/relay-transport.d.ts +23 -0
  269. package/dist/server/server/relay-transport.d.ts.map +1 -0
  270. package/dist/server/server/relay-transport.js +457 -0
  271. package/dist/server/server/relay-transport.js.map +1 -0
  272. package/dist/server/server/server-id.d.ts +17 -0
  273. package/dist/server/server/server-id.d.ts.map +1 -0
  274. package/dist/server/server/server-id.js +63 -0
  275. package/dist/server/server/server-id.js.map +1 -0
  276. package/dist/server/server/session.d.ts +280 -0
  277. package/dist/server/server/session.d.ts.map +1 -0
  278. package/dist/server/server/session.js +4395 -0
  279. package/dist/server/server/session.js.map +1 -0
  280. package/dist/server/server/terminal-mcp/index.d.ts +4 -0
  281. package/dist/server/server/terminal-mcp/index.d.ts.map +1 -0
  282. package/dist/server/server/terminal-mcp/index.js +3 -0
  283. package/dist/server/server/terminal-mcp/index.js.map +1 -0
  284. package/dist/server/server/terminal-mcp/server.d.ts +10 -0
  285. package/dist/server/server/terminal-mcp/server.d.ts.map +1 -0
  286. package/dist/server/server/terminal-mcp/server.js +217 -0
  287. package/dist/server/server/terminal-mcp/server.js.map +1 -0
  288. package/dist/server/server/terminal-mcp/terminal-manager.d.ts +123 -0
  289. package/dist/server/server/terminal-mcp/terminal-manager.d.ts.map +1 -0
  290. package/dist/server/server/terminal-mcp/terminal-manager.js +351 -0
  291. package/dist/server/server/terminal-mcp/terminal-manager.js.map +1 -0
  292. package/dist/server/server/terminal-mcp/tmux.d.ts +207 -0
  293. package/dist/server/server/terminal-mcp/tmux.d.ts.map +1 -0
  294. package/dist/server/server/terminal-mcp/tmux.js +924 -0
  295. package/dist/server/server/terminal-mcp/tmux.js.map +1 -0
  296. package/dist/server/server/types.d.ts +5 -0
  297. package/dist/server/server/types.d.ts.map +1 -0
  298. package/dist/server/server/types.js +3 -0
  299. package/dist/server/server/types.js.map +1 -0
  300. package/dist/server/server/utils/diff-highlighter.d.ts +52 -0
  301. package/dist/server/server/utils/diff-highlighter.d.ts.map +1 -0
  302. package/dist/server/server/utils/diff-highlighter.js +244 -0
  303. package/dist/server/server/utils/diff-highlighter.js.map +1 -0
  304. package/dist/server/server/utils/syntax-highlighter.d.ts +10 -0
  305. package/dist/server/server/utils/syntax-highlighter.d.ts.map +1 -0
  306. package/dist/server/server/utils/syntax-highlighter.js +145 -0
  307. package/dist/server/server/utils/syntax-highlighter.js.map +1 -0
  308. package/dist/server/server/websocket-server.d.ts +79 -0
  309. package/dist/server/server/websocket-server.d.ts.map +1 -0
  310. package/dist/server/server/websocket-server.js +742 -0
  311. package/dist/server/server/websocket-server.js.map +1 -0
  312. package/dist/server/server/worktree-bootstrap.d.ts +29 -0
  313. package/dist/server/server/worktree-bootstrap.d.ts.map +1 -0
  314. package/dist/server/server/worktree-bootstrap.js +454 -0
  315. package/dist/server/server/worktree-bootstrap.js.map +1 -0
  316. package/dist/server/shared/agent-attention-notification.d.ts +40 -0
  317. package/dist/server/shared/agent-attention-notification.d.ts.map +1 -0
  318. package/dist/server/shared/agent-attention-notification.js +130 -0
  319. package/dist/server/shared/agent-attention-notification.js.map +1 -0
  320. package/dist/server/shared/agent-lifecycle.d.ts +3 -0
  321. package/dist/server/shared/agent-lifecycle.d.ts.map +1 -0
  322. package/dist/server/shared/agent-lifecycle.js +8 -0
  323. package/dist/server/shared/agent-lifecycle.js.map +1 -0
  324. package/dist/server/shared/binary-mux.d.ts +31 -0
  325. package/dist/server/shared/binary-mux.d.ts.map +1 -0
  326. package/dist/server/shared/binary-mux.js +114 -0
  327. package/dist/server/shared/binary-mux.js.map +1 -0
  328. package/dist/server/shared/connection-offer.d.ts +62 -0
  329. package/dist/server/shared/connection-offer.d.ts.map +1 -0
  330. package/dist/server/shared/connection-offer.js +17 -0
  331. package/dist/server/shared/connection-offer.js.map +1 -0
  332. package/dist/server/shared/daemon-endpoints.d.ts +27 -0
  333. package/dist/server/shared/daemon-endpoints.d.ts.map +1 -0
  334. package/dist/server/shared/daemon-endpoints.js +113 -0
  335. package/dist/server/shared/daemon-endpoints.js.map +1 -0
  336. package/dist/server/shared/messages.d.ts +36982 -0
  337. package/dist/server/shared/messages.d.ts.map +1 -0
  338. package/dist/server/shared/messages.js +1793 -0
  339. package/dist/server/shared/messages.js.map +1 -0
  340. package/dist/server/shared/path-utils.d.ts +2 -0
  341. package/dist/server/shared/path-utils.d.ts.map +1 -0
  342. package/dist/server/shared/path-utils.js +16 -0
  343. package/dist/server/shared/path-utils.js.map +1 -0
  344. package/dist/server/shared/terminal-key-input.d.ts +9 -0
  345. package/dist/server/shared/terminal-key-input.d.ts.map +1 -0
  346. package/dist/server/shared/terminal-key-input.js +132 -0
  347. package/dist/server/shared/terminal-key-input.js.map +1 -0
  348. package/dist/server/shared/tool-call-display.d.ts +11 -0
  349. package/dist/server/shared/tool-call-display.d.ts.map +1 -0
  350. package/dist/server/shared/tool-call-display.js +102 -0
  351. package/dist/server/shared/tool-call-display.js.map +1 -0
  352. package/dist/server/shared/tool-call-interpretation.d.ts +14 -0
  353. package/dist/server/shared/tool-call-interpretation.d.ts.map +1 -0
  354. package/dist/server/shared/tool-call-interpretation.js +76 -0
  355. package/dist/server/shared/tool-call-interpretation.js.map +1 -0
  356. package/dist/server/terminal/terminal-manager.d.ts +30 -0
  357. package/dist/server/terminal/terminal-manager.d.ts.map +1 -0
  358. package/dist/server/terminal/terminal-manager.js +148 -0
  359. package/dist/server/terminal/terminal-manager.js.map +1 -0
  360. package/dist/server/terminal/terminal.d.ts +93 -0
  361. package/dist/server/terminal/terminal.d.ts.map +1 -0
  362. package/dist/server/terminal/terminal.js +386 -0
  363. package/dist/server/terminal/terminal.js.map +1 -0
  364. package/dist/server/utils/checkout-git.d.ts +150 -0
  365. package/dist/server/utils/checkout-git.d.ts.map +1 -0
  366. package/dist/server/utils/checkout-git.js +1427 -0
  367. package/dist/server/utils/checkout-git.js.map +1 -0
  368. package/dist/server/utils/city-names.d.ts +17 -0
  369. package/dist/server/utils/city-names.d.ts.map +1 -0
  370. package/dist/server/utils/city-names.js +122 -0
  371. package/dist/server/utils/city-names.js.map +1 -0
  372. package/dist/server/utils/directory-suggestions.d.ts +31 -0
  373. package/dist/server/utils/directory-suggestions.d.ts.map +1 -0
  374. package/dist/server/utils/directory-suggestions.js +678 -0
  375. package/dist/server/utils/directory-suggestions.js.map +1 -0
  376. package/dist/server/utils/git-clone.d.ts +10 -0
  377. package/dist/server/utils/git-clone.d.ts.map +1 -0
  378. package/dist/server/utils/git-clone.js +45 -0
  379. package/dist/server/utils/git-clone.js.map +1 -0
  380. package/dist/server/utils/git-init.d.ts +9 -0
  381. package/dist/server/utils/git-init.d.ts.map +1 -0
  382. package/dist/server/utils/git-init.js +91 -0
  383. package/dist/server/utils/git-init.js.map +1 -0
  384. package/dist/server/utils/path.d.ts +5 -0
  385. package/dist/server/utils/path.d.ts.map +1 -0
  386. package/dist/server/utils/path.js +15 -0
  387. package/dist/server/utils/path.js.map +1 -0
  388. package/dist/server/utils/project-icon.d.ts +39 -0
  389. package/dist/server/utils/project-icon.d.ts.map +1 -0
  390. package/dist/server/utils/project-icon.js +391 -0
  391. package/dist/server/utils/project-icon.js.map +1 -0
  392. package/dist/server/utils/worktree-metadata.d.ts +47 -0
  393. package/dist/server/utils/worktree-metadata.d.ts.map +1 -0
  394. package/dist/server/utils/worktree-metadata.js +116 -0
  395. package/dist/server/utils/worktree-metadata.js.map +1 -0
  396. package/dist/server/utils/worktree.d.ts +175 -0
  397. package/dist/server/utils/worktree.d.ts.map +1 -0
  398. package/dist/server/utils/worktree.js +858 -0
  399. package/dist/server/utils/worktree.js.map +1 -0
  400. package/package.json +107 -0
@@ -0,0 +1,4395 @@
1
+ import { v4 as uuidv4 } from 'uuid';
2
+ import { watch } from 'node:fs';
3
+ import { exec } from 'child_process';
4
+ import { promisify } from 'util';
5
+ import { resolve, sep, basename, dirname } from 'path';
6
+ import { homedir } from 'node:os';
7
+ import { z } from 'zod';
8
+ import { serializeAgentStreamEvent, } from './messages.js';
9
+ import { BinaryMuxChannel, TerminalBinaryFlags, TerminalBinaryMessageType, } from '../shared/binary-mux.js';
10
+ import { buildConfigOverrides, buildSessionConfig, extractTimestamps, extractTimelineSnapshot, } from './persistence-hooks.js';
11
+ import { experimental_createMCPClient } from 'ai';
12
+ import { buildProviderRegistry } from './agent/provider-registry.js';
13
+ import { scheduleAgentMetadataGeneration } from './agent/agent-metadata-generator.js';
14
+ import { resolveEffectiveThinkingOptionId, toAgentPayload } from './agent/agent-projections.js';
15
+ import { appendTimelineItemIfAgentKnown, emitLiveTimelineItemIfAgentKnown, } from './agent/timeline-append.js';
16
+ import { projectTimelineRows, selectTimelineWindowByProjectedLimit, } from './agent/timeline-projection.js';
17
+ import { DEFAULT_STRUCTURED_GENERATION_PROVIDERS, StructuredAgentFallbackError, StructuredAgentResponseError, generateStructuredAgentResponseWithFallback, } from './agent/agent-response-loop.js';
18
+ import { isValidAgentProvider, AGENT_PROVIDER_IDS } from './agent/provider-manifest.js';
19
+ import { listDirectoryEntries, readExplorerFile, getDownloadableFileInfo, } from './file-explorer/service.js';
20
+ import { slugify, validateBranchSlug, listJunctionWorktrees, deleteJunctionWorktree, isJunctionOwnedWorktreeCwd, resolveJunctionWorktreeRootForCwd, createInRepoWorktree, } from '../utils/worktree.js';
21
+ import { runAsyncWorktreeBootstrap } from './worktree-bootstrap.js';
22
+ import { getCheckoutDiff, getCheckoutStatus, getCheckoutStatusLite, listBranchSuggestions, NotGitRepoError, MergeConflictError, MergeFromBaseConflictError, commitChanges, mergeToBase, mergeFromBase, pushCurrentBranch, createPullRequest, getPullRequestStatus, resolveBaseRef, } from '../utils/checkout-git.js';
23
+ import { getProjectIcon } from '../utils/project-icon.js';
24
+ import { expandTilde } from '../utils/path.js';
25
+ import { searchHomeDirectories, searchWorkspaceEntries, searchGitRepositories, checkIsGitRepo } from '../utils/directory-suggestions.js';
26
+ import { cloneRepository } from '../utils/git-clone.js';
27
+ import { initRepository } from '../utils/git-init.js';
28
+ import { resolveClientMessageId } from './client-message-id.js';
29
+ const execAsync = promisify(exec);
30
+ const READ_ONLY_GIT_ENV = {
31
+ ...process.env,
32
+ GIT_OPTIONAL_LOCKS: '0',
33
+ };
34
+ const pendingAgentInitializations = new Map();
35
+ const DEFAULT_AGENT_PROVIDER = AGENT_PROVIDER_IDS[0];
36
+ const CHECKOUT_DIFF_WATCH_DEBOUNCE_MS = 150;
37
+ const CHECKOUT_DIFF_FALLBACK_REFRESH_MS = 5000;
38
+ const TERMINAL_STREAM_WINDOW_BYTES = 256 * 1024;
39
+ const TERMINAL_STREAM_MAX_PENDING_BYTES = 2 * 1024 * 1024;
40
+ const TERMINAL_STREAM_MAX_PENDING_CHUNKS = 2048;
41
+ function deriveRemoteProjectKey(remoteUrl) {
42
+ if (!remoteUrl) {
43
+ return null;
44
+ }
45
+ const trimmed = remoteUrl.trim();
46
+ if (!trimmed) {
47
+ return null;
48
+ }
49
+ let host = null;
50
+ let path = null;
51
+ const scpLike = trimmed.match(/^[^@]+@([^:]+):(.+)$/);
52
+ if (scpLike) {
53
+ host = scpLike[1] ?? null;
54
+ path = scpLike[2] ?? null;
55
+ }
56
+ else if (trimmed.includes('://')) {
57
+ try {
58
+ const parsed = new URL(trimmed);
59
+ host = parsed.hostname || null;
60
+ path = parsed.pathname ? parsed.pathname.replace(/^\//, '') : null;
61
+ }
62
+ catch {
63
+ return null;
64
+ }
65
+ }
66
+ if (!host || !path) {
67
+ return null;
68
+ }
69
+ let cleanedPath = path.trim().replace(/^\/+/, '').replace(/\/+$/, '');
70
+ if (cleanedPath.endsWith('.git')) {
71
+ cleanedPath = cleanedPath.slice(0, -4);
72
+ }
73
+ if (!cleanedPath.includes('/')) {
74
+ return null;
75
+ }
76
+ const cleanedHost = host.toLowerCase();
77
+ if (cleanedHost === 'github.com') {
78
+ return `remote:github.com/${cleanedPath}`;
79
+ }
80
+ return `remote:${cleanedHost}/${cleanedPath}`;
81
+ }
82
+ function deriveProjectGroupingKey(options) {
83
+ const remoteKey = deriveRemoteProjectKey(options.remoteUrl);
84
+ if (remoteKey) {
85
+ return remoteKey;
86
+ }
87
+ const worktreeMarker = '.junction/';
88
+ const idx = options.cwd.indexOf(worktreeMarker);
89
+ if (idx !== -1) {
90
+ return options.cwd.slice(0, idx).replace(/\/$/, '');
91
+ }
92
+ return options.cwd;
93
+ }
94
+ function deriveProjectGroupingName(projectKey) {
95
+ const githubRemotePrefix = 'remote:github.com/';
96
+ if (projectKey.startsWith(githubRemotePrefix)) {
97
+ return projectKey.slice(githubRemotePrefix.length) || projectKey;
98
+ }
99
+ const segments = projectKey.split(/[\\/]/).filter(Boolean);
100
+ return segments[segments.length - 1] || projectKey;
101
+ }
102
+ class SessionRequestError extends Error {
103
+ constructor(code, message) {
104
+ super(message);
105
+ this.code = code;
106
+ this.name = 'SessionRequestError';
107
+ }
108
+ }
109
+ const SAFE_GIT_REF_PATTERN = /^[A-Za-z0-9._\/-]+$/;
110
+ function coerceAgentProvider(logger, value, agentId) {
111
+ if (isValidAgentProvider(value)) {
112
+ return value;
113
+ }
114
+ logger.warn({ value, agentId, defaultProvider: DEFAULT_AGENT_PROVIDER }, `Unknown provider '${value}' for agent ${agentId ?? 'unknown'}; defaulting to '${DEFAULT_AGENT_PROVIDER}'`);
115
+ return DEFAULT_AGENT_PROVIDER;
116
+ }
117
+ function toAgentPersistenceHandle(logger, handle) {
118
+ if (!handle) {
119
+ return null;
120
+ }
121
+ const provider = handle.provider;
122
+ if (!isValidAgentProvider(provider)) {
123
+ logger.warn({ provider }, `Ignoring persistence handle with unknown provider '${provider}'`);
124
+ return null;
125
+ }
126
+ if (!handle.sessionId) {
127
+ logger.warn('Ignoring persistence handle missing sessionId');
128
+ return null;
129
+ }
130
+ return {
131
+ provider,
132
+ sessionId: handle.sessionId,
133
+ nativeHandle: handle.nativeHandle,
134
+ metadata: handle.metadata,
135
+ };
136
+ }
137
+ /**
138
+ * Session represents a single connected client session.
139
+ * It owns all state management, orchestration logic, and message processing.
140
+ * Session has no knowledge of WebSockets - it only emits and receives messages.
141
+ */
142
+ export class Session {
143
+ constructor(options) {
144
+ // Per-session MCP client and tools
145
+ this.agentMcpClient = null;
146
+ this.agentTools = null;
147
+ this.unsubscribeAgentEvents = null;
148
+ this.agentUpdatesSubscription = null;
149
+ this.clientActivity = null;
150
+ this.MOBILE_BACKGROUND_STREAM_GRACE_MS = 60000;
151
+ this.subscribedTerminalDirectories = new Set();
152
+ this.unsubscribeTerminalsChanged = null;
153
+ this.terminalSubscriptions = new Map();
154
+ this.terminalExitSubscriptions = new Map();
155
+ this.terminalStreams = new Map();
156
+ this.terminalStreamByTerminalId = new Map();
157
+ this.nextTerminalStreamId = 1;
158
+ this.checkoutDiffSubscriptions = new Map();
159
+ this.checkoutDiffTargets = new Map();
160
+ const { clientId, userId, onMessage, onBinaryMessage, onLifecycleIntent, logger, downloadTokenStore, junctionHome, agentManager, agentStorage, createAgentMcpTransport, terminalManager, agentProviderRuntimeSettings, } = options;
161
+ this.clientId = clientId;
162
+ this.userId = userId;
163
+ this.sessionId = uuidv4();
164
+ this.onMessage = onMessage;
165
+ this.onBinaryMessage = onBinaryMessage ?? null;
166
+ this.onLifecycleIntent = onLifecycleIntent ?? null;
167
+ this.downloadTokenStore = downloadTokenStore;
168
+ this.junctionHome = junctionHome;
169
+ this.agentManager = agentManager;
170
+ this.agentStorage = agentStorage;
171
+ this.createAgentMcpTransport = createAgentMcpTransport;
172
+ this.terminalManager = terminalManager;
173
+ if (this.terminalManager) {
174
+ this.unsubscribeTerminalsChanged = this.terminalManager.subscribeTerminalsChanged((event) => this.handleTerminalsChanged(event));
175
+ }
176
+ this.agentProviderRuntimeSettings = agentProviderRuntimeSettings;
177
+ this.abortController = new AbortController();
178
+ this.sessionLogger = logger.child({
179
+ module: 'session',
180
+ clientId: this.clientId,
181
+ sessionId: this.sessionId,
182
+ });
183
+ this.providerRegistry = buildProviderRegistry(this.sessionLogger, {
184
+ runtimeSettings: this.agentProviderRuntimeSettings,
185
+ });
186
+ // Initialize agent MCP client asynchronously
187
+ void this.initializeAgentMcp();
188
+ this.subscribeToAgentEvents();
189
+ this.sessionLogger.trace('Session created');
190
+ }
191
+ /**
192
+ * Get the client's current activity state
193
+ */
194
+ getClientActivity() {
195
+ return this.clientActivity;
196
+ }
197
+ /**
198
+ * Send initial state to client after connection
199
+ */
200
+ async sendInitialState() {
201
+ // No unsolicited agent list hydration. Callers must use fetch_agents_request.
202
+ }
203
+ /**
204
+ * Normalize a user prompt (with optional image metadata) for AgentManager
205
+ */
206
+ buildAgentPrompt(text, images) {
207
+ const normalized = text?.trim() ?? '';
208
+ if (!images || images.length === 0) {
209
+ return normalized;
210
+ }
211
+ const blocks = [];
212
+ if (normalized.length > 0) {
213
+ blocks.push({ type: 'text', text: normalized });
214
+ }
215
+ for (const image of images) {
216
+ blocks.push({ type: 'image', data: image.data, mimeType: image.mimeType });
217
+ }
218
+ return blocks;
219
+ }
220
+ /**
221
+ * Interrupt the agent's active run so the next prompt starts a fresh turn.
222
+ * Returns once the manager confirms the stream has been cancelled.
223
+ */
224
+ async interruptAgentIfRunning(agentId) {
225
+ const snapshot = this.agentManager.getAgent(agentId);
226
+ if (!snapshot) {
227
+ throw new Error(`Agent ${agentId} not found`);
228
+ }
229
+ if (snapshot.lifecycle !== 'running' && !snapshot.pendingRun) {
230
+ return;
231
+ }
232
+ this.sessionLogger.debug({ agentId, lifecycle: snapshot.lifecycle, pendingRun: Boolean(snapshot.pendingRun) }, 'interruptAgentIfRunning: interrupting');
233
+ try {
234
+ const t0 = Date.now();
235
+ const cancelled = await this.agentManager.cancelAgentRun(agentId);
236
+ this.sessionLogger.debug({ agentId, cancelled, durationMs: Date.now() - t0 }, 'interruptAgentIfRunning: cancelAgentRun completed');
237
+ if (!cancelled) {
238
+ this.sessionLogger.warn({ agentId }, 'interruptAgentIfRunning: reported running but no active run was cancelled');
239
+ }
240
+ }
241
+ catch (error) {
242
+ throw error;
243
+ }
244
+ }
245
+ /**
246
+ * Start streaming an agent run and forward results via the websocket broadcast
247
+ */
248
+ startAgentStream(agentId, prompt, runOptions) {
249
+ let iterator;
250
+ try {
251
+ iterator = this.agentManager.streamAgent(agentId, prompt, runOptions);
252
+ }
253
+ catch (error) {
254
+ this.handleAgentRunError(agentId, error, 'Failed to start agent run');
255
+ const message = error instanceof Error ? error.message : typeof error === 'string' ? error : 'Unknown error';
256
+ return { ok: false, error: message };
257
+ }
258
+ void (async () => {
259
+ try {
260
+ for await (const _ of iterator) {
261
+ // Events are forwarded via the session's AgentManager subscription.
262
+ }
263
+ }
264
+ catch (error) {
265
+ this.handleAgentRunError(agentId, error, 'Agent stream failed');
266
+ }
267
+ })();
268
+ return { ok: true };
269
+ }
270
+ handleAgentRunError(agentId, error, context) {
271
+ const message = error instanceof Error ? error.message : typeof error === 'string' ? error : 'Unknown error';
272
+ this.sessionLogger.error({ err: error, agentId, context }, `${context} for agent ${agentId}`);
273
+ this.emit({
274
+ type: 'activity_log',
275
+ payload: {
276
+ id: uuidv4(),
277
+ timestamp: new Date(),
278
+ type: 'error',
279
+ content: `${context}: ${message}`,
280
+ },
281
+ });
282
+ }
283
+ /**
284
+ * Initialize Agent MCP client for this session using in-memory transport
285
+ */
286
+ async initializeAgentMcp() {
287
+ try {
288
+ // Create an in-memory transport connected to the Agent MCP server
289
+ const transport = await this.createAgentMcpTransport();
290
+ this.agentMcpClient = await experimental_createMCPClient({
291
+ transport,
292
+ });
293
+ this.agentTools = (await this.agentMcpClient.tools());
294
+ const agentToolCount = Object.keys(this.agentTools ?? {}).length;
295
+ this.sessionLogger.trace({ agentToolCount }, `Agent MCP initialized with ${agentToolCount} tools`);
296
+ }
297
+ catch (error) {
298
+ this.sessionLogger.error({ err: error }, 'Failed to initialize Agent MCP');
299
+ }
300
+ }
301
+ /**
302
+ * Subscribe to AgentManager events and forward them to the client
303
+ */
304
+ subscribeToAgentEvents() {
305
+ if (this.unsubscribeAgentEvents) {
306
+ this.unsubscribeAgentEvents();
307
+ }
308
+ this.unsubscribeAgentEvents = this.agentManager.subscribe((event) => {
309
+ if (event.type === 'agent_state') {
310
+ void this.forwardAgentUpdate(event.agent);
311
+ return;
312
+ }
313
+ // Reduce bandwidth/CPU on mobile: only forward high-frequency agent stream events
314
+ // for the focused agent, with a short grace window while backgrounded.
315
+ // History catch-up is handled via pull-based `fetch_agent_timeline_request`.
316
+ const activity = this.clientActivity;
317
+ if (activity?.deviceType === 'mobile') {
318
+ if (!activity.focusedAgentId) {
319
+ return;
320
+ }
321
+ if (activity.focusedAgentId !== event.agentId) {
322
+ return;
323
+ }
324
+ if (!activity.appVisible) {
325
+ const hiddenForMs = Date.now() - activity.appVisibilityChangedAt.getTime();
326
+ if (hiddenForMs >= this.MOBILE_BACKGROUND_STREAM_GRACE_MS) {
327
+ return;
328
+ }
329
+ }
330
+ }
331
+ const serializedEvent = serializeAgentStreamEvent(event.event);
332
+ if (!serializedEvent) {
333
+ return;
334
+ }
335
+ const payload = {
336
+ agentId: event.agentId,
337
+ event: serializedEvent,
338
+ timestamp: new Date().toISOString(),
339
+ ...(typeof event.seq === 'number' ? { seq: event.seq } : {}),
340
+ ...(typeof event.epoch === 'string' ? { epoch: event.epoch } : {}),
341
+ };
342
+ this.emit({
343
+ type: 'agent_stream',
344
+ payload,
345
+ });
346
+ if (event.event.type === 'permission_requested') {
347
+ this.emit({
348
+ type: 'agent_permission_request',
349
+ payload: {
350
+ agentId: event.agentId,
351
+ request: event.event.request,
352
+ },
353
+ });
354
+ }
355
+ else if (event.event.type === 'permission_resolved') {
356
+ this.emit({
357
+ type: 'agent_permission_resolved',
358
+ payload: {
359
+ agentId: event.agentId,
360
+ requestId: event.event.requestId,
361
+ resolution: event.event.resolution,
362
+ },
363
+ });
364
+ }
365
+ // Title updates may be applied asynchronously after agent creation.
366
+ }, { replayState: false });
367
+ }
368
+ async buildAgentPayload(agent) {
369
+ const storedRecord = await this.agentStorage.get(agent.id);
370
+ const title = storedRecord?.title ?? null;
371
+ const payload = toAgentPayload(agent, { title });
372
+ payload.archivedAt = storedRecord?.archivedAt ?? null;
373
+ return payload;
374
+ }
375
+ buildStoredAgentPayload(record) {
376
+ const defaultCapabilities = {
377
+ supportsStreaming: false,
378
+ supportsSessionPersistence: true,
379
+ supportsDynamicModes: false,
380
+ supportsMcpServers: false,
381
+ supportsReasoningStream: false,
382
+ supportsToolInvocations: true,
383
+ };
384
+ const createdAt = new Date(record.createdAt);
385
+ const updatedAt = new Date(record.lastActivityAt ?? record.updatedAt);
386
+ const lastUserMessageAt = record.lastUserMessageAt ? new Date(record.lastUserMessageAt) : null;
387
+ const provider = coerceAgentProvider(this.sessionLogger, record.provider, record.id);
388
+ const runtimeInfo = record.runtimeInfo
389
+ ? {
390
+ provider: coerceAgentProvider(this.sessionLogger, record.runtimeInfo.provider, record.id),
391
+ sessionId: record.runtimeInfo.sessionId,
392
+ ...(Object.prototype.hasOwnProperty.call(record.runtimeInfo, 'model')
393
+ ? { model: record.runtimeInfo.model ?? null }
394
+ : {}),
395
+ ...(Object.prototype.hasOwnProperty.call(record.runtimeInfo, 'thinkingOptionId')
396
+ ? { thinkingOptionId: record.runtimeInfo.thinkingOptionId ?? null }
397
+ : {}),
398
+ ...(Object.prototype.hasOwnProperty.call(record.runtimeInfo, 'modeId')
399
+ ? { modeId: record.runtimeInfo.modeId ?? null }
400
+ : {}),
401
+ ...(record.runtimeInfo.extra ? { extra: record.runtimeInfo.extra } : {}),
402
+ }
403
+ : undefined;
404
+ return {
405
+ id: record.id,
406
+ provider,
407
+ cwd: record.cwd,
408
+ model: record.config?.model ?? null,
409
+ thinkingOptionId: record.config?.thinkingOptionId ?? null,
410
+ effectiveThinkingOptionId: resolveEffectiveThinkingOptionId({
411
+ runtimeInfo,
412
+ configuredThinkingOptionId: record.config?.thinkingOptionId ?? null,
413
+ }),
414
+ ...(runtimeInfo ? { runtimeInfo } : {}),
415
+ createdAt: createdAt.toISOString(),
416
+ updatedAt: updatedAt.toISOString(),
417
+ lastUserMessageAt: lastUserMessageAt ? lastUserMessageAt.toISOString() : null,
418
+ status: record.lastStatus,
419
+ capabilities: defaultCapabilities,
420
+ currentModeId: record.lastModeId ?? null,
421
+ availableModes: [],
422
+ pendingPermissions: [],
423
+ persistence: toAgentPersistenceHandle(this.sessionLogger, record.persistence),
424
+ lastUsage: undefined,
425
+ lastError: undefined,
426
+ title: record.title ?? null,
427
+ requiresAttention: record.requiresAttention ?? false,
428
+ attentionReason: record.attentionReason ?? null,
429
+ attentionTimestamp: record.attentionTimestamp ?? null,
430
+ archivedAt: record.archivedAt ?? null,
431
+ labels: record.labels,
432
+ };
433
+ }
434
+ async ensureAgentLoaded(agentId) {
435
+ const existing = this.agentManager.getAgent(agentId);
436
+ if (existing) {
437
+ return existing;
438
+ }
439
+ const inflight = pendingAgentInitializations.get(agentId);
440
+ if (inflight) {
441
+ return inflight;
442
+ }
443
+ const initPromise = (async () => {
444
+ const record = await this.agentStorage.get(agentId);
445
+ if (!record) {
446
+ throw new Error(`Agent not found: ${agentId}`);
447
+ }
448
+ const handle = toAgentPersistenceHandle(this.sessionLogger, record.persistence);
449
+ let snapshot;
450
+ if (handle) {
451
+ snapshot = await this.agentManager.resumeAgentFromPersistence(handle, buildConfigOverrides(record), agentId, {
452
+ ...extractTimestamps(record),
453
+ ...extractTimelineSnapshot(record),
454
+ });
455
+ this.sessionLogger.info({ agentId, provider: record.provider }, 'Agent resumed from persistence');
456
+ }
457
+ else {
458
+ const config = buildSessionConfig(record);
459
+ snapshot = await this.agentManager.createAgent(config, agentId, { labels: record.labels });
460
+ this.sessionLogger.info({ agentId, provider: record.provider }, 'Agent created from stored config');
461
+ }
462
+ await this.agentManager.hydrateTimelineFromProvider(agentId);
463
+ return this.agentManager.getAgent(agentId) ?? snapshot;
464
+ })();
465
+ pendingAgentInitializations.set(agentId, initPromise);
466
+ try {
467
+ return await initPromise;
468
+ }
469
+ finally {
470
+ const current = pendingAgentInitializations.get(agentId);
471
+ if (current === initPromise) {
472
+ pendingAgentInitializations.delete(agentId);
473
+ }
474
+ }
475
+ }
476
+ matchesAgentFilter(options) {
477
+ const { agent, project, filter } = options;
478
+ if (filter?.labels) {
479
+ const matchesLabels = Object.entries(filter.labels).every(([key, value]) => agent.labels[key] === value);
480
+ if (!matchesLabels) {
481
+ return false;
482
+ }
483
+ }
484
+ const includeArchived = filter?.includeArchived ?? false;
485
+ if (!includeArchived && agent.archivedAt) {
486
+ return false;
487
+ }
488
+ if (filter?.thinkingOptionId !== undefined) {
489
+ const expectedThinkingOptionId = resolveEffectiveThinkingOptionId({
490
+ configuredThinkingOptionId: filter.thinkingOptionId ?? null,
491
+ });
492
+ const resolvedThinkingOptionId = agent.effectiveThinkingOptionId ??
493
+ resolveEffectiveThinkingOptionId({
494
+ runtimeInfo: agent.runtimeInfo,
495
+ configuredThinkingOptionId: agent.thinkingOptionId ?? null,
496
+ });
497
+ if (resolvedThinkingOptionId !== expectedThinkingOptionId) {
498
+ return false;
499
+ }
500
+ }
501
+ if (filter?.statuses && filter.statuses.length > 0) {
502
+ const statuses = new Set(filter.statuses);
503
+ if (!statuses.has(agent.status)) {
504
+ return false;
505
+ }
506
+ }
507
+ if (typeof filter?.requiresAttention === 'boolean') {
508
+ const requiresAttention = agent.requiresAttention ?? false;
509
+ if (requiresAttention !== filter.requiresAttention) {
510
+ return false;
511
+ }
512
+ }
513
+ if (filter?.projectKeys && filter.projectKeys.length > 0) {
514
+ const projectKeys = new Set(filter.projectKeys.filter((item) => item.trim().length > 0));
515
+ if (projectKeys.size > 0 && !projectKeys.has(project.projectKey)) {
516
+ return false;
517
+ }
518
+ }
519
+ return true;
520
+ }
521
+ getAgentUpdateTargetId(update) {
522
+ return update.kind === 'remove' ? update.agentId : update.agent.id;
523
+ }
524
+ bufferOrEmitAgentUpdate(subscription, payload) {
525
+ if (subscription.isBootstrapping) {
526
+ subscription.pendingUpdatesByAgentId.set(this.getAgentUpdateTargetId(payload), payload);
527
+ return;
528
+ }
529
+ this.emit({
530
+ type: 'agent_update',
531
+ payload,
532
+ });
533
+ }
534
+ flushBootstrappedAgentUpdates(options) {
535
+ const subscription = this.agentUpdatesSubscription;
536
+ if (!subscription || !subscription.isBootstrapping) {
537
+ return;
538
+ }
539
+ subscription.isBootstrapping = false;
540
+ const pending = Array.from(subscription.pendingUpdatesByAgentId.values());
541
+ subscription.pendingUpdatesByAgentId.clear();
542
+ for (const payload of pending) {
543
+ if (payload.kind === 'upsert') {
544
+ const snapshotUpdatedAt = options?.snapshotUpdatedAtByAgentId?.get(payload.agent.id);
545
+ if (typeof snapshotUpdatedAt === 'number') {
546
+ const updateUpdatedAt = Date.parse(payload.agent.updatedAt);
547
+ if (!Number.isNaN(updateUpdatedAt) && updateUpdatedAt <= snapshotUpdatedAt) {
548
+ continue;
549
+ }
550
+ }
551
+ }
552
+ this.emit({
553
+ type: 'agent_update',
554
+ payload,
555
+ });
556
+ }
557
+ }
558
+ buildFallbackProjectCheckout(cwd) {
559
+ return {
560
+ cwd,
561
+ isGit: false,
562
+ currentBranch: null,
563
+ remoteUrl: null,
564
+ isJunctionOwnedWorktree: false,
565
+ mainRepoRoot: null,
566
+ };
567
+ }
568
+ toProjectCheckoutLite(cwd, status) {
569
+ if (!status.isGit) {
570
+ return this.buildFallbackProjectCheckout(cwd);
571
+ }
572
+ if (status.isJunctionOwnedWorktree) {
573
+ return {
574
+ cwd,
575
+ isGit: true,
576
+ currentBranch: status.currentBranch,
577
+ remoteUrl: status.remoteUrl,
578
+ isJunctionOwnedWorktree: true,
579
+ mainRepoRoot: status.mainRepoRoot,
580
+ };
581
+ }
582
+ return {
583
+ cwd,
584
+ isGit: true,
585
+ currentBranch: status.currentBranch,
586
+ remoteUrl: status.remoteUrl,
587
+ isJunctionOwnedWorktree: false,
588
+ mainRepoRoot: null,
589
+ };
590
+ }
591
+ async buildProjectPlacement(cwd) {
592
+ const checkout = await getCheckoutStatusLite(cwd, { junctionHome: this.junctionHome })
593
+ .then((status) => this.toProjectCheckoutLite(cwd, status))
594
+ .catch(() => this.buildFallbackProjectCheckout(cwd));
595
+ const projectKey = deriveProjectGroupingKey({
596
+ cwd,
597
+ remoteUrl: checkout.remoteUrl,
598
+ });
599
+ return {
600
+ projectKey,
601
+ projectName: deriveProjectGroupingName(projectKey),
602
+ checkout,
603
+ };
604
+ }
605
+ async forwardAgentUpdate(agent) {
606
+ try {
607
+ const subscription = this.agentUpdatesSubscription;
608
+ if (!subscription) {
609
+ return;
610
+ }
611
+ const payload = await this.buildAgentPayload(agent);
612
+ const project = await this.buildProjectPlacement(payload.cwd);
613
+ const matches = this.matchesAgentFilter({
614
+ agent: payload,
615
+ project,
616
+ filter: subscription.filter,
617
+ });
618
+ if (matches) {
619
+ this.bufferOrEmitAgentUpdate(subscription, {
620
+ kind: 'upsert',
621
+ agent: payload,
622
+ project,
623
+ });
624
+ return;
625
+ }
626
+ this.bufferOrEmitAgentUpdate(subscription, {
627
+ kind: 'remove',
628
+ agentId: payload.id,
629
+ });
630
+ }
631
+ catch (error) {
632
+ this.sessionLogger.error({ err: error }, 'Failed to emit agent update');
633
+ }
634
+ }
635
+ /**
636
+ * Main entry point for processing session messages
637
+ */
638
+ async handleMessage(msg) {
639
+ try {
640
+ switch (msg.type) {
641
+ case 'abort_request':
642
+ await this.handleAbort();
643
+ break;
644
+ case 'fetch_agents_request':
645
+ await this.handleFetchAgents(msg);
646
+ break;
647
+ case 'fetch_agent_request':
648
+ await this.handleFetchAgent(msg.agentId, msg.requestId);
649
+ break;
650
+ case 'delete_agent_request':
651
+ await this.handleDeleteAgentRequest(msg.agentId, msg.requestId);
652
+ break;
653
+ case 'archive_agent_request':
654
+ await this.handleArchiveAgentRequest(msg.agentId, msg.requestId);
655
+ break;
656
+ case 'update_agent_request':
657
+ await this.handleUpdateAgentRequest(msg.agentId, msg.name, msg.labels, msg.requestId);
658
+ break;
659
+ case 'send_agent_message_request':
660
+ await this.handleSendAgentMessageRequest(msg);
661
+ break;
662
+ case 'wait_for_finish_request':
663
+ await this.handleWaitForFinish(msg.agentId, msg.requestId, msg.timeoutMs);
664
+ break;
665
+ case 'create_agent_request':
666
+ await this.handleCreateAgentRequest(msg);
667
+ break;
668
+ case 'resume_agent_request':
669
+ await this.handleResumeAgentRequest(msg);
670
+ break;
671
+ case 'refresh_agent_request':
672
+ await this.handleRefreshAgentRequest(msg);
673
+ break;
674
+ case 'cancel_agent_request':
675
+ await this.handleCancelAgentRequest(msg.agentId);
676
+ break;
677
+ case 'restart_server_request':
678
+ await this.handleRestartServerRequest(msg.requestId, msg.reason);
679
+ break;
680
+ case 'shutdown_server_request':
681
+ await this.handleShutdownServerRequest(msg.requestId);
682
+ break;
683
+ case 'fetch_agent_timeline_request':
684
+ await this.handleFetchAgentTimelineRequest(msg);
685
+ break;
686
+ case 'set_agent_mode_request':
687
+ await this.handleSetAgentModeRequest(msg.agentId, msg.modeId, msg.requestId);
688
+ break;
689
+ case 'set_agent_model_request':
690
+ await this.handleSetAgentModelRequest(msg.agentId, msg.modelId, msg.requestId);
691
+ break;
692
+ case 'set_agent_thinking_request':
693
+ await this.handleSetAgentThinkingRequest(msg.agentId, msg.thinkingOptionId, msg.requestId);
694
+ break;
695
+ case 'agent_permission_response':
696
+ await this.handleAgentPermissionResponse(msg.agentId, msg.requestId, msg.response);
697
+ break;
698
+ case 'checkout_status_request':
699
+ await this.handleCheckoutStatusRequest(msg);
700
+ break;
701
+ case 'validate_branch_request':
702
+ await this.handleValidateBranchRequest(msg);
703
+ break;
704
+ case 'branch_suggestions_request':
705
+ await this.handleBranchSuggestionsRequest(msg);
706
+ break;
707
+ case 'directory_suggestions_request':
708
+ await this.handleDirectorySuggestionsRequest(msg);
709
+ break;
710
+ case 'git_clone_request':
711
+ await this.handleGitCloneRequest(msg);
712
+ break;
713
+ case 'git_init_request':
714
+ await this.handleGitInitRequest(msg);
715
+ break;
716
+ case 'subscribe_checkout_diff_request':
717
+ await this.handleSubscribeCheckoutDiffRequest(msg);
718
+ break;
719
+ case 'unsubscribe_checkout_diff_request':
720
+ this.handleUnsubscribeCheckoutDiffRequest(msg);
721
+ break;
722
+ case 'checkout_commit_request':
723
+ await this.handleCheckoutCommitRequest(msg);
724
+ break;
725
+ case 'checkout_merge_request':
726
+ await this.handleCheckoutMergeRequest(msg);
727
+ break;
728
+ case 'checkout_merge_from_base_request':
729
+ await this.handleCheckoutMergeFromBaseRequest(msg);
730
+ break;
731
+ case 'checkout_push_request':
732
+ await this.handleCheckoutPushRequest(msg);
733
+ break;
734
+ case 'checkout_pr_create_request':
735
+ await this.handleCheckoutPrCreateRequest(msg);
736
+ break;
737
+ case 'checkout_pr_status_request':
738
+ await this.handleCheckoutPrStatusRequest(msg);
739
+ break;
740
+ case 'junction_worktree_list_request':
741
+ await this.handleJunctionWorktreeListRequest(msg);
742
+ break;
743
+ case 'junction_worktree_archive_request':
744
+ await this.handleJunctionWorktreeArchiveRequest(msg);
745
+ break;
746
+ case 'file_explorer_request':
747
+ await this.handleFileExplorerRequest(msg);
748
+ break;
749
+ case 'workspace_file_explorer_request':
750
+ await this.handleWorkspaceFileExplorerRequest(msg);
751
+ break;
752
+ case 'project_icon_request':
753
+ await this.handleProjectIconRequest(msg);
754
+ break;
755
+ case 'file_download_token_request':
756
+ await this.handleFileDownloadTokenRequest(msg);
757
+ break;
758
+ case 'list_provider_models_request':
759
+ await this.handleListProviderModelsRequest(msg);
760
+ break;
761
+ case 'list_available_providers_request':
762
+ await this.handleListAvailableProvidersRequest(msg);
763
+ break;
764
+ case 'clear_agent_attention':
765
+ await this.handleClearAgentAttention(msg.agentId);
766
+ break;
767
+ case 'client_heartbeat':
768
+ this.handleClientHeartbeat(msg);
769
+ break;
770
+ case 'ping': {
771
+ const now = Date.now();
772
+ this.emit({
773
+ type: 'pong',
774
+ payload: {
775
+ requestId: msg.requestId,
776
+ clientSentAt: msg.clientSentAt,
777
+ serverReceivedAt: now,
778
+ serverSentAt: now,
779
+ },
780
+ });
781
+ break;
782
+ }
783
+ case 'list_commands_request':
784
+ await this.handleListCommandsRequest(msg);
785
+ break;
786
+ case 'subscribe_terminals_request':
787
+ this.handleSubscribeTerminalsRequest(msg);
788
+ break;
789
+ case 'unsubscribe_terminals_request':
790
+ this.handleUnsubscribeTerminalsRequest(msg);
791
+ break;
792
+ case 'list_terminals_request':
793
+ await this.handleListTerminalsRequest(msg);
794
+ break;
795
+ case 'create_terminal_request':
796
+ await this.handleCreateTerminalRequest(msg);
797
+ break;
798
+ case 'subscribe_terminal_request':
799
+ await this.handleSubscribeTerminalRequest(msg);
800
+ break;
801
+ case 'unsubscribe_terminal_request':
802
+ this.handleUnsubscribeTerminalRequest(msg);
803
+ break;
804
+ case 'terminal_input':
805
+ this.handleTerminalInput(msg);
806
+ break;
807
+ case 'kill_terminal_request':
808
+ await this.handleKillTerminalRequest(msg);
809
+ break;
810
+ case 'attach_terminal_stream_request':
811
+ await this.handleAttachTerminalStreamRequest(msg);
812
+ break;
813
+ case 'detach_terminal_stream_request':
814
+ this.handleDetachTerminalStreamRequest(msg);
815
+ break;
816
+ }
817
+ }
818
+ catch (error) {
819
+ const err = error instanceof Error ? error : new Error(String(error));
820
+ this.sessionLogger.error({ err }, 'Error handling message');
821
+ const requestId = msg.requestId;
822
+ if (typeof requestId === 'string') {
823
+ try {
824
+ this.emit({
825
+ type: 'rpc_error',
826
+ payload: {
827
+ requestId,
828
+ requestType: msg.type,
829
+ error: 'Request failed',
830
+ code: 'handler_error',
831
+ },
832
+ });
833
+ }
834
+ catch (emitError) {
835
+ this.sessionLogger.error({ err: emitError }, 'Failed to emit rpc_error');
836
+ }
837
+ }
838
+ this.emit({
839
+ type: 'activity_log',
840
+ payload: {
841
+ id: uuidv4(),
842
+ timestamp: new Date(),
843
+ type: 'error',
844
+ content: `Error: ${err.message}`,
845
+ },
846
+ });
847
+ }
848
+ }
849
+ handleBinaryFrame(frame) {
850
+ switch (frame.channel) {
851
+ case BinaryMuxChannel.Terminal:
852
+ this.handleTerminalBinaryFrame(frame);
853
+ break;
854
+ default:
855
+ this.sessionLogger.warn({ channel: frame.channel, messageType: frame.messageType }, 'Unhandled binary mux channel');
856
+ break;
857
+ }
858
+ }
859
+ handleTerminalBinaryFrame(frame) {
860
+ if (frame.messageType === TerminalBinaryMessageType.InputUtf8) {
861
+ const binding = this.terminalStreams.get(frame.streamId);
862
+ if (!binding) {
863
+ this.sessionLogger.warn({ streamId: frame.streamId }, 'Terminal stream not found for input');
864
+ return;
865
+ }
866
+ if (!this.terminalManager) {
867
+ return;
868
+ }
869
+ const session = this.terminalManager.getTerminal(binding.terminalId);
870
+ if (!session) {
871
+ this.detachTerminalStream(frame.streamId, { emitExit: true });
872
+ return;
873
+ }
874
+ const payload = frame.payload ?? new Uint8Array(0);
875
+ if (payload.byteLength === 0) {
876
+ return;
877
+ }
878
+ const text = Buffer.from(payload).toString('utf8');
879
+ if (!text) {
880
+ return;
881
+ }
882
+ session.send({ type: 'input', data: text });
883
+ return;
884
+ }
885
+ if (frame.messageType === TerminalBinaryMessageType.Ack) {
886
+ const binding = this.terminalStreams.get(frame.streamId);
887
+ if (binding) {
888
+ if (!Number.isFinite(frame.offset) || frame.offset < 0) {
889
+ return;
890
+ }
891
+ const nextAckOffset = Math.max(binding.lastAckOffset, Math.min(Math.floor(frame.offset), binding.lastOutputOffset));
892
+ if (nextAckOffset > binding.lastAckOffset) {
893
+ binding.lastAckOffset = nextAckOffset;
894
+ this.flushPendingTerminalStreamChunks(frame.streamId, binding);
895
+ }
896
+ }
897
+ return;
898
+ }
899
+ this.sessionLogger.warn({ streamId: frame.streamId, messageType: frame.messageType }, 'Unhandled terminal binary frame');
900
+ }
901
+ async handleRestartServerRequest(requestId, reason) {
902
+ const payload = {
903
+ status: 'restart_requested',
904
+ clientId: this.clientId,
905
+ };
906
+ if (reason && reason.trim().length > 0) {
907
+ payload.reason = reason;
908
+ }
909
+ payload.requestId = requestId;
910
+ this.sessionLogger.warn({ reason }, 'Restart requested via websocket');
911
+ this.emit({
912
+ type: 'status',
913
+ payload,
914
+ });
915
+ this.emitLifecycleIntent({
916
+ type: 'restart',
917
+ clientId: this.clientId,
918
+ requestId,
919
+ ...(reason ? { reason } : {}),
920
+ });
921
+ }
922
+ async handleShutdownServerRequest(requestId) {
923
+ this.sessionLogger.warn('Shutdown requested via websocket');
924
+ this.emit({
925
+ type: 'status',
926
+ payload: {
927
+ status: 'shutdown_requested',
928
+ clientId: this.clientId,
929
+ requestId,
930
+ },
931
+ });
932
+ this.emitLifecycleIntent({
933
+ type: 'shutdown',
934
+ clientId: this.clientId,
935
+ requestId,
936
+ });
937
+ }
938
+ emitLifecycleIntent(intent) {
939
+ if (!this.onLifecycleIntent) {
940
+ return;
941
+ }
942
+ try {
943
+ this.onLifecycleIntent(intent);
944
+ }
945
+ catch (error) {
946
+ this.sessionLogger.error({ err: error, intent }, 'Lifecycle intent handler failed');
947
+ }
948
+ }
949
+ async handleDeleteAgentRequest(agentId, requestId) {
950
+ this.sessionLogger.info({ agentId }, `Deleting agent ${agentId} from registry`);
951
+ // Prevent the persistence hook from re-creating the record while we close/delete.
952
+ this.agentStorage.beginDelete(agentId);
953
+ try {
954
+ await this.agentManager.closeAgent(agentId);
955
+ }
956
+ catch (error) {
957
+ this.sessionLogger.warn({ err: error, agentId }, `Failed to close agent ${agentId} during delete`);
958
+ }
959
+ try {
960
+ await this.agentStorage.remove(agentId);
961
+ }
962
+ catch (error) {
963
+ this.sessionLogger.error({ err: error, agentId }, `Failed to remove agent ${agentId} from registry`);
964
+ }
965
+ this.emit({
966
+ type: 'agent_deleted',
967
+ payload: {
968
+ agentId,
969
+ requestId,
970
+ },
971
+ });
972
+ if (this.agentUpdatesSubscription) {
973
+ this.bufferOrEmitAgentUpdate(this.agentUpdatesSubscription, {
974
+ kind: 'remove',
975
+ agentId,
976
+ });
977
+ }
978
+ }
979
+ async handleArchiveAgentRequest(agentId, requestId) {
980
+ this.sessionLogger.info({ agentId }, `Archiving agent ${agentId}`);
981
+ if (this.agentManager.getAgent(agentId)) {
982
+ await this.interruptAgentIfRunning(agentId);
983
+ }
984
+ const archivedAt = new Date().toISOString();
985
+ const existing = await this.agentStorage.get(agentId);
986
+ let archivedRecord = existing;
987
+ if (!archivedRecord) {
988
+ const liveAgent = this.agentManager.getAgent(agentId);
989
+ if (!liveAgent) {
990
+ throw new Error(`Agent not found: ${agentId}`);
991
+ }
992
+ await this.agentStorage.applySnapshot(liveAgent, {
993
+ internal: liveAgent.internal,
994
+ });
995
+ archivedRecord = await this.agentStorage.get(agentId);
996
+ if (!archivedRecord) {
997
+ throw new Error(`Agent not found in storage after snapshot: ${agentId}`);
998
+ }
999
+ }
1000
+ archivedRecord = {
1001
+ ...archivedRecord,
1002
+ archivedAt,
1003
+ };
1004
+ await this.agentStorage.upsert(archivedRecord);
1005
+ this.agentManager.notifyAgentState(agentId);
1006
+ this.emit({
1007
+ type: 'agent_archived',
1008
+ payload: {
1009
+ agentId,
1010
+ archivedAt,
1011
+ requestId,
1012
+ },
1013
+ });
1014
+ await this.maybeArchiveWorktreeAfterLastAgentArchived({
1015
+ archivedAgentId: agentId,
1016
+ archivedAgentCwd: archivedRecord.cwd,
1017
+ requestId,
1018
+ });
1019
+ }
1020
+ async getArchivedAt(agentId) {
1021
+ const record = await this.agentStorage.get(agentId);
1022
+ return record?.archivedAt ?? null;
1023
+ }
1024
+ async handleUpdateAgentRequest(agentId, name, labels, requestId) {
1025
+ this.sessionLogger.info({
1026
+ agentId,
1027
+ requestId,
1028
+ hasName: typeof name === 'string',
1029
+ labelCount: labels ? Object.keys(labels).length : 0,
1030
+ }, 'session: update_agent_request');
1031
+ const normalizedName = name?.trim();
1032
+ const normalizedLabels = labels && Object.keys(labels).length > 0 ? labels : undefined;
1033
+ if (!normalizedName && !normalizedLabels) {
1034
+ this.emit({
1035
+ type: 'update_agent_response',
1036
+ payload: {
1037
+ requestId,
1038
+ agentId,
1039
+ accepted: false,
1040
+ error: 'Nothing to update (provide name and/or labels)',
1041
+ },
1042
+ });
1043
+ return;
1044
+ }
1045
+ try {
1046
+ const liveAgent = this.agentManager.getAgent(agentId);
1047
+ if (liveAgent) {
1048
+ if (normalizedName) {
1049
+ await this.agentManager.setTitle(agentId, normalizedName);
1050
+ }
1051
+ if (normalizedLabels) {
1052
+ await this.agentManager.setLabels(agentId, normalizedLabels);
1053
+ }
1054
+ }
1055
+ else {
1056
+ const existing = await this.agentStorage.get(agentId);
1057
+ if (!existing) {
1058
+ throw new Error(`Agent not found: ${agentId}`);
1059
+ }
1060
+ await this.agentStorage.upsert({
1061
+ ...existing,
1062
+ ...(normalizedName ? { title: normalizedName } : {}),
1063
+ ...(normalizedLabels ? { labels: { ...existing.labels, ...normalizedLabels } } : {}),
1064
+ });
1065
+ }
1066
+ this.emit({
1067
+ type: 'update_agent_response',
1068
+ payload: { requestId, agentId, accepted: true, error: null },
1069
+ });
1070
+ }
1071
+ catch (error) {
1072
+ this.sessionLogger.error({ err: error, agentId, requestId }, 'session: update_agent_request error');
1073
+ this.emit({
1074
+ type: 'activity_log',
1075
+ payload: {
1076
+ id: uuidv4(),
1077
+ timestamp: new Date(),
1078
+ type: 'error',
1079
+ content: `Failed to update agent: ${error.message}`,
1080
+ },
1081
+ });
1082
+ this.emit({
1083
+ type: 'update_agent_response',
1084
+ payload: {
1085
+ requestId,
1086
+ agentId,
1087
+ accepted: false,
1088
+ error: error?.message ? String(error.message) : 'Failed to update agent',
1089
+ },
1090
+ });
1091
+ }
1092
+ }
1093
+ /**
1094
+ * Handle text message to agent (with optional image attachments)
1095
+ */
1096
+ async handleSendAgentMessage(agentId, text, messageId, images, runOptions) {
1097
+ 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)` : ''}`);
1098
+ try {
1099
+ await this.ensureAgentLoaded(agentId);
1100
+ }
1101
+ catch (error) {
1102
+ this.handleAgentRunError(agentId, error, 'Failed to initialize agent before sending prompt');
1103
+ return;
1104
+ }
1105
+ const archivedAt = await this.getArchivedAt(agentId);
1106
+ if (archivedAt) {
1107
+ this.handleAgentRunError(agentId, new Error(`Agent ${agentId} is archived`), 'Refusing to send prompt to archived agent');
1108
+ return;
1109
+ }
1110
+ try {
1111
+ await this.interruptAgentIfRunning(agentId);
1112
+ }
1113
+ catch (error) {
1114
+ this.handleAgentRunError(agentId, error, 'Failed to interrupt running agent before sending prompt');
1115
+ return;
1116
+ }
1117
+ const prompt = this.buildAgentPrompt(text, images);
1118
+ try {
1119
+ this.agentManager.recordUserMessage(agentId, text, {
1120
+ messageId,
1121
+ emitState: false,
1122
+ });
1123
+ }
1124
+ catch (error) {
1125
+ this.sessionLogger.error({ err: error, agentId }, `Failed to record user message for agent ${agentId}`);
1126
+ }
1127
+ this.startAgentStream(agentId, prompt, runOptions);
1128
+ }
1129
+ /**
1130
+ * Handle create agent request
1131
+ */
1132
+ async handleCreateAgentRequest(msg) {
1133
+ const { config, worktreeName, requestId, initialPrompt, clientMessageId, outputSchema, git, images, labels, } = msg;
1134
+ this.sessionLogger.info({ cwd: config.cwd, provider: config.provider, worktreeName }, `Creating agent in ${config.cwd} (${config.provider})${worktreeName ? ` with worktree ${worktreeName}` : ''}`);
1135
+ try {
1136
+ const { sessionConfig, worktreeConfig, autoWorkspaceName } = await this.buildAgentSessionConfig(config, git, worktreeName, labels);
1137
+ const mergedLabels = autoWorkspaceName
1138
+ ? { ...labels, 'junction:workspace': autoWorkspaceName }
1139
+ : labels;
1140
+ const snapshot = await this.agentManager.createAgent(sessionConfig, undefined, { labels: mergedLabels });
1141
+ await this.forwardAgentUpdate(snapshot);
1142
+ if (requestId) {
1143
+ const agentPayload = await this.getAgentPayloadById(snapshot.id);
1144
+ if (!agentPayload) {
1145
+ throw new Error(`Agent ${snapshot.id} not found after creation`);
1146
+ }
1147
+ this.emit({
1148
+ type: 'status',
1149
+ payload: {
1150
+ status: 'agent_created',
1151
+ agentId: snapshot.id,
1152
+ requestId,
1153
+ agent: agentPayload,
1154
+ },
1155
+ });
1156
+ }
1157
+ const trimmedPrompt = initialPrompt?.trim();
1158
+ if (trimmedPrompt) {
1159
+ scheduleAgentMetadataGeneration({
1160
+ agentManager: this.agentManager,
1161
+ agentId: snapshot.id,
1162
+ cwd: snapshot.cwd,
1163
+ initialPrompt: trimmedPrompt,
1164
+ explicitTitle: snapshot.config.title,
1165
+ junctionHome: this.junctionHome,
1166
+ logger: this.sessionLogger,
1167
+ });
1168
+ void this.handleSendAgentMessage(snapshot.id, trimmedPrompt, resolveClientMessageId(clientMessageId), images, outputSchema ? { outputSchema } : undefined).catch((promptError) => {
1169
+ this.sessionLogger.error({ err: promptError, agentId: snapshot.id }, `Failed to run initial prompt for agent ${snapshot.id}`);
1170
+ this.emit({
1171
+ type: 'activity_log',
1172
+ payload: {
1173
+ id: uuidv4(),
1174
+ timestamp: new Date(),
1175
+ type: 'error',
1176
+ content: `Initial prompt failed: ${promptError?.message ?? promptError}`,
1177
+ },
1178
+ });
1179
+ });
1180
+ }
1181
+ if (worktreeConfig) {
1182
+ void runAsyncWorktreeBootstrap({
1183
+ agentId: snapshot.id,
1184
+ worktree: worktreeConfig,
1185
+ terminalManager: this.terminalManager,
1186
+ appendTimelineItem: (item) => appendTimelineItemIfAgentKnown({
1187
+ agentManager: this.agentManager,
1188
+ agentId: snapshot.id,
1189
+ item,
1190
+ }),
1191
+ emitLiveTimelineItem: (item) => emitLiveTimelineItemIfAgentKnown({
1192
+ agentManager: this.agentManager,
1193
+ agentId: snapshot.id,
1194
+ item,
1195
+ }),
1196
+ logger: this.sessionLogger,
1197
+ });
1198
+ }
1199
+ this.sessionLogger.info({ agentId: snapshot.id, provider: snapshot.provider }, `Created agent ${snapshot.id} (${snapshot.provider})`);
1200
+ }
1201
+ catch (error) {
1202
+ this.sessionLogger.error({ err: error }, 'Failed to create agent');
1203
+ if (requestId) {
1204
+ this.emit({
1205
+ type: 'status',
1206
+ payload: {
1207
+ status: 'agent_create_failed',
1208
+ requestId,
1209
+ error: error?.message ?? String(error),
1210
+ },
1211
+ });
1212
+ }
1213
+ this.emit({
1214
+ type: 'activity_log',
1215
+ payload: {
1216
+ id: uuidv4(),
1217
+ timestamp: new Date(),
1218
+ type: 'error',
1219
+ content: `Failed to create agent: ${error.message}`,
1220
+ },
1221
+ });
1222
+ }
1223
+ }
1224
+ async handleResumeAgentRequest(msg) {
1225
+ const { handle, overrides, requestId } = msg;
1226
+ if (!handle) {
1227
+ this.sessionLogger.warn('Resume request missing persistence handle');
1228
+ this.emit({
1229
+ type: 'activity_log',
1230
+ payload: {
1231
+ id: uuidv4(),
1232
+ timestamp: new Date(),
1233
+ type: 'error',
1234
+ content: 'Unable to resume agent: missing persistence handle',
1235
+ },
1236
+ });
1237
+ return;
1238
+ }
1239
+ this.sessionLogger.info({ sessionId: handle.sessionId, provider: handle.provider }, `Resuming agent ${handle.sessionId} (${handle.provider})`);
1240
+ try {
1241
+ const snapshot = await this.agentManager.resumeAgentFromPersistence(handle, overrides);
1242
+ await this.agentManager.hydrateTimelineFromProvider(snapshot.id);
1243
+ await this.forwardAgentUpdate(snapshot);
1244
+ const timelineSize = this.agentManager.getTimeline(snapshot.id).length;
1245
+ if (requestId) {
1246
+ const agentPayload = await this.getAgentPayloadById(snapshot.id);
1247
+ if (!agentPayload) {
1248
+ throw new Error(`Agent ${snapshot.id} not found after resume`);
1249
+ }
1250
+ this.emit({
1251
+ type: 'status',
1252
+ payload: {
1253
+ status: 'agent_resumed',
1254
+ agentId: snapshot.id,
1255
+ requestId,
1256
+ timelineSize,
1257
+ agent: agentPayload,
1258
+ },
1259
+ });
1260
+ }
1261
+ }
1262
+ catch (error) {
1263
+ this.sessionLogger.error({ err: error }, 'Failed to resume agent');
1264
+ this.emit({
1265
+ type: 'activity_log',
1266
+ payload: {
1267
+ id: uuidv4(),
1268
+ timestamp: new Date(),
1269
+ type: 'error',
1270
+ content: `Failed to resume agent: ${error.message}`,
1271
+ },
1272
+ });
1273
+ }
1274
+ }
1275
+ async handleRefreshAgentRequest(msg) {
1276
+ const { agentId, requestId } = msg;
1277
+ this.sessionLogger.info({ agentId }, `Refreshing agent ${agentId} from persistence`);
1278
+ try {
1279
+ let snapshot;
1280
+ const existing = this.agentManager.getAgent(agentId);
1281
+ if (existing) {
1282
+ await this.interruptAgentIfRunning(agentId);
1283
+ if (existing.persistence) {
1284
+ snapshot = await this.agentManager.reloadAgentSession(agentId);
1285
+ }
1286
+ else {
1287
+ snapshot = existing;
1288
+ }
1289
+ }
1290
+ else {
1291
+ const record = await this.agentStorage.get(agentId);
1292
+ if (!record) {
1293
+ throw new Error(`Agent not found: ${agentId}`);
1294
+ }
1295
+ const handle = toAgentPersistenceHandle(this.sessionLogger, record.persistence);
1296
+ if (!handle) {
1297
+ throw new Error(`Agent ${agentId} cannot be refreshed because it lacks persistence`);
1298
+ }
1299
+ snapshot = await this.agentManager.resumeAgentFromPersistence(handle, buildConfigOverrides(record), agentId, {
1300
+ ...extractTimestamps(record),
1301
+ ...extractTimelineSnapshot(record),
1302
+ });
1303
+ }
1304
+ await this.agentManager.hydrateTimelineFromProvider(agentId);
1305
+ await this.forwardAgentUpdate(snapshot);
1306
+ const timelineSize = this.agentManager.getTimeline(agentId).length;
1307
+ if (requestId) {
1308
+ this.emit({
1309
+ type: 'status',
1310
+ payload: {
1311
+ status: 'agent_refreshed',
1312
+ agentId,
1313
+ requestId,
1314
+ timelineSize,
1315
+ },
1316
+ });
1317
+ }
1318
+ }
1319
+ catch (error) {
1320
+ this.sessionLogger.error({ err: error, agentId }, `Failed to refresh agent ${agentId}`);
1321
+ this.emit({
1322
+ type: 'activity_log',
1323
+ payload: {
1324
+ id: uuidv4(),
1325
+ timestamp: new Date(),
1326
+ type: 'error',
1327
+ content: `Failed to refresh agent: ${error.message}`,
1328
+ },
1329
+ });
1330
+ }
1331
+ }
1332
+ async handleCancelAgentRequest(agentId) {
1333
+ this.sessionLogger.info({ agentId }, `Cancel request received for agent ${agentId}`);
1334
+ try {
1335
+ await this.interruptAgentIfRunning(agentId);
1336
+ }
1337
+ catch (error) {
1338
+ this.handleAgentRunError(agentId, error, 'Failed to cancel running agent on request');
1339
+ }
1340
+ }
1341
+ async buildAgentSessionConfig(config, gitOptions, legacyWorktreeName, _labels) {
1342
+ const cwd = expandTilde(config.cwd);
1343
+ const normalized = this.normalizeGitOptions(gitOptions, legacyWorktreeName);
1344
+ let repoRoot;
1345
+ try {
1346
+ const { stdout } = await execAsync('git rev-parse --path-format=absolute --git-common-dir', {
1347
+ cwd,
1348
+ env: READ_ONLY_GIT_ENV,
1349
+ });
1350
+ const gitCommonDir = stdout.trim();
1351
+ if (!gitCommonDir) {
1352
+ throw new Error('missing git common dir');
1353
+ }
1354
+ repoRoot =
1355
+ basename(gitCommonDir) === '.git'
1356
+ ? dirname(gitCommonDir)
1357
+ : gitCommonDir;
1358
+ }
1359
+ catch {
1360
+ throw new Error('Selected project must be a git repository. Junction always creates a new worktree in .junction/.');
1361
+ }
1362
+ const baseBranch = normalized?.baseBranch ?? (await resolveBaseRef(repoRoot));
1363
+ if (!baseBranch) {
1364
+ throw new Error('Unable to determine a base branch for worktree creation');
1365
+ }
1366
+ this.sessionLogger.info({ repoRoot, baseBranch }, 'Creating in-repo worktree for new agent');
1367
+ const createdWorktree = await createInRepoWorktree({
1368
+ repoRoot,
1369
+ baseBranch,
1370
+ runSetup: false,
1371
+ });
1372
+ return {
1373
+ sessionConfig: {
1374
+ ...config,
1375
+ cwd: createdWorktree.worktreePath,
1376
+ },
1377
+ worktreeConfig: createdWorktree,
1378
+ autoWorkspaceName: createdWorktree.slug,
1379
+ };
1380
+ }
1381
+ async handleListProviderModelsRequest(msg) {
1382
+ const fetchedAt = new Date().toISOString();
1383
+ try {
1384
+ const models = await this.providerRegistry[msg.provider].fetchModels({
1385
+ cwd: msg.cwd ? expandTilde(msg.cwd) : undefined,
1386
+ });
1387
+ this.emit({
1388
+ type: 'list_provider_models_response',
1389
+ payload: {
1390
+ provider: msg.provider,
1391
+ models,
1392
+ error: null,
1393
+ fetchedAt,
1394
+ requestId: msg.requestId,
1395
+ },
1396
+ });
1397
+ }
1398
+ catch (error) {
1399
+ this.sessionLogger.error({ err: error, provider: msg.provider }, `Failed to list models for ${msg.provider}`);
1400
+ this.emit({
1401
+ type: 'list_provider_models_response',
1402
+ payload: {
1403
+ provider: msg.provider,
1404
+ error: error?.message ?? String(error),
1405
+ fetchedAt,
1406
+ requestId: msg.requestId,
1407
+ },
1408
+ });
1409
+ }
1410
+ }
1411
+ async handleListAvailableProvidersRequest(msg) {
1412
+ const fetchedAt = new Date().toISOString();
1413
+ try {
1414
+ const providers = await this.agentManager.listProviderAvailability();
1415
+ this.emit({
1416
+ type: 'list_available_providers_response',
1417
+ payload: {
1418
+ providers,
1419
+ error: null,
1420
+ fetchedAt,
1421
+ requestId: msg.requestId,
1422
+ },
1423
+ });
1424
+ }
1425
+ catch (error) {
1426
+ this.sessionLogger.error({ err: error }, 'Failed to list provider availability');
1427
+ this.emit({
1428
+ type: 'list_available_providers_response',
1429
+ payload: {
1430
+ providers: [],
1431
+ error: error?.message ?? String(error),
1432
+ fetchedAt,
1433
+ requestId: msg.requestId,
1434
+ },
1435
+ });
1436
+ }
1437
+ }
1438
+ normalizeGitOptions(gitOptions, legacyWorktreeName) {
1439
+ const fallbackOptions = legacyWorktreeName
1440
+ ? {
1441
+ createWorktree: true,
1442
+ createNewBranch: true,
1443
+ newBranchName: legacyWorktreeName,
1444
+ worktreeSlug: legacyWorktreeName,
1445
+ }
1446
+ : undefined;
1447
+ const merged = gitOptions ?? fallbackOptions;
1448
+ if (!merged) {
1449
+ return null;
1450
+ }
1451
+ const baseBranch = merged.baseBranch?.trim() || undefined;
1452
+ const createWorktree = Boolean(merged.createWorktree);
1453
+ const createNewBranch = Boolean(merged.createNewBranch);
1454
+ const normalizedBranchName = merged.newBranchName ? slugify(merged.newBranchName) : undefined;
1455
+ const normalizedWorktreeSlug = merged.worktreeSlug
1456
+ ? slugify(merged.worktreeSlug)
1457
+ : normalizedBranchName;
1458
+ if (!createWorktree && !createNewBranch && !baseBranch) {
1459
+ return null;
1460
+ }
1461
+ if (baseBranch) {
1462
+ this.assertSafeGitRef(baseBranch, 'base branch');
1463
+ }
1464
+ if (createWorktree && !baseBranch) {
1465
+ throw new Error('Base branch is required when creating a worktree');
1466
+ }
1467
+ if (createNewBranch && !baseBranch) {
1468
+ throw new Error('Base branch is required when creating a new branch');
1469
+ }
1470
+ if (createNewBranch) {
1471
+ if (!normalizedBranchName) {
1472
+ throw new Error('New branch name is required');
1473
+ }
1474
+ const validation = validateBranchSlug(normalizedBranchName);
1475
+ if (!validation.valid) {
1476
+ throw new Error(`Invalid branch name: ${validation.error}`);
1477
+ }
1478
+ }
1479
+ if (normalizedWorktreeSlug) {
1480
+ const validation = validateBranchSlug(normalizedWorktreeSlug);
1481
+ if (!validation.valid) {
1482
+ throw new Error(`Invalid worktree name: ${validation.error}`);
1483
+ }
1484
+ }
1485
+ return {
1486
+ baseBranch,
1487
+ createNewBranch,
1488
+ newBranchName: normalizedBranchName,
1489
+ createWorktree,
1490
+ worktreeSlug: normalizedWorktreeSlug,
1491
+ };
1492
+ }
1493
+ assertSafeGitRef(ref, label) {
1494
+ if (!SAFE_GIT_REF_PATTERN.test(ref) || ref.includes('..') || ref.includes('@{')) {
1495
+ throw new Error(`Invalid ${label}: ${ref}`);
1496
+ }
1497
+ }
1498
+ toCheckoutError(error) {
1499
+ if (error instanceof NotGitRepoError) {
1500
+ return { code: 'NOT_GIT_REPO', message: error.message };
1501
+ }
1502
+ if (error instanceof MergeConflictError) {
1503
+ return { code: 'MERGE_CONFLICT', message: error.message };
1504
+ }
1505
+ if (error instanceof MergeFromBaseConflictError) {
1506
+ return { code: 'MERGE_CONFLICT', message: error.message };
1507
+ }
1508
+ if (error instanceof Error) {
1509
+ return { code: 'UNKNOWN', message: error.message };
1510
+ }
1511
+ return { code: 'UNKNOWN', message: String(error) };
1512
+ }
1513
+ isPathWithinRoot(rootPath, candidatePath) {
1514
+ const resolvedRoot = resolve(rootPath);
1515
+ const resolvedCandidate = resolve(candidatePath);
1516
+ if (resolvedCandidate === resolvedRoot) {
1517
+ return true;
1518
+ }
1519
+ return resolvedCandidate.startsWith(resolvedRoot + sep);
1520
+ }
1521
+ async generateCommitMessage(cwd) {
1522
+ const diff = await getCheckoutDiff(cwd, { mode: 'uncommitted', includeStructured: true }, { junctionHome: this.junctionHome });
1523
+ const schema = z.object({
1524
+ message: z
1525
+ .string()
1526
+ .min(1)
1527
+ .max(72)
1528
+ .describe('Concise git commit message, imperative mood, no trailing period.'),
1529
+ });
1530
+ const fileList = diff.structured && diff.structured.length > 0
1531
+ ? [
1532
+ 'Files changed:',
1533
+ ...diff.structured.map((file) => {
1534
+ const changeType = file.isNew ? 'A' : file.isDeleted ? 'D' : 'M';
1535
+ const status = file.status && file.status !== 'ok' ? ` [${file.status}]` : '';
1536
+ return `${changeType}\t${file.path}\t(+${file.additions} -${file.deletions})${status}`;
1537
+ }),
1538
+ ].join('\n')
1539
+ : 'Files changed: (unknown)';
1540
+ const maxPatchChars = 120000;
1541
+ const patch = diff.diff.length > maxPatchChars
1542
+ ? `${diff.diff.slice(0, maxPatchChars)}\n\n... (diff truncated to ${maxPatchChars} chars)\n`
1543
+ : diff.diff;
1544
+ const prompt = [
1545
+ 'Write a concise git commit message for the changes below.',
1546
+ "Return JSON only with a single field 'message'.",
1547
+ '',
1548
+ fileList,
1549
+ '',
1550
+ patch.length > 0 ? patch : '(No diff available)',
1551
+ ].join('\n');
1552
+ try {
1553
+ const result = await generateStructuredAgentResponseWithFallback({
1554
+ manager: this.agentManager,
1555
+ cwd,
1556
+ prompt,
1557
+ schema,
1558
+ schemaName: 'CommitMessage',
1559
+ maxRetries: 2,
1560
+ providers: DEFAULT_STRUCTURED_GENERATION_PROVIDERS,
1561
+ agentConfigOverrides: {
1562
+ title: 'Commit generator',
1563
+ internal: true,
1564
+ },
1565
+ });
1566
+ return result.message;
1567
+ }
1568
+ catch (error) {
1569
+ if (error instanceof StructuredAgentResponseError ||
1570
+ error instanceof StructuredAgentFallbackError) {
1571
+ return 'Update files';
1572
+ }
1573
+ throw error;
1574
+ }
1575
+ }
1576
+ async generatePullRequestText(cwd, baseRef) {
1577
+ const diff = await getCheckoutDiff(cwd, {
1578
+ mode: 'base',
1579
+ baseRef,
1580
+ includeStructured: true,
1581
+ }, { junctionHome: this.junctionHome });
1582
+ const schema = z.object({
1583
+ title: z.string().min(1).max(72),
1584
+ body: z.string().min(1),
1585
+ });
1586
+ const fileList = diff.structured && diff.structured.length > 0
1587
+ ? [
1588
+ 'Files changed:',
1589
+ ...diff.structured.map((file) => {
1590
+ const changeType = file.isNew ? 'A' : file.isDeleted ? 'D' : 'M';
1591
+ const status = file.status && file.status !== 'ok' ? ` [${file.status}]` : '';
1592
+ return `${changeType}\t${file.path}\t(+${file.additions} -${file.deletions})${status}`;
1593
+ }),
1594
+ ].join('\n')
1595
+ : 'Files changed: (unknown)';
1596
+ const maxPatchChars = 200000;
1597
+ const patch = diff.diff.length > maxPatchChars
1598
+ ? `${diff.diff.slice(0, maxPatchChars)}\n\n... (diff truncated to ${maxPatchChars} chars)\n`
1599
+ : diff.diff;
1600
+ const prompt = [
1601
+ 'Write a pull request title and body for the changes below.',
1602
+ "Return JSON only with fields 'title' and 'body'.",
1603
+ '',
1604
+ fileList,
1605
+ '',
1606
+ patch.length > 0 ? patch : '(No diff available)',
1607
+ ].join('\n');
1608
+ try {
1609
+ return await generateStructuredAgentResponseWithFallback({
1610
+ manager: this.agentManager,
1611
+ cwd,
1612
+ prompt,
1613
+ schema,
1614
+ schemaName: 'PullRequest',
1615
+ maxRetries: 2,
1616
+ providers: DEFAULT_STRUCTURED_GENERATION_PROVIDERS,
1617
+ agentConfigOverrides: {
1618
+ title: 'PR generator',
1619
+ internal: true,
1620
+ },
1621
+ });
1622
+ }
1623
+ catch (error) {
1624
+ if (error instanceof StructuredAgentResponseError ||
1625
+ error instanceof StructuredAgentFallbackError) {
1626
+ return {
1627
+ title: 'Update changes',
1628
+ body: 'Automated PR generated by Junction.',
1629
+ };
1630
+ }
1631
+ throw error;
1632
+ }
1633
+ }
1634
+ /**
1635
+ * Handle set agent mode request
1636
+ */
1637
+ async handleSetAgentModeRequest(agentId, modeId, requestId) {
1638
+ this.sessionLogger.info({ agentId, modeId, requestId }, 'session: set_agent_mode_request');
1639
+ try {
1640
+ await this.agentManager.setAgentMode(agentId, modeId);
1641
+ this.sessionLogger.info({ agentId, modeId, requestId }, 'session: set_agent_mode_request success');
1642
+ this.emit({
1643
+ type: 'set_agent_mode_response',
1644
+ payload: { requestId, agentId, accepted: true, error: null },
1645
+ });
1646
+ }
1647
+ catch (error) {
1648
+ this.sessionLogger.error({ err: error, agentId, modeId, requestId }, 'session: set_agent_mode_request error');
1649
+ this.emit({
1650
+ type: 'activity_log',
1651
+ payload: {
1652
+ id: uuidv4(),
1653
+ timestamp: new Date(),
1654
+ type: 'error',
1655
+ content: `Failed to set agent mode: ${error.message}`,
1656
+ },
1657
+ });
1658
+ this.emit({
1659
+ type: 'set_agent_mode_response',
1660
+ payload: {
1661
+ requestId,
1662
+ agentId,
1663
+ accepted: false,
1664
+ error: error?.message ? String(error.message) : 'Failed to set agent mode',
1665
+ },
1666
+ });
1667
+ }
1668
+ }
1669
+ async handleSetAgentModelRequest(agentId, modelId, requestId) {
1670
+ this.sessionLogger.info({ agentId, modelId, requestId }, 'session: set_agent_model_request');
1671
+ try {
1672
+ await this.agentManager.setAgentModel(agentId, modelId);
1673
+ this.sessionLogger.info({ agentId, modelId, requestId }, 'session: set_agent_model_request success');
1674
+ this.emit({
1675
+ type: 'set_agent_model_response',
1676
+ payload: { requestId, agentId, accepted: true, error: null },
1677
+ });
1678
+ }
1679
+ catch (error) {
1680
+ this.sessionLogger.error({ err: error, agentId, modelId, requestId }, 'session: set_agent_model_request error');
1681
+ this.emit({
1682
+ type: 'activity_log',
1683
+ payload: {
1684
+ id: uuidv4(),
1685
+ timestamp: new Date(),
1686
+ type: 'error',
1687
+ content: `Failed to set agent model: ${error.message}`,
1688
+ },
1689
+ });
1690
+ this.emit({
1691
+ type: 'set_agent_model_response',
1692
+ payload: {
1693
+ requestId,
1694
+ agentId,
1695
+ accepted: false,
1696
+ error: error?.message ? String(error.message) : 'Failed to set agent model',
1697
+ },
1698
+ });
1699
+ }
1700
+ }
1701
+ async handleSetAgentThinkingRequest(agentId, thinkingOptionId, requestId) {
1702
+ this.sessionLogger.info({ agentId, thinkingOptionId, requestId }, 'session: set_agent_thinking_request');
1703
+ try {
1704
+ await this.agentManager.setAgentThinkingOption(agentId, thinkingOptionId);
1705
+ this.sessionLogger.info({ agentId, thinkingOptionId, requestId }, 'session: set_agent_thinking_request success');
1706
+ this.emit({
1707
+ type: 'set_agent_thinking_response',
1708
+ payload: { requestId, agentId, accepted: true, error: null },
1709
+ });
1710
+ }
1711
+ catch (error) {
1712
+ this.sessionLogger.error({ err: error, agentId, thinkingOptionId, requestId }, 'session: set_agent_thinking_request error');
1713
+ this.emit({
1714
+ type: 'activity_log',
1715
+ payload: {
1716
+ id: uuidv4(),
1717
+ timestamp: new Date(),
1718
+ type: 'error',
1719
+ content: `Failed to set agent thinking option: ${error.message}`,
1720
+ },
1721
+ });
1722
+ this.emit({
1723
+ type: 'set_agent_thinking_response',
1724
+ payload: {
1725
+ requestId,
1726
+ agentId,
1727
+ accepted: false,
1728
+ error: error?.message ? String(error.message) : 'Failed to set agent thinking option',
1729
+ },
1730
+ });
1731
+ }
1732
+ }
1733
+ /**
1734
+ * Handle clearing agent attention flag
1735
+ */
1736
+ async handleClearAgentAttention(agentId) {
1737
+ const agentIds = Array.isArray(agentId) ? agentId : [agentId];
1738
+ try {
1739
+ await Promise.all(agentIds.map((id) => this.agentManager.clearAgentAttention(id)));
1740
+ }
1741
+ catch (error) {
1742
+ this.sessionLogger.error({ err: error, agentIds }, 'Failed to clear agent attention');
1743
+ // Don't throw - this is not critical
1744
+ }
1745
+ }
1746
+ /**
1747
+ * Handle client heartbeat for activity tracking
1748
+ */
1749
+ handleClientHeartbeat(msg) {
1750
+ const appVisibilityChangedAt = msg.appVisibilityChangedAt
1751
+ ? new Date(msg.appVisibilityChangedAt)
1752
+ : new Date(msg.lastActivityAt);
1753
+ this.clientActivity = {
1754
+ deviceType: msg.deviceType,
1755
+ focusedAgentId: msg.focusedAgentId,
1756
+ lastActivityAt: new Date(msg.lastActivityAt),
1757
+ appVisible: msg.appVisible,
1758
+ appVisibilityChangedAt,
1759
+ };
1760
+ }
1761
+ /**
1762
+ * Handle list commands request for an agent
1763
+ */
1764
+ async handleListCommandsRequest(msg) {
1765
+ const { agentId, requestId, draftConfig } = msg;
1766
+ this.sessionLogger.debug({ agentId, draftConfig }, `Handling list commands request for agent ${agentId}`);
1767
+ try {
1768
+ const agents = this.agentManager.listAgents();
1769
+ const agent = agents.find((a) => a.id === agentId);
1770
+ if (agent?.session?.listCommands) {
1771
+ const commands = await agent.session.listCommands();
1772
+ this.emit({
1773
+ type: 'list_commands_response',
1774
+ payload: {
1775
+ agentId,
1776
+ commands,
1777
+ error: null,
1778
+ requestId,
1779
+ },
1780
+ });
1781
+ return;
1782
+ }
1783
+ if (!agent && draftConfig) {
1784
+ const sessionConfig = {
1785
+ provider: draftConfig.provider,
1786
+ cwd: expandTilde(draftConfig.cwd),
1787
+ ...(draftConfig.modeId ? { modeId: draftConfig.modeId } : {}),
1788
+ ...(draftConfig.model ? { model: draftConfig.model } : {}),
1789
+ ...(draftConfig.thinkingOptionId
1790
+ ? { thinkingOptionId: draftConfig.thinkingOptionId }
1791
+ : {}),
1792
+ };
1793
+ const commands = await this.agentManager.listDraftCommands(sessionConfig);
1794
+ this.emit({
1795
+ type: 'list_commands_response',
1796
+ payload: {
1797
+ agentId,
1798
+ commands,
1799
+ error: null,
1800
+ requestId,
1801
+ },
1802
+ });
1803
+ return;
1804
+ }
1805
+ this.emit({
1806
+ type: 'list_commands_response',
1807
+ payload: {
1808
+ agentId,
1809
+ commands: [],
1810
+ error: agent ? `Agent does not support listing commands` : `Agent not found: ${agentId}`,
1811
+ requestId,
1812
+ },
1813
+ });
1814
+ }
1815
+ catch (error) {
1816
+ this.sessionLogger.error({ err: error, agentId, draftConfig }, 'Failed to list commands');
1817
+ this.emit({
1818
+ type: 'list_commands_response',
1819
+ payload: {
1820
+ agentId,
1821
+ commands: [],
1822
+ error: error.message,
1823
+ requestId,
1824
+ },
1825
+ });
1826
+ }
1827
+ }
1828
+ /**
1829
+ * Handle agent permission response from user
1830
+ */
1831
+ async handleAgentPermissionResponse(agentId, requestId, response) {
1832
+ this.sessionLogger.debug({ agentId, requestId }, `Handling permission response for agent ${agentId}, request ${requestId}`);
1833
+ try {
1834
+ await this.agentManager.respondToPermission(agentId, requestId, response);
1835
+ this.sessionLogger.debug({ agentId }, `Permission response forwarded to agent ${agentId}`);
1836
+ }
1837
+ catch (error) {
1838
+ this.sessionLogger.error({ err: error, agentId, requestId }, 'Failed to respond to permission');
1839
+ this.emit({
1840
+ type: 'activity_log',
1841
+ payload: {
1842
+ id: uuidv4(),
1843
+ timestamp: new Date(),
1844
+ type: 'error',
1845
+ content: `Failed to respond to permission: ${error.message}`,
1846
+ },
1847
+ });
1848
+ throw error;
1849
+ }
1850
+ }
1851
+ async handleCheckoutStatusRequest(msg) {
1852
+ const { cwd, requestId } = msg;
1853
+ const resolvedCwd = expandTilde(cwd);
1854
+ try {
1855
+ const status = await getCheckoutStatus(resolvedCwd, { junctionHome: this.junctionHome });
1856
+ if (!status.isGit) {
1857
+ this.emit({
1858
+ type: 'checkout_status_response',
1859
+ payload: {
1860
+ cwd,
1861
+ isGit: false,
1862
+ repoRoot: null,
1863
+ currentBranch: null,
1864
+ isDirty: null,
1865
+ baseRef: null,
1866
+ aheadBehind: null,
1867
+ aheadOfOrigin: null,
1868
+ behindOfOrigin: null,
1869
+ hasRemote: false,
1870
+ remoteUrl: null,
1871
+ isJunctionOwnedWorktree: false,
1872
+ error: null,
1873
+ requestId,
1874
+ },
1875
+ });
1876
+ return;
1877
+ }
1878
+ if (status.isJunctionOwnedWorktree) {
1879
+ this.emit({
1880
+ type: 'checkout_status_response',
1881
+ payload: {
1882
+ cwd,
1883
+ isGit: true,
1884
+ repoRoot: status.repoRoot ?? null,
1885
+ mainRepoRoot: status.mainRepoRoot,
1886
+ currentBranch: status.currentBranch ?? null,
1887
+ isDirty: status.isDirty ?? null,
1888
+ baseRef: status.baseRef,
1889
+ aheadBehind: status.aheadBehind ?? null,
1890
+ aheadOfOrigin: status.aheadOfOrigin ?? null,
1891
+ behindOfOrigin: status.behindOfOrigin ?? null,
1892
+ hasRemote: status.hasRemote,
1893
+ remoteUrl: status.remoteUrl,
1894
+ isJunctionOwnedWorktree: true,
1895
+ error: null,
1896
+ requestId,
1897
+ },
1898
+ });
1899
+ return;
1900
+ }
1901
+ this.emit({
1902
+ type: 'checkout_status_response',
1903
+ payload: {
1904
+ cwd,
1905
+ isGit: true,
1906
+ repoRoot: status.repoRoot ?? null,
1907
+ currentBranch: status.currentBranch ?? null,
1908
+ isDirty: status.isDirty ?? null,
1909
+ baseRef: status.baseRef ?? null,
1910
+ aheadBehind: status.aheadBehind ?? null,
1911
+ aheadOfOrigin: status.aheadOfOrigin ?? null,
1912
+ behindOfOrigin: status.behindOfOrigin ?? null,
1913
+ hasRemote: status.hasRemote,
1914
+ remoteUrl: status.remoteUrl,
1915
+ isJunctionOwnedWorktree: false,
1916
+ error: null,
1917
+ requestId,
1918
+ },
1919
+ });
1920
+ }
1921
+ catch (error) {
1922
+ this.emit({
1923
+ type: 'checkout_status_response',
1924
+ payload: {
1925
+ cwd,
1926
+ isGit: false,
1927
+ repoRoot: null,
1928
+ currentBranch: null,
1929
+ isDirty: null,
1930
+ baseRef: null,
1931
+ aheadBehind: null,
1932
+ aheadOfOrigin: null,
1933
+ behindOfOrigin: null,
1934
+ hasRemote: false,
1935
+ remoteUrl: null,
1936
+ isJunctionOwnedWorktree: false,
1937
+ error: this.toCheckoutError(error),
1938
+ requestId,
1939
+ },
1940
+ });
1941
+ }
1942
+ }
1943
+ async handleValidateBranchRequest(msg) {
1944
+ const { cwd, branchName, requestId } = msg;
1945
+ try {
1946
+ const resolvedCwd = expandTilde(cwd);
1947
+ // Try local branch first
1948
+ try {
1949
+ await execAsync(`git rev-parse --verify ${branchName}`, {
1950
+ cwd: resolvedCwd,
1951
+ env: READ_ONLY_GIT_ENV,
1952
+ });
1953
+ this.emit({
1954
+ type: 'validate_branch_response',
1955
+ payload: {
1956
+ exists: true,
1957
+ resolvedRef: branchName,
1958
+ isRemote: false,
1959
+ error: null,
1960
+ requestId,
1961
+ },
1962
+ });
1963
+ return;
1964
+ }
1965
+ catch {
1966
+ // Local branch doesn't exist, try remote
1967
+ }
1968
+ // Try remote branch (origin/{branchName})
1969
+ try {
1970
+ await execAsync(`git rev-parse --verify origin/${branchName}`, {
1971
+ cwd: resolvedCwd,
1972
+ env: READ_ONLY_GIT_ENV,
1973
+ });
1974
+ this.emit({
1975
+ type: 'validate_branch_response',
1976
+ payload: {
1977
+ exists: true,
1978
+ resolvedRef: `origin/${branchName}`,
1979
+ isRemote: true,
1980
+ error: null,
1981
+ requestId,
1982
+ },
1983
+ });
1984
+ return;
1985
+ }
1986
+ catch {
1987
+ // Remote branch doesn't exist either
1988
+ }
1989
+ // Branch not found anywhere
1990
+ this.emit({
1991
+ type: 'validate_branch_response',
1992
+ payload: {
1993
+ exists: false,
1994
+ resolvedRef: null,
1995
+ isRemote: false,
1996
+ error: null,
1997
+ requestId,
1998
+ },
1999
+ });
2000
+ }
2001
+ catch (error) {
2002
+ this.emit({
2003
+ type: 'validate_branch_response',
2004
+ payload: {
2005
+ exists: false,
2006
+ resolvedRef: null,
2007
+ isRemote: false,
2008
+ error: error instanceof Error ? error.message : String(error),
2009
+ requestId,
2010
+ },
2011
+ });
2012
+ }
2013
+ }
2014
+ async handleBranchSuggestionsRequest(msg) {
2015
+ const { cwd, query, limit, requestId } = msg;
2016
+ try {
2017
+ const resolvedCwd = expandTilde(cwd);
2018
+ const branches = await listBranchSuggestions(resolvedCwd, { query, limit });
2019
+ this.emit({
2020
+ type: 'branch_suggestions_response',
2021
+ payload: {
2022
+ branches,
2023
+ error: null,
2024
+ requestId,
2025
+ },
2026
+ });
2027
+ }
2028
+ catch (error) {
2029
+ this.emit({
2030
+ type: 'branch_suggestions_response',
2031
+ payload: {
2032
+ branches: [],
2033
+ error: error instanceof Error ? error.message : String(error),
2034
+ requestId,
2035
+ },
2036
+ });
2037
+ }
2038
+ }
2039
+ async handleDirectorySuggestionsRequest(msg) {
2040
+ const { query, limit, requestId, cwd, includeFiles, includeDirectories, onlyGitRepos } = msg;
2041
+ try {
2042
+ const workspaceCwd = cwd?.trim();
2043
+ let entries;
2044
+ if (workspaceCwd) {
2045
+ entries = await searchWorkspaceEntries({
2046
+ cwd: expandTilde(workspaceCwd),
2047
+ query,
2048
+ limit,
2049
+ includeFiles,
2050
+ includeDirectories,
2051
+ });
2052
+ }
2053
+ else if (onlyGitRepos) {
2054
+ entries = await searchGitRepositories({
2055
+ homeDir: process.env.HOME ?? homedir(),
2056
+ query,
2057
+ limit,
2058
+ });
2059
+ }
2060
+ else {
2061
+ const dirs = await searchHomeDirectories({
2062
+ homeDir: process.env.HOME ?? homedir(),
2063
+ query,
2064
+ limit,
2065
+ });
2066
+ entries = await Promise.all(dirs.map(async (dirPath) => ({
2067
+ path: dirPath,
2068
+ kind: 'directory',
2069
+ isGitRepo: await checkIsGitRepo(dirPath),
2070
+ })));
2071
+ }
2072
+ const directories = entries
2073
+ .filter((entry) => entry.kind === 'directory')
2074
+ .map((entry) => entry.path);
2075
+ this.emit({
2076
+ type: 'directory_suggestions_response',
2077
+ payload: {
2078
+ directories,
2079
+ entries,
2080
+ error: null,
2081
+ requestId,
2082
+ },
2083
+ });
2084
+ }
2085
+ catch (error) {
2086
+ this.emit({
2087
+ type: 'directory_suggestions_response',
2088
+ payload: {
2089
+ directories: [],
2090
+ entries: [],
2091
+ error: error instanceof Error ? error.message : String(error),
2092
+ requestId,
2093
+ },
2094
+ });
2095
+ }
2096
+ }
2097
+ async handleGitCloneRequest(msg) {
2098
+ const { url, targetDirectory, requestId } = msg;
2099
+ try {
2100
+ const result = await cloneRepository({
2101
+ url,
2102
+ targetDirectory: expandTilde(targetDirectory),
2103
+ homeDir: process.env.HOME ?? homedir(),
2104
+ });
2105
+ this.emit({
2106
+ type: 'git_clone_response',
2107
+ payload: {
2108
+ clonedPath: result.clonedPath,
2109
+ error: null,
2110
+ requestId,
2111
+ },
2112
+ });
2113
+ }
2114
+ catch (error) {
2115
+ this.emit({
2116
+ type: 'git_clone_response',
2117
+ payload: {
2118
+ clonedPath: null,
2119
+ error: error instanceof Error ? error.message : String(error),
2120
+ requestId,
2121
+ },
2122
+ });
2123
+ }
2124
+ }
2125
+ async handleGitInitRequest(msg) {
2126
+ const { targetDirectory, projectName, requestId } = msg;
2127
+ try {
2128
+ const result = await initRepository({
2129
+ targetDirectory: expandTilde(targetDirectory),
2130
+ projectName,
2131
+ homeDir: process.env.HOME ?? homedir(),
2132
+ });
2133
+ this.emit({
2134
+ type: 'git_init_response',
2135
+ payload: {
2136
+ createdPath: result.createdPath,
2137
+ error: null,
2138
+ requestId,
2139
+ },
2140
+ });
2141
+ }
2142
+ catch (error) {
2143
+ this.emit({
2144
+ type: 'git_init_response',
2145
+ payload: {
2146
+ createdPath: null,
2147
+ error: error instanceof Error ? error.message : String(error),
2148
+ requestId,
2149
+ },
2150
+ });
2151
+ }
2152
+ }
2153
+ normalizeCheckoutDiffCompare(compare) {
2154
+ if (compare.mode === 'uncommitted' || compare.mode === 'staged' || compare.mode === 'unstaged') {
2155
+ return { mode: compare.mode };
2156
+ }
2157
+ const trimmedBaseRef = compare.baseRef?.trim();
2158
+ return trimmedBaseRef ? { mode: 'base', baseRef: trimmedBaseRef } : { mode: 'base' };
2159
+ }
2160
+ buildCheckoutDiffTargetKey(cwd, compare) {
2161
+ return JSON.stringify([
2162
+ cwd,
2163
+ compare.mode,
2164
+ compare.mode === 'base' ? (compare.baseRef ?? '') : '',
2165
+ ]);
2166
+ }
2167
+ closeCheckoutDiffWatchTarget(target) {
2168
+ if (target.debounceTimer) {
2169
+ clearTimeout(target.debounceTimer);
2170
+ target.debounceTimer = null;
2171
+ }
2172
+ if (target.fallbackRefreshInterval) {
2173
+ clearInterval(target.fallbackRefreshInterval);
2174
+ target.fallbackRefreshInterval = null;
2175
+ }
2176
+ for (const watcher of target.watchers) {
2177
+ watcher.close();
2178
+ }
2179
+ target.watchers = [];
2180
+ }
2181
+ removeCheckoutDiffSubscription(subscriptionId) {
2182
+ const subscription = this.checkoutDiffSubscriptions.get(subscriptionId);
2183
+ if (!subscription) {
2184
+ return;
2185
+ }
2186
+ this.checkoutDiffSubscriptions.delete(subscriptionId);
2187
+ const target = this.checkoutDiffTargets.get(subscription.targetKey);
2188
+ if (!target) {
2189
+ return;
2190
+ }
2191
+ target.subscriptions.delete(subscriptionId);
2192
+ if (target.subscriptions.size === 0) {
2193
+ this.closeCheckoutDiffWatchTarget(target);
2194
+ this.checkoutDiffTargets.delete(subscription.targetKey);
2195
+ }
2196
+ }
2197
+ async resolveCheckoutGitDir(cwd) {
2198
+ try {
2199
+ const { stdout } = await execAsync('git rev-parse --absolute-git-dir', {
2200
+ cwd,
2201
+ env: READ_ONLY_GIT_ENV,
2202
+ });
2203
+ const gitDir = stdout.trim();
2204
+ return gitDir.length > 0 ? gitDir : null;
2205
+ }
2206
+ catch {
2207
+ return null;
2208
+ }
2209
+ }
2210
+ async resolveCheckoutWatchRoot(cwd) {
2211
+ try {
2212
+ const { stdout } = await execAsync('git rev-parse --path-format=absolute --show-toplevel', {
2213
+ cwd,
2214
+ env: READ_ONLY_GIT_ENV,
2215
+ });
2216
+ const root = stdout.trim();
2217
+ return root.length > 0 ? root : null;
2218
+ }
2219
+ catch {
2220
+ return null;
2221
+ }
2222
+ }
2223
+ scheduleCheckoutDiffTargetRefresh(target) {
2224
+ if (target.debounceTimer) {
2225
+ clearTimeout(target.debounceTimer);
2226
+ }
2227
+ target.debounceTimer = setTimeout(() => {
2228
+ target.debounceTimer = null;
2229
+ void this.refreshCheckoutDiffTarget(target);
2230
+ }, CHECKOUT_DIFF_WATCH_DEBOUNCE_MS);
2231
+ }
2232
+ emitCheckoutDiffUpdate(target, snapshot) {
2233
+ if (target.subscriptions.size === 0) {
2234
+ return;
2235
+ }
2236
+ for (const subscriptionId of target.subscriptions) {
2237
+ this.emit({
2238
+ type: 'checkout_diff_update',
2239
+ payload: {
2240
+ subscriptionId,
2241
+ ...snapshot,
2242
+ },
2243
+ });
2244
+ }
2245
+ }
2246
+ checkoutDiffSnapshotFingerprint(snapshot) {
2247
+ return JSON.stringify(snapshot);
2248
+ }
2249
+ async computeCheckoutDiffSnapshot(cwd, compare, options) {
2250
+ const diffCwd = options?.diffCwd ?? cwd;
2251
+ try {
2252
+ const diffResult = await getCheckoutDiff(diffCwd, {
2253
+ mode: compare.mode,
2254
+ baseRef: compare.baseRef,
2255
+ includeStructured: true,
2256
+ }, { junctionHome: this.junctionHome });
2257
+ const files = [...(diffResult.structured ?? [])];
2258
+ files.sort((a, b) => {
2259
+ if (a.path === b.path)
2260
+ return 0;
2261
+ return a.path < b.path ? -1 : 1;
2262
+ });
2263
+ return {
2264
+ cwd,
2265
+ files,
2266
+ error: null,
2267
+ };
2268
+ }
2269
+ catch (error) {
2270
+ return {
2271
+ cwd,
2272
+ files: [],
2273
+ error: this.toCheckoutError(error),
2274
+ };
2275
+ }
2276
+ }
2277
+ async refreshCheckoutDiffTarget(target) {
2278
+ if (target.refreshPromise) {
2279
+ target.refreshQueued = true;
2280
+ return;
2281
+ }
2282
+ target.refreshPromise = (async () => {
2283
+ do {
2284
+ target.refreshQueued = false;
2285
+ const snapshot = await this.computeCheckoutDiffSnapshot(target.cwd, target.compare, {
2286
+ diffCwd: target.diffCwd,
2287
+ });
2288
+ target.latestPayload = snapshot;
2289
+ const fingerprint = this.checkoutDiffSnapshotFingerprint(snapshot);
2290
+ if (fingerprint !== target.latestFingerprint) {
2291
+ target.latestFingerprint = fingerprint;
2292
+ this.emitCheckoutDiffUpdate(target, snapshot);
2293
+ }
2294
+ } while (target.refreshQueued);
2295
+ })();
2296
+ try {
2297
+ await target.refreshPromise;
2298
+ }
2299
+ finally {
2300
+ target.refreshPromise = null;
2301
+ }
2302
+ }
2303
+ async ensureCheckoutDiffWatchTarget(cwd, compare) {
2304
+ const targetKey = this.buildCheckoutDiffTargetKey(cwd, compare);
2305
+ const existing = this.checkoutDiffTargets.get(targetKey);
2306
+ if (existing) {
2307
+ return existing;
2308
+ }
2309
+ const watchRoot = await this.resolveCheckoutWatchRoot(cwd);
2310
+ const target = {
2311
+ key: targetKey,
2312
+ cwd,
2313
+ diffCwd: watchRoot ?? cwd,
2314
+ compare,
2315
+ subscriptions: new Set(),
2316
+ watchers: [],
2317
+ fallbackRefreshInterval: null,
2318
+ debounceTimer: null,
2319
+ refreshPromise: null,
2320
+ refreshQueued: false,
2321
+ latestPayload: null,
2322
+ latestFingerprint: null,
2323
+ };
2324
+ const repoWatchPath = watchRoot ?? cwd;
2325
+ const watchPaths = new Set([repoWatchPath]);
2326
+ const gitDir = await this.resolveCheckoutGitDir(cwd);
2327
+ if (gitDir) {
2328
+ watchPaths.add(gitDir);
2329
+ }
2330
+ let hasRecursiveRepoCoverage = false;
2331
+ const allowRecursiveRepoWatch = process.platform !== 'linux';
2332
+ for (const watchPath of watchPaths) {
2333
+ const shouldTryRecursive = watchPath === repoWatchPath && allowRecursiveRepoWatch;
2334
+ const createWatcher = (recursive) => watch(watchPath, { recursive }, () => {
2335
+ this.scheduleCheckoutDiffTargetRefresh(target);
2336
+ });
2337
+ let watcher = null;
2338
+ let watcherIsRecursive = false;
2339
+ try {
2340
+ if (shouldTryRecursive) {
2341
+ watcher = createWatcher(true);
2342
+ watcherIsRecursive = true;
2343
+ }
2344
+ else {
2345
+ watcher = createWatcher(false);
2346
+ }
2347
+ }
2348
+ catch (error) {
2349
+ if (shouldTryRecursive) {
2350
+ try {
2351
+ watcher = createWatcher(false);
2352
+ this.sessionLogger.warn({ err: error, watchPath, cwd, compare }, 'Checkout diff recursive watch unavailable; using non-recursive fallback');
2353
+ }
2354
+ catch (fallbackError) {
2355
+ this.sessionLogger.warn({ err: fallbackError, watchPath, cwd, compare }, 'Failed to start checkout diff watcher');
2356
+ }
2357
+ }
2358
+ else {
2359
+ this.sessionLogger.warn({ err: error, watchPath, cwd, compare }, 'Failed to start checkout diff watcher');
2360
+ }
2361
+ }
2362
+ if (!watcher) {
2363
+ continue;
2364
+ }
2365
+ watcher.on('error', (error) => {
2366
+ this.sessionLogger.warn({ err: error, watchPath, cwd, compare }, 'Checkout diff watcher error');
2367
+ });
2368
+ target.watchers.push(watcher);
2369
+ if (watchPath === repoWatchPath && watcherIsRecursive) {
2370
+ hasRecursiveRepoCoverage = true;
2371
+ }
2372
+ }
2373
+ const missingRepoCoverage = !hasRecursiveRepoCoverage;
2374
+ if (target.watchers.length === 0 || missingRepoCoverage) {
2375
+ target.fallbackRefreshInterval = setInterval(() => {
2376
+ this.scheduleCheckoutDiffTargetRefresh(target);
2377
+ }, CHECKOUT_DIFF_FALLBACK_REFRESH_MS);
2378
+ this.sessionLogger.warn({
2379
+ cwd,
2380
+ compare,
2381
+ intervalMs: CHECKOUT_DIFF_FALLBACK_REFRESH_MS,
2382
+ reason: target.watchers.length === 0 ? 'no_watchers' : 'missing_recursive_repo_root_coverage',
2383
+ }, 'Checkout diff watchers unavailable; using timed refresh fallback');
2384
+ }
2385
+ this.checkoutDiffTargets.set(targetKey, target);
2386
+ return target;
2387
+ }
2388
+ async handleSubscribeCheckoutDiffRequest(msg) {
2389
+ const cwd = expandTilde(msg.cwd);
2390
+ const compare = this.normalizeCheckoutDiffCompare(msg.compare);
2391
+ this.removeCheckoutDiffSubscription(msg.subscriptionId);
2392
+ const target = await this.ensureCheckoutDiffWatchTarget(cwd, compare);
2393
+ target.subscriptions.add(msg.subscriptionId);
2394
+ this.checkoutDiffSubscriptions.set(msg.subscriptionId, {
2395
+ targetKey: target.key,
2396
+ });
2397
+ const snapshot = target.latestPayload ??
2398
+ (await this.computeCheckoutDiffSnapshot(cwd, compare, {
2399
+ diffCwd: target.diffCwd,
2400
+ }));
2401
+ target.latestPayload = snapshot;
2402
+ target.latestFingerprint = this.checkoutDiffSnapshotFingerprint(snapshot);
2403
+ this.emit({
2404
+ type: 'subscribe_checkout_diff_response',
2405
+ payload: {
2406
+ subscriptionId: msg.subscriptionId,
2407
+ ...snapshot,
2408
+ requestId: msg.requestId,
2409
+ },
2410
+ });
2411
+ }
2412
+ handleUnsubscribeCheckoutDiffRequest(msg) {
2413
+ this.removeCheckoutDiffSubscription(msg.subscriptionId);
2414
+ }
2415
+ scheduleCheckoutDiffRefreshForCwd(cwd) {
2416
+ const resolvedCwd = expandTilde(cwd);
2417
+ for (const target of this.checkoutDiffTargets.values()) {
2418
+ if (target.cwd !== resolvedCwd && target.diffCwd !== resolvedCwd) {
2419
+ continue;
2420
+ }
2421
+ this.scheduleCheckoutDiffTargetRefresh(target);
2422
+ }
2423
+ }
2424
+ async handleCheckoutCommitRequest(msg) {
2425
+ const { cwd, requestId } = msg;
2426
+ try {
2427
+ let message = msg.message?.trim() ?? '';
2428
+ if (!message) {
2429
+ message = await this.generateCommitMessage(cwd);
2430
+ }
2431
+ if (!message) {
2432
+ throw new Error('Commit message is required');
2433
+ }
2434
+ await commitChanges(cwd, {
2435
+ message,
2436
+ addAll: msg.addAll ?? true,
2437
+ });
2438
+ this.scheduleCheckoutDiffRefreshForCwd(cwd);
2439
+ this.emit({
2440
+ type: 'checkout_commit_response',
2441
+ payload: {
2442
+ cwd,
2443
+ success: true,
2444
+ error: null,
2445
+ requestId,
2446
+ },
2447
+ });
2448
+ }
2449
+ catch (error) {
2450
+ this.emit({
2451
+ type: 'checkout_commit_response',
2452
+ payload: {
2453
+ cwd,
2454
+ success: false,
2455
+ error: this.toCheckoutError(error),
2456
+ requestId,
2457
+ },
2458
+ });
2459
+ }
2460
+ }
2461
+ async handleCheckoutMergeRequest(msg) {
2462
+ const { cwd, requestId } = msg;
2463
+ try {
2464
+ const status = await getCheckoutStatus(cwd, { junctionHome: this.junctionHome });
2465
+ if (!status.isGit) {
2466
+ try {
2467
+ await execAsync('git rev-parse --is-inside-work-tree', {
2468
+ cwd,
2469
+ env: READ_ONLY_GIT_ENV,
2470
+ });
2471
+ }
2472
+ catch (error) {
2473
+ const details = typeof error?.stderr === 'string'
2474
+ ? String(error.stderr).trim()
2475
+ : error instanceof Error
2476
+ ? error.message
2477
+ : String(error);
2478
+ throw new Error(`Not a git repository: ${cwd}\n${details}`.trim());
2479
+ }
2480
+ }
2481
+ if (msg.requireCleanTarget) {
2482
+ const { stdout } = await execAsync('git status --porcelain', {
2483
+ cwd,
2484
+ env: READ_ONLY_GIT_ENV,
2485
+ });
2486
+ if (stdout.trim().length > 0) {
2487
+ throw new Error('Working directory has uncommitted changes.');
2488
+ }
2489
+ }
2490
+ let baseRef = msg.baseRef ?? (status.isGit ? status.baseRef : null);
2491
+ if (!baseRef) {
2492
+ throw new Error('Base branch is required for merge');
2493
+ }
2494
+ if (baseRef.startsWith('origin/')) {
2495
+ baseRef = baseRef.slice('origin/'.length);
2496
+ }
2497
+ await mergeToBase(cwd, {
2498
+ baseRef,
2499
+ mode: msg.strategy === 'squash' ? 'squash' : 'merge',
2500
+ }, { junctionHome: this.junctionHome });
2501
+ this.scheduleCheckoutDiffRefreshForCwd(cwd);
2502
+ this.emit({
2503
+ type: 'checkout_merge_response',
2504
+ payload: {
2505
+ cwd,
2506
+ success: true,
2507
+ error: null,
2508
+ requestId,
2509
+ },
2510
+ });
2511
+ }
2512
+ catch (error) {
2513
+ this.emit({
2514
+ type: 'checkout_merge_response',
2515
+ payload: {
2516
+ cwd,
2517
+ success: false,
2518
+ error: this.toCheckoutError(error),
2519
+ requestId,
2520
+ },
2521
+ });
2522
+ }
2523
+ }
2524
+ async handleCheckoutMergeFromBaseRequest(msg) {
2525
+ const { cwd, requestId } = msg;
2526
+ try {
2527
+ if (msg.requireCleanTarget ?? true) {
2528
+ const { stdout } = await execAsync('git status --porcelain', {
2529
+ cwd,
2530
+ env: READ_ONLY_GIT_ENV,
2531
+ });
2532
+ if (stdout.trim().length > 0) {
2533
+ throw new Error('Working directory has uncommitted changes.');
2534
+ }
2535
+ }
2536
+ await mergeFromBase(cwd, {
2537
+ baseRef: msg.baseRef,
2538
+ requireCleanTarget: msg.requireCleanTarget ?? true,
2539
+ });
2540
+ this.scheduleCheckoutDiffRefreshForCwd(cwd);
2541
+ this.emit({
2542
+ type: 'checkout_merge_from_base_response',
2543
+ payload: {
2544
+ cwd,
2545
+ success: true,
2546
+ error: null,
2547
+ requestId,
2548
+ },
2549
+ });
2550
+ }
2551
+ catch (error) {
2552
+ this.emit({
2553
+ type: 'checkout_merge_from_base_response',
2554
+ payload: {
2555
+ cwd,
2556
+ success: false,
2557
+ error: this.toCheckoutError(error),
2558
+ requestId,
2559
+ },
2560
+ });
2561
+ }
2562
+ }
2563
+ async handleCheckoutPushRequest(msg) {
2564
+ const { cwd, requestId } = msg;
2565
+ try {
2566
+ await pushCurrentBranch(cwd);
2567
+ this.emit({
2568
+ type: 'checkout_push_response',
2569
+ payload: {
2570
+ cwd,
2571
+ success: true,
2572
+ error: null,
2573
+ requestId,
2574
+ },
2575
+ });
2576
+ }
2577
+ catch (error) {
2578
+ this.emit({
2579
+ type: 'checkout_push_response',
2580
+ payload: {
2581
+ cwd,
2582
+ success: false,
2583
+ error: this.toCheckoutError(error),
2584
+ requestId,
2585
+ },
2586
+ });
2587
+ }
2588
+ }
2589
+ async handleCheckoutPrCreateRequest(msg) {
2590
+ const { cwd, requestId } = msg;
2591
+ try {
2592
+ let title = msg.title?.trim() ?? '';
2593
+ let body = msg.body?.trim() ?? '';
2594
+ if (!title || !body) {
2595
+ const generated = await this.generatePullRequestText(cwd, msg.baseRef);
2596
+ if (!title)
2597
+ title = generated.title;
2598
+ if (!body)
2599
+ body = generated.body;
2600
+ }
2601
+ const result = await createPullRequest(cwd, {
2602
+ title,
2603
+ body,
2604
+ base: msg.baseRef,
2605
+ });
2606
+ this.emit({
2607
+ type: 'checkout_pr_create_response',
2608
+ payload: {
2609
+ cwd,
2610
+ url: result.url ?? null,
2611
+ number: result.number ?? null,
2612
+ error: null,
2613
+ requestId,
2614
+ },
2615
+ });
2616
+ }
2617
+ catch (error) {
2618
+ this.emit({
2619
+ type: 'checkout_pr_create_response',
2620
+ payload: {
2621
+ cwd,
2622
+ url: null,
2623
+ number: null,
2624
+ error: this.toCheckoutError(error),
2625
+ requestId,
2626
+ },
2627
+ });
2628
+ }
2629
+ }
2630
+ async handleCheckoutPrStatusRequest(msg) {
2631
+ const { cwd, requestId } = msg;
2632
+ try {
2633
+ const prStatus = await getPullRequestStatus(cwd);
2634
+ this.emit({
2635
+ type: 'checkout_pr_status_response',
2636
+ payload: {
2637
+ cwd,
2638
+ status: prStatus.status,
2639
+ githubFeaturesEnabled: prStatus.githubFeaturesEnabled,
2640
+ error: null,
2641
+ requestId,
2642
+ },
2643
+ });
2644
+ }
2645
+ catch (error) {
2646
+ this.emit({
2647
+ type: 'checkout_pr_status_response',
2648
+ payload: {
2649
+ cwd,
2650
+ status: null,
2651
+ githubFeaturesEnabled: true,
2652
+ error: this.toCheckoutError(error),
2653
+ requestId,
2654
+ },
2655
+ });
2656
+ }
2657
+ }
2658
+ async handleJunctionWorktreeListRequest(msg) {
2659
+ const { requestId } = msg;
2660
+ const cwd = msg.repoRoot ?? msg.cwd;
2661
+ if (!cwd) {
2662
+ this.emit({
2663
+ type: 'junction_worktree_list_response',
2664
+ payload: {
2665
+ worktrees: [],
2666
+ error: { code: 'UNKNOWN', message: 'cwd or repoRoot is required' },
2667
+ requestId,
2668
+ },
2669
+ });
2670
+ return;
2671
+ }
2672
+ try {
2673
+ const worktrees = await listJunctionWorktrees({ cwd, junctionHome: this.junctionHome });
2674
+ this.emit({
2675
+ type: 'junction_worktree_list_response',
2676
+ payload: {
2677
+ worktrees: worktrees.map((entry) => ({
2678
+ worktreePath: entry.path,
2679
+ branchName: entry.branchName ?? null,
2680
+ head: entry.head ?? null,
2681
+ })),
2682
+ error: null,
2683
+ requestId,
2684
+ },
2685
+ });
2686
+ }
2687
+ catch (error) {
2688
+ this.emit({
2689
+ type: 'junction_worktree_list_response',
2690
+ payload: {
2691
+ worktrees: [],
2692
+ error: this.toCheckoutError(error),
2693
+ requestId,
2694
+ },
2695
+ });
2696
+ }
2697
+ }
2698
+ async maybeArchiveWorktreeAfterLastAgentArchived(options) {
2699
+ try {
2700
+ const ownership = await isJunctionOwnedWorktreeCwd(options.archivedAgentCwd, {
2701
+ junctionHome: this.junctionHome,
2702
+ });
2703
+ if (!ownership.allowed) {
2704
+ return;
2705
+ }
2706
+ const resolvedWorktree = await resolveJunctionWorktreeRootForCwd(options.archivedAgentCwd, {
2707
+ junctionHome: this.junctionHome,
2708
+ });
2709
+ if (!resolvedWorktree) {
2710
+ return;
2711
+ }
2712
+ const records = await this.agentStorage.list();
2713
+ const recordsById = new Map(records.map((record) => [record.id, record]));
2714
+ const targetPath = resolvedWorktree.worktreePath;
2715
+ const hasRemainingNonArchivedRecord = records.some((record) => {
2716
+ if (record.id === options.archivedAgentId || record.archivedAt) {
2717
+ return false;
2718
+ }
2719
+ return this.isPathWithinRoot(targetPath, record.cwd);
2720
+ });
2721
+ if (hasRemainingNonArchivedRecord) {
2722
+ return;
2723
+ }
2724
+ const hasUnknownLiveAgent = this.agentManager.listAgents().some((agent) => {
2725
+ if (agent.id === options.archivedAgentId) {
2726
+ return false;
2727
+ }
2728
+ if (!this.isPathWithinRoot(targetPath, agent.cwd)) {
2729
+ return false;
2730
+ }
2731
+ return !recordsById.has(agent.id);
2732
+ });
2733
+ if (hasUnknownLiveAgent) {
2734
+ return;
2735
+ }
2736
+ const repoRoot = ownership.repoRoot;
2737
+ if (!repoRoot) {
2738
+ this.sessionLogger.warn({ agentId: options.archivedAgentId, worktreePath: targetPath }, 'Unable to resolve repo root for auto-archive after agent archive');
2739
+ return;
2740
+ }
2741
+ await this.archiveJunctionWorktree({
2742
+ targetPath,
2743
+ repoRoot,
2744
+ requestId: options.requestId,
2745
+ });
2746
+ }
2747
+ catch (error) {
2748
+ this.sessionLogger.warn({ err: error, agentId: options.archivedAgentId, cwd: options.archivedAgentCwd }, 'Failed to auto-archive worktree after agent archive');
2749
+ }
2750
+ }
2751
+ async archiveJunctionWorktree(options) {
2752
+ let targetPath = options.targetPath;
2753
+ const resolvedWorktree = await resolveJunctionWorktreeRootForCwd(targetPath, {
2754
+ junctionHome: this.junctionHome,
2755
+ });
2756
+ if (resolvedWorktree) {
2757
+ targetPath = resolvedWorktree.worktreePath;
2758
+ }
2759
+ const removedAgents = new Set();
2760
+ const agents = this.agentManager.listAgents();
2761
+ for (const agent of agents) {
2762
+ if (this.isPathWithinRoot(targetPath, agent.cwd)) {
2763
+ removedAgents.add(agent.id);
2764
+ try {
2765
+ await this.agentManager.closeAgent(agent.id);
2766
+ }
2767
+ catch {
2768
+ // ignore cleanup errors
2769
+ }
2770
+ try {
2771
+ await this.agentStorage.remove(agent.id);
2772
+ }
2773
+ catch {
2774
+ // ignore cleanup errors
2775
+ }
2776
+ }
2777
+ }
2778
+ const registryRecords = await this.agentStorage.list();
2779
+ for (const record of registryRecords) {
2780
+ if (this.isPathWithinRoot(targetPath, record.cwd)) {
2781
+ removedAgents.add(record.id);
2782
+ try {
2783
+ await this.agentStorage.remove(record.id);
2784
+ }
2785
+ catch {
2786
+ // ignore cleanup errors
2787
+ }
2788
+ }
2789
+ }
2790
+ await this.killTerminalsUnderPath(targetPath);
2791
+ await deleteJunctionWorktree({
2792
+ cwd: options.repoRoot,
2793
+ worktreePath: targetPath,
2794
+ junctionHome: this.junctionHome,
2795
+ });
2796
+ for (const agentId of removedAgents) {
2797
+ this.emit({
2798
+ type: 'agent_deleted',
2799
+ payload: {
2800
+ agentId,
2801
+ requestId: options.requestId,
2802
+ },
2803
+ });
2804
+ }
2805
+ return Array.from(removedAgents);
2806
+ }
2807
+ async handleJunctionWorktreeArchiveRequest(msg) {
2808
+ const { requestId } = msg;
2809
+ let targetPath = msg.worktreePath;
2810
+ let repoRoot = msg.repoRoot ?? null;
2811
+ try {
2812
+ if (!targetPath) {
2813
+ if (!repoRoot || !msg.branchName) {
2814
+ throw new Error('worktreePath or repoRoot+branchName is required');
2815
+ }
2816
+ const worktrees = await listJunctionWorktrees({ cwd: repoRoot, junctionHome: this.junctionHome });
2817
+ const match = worktrees.find((entry) => entry.branchName === msg.branchName);
2818
+ if (!match) {
2819
+ throw new Error(`Junction worktree not found for branch ${msg.branchName}`);
2820
+ }
2821
+ targetPath = match.path;
2822
+ }
2823
+ const ownership = await isJunctionOwnedWorktreeCwd(targetPath, {
2824
+ junctionHome: this.junctionHome,
2825
+ });
2826
+ if (!ownership.allowed) {
2827
+ this.emit({
2828
+ type: 'junction_worktree_archive_response',
2829
+ payload: {
2830
+ success: false,
2831
+ removedAgents: [],
2832
+ error: {
2833
+ code: 'NOT_ALLOWED',
2834
+ message: 'Worktree is not a Junction-owned worktree',
2835
+ },
2836
+ requestId,
2837
+ },
2838
+ });
2839
+ return;
2840
+ }
2841
+ repoRoot = ownership.repoRoot ?? repoRoot ?? null;
2842
+ if (!repoRoot) {
2843
+ throw new Error('Unable to resolve repo root for worktree');
2844
+ }
2845
+ const removedAgents = await this.archiveJunctionWorktree({
2846
+ targetPath,
2847
+ repoRoot,
2848
+ requestId,
2849
+ });
2850
+ this.emit({
2851
+ type: 'junction_worktree_archive_response',
2852
+ payload: {
2853
+ success: true,
2854
+ removedAgents,
2855
+ error: null,
2856
+ requestId,
2857
+ },
2858
+ });
2859
+ }
2860
+ catch (error) {
2861
+ this.emit({
2862
+ type: 'junction_worktree_archive_response',
2863
+ payload: {
2864
+ success: false,
2865
+ removedAgents: [],
2866
+ error: this.toCheckoutError(error),
2867
+ requestId,
2868
+ },
2869
+ });
2870
+ }
2871
+ }
2872
+ /**
2873
+ * Handle read-only file explorer requests scoped to an agent's cwd
2874
+ */
2875
+ async handleFileExplorerRequest(request) {
2876
+ const { agentId, path: requestedPath = '.', mode, requestId } = request;
2877
+ try {
2878
+ const agents = this.agentManager.listAgents();
2879
+ const agent = agents.find((a) => a.id === agentId);
2880
+ if (!agent) {
2881
+ this.emit({
2882
+ type: 'file_explorer_response',
2883
+ payload: {
2884
+ agentId,
2885
+ path: requestedPath,
2886
+ mode,
2887
+ directory: null,
2888
+ file: null,
2889
+ error: `Agent not found: ${agentId}`,
2890
+ requestId,
2891
+ },
2892
+ });
2893
+ return;
2894
+ }
2895
+ if (mode === 'list') {
2896
+ const directory = await listDirectoryEntries({
2897
+ root: agent.cwd,
2898
+ relativePath: requestedPath,
2899
+ });
2900
+ this.emit({
2901
+ type: 'file_explorer_response',
2902
+ payload: {
2903
+ agentId,
2904
+ path: directory.path,
2905
+ mode,
2906
+ directory,
2907
+ file: null,
2908
+ error: null,
2909
+ requestId,
2910
+ },
2911
+ });
2912
+ }
2913
+ else {
2914
+ const file = await readExplorerFile({
2915
+ root: agent.cwd,
2916
+ relativePath: requestedPath,
2917
+ });
2918
+ this.emit({
2919
+ type: 'file_explorer_response',
2920
+ payload: {
2921
+ agentId,
2922
+ path: file.path,
2923
+ mode,
2924
+ directory: null,
2925
+ file,
2926
+ error: null,
2927
+ requestId,
2928
+ },
2929
+ });
2930
+ }
2931
+ }
2932
+ catch (error) {
2933
+ this.sessionLogger.error({ err: error, agentId, path: requestedPath }, `Failed to fulfill file explorer request for agent ${agentId}`);
2934
+ this.emit({
2935
+ type: 'file_explorer_response',
2936
+ payload: {
2937
+ agentId,
2938
+ path: requestedPath,
2939
+ mode,
2940
+ directory: null,
2941
+ file: null,
2942
+ error: error.message,
2943
+ requestId,
2944
+ },
2945
+ });
2946
+ }
2947
+ }
2948
+ /**
2949
+ * Handle read-only file explorer requests scoped to a workspace cwd
2950
+ */
2951
+ async handleWorkspaceFileExplorerRequest(request) {
2952
+ const { cwd, path: requestedPath = '.', mode, requestId } = request;
2953
+ try {
2954
+ const root = expandTilde(cwd);
2955
+ if (mode === 'list') {
2956
+ const directory = await listDirectoryEntries({
2957
+ root,
2958
+ relativePath: requestedPath,
2959
+ });
2960
+ this.emit({
2961
+ type: 'workspace_file_explorer_response',
2962
+ payload: {
2963
+ cwd,
2964
+ path: directory.path,
2965
+ mode,
2966
+ directory,
2967
+ file: null,
2968
+ error: null,
2969
+ requestId,
2970
+ },
2971
+ });
2972
+ }
2973
+ else {
2974
+ const file = await readExplorerFile({
2975
+ root,
2976
+ relativePath: requestedPath,
2977
+ });
2978
+ this.emit({
2979
+ type: 'workspace_file_explorer_response',
2980
+ payload: {
2981
+ cwd,
2982
+ path: file.path,
2983
+ mode,
2984
+ directory: null,
2985
+ file,
2986
+ error: null,
2987
+ requestId,
2988
+ },
2989
+ });
2990
+ }
2991
+ }
2992
+ catch (error) {
2993
+ this.sessionLogger.error({ err: error, cwd, path: requestedPath }, `Failed to fulfill workspace file explorer request for cwd ${cwd}`);
2994
+ this.emit({
2995
+ type: 'workspace_file_explorer_response',
2996
+ payload: {
2997
+ cwd,
2998
+ path: requestedPath,
2999
+ mode,
3000
+ directory: null,
3001
+ file: null,
3002
+ error: error.message,
3003
+ requestId,
3004
+ },
3005
+ });
3006
+ }
3007
+ }
3008
+ /**
3009
+ * Handle project icon request for a given cwd
3010
+ */
3011
+ async handleProjectIconRequest(request) {
3012
+ const { cwd, requestId } = request;
3013
+ try {
3014
+ const icon = await getProjectIcon(cwd);
3015
+ this.emit({
3016
+ type: 'project_icon_response',
3017
+ payload: {
3018
+ cwd,
3019
+ icon,
3020
+ error: null,
3021
+ requestId,
3022
+ },
3023
+ });
3024
+ }
3025
+ catch (error) {
3026
+ this.emit({
3027
+ type: 'project_icon_response',
3028
+ payload: {
3029
+ cwd,
3030
+ icon: null,
3031
+ error: error.message,
3032
+ requestId,
3033
+ },
3034
+ });
3035
+ }
3036
+ }
3037
+ /**
3038
+ * Handle file download token request scoped to an agent's cwd
3039
+ */
3040
+ async handleFileDownloadTokenRequest(request) {
3041
+ const { agentId, path: requestedPath, requestId } = request;
3042
+ this.sessionLogger.debug({ agentId, path: requestedPath }, `Handling file download token request for agent ${agentId} (${requestedPath})`);
3043
+ try {
3044
+ const agents = this.agentManager.listAgents();
3045
+ const agent = agents.find((a) => a.id === agentId);
3046
+ if (!agent) {
3047
+ this.emit({
3048
+ type: 'file_download_token_response',
3049
+ payload: {
3050
+ agentId,
3051
+ path: requestedPath,
3052
+ token: null,
3053
+ fileName: null,
3054
+ mimeType: null,
3055
+ size: null,
3056
+ error: `Agent not found: ${agentId}`,
3057
+ requestId,
3058
+ },
3059
+ });
3060
+ return;
3061
+ }
3062
+ const info = await getDownloadableFileInfo({
3063
+ root: agent.cwd,
3064
+ relativePath: requestedPath,
3065
+ });
3066
+ const entry = this.downloadTokenStore.issueToken({
3067
+ agentId,
3068
+ path: info.path,
3069
+ absolutePath: info.absolutePath,
3070
+ fileName: info.fileName,
3071
+ mimeType: info.mimeType,
3072
+ size: info.size,
3073
+ });
3074
+ this.emit({
3075
+ type: 'file_download_token_response',
3076
+ payload: {
3077
+ agentId,
3078
+ path: info.path,
3079
+ token: entry.token,
3080
+ fileName: entry.fileName,
3081
+ mimeType: entry.mimeType,
3082
+ size: entry.size,
3083
+ error: null,
3084
+ requestId,
3085
+ },
3086
+ });
3087
+ }
3088
+ catch (error) {
3089
+ this.sessionLogger.error({ err: error, agentId, path: requestedPath }, `Failed to issue download token for agent ${agentId}`);
3090
+ this.emit({
3091
+ type: 'file_download_token_response',
3092
+ payload: {
3093
+ agentId,
3094
+ path: requestedPath,
3095
+ token: null,
3096
+ fileName: null,
3097
+ mimeType: null,
3098
+ size: null,
3099
+ error: error.message,
3100
+ requestId,
3101
+ },
3102
+ });
3103
+ }
3104
+ }
3105
+ /**
3106
+ * Build the current agent list payload (live + persisted), optionally filtered by labels.
3107
+ */
3108
+ async listAgentPayloads(filter) {
3109
+ // Get live agents with session modes
3110
+ const agentSnapshots = this.agentManager.listAgents();
3111
+ const liveAgents = await Promise.all(agentSnapshots.map((agent) => this.buildAgentPayload(agent)));
3112
+ // Add persisted agents that have not been lazily initialized yet
3113
+ // (excluding internal agents which are for ephemeral system tasks)
3114
+ const registryRecords = await this.agentStorage.list();
3115
+ const liveIds = new Set(agentSnapshots.map((a) => a.id));
3116
+ const persistedAgents = registryRecords
3117
+ .filter((record) => !liveIds.has(record.id) && !record.internal)
3118
+ .map((record) => this.buildStoredAgentPayload(record));
3119
+ let agents = [...liveAgents, ...persistedAgents];
3120
+ // Filter by labels if filter provided
3121
+ if (filter?.labels) {
3122
+ const filterLabels = filter.labels;
3123
+ agents = agents.filter((agent) => Object.entries(filterLabels).every(([key, value]) => agent.labels[key] === value));
3124
+ }
3125
+ return agents;
3126
+ }
3127
+ async resolveAgentIdentifier(identifier) {
3128
+ const trimmed = identifier.trim();
3129
+ if (!trimmed) {
3130
+ return { ok: false, error: 'Agent identifier cannot be empty' };
3131
+ }
3132
+ const stored = await this.agentStorage.list();
3133
+ const storedRecords = stored.filter((record) => !record.internal);
3134
+ const knownIds = new Set();
3135
+ for (const record of storedRecords) {
3136
+ knownIds.add(record.id);
3137
+ }
3138
+ for (const agent of this.agentManager.listAgents()) {
3139
+ knownIds.add(agent.id);
3140
+ }
3141
+ if (knownIds.has(trimmed)) {
3142
+ return { ok: true, agentId: trimmed };
3143
+ }
3144
+ const prefixMatches = Array.from(knownIds).filter((id) => id.startsWith(trimmed));
3145
+ if (prefixMatches.length === 1) {
3146
+ return { ok: true, agentId: prefixMatches[0] };
3147
+ }
3148
+ if (prefixMatches.length > 1) {
3149
+ return {
3150
+ ok: false,
3151
+ error: `Agent identifier "${trimmed}" is ambiguous (${prefixMatches
3152
+ .slice(0, 5)
3153
+ .map((id) => id.slice(0, 8))
3154
+ .join(', ')}${prefixMatches.length > 5 ? ', …' : ''})`,
3155
+ };
3156
+ }
3157
+ const titleMatches = storedRecords.filter((record) => record.title === trimmed);
3158
+ if (titleMatches.length === 1) {
3159
+ return { ok: true, agentId: titleMatches[0].id };
3160
+ }
3161
+ if (titleMatches.length > 1) {
3162
+ return {
3163
+ ok: false,
3164
+ error: `Agent title "${trimmed}" is ambiguous (${titleMatches
3165
+ .slice(0, 5)
3166
+ .map((r) => r.id.slice(0, 8))
3167
+ .join(', ')}${titleMatches.length > 5 ? ', …' : ''})`,
3168
+ };
3169
+ }
3170
+ return { ok: false, error: `Agent not found: ${trimmed}` };
3171
+ }
3172
+ async getAgentPayloadById(agentId) {
3173
+ const live = this.agentManager.getAgent(agentId);
3174
+ if (live) {
3175
+ return await this.buildAgentPayload(live);
3176
+ }
3177
+ const record = await this.agentStorage.get(agentId);
3178
+ if (!record || record.internal) {
3179
+ return null;
3180
+ }
3181
+ return this.buildStoredAgentPayload(record);
3182
+ }
3183
+ normalizeFetchAgentsSort(sort) {
3184
+ const fallback = [{ key: 'updated_at', direction: 'desc' }];
3185
+ if (!sort || sort.length === 0) {
3186
+ return fallback;
3187
+ }
3188
+ const deduped = [];
3189
+ const seen = new Set();
3190
+ for (const entry of sort) {
3191
+ if (seen.has(entry.key)) {
3192
+ continue;
3193
+ }
3194
+ seen.add(entry.key);
3195
+ deduped.push(entry);
3196
+ }
3197
+ return deduped.length > 0 ? deduped : fallback;
3198
+ }
3199
+ getStatusPriority(agent) {
3200
+ const attentionReason = agent.attentionReason ?? null;
3201
+ const hasPendingPermission = (agent.pendingPermissions?.length ?? 0) > 0;
3202
+ if (hasPendingPermission || attentionReason === 'permission') {
3203
+ return 0;
3204
+ }
3205
+ if (agent.status === 'error' || attentionReason === 'error') {
3206
+ return 1;
3207
+ }
3208
+ if (agent.status === 'running') {
3209
+ return 2;
3210
+ }
3211
+ if (agent.status === 'initializing') {
3212
+ return 3;
3213
+ }
3214
+ return 4;
3215
+ }
3216
+ getFetchAgentsSortValue(entry, key) {
3217
+ switch (key) {
3218
+ case 'status_priority':
3219
+ return this.getStatusPriority(entry.agent);
3220
+ case 'created_at':
3221
+ return Date.parse(entry.agent.createdAt);
3222
+ case 'updated_at':
3223
+ return Date.parse(entry.agent.updatedAt);
3224
+ case 'title':
3225
+ return entry.agent.title?.toLocaleLowerCase() ?? '';
3226
+ }
3227
+ }
3228
+ getFetchAgentsSortValueFromAgent(agent, key) {
3229
+ switch (key) {
3230
+ case 'status_priority':
3231
+ return this.getStatusPriority(agent);
3232
+ case 'created_at':
3233
+ return Date.parse(agent.createdAt);
3234
+ case 'updated_at':
3235
+ return Date.parse(agent.updatedAt);
3236
+ case 'title':
3237
+ return agent.title?.toLocaleLowerCase() ?? '';
3238
+ }
3239
+ }
3240
+ compareSortValues(left, right) {
3241
+ if (left === right) {
3242
+ return 0;
3243
+ }
3244
+ if (left === null) {
3245
+ return -1;
3246
+ }
3247
+ if (right === null) {
3248
+ return 1;
3249
+ }
3250
+ if (typeof left === 'number' && typeof right === 'number') {
3251
+ return left < right ? -1 : 1;
3252
+ }
3253
+ return String(left).localeCompare(String(right));
3254
+ }
3255
+ compareFetchAgentsAgents(left, right, sort) {
3256
+ for (const spec of sort) {
3257
+ const leftValue = this.getFetchAgentsSortValueFromAgent(left, spec.key);
3258
+ const rightValue = this.getFetchAgentsSortValueFromAgent(right, spec.key);
3259
+ const base = this.compareSortValues(leftValue, rightValue);
3260
+ if (base === 0) {
3261
+ continue;
3262
+ }
3263
+ return spec.direction === 'asc' ? base : -base;
3264
+ }
3265
+ return left.id.localeCompare(right.id);
3266
+ }
3267
+ encodeFetchAgentsCursor(entry, sort) {
3268
+ const values = {};
3269
+ for (const spec of sort) {
3270
+ values[spec.key] = this.getFetchAgentsSortValue(entry, spec.key);
3271
+ }
3272
+ return Buffer.from(JSON.stringify({
3273
+ sort,
3274
+ values,
3275
+ id: entry.agent.id,
3276
+ }), 'utf8').toString('base64url');
3277
+ }
3278
+ decodeFetchAgentsCursor(cursor, sort) {
3279
+ let parsed;
3280
+ try {
3281
+ parsed = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
3282
+ }
3283
+ catch {
3284
+ throw new SessionRequestError('invalid_cursor', 'Invalid fetch_agents cursor');
3285
+ }
3286
+ if (!parsed || typeof parsed !== 'object') {
3287
+ throw new SessionRequestError('invalid_cursor', 'Invalid fetch_agents cursor');
3288
+ }
3289
+ const payload = parsed;
3290
+ if (!Array.isArray(payload.sort) || typeof payload.id !== 'string') {
3291
+ throw new SessionRequestError('invalid_cursor', 'Invalid fetch_agents cursor');
3292
+ }
3293
+ if (!payload.values || typeof payload.values !== 'object') {
3294
+ throw new SessionRequestError('invalid_cursor', 'Invalid fetch_agents cursor');
3295
+ }
3296
+ const cursorSort = [];
3297
+ for (const item of payload.sort) {
3298
+ if (!item ||
3299
+ typeof item !== 'object' ||
3300
+ typeof item.key !== 'string' ||
3301
+ typeof item.direction !== 'string') {
3302
+ throw new SessionRequestError('invalid_cursor', 'Invalid fetch_agents cursor');
3303
+ }
3304
+ const key = item.key;
3305
+ const direction = item.direction;
3306
+ if ((key !== 'status_priority' &&
3307
+ key !== 'created_at' &&
3308
+ key !== 'updated_at' &&
3309
+ key !== 'title') ||
3310
+ (direction !== 'asc' && direction !== 'desc')) {
3311
+ throw new SessionRequestError('invalid_cursor', 'Invalid fetch_agents cursor');
3312
+ }
3313
+ cursorSort.push({ key, direction });
3314
+ }
3315
+ if (cursorSort.length !== sort.length ||
3316
+ cursorSort.some((entry, index) => entry.key !== sort[index]?.key || entry.direction !== sort[index]?.direction)) {
3317
+ throw new SessionRequestError('invalid_cursor', 'fetch_agents cursor does not match current sort');
3318
+ }
3319
+ return {
3320
+ sort: cursorSort,
3321
+ values: payload.values,
3322
+ id: payload.id,
3323
+ };
3324
+ }
3325
+ compareAgentWithCursor(agent, cursor, sort) {
3326
+ for (const spec of sort) {
3327
+ const leftValue = this.getFetchAgentsSortValueFromAgent(agent, spec.key);
3328
+ const rightValue = cursor.values[spec.key] !== undefined ? (cursor.values[spec.key] ?? null) : null;
3329
+ const base = this.compareSortValues(leftValue, rightValue);
3330
+ if (base === 0) {
3331
+ continue;
3332
+ }
3333
+ return spec.direction === 'asc' ? base : -base;
3334
+ }
3335
+ return agent.id.localeCompare(cursor.id);
3336
+ }
3337
+ async listFetchAgentsEntries(request) {
3338
+ const filter = request.filter;
3339
+ const sort = this.normalizeFetchAgentsSort(request.sort);
3340
+ const agents = await this.listAgentPayloads({
3341
+ labels: filter?.labels,
3342
+ });
3343
+ const placementByCwd = new Map();
3344
+ const getPlacement = (cwd) => {
3345
+ const existing = placementByCwd.get(cwd);
3346
+ if (existing) {
3347
+ return existing;
3348
+ }
3349
+ const placementPromise = this.buildProjectPlacement(cwd);
3350
+ placementByCwd.set(cwd, placementPromise);
3351
+ return placementPromise;
3352
+ };
3353
+ let candidates = [...agents];
3354
+ candidates.sort((left, right) => this.compareFetchAgentsAgents(left, right, sort));
3355
+ const cursorToken = request.page?.cursor;
3356
+ if (cursorToken) {
3357
+ const cursor = this.decodeFetchAgentsCursor(cursorToken, sort);
3358
+ candidates = candidates.filter((agent) => this.compareAgentWithCursor(agent, cursor, sort) > 0);
3359
+ }
3360
+ const limit = request.page?.limit ?? 200;
3361
+ const matchedEntries = [];
3362
+ const batchSize = 25;
3363
+ for (let start = 0; start < candidates.length && matchedEntries.length <= limit; start += batchSize) {
3364
+ const batch = candidates.slice(start, start + batchSize);
3365
+ const batchEntries = await Promise.all(batch.map(async (agent) => ({
3366
+ agent,
3367
+ project: await getPlacement(agent.cwd),
3368
+ })));
3369
+ for (const entry of batchEntries) {
3370
+ if (!this.matchesAgentFilter({
3371
+ agent: entry.agent,
3372
+ project: entry.project,
3373
+ filter,
3374
+ })) {
3375
+ continue;
3376
+ }
3377
+ matchedEntries.push(entry);
3378
+ if (matchedEntries.length > limit) {
3379
+ break;
3380
+ }
3381
+ }
3382
+ }
3383
+ const pagedEntries = matchedEntries.slice(0, limit);
3384
+ const hasMore = matchedEntries.length > limit;
3385
+ const nextCursor = hasMore && pagedEntries.length > 0
3386
+ ? this.encodeFetchAgentsCursor(pagedEntries[pagedEntries.length - 1], sort)
3387
+ : null;
3388
+ return {
3389
+ entries: pagedEntries,
3390
+ pageInfo: {
3391
+ nextCursor,
3392
+ prevCursor: request.page?.cursor ?? null,
3393
+ hasMore,
3394
+ },
3395
+ };
3396
+ }
3397
+ async handleAbort() {
3398
+ this.sessionLogger.info('Abort request');
3399
+ this.abortController.abort();
3400
+ }
3401
+ async handleFetchAgents(request) {
3402
+ const requestedSubscriptionId = request.subscribe?.subscriptionId?.trim();
3403
+ const subscriptionId = request.subscribe
3404
+ ? requestedSubscriptionId && requestedSubscriptionId.length > 0
3405
+ ? requestedSubscriptionId
3406
+ : uuidv4()
3407
+ : null;
3408
+ try {
3409
+ if (subscriptionId) {
3410
+ this.agentUpdatesSubscription = {
3411
+ subscriptionId,
3412
+ filter: request.filter,
3413
+ isBootstrapping: true,
3414
+ pendingUpdatesByAgentId: new Map(),
3415
+ };
3416
+ }
3417
+ const payload = await this.listFetchAgentsEntries(request);
3418
+ const snapshotUpdatedAtByAgentId = new Map();
3419
+ for (const entry of payload.entries) {
3420
+ const parsedUpdatedAt = Date.parse(entry.agent.updatedAt);
3421
+ if (!Number.isNaN(parsedUpdatedAt)) {
3422
+ snapshotUpdatedAtByAgentId.set(entry.agent.id, parsedUpdatedAt);
3423
+ }
3424
+ }
3425
+ this.emit({
3426
+ type: 'fetch_agents_response',
3427
+ payload: {
3428
+ requestId: request.requestId,
3429
+ ...(subscriptionId ? { subscriptionId } : {}),
3430
+ ...payload,
3431
+ },
3432
+ });
3433
+ if (subscriptionId && this.agentUpdatesSubscription?.subscriptionId === subscriptionId) {
3434
+ this.flushBootstrappedAgentUpdates({ snapshotUpdatedAtByAgentId });
3435
+ }
3436
+ }
3437
+ catch (error) {
3438
+ if (subscriptionId && this.agentUpdatesSubscription?.subscriptionId === subscriptionId) {
3439
+ this.agentUpdatesSubscription = null;
3440
+ }
3441
+ const code = error instanceof SessionRequestError ? error.code : 'fetch_agents_failed';
3442
+ const message = error instanceof Error ? error.message : 'Failed to fetch agents';
3443
+ this.sessionLogger.error({ err: error }, 'Failed to handle fetch_agents_request');
3444
+ this.emit({
3445
+ type: 'rpc_error',
3446
+ payload: {
3447
+ requestId: request.requestId,
3448
+ requestType: request.type,
3449
+ error: message,
3450
+ code,
3451
+ },
3452
+ });
3453
+ }
3454
+ }
3455
+ async handleFetchAgent(agentIdOrIdentifier, requestId) {
3456
+ const resolved = await this.resolveAgentIdentifier(agentIdOrIdentifier);
3457
+ if (!resolved.ok) {
3458
+ this.emit({
3459
+ type: 'fetch_agent_response',
3460
+ payload: { requestId, agent: null, project: null, error: resolved.error },
3461
+ });
3462
+ return;
3463
+ }
3464
+ const agent = await this.getAgentPayloadById(resolved.agentId);
3465
+ if (!agent) {
3466
+ this.emit({
3467
+ type: 'fetch_agent_response',
3468
+ payload: {
3469
+ requestId,
3470
+ agent: null,
3471
+ project: null,
3472
+ error: `Agent not found: ${resolved.agentId}`,
3473
+ },
3474
+ });
3475
+ return;
3476
+ }
3477
+ const project = await this.buildProjectPlacement(agent.cwd);
3478
+ this.emit({
3479
+ type: 'fetch_agent_response',
3480
+ payload: { requestId, agent, project, error: null },
3481
+ });
3482
+ }
3483
+ async handleFetchAgentTimelineRequest(msg) {
3484
+ const direction = msg.direction ?? (msg.cursor ? 'after' : 'tail');
3485
+ const projection = msg.projection ?? 'projected';
3486
+ const requestedLimit = msg.limit;
3487
+ const limit = requestedLimit ?? (direction === 'after' ? 0 : undefined);
3488
+ const shouldLimitByProjectedWindow = projection === 'canonical' &&
3489
+ direction === 'tail' &&
3490
+ typeof requestedLimit === 'number' &&
3491
+ requestedLimit > 0;
3492
+ const cursor = msg.cursor
3493
+ ? {
3494
+ epoch: msg.cursor.epoch,
3495
+ seq: msg.cursor.seq,
3496
+ }
3497
+ : undefined;
3498
+ try {
3499
+ const snapshot = await this.ensureAgentLoaded(msg.agentId);
3500
+ let timeline = this.agentManager.fetchTimeline(msg.agentId, {
3501
+ direction,
3502
+ cursor,
3503
+ limit: shouldLimitByProjectedWindow && typeof requestedLimit === 'number'
3504
+ ? Math.max(1, Math.floor(requestedLimit))
3505
+ : limit,
3506
+ });
3507
+ let hasOlder = timeline.hasOlder;
3508
+ let hasNewer = timeline.hasNewer;
3509
+ let startCursor = null;
3510
+ let endCursor = null;
3511
+ let entries;
3512
+ if (shouldLimitByProjectedWindow) {
3513
+ const projectedLimit = Math.max(1, Math.floor(requestedLimit));
3514
+ let fetchLimit = projectedLimit;
3515
+ let projectedWindow = selectTimelineWindowByProjectedLimit({
3516
+ rows: timeline.rows,
3517
+ provider: snapshot.provider,
3518
+ direction,
3519
+ limit: projectedLimit,
3520
+ collapseToolLifecycle: false,
3521
+ });
3522
+ while (timeline.hasOlder) {
3523
+ const needsMoreProjectedEntries = projectedWindow.projectedEntries.length < projectedLimit;
3524
+ const firstLoadedRow = timeline.rows[0];
3525
+ const firstSelectedRow = projectedWindow.selectedRows[0];
3526
+ const startsAtLoadedBoundary = firstLoadedRow != null &&
3527
+ firstSelectedRow != null &&
3528
+ firstSelectedRow.seq === firstLoadedRow.seq;
3529
+ const boundaryIsAssistantChunk = startsAtLoadedBoundary && firstLoadedRow.item.type === 'assistant_message';
3530
+ if (!needsMoreProjectedEntries && !boundaryIsAssistantChunk) {
3531
+ break;
3532
+ }
3533
+ const maxRows = Math.max(0, timeline.window.maxSeq - timeline.window.minSeq + 1);
3534
+ const nextFetchLimit = Math.min(maxRows, fetchLimit * 2);
3535
+ if (nextFetchLimit <= fetchLimit) {
3536
+ break;
3537
+ }
3538
+ fetchLimit = nextFetchLimit;
3539
+ timeline = this.agentManager.fetchTimeline(msg.agentId, {
3540
+ direction,
3541
+ cursor,
3542
+ limit: fetchLimit,
3543
+ });
3544
+ projectedWindow = selectTimelineWindowByProjectedLimit({
3545
+ rows: timeline.rows,
3546
+ provider: snapshot.provider,
3547
+ direction,
3548
+ limit: projectedLimit,
3549
+ collapseToolLifecycle: false,
3550
+ });
3551
+ }
3552
+ const selectedRows = projectedWindow.selectedRows;
3553
+ entries = projectTimelineRows(selectedRows, snapshot.provider, projection);
3554
+ if (projectedWindow.minSeq !== null && projectedWindow.maxSeq !== null) {
3555
+ startCursor = { epoch: timeline.epoch, seq: projectedWindow.minSeq };
3556
+ endCursor = { epoch: timeline.epoch, seq: projectedWindow.maxSeq };
3557
+ hasOlder = projectedWindow.minSeq > timeline.window.minSeq;
3558
+ hasNewer = false;
3559
+ }
3560
+ }
3561
+ else {
3562
+ const firstRow = timeline.rows[0];
3563
+ const lastRow = timeline.rows[timeline.rows.length - 1];
3564
+ startCursor = firstRow ? { epoch: timeline.epoch, seq: firstRow.seq } : null;
3565
+ endCursor = lastRow ? { epoch: timeline.epoch, seq: lastRow.seq } : null;
3566
+ entries = projectTimelineRows(timeline.rows, snapshot.provider, projection);
3567
+ }
3568
+ this.emit({
3569
+ type: 'fetch_agent_timeline_response',
3570
+ payload: {
3571
+ requestId: msg.requestId,
3572
+ agentId: msg.agentId,
3573
+ direction,
3574
+ projection,
3575
+ epoch: timeline.epoch,
3576
+ reset: timeline.reset,
3577
+ staleCursor: timeline.staleCursor,
3578
+ gap: timeline.gap,
3579
+ window: timeline.window,
3580
+ startCursor,
3581
+ endCursor,
3582
+ hasOlder,
3583
+ hasNewer,
3584
+ entries,
3585
+ error: null,
3586
+ },
3587
+ });
3588
+ }
3589
+ catch (error) {
3590
+ this.sessionLogger.error({ err: error, agentId: msg.agentId }, 'Failed to handle fetch_agent_timeline_request');
3591
+ this.emit({
3592
+ type: 'fetch_agent_timeline_response',
3593
+ payload: {
3594
+ requestId: msg.requestId,
3595
+ agentId: msg.agentId,
3596
+ direction,
3597
+ projection,
3598
+ epoch: '',
3599
+ reset: false,
3600
+ staleCursor: false,
3601
+ gap: false,
3602
+ window: { minSeq: 0, maxSeq: 0, nextSeq: 0 },
3603
+ startCursor: null,
3604
+ endCursor: null,
3605
+ hasOlder: false,
3606
+ hasNewer: false,
3607
+ entries: [],
3608
+ error: error instanceof Error ? error.message : String(error),
3609
+ },
3610
+ });
3611
+ }
3612
+ }
3613
+ async handleSendAgentMessageRequest(msg) {
3614
+ const resolved = await this.resolveAgentIdentifier(msg.agentId);
3615
+ if (!resolved.ok) {
3616
+ this.emit({
3617
+ type: 'send_agent_message_response',
3618
+ payload: {
3619
+ requestId: msg.requestId,
3620
+ agentId: msg.agentId,
3621
+ accepted: false,
3622
+ error: resolved.error,
3623
+ },
3624
+ });
3625
+ return;
3626
+ }
3627
+ try {
3628
+ const agentId = resolved.agentId;
3629
+ const archivedAt = await this.getArchivedAt(agentId);
3630
+ if (archivedAt) {
3631
+ this.emit({
3632
+ type: 'send_agent_message_response',
3633
+ payload: {
3634
+ requestId: msg.requestId,
3635
+ agentId,
3636
+ accepted: false,
3637
+ error: `Agent ${agentId} is archived`,
3638
+ },
3639
+ });
3640
+ return;
3641
+ }
3642
+ await this.ensureAgentLoaded(agentId);
3643
+ await this.interruptAgentIfRunning(agentId);
3644
+ try {
3645
+ this.agentManager.recordUserMessage(agentId, msg.text, {
3646
+ messageId: msg.messageId,
3647
+ emitState: false,
3648
+ });
3649
+ }
3650
+ catch (error) {
3651
+ this.sessionLogger.error({ err: error, agentId }, 'Failed to record user message for send_agent_message_request');
3652
+ }
3653
+ const prompt = this.buildAgentPrompt(msg.text, msg.images);
3654
+ const started = this.startAgentStream(agentId, prompt);
3655
+ if (!started.ok) {
3656
+ this.emit({
3657
+ type: 'send_agent_message_response',
3658
+ payload: {
3659
+ requestId: msg.requestId,
3660
+ agentId,
3661
+ accepted: false,
3662
+ error: started.error,
3663
+ },
3664
+ });
3665
+ return;
3666
+ }
3667
+ const startAbort = new AbortController();
3668
+ const startTimeoutMs = 15000;
3669
+ const startTimeout = setTimeout(() => startAbort.abort('timeout'), startTimeoutMs);
3670
+ try {
3671
+ await this.agentManager.waitForAgentRunStart(agentId, { signal: startAbort.signal });
3672
+ }
3673
+ catch (error) {
3674
+ const message = error instanceof Error
3675
+ ? error.message
3676
+ : typeof error === 'string'
3677
+ ? error
3678
+ : 'Unknown error';
3679
+ this.emit({
3680
+ type: 'send_agent_message_response',
3681
+ payload: {
3682
+ requestId: msg.requestId,
3683
+ agentId,
3684
+ accepted: false,
3685
+ error: message,
3686
+ },
3687
+ });
3688
+ return;
3689
+ }
3690
+ finally {
3691
+ clearTimeout(startTimeout);
3692
+ }
3693
+ this.emit({
3694
+ type: 'send_agent_message_response',
3695
+ payload: {
3696
+ requestId: msg.requestId,
3697
+ agentId,
3698
+ accepted: true,
3699
+ error: null,
3700
+ },
3701
+ });
3702
+ }
3703
+ catch (error) {
3704
+ const message = error instanceof Error ? error.message : typeof error === 'string' ? error : 'Unknown error';
3705
+ this.emit({
3706
+ type: 'send_agent_message_response',
3707
+ payload: {
3708
+ requestId: msg.requestId,
3709
+ agentId: resolved.agentId,
3710
+ accepted: false,
3711
+ error: message,
3712
+ },
3713
+ });
3714
+ }
3715
+ }
3716
+ async handleWaitForFinish(agentIdOrIdentifier, requestId, timeoutMs) {
3717
+ const resolved = await this.resolveAgentIdentifier(agentIdOrIdentifier);
3718
+ if (!resolved.ok) {
3719
+ this.emit({
3720
+ type: 'wait_for_finish_response',
3721
+ payload: {
3722
+ requestId,
3723
+ status: 'error',
3724
+ final: null,
3725
+ error: resolved.error,
3726
+ lastMessage: null,
3727
+ },
3728
+ });
3729
+ return;
3730
+ }
3731
+ const agentId = resolved.agentId;
3732
+ const live = this.agentManager.getAgent(agentId);
3733
+ if (!live) {
3734
+ const record = await this.agentStorage.get(agentId);
3735
+ if (!record || record.internal) {
3736
+ this.emit({
3737
+ type: 'wait_for_finish_response',
3738
+ payload: {
3739
+ requestId,
3740
+ status: 'error',
3741
+ final: null,
3742
+ error: `Agent not found: ${agentId}`,
3743
+ lastMessage: null,
3744
+ },
3745
+ });
3746
+ return;
3747
+ }
3748
+ const final = this.buildStoredAgentPayload(record);
3749
+ const status = record.attentionReason === 'permission'
3750
+ ? 'permission'
3751
+ : record.lastStatus === 'error'
3752
+ ? 'error'
3753
+ : 'idle';
3754
+ this.emit({
3755
+ type: 'wait_for_finish_response',
3756
+ payload: { requestId, status, final, error: null, lastMessage: null },
3757
+ });
3758
+ return;
3759
+ }
3760
+ const abortController = new AbortController();
3761
+ const hasTimeout = typeof timeoutMs === 'number' && timeoutMs > 0;
3762
+ const timeoutHandle = hasTimeout
3763
+ ? setTimeout(() => {
3764
+ abortController.abort('timeout');
3765
+ }, timeoutMs)
3766
+ : null;
3767
+ try {
3768
+ let result = await this.agentManager.waitForAgentEvent(agentId, {
3769
+ signal: abortController.signal,
3770
+ });
3771
+ let final = await this.getAgentPayloadById(agentId);
3772
+ if (!final) {
3773
+ throw new Error(`Agent ${agentId} disappeared while waiting`);
3774
+ }
3775
+ let status = result.permission ? 'permission' : result.status === 'error' ? 'error' : 'idle';
3776
+ this.emit({
3777
+ type: 'wait_for_finish_response',
3778
+ payload: { requestId, status, final, error: null, lastMessage: result.lastMessage },
3779
+ });
3780
+ }
3781
+ catch (error) {
3782
+ const isAbort = error instanceof Error &&
3783
+ (error.name === 'AbortError' || error.message.toLowerCase().includes('aborted'));
3784
+ if (!isAbort) {
3785
+ const message = error instanceof Error
3786
+ ? error.message
3787
+ : typeof error === 'string'
3788
+ ? error
3789
+ : 'Unknown error';
3790
+ this.sessionLogger.error({ err: error, agentId }, 'wait_for_finish_request failed');
3791
+ const final = await this.getAgentPayloadById(agentId);
3792
+ this.emit({
3793
+ type: 'wait_for_finish_response',
3794
+ payload: {
3795
+ requestId,
3796
+ status: 'error',
3797
+ final,
3798
+ error: message,
3799
+ lastMessage: null,
3800
+ },
3801
+ });
3802
+ return;
3803
+ }
3804
+ const final = await this.getAgentPayloadById(agentId);
3805
+ if (!final) {
3806
+ throw new Error(`Agent ${agentId} disappeared while waiting`);
3807
+ }
3808
+ this.emit({
3809
+ type: 'wait_for_finish_response',
3810
+ payload: { requestId, status: 'timeout', final, error: null, lastMessage: null },
3811
+ });
3812
+ }
3813
+ finally {
3814
+ if (timeoutHandle) {
3815
+ clearTimeout(timeoutHandle);
3816
+ }
3817
+ }
3818
+ }
3819
+ /**
3820
+ * Emit a message to the client
3821
+ */
3822
+ emit(msg) {
3823
+ this.onMessage(msg);
3824
+ }
3825
+ emitBinary(frame) {
3826
+ if (!this.onBinaryMessage) {
3827
+ return;
3828
+ }
3829
+ try {
3830
+ this.onBinaryMessage(frame);
3831
+ }
3832
+ catch (error) {
3833
+ this.sessionLogger.error({ err: error }, 'Failed to emit binary frame');
3834
+ }
3835
+ }
3836
+ /**
3837
+ * Clean up session resources
3838
+ */
3839
+ async cleanup() {
3840
+ this.sessionLogger.trace('Cleaning up');
3841
+ if (this.unsubscribeAgentEvents) {
3842
+ this.unsubscribeAgentEvents();
3843
+ this.unsubscribeAgentEvents = null;
3844
+ }
3845
+ // Abort any ongoing operations
3846
+ this.abortController.abort();
3847
+ // Close MCP clients
3848
+ if (this.agentMcpClient) {
3849
+ try {
3850
+ await this.agentMcpClient.close();
3851
+ }
3852
+ catch (error) {
3853
+ this.sessionLogger.error({ err: error }, 'Failed to close Agent MCP client');
3854
+ }
3855
+ this.agentMcpClient = null;
3856
+ this.agentTools = null;
3857
+ }
3858
+ // Unsubscribe from all terminals
3859
+ if (this.unsubscribeTerminalsChanged) {
3860
+ this.unsubscribeTerminalsChanged();
3861
+ this.unsubscribeTerminalsChanged = null;
3862
+ }
3863
+ this.subscribedTerminalDirectories.clear();
3864
+ for (const unsubscribe of this.terminalSubscriptions.values()) {
3865
+ unsubscribe();
3866
+ }
3867
+ this.terminalSubscriptions.clear();
3868
+ for (const unsubscribeExit of this.terminalExitSubscriptions.values()) {
3869
+ unsubscribeExit();
3870
+ }
3871
+ this.terminalExitSubscriptions.clear();
3872
+ this.detachAllTerminalStreams({ emitExit: false });
3873
+ for (const target of this.checkoutDiffTargets.values()) {
3874
+ this.closeCheckoutDiffWatchTarget(target);
3875
+ }
3876
+ this.checkoutDiffTargets.clear();
3877
+ this.checkoutDiffSubscriptions.clear();
3878
+ }
3879
+ // ============================================================================
3880
+ // Terminal Handlers
3881
+ // ============================================================================
3882
+ ensureTerminalExitSubscription(terminal) {
3883
+ if (this.terminalExitSubscriptions.has(terminal.id)) {
3884
+ return;
3885
+ }
3886
+ const unsubscribeExit = terminal.onExit(() => {
3887
+ this.handleTerminalExited(terminal.id);
3888
+ });
3889
+ this.terminalExitSubscriptions.set(terminal.id, unsubscribeExit);
3890
+ }
3891
+ handleTerminalExited(terminalId) {
3892
+ const unsubscribeExit = this.terminalExitSubscriptions.get(terminalId);
3893
+ if (unsubscribeExit) {
3894
+ unsubscribeExit();
3895
+ this.terminalExitSubscriptions.delete(terminalId);
3896
+ }
3897
+ const unsubscribe = this.terminalSubscriptions.get(terminalId);
3898
+ if (unsubscribe) {
3899
+ try {
3900
+ unsubscribe();
3901
+ }
3902
+ catch (error) {
3903
+ this.sessionLogger.warn({ err: error, terminalId }, 'Failed to unsubscribe terminal after process exit');
3904
+ }
3905
+ this.terminalSubscriptions.delete(terminalId);
3906
+ }
3907
+ const streamId = this.terminalStreamByTerminalId.get(terminalId);
3908
+ if (typeof streamId === 'number') {
3909
+ this.detachTerminalStream(streamId, { emitExit: true });
3910
+ }
3911
+ }
3912
+ emitTerminalsChangedSnapshot(input) {
3913
+ this.emit({
3914
+ type: 'terminals_changed',
3915
+ payload: {
3916
+ cwd: input.cwd,
3917
+ terminals: input.terminals,
3918
+ },
3919
+ });
3920
+ }
3921
+ handleTerminalsChanged(event) {
3922
+ if (!this.subscribedTerminalDirectories.has(event.cwd)) {
3923
+ return;
3924
+ }
3925
+ this.emitTerminalsChangedSnapshot({
3926
+ cwd: event.cwd,
3927
+ terminals: event.terminals.map((terminal) => ({
3928
+ id: terminal.id,
3929
+ name: terminal.name,
3930
+ })),
3931
+ });
3932
+ }
3933
+ handleSubscribeTerminalsRequest(msg) {
3934
+ this.subscribedTerminalDirectories.add(msg.cwd);
3935
+ void this.emitInitialTerminalsChangedSnapshot(msg.cwd);
3936
+ }
3937
+ handleUnsubscribeTerminalsRequest(msg) {
3938
+ this.subscribedTerminalDirectories.delete(msg.cwd);
3939
+ }
3940
+ async emitInitialTerminalsChangedSnapshot(cwd) {
3941
+ if (!this.terminalManager || !this.subscribedTerminalDirectories.has(cwd)) {
3942
+ return;
3943
+ }
3944
+ const hadDirectoryBeforeSubscribe = this.terminalManager.listDirectories().includes(cwd);
3945
+ try {
3946
+ const terminals = await this.terminalManager.getTerminals(cwd);
3947
+ for (const terminal of terminals) {
3948
+ this.ensureTerminalExitSubscription(terminal);
3949
+ }
3950
+ // New directories auto-create Terminal 1, which already emits through
3951
+ // terminal-manager change listeners.
3952
+ if (!hadDirectoryBeforeSubscribe) {
3953
+ return;
3954
+ }
3955
+ if (!this.subscribedTerminalDirectories.has(cwd)) {
3956
+ return;
3957
+ }
3958
+ this.emitTerminalsChangedSnapshot({
3959
+ cwd,
3960
+ terminals: terminals.map((terminal) => ({
3961
+ id: terminal.id,
3962
+ name: terminal.name,
3963
+ })),
3964
+ });
3965
+ }
3966
+ catch (error) {
3967
+ this.sessionLogger.warn({ err: error, cwd }, 'Failed to emit initial terminal snapshot');
3968
+ }
3969
+ }
3970
+ async handleListTerminalsRequest(msg) {
3971
+ if (!this.terminalManager) {
3972
+ this.emit({
3973
+ type: 'list_terminals_response',
3974
+ payload: {
3975
+ cwd: msg.cwd,
3976
+ terminals: [],
3977
+ requestId: msg.requestId,
3978
+ },
3979
+ });
3980
+ return;
3981
+ }
3982
+ try {
3983
+ const terminals = await this.terminalManager.getTerminals(msg.cwd);
3984
+ for (const terminal of terminals) {
3985
+ this.ensureTerminalExitSubscription(terminal);
3986
+ }
3987
+ this.emit({
3988
+ type: 'list_terminals_response',
3989
+ payload: {
3990
+ cwd: msg.cwd,
3991
+ terminals: terminals.map((t) => ({ id: t.id, name: t.name })),
3992
+ requestId: msg.requestId,
3993
+ },
3994
+ });
3995
+ }
3996
+ catch (error) {
3997
+ this.sessionLogger.error({ err: error, cwd: msg.cwd }, 'Failed to list terminals');
3998
+ this.emit({
3999
+ type: 'list_terminals_response',
4000
+ payload: {
4001
+ cwd: msg.cwd,
4002
+ terminals: [],
4003
+ requestId: msg.requestId,
4004
+ },
4005
+ });
4006
+ }
4007
+ }
4008
+ async handleCreateTerminalRequest(msg) {
4009
+ if (!this.terminalManager) {
4010
+ this.emit({
4011
+ type: 'create_terminal_response',
4012
+ payload: {
4013
+ terminal: null,
4014
+ error: 'Terminal manager not available',
4015
+ requestId: msg.requestId,
4016
+ },
4017
+ });
4018
+ return;
4019
+ }
4020
+ try {
4021
+ const session = await this.terminalManager.createTerminal({
4022
+ cwd: msg.cwd,
4023
+ name: msg.name,
4024
+ });
4025
+ this.ensureTerminalExitSubscription(session);
4026
+ this.emit({
4027
+ type: 'create_terminal_response',
4028
+ payload: {
4029
+ terminal: { id: session.id, name: session.name, cwd: session.cwd },
4030
+ error: null,
4031
+ requestId: msg.requestId,
4032
+ },
4033
+ });
4034
+ }
4035
+ catch (error) {
4036
+ this.sessionLogger.error({ err: error, cwd: msg.cwd }, 'Failed to create terminal');
4037
+ this.emit({
4038
+ type: 'create_terminal_response',
4039
+ payload: {
4040
+ terminal: null,
4041
+ error: error.message,
4042
+ requestId: msg.requestId,
4043
+ },
4044
+ });
4045
+ }
4046
+ }
4047
+ async handleSubscribeTerminalRequest(msg) {
4048
+ if (!this.terminalManager) {
4049
+ this.emit({
4050
+ type: 'subscribe_terminal_response',
4051
+ payload: {
4052
+ terminalId: msg.terminalId,
4053
+ state: null,
4054
+ error: 'Terminal manager not available',
4055
+ requestId: msg.requestId,
4056
+ },
4057
+ });
4058
+ return;
4059
+ }
4060
+ const session = this.terminalManager.getTerminal(msg.terminalId);
4061
+ if (!session) {
4062
+ this.emit({
4063
+ type: 'subscribe_terminal_response',
4064
+ payload: {
4065
+ terminalId: msg.terminalId,
4066
+ state: null,
4067
+ error: 'Terminal not found',
4068
+ requestId: msg.requestId,
4069
+ },
4070
+ });
4071
+ return;
4072
+ }
4073
+ this.ensureTerminalExitSubscription(session);
4074
+ // Unsubscribe from previous subscription if any
4075
+ const existing = this.terminalSubscriptions.get(msg.terminalId);
4076
+ if (existing) {
4077
+ existing();
4078
+ }
4079
+ // Subscribe to terminal updates
4080
+ const unsubscribe = session.subscribe((serverMsg) => {
4081
+ if (serverMsg.type === 'full') {
4082
+ this.emit({
4083
+ type: 'terminal_output',
4084
+ payload: {
4085
+ terminalId: msg.terminalId,
4086
+ state: serverMsg.state,
4087
+ },
4088
+ });
4089
+ }
4090
+ });
4091
+ this.terminalSubscriptions.set(msg.terminalId, unsubscribe);
4092
+ // Send initial state
4093
+ this.emit({
4094
+ type: 'subscribe_terminal_response',
4095
+ payload: {
4096
+ terminalId: msg.terminalId,
4097
+ state: session.getState(),
4098
+ error: null,
4099
+ requestId: msg.requestId,
4100
+ },
4101
+ });
4102
+ }
4103
+ handleUnsubscribeTerminalRequest(msg) {
4104
+ const unsubscribe = this.terminalSubscriptions.get(msg.terminalId);
4105
+ if (unsubscribe) {
4106
+ unsubscribe();
4107
+ this.terminalSubscriptions.delete(msg.terminalId);
4108
+ }
4109
+ }
4110
+ handleTerminalInput(msg) {
4111
+ if (!this.terminalManager) {
4112
+ return;
4113
+ }
4114
+ const session = this.terminalManager.getTerminal(msg.terminalId);
4115
+ if (!session) {
4116
+ this.sessionLogger.warn({ terminalId: msg.terminalId }, 'Terminal not found for input');
4117
+ return;
4118
+ }
4119
+ this.ensureTerminalExitSubscription(session);
4120
+ session.send(msg.message);
4121
+ }
4122
+ killTrackedTerminal(terminalId, options) {
4123
+ const unsubscribe = this.terminalSubscriptions.get(terminalId);
4124
+ if (unsubscribe) {
4125
+ unsubscribe();
4126
+ this.terminalSubscriptions.delete(terminalId);
4127
+ }
4128
+ const streamId = this.terminalStreamByTerminalId.get(terminalId);
4129
+ if (typeof streamId === 'number') {
4130
+ this.detachTerminalStream(streamId, { emitExit: options?.emitExit ?? true });
4131
+ }
4132
+ this.terminalManager?.killTerminal(terminalId);
4133
+ }
4134
+ async killTerminalsUnderPath(rootPath) {
4135
+ if (!this.terminalManager) {
4136
+ return;
4137
+ }
4138
+ const cleanupErrors = [];
4139
+ const terminalDirectories = [...this.terminalManager.listDirectories()];
4140
+ for (const terminalCwd of terminalDirectories) {
4141
+ if (!this.isPathWithinRoot(rootPath, terminalCwd)) {
4142
+ continue;
4143
+ }
4144
+ try {
4145
+ const terminals = await this.terminalManager.getTerminals(terminalCwd);
4146
+ for (const terminal of [...terminals]) {
4147
+ this.killTrackedTerminal(terminal.id, { emitExit: true });
4148
+ }
4149
+ }
4150
+ catch (error) {
4151
+ const message = error instanceof Error ? error.message : String(error);
4152
+ cleanupErrors.push({ cwd: terminalCwd, message });
4153
+ this.sessionLogger.warn({ err: error, cwd: terminalCwd }, 'Failed to clean up worktree terminals during archive');
4154
+ }
4155
+ }
4156
+ if (cleanupErrors.length > 0) {
4157
+ const details = cleanupErrors.map((entry) => `${entry.cwd}: ${entry.message}`).join('; ');
4158
+ throw new Error(`Failed to clean up worktree terminals during archive (${details})`);
4159
+ }
4160
+ }
4161
+ async handleKillTerminalRequest(msg) {
4162
+ if (!this.terminalManager) {
4163
+ this.emit({
4164
+ type: 'kill_terminal_response',
4165
+ payload: {
4166
+ terminalId: msg.terminalId,
4167
+ success: false,
4168
+ requestId: msg.requestId,
4169
+ },
4170
+ });
4171
+ return;
4172
+ }
4173
+ this.killTrackedTerminal(msg.terminalId, { emitExit: true });
4174
+ this.emit({
4175
+ type: 'kill_terminal_response',
4176
+ payload: {
4177
+ terminalId: msg.terminalId,
4178
+ success: true,
4179
+ requestId: msg.requestId,
4180
+ },
4181
+ });
4182
+ }
4183
+ async handleAttachTerminalStreamRequest(msg) {
4184
+ if (!this.terminalManager || !this.onBinaryMessage) {
4185
+ this.emit({
4186
+ type: 'attach_terminal_stream_response',
4187
+ payload: {
4188
+ terminalId: msg.terminalId,
4189
+ streamId: null,
4190
+ replayedFrom: 0,
4191
+ currentOffset: 0,
4192
+ earliestAvailableOffset: 0,
4193
+ reset: true,
4194
+ error: 'Terminal streaming not available',
4195
+ requestId: msg.requestId,
4196
+ },
4197
+ });
4198
+ return;
4199
+ }
4200
+ const session = this.terminalManager.getTerminal(msg.terminalId);
4201
+ if (!session) {
4202
+ this.emit({
4203
+ type: 'attach_terminal_stream_response',
4204
+ payload: {
4205
+ terminalId: msg.terminalId,
4206
+ streamId: null,
4207
+ replayedFrom: 0,
4208
+ currentOffset: 0,
4209
+ earliestAvailableOffset: 0,
4210
+ reset: true,
4211
+ error: 'Terminal not found',
4212
+ requestId: msg.requestId,
4213
+ },
4214
+ });
4215
+ return;
4216
+ }
4217
+ if (msg.rows || msg.cols) {
4218
+ const state = session.getState();
4219
+ session.send({
4220
+ type: 'resize',
4221
+ rows: msg.rows ?? state.rows,
4222
+ cols: msg.cols ?? state.cols,
4223
+ });
4224
+ }
4225
+ const existingStreamId = this.terminalStreamByTerminalId.get(msg.terminalId);
4226
+ if (typeof existingStreamId === 'number') {
4227
+ // Replacing an active stream can happen when multiple UI surfaces attach to the
4228
+ // same terminal. Emit exit for the replaced stream so stale listeners reconnect
4229
+ // instead of continuing to send input to an invalid stream id.
4230
+ this.detachTerminalStream(existingStreamId, { emitExit: true });
4231
+ }
4232
+ const streamId = this.allocateTerminalStreamId();
4233
+ const initialOffset = Math.max(0, Math.floor(msg.resumeOffset ?? 0));
4234
+ const binding = {
4235
+ terminalId: msg.terminalId,
4236
+ unsubscribe: () => { },
4237
+ lastOutputOffset: initialOffset,
4238
+ lastAckOffset: initialOffset,
4239
+ pendingChunks: [],
4240
+ pendingBytes: 0,
4241
+ };
4242
+ this.terminalStreams.set(streamId, binding);
4243
+ this.terminalStreamByTerminalId.set(msg.terminalId, streamId);
4244
+ let rawSub;
4245
+ try {
4246
+ rawSub = session.subscribeRaw((chunk) => {
4247
+ const currentBinding = this.terminalStreams.get(streamId);
4248
+ if (!currentBinding) {
4249
+ return;
4250
+ }
4251
+ this.enqueueOrEmitTerminalStreamChunk(streamId, currentBinding, {
4252
+ data: chunk.data,
4253
+ startOffset: chunk.startOffset,
4254
+ endOffset: chunk.endOffset,
4255
+ replay: chunk.replay,
4256
+ });
4257
+ }, { fromOffset: msg.resumeOffset ?? 0 });
4258
+ }
4259
+ catch (error) {
4260
+ this.terminalStreams.delete(streamId);
4261
+ this.terminalStreamByTerminalId.delete(msg.terminalId);
4262
+ throw error;
4263
+ }
4264
+ binding.unsubscribe = rawSub.unsubscribe;
4265
+ binding.lastAckOffset = rawSub.replayedFrom;
4266
+ if (binding.lastOutputOffset < rawSub.replayedFrom) {
4267
+ binding.lastOutputOffset = rawSub.replayedFrom;
4268
+ }
4269
+ this.flushPendingTerminalStreamChunks(streamId, binding);
4270
+ this.emit({
4271
+ type: 'attach_terminal_stream_response',
4272
+ payload: {
4273
+ terminalId: msg.terminalId,
4274
+ streamId,
4275
+ replayedFrom: rawSub.replayedFrom,
4276
+ currentOffset: rawSub.currentOffset,
4277
+ earliestAvailableOffset: rawSub.earliestAvailableOffset,
4278
+ reset: rawSub.reset,
4279
+ error: null,
4280
+ requestId: msg.requestId,
4281
+ },
4282
+ });
4283
+ }
4284
+ getTerminalStreamChunkByteLength(chunk) {
4285
+ return Math.max(0, chunk.endOffset - chunk.startOffset);
4286
+ }
4287
+ canEmitTerminalStreamChunk(binding, chunk) {
4288
+ return chunk.startOffset < binding.lastAckOffset + TERMINAL_STREAM_WINDOW_BYTES;
4289
+ }
4290
+ emitTerminalStreamChunk(streamId, binding, chunk) {
4291
+ const payload = new Uint8Array(Buffer.from(chunk.data, 'utf8'));
4292
+ this.emitBinary({
4293
+ channel: BinaryMuxChannel.Terminal,
4294
+ messageType: TerminalBinaryMessageType.OutputUtf8,
4295
+ streamId,
4296
+ offset: chunk.startOffset,
4297
+ flags: chunk.replay ? TerminalBinaryFlags.Replay : 0,
4298
+ payload,
4299
+ });
4300
+ binding.lastOutputOffset = chunk.endOffset;
4301
+ }
4302
+ enqueueOrEmitTerminalStreamChunk(streamId, binding, chunk) {
4303
+ const chunkBytes = this.getTerminalStreamChunkByteLength(chunk);
4304
+ if (binding.pendingChunks.length > 0 || !this.canEmitTerminalStreamChunk(binding, chunk)) {
4305
+ if (binding.pendingChunks.length >= TERMINAL_STREAM_MAX_PENDING_CHUNKS ||
4306
+ binding.pendingBytes + chunkBytes > TERMINAL_STREAM_MAX_PENDING_BYTES) {
4307
+ this.sessionLogger.warn({
4308
+ streamId,
4309
+ pendingChunks: binding.pendingChunks.length,
4310
+ pendingBytes: binding.pendingBytes,
4311
+ chunkBytes,
4312
+ }, 'Terminal stream pending buffer overflow; closing stream');
4313
+ this.detachTerminalStream(streamId, { emitExit: true });
4314
+ return;
4315
+ }
4316
+ binding.pendingChunks.push(chunk);
4317
+ binding.pendingBytes += chunkBytes;
4318
+ return;
4319
+ }
4320
+ this.emitTerminalStreamChunk(streamId, binding, chunk);
4321
+ }
4322
+ flushPendingTerminalStreamChunks(streamId, binding) {
4323
+ while (binding.pendingChunks.length > 0) {
4324
+ const next = binding.pendingChunks[0];
4325
+ if (!next || !this.canEmitTerminalStreamChunk(binding, next)) {
4326
+ break;
4327
+ }
4328
+ binding.pendingChunks.shift();
4329
+ binding.pendingBytes -= this.getTerminalStreamChunkByteLength(next);
4330
+ if (binding.pendingBytes < 0) {
4331
+ binding.pendingBytes = 0;
4332
+ }
4333
+ this.emitTerminalStreamChunk(streamId, binding, next);
4334
+ }
4335
+ }
4336
+ handleDetachTerminalStreamRequest(msg) {
4337
+ const success = this.detachTerminalStream(msg.streamId, { emitExit: false });
4338
+ this.emit({
4339
+ type: 'detach_terminal_stream_response',
4340
+ payload: {
4341
+ streamId: msg.streamId,
4342
+ success,
4343
+ requestId: msg.requestId,
4344
+ },
4345
+ });
4346
+ }
4347
+ detachAllTerminalStreams(options) {
4348
+ for (const streamId of Array.from(this.terminalStreams.keys())) {
4349
+ this.detachTerminalStream(streamId, options);
4350
+ }
4351
+ }
4352
+ detachTerminalStream(streamId, options) {
4353
+ const binding = this.terminalStreams.get(streamId);
4354
+ if (!binding) {
4355
+ return false;
4356
+ }
4357
+ try {
4358
+ binding.unsubscribe();
4359
+ }
4360
+ catch (error) {
4361
+ this.sessionLogger.warn({ err: error, streamId }, 'Failed to unsubscribe terminal stream');
4362
+ }
4363
+ this.terminalStreams.delete(streamId);
4364
+ if (this.terminalStreamByTerminalId.get(binding.terminalId) === streamId) {
4365
+ this.terminalStreamByTerminalId.delete(binding.terminalId);
4366
+ }
4367
+ if (options?.emitExit) {
4368
+ this.emit({
4369
+ type: 'terminal_stream_exit',
4370
+ payload: {
4371
+ streamId,
4372
+ terminalId: binding.terminalId,
4373
+ },
4374
+ });
4375
+ }
4376
+ return true;
4377
+ }
4378
+ allocateTerminalStreamId() {
4379
+ let attempts = 0;
4380
+ while (attempts < 0xffffffff) {
4381
+ const candidate = this.nextTerminalStreamId >>> 0;
4382
+ this.nextTerminalStreamId = ((this.nextTerminalStreamId + 1) & 0xffffffff) >>> 0;
4383
+ if (candidate === 0) {
4384
+ attempts += 1;
4385
+ continue;
4386
+ }
4387
+ if (!this.terminalStreams.has(candidate)) {
4388
+ return candidate;
4389
+ }
4390
+ attempts += 1;
4391
+ }
4392
+ throw new Error('Unable to allocate terminal stream id');
4393
+ }
4394
+ }
4395
+ //# sourceMappingURL=session.js.map