@otto-code/server 0.5.0

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 (734) hide show
  1. package/.env.example +31 -0
  2. package/README.md +111 -0
  3. package/dist/scripts/dev-runner.js +37 -0
  4. package/dist/scripts/mcp-stdio-socket-bridge-cli.mjs +62 -0
  5. package/dist/scripts/supervisor-entrypoint.js +141 -0
  6. package/dist/scripts/supervisor-log-config.js +31 -0
  7. package/dist/scripts/supervisor.js +252 -0
  8. package/dist/server/executable-resolution/executable-resolution.d.ts +10 -0
  9. package/dist/server/executable-resolution/executable-resolution.js +118 -0
  10. package/dist/server/executable-resolution/windows.d.ts +18 -0
  11. package/dist/server/executable-resolution/windows.js +65 -0
  12. package/dist/server/server/agent/activity-curator.d.ts +28 -0
  13. package/dist/server/server/agent/activity-curator.js +227 -0
  14. package/dist/server/server/agent/agent-archive.d.ts +11 -0
  15. package/dist/server/server/agent/agent-archive.js +16 -0
  16. package/dist/server/server/agent/agent-loading.d.ts +14 -0
  17. package/dist/server/server/agent/agent-loading.js +54 -0
  18. package/dist/server/server/agent/agent-manager.d.ts +470 -0
  19. package/dist/server/server/agent/agent-manager.js +3049 -0
  20. package/dist/server/server/agent/agent-personalities.d.ts +59 -0
  21. package/dist/server/server/agent/agent-personalities.js +103 -0
  22. package/dist/server/server/agent/agent-projections.d.ts +35 -0
  23. package/dist/server/server/agent/agent-projections.js +488 -0
  24. package/dist/server/server/agent/agent-prompt.d.ts +76 -0
  25. package/dist/server/server/agent/agent-prompt.js +230 -0
  26. package/dist/server/server/agent/agent-response-loop.d.ts +72 -0
  27. package/dist/server/server/agent/agent-response-loop.js +333 -0
  28. package/dist/server/server/agent/agent-sdk-types.d.ts +736 -0
  29. package/dist/server/server/agent/agent-sdk-types.js +22 -0
  30. package/dist/server/server/agent/agent-storage.d.ts +147 -0
  31. package/dist/server/server/agent/agent-storage.js +326 -0
  32. package/dist/server/server/agent/agent-stream-coalescer.d.ts +41 -0
  33. package/dist/server/server/agent/agent-stream-coalescer.js +166 -0
  34. package/dist/server/server/agent/agent-timeline-store-types.d.ts +50 -0
  35. package/dist/server/server/agent/agent-timeline-store-types.js +2 -0
  36. package/dist/server/server/agent/agent-timeline-store.d.ts +27 -0
  37. package/dist/server/server/agent/agent-timeline-store.js +230 -0
  38. package/dist/server/server/agent/audio-utils.d.ts +3 -0
  39. package/dist/server/server/agent/audio-utils.js +19 -0
  40. package/dist/server/server/agent/create-agent/create.d.ts +91 -0
  41. package/dist/server/server/agent/create-agent/create.js +370 -0
  42. package/dist/server/server/agent/create-agent-lifecycle-dispatch.d.ts +54 -0
  43. package/dist/server/server/agent/create-agent-lifecycle-dispatch.js +152 -0
  44. package/dist/server/server/agent/create-agent-mode.d.ts +13 -0
  45. package/dist/server/server/agent/create-agent-mode.js +62 -0
  46. package/dist/server/server/agent/create-agent-title.d.ts +10 -0
  47. package/dist/server/server/agent/create-agent-title.js +34 -0
  48. package/dist/server/server/agent/dictation-debug.d.ts +13 -0
  49. package/dist/server/server/agent/dictation-debug.js +50 -0
  50. package/dist/server/server/agent/effort-levels.d.ts +2 -0
  51. package/dist/server/server/agent/effort-levels.js +7 -0
  52. package/dist/server/server/agent/foreground-run-state.d.ts +50 -0
  53. package/dist/server/server/agent/foreground-run-state.js +160 -0
  54. package/dist/server/server/agent/import-sessions.d.ts +47 -0
  55. package/dist/server/server/agent/import-sessions.js +145 -0
  56. package/dist/server/server/agent/lifecycle-command.d.ts +73 -0
  57. package/dist/server/server/agent/lifecycle-command.js +92 -0
  58. package/dist/server/server/agent/mcp-server.d.ts +5 -0
  59. package/dist/server/server/agent/mcp-server.js +66 -0
  60. package/dist/server/server/agent/mcp-shared.d.ts +192 -0
  61. package/dist/server/server/agent/mcp-shared.js +201 -0
  62. package/dist/server/server/agent/pcm16-resampler.d.ts +14 -0
  63. package/dist/server/server/agent/pcm16-resampler.js +63 -0
  64. package/dist/server/server/agent/permission-response.d.ts +15 -0
  65. package/dist/server/server/agent/permission-response.js +14 -0
  66. package/dist/server/server/agent/personality-stats-store.d.ts +23 -0
  67. package/dist/server/server/agent/personality-stats-store.js +65 -0
  68. package/dist/server/server/agent/prompt-attachments.d.ts +7 -0
  69. package/dist/server/server/agent/prompt-attachments.js +117 -0
  70. package/dist/server/server/agent/provider-history-timestamps.d.ts +2 -0
  71. package/dist/server/server/agent/provider-history-timestamps.js +16 -0
  72. package/dist/server/server/agent/provider-launch-config.d.ts +43 -0
  73. package/dist/server/server/agent/provider-launch-config.js +167 -0
  74. package/dist/server/server/agent/provider-notices.d.ts +3 -0
  75. package/dist/server/server/agent/provider-notices.js +5 -0
  76. package/dist/server/server/agent/provider-registry.d.ts +41 -0
  77. package/dist/server/server/agent/provider-registry.js +532 -0
  78. package/dist/server/server/agent/provider-session-import.d.ts +10 -0
  79. package/dist/server/server/agent/provider-session-import.js +49 -0
  80. package/dist/server/server/agent/provider-snapshot-manager.d.ts +142 -0
  81. package/dist/server/server/agent/provider-snapshot-manager.js +690 -0
  82. package/dist/server/server/agent/providers/acp-agent.d.ts +353 -0
  83. package/dist/server/server/agent/providers/acp-agent.js +2439 -0
  84. package/dist/server/server/agent/providers/claude/agent.d.ts +66 -0
  85. package/dist/server/server/agent/providers/claude/agent.js +4535 -0
  86. package/dist/server/server/agent/providers/claude/feature-definitions.d.ts +8 -0
  87. package/dist/server/server/agent/providers/claude/feature-definitions.js +24 -0
  88. package/dist/server/server/agent/providers/claude/model-manifest.d.ts +90 -0
  89. package/dist/server/server/agent/providers/claude/model-manifest.js +205 -0
  90. package/dist/server/server/agent/providers/claude/models.d.ts +11 -0
  91. package/dist/server/server/agent/providers/claude/models.js +97 -0
  92. package/dist/server/server/agent/providers/claude/partial-json.d.ts +5 -0
  93. package/dist/server/server/agent/providers/claude/partial-json.js +306 -0
  94. package/dist/server/server/agent/providers/claude/project-dir.d.ts +6 -0
  95. package/dist/server/server/agent/providers/claude/project-dir.js +57 -0
  96. package/dist/server/server/agent/providers/claude/query.d.ts +17 -0
  97. package/dist/server/server/agent/providers/claude/query.js +86 -0
  98. package/dist/server/server/agent/providers/claude/rewind.d.ts +30 -0
  99. package/dist/server/server/agent/providers/claude/rewind.js +30 -0
  100. package/dist/server/server/agent/providers/claude/sidechain-tracker.d.ts +22 -0
  101. package/dist/server/server/agent/providers/claude/sidechain-tracker.js +232 -0
  102. package/dist/server/server/agent/providers/claude/task-notification-tool-call.d.ts +35 -0
  103. package/dist/server/server/agent/providers/claude/task-notification-tool-call.js +272 -0
  104. package/dist/server/server/agent/providers/claude/test-rewind-claude-sdk.d.ts +24 -0
  105. package/dist/server/server/agent/providers/claude/test-rewind-claude-sdk.js +23 -0
  106. package/dist/server/server/agent/providers/claude/tool-call-detail-parser.d.ts +3 -0
  107. package/dist/server/server/agent/providers/claude/tool-call-detail-parser.js +130 -0
  108. package/dist/server/server/agent/providers/claude/tool-call-mapper.d.ts +16 -0
  109. package/dist/server/server/agent/providers/claude/tool-call-mapper.js +129 -0
  110. package/dist/server/server/agent/providers/codex/app-server-transport.d.ts +90 -0
  111. package/dist/server/server/agent/providers/codex/app-server-transport.js +262 -0
  112. package/dist/server/server/agent/providers/codex/rewind.d.ts +21 -0
  113. package/dist/server/server/agent/providers/codex/rewind.js +46 -0
  114. package/dist/server/server/agent/providers/codex/test-utils/fake-app-server.d.ts +36 -0
  115. package/dist/server/server/agent/providers/codex/test-utils/fake-app-server.js +217 -0
  116. package/dist/server/server/agent/providers/codex/tool-call-detail-parser.d.ts +12 -0
  117. package/dist/server/server/agent/providers/codex/tool-call-detail-parser.js +104 -0
  118. package/dist/server/server/agent/providers/codex/tool-call-mapper.d.ts +24 -0
  119. package/dist/server/server/agent/providers/codex/tool-call-mapper.js +830 -0
  120. package/dist/server/server/agent/providers/codex-app-server-agent.d.ts +308 -0
  121. package/dist/server/server/agent/providers/codex-app-server-agent.js +4515 -0
  122. package/dist/server/server/agent/providers/codex-feature-definitions.d.ts +11 -0
  123. package/dist/server/server/agent/providers/codex-feature-definitions.js +45 -0
  124. package/dist/server/server/agent/providers/copilot-acp-agent.d.ts +26 -0
  125. package/dist/server/server/agent/providers/copilot-acp-agent.js +188 -0
  126. package/dist/server/server/agent/providers/cursor-acp-agent.d.ts +15 -0
  127. package/dist/server/server/agent/providers/cursor-acp-agent.js +18 -0
  128. package/dist/server/server/agent/providers/diagnostic-utils.d.ts +36 -0
  129. package/dist/server/server/agent/providers/diagnostic-utils.js +355 -0
  130. package/dist/server/server/agent/providers/generic-acp-agent.d.ts +42 -0
  131. package/dist/server/server/agent/providers/generic-acp-agent.js +160 -0
  132. package/dist/server/server/agent/providers/kiro-acp-agent.d.ts +17 -0
  133. package/dist/server/server/agent/providers/kiro-acp-agent.js +78 -0
  134. package/dist/server/server/agent/providers/mock-load-test-agent.d.ts +85 -0
  135. package/dist/server/server/agent/providers/mock-load-test-agent.js +1010 -0
  136. package/dist/server/server/agent/providers/mock-slow-provider.d.ts +14 -0
  137. package/dist/server/server/agent/providers/mock-slow-provider.js +39 -0
  138. package/dist/server/server/agent/providers/openai-compat-agent.d.ts +422 -0
  139. package/dist/server/server/agent/providers/openai-compat-agent.js +2536 -0
  140. package/dist/server/server/agent/providers/openai-compat-feature-definitions.d.ts +40 -0
  141. package/dist/server/server/agent/providers/openai-compat-feature-definitions.js +82 -0
  142. package/dist/server/server/agent/providers/openai-compat-mcp.d.ts +87 -0
  143. package/dist/server/server/agent/providers/openai-compat-mcp.js +353 -0
  144. package/dist/server/server/agent/providers/openai-compat-otto-tool-permissions.d.ts +29 -0
  145. package/dist/server/server/agent/providers/openai-compat-otto-tool-permissions.js +96 -0
  146. package/dist/server/server/agent/providers/openai-compat-tools.d.ts +69 -0
  147. package/dist/server/server/agent/providers/openai-compat-tools.js +1224 -0
  148. package/dist/server/server/agent/providers/opencode/paths.d.ts +2 -0
  149. package/dist/server/server/agent/providers/opencode/paths.js +7 -0
  150. package/dist/server/server/agent/providers/opencode/rewind.d.ts +18 -0
  151. package/dist/server/server/agent/providers/opencode/rewind.js +14 -0
  152. package/dist/server/server/agent/providers/opencode/server-manager.d.ts +91 -0
  153. package/dist/server/server/agent/providers/opencode/server-manager.js +425 -0
  154. package/dist/server/server/agent/providers/opencode/test-server-manager.d.ts +25 -0
  155. package/dist/server/server/agent/providers/opencode/test-server-manager.js +39 -0
  156. package/dist/server/server/agent/providers/opencode/test-utils/test-opencode-harness.d.ts +92 -0
  157. package/dist/server/server/agent/providers/opencode/test-utils/test-opencode-harness.js +260 -0
  158. package/dist/server/server/agent/providers/opencode/tool-call-detail-parser.d.ts +3 -0
  159. package/dist/server/server/agent/providers/opencode/tool-call-detail-parser.js +256 -0
  160. package/dist/server/server/agent/providers/opencode/tool-call-mapper.d.ts +13 -0
  161. package/dist/server/server/agent/providers/opencode/tool-call-mapper.js +53 -0
  162. package/dist/server/server/agent/providers/opencode-agent.d.ts +286 -0
  163. package/dist/server/server/agent/providers/opencode-agent.js +2830 -0
  164. package/dist/server/server/agent/providers/pi/agent.d.ts +132 -0
  165. package/dist/server/server/agent/providers/pi/agent.js +1649 -0
  166. package/dist/server/server/agent/providers/pi/cli-runtime.d.ts +21 -0
  167. package/dist/server/server/agent/providers/pi/cli-runtime.js +261 -0
  168. package/dist/server/server/agent/providers/pi/history-mapper.d.ts +9 -0
  169. package/dist/server/server/agent/providers/pi/history-mapper.js +140 -0
  170. package/dist/server/server/agent/providers/pi/rewind.d.ts +8 -0
  171. package/dist/server/server/agent/providers/pi/rewind.js +8 -0
  172. package/dist/server/server/agent/providers/pi/rpc-types.d.ts +208 -0
  173. package/dist/server/server/agent/providers/pi/rpc-types.js +2 -0
  174. package/dist/server/server/agent/providers/pi/runtime.d.ts +57 -0
  175. package/dist/server/server/agent/providers/pi/runtime.js +56 -0
  176. package/dist/server/server/agent/providers/pi/session-descriptor.d.ts +16 -0
  177. package/dist/server/server/agent/providers/pi/session-descriptor.js +346 -0
  178. package/dist/server/server/agent/providers/pi/test-utils/fake-pi.d.ts +83 -0
  179. package/dist/server/server/agent/providers/pi/test-utils/fake-pi.js +184 -0
  180. package/dist/server/server/agent/providers/pi/tool-call-mapper.d.ts +112 -0
  181. package/dist/server/server/agent/providers/pi/tool-call-mapper.js +284 -0
  182. package/dist/server/server/agent/providers/provider-image-output.d.ts +25 -0
  183. package/dist/server/server/agent/providers/provider-image-output.js +111 -0
  184. package/dist/server/server/agent/providers/provider-runner.d.ts +27 -0
  185. package/dist/server/server/agent/providers/provider-runner.js +81 -0
  186. package/dist/server/server/agent/providers/test-utils/session-stream-adapter.d.ts +3 -0
  187. package/dist/server/server/agent/providers/test-utils/session-stream-adapter.js +60 -0
  188. package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts +884 -0
  189. package/dist/server/server/agent/providers/tool-call-detail-primitives.js +778 -0
  190. package/dist/server/server/agent/providers/tool-call-mapper-utils.d.ts +25 -0
  191. package/dist/server/server/agent/providers/tool-call-mapper-utils.js +208 -0
  192. package/dist/server/server/agent/providers/trae-acp-agent.d.ts +15 -0
  193. package/dist/server/server/agent/providers/trae-acp-agent.js +18 -0
  194. package/dist/server/server/agent/recordings-debug.d.ts +3 -0
  195. package/dist/server/server/agent/recordings-debug.js +19 -0
  196. package/dist/server/server/agent/rewind/rewind.d.ts +10 -0
  197. package/dist/server/server/agent/rewind/rewind.js +29 -0
  198. package/dist/server/server/agent/rewind/test-rewind-session.d.ts +48 -0
  199. package/dist/server/server/agent/rewind/test-rewind-session.js +88 -0
  200. package/dist/server/server/agent/runtime-mcp-config.d.ts +14 -0
  201. package/dist/server/server/agent/runtime-mcp-config.js +53 -0
  202. package/dist/server/server/agent/structured-generation-providers.d.ts +29 -0
  203. package/dist/server/server/agent/structured-generation-providers.js +236 -0
  204. package/dist/server/server/agent/stt-debug.d.ts +10 -0
  205. package/dist/server/server/agent/stt-debug.js +33 -0
  206. package/dist/server/server/agent/stt-manager.d.ts +38 -0
  207. package/dist/server/server/agent/stt-manager.js +246 -0
  208. package/dist/server/server/agent/system-prompt.d.ts +2 -0
  209. package/dist/server/server/agent/system-prompt.js +8 -0
  210. package/dist/server/server/agent/timeline-append.d.ts +10 -0
  211. package/dist/server/server/agent/timeline-append.js +27 -0
  212. package/dist/server/server/agent/timeline-projection.d.ts +71 -0
  213. package/dist/server/server/agent/timeline-projection.js +373 -0
  214. package/dist/server/server/agent/tools/otto-tools.d.ts +80 -0
  215. package/dist/server/server/agent/tools/otto-tools.js +2912 -0
  216. package/dist/server/server/agent/tools/types.d.ts +36 -0
  217. package/dist/server/server/agent/tools/types.js +2 -0
  218. package/dist/server/server/agent/tts-debug.d.ts +8 -0
  219. package/dist/server/server/agent/tts-debug.js +24 -0
  220. package/dist/server/server/agent/tts-manager.d.ts +41 -0
  221. package/dist/server/server/agent/tts-manager.js +375 -0
  222. package/dist/server/server/agent-attention-policy.d.ts +29 -0
  223. package/dist/server/server/agent-attention-policy.js +36 -0
  224. package/dist/server/server/artifact/artifact-prompt.d.ts +2 -0
  225. package/dist/server/server/artifact/artifact-prompt.js +15 -0
  226. package/dist/server/server/artifact/artifact-service.d.ts +91 -0
  227. package/dist/server/server/artifact/artifact-service.js +419 -0
  228. package/dist/server/server/artifact/artifact-store.d.ts +24 -0
  229. package/dist/server/server/artifact/artifact-store.js +168 -0
  230. package/dist/server/server/artifact/artifact-watcher.d.ts +34 -0
  231. package/dist/server/server/artifact/artifact-watcher.js +146 -0
  232. package/dist/server/server/artifact/html-validator.d.ts +26 -0
  233. package/dist/server/server/artifact/html-validator.js +92 -0
  234. package/dist/server/server/atomic-file.d.ts +5 -0
  235. package/dist/server/server/atomic-file.js +19 -0
  236. package/dist/server/server/auth.d.ts +38 -0
  237. package/dist/server/server/auth.js +143 -0
  238. package/dist/server/server/auto-archive-on-merge/archive-if-safe.d.ts +40 -0
  239. package/dist/server/server/auto-archive-on-merge/archive-if-safe.js +92 -0
  240. package/dist/server/server/auto-archive-on-merge/index.d.ts +8 -0
  241. package/dist/server/server/auto-archive-on-merge/index.js +15 -0
  242. package/dist/server/server/bootstrap.d.ts +127 -0
  243. package/dist/server/server/bootstrap.js +1331 -0
  244. package/dist/server/server/browser-tools/broker.d.ts +45 -0
  245. package/dist/server/server/browser-tools/broker.js +390 -0
  246. package/dist/server/server/browser-tools/errors.d.ts +22 -0
  247. package/dist/server/server/browser-tools/errors.js +27 -0
  248. package/dist/server/server/browser-tools/policy.d.ts +10 -0
  249. package/dist/server/server/browser-tools/policy.js +16 -0
  250. package/dist/server/server/browser-tools/tools.d.ts +24 -0
  251. package/dist/server/server/browser-tools/tools.js +967 -0
  252. package/dist/server/server/chat/chat-mentions.d.ts +47 -0
  253. package/dist/server/server/chat/chat-mentions.js +113 -0
  254. package/dist/server/server/chat/chat-service.d.ts +78 -0
  255. package/dist/server/server/chat/chat-service.js +337 -0
  256. package/dist/server/server/checkout/status-projection.d.ts +19 -0
  257. package/dist/server/server/checkout/status-projection.js +117 -0
  258. package/dist/server/server/checkout-diff-manager.d.ts +41 -0
  259. package/dist/server/server/checkout-diff-manager.js +189 -0
  260. package/dist/server/server/checkout-git-utils.d.ts +11 -0
  261. package/dist/server/server/checkout-git-utils.js +34 -0
  262. package/dist/server/server/client-message-id.d.ts +3 -0
  263. package/dist/server/server/client-message-id.js +12 -0
  264. package/dist/server/server/config.d.ts +22 -0
  265. package/dist/server/server/config.js +330 -0
  266. package/dist/server/server/connection-offer.d.ts +20 -0
  267. package/dist/server/server/connection-offer.js +59 -0
  268. package/dist/server/server/daemon-config-store.d.ts +44 -0
  269. package/dist/server/server/daemon-config-store.js +530 -0
  270. package/dist/server/server/daemon-keypair.d.ts +8 -0
  271. package/dist/server/server/daemon-keypair.js +42 -0
  272. package/dist/server/server/daemon-version.d.ts +5 -0
  273. package/dist/server/server/daemon-version.js +22 -0
  274. package/dist/server/server/daemon-worker.d.ts +2 -0
  275. package/dist/server/server/daemon-worker.js +251 -0
  276. package/dist/server/server/dictation/dictation-stream-manager.d.ts +87 -0
  277. package/dist/server/server/dictation/dictation-stream-manager.js +572 -0
  278. package/dist/server/server/exports.d.ts +24 -0
  279. package/dist/server/server/exports.js +24 -0
  280. package/dist/server/server/file-download/token-store.d.ts +24 -0
  281. package/dist/server/server/file-download/token-store.js +40 -0
  282. package/dist/server/server/file-explorer/code-index.d.ts +28 -0
  283. package/dist/server/server/file-explorer/code-index.js +127 -0
  284. package/dist/server/server/file-explorer/file-search.d.ts +72 -0
  285. package/dist/server/server/file-explorer/file-search.js +297 -0
  286. package/dist/server/server/file-explorer/file-watcher.d.ts +40 -0
  287. package/dist/server/server/file-explorer/file-watcher.js +0 -0
  288. package/dist/server/server/file-explorer/gitignore.d.ts +39 -0
  289. package/dist/server/server/file-explorer/gitignore.js +191 -0
  290. package/dist/server/server/file-explorer/service.d.ts +103 -0
  291. package/dist/server/server/file-explorer/service.js +420 -0
  292. package/dist/server/server/file-upload/index.d.ts +27 -0
  293. package/dist/server/server/file-upload/index.js +158 -0
  294. package/dist/server/server/hostnames.d.ts +13 -0
  295. package/dist/server/server/hostnames.js +94 -0
  296. package/dist/server/server/json-utils.d.ts +11 -0
  297. package/dist/server/server/json-utils.js +45 -0
  298. package/dist/server/server/lifecycle-reasons.d.ts +4 -0
  299. package/dist/server/server/lifecycle-reasons.js +6 -0
  300. package/dist/server/server/logger.d.ts +29 -0
  301. package/dist/server/server/logger.js +125 -0
  302. package/dist/server/server/loop-service.d.ts +234 -0
  303. package/dist/server/server/loop-service.js +804 -0
  304. package/dist/server/server/managed-processes/managed-processes.d.ts +76 -0
  305. package/dist/server/server/managed-processes/managed-processes.js +326 -0
  306. package/dist/server/server/messages.d.ts +9 -0
  307. package/dist/server/server/messages.js +29 -0
  308. package/dist/server/server/migrations/backfill-workspace-id.migration.d.ts +9 -0
  309. package/dist/server/server/migrations/backfill-workspace-id.migration.js +64 -0
  310. package/dist/server/server/otto-env.d.ts +13 -0
  311. package/dist/server/server/otto-env.js +53 -0
  312. package/dist/server/server/otto-home.d.ts +2 -0
  313. package/dist/server/server/otto-home.js +19 -0
  314. package/dist/server/server/otto-worktree-service.d.ts +52 -0
  315. package/dist/server/server/otto-worktree-service.js +294 -0
  316. package/dist/server/server/package-version.d.ts +19 -0
  317. package/dist/server/server/package-version.js +60 -0
  318. package/dist/server/server/pagination/cursor.d.ts +16 -0
  319. package/dist/server/server/pagination/cursor.js +62 -0
  320. package/dist/server/server/pagination/sortable-pager.d.ts +24 -0
  321. package/dist/server/server/pagination/sortable-pager.js +68 -0
  322. package/dist/server/server/pairing-offer.d.ts +18 -0
  323. package/dist/server/server/pairing-offer.js +47 -0
  324. package/dist/server/server/pairing-qr.d.ts +7 -0
  325. package/dist/server/server/pairing-qr.js +45 -0
  326. package/dist/server/server/path-utils.d.ts +5 -0
  327. package/dist/server/server/path-utils.js +34 -0
  328. package/dist/server/server/persisted-config.d.ts +417 -0
  329. package/dist/server/server/persisted-config.js +471 -0
  330. package/dist/server/server/persistence-hooks.d.ts +31 -0
  331. package/dist/server/server/persistence-hooks.js +93 -0
  332. package/dist/server/server/pid-lock.d.ts +32 -0
  333. package/dist/server/server/pid-lock.js +170 -0
  334. package/dist/server/server/preview/dev-server-manager.d.ts +108 -0
  335. package/dist/server/server/preview/dev-server-manager.js +426 -0
  336. package/dist/server/server/preview/launch-config.d.ts +37 -0
  337. package/dist/server/server/preview/launch-config.js +63 -0
  338. package/dist/server/server/preview/preview-tools.d.ts +30 -0
  339. package/dist/server/server/preview/preview-tools.js +249 -0
  340. package/dist/server/server/private-files.d.ts +6 -0
  341. package/dist/server/server/private-files.js +37 -0
  342. package/dist/server/server/process-diagnostics.d.ts +17 -0
  343. package/dist/server/server/process-diagnostics.js +22 -0
  344. package/dist/server/server/push/notifications.d.ts +9 -0
  345. package/dist/server/server/push/notifications.js +15 -0
  346. package/dist/server/server/push/push-service.d.ts +20 -0
  347. package/dist/server/server/push/push-service.js +66 -0
  348. package/dist/server/server/push/token-store.d.ts +18 -0
  349. package/dist/server/server/push/token-store.js +68 -0
  350. package/dist/server/server/relay-transport.d.ts +31 -0
  351. package/dist/server/server/relay-transport.js +514 -0
  352. package/dist/server/server/resolve-workspace-id-for-path.d.ts +3 -0
  353. package/dist/server/server/resolve-workspace-id-for-path.js +41 -0
  354. package/dist/server/server/resolve-worktree-creation-intent.d.ts +32 -0
  355. package/dist/server/server/resolve-worktree-creation-intent.js +121 -0
  356. package/dist/server/server/schedule/cron.d.ts +4 -0
  357. package/dist/server/server/schedule/cron.js +81 -0
  358. package/dist/server/server/schedule/service.d.ts +89 -0
  359. package/dist/server/server/schedule/service.js +882 -0
  360. package/dist/server/server/schedule/store.d.ts +27 -0
  361. package/dist/server/server/schedule/store.js +190 -0
  362. package/dist/server/server/script-health-monitor.d.ts +39 -0
  363. package/dist/server/server/script-health-monitor.js +159 -0
  364. package/dist/server/server/script-proxy.d.ts +3 -0
  365. package/dist/server/server/script-proxy.js +2 -0
  366. package/dist/server/server/script-route-branch-handler.d.ts +10 -0
  367. package/dist/server/server/script-route-branch-handler.js +11 -0
  368. package/dist/server/server/script-status-projection.d.ts +36 -0
  369. package/dist/server/server/script-status-projection.js +200 -0
  370. package/dist/server/server/server-id.d.ts +17 -0
  371. package/dist/server/server/server-id.js +68 -0
  372. package/dist/server/server/service-proxy.d.ts +237 -0
  373. package/dist/server/server/service-proxy.js +714 -0
  374. package/dist/server/server/session/agent-config/agent-config-session.d.ts +59 -0
  375. package/dist/server/server/session/agent-config/agent-config-session.js +110 -0
  376. package/dist/server/server/session/agent-updates/agent-updates-service.d.ts +65 -0
  377. package/dist/server/server/session/agent-updates/agent-updates-service.js +232 -0
  378. package/dist/server/server/session/artifact/artifact-session.d.ts +45 -0
  379. package/dist/server/server/session/artifact/artifact-session.js +293 -0
  380. package/dist/server/server/session/chat/chat-schedule-loop-session.d.ts +120 -0
  381. package/dist/server/server/session/chat/chat-schedule-loop-session.js +489 -0
  382. package/dist/server/server/session/checkout/checkout-session.d.ts +146 -0
  383. package/dist/server/server/session/checkout/checkout-session.js +1000 -0
  384. package/dist/server/server/session/checkout/git-metadata-generator.d.ts +53 -0
  385. package/dist/server/server/session/checkout/git-metadata-generator.js +159 -0
  386. package/dist/server/server/session/daemon/daemon-self-update-session-controller.d.ts +32 -0
  387. package/dist/server/server/session/daemon/daemon-self-update-session-controller.js +88 -0
  388. package/dist/server/server/session/daemon/daemon-self-updater.d.ts +32 -0
  389. package/dist/server/server/session/daemon/daemon-self-updater.js +56 -0
  390. package/dist/server/server/session/daemon/daemon-session.d.ts +76 -0
  391. package/dist/server/server/session/daemon/daemon-session.js +148 -0
  392. package/dist/server/server/session/daemon/diagnostics.d.ts +41 -0
  393. package/dist/server/server/session/daemon/diagnostics.js +431 -0
  394. package/dist/server/server/session/daemon/install-origin.d.ts +7 -0
  395. package/dist/server/server/session/daemon/install-origin.js +64 -0
  396. package/dist/server/server/session/daemon/npm-global-cli.d.ts +29 -0
  397. package/dist/server/server/session/daemon/npm-global-cli.js +98 -0
  398. package/dist/server/server/session/files/workspace-files-session.d.ts +60 -0
  399. package/dist/server/server/session/files/workspace-files-session.js +518 -0
  400. package/dist/server/server/session/git-mutation/git-mutation-service.d.ts +34 -0
  401. package/dist/server/server/session/git-mutation/git-mutation-service.js +71 -0
  402. package/dist/server/server/session/project-config/project-config-session.d.ts +34 -0
  403. package/dist/server/server/session/project-config/project-config-session.js +125 -0
  404. package/dist/server/server/session/provider/provider-catalog-session.d.ts +74 -0
  405. package/dist/server/server/session/provider/provider-catalog-session.js +343 -0
  406. package/dist/server/server/session/voice/voice-session.d.ts +167 -0
  407. package/dist/server/server/session/voice/voice-session.js +912 -0
  408. package/dist/server/server/session/voice/voice-turn-controller.d.ts +38 -0
  409. package/dist/server/server/session/voice/voice-turn-controller.js +393 -0
  410. package/dist/server/server/session/workspace-git-observer/workspace-git-observer-service.d.ts +36 -0
  411. package/dist/server/server/session/workspace-git-observer/workspace-git-observer-service.js +134 -0
  412. package/dist/server/server/session/workspace-provisioning/workspace-provisioning-service.d.ts +34 -0
  413. package/dist/server/server/session/workspace-provisioning/workspace-provisioning-service.js +190 -0
  414. package/dist/server/server/session/workspace-scripts/workspace-scripts-service.d.ts +41 -0
  415. package/dist/server/server/session/workspace-scripts/workspace-scripts-service.js +100 -0
  416. package/dist/server/server/session.d.ts +450 -0
  417. package/dist/server/server/session.js +4763 -0
  418. package/dist/server/server/speech/audio.d.ts +10 -0
  419. package/dist/server/server/speech/audio.js +101 -0
  420. package/dist/server/server/speech/provider-resolver.d.ts +3 -0
  421. package/dist/server/server/speech/provider-resolver.js +7 -0
  422. package/dist/server/server/speech/providers/local/config.d.ts +30 -0
  423. package/dist/server/server/speech/providers/local/config.js +133 -0
  424. package/dist/server/server/speech/providers/local/models.d.ts +13 -0
  425. package/dist/server/server/speech/providers/local/models.js +21 -0
  426. package/dist/server/server/speech/providers/local/runtime.d.ts +31 -0
  427. package/dist/server/server/speech/providers/local/runtime.js +135 -0
  428. package/dist/server/server/speech/providers/local/sherpa/assets/silero_vad.onnx +0 -0
  429. package/dist/server/server/speech/providers/local/sherpa/model-catalog.d.ts +109 -0
  430. package/dist/server/server/speech/providers/local/sherpa/model-catalog.js +129 -0
  431. package/dist/server/server/speech/providers/local/sherpa/model-downloader.d.ts +16 -0
  432. package/dist/server/server/speech/providers/local/sherpa/model-downloader.js +195 -0
  433. package/dist/server/server/speech/providers/local/sherpa/sherpa-offline-recognizer.d.ts +49 -0
  434. package/dist/server/server/speech/providers/local/sherpa/sherpa-offline-recognizer.js +73 -0
  435. package/dist/server/server/speech/providers/local/sherpa/sherpa-onnx-node-loader.d.ts +9 -0
  436. package/dist/server/server/speech/providers/local/sherpa/sherpa-onnx-node-loader.js +107 -0
  437. package/dist/server/server/speech/providers/local/sherpa/sherpa-parakeet-realtime-session.d.ts +28 -0
  438. package/dist/server/server/speech/providers/local/sherpa/sherpa-parakeet-realtime-session.js +137 -0
  439. package/dist/server/server/speech/providers/local/sherpa/sherpa-parakeet-stt.d.ts +21 -0
  440. package/dist/server/server/speech/providers/local/sherpa/sherpa-parakeet-stt.js +133 -0
  441. package/dist/server/server/speech/providers/local/sherpa/sherpa-runtime-env.d.ts +18 -0
  442. package/dist/server/server/speech/providers/local/sherpa/sherpa-runtime-env.js +84 -0
  443. package/dist/server/server/speech/providers/local/sherpa/sherpa-tts.d.ts +24 -0
  444. package/dist/server/server/speech/providers/local/sherpa/sherpa-tts.js +145 -0
  445. package/dist/server/server/speech/providers/local/sherpa/silero-vad-provider.d.ts +19 -0
  446. package/dist/server/server/speech/providers/local/sherpa/silero-vad-provider.js +49 -0
  447. package/dist/server/server/speech/providers/local/sherpa/silero-vad-session.d.ts +38 -0
  448. package/dist/server/server/speech/providers/local/sherpa/silero-vad-session.js +176 -0
  449. package/dist/server/server/speech/providers/local/sherpa/tts-voices.d.ts +10 -0
  450. package/dist/server/server/speech/providers/local/sherpa/tts-voices.js +86 -0
  451. package/dist/server/server/speech/providers/local/worker-bytes.d.ts +4 -0
  452. package/dist/server/server/speech/providers/local/worker-bytes.js +9 -0
  453. package/dist/server/server/speech/providers/local/worker-client.d.ts +93 -0
  454. package/dist/server/server/speech/providers/local/worker-client.js +591 -0
  455. package/dist/server/server/speech/providers/local/worker-process.d.ts +2 -0
  456. package/dist/server/server/speech/providers/local/worker-process.js +270 -0
  457. package/dist/server/server/speech/providers/local/worker-protocol.d.ts +96 -0
  458. package/dist/server/server/speech/providers/local/worker-protocol.js +2 -0
  459. package/dist/server/server/speech/providers/openai/config.d.ts +19 -0
  460. package/dist/server/server/speech/providers/openai/config.js +160 -0
  461. package/dist/server/server/speech/providers/openai/runtime.d.ts +29 -0
  462. package/dist/server/server/speech/providers/openai/runtime.js +102 -0
  463. package/dist/server/server/speech/providers/openai/stt.d.ts +23 -0
  464. package/dist/server/server/speech/providers/openai/stt.js +209 -0
  465. package/dist/server/server/speech/providers/openai/tts.d.ts +20 -0
  466. package/dist/server/server/speech/providers/openai/tts.js +70 -0
  467. package/dist/server/server/speech/speech-config-resolver.d.ts +11 -0
  468. package/dist/server/server/speech/speech-config-resolver.js +132 -0
  469. package/dist/server/server/speech/speech-provider.d.ts +71 -0
  470. package/dist/server/server/speech/speech-provider.js +2 -0
  471. package/dist/server/server/speech/speech-runtime.d.ts +67 -0
  472. package/dist/server/server/speech/speech-runtime.js +615 -0
  473. package/dist/server/server/speech/speech-settings-options.d.ts +14 -0
  474. package/dist/server/server/speech/speech-settings-options.js +73 -0
  475. package/dist/server/server/speech/speech-types.d.ts +22 -0
  476. package/dist/server/server/speech/speech-types.js +8 -0
  477. package/dist/server/server/speech/turn-detection-provider.d.ts +23 -0
  478. package/dist/server/server/speech/turn-detection-provider.js +2 -0
  479. package/dist/server/server/utils/diff-highlighter.d.ts +57 -0
  480. package/dist/server/server/utils/diff-highlighter.js +279 -0
  481. package/dist/server/server/voice-config.d.ts +15 -0
  482. package/dist/server/server/voice-config.js +54 -0
  483. package/dist/server/server/voice-permission-policy.d.ts +4 -0
  484. package/dist/server/server/voice-permission-policy.js +13 -0
  485. package/dist/server/server/voice-types.d.ts +12 -0
  486. package/dist/server/server/voice-types.js +2 -0
  487. package/dist/server/server/web-ui.d.ts +10 -0
  488. package/dist/server/server/web-ui.js +205 -0
  489. package/dist/server/server/websocket/runtime-metrics.d.ts +94 -0
  490. package/dist/server/server/websocket/runtime-metrics.js +148 -0
  491. package/dist/server/server/websocket-server.d.ts +187 -0
  492. package/dist/server/server/websocket-server.js +1766 -0
  493. package/dist/server/server/workspace-archive-service.d.ts +65 -0
  494. package/dist/server/server/workspace-archive-service.js +229 -0
  495. package/dist/server/server/workspace-auto-name.d.ts +55 -0
  496. package/dist/server/server/workspace-auto-name.js +128 -0
  497. package/dist/server/server/workspace-bootstrap-dedupe.d.ts +34 -0
  498. package/dist/server/server/workspace-bootstrap-dedupe.js +23 -0
  499. package/dist/server/server/workspace-directory.d.ts +93 -0
  500. package/dist/server/server/workspace-directory.js +422 -0
  501. package/dist/server/server/workspace-git-metadata.d.ts +24 -0
  502. package/dist/server/server/workspace-git-metadata.js +55 -0
  503. package/dist/server/server/workspace-git-service.d.ts +270 -0
  504. package/dist/server/server/workspace-git-service.js +1423 -0
  505. package/dist/server/server/workspace-reconciliation-service.d.ts +57 -0
  506. package/dist/server/server/workspace-reconciliation-service.js +278 -0
  507. package/dist/server/server/workspace-registry-bootstrap.d.ts +13 -0
  508. package/dist/server/server/workspace-registry-bootstrap.js +121 -0
  509. package/dist/server/server/workspace-registry-model.d.ts +55 -0
  510. package/dist/server/server/workspace-registry-model.js +190 -0
  511. package/dist/server/server/workspace-registry.d.ts +117 -0
  512. package/dist/server/server/workspace-registry.js +194 -0
  513. package/dist/server/server/workspace-script-runtime-store.d.ts +28 -0
  514. package/dist/server/server/workspace-script-runtime-store.js +78 -0
  515. package/dist/server/server/workspace-service-env.d.ts +18 -0
  516. package/dist/server/server/workspace-service-env.js +85 -0
  517. package/dist/server/server/workspace-service-port-registry.d.ts +19 -0
  518. package/dist/server/server/workspace-service-port-registry.js +59 -0
  519. package/dist/server/server/worktree/commands.d.ts +55 -0
  520. package/dist/server/server/worktree/commands.js +107 -0
  521. package/dist/server/server/worktree-bootstrap.d.ts +76 -0
  522. package/dist/server/server/worktree-bootstrap.js +720 -0
  523. package/dist/server/server/worktree-branch-name-generator.d.ts +35 -0
  524. package/dist/server/server/worktree-branch-name-generator.js +91 -0
  525. package/dist/server/server/worktree-core.d.ts +31 -0
  526. package/dist/server/server/worktree-core.js +105 -0
  527. package/dist/server/server/worktree-errors.d.ts +12 -0
  528. package/dist/server/server/worktree-errors.js +28 -0
  529. package/dist/server/server/worktree-session.d.ts +152 -0
  530. package/dist/server/server/worktree-session.js +466 -0
  531. package/dist/server/services/git-hosting/bitbucket-cloud-service.d.ts +24 -0
  532. package/dist/server/services/git-hosting/bitbucket-cloud-service.js +754 -0
  533. package/dist/server/services/git-hosting/github-hosting-service.d.ts +4 -0
  534. package/dist/server/services/git-hosting/github-hosting-service.js +11 -0
  535. package/dist/server/services/git-hosting/hosting-http-client.d.ts +20 -0
  536. package/dist/server/services/git-hosting/hosting-http-client.js +131 -0
  537. package/dist/server/services/git-hosting/request-cache.d.ts +20 -0
  538. package/dist/server/services/git-hosting/request-cache.js +68 -0
  539. package/dist/server/services/git-hosting/resolver.d.ts +32 -0
  540. package/dist/server/services/git-hosting/resolver.js +152 -0
  541. package/dist/server/services/git-hosting/router.d.ts +4 -0
  542. package/dist/server/services/git-hosting/router.js +105 -0
  543. package/dist/server/services/git-hosting/status-poller.d.ts +39 -0
  544. package/dist/server/services/git-hosting/status-poller.js +149 -0
  545. package/dist/server/services/git-hosting/types.d.ts +53 -0
  546. package/dist/server/services/git-hosting/types.js +75 -0
  547. package/dist/server/services/github-service.d.ts +339 -0
  548. package/dist/server/services/github-service.js +2109 -0
  549. package/dist/server/services/quota-fetcher/manifest.d.ts +4 -0
  550. package/dist/server/services/quota-fetcher/manifest.js +52 -0
  551. package/dist/server/services/quota-fetcher/provider.d.ts +17 -0
  552. package/dist/server/services/quota-fetcher/provider.js +2 -0
  553. package/dist/server/services/quota-fetcher/providers/claude.d.ts +26 -0
  554. package/dist/server/services/quota-fetcher/providers/claude.js +217 -0
  555. package/dist/server/services/quota-fetcher/providers/codex.d.ts +23 -0
  556. package/dist/server/services/quota-fetcher/providers/codex.js +211 -0
  557. package/dist/server/services/quota-fetcher/providers/copilot.d.ts +17 -0
  558. package/dist/server/services/quota-fetcher/providers/copilot.js +75 -0
  559. package/dist/server/services/quota-fetcher/providers/cursor.d.ts +17 -0
  560. package/dist/server/services/quota-fetcher/providers/cursor.js +123 -0
  561. package/dist/server/services/quota-fetcher/providers/grok.d.ts +18 -0
  562. package/dist/server/services/quota-fetcher/providers/grok.js +89 -0
  563. package/dist/server/services/quota-fetcher/providers/kimi.d.ts +20 -0
  564. package/dist/server/services/quota-fetcher/providers/kimi.js +89 -0
  565. package/dist/server/services/quota-fetcher/providers/minimax.d.ts +29 -0
  566. package/dist/server/services/quota-fetcher/providers/minimax.js +227 -0
  567. package/dist/server/services/quota-fetcher/providers/zai.d.ts +17 -0
  568. package/dist/server/services/quota-fetcher/providers/zai.js +58 -0
  569. package/dist/server/services/quota-fetcher/service.d.ts +28 -0
  570. package/dist/server/services/quota-fetcher/service.js +58 -0
  571. package/dist/server/services/quota-fetcher/usage.d.ts +22 -0
  572. package/dist/server/services/quota-fetcher/usage.js +49 -0
  573. package/dist/server/terminal/activity/terminal-activity-tracker.d.ts +20 -0
  574. package/dist/server/terminal/activity/terminal-activity-tracker.js +59 -0
  575. package/dist/server/terminal/agent-hooks/agent-hook-installer.d.ts +62 -0
  576. package/dist/server/terminal/agent-hooks/agent-hook-installer.js +117 -0
  577. package/dist/server/terminal/agent-hooks/claude/claude-settings.d.ts +7 -0
  578. package/dist/server/terminal/agent-hooks/claude/claude-settings.js +88 -0
  579. package/dist/server/terminal/agent-hooks/claude/claude.d.ts +4 -0
  580. package/dist/server/terminal/agent-hooks/claude/claude.js +47 -0
  581. package/dist/server/terminal/agent-hooks/codex/codex-settings.d.ts +7 -0
  582. package/dist/server/terminal/agent-hooks/codex/codex-settings.js +99 -0
  583. package/dist/server/terminal/agent-hooks/codex/codex.d.ts +4 -0
  584. package/dist/server/terminal/agent-hooks/codex/codex.js +30 -0
  585. package/dist/server/terminal/agent-hooks/opencode/opencode-plugin.d.ts +4 -0
  586. package/dist/server/terminal/agent-hooks/opencode/opencode-plugin.js +46 -0
  587. package/dist/server/terminal/agent-hooks/opencode/opencode.d.ts +3 -0
  588. package/dist/server/terminal/agent-hooks/opencode/opencode.js +23 -0
  589. package/dist/server/terminal/agent-hooks/provider-registry.d.ts +24 -0
  590. package/dist/server/terminal/agent-hooks/provider-registry.js +36 -0
  591. package/dist/server/terminal/agent-hooks/terminal-agent-hook-setting.d.ts +10 -0
  592. package/dist/server/terminal/agent-hooks/terminal-agent-hook-setting.js +26 -0
  593. package/dist/server/terminal/shell-integration/zsh/.zshenv +17 -0
  594. package/dist/server/terminal/shell-integration/zsh/otto-integration.zsh +32 -0
  595. package/dist/server/terminal/terminal-capture.d.ts +12 -0
  596. package/dist/server/terminal/terminal-capture.js +43 -0
  597. package/dist/server/terminal/terminal-manager-factory.d.ts +6 -0
  598. package/dist/server/terminal/terminal-manager-factory.js +5 -0
  599. package/dist/server/terminal/terminal-manager.d.ts +78 -0
  600. package/dist/server/terminal/terminal-manager.js +319 -0
  601. package/dist/server/terminal/terminal-output-coalescer.d.ts +34 -0
  602. package/dist/server/terminal/terminal-output-coalescer.js +73 -0
  603. package/dist/server/terminal/terminal-restore.d.ts +18 -0
  604. package/dist/server/terminal/terminal-restore.js +52 -0
  605. package/dist/server/terminal/terminal-session-controller.d.ts +85 -0
  606. package/dist/server/terminal/terminal-session-controller.js +819 -0
  607. package/dist/server/terminal/terminal-ts-loader.mjs +20 -0
  608. package/dist/server/terminal/terminal-worker-process.d.ts +2 -0
  609. package/dist/server/terminal/terminal-worker-process.js +304 -0
  610. package/dist/server/terminal/terminal-worker-protocol.d.ts +119 -0
  611. package/dist/server/terminal/terminal-worker-protocol.js +2 -0
  612. package/dist/server/terminal/terminal.d.ts +151 -0
  613. package/dist/server/terminal/terminal.js +1155 -0
  614. package/dist/server/terminal/worker-terminal-manager.d.ts +20 -0
  615. package/dist/server/terminal/worker-terminal-manager.js +688 -0
  616. package/dist/server/utils/build-metadata-prompt.d.ts +19 -0
  617. package/dist/server/utils/build-metadata-prompt.js +29 -0
  618. package/dist/server/utils/checkout-git.d.ts +253 -0
  619. package/dist/server/utils/checkout-git.js +2241 -0
  620. package/dist/server/utils/directory-suggestions.d.ts +26 -0
  621. package/dist/server/utils/directory-suggestions.js +830 -0
  622. package/dist/server/utils/git-rev-parse-path.d.ts +3 -0
  623. package/dist/server/utils/git-rev-parse-path.js +21 -0
  624. package/dist/server/utils/github-remote.d.ts +9 -0
  625. package/dist/server/utils/github-remote.js +62 -0
  626. package/dist/server/utils/otto-config-file.d.ts +30 -0
  627. package/dist/server/utils/otto-config-file.js +90 -0
  628. package/dist/server/utils/path.d.ts +18 -0
  629. package/dist/server/utils/path.js +123 -0
  630. package/dist/server/utils/project-icon.d.ts +39 -0
  631. package/dist/server/utils/project-icon.js +364 -0
  632. package/dist/server/utils/promise-timeout.d.ts +9 -0
  633. package/dist/server/utils/promise-timeout.js +25 -0
  634. package/dist/server/utils/run-git-command.d.ts +37 -0
  635. package/dist/server/utils/run-git-command.js +232 -0
  636. package/dist/server/utils/spawn.d.ts +25 -0
  637. package/dist/server/utils/spawn.js +59 -0
  638. package/dist/server/utils/string-command-shell.d.ts +13 -0
  639. package/dist/server/utils/string-command-shell.js +37 -0
  640. package/dist/server/utils/tree-kill.d.ts +18 -0
  641. package/dist/server/utils/tree-kill.js +77 -0
  642. package/dist/server/utils/windows-command.d.ts +15 -0
  643. package/dist/server/utils/windows-command.js +41 -0
  644. package/dist/server/utils/worktree-metadata.d.ts +39 -0
  645. package/dist/server/utils/worktree-metadata.js +174 -0
  646. package/dist/server/utils/worktree.d.ts +205 -0
  647. package/dist/server/utils/worktree.js +1120 -0
  648. package/dist/server/web-ui/_expo/static/css/xterm-3bb1704bf6cb0876640973dc0244b4cb.css +1 -0
  649. package/dist/server/web-ui/_expo/static/css/xterm-3bb1704bf6cb0876640973dc0244b4cb.css.br +0 -0
  650. package/dist/server/web-ui/_expo/static/css/xterm-3bb1704bf6cb0876640973dc0244b4cb.css.gz +0 -0
  651. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-a25c6055b348629689f75618db2c83e1.js +1 -0
  652. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-a25c6055b348629689f75618db2c83e1.js.br +0 -0
  653. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-a25c6055b348629689f75618db2c83e1.js.gz +0 -0
  654. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-7dc26ea1611eb9d7c2971a6745e5bf5c.js +1 -0
  655. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-7dc26ea1611eb9d7c2971a6745e5bf5c.js.br +0 -0
  656. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-7dc26ea1611eb9d7c2971a6745e5bf5c.js.gz +0 -0
  657. package/dist/server/web-ui/_expo/static/js/web/index-ea91f496cee180fa5bf308c8cc177646.js +3199 -0
  658. package/dist/server/web-ui/_expo/static/js/web/index-ea91f496cee180fa5bf308c8cc177646.js.br +0 -0
  659. package/dist/server/web-ui/_expo/static/js/web/index-ea91f496cee180fa5bf308c8cc177646.js.gz +0 -0
  660. package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-b6b853a967343babf305512fe8c5958c.js +1 -0
  661. package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-b6b853a967343babf305512fe8c5958c.js.br +0 -0
  662. package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-b6b853a967343babf305512fe8c5958c.js.gz +0 -0
  663. package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-17223e158406802adc686d02d946573d.js +3 -0
  664. package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-17223e158406802adc686d02d946573d.js.br +0 -0
  665. package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-17223e158406802adc686d02d946573d.js.gz +0 -0
  666. package/dist/server/web-ui/apple-touch-icon.png +0 -0
  667. package/dist/server/web-ui/assets/__node_modules/@expo-google-fonts/inter/400Regular/Inter_400Regular.51b6ad87261f18b6433ec52871ddfabc.ttf +0 -0
  668. package/dist/server/web-ui/assets/__node_modules/@expo-google-fonts/jetbrains-mono/400Regular/JetBrainsMono_400Regular.a0147b5ab9e4946e81879aef45313def.ttf +0 -0
  669. package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/back-icon-mask.0a328cd9c1afd0afe8e3b1ec5165b1b4.png +0 -0
  670. package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/back-icon.35ba0eaec5a4f5ed12ca16fabeae451d.png +0 -0
  671. package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/clear-icon.c94f6478e7ae0cdd9f15de1fcb9e5e55.png +0 -0
  672. package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/clear-icon.c94f6478e7ae0cdd9f15de1fcb9e5e55@2x.png +0 -0
  673. package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/clear-icon.c94f6478e7ae0cdd9f15de1fcb9e5e55@3x.png +0 -0
  674. package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/clear-icon.c94f6478e7ae0cdd9f15de1fcb9e5e55@4x.png +0 -0
  675. package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/close-icon.808e1b1b9b53114ec2838071a7e6daa7.png +0 -0
  676. package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/close-icon.808e1b1b9b53114ec2838071a7e6daa7@2x.png +0 -0
  677. package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/close-icon.808e1b1b9b53114ec2838071a7e6daa7@3x.png +0 -0
  678. package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/close-icon.808e1b1b9b53114ec2838071a7e6daa7@4x.png +0 -0
  679. package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/search-icon.286d67d3f74808a60a78d3ebf1a5fb57.png +0 -0
  680. package/dist/server/web-ui/assets/__node_modules/expo-router/assets/arrow_down.017bc6ba3fc25503e5eb5e53826d48a8.png +0 -0
  681. package/dist/server/web-ui/assets/__node_modules/expo-router/assets/error.d1ea1496f9057eb392d5bbf3732a61b7.png +0 -0
  682. package/dist/server/web-ui/assets/__node_modules/expo-router/assets/file.19eeb73b9593a38f8e9f418337fc7d10.png +0 -0
  683. package/dist/server/web-ui/assets/__node_modules/expo-router/assets/forward.d8b800c443b8972542883e0b9de2bdc6.png +0 -0
  684. package/dist/server/web-ui/assets/__node_modules/expo-router/assets/pkg.ab19f4cbc543357183a20571f68380a3.png +0 -0
  685. package/dist/server/web-ui/assets/__node_modules/expo-router/assets/sitemap.412dd9275b6b48ad28f5e3d81bb1f626.png +0 -0
  686. package/dist/server/web-ui/assets/__node_modules/expo-router/assets/unmatched.20e71bdf79e3a97bf55fd9e164041578.png +0 -0
  687. package/dist/server/web-ui/assets/assets/images/editor-apps/antigravity.6e91a685c33435e0b466a56db86cf141.png +0 -0
  688. package/dist/server/web-ui/assets/assets/images/editor-apps/clion.baa1f4eb572e962a6117bdbee95749e4.png +0 -0
  689. package/dist/server/web-ui/assets/assets/images/editor-apps/cursor.c31d6bce4fe9aadc3fe59962f4c4fcf3.png +0 -0
  690. package/dist/server/web-ui/assets/assets/images/editor-apps/finder.7f68fc2c475621a672e1be09309d5567.png +0 -0
  691. package/dist/server/web-ui/assets/assets/images/editor-apps/goland.0796ab2b9be2637fd1cdcde891fad3ca.png +0 -0
  692. package/dist/server/web-ui/assets/assets/images/editor-apps/intellij-idea.33bc9ff9c0815d8ad43f618c8c244401.png +0 -0
  693. package/dist/server/web-ui/assets/assets/images/editor-apps/phpstorm.c60b33bc7a8309143d232e14b76554c5.png +0 -0
  694. package/dist/server/web-ui/assets/assets/images/editor-apps/pycharm.88f25ac484989c45c25323e0adccfa9f.png +0 -0
  695. package/dist/server/web-ui/assets/assets/images/editor-apps/rider.1a1091dc3a0ede0461f0aa38a54f080a.png +0 -0
  696. package/dist/server/web-ui/assets/assets/images/editor-apps/rubymine.0e5b66fd7d43f8cbac0f05bba97c9a21.png +0 -0
  697. package/dist/server/web-ui/assets/assets/images/editor-apps/rustrover.3590ef05482369f41d3484c90272570d.png +0 -0
  698. package/dist/server/web-ui/assets/assets/images/editor-apps/vscode.832bdb4c685d930f1c864c793703600b.png +0 -0
  699. package/dist/server/web-ui/assets/assets/images/editor-apps/webstorm.aa5dc2cd8c20cc0a155c4c5c5ab3c5f5.png +0 -0
  700. package/dist/server/web-ui/assets/assets/images/editor-apps/zed.f3a670b7f9aa226da4fe53fb86f1abbd.png +0 -0
  701. package/dist/server/web-ui/assets/assets/images/favicon-dark-attention.3b2c331d11122d8f743c73a5878593be.png +0 -0
  702. package/dist/server/web-ui/assets/assets/images/favicon-dark-running.6077c2ab71021b31c82a42e0b42ad837.png +0 -0
  703. package/dist/server/web-ui/assets/assets/images/favicon-dark.12984efe6d435a13794a74cd2f858dcd.png +0 -0
  704. package/dist/server/web-ui/assets/assets/images/favicon-light-attention.3b2c331d11122d8f743c73a5878593be.png +0 -0
  705. package/dist/server/web-ui/assets/assets/images/favicon-light-running.6077c2ab71021b31c82a42e0b42ad837.png +0 -0
  706. package/dist/server/web-ui/assets/assets/images/favicon-light.12984efe6d435a13794a74cd2f858dcd.png +0 -0
  707. package/dist/server/web-ui/assets/assets/images/notification-icon.2394c3b427c0f5ee2bb84864d8fb0f45.png +0 -0
  708. package/dist/server/web-ui/favicon.ico +0 -0
  709. package/dist/server/web-ui/index.html +147 -0
  710. package/dist/server/web-ui/index.html.br +0 -0
  711. package/dist/server/web-ui/index.html.gz +0 -0
  712. package/dist/server/web-ui/manifest.json +27 -0
  713. package/dist/server/web-ui/manifest.json.br +0 -0
  714. package/dist/server/web-ui/manifest.json.gz +0 -0
  715. package/dist/server/web-ui/metadata.json +1 -0
  716. package/dist/server/web-ui/metadata.json.br +1 -0
  717. package/dist/server/web-ui/metadata.json.gz +0 -0
  718. package/dist/server/web-ui/pwa-icon-192.png +0 -0
  719. package/dist/server/web-ui/pwa-icon-512.png +0 -0
  720. package/dist/server/web-ui/robots.txt +2 -0
  721. package/dist/src/executable-resolution/executable-resolution.js +118 -0
  722. package/dist/src/executable-resolution/windows.js +65 -0
  723. package/dist/src/server/agent/provider-launch-config.js +167 -0
  724. package/dist/src/server/otto-env.js +53 -0
  725. package/dist/src/server/otto-home.js +19 -0
  726. package/dist/src/server/persisted-config.js +471 -0
  727. package/dist/src/server/pid-lock.js +170 -0
  728. package/dist/src/server/private-files.js +37 -0
  729. package/dist/src/server/speech/providers/local/sherpa/sherpa-runtime-env.js +84 -0
  730. package/dist/src/terminal/shell-integration/zsh/.zshenv +17 -0
  731. package/dist/src/terminal/shell-integration/zsh/otto-integration.zsh +32 -0
  732. package/dist/src/utils/spawn.js +59 -0
  733. package/dist/src/utils/windows-command.js +41 -0
  734. package/package.json +118 -0
@@ -0,0 +1,4763 @@
1
+ import equal from "fast-deep-equal";
2
+ import { v4 as uuidv4 } from "uuid";
3
+ import { stat } from "node:fs/promises";
4
+ import { basename, normalize, resolve, sep } from "path";
5
+ import { homedir } from "node:os";
6
+ import { CLIENT_CAPS } from "@otto-code/protocol/client-capabilities";
7
+ import { serializeAgentStreamEvent, } from "./messages.js";
8
+ import { TerminalSessionController } from "../terminal/terminal-session-controller.js";
9
+ import { CursorError } from "./pagination/cursor.js";
10
+ import { SortablePager } from "./pagination/sortable-pager.js";
11
+ import { EMPTY_SPEECH_SETTINGS_OPTIONS, } from "./speech/speech-settings-options.js";
12
+ import { buildConfigOverrides, extractTimestamps, isStoredAgentProviderAvailable, toAgentPersistenceHandle, } from "./persistence-hooks.js";
13
+ import { ensureAgentLoaded } from "./agent/agent-loading.js";
14
+ import { formatSystemNotificationPrompt, sendPromptToAgent, waitForAgentRunStartWithTimeout, unarchiveAgentState, } from "./agent/agent-prompt.js";
15
+ import { resolveCreateAgentTitles, resolveFirstAgentPromptTitle, } from "./agent/create-agent-title.js";
16
+ import { respondToAgentPermission } from "./agent/permission-response.js";
17
+ import { spawnWorkspaceScript } from "./worktree-bootstrap.js";
18
+ import { createWorkspaceScriptsService, } from "./session/workspace-scripts/workspace-scripts-service.js";
19
+ import { redactDaemonConfigForClient } from "./daemon-config-store.js";
20
+ import { getErrorMessage, getErrorMessageOr } from "@otto-code/protocol/error-utils";
21
+ import { getAgentStatusPriority } from "@otto-code/protocol/agent-state-bucket";
22
+ import { getParentAgentIdFromLabels } from "@otto-code/protocol/agent-labels";
23
+ import { normalizeGitHostingProviderId } from "@otto-code/protocol/messages";
24
+ import { CLIENT_SHUTDOWN_RPC_REASON, normalizeClientRestartRpcReason, } from "./lifecycle-reasons.js";
25
+ import { createAgentCommand } from "./agent/create-agent/create.js";
26
+ import { archiveAgentCommand, cancelAgentRunCommand, closeAgentCommand, detachAgentCommand, setAgentModeCommand, updateAgentCommand, } from "./agent/lifecycle-command.js";
27
+ import { buildStoredAgentPayload, resolveStoredAgentPayloadUpdatedAt, toAgentPayload, } from "./agent/agent-projections.js";
28
+ import { appendTimelineItemIfAgentKnown, emitLiveTimelineItemIfAgentKnown, } from "./agent/timeline-append.js";
29
+ import { projectTimelineRows, selectProjectedTimelinePage, } from "./agent/timeline-projection.js";
30
+ import { buildAgentForkContextAttachment } from "./agent/activity-curator.js";
31
+ import { getAgentStreamEventTurnId, } from "./agent/agent-sdk-types.js";
32
+ import { resolvePersonality, } from "./agent/agent-personalities.js";
33
+ import { ImportSessionsRequestError, importProviderSession, listImportableProviderSessions, normalizeImportAgentRequest, } from "./agent/import-sessions.js";
34
+ import { checkoutLiteFromGitSnapshot, deriveWorkspaceDisplayName, } from "./workspace-registry-model.js";
35
+ import { resolveWorkspaceIdForPath } from "./resolve-workspace-id-for-path.js";
36
+ import { resolveProjectDisplayName, resolveWorkspaceDisplayName, resolveWorkspaceName, } from "./workspace-registry.js";
37
+ import { wrapSpokenInput } from "./voice-config.js";
38
+ import { isVoicePermissionAllowed } from "./voice-permission-policy.js";
39
+ import { VoiceSession } from "./session/voice/voice-session.js";
40
+ import { CheckoutSession } from "./session/checkout/checkout-session.js";
41
+ import { createWorkspaceGitObserverService, } from "./session/workspace-git-observer/workspace-git-observer-service.js";
42
+ import { createAgentStructuredTextGeneration, createGitMetadataGenerator, } from "./session/checkout/git-metadata-generator.js";
43
+ import { ChatScheduleLoopSession } from "./session/chat/chat-schedule-loop-session.js";
44
+ import { ProviderCatalogSession } from "./session/provider/provider-catalog-session.js";
45
+ import { WorkspaceFilesSession } from "./session/files/workspace-files-session.js";
46
+ import { AgentConfigSession } from "./session/agent-config/agent-config-session.js";
47
+ import { ArtifactSession } from "./session/artifact/artifact-session.js";
48
+ import { ArtifactService } from "./artifact/artifact-service.js";
49
+ import { ProjectConfigSession } from "./session/project-config/project-config-session.js";
50
+ import { DaemonSession } from "./session/daemon/daemon-session.js";
51
+ import { readLaunchConfig, LaunchConfigError } from "./preview/launch-config.js";
52
+ import { archivePersistedWorkspaceRecord, archiveWorkspaceContents, } from "./workspace-archive-service.js";
53
+ import { WorkspaceReconciliationService } from "./workspace-reconciliation-service.js";
54
+ import { renameCurrentBranch as renameCurrentBranchDefault } from "../utils/checkout-git.js";
55
+ import { createGitMutationService, } from "./session/git-mutation/git-mutation-service.js";
56
+ import { createWorkspaceProvisioningService, } from "./session/workspace-provisioning/workspace-provisioning-service.js";
57
+ import { createAgentUpdatesService, matchesAgentUpdatesFilter, } from "./session/agent-updates/agent-updates-service.js";
58
+ import { expandTilde } from "../utils/path.js";
59
+ import { searchHomeDirectories, searchWorkspaceEntries } from "../utils/directory-suggestions.js";
60
+ import { createGitHubService } from "../services/github-service.js";
61
+ import { summarizeFetchWorkspacesEntries, workspaceIdsOnCheckout, WorkspaceDirectory, } from "./workspace-directory.js";
62
+ import { shouldEmitPendingBootstrapUpdate } from "./workspace-bootstrap-dedupe.js";
63
+ import { createLocalCheckoutWorkspace, createOttoWorktree, } from "./otto-worktree-service.js";
64
+ import { buildAgentSessionConfig as buildWorktreeAgentSessionConfig, createOttoWorktreeWorkflow as createWorktreeWorkflow, handleCreateOttoWorktreeRequest as handleCreateWorktreeRequest, handleOttoWorktreeArchiveRequest as handleWorktreeArchiveRequest, handleOttoWorktreeListRequest as handleWorktreeListRequest, handleWorkspaceSetupStatusRequest as handleWorkspaceSetupStatusRequestMessage, } from "./worktree-session.js";
65
+ import { archiveByScope } from "./workspace-archive-service.js";
66
+ import { WorktreeRequestError, toWorktreeRequestError, toWorktreeWireError, } from "./worktree-errors.js";
67
+ import { createWorktree } from "../utils/worktree.js";
68
+ import { runGitCommand } from "../utils/run-git-command.js";
69
+ import { CreateAgentLifecycleDispatch } from "./agent/create-agent-lifecycle-dispatch.js";
70
+ // TODO: Remove once all app store clients are on >=0.1.45 and understand arbitrary provider strings.
71
+ // Clients before 0.1.45 validate providers with z.enum(["claude", "codex", "opencode"]) and reject
72
+ // the entire session message if they encounter an unknown provider.
73
+ const LEGACY_PROVIDER_IDS = new Set(["claude", "codex", "opencode"]);
74
+ const MIN_VERSION_ALL_PROVIDERS = "0.1.45";
75
+ function errorToFriendlyMessage(error) {
76
+ if (error instanceof Error)
77
+ return error.message;
78
+ if (typeof error === "string")
79
+ return error;
80
+ return "Unknown error";
81
+ }
82
+ function resolveSubscriptionId(subscribe, requestedSubscriptionId) {
83
+ if (!subscribe)
84
+ return null;
85
+ if (requestedSubscriptionId && requestedSubscriptionId.length > 0) {
86
+ return requestedSubscriptionId;
87
+ }
88
+ return uuidv4();
89
+ }
90
+ function buildWorkspaceCheckout(workspace, project,
91
+ // The persisted `branch` field is the source of truth, but it is null for
92
+ // records created before branch was lifted to its own field (no migrations,
93
+ // per data-model.md) and for any path that didn't backfill it. Fall back to
94
+ // the live git branch so checkout.currentBranch never regresses to null.
95
+ fallbackBranch) {
96
+ if (project.kind !== "git") {
97
+ return {
98
+ cwd: workspace.cwd,
99
+ isGit: false,
100
+ currentBranch: null,
101
+ remoteUrl: null,
102
+ worktreeRoot: null,
103
+ isOttoOwnedWorktree: false,
104
+ mainRepoRoot: null,
105
+ };
106
+ }
107
+ const currentBranch = workspace.branch ?? fallbackBranch ?? null;
108
+ if (workspace.kind === "worktree") {
109
+ return {
110
+ cwd: workspace.cwd,
111
+ isGit: true,
112
+ currentBranch,
113
+ remoteUrl: null,
114
+ worktreeRoot: workspace.cwd,
115
+ isOttoOwnedWorktree: true,
116
+ mainRepoRoot: project.rootPath,
117
+ };
118
+ }
119
+ return {
120
+ cwd: workspace.cwd,
121
+ isGit: true,
122
+ currentBranch,
123
+ remoteUrl: null,
124
+ worktreeRoot: workspace.cwd,
125
+ isOttoOwnedWorktree: false,
126
+ mainRepoRoot: null,
127
+ };
128
+ }
129
+ function isAppVersionAtLeast(appVersion, minVersion) {
130
+ if (!appVersion)
131
+ return false;
132
+ // Strip prerelease suffix: "0.1.45-beta.4" -> "0.1.45"
133
+ const base = appVersion.replace(/-.*$/, "");
134
+ const parts = base.split(".").map(Number);
135
+ const minParts = minVersion.split(".").map(Number);
136
+ for (let i = 0; i < minParts.length; i++) {
137
+ const a = parts[i] ?? 0;
138
+ const b = minParts[i] ?? 0;
139
+ if (a > b)
140
+ return true;
141
+ if (a < b)
142
+ return false;
143
+ }
144
+ return true;
145
+ }
146
+ function clientSupportsAllProviders(appVersion) {
147
+ return isAppVersionAtLeast(appVersion, MIN_VERSION_ALL_PROVIDERS);
148
+ }
149
+ function beginAgentDeleteIfSupported(agentStorage, agentId) {
150
+ if ("beginDelete" in agentStorage && typeof agentStorage.beginDelete === "function") {
151
+ agentStorage.beginDelete(agentId);
152
+ }
153
+ }
154
+ const FETCH_AGENTS_SORT_KEYS = ["status_priority", "created_at", "updated_at", "title"];
155
+ export function resolveWaitForFinishError(options) {
156
+ if (options.status !== "error") {
157
+ return null;
158
+ }
159
+ const message = options.final?.lastError;
160
+ return typeof message === "string" && message.trim().length > 0 ? message : "Agent failed";
161
+ }
162
+ class SessionRequestError extends Error {
163
+ constructor(code, message) {
164
+ super(message);
165
+ this.code = code;
166
+ this.name = "SessionRequestError";
167
+ }
168
+ }
169
+ const nodeSessionFileSystem = {
170
+ async isDirectory(path) {
171
+ const stats = await stat(path).catch(() => null);
172
+ return stats?.isDirectory() ?? false;
173
+ },
174
+ };
175
+ function parseClientCapabilities(capabilities) {
176
+ if (!capabilities) {
177
+ return new Set();
178
+ }
179
+ const known = new Set(Object.values(CLIENT_CAPS));
180
+ const result = [];
181
+ for (const [key, value] of Object.entries(capabilities)) {
182
+ if (value === true && known.has(key)) {
183
+ result.push(key);
184
+ }
185
+ }
186
+ return new Set(result);
187
+ }
188
+ function describeRegistryTransition(record) {
189
+ if (!record) {
190
+ return "created";
191
+ }
192
+ return record.archivedAt ? "unarchived" : "existing";
193
+ }
194
+ /**
195
+ * Session represents a single connected client session.
196
+ * It owns all state management, orchestration logic, and message processing.
197
+ * Session has no knowledge of WebSockets - it only emits and receives messages.
198
+ */
199
+ export class Session {
200
+ constructor(options) {
201
+ this.unsubscribeAgentEvents = null;
202
+ this.unsubscribeTerminalWorkspaceContributionEvents = null;
203
+ this.workspaceUpdatesSubscription = null;
204
+ this.clientActivity = null;
205
+ this.inflightRequests = 0;
206
+ this.peakInflightRequests = 0;
207
+ this.agentsPager = new SortablePager({
208
+ validKeys: FETCH_AGENTS_SORT_KEYS,
209
+ defaultSort: [{ key: "updated_at", direction: "desc" }],
210
+ label: "fetch_agents",
211
+ getId: (agent) => agent.id,
212
+ getSortValue: (agent, key) => {
213
+ switch (key) {
214
+ case "status_priority":
215
+ return getAgentStatusPriority({
216
+ status: agent.status,
217
+ pendingPermissionCount: agent.pendingPermissions?.length ?? 0,
218
+ requiresAttention: agent.requiresAttention,
219
+ attentionReason: agent.attentionReason ?? null,
220
+ });
221
+ case "created_at":
222
+ return Date.parse(agent.createdAt);
223
+ case "updated_at":
224
+ return Date.parse(agent.updatedAt);
225
+ case "title":
226
+ return agent.title?.toLocaleLowerCase() ?? "";
227
+ }
228
+ },
229
+ });
230
+ const { clientId, appVersion, clientCapabilities, onMessage, onBinaryMessage, getTransportBufferedAmount, onLifecycleIntent, logger, downloadTokenStore, pushTokenStore, ottoHome, worktreesRoot, agentManager, agentStorage, projectRegistry, workspaceRegistry, filesystem, chatService, scheduleService, loopService, checkoutDiffManager, github, gitHostingResolver, renameCurrentBranch, workspaceGitService, workspaceAutoName, daemonConfigStore, stt, sttLanguage, tts, terminalManager, previewDevServers, providerSnapshotManager, providerUsageService, serviceProxy, scriptRuntimeStore, workspaceSetupSnapshots, onBranchChanged, getDaemonTcpPort, getDaemonTcpHost, serviceProxyPublicBaseUrl, resolveScriptHealth, voice, voiceBridge, dictation, getSpeechSettingsOptions, previewTts, getPersonalityStats, serverId, daemonVersion, daemonRuntimeConfig, getWebSocketRuntimeMetrics, } = options;
231
+ this.clientId = clientId;
232
+ this.getSpeechSettingsOptions = getSpeechSettingsOptions ?? null;
233
+ this.previewTts = previewTts;
234
+ this.getPersonalityStats = getPersonalityStats ?? null;
235
+ this.appVersion = appVersion ?? null;
236
+ this.clientCapabilities = parseClientCapabilities(clientCapabilities);
237
+ this.sessionId = uuidv4();
238
+ this.onMessage = onMessage;
239
+ this.onBinaryMessage = onBinaryMessage ?? null;
240
+ this.getTransportBufferedAmount = getTransportBufferedAmount ?? (() => 0);
241
+ this.onLifecycleIntent = onLifecycleIntent ?? null;
242
+ this.pushTokenStore = pushTokenStore;
243
+ this.ottoHome = ottoHome;
244
+ this.worktreesRoot = worktreesRoot;
245
+ this.sessionLogger = logger.child({
246
+ module: "session",
247
+ clientId: this.clientId,
248
+ sessionId: this.sessionId,
249
+ });
250
+ this.workspaceFilesSession = new WorkspaceFilesSession({
251
+ host: {
252
+ emit: (msg) => this.emit(msg),
253
+ emitBinary: (frame) => this.emitBinary(frame),
254
+ hasBinaryChannel: () => this.onBinaryMessage !== null,
255
+ },
256
+ downloadTokenStore,
257
+ ottoHome,
258
+ logger: this.sessionLogger,
259
+ });
260
+ this.agentManager = agentManager;
261
+ this.agentStorage = agentStorage;
262
+ this.projectRegistry = projectRegistry;
263
+ this.workspaceRegistry = workspaceRegistry;
264
+ this.filesystem = filesystem ?? nodeSessionFileSystem;
265
+ this.github = github ?? createGitHubService();
266
+ this.gitHostingResolver = gitHostingResolver ?? null;
267
+ this.renameCurrentBranch = renameCurrentBranch ?? renameCurrentBranchDefault;
268
+ this.workspaceGitService = workspaceGitService;
269
+ this.gitMutation = createGitMutationService({
270
+ workspaceGitService: this.workspaceGitService,
271
+ github: this.github,
272
+ logger: this.sessionLogger,
273
+ });
274
+ this.workspaceAutoName = workspaceAutoName;
275
+ this.workspaceProvisioning = createWorkspaceProvisioningService({
276
+ workspaceRegistry: this.workspaceRegistry,
277
+ projectRegistry: this.projectRegistry,
278
+ workspaceGitService: this.workspaceGitService,
279
+ });
280
+ this.checkoutSession = new CheckoutSession({
281
+ host: {
282
+ emit: (msg) => this.emit(msg),
283
+ emitWorkspaceUpdateForCwd: (cwd) => this.emitWorkspaceUpdateForCwd(cwd),
284
+ handleWorkspaceGitBranchSnapshot: (cwd, branchName) => this.workspaceGitObserver.handleBranchSnapshot(cwd, branchName),
285
+ renameCurrentBranch: (cwd, branch) => this.renameCurrentBranch(cwd, branch),
286
+ },
287
+ gitMutation: this.gitMutation,
288
+ workspaceGitService: this.workspaceGitService,
289
+ github: this.github,
290
+ ...(this.gitHostingResolver ? { gitHostingResolver: this.gitHostingResolver } : {}),
291
+ checkoutDiffManager,
292
+ gitMetadataGenerator: createGitMetadataGenerator({
293
+ workspaceGitService: this.workspaceGitService,
294
+ generation: createAgentStructuredTextGeneration({
295
+ agentManager: this.agentManager,
296
+ providerSnapshotManager,
297
+ readDaemonConfig: () => this.readStructuredGenerationDaemonConfig(),
298
+ getFocusedSelection: (cwd) => this.getFocusedAgentSelectionForCwd(cwd),
299
+ }),
300
+ }),
301
+ ottoHome: this.ottoHome,
302
+ worktreesRoot: this.worktreesRoot,
303
+ logger: this.sessionLogger,
304
+ });
305
+ this.workspaceGitObserver = createWorkspaceGitObserverService({
306
+ workspaceGitService: this.workspaceGitService,
307
+ describeWorkspaceRecordWithGitData: (workspace) => this.describeWorkspaceRecordWithGitData(workspace),
308
+ emitWorkspaceUpdateForCwd: (cwd) => this.emitWorkspaceUpdateForCwd(cwd),
309
+ emitWorkspaceUpdateForWorkspaceId: (workspaceId) => this.emitWorkspaceUpdateForWorkspaceId(workspaceId),
310
+ emitStatusUpdate: (cwd, snapshot) => this.checkoutSession.emitStatusUpdate(cwd, snapshot),
311
+ onBranchChanged,
312
+ logger: this.sessionLogger,
313
+ });
314
+ this.chatScheduleLoopSession = new ChatScheduleLoopSession({
315
+ host: {
316
+ emit: (msg) => this.emit(msg),
317
+ listStoredAgents: () => this.agentStorage.list(),
318
+ listLiveAgents: () => this.agentManager.listAgents(),
319
+ resolveAgentIdentifier: (identifier) => this.resolveAgentIdentifier(identifier),
320
+ sendAgentMessage: async (agentId, text) => {
321
+ await sendPromptToAgent({
322
+ agentManager: this.agentManager,
323
+ agentStorage: this.agentStorage,
324
+ agentId,
325
+ prompt: formatSystemNotificationPrompt(text),
326
+ unarchive: false,
327
+ logger: this.sessionLogger,
328
+ });
329
+ },
330
+ },
331
+ chatService,
332
+ scheduleService,
333
+ loopService,
334
+ clientId: this.clientId,
335
+ logger: this.sessionLogger,
336
+ });
337
+ this.providerCatalogSession = new ProviderCatalogSession({
338
+ host: {
339
+ emit: (msg) => this.emit(msg),
340
+ isProviderVisibleToClient: (provider) => this.isProviderVisibleToClient(provider),
341
+ supportsCustomModeIcons: () => this.supports(CLIENT_CAPS.customModeIcons),
342
+ listProviderAvailability: () => this.agentManager.listProviderAvailability(),
343
+ listDraftFeatures: (config) => this.agentManager.listDraftFeatures(config),
344
+ },
345
+ providerSnapshotManager,
346
+ providerUsageService,
347
+ logger: this.sessionLogger,
348
+ });
349
+ this.agentConfigSession = new AgentConfigSession({
350
+ host: {
351
+ emit: (msg) => this.emit(msg),
352
+ },
353
+ operations: {
354
+ setMode: async (agentId, modeId) => (await setAgentModeCommand({ agentManager }, { agentId, modeId })).notice,
355
+ setModel: (agentId, modelId) => agentManager.setAgentModel(agentId, modelId),
356
+ setFeature: (agentId, featureId, value) => agentManager.setAgentFeature(agentId, featureId, value),
357
+ setThinking: (agentId, thinkingOptionId) => agentManager.setAgentThinkingOption(agentId, thinkingOptionId),
358
+ setPersonality: async (agentId, personalityId) => {
359
+ const snapshot = personalityId === null
360
+ ? null
361
+ : await this.resolvePersonalitySnapshotForAgent(agentId, personalityId);
362
+ return agentManager.setAgentPersonality(agentId, snapshot);
363
+ },
364
+ },
365
+ logger: this.sessionLogger,
366
+ });
367
+ this.projectConfigSession = new ProjectConfigSession({
368
+ host: {
369
+ emit: (msg) => this.emit(msg),
370
+ },
371
+ projectRegistry: this.projectRegistry,
372
+ logger: this.sessionLogger,
373
+ });
374
+ this.daemonSession = new DaemonSession({
375
+ host: {
376
+ emit: (msg) => this.emit(msg),
377
+ emitLifecycleIntent: (intent) => this.emitLifecycleIntent(intent),
378
+ },
379
+ clientId: this.clientId,
380
+ ottoHome: this.ottoHome,
381
+ serverId,
382
+ daemonVersion,
383
+ daemonRuntimeConfig,
384
+ getWebSocketRuntimeMetrics,
385
+ listProviderAvailability: () => this.agentManager.listProviderAvailability(),
386
+ listAgents: () => this.agentManager.listAgents(),
387
+ listProjects: () => this.projectRegistry.list(),
388
+ listWorkspaces: () => this.workspaceRegistry.list(),
389
+ logger: this.sessionLogger,
390
+ });
391
+ const artifactService = new ArtifactService({
392
+ projectCwd: this.ottoHome,
393
+ logger: this.sessionLogger,
394
+ agentManager: this.agentManager,
395
+ providerSnapshotManager,
396
+ broadcastArtifactUpdate: (metadata) => {
397
+ this.emit({
398
+ type: "artifact.updated.notification",
399
+ payload: { artifact: metadata },
400
+ });
401
+ },
402
+ });
403
+ this.artifactSession = new ArtifactSession({
404
+ host: {
405
+ emit: (msg) => this.emit(msg),
406
+ },
407
+ artifactService,
408
+ logger: this.sessionLogger,
409
+ });
410
+ this.daemonConfigStore = daemonConfigStore;
411
+ this.terminalManager = terminalManager;
412
+ this.previewDevServers = previewDevServers ?? null;
413
+ this.terminalController = new TerminalSessionController({
414
+ terminalManager,
415
+ emit: (msg) => this.emit(msg),
416
+ emitBinary: (frame) => this.emitBinary(frame),
417
+ hasBinaryChannel: () => this.onBinaryMessage !== null,
418
+ isPathWithinRoot: (rootPath, candidatePath) => this.isPathWithinRoot(rootPath, candidatePath),
419
+ sessionLogger: this.sessionLogger,
420
+ listTerminalWorkspaceRefs: () => this.listActiveWorkspaceRefs(),
421
+ clientSupportsWrapReflow: () => this.clientCapabilities.has(CLIENT_CAPS.terminalReflowableSnapshot),
422
+ getClientBufferedAmount: () => this.getTransportBufferedAmount(),
423
+ });
424
+ this.agentUpdates = createAgentUpdatesService({
425
+ emit: (message) => this.emit(message),
426
+ buildAgentPayload: (agent) => this.buildAgentPayload(agent),
427
+ buildStoredAgentPayload: (record) => this.buildStoredAgentPayload(record),
428
+ isProviderVisibleToClient: (provider) => this.isProviderVisibleToClient(provider),
429
+ buildProjectPlacementForWorkspaceId: (workspaceId) => this.buildProjectPlacementForWorkspaceId(workspaceId),
430
+ emitWorkspaceUpdateForWorkspaceId: (workspaceId) => this.emitWorkspaceUpdateForWorkspaceId(workspaceId),
431
+ logger: this.sessionLogger,
432
+ });
433
+ this.createAgentLifecycleDispatch = new CreateAgentLifecycleDispatch({
434
+ ottoHome: this.ottoHome,
435
+ worktreesRoot: this.worktreesRoot,
436
+ agentManager: this.agentManager,
437
+ agentStorage: this.agentStorage,
438
+ github: this.github,
439
+ workspaceGitService: this.workspaceGitService,
440
+ createOttoWorktreeWorkflow: (input, workflowOptions) => this.createOttoWorktreeWorkflow(input, workflowOptions),
441
+ archiveAgentForClose: (agentId) => this.archiveAgentForClose(agentId),
442
+ findWorkspaceIdForCwd: (cwd) => this.findWorkspaceIdForCwd(cwd),
443
+ listActiveWorkspaces: () => this.listActiveWorkspaceRefs(),
444
+ archiveWorkspaceRecord: (workspaceId) => this.archiveWorkspaceRecord(workspaceId),
445
+ emit: (message) => this.emit(message),
446
+ emitAgentRemove: (agentId) => this.agentUpdates.removeAgent(agentId),
447
+ emitWorkspaceUpdatesForWorkspaceIds: (workspaceIds) => this.emitWorkspaceUpdatesForWorkspaceIds(workspaceIds),
448
+ markWorkspaceArchiving: (workspaceIds, archivingAt) => this.markWorkspaceArchiving(workspaceIds, archivingAt),
449
+ clearWorkspaceArchiving: (workspaceIds) => this.clearWorkspaceArchiving(workspaceIds),
450
+ killTerminalsForWorkspace: (workspaceId) => this.terminalController.killTerminalsForWorkspace(workspaceId),
451
+ logger: this.sessionLogger,
452
+ });
453
+ this.providerSnapshotManager = providerSnapshotManager;
454
+ this.serviceProxy = serviceProxy ?? null;
455
+ this.scriptRuntimeStore = scriptRuntimeStore ?? null;
456
+ this.workspaceSetupSnapshots = workspaceSetupSnapshots ?? new Map();
457
+ this.getDaemonTcpPort = getDaemonTcpPort ?? null;
458
+ this.getDaemonTcpHost = getDaemonTcpHost ?? null;
459
+ this.serviceProxyPublicBaseUrl = serviceProxyPublicBaseUrl ?? null;
460
+ this.resolveScriptHealth = resolveScriptHealth ?? null;
461
+ this.workspaceScripts = createWorkspaceScriptsService({
462
+ serviceProxy: this.serviceProxy,
463
+ scriptRuntimeStore: this.scriptRuntimeStore,
464
+ terminalManager: this.terminalManager,
465
+ workspaceRegistry: this.workspaceRegistry,
466
+ workspaceGitService: this.workspaceGitService,
467
+ getDaemonTcpPort: this.getDaemonTcpPort,
468
+ getDaemonTcpHost: this.getDaemonTcpHost,
469
+ serviceProxyPublicBaseUrl: this.serviceProxyPublicBaseUrl,
470
+ resolveScriptHealth: this.resolveScriptHealth,
471
+ logger: this.sessionLogger,
472
+ emit: (message) => this.emit(message),
473
+ spawnWorkspaceScript,
474
+ });
475
+ this.subscribeToOptionalManagers();
476
+ this.workspaceDirectory = new WorkspaceDirectory({
477
+ logger: this.sessionLogger,
478
+ projectRegistry: this.projectRegistry,
479
+ workspaceRegistry: this.workspaceRegistry,
480
+ listAgentPayloads: () => this.listAgentPayloads(),
481
+ listTerminalActivityContributions: () => this.listTerminalActivityContributions(),
482
+ isProviderVisibleToClient: (provider) => this.isProviderVisibleToClient(provider),
483
+ buildWorkspaceDescriptor: (input) => this.buildWorkspaceDescriptor(input),
484
+ });
485
+ this.voiceSession = new VoiceSession({
486
+ host: {
487
+ emit: (msg) => this.emit(msg),
488
+ loadAgent: (agentId) => ensureAgentLoaded(agentId, {
489
+ agentManager: this.agentManager,
490
+ agentStorage: this.agentStorage,
491
+ logger: this.sessionLogger,
492
+ }),
493
+ reloadAgentSession: (agentId, overrides) => this.agentManager.reloadAgentSession(agentId, overrides),
494
+ sendSpokenInput: async (agentId, text) => {
495
+ await this.handleSendAgentMessage(agentId, text, undefined, undefined, undefined, undefined, { spokenInput: true });
496
+ },
497
+ interruptAgentIfRunning: (agentId) => this.interruptAgentIfRunning(agentId),
498
+ hasActiveAgentRun: (agentId) => this.hasActiveAgentRun(agentId),
499
+ },
500
+ logger: this.sessionLogger,
501
+ sessionId: this.sessionId,
502
+ sttLanguage,
503
+ tts,
504
+ stt,
505
+ voice,
506
+ voiceBridge,
507
+ dictation,
508
+ });
509
+ this.subscribeToAgentEvents();
510
+ this.sessionLogger.trace({}, "agent.session.lifecycle.created");
511
+ }
512
+ updateAppVersion(appVersion) {
513
+ if (appVersion && appVersion !== this.appVersion) {
514
+ this.appVersion = appVersion;
515
+ }
516
+ }
517
+ updateClientCapabilities(capabilities) {
518
+ this.clientCapabilities = parseClientCapabilities(capabilities);
519
+ }
520
+ supports(capability) {
521
+ return this.clientCapabilities.has(capability);
522
+ }
523
+ async syncWorkspaceGitObserverForWorkspace(workspace) {
524
+ await this.workspaceGitObserver.syncObserverForWorkspace(workspace);
525
+ }
526
+ async emitWorkspaceUpdateForWorkspaceId(workspaceId) {
527
+ await this.emitWorkspaceUpdatesForWorkspaceIds([workspaceId], { skipReconcile: true });
528
+ }
529
+ async archiveWorkspaceRecordForExternalMutation(workspaceId) {
530
+ await this.archiveWorkspaceRecord(workspaceId);
531
+ }
532
+ markWorkspaceArchivingForExternalMutation(workspaceIds, archivingAt) {
533
+ this.markWorkspaceArchiving(workspaceIds, archivingAt);
534
+ }
535
+ clearWorkspaceArchivingForExternalMutation(workspaceIds) {
536
+ this.clearWorkspaceArchiving(workspaceIds);
537
+ }
538
+ async emitWorkspaceUpdatesForExternalWorkspaceIds(workspaceIds) {
539
+ await this.emitWorkspaceUpdatesForWorkspaceIds(workspaceIds);
540
+ }
541
+ async warmWorkspaceGitDataForWorkspace(workspace) {
542
+ await this.workspaceGitObserver.warmGitData(workspace);
543
+ }
544
+ /**
545
+ * Get the client's current activity state
546
+ */
547
+ getClientActivity() {
548
+ return this.clientActivity;
549
+ }
550
+ getFocusedAgentSelectionForCwd(cwd) {
551
+ const focusedAgentId = this.clientActivity?.focusedAgentId;
552
+ if (!focusedAgentId) {
553
+ return undefined;
554
+ }
555
+ const agent = this.agentManager.getAgent(focusedAgentId);
556
+ if (!agent || agent.cwd !== cwd) {
557
+ return undefined;
558
+ }
559
+ return {
560
+ provider: agent.provider,
561
+ model: agent.runtimeInfo?.model ?? agent.config.model ?? null,
562
+ thinkingOptionId: agent.runtimeInfo?.thinkingOptionId ?? agent.config.thinkingOptionId ?? null,
563
+ };
564
+ }
565
+ readStructuredGenerationDaemonConfig() {
566
+ return {
567
+ metadataGeneration: this.daemonConfigStore.get().metadataGeneration,
568
+ };
569
+ }
570
+ getRuntimeMetrics() {
571
+ const terminalMetrics = this.terminalController.getMetrics();
572
+ return {
573
+ terminalDirectorySubscriptionCount: terminalMetrics.directorySubscriptionCount,
574
+ terminalSubscriptionCount: terminalMetrics.streamSubscriptionCount,
575
+ inflightRequests: this.inflightRequests,
576
+ peakInflightRequests: this.peakInflightRequests,
577
+ };
578
+ }
579
+ emitServerMessage(message) {
580
+ this.emit(message);
581
+ }
582
+ /**
583
+ * Send initial state to client after connection
584
+ */
585
+ async sendInitialState() {
586
+ // No unsolicited agent list hydration. Callers must use fetch_agents_request.
587
+ }
588
+ /**
589
+ * Normalize a user prompt (with optional image metadata) for AgentManager
590
+ */
591
+ buildAgentPrompt(text, images, attachments) {
592
+ const normalized = text?.trim() ?? "";
593
+ const hasImages = Boolean(images && images.length > 0);
594
+ const hasAttachments = Boolean(attachments && attachments.length > 0);
595
+ if (!hasImages && !hasAttachments) {
596
+ return normalized;
597
+ }
598
+ const blocks = [];
599
+ if (normalized.length > 0) {
600
+ blocks.push({ type: "text", text: normalized });
601
+ }
602
+ for (const image of images ?? []) {
603
+ blocks.push({ type: "image", data: image.data, mimeType: image.mimeType });
604
+ }
605
+ for (const attachment of attachments ?? []) {
606
+ blocks.push(attachment);
607
+ }
608
+ return blocks;
609
+ }
610
+ /**
611
+ * Interrupt the agent's active run so the next prompt starts a fresh turn.
612
+ * Returns once the manager confirms the stream has been cancelled.
613
+ */
614
+ async interruptAgentIfRunning(agentId) {
615
+ const snapshot = this.agentManager.getAgent(agentId);
616
+ if (!snapshot) {
617
+ this.sessionLogger.trace({ agentId }, "agent.session.interrupt.not_found");
618
+ throw new Error(`Agent ${agentId} not found`);
619
+ }
620
+ const hasInFlightRun = this.agentManager.hasInFlightRun(agentId);
621
+ if (!hasInFlightRun) {
622
+ this.sessionLogger.trace({
623
+ agentId,
624
+ provider: snapshot.provider,
625
+ lifecycle: snapshot.lifecycle,
626
+ hasInFlightRun,
627
+ }, "agent.session.interrupt.skip_not_running");
628
+ return;
629
+ }
630
+ this.sessionLogger.debug({ agentId, lifecycle: snapshot.lifecycle, hasInFlightRun }, "interruptAgentIfRunning: interrupting");
631
+ const t0 = Date.now();
632
+ const cancelled = await this.agentManager.cancelAgentRun(agentId);
633
+ this.sessionLogger.debug({ agentId, cancelled, durationMs: Date.now() - t0 }, "interruptAgentIfRunning: cancelAgentRun completed");
634
+ if (!cancelled) {
635
+ this.sessionLogger.warn({ agentId }, "interruptAgentIfRunning: reported running but no active run was cancelled");
636
+ }
637
+ }
638
+ hasActiveAgentRun(agentId) {
639
+ if (!agentId) {
640
+ return false;
641
+ }
642
+ return this.agentManager.hasInFlightRun(agentId);
643
+ }
644
+ handleAgentRunError(agentId, error, context) {
645
+ const message = errorToFriendlyMessage(error);
646
+ this.sessionLogger.error({ err: error, agentId, context }, `${context} for agent ${agentId}`);
647
+ this.emit({
648
+ type: "activity_log",
649
+ payload: {
650
+ id: uuidv4(),
651
+ timestamp: new Date(),
652
+ type: "error",
653
+ content: `${context}: ${message}`,
654
+ },
655
+ });
656
+ }
657
+ /**
658
+ * Subscribe to AgentManager events and forward them to the client
659
+ */
660
+ subscribeToOptionalManagers() {
661
+ this.terminalController.start();
662
+ if (this.terminalManager) {
663
+ this.unsubscribeTerminalWorkspaceContributionEvents =
664
+ this.terminalManager.subscribeTerminalWorkspaceContributionChanged((event) => {
665
+ void this.emitWorkspaceUpdateForTerminalContribution(event).catch((error) => {
666
+ this.sessionLogger.warn({ err: error, terminalId: event.terminalId }, "Failed to emit workspace update after terminal contribution changed");
667
+ });
668
+ });
669
+ }
670
+ this.providerCatalogSession.start();
671
+ }
672
+ subscribeToAgentEvents() {
673
+ if (this.unsubscribeAgentEvents) {
674
+ this.unsubscribeAgentEvents();
675
+ }
676
+ this.unsubscribeAgentEvents = this.agentManager.subscribe((event) => {
677
+ if (event.type === "agent_state") {
678
+ this.sessionLogger.trace({
679
+ agentId: event.agent.id,
680
+ provider: event.agent.provider,
681
+ providerSessionId: event.agent.persistence?.sessionId ?? undefined,
682
+ turnId: event.agent.activeForegroundTurnId ?? undefined,
683
+ lifecycle: event.agent.lifecycle,
684
+ }, "agent.session.forward_update");
685
+ void this.agentUpdates.forwardLiveAgent(event.agent);
686
+ return;
687
+ }
688
+ if (event.type === "observed_agent_state") {
689
+ // Synthetic snapshot for an observed subagent (no ManagedAgent
690
+ // runtime). Forward through the same filter/placement path as live
691
+ // agents. See projects/observed-subagents/observed-subagents.md.
692
+ void this.agentUpdates.forwardLiveAgentPayload(event.payload);
693
+ return;
694
+ }
695
+ if (this.voiceSession.isActiveForAgent(event.agentId) &&
696
+ event.event.type === "permission_requested" &&
697
+ isVoicePermissionAllowed(event.event.request)) {
698
+ const requestId = event.event.request.id;
699
+ void this.agentManager
700
+ .respondToPermission(event.agentId, requestId, {
701
+ behavior: "allow",
702
+ })
703
+ .catch((error) => {
704
+ this.sessionLogger.warn({
705
+ err: error,
706
+ agentId: event.agentId,
707
+ requestId,
708
+ }, "Failed to auto-allow speak tool permission in voice mode");
709
+ });
710
+ }
711
+ const serializedEvent = serializeAgentStreamEvent(event.event);
712
+ if (!serializedEvent) {
713
+ return;
714
+ }
715
+ this.sessionLogger.trace({
716
+ agentId: event.agentId,
717
+ provider: event.event.provider,
718
+ turnId: getAgentStreamEventTurnId(event.event),
719
+ seq: event.seq,
720
+ epoch: event.epoch,
721
+ event: event.event,
722
+ }, "agent.session.forward_stream");
723
+ this.emit({
724
+ type: "agent_stream",
725
+ payload: this.buildAgentStreamPayload(event, serializedEvent),
726
+ });
727
+ if (event.event.type === "permission_requested") {
728
+ this.emit({
729
+ type: "agent_permission_request",
730
+ payload: {
731
+ agentId: event.agentId,
732
+ request: event.event.request,
733
+ },
734
+ });
735
+ }
736
+ else if (event.event.type === "permission_resolved") {
737
+ this.emit({
738
+ type: "agent_permission_resolved",
739
+ payload: {
740
+ agentId: event.agentId,
741
+ requestId: event.event.requestId,
742
+ resolution: event.event.resolution,
743
+ },
744
+ });
745
+ }
746
+ // Title updates may be applied asynchronously after agent creation.
747
+ }, { replayState: false });
748
+ }
749
+ buildAgentStreamPayload(event, serializedEvent) {
750
+ return {
751
+ agentId: event.agentId,
752
+ event: serializedEvent,
753
+ timestamp: event.timestamp ?? new Date().toISOString(),
754
+ ...(typeof event.seq === "number" ? { seq: event.seq } : {}),
755
+ ...(typeof event.epoch === "string" ? { epoch: event.epoch } : {}),
756
+ };
757
+ }
758
+ async buildAgentPayload(agent) {
759
+ const storedRecord = await this.agentStorage.get(agent.id);
760
+ const title = storedRecord?.title ?? null;
761
+ const payload = toAgentPayload(agent, { title });
762
+ const storedUpdatedAt = storedRecord ? resolveStoredAgentPayloadUpdatedAt(storedRecord) : null;
763
+ if (storedUpdatedAt) {
764
+ const liveUpdatedAt = Date.parse(payload.updatedAt);
765
+ const persistedUpdatedAt = Date.parse(storedUpdatedAt);
766
+ if (Number.isNaN(liveUpdatedAt) || persistedUpdatedAt > liveUpdatedAt) {
767
+ payload.updatedAt = storedUpdatedAt;
768
+ }
769
+ }
770
+ payload.archivedAt = storedRecord?.archivedAt ?? null;
771
+ return payload;
772
+ }
773
+ buildStoredAgentPayload(record, registeredProviderIds = new Set(this.providerSnapshotManager.listRegisteredProviderIds())) {
774
+ return buildStoredAgentPayload(record, registeredProviderIds);
775
+ }
776
+ isProviderVisibleToClient(provider) {
777
+ if (clientSupportsAllProviders(this.appVersion)) {
778
+ return true;
779
+ }
780
+ return LEGACY_PROVIDER_IDS.has(provider);
781
+ }
782
+ async buildProjectPlacementForWorkspace(workspace, projectRecord) {
783
+ const project = projectRecord ?? (await this.projectRegistry.get(workspace.projectId));
784
+ if (!project) {
785
+ throw new Error(`Project not found for workspace ${workspace.workspaceId}`);
786
+ }
787
+ const liveBranch = this.workspaceGitService.peekSnapshot(workspace.cwd)?.git.currentBranch ?? null;
788
+ const checkout = buildWorkspaceCheckout(workspace, project, liveBranch);
789
+ return {
790
+ projectKey: project.projectId,
791
+ projectName: resolveProjectDisplayName(project),
792
+ workspaceName: resolveWorkspaceDisplayName(workspace),
793
+ checkout,
794
+ };
795
+ }
796
+ async buildProjectPlacementForWorkspaceId(workspaceId) {
797
+ const workspace = await this.workspaceRegistry.get(workspaceId);
798
+ if (!workspace)
799
+ return null;
800
+ const project = await this.projectRegistry.get(workspace.projectId);
801
+ if (!project)
802
+ return null;
803
+ return this.buildProjectPlacementForWorkspace(workspace, project);
804
+ }
805
+ /**
806
+ * Main entry point for processing session messages
807
+ */
808
+ async handleMessage(msg) {
809
+ this.inflightRequests++;
810
+ if (this.inflightRequests > this.peakInflightRequests) {
811
+ this.peakInflightRequests = this.inflightRequests;
812
+ }
813
+ try {
814
+ this.sessionLogger.trace({
815
+ messageType: msg.type,
816
+ payloadBytes: JSON.stringify(msg).length,
817
+ }, "agent.session.inbound");
818
+ try {
819
+ await this.dispatchInboundMessage(msg);
820
+ }
821
+ catch (error) {
822
+ const err = error instanceof Error ? error : new Error(String(error));
823
+ this.sessionLogger.error({ err }, "Error handling message");
824
+ const requestId = "requestId" in msg && typeof msg.requestId === "string" ? msg.requestId : undefined;
825
+ if (typeof requestId === "string") {
826
+ try {
827
+ this.emit({
828
+ type: "rpc_error",
829
+ payload: {
830
+ requestId,
831
+ requestType: msg.type,
832
+ error: `Request failed: ${err.message}`,
833
+ code: "handler_error",
834
+ },
835
+ });
836
+ }
837
+ catch (emitError) {
838
+ this.sessionLogger.error({ err: emitError }, "Failed to emit rpc_error");
839
+ }
840
+ }
841
+ this.emit({
842
+ type: "activity_log",
843
+ payload: {
844
+ id: uuidv4(),
845
+ timestamp: new Date(),
846
+ type: "error",
847
+ content: `Error: ${err.message}`,
848
+ },
849
+ });
850
+ }
851
+ }
852
+ finally {
853
+ this.inflightRequests--;
854
+ }
855
+ }
856
+ async dispatchInboundMessage(msg) {
857
+ const promise = this.dispatchVoiceAndControlMessage(msg) ??
858
+ this.dispatchAgentRewindMessage(msg) ??
859
+ this.dispatchAgentPersonalityMessage(msg) ??
860
+ this.dispatchAgentRelationshipMessage(msg) ??
861
+ this.dispatchAgentTimelineMessage(msg) ??
862
+ this.dispatchAgentLifecycleMessage(msg) ??
863
+ this.dispatchAgentConfigMessage(msg) ??
864
+ this.dispatchCheckoutMessage(msg) ??
865
+ this.dispatchPreviewMessage(msg) ??
866
+ this.dispatchWorkspaceAndProjectMessage(msg) ??
867
+ this.dispatchWorkspaceFilesMessage(msg) ??
868
+ this.dispatchProviderMessage(msg) ??
869
+ this.dispatchTerminalMessage(msg) ??
870
+ this.dispatchChatScheduleLoopMessage(msg) ??
871
+ this.dispatchArtifactMessage(msg) ??
872
+ this.dispatchMiscMessage(msg);
873
+ if (promise)
874
+ await promise;
875
+ }
876
+ dispatchVoiceAndControlMessage(msg) {
877
+ switch (msg.type) {
878
+ case "voice_audio_chunk":
879
+ return this.voiceSession.handleAudioChunk(msg);
880
+ case "abort_request":
881
+ return this.voiceSession.handleAbort();
882
+ case "audio_played":
883
+ this.voiceSession.handleAudioPlayed(msg.id);
884
+ return undefined;
885
+ case "set_voice_mode":
886
+ return this.voiceSession.handleSetVoiceMode(msg.enabled, msg.agentId, msg.requestId);
887
+ case "dictation_stream_start":
888
+ return this.voiceSession.handleDictationStreamStart(msg);
889
+ case "dictation_stream_chunk":
890
+ return this.voiceSession.handleDictationChunk({
891
+ dictationId: msg.dictationId,
892
+ seq: msg.seq,
893
+ audioBase64: msg.audio,
894
+ format: msg.format,
895
+ });
896
+ case "dictation_stream_finish":
897
+ return this.voiceSession.handleDictationFinish(msg.dictationId, msg.finalSeq);
898
+ case "dictation_stream_cancel":
899
+ this.voiceSession.handleDictationCancel(msg.dictationId);
900
+ return undefined;
901
+ case "restart_server_request":
902
+ return this.handleRestartServerRequest(msg.requestId, msg.reason);
903
+ case "shutdown_server_request":
904
+ return this.handleShutdownServerRequest(msg.requestId);
905
+ case "client_heartbeat":
906
+ this.handleClientHeartbeat(msg);
907
+ return undefined;
908
+ case "ping": {
909
+ const now = Date.now();
910
+ this.emit({
911
+ type: "pong",
912
+ payload: {
913
+ requestId: msg.requestId,
914
+ clientSentAt: msg.clientSentAt,
915
+ serverReceivedAt: now,
916
+ serverSentAt: now,
917
+ },
918
+ });
919
+ return undefined;
920
+ }
921
+ default:
922
+ return undefined;
923
+ }
924
+ }
925
+ dispatchAgentRewindMessage(msg) {
926
+ switch (msg.type) {
927
+ case "agent.rewind.request":
928
+ return this.handleAgentRewindRequest(msg);
929
+ default:
930
+ return undefined;
931
+ }
932
+ }
933
+ // Kept out of dispatchAgentConfigMessage only because that switch sits at the
934
+ // complexity ceiling; the handler itself lives with its config siblings.
935
+ dispatchAgentPersonalityMessage(msg) {
936
+ switch (msg.type) {
937
+ case "agent.personality.set.request":
938
+ return this.agentConfigSession.handleAgentPersonalitySetRequest(msg);
939
+ default:
940
+ return undefined;
941
+ }
942
+ }
943
+ dispatchAgentRelationshipMessage(msg) {
944
+ switch (msg.type) {
945
+ case "agent.detach.request":
946
+ return this.handleDetachAgentRequest(msg.agentId, msg.requestId);
947
+ case "agent.subagent.stop.request":
948
+ return this.handleStopObservedSubagentRequest(msg.agentId, msg.requestId);
949
+ default:
950
+ return undefined;
951
+ }
952
+ }
953
+ async handleStopObservedSubagentRequest(agentId, requestId) {
954
+ try {
955
+ await this.agentManager.stopObservedSubagent(agentId);
956
+ this.emit({
957
+ type: "agent.subagent.stop.response",
958
+ payload: {
959
+ requestId,
960
+ agentId,
961
+ accepted: true,
962
+ error: null,
963
+ },
964
+ });
965
+ }
966
+ catch (error) {
967
+ const message = getErrorMessageOr(error, "Failed to stop observed subagent");
968
+ this.sessionLogger.error({ err: error, agentId, requestId }, "Failed to stop observed subagent");
969
+ this.emit({
970
+ type: "agent.subagent.stop.response",
971
+ payload: {
972
+ requestId,
973
+ agentId,
974
+ accepted: false,
975
+ error: message,
976
+ },
977
+ });
978
+ }
979
+ }
980
+ dispatchAgentTimelineMessage(msg) {
981
+ switch (msg.type) {
982
+ case "fetch_agent_timeline_request":
983
+ return this.handleFetchAgentTimelineRequest(msg);
984
+ case "agent.fork_context.request":
985
+ return this.handleAgentForkContextRequest(msg);
986
+ default:
987
+ return undefined;
988
+ }
989
+ }
990
+ dispatchAgentLifecycleMessage(msg) {
991
+ switch (msg.type) {
992
+ case "fetch_agents_request":
993
+ return this.handleFetchAgents(msg);
994
+ case "fetch_agent_history_request":
995
+ return this.handleFetchAgentHistory(msg);
996
+ case "fetch_recent_provider_sessions_request":
997
+ return this.handleFetchRecentProviderSessions(msg);
998
+ case "fetch_agent_request":
999
+ return this.handleFetchAgent(msg.agentId, msg.requestId);
1000
+ case "delete_agent_request":
1001
+ return this.handleDeleteAgentRequest(msg.agentId, msg.requestId);
1002
+ case "archive_agent_request":
1003
+ return this.handleArchiveAgentRequest(msg.agentId, msg.requestId);
1004
+ case "close_items_request":
1005
+ return this.handleCloseItemsRequest(msg);
1006
+ case "update_agent_request":
1007
+ return this.handleUpdateAgentRequest(msg.agentId, msg.name, msg.labels, msg.requestId);
1008
+ case "project.rename.request":
1009
+ return this.handleProjectRenameRequest(msg.projectId, msg.customName, msg.requestId);
1010
+ case "send_agent_message_request":
1011
+ return this.handleSendAgentMessageRequest(msg);
1012
+ case "wait_for_finish_request":
1013
+ return this.handleWaitForFinish(msg.agentId, msg.requestId, msg.timeoutMs);
1014
+ case "create_agent_request":
1015
+ return this.handleCreateAgentRequest(msg);
1016
+ case "resume_agent_request":
1017
+ return this.handleResumeAgentRequest(msg);
1018
+ case "import_agent_request":
1019
+ return this.handleImportAgentRequest(msg);
1020
+ case "refresh_agent_request":
1021
+ return this.handleRefreshAgentRequest(msg);
1022
+ case "cancel_agent_request":
1023
+ return this.handleCancelAgentRequest(msg.agentId, msg.requestId);
1024
+ case "agent_permission_response":
1025
+ return this.handleAgentPermissionResponse(msg.agentId, msg.requestId, msg.response);
1026
+ case "clear_agent_attention":
1027
+ return this.handleClearAgentAttention(msg.agentId, msg.requestId);
1028
+ default:
1029
+ return undefined;
1030
+ }
1031
+ }
1032
+ dispatchAgentConfigMessage(msg) {
1033
+ switch (msg.type) {
1034
+ case "set_agent_mode_request":
1035
+ return this.agentConfigSession.handleSetAgentModeRequest(msg);
1036
+ case "set_agent_model_request":
1037
+ return this.agentConfigSession.handleSetAgentModelRequest(msg);
1038
+ case "set_agent_feature_request":
1039
+ return this.agentConfigSession.handleSetAgentFeatureRequest(msg);
1040
+ case "set_agent_thinking_request":
1041
+ return this.agentConfigSession.handleSetAgentThinkingRequest(msg);
1042
+ case "get_daemon_config_request":
1043
+ this.emit({
1044
+ type: "get_daemon_config_response",
1045
+ payload: {
1046
+ requestId: msg.requestId,
1047
+ config: redactDaemonConfigForClient(this.daemonConfigStore.get()),
1048
+ },
1049
+ });
1050
+ return undefined;
1051
+ case "daemon.get_status.request":
1052
+ return this.daemonSession.handleGetStatusRequest(msg);
1053
+ case "daemon.get_pairing_offer.request":
1054
+ return this.daemonSession.handleGetPairingOfferRequest(msg);
1055
+ case "diagnostics.request":
1056
+ return this.daemonSession.handleDiagnosticsRequest(msg);
1057
+ case "daemon.update.request":
1058
+ return this.daemonSession.handleUpdateRequest(msg);
1059
+ case "set_daemon_config_request":
1060
+ this.emit({
1061
+ type: "set_daemon_config_response",
1062
+ payload: {
1063
+ requestId: msg.requestId,
1064
+ config: redactDaemonConfigForClient(this.daemonConfigStore.patch(msg.config)),
1065
+ },
1066
+ });
1067
+ return undefined;
1068
+ case "speech.settings.get_options.request":
1069
+ this.emit({
1070
+ type: "speech.settings.get_options.response",
1071
+ payload: {
1072
+ requestId: msg.requestId,
1073
+ options: this.getSpeechSettingsOptions?.() ?? EMPTY_SPEECH_SETTINGS_OPTIONS,
1074
+ },
1075
+ });
1076
+ return undefined;
1077
+ case "speech.tts.preview.request":
1078
+ this.handleTtsPreviewRequest(msg.requestId, msg.text, msg.voice);
1079
+ return undefined;
1080
+ case "agentPersonalities.get_stats.request": {
1081
+ const statsRequestId = msg.requestId;
1082
+ // The stats store is async (file-backed); resolve then emit.
1083
+ void Promise.resolve(this.getPersonalityStats?.() ?? {}).then((stats) => {
1084
+ this.emit({
1085
+ type: "agentPersonalities.get_stats.response",
1086
+ payload: { requestId: statsRequestId, stats },
1087
+ });
1088
+ return undefined;
1089
+ });
1090
+ return undefined;
1091
+ }
1092
+ case "read_project_config_request":
1093
+ return this.projectConfigSession.handleReadProjectConfigRequest(msg);
1094
+ case "write_project_config_request":
1095
+ return this.projectConfigSession.handleWriteProjectConfigRequest(msg);
1096
+ default:
1097
+ return undefined;
1098
+ }
1099
+ }
1100
+ // Synthesize a short preview sample and reply once (or with an error). Async
1101
+ // work is fire-and-forget: the correlated response is emitted from the
1102
+ // promise, mirroring how the get_stats request replies.
1103
+ handleTtsPreviewRequest(requestId, text, voice) {
1104
+ const preview = this.previewTts;
1105
+ if (!preview) {
1106
+ this.emit({
1107
+ type: "speech.tts.preview.response",
1108
+ payload: { requestId, error: "TTS is not configured on this host." },
1109
+ });
1110
+ return;
1111
+ }
1112
+ void preview({ text, voice })
1113
+ .then((result) => {
1114
+ this.emit({
1115
+ type: "speech.tts.preview.response",
1116
+ payload: result
1117
+ ? { requestId, audio: result.audio, format: result.format }
1118
+ : { requestId, error: "No audio was produced for this voice." },
1119
+ });
1120
+ return undefined;
1121
+ })
1122
+ .catch((error) => {
1123
+ this.emit({
1124
+ type: "speech.tts.preview.response",
1125
+ payload: {
1126
+ requestId,
1127
+ error: error instanceof Error ? error.message : "Voice preview failed.",
1128
+ },
1129
+ });
1130
+ });
1131
+ }
1132
+ // eslint-disable-next-line complexity
1133
+ dispatchCheckoutMessage(msg) {
1134
+ switch (msg.type) {
1135
+ case "checkout_status_request":
1136
+ return this.checkoutSession.handleStatusRequest(msg);
1137
+ case "validate_branch_request":
1138
+ return this.checkoutSession.handleValidateBranchRequest(msg);
1139
+ case "branch_suggestions_request":
1140
+ return this.checkoutSession.handleBranchSuggestionsRequest(msg);
1141
+ case "directory_suggestions_request":
1142
+ return this.handleDirectorySuggestionsRequest(msg);
1143
+ case "subscribe_checkout_diff_request":
1144
+ return this.checkoutSession.handleSubscribeDiffRequest(msg);
1145
+ case "unsubscribe_checkout_diff_request":
1146
+ this.checkoutSession.handleUnsubscribeDiffRequest(msg);
1147
+ return undefined;
1148
+ case "checkout_switch_branch_request":
1149
+ return this.checkoutSession.handleCheckoutSwitchBranchRequest(msg);
1150
+ case "checkout.rename_branch.request":
1151
+ return this.checkoutSession.handleCheckoutRenameBranchRequest(msg);
1152
+ case "checkout_commit_request":
1153
+ return this.checkoutSession.handleCheckoutCommitRequest(msg);
1154
+ case "checkout_merge_request":
1155
+ return this.checkoutSession.handleCheckoutMergeRequest(msg);
1156
+ case "checkout_merge_from_base_request":
1157
+ return this.checkoutSession.handleCheckoutMergeFromBaseRequest(msg);
1158
+ case "checkout_pull_request":
1159
+ return this.checkoutSession.handleCheckoutPullRequest(msg);
1160
+ case "checkout_push_request":
1161
+ return this.checkoutSession.handleCheckoutPushRequest(msg);
1162
+ case "checkout.refresh.request":
1163
+ return this.checkoutSession.handleRefreshRequest(msg);
1164
+ case "checkout_pr_create_request":
1165
+ return this.checkoutSession.handleCheckoutPrCreateRequest(msg);
1166
+ case "checkout_pr_merge_request":
1167
+ return this.checkoutSession.handleCheckoutPrMergeRequest(msg);
1168
+ case "checkout.github.set_auto_merge.request":
1169
+ return this.checkoutSession.handleCheckoutGithubSetAutoMergeRequest(msg);
1170
+ case "checkout.github.get_check_details.request":
1171
+ return this.checkoutSession.handleCheckoutGithubGetCheckDetailsRequest(msg);
1172
+ case "checkout_pr_status_request":
1173
+ return this.checkoutSession.handleCheckoutPrStatusRequest(msg);
1174
+ case "pull_request_timeline_request":
1175
+ return this.checkoutSession.handlePullRequestTimelineRequest(msg);
1176
+ case "github_search_request":
1177
+ return this.checkoutSession.handleGitHubSearchRequest(msg);
1178
+ case "hosting.search.request":
1179
+ return this.checkoutSession.handleHostingSearchRequest(msg);
1180
+ case "hosting.auth_status.request":
1181
+ return this.handleHostingAuthStatusRequest(msg);
1182
+ case "stash_save_request":
1183
+ return this.checkoutSession.handleStashSaveRequest(msg);
1184
+ case "stash_pop_request":
1185
+ return this.checkoutSession.handleStashPopRequest(msg);
1186
+ case "stash_list_request":
1187
+ return this.checkoutSession.handleStashListRequest(msg);
1188
+ default:
1189
+ return undefined;
1190
+ }
1191
+ }
1192
+ // Connection check for the host Git providers settings section: resolves a
1193
+ // provider's host-level credentials and performs a single forced auth probe.
1194
+ // User-initiated only — never called from polling or reconciliation paths.
1195
+ async handleHostingAuthStatusRequest(msg) {
1196
+ const { provider, requestId } = msg;
1197
+ const emitResult = (payload) => {
1198
+ this.emit({
1199
+ type: "hosting.auth_status.response",
1200
+ payload: { provider, requestId, ...payload },
1201
+ });
1202
+ };
1203
+ if (!this.gitHostingResolver) {
1204
+ emitResult({ authenticated: false, error: "Hosting is unavailable" });
1205
+ return;
1206
+ }
1207
+ // The wire provider id is an open string; a request naming a provider this
1208
+ // build doesn't support resolves to no service rather than throwing.
1209
+ const providerId = normalizeGitHostingProviderId(provider);
1210
+ if (!providerId) {
1211
+ emitResult({ authenticated: false, error: null });
1212
+ return;
1213
+ }
1214
+ try {
1215
+ const resolved = this.gitHostingResolver.resolveForProvider(providerId);
1216
+ if (!resolved.service) {
1217
+ emitResult({ authenticated: false, error: null });
1218
+ return;
1219
+ }
1220
+ // gh reads global config; Bitbucket ignores cwd. ottoHome is a stable
1221
+ // directory for a provider-level (non-workspace) auth check.
1222
+ const authenticated = await resolved.service.isAuthenticated({
1223
+ cwd: this.ottoHome,
1224
+ force: true,
1225
+ reason: "hosting-auth-status",
1226
+ });
1227
+ emitResult({ authenticated, error: null });
1228
+ }
1229
+ catch (error) {
1230
+ emitResult({
1231
+ authenticated: false,
1232
+ error: error instanceof Error ? error.message : String(error),
1233
+ });
1234
+ }
1235
+ }
1236
+ dispatchPreviewMessage(msg) {
1237
+ switch (msg.type) {
1238
+ case "preview.list_config.request":
1239
+ return this.handlePreviewListConfigRequest(msg.cwd, msg.requestId);
1240
+ case "preview.start.request":
1241
+ return this.handlePreviewStartRequest(msg.cwd, msg.name, msg.requestId);
1242
+ case "preview.bind_tab.request":
1243
+ return this.handlePreviewBindTabRequest(msg.serverId, msg.browserId, msg.requestId);
1244
+ case "preview.stop.request":
1245
+ return this.handlePreviewStopRequest(msg.serverId, msg.requestId);
1246
+ default:
1247
+ return undefined;
1248
+ }
1249
+ }
1250
+ dispatchWorkspaceAndProjectMessage(msg) {
1251
+ switch (msg.type) {
1252
+ case "fetch_workspaces_request":
1253
+ return this.handleFetchWorkspacesRequest(msg);
1254
+ case "otto_worktree_list_request":
1255
+ return this.handleOttoWorktreeListRequest(msg);
1256
+ case "otto_worktree_archive_request":
1257
+ return this.handleOttoWorktreeArchiveRequest(msg);
1258
+ case "create_otto_worktree_request":
1259
+ return this.handleCreateOttoWorktreeRequest(msg);
1260
+ case "workspace_setup_status_request":
1261
+ return this.handleWorkspaceSetupStatusRequest(msg);
1262
+ // COMPAT(desktopEditorBridge): added in v0.1.88, remove after 2026-12-03 once old clients no longer call daemon editor RPCs.
1263
+ case "list_available_editors_request":
1264
+ return this.handleLegacyListAvailableEditorsRequest(msg);
1265
+ case "open_in_editor_request":
1266
+ return this.handleLegacyOpenInEditorRequest(msg);
1267
+ case "open_project_request":
1268
+ return this.handleOpenProjectRequest(msg);
1269
+ case "project.add.request":
1270
+ return this.handleProjectAddRequest(msg);
1271
+ case "archive_workspace_request":
1272
+ return this.handleArchiveWorkspaceRequest(msg);
1273
+ case "project.remove.request":
1274
+ return this.handleProjectRemoveRequest(msg);
1275
+ case "workspace.create.request":
1276
+ return this.handleWorkspaceCreateRequest(msg);
1277
+ case "workspace.clear_attention.request":
1278
+ return this.handleWorkspaceClearAttentionRequest(msg);
1279
+ case "workspace.title.set.request":
1280
+ return this.handleWorkspaceTitleSetRequest(msg.workspaceId, msg.title, msg.requestId);
1281
+ default:
1282
+ return undefined;
1283
+ }
1284
+ }
1285
+ dispatchWorkspaceFilesMessage(msg) {
1286
+ switch (msg.type) {
1287
+ case "file_explorer_request":
1288
+ return this.workspaceFilesSession.handleFileExplorerRequest(msg);
1289
+ case "project_icon_request":
1290
+ return this.workspaceFilesSession.handleProjectIconRequest(msg);
1291
+ case "file_download_token_request":
1292
+ return this.workspaceFilesSession.handleFileDownloadTokenRequest(msg);
1293
+ case "file.upload.request":
1294
+ this.workspaceFilesSession.handleFileUploadRequest(msg);
1295
+ return undefined;
1296
+ case "file.write.request":
1297
+ return this.workspaceFilesSession.handleFileWriteRequest(msg);
1298
+ case "file.watch.subscribe.request":
1299
+ return this.workspaceFilesSession.handleFileWatchSubscribeRequest(msg);
1300
+ case "file.watch.unsubscribe.request":
1301
+ this.workspaceFilesSession.handleFileWatchUnsubscribeRequest(msg);
1302
+ return undefined;
1303
+ case "file.search.request":
1304
+ return this.workspaceFilesSession.handleFileSearchRequest(msg);
1305
+ case "file.replace.request":
1306
+ return this.workspaceFilesSession.handleFileReplaceRequest(msg);
1307
+ case "code.list_files.request":
1308
+ return this.workspaceFilesSession.handleCodeListFilesRequest(msg);
1309
+ case "code.symbols.request":
1310
+ return this.workspaceFilesSession.handleCodeSymbolsRequest(msg);
1311
+ case "code.outline.request":
1312
+ return this.workspaceFilesSession.handleCodeOutlineRequest(msg);
1313
+ default:
1314
+ return undefined;
1315
+ }
1316
+ }
1317
+ dispatchProviderMessage(msg) {
1318
+ switch (msg.type) {
1319
+ case "list_provider_models_request":
1320
+ return this.providerCatalogSession.handleListProviderModelsRequest(msg);
1321
+ case "list_provider_modes_request":
1322
+ return this.providerCatalogSession.handleListProviderModesRequest(msg);
1323
+ case "list_provider_features_request":
1324
+ return this.providerCatalogSession.handleListProviderFeaturesRequest(msg);
1325
+ case "list_available_providers_request":
1326
+ return this.providerCatalogSession.handleListAvailableProvidersRequest(msg);
1327
+ case "get_providers_snapshot_request":
1328
+ return this.providerCatalogSession.handleGetProvidersSnapshotRequest(msg);
1329
+ case "refresh_providers_snapshot_request":
1330
+ return this.providerCatalogSession.handleRefreshProvidersSnapshotRequest(msg);
1331
+ case "provider_diagnostic_request":
1332
+ return this.providerCatalogSession.handleProviderDiagnosticRequest(msg);
1333
+ case "provider.usage.list.request":
1334
+ return this.providerCatalogSession.handleProviderUsageListRequest(msg);
1335
+ default:
1336
+ return undefined;
1337
+ }
1338
+ }
1339
+ dispatchTerminalMessage(msg) {
1340
+ if (msg.type === "start_workspace_script_request") {
1341
+ return this.handleStartWorkspaceScriptRequest(msg);
1342
+ }
1343
+ return this.terminalController.dispatch(msg);
1344
+ }
1345
+ // eslint-disable-next-line complexity
1346
+ dispatchChatScheduleLoopMessage(msg) {
1347
+ switch (msg.type) {
1348
+ case "chat/create":
1349
+ return this.chatScheduleLoopSession.handleChatCreateRequest(msg);
1350
+ case "chat/list":
1351
+ return this.chatScheduleLoopSession.handleChatListRequest(msg);
1352
+ case "chat/inspect":
1353
+ return this.chatScheduleLoopSession.handleChatInspectRequest(msg);
1354
+ case "chat/delete":
1355
+ return this.chatScheduleLoopSession.handleChatDeleteRequest(msg);
1356
+ case "chat/post":
1357
+ return this.chatScheduleLoopSession.handleChatPostRequest(msg);
1358
+ case "chat/read":
1359
+ return this.chatScheduleLoopSession.handleChatReadRequest(msg);
1360
+ case "chat/wait":
1361
+ return this.chatScheduleLoopSession.handleChatWaitRequest(msg);
1362
+ case "loop/run":
1363
+ return this.chatScheduleLoopSession.handleLoopRunRequest(msg);
1364
+ case "loop/list":
1365
+ return this.chatScheduleLoopSession.handleLoopListRequest(msg);
1366
+ case "loop/inspect":
1367
+ return this.chatScheduleLoopSession.handleLoopInspectRequest(msg);
1368
+ case "loop/logs":
1369
+ return this.chatScheduleLoopSession.handleLoopLogsRequest(msg);
1370
+ case "loop/stop":
1371
+ return this.chatScheduleLoopSession.handleLoopStopRequest(msg);
1372
+ case "schedule/create":
1373
+ return this.chatScheduleLoopSession.handleScheduleCreateRequest(msg);
1374
+ case "schedule/list":
1375
+ return this.chatScheduleLoopSession.handleScheduleListRequest(msg);
1376
+ case "schedule/inspect":
1377
+ return this.chatScheduleLoopSession.handleScheduleInspectRequest(msg);
1378
+ case "schedule/logs":
1379
+ return this.chatScheduleLoopSession.handleScheduleLogsRequest(msg);
1380
+ case "schedule/pause":
1381
+ return this.chatScheduleLoopSession.handleSchedulePauseRequest(msg);
1382
+ case "schedule/resume":
1383
+ return this.chatScheduleLoopSession.handleScheduleResumeRequest(msg);
1384
+ case "schedule/delete":
1385
+ return this.chatScheduleLoopSession.handleScheduleDeleteRequest(msg);
1386
+ case "schedule/run-once":
1387
+ return this.chatScheduleLoopSession.handleScheduleRunOnceRequest(msg);
1388
+ case "schedule/update":
1389
+ return this.chatScheduleLoopSession.handleScheduleUpdateRequest(msg);
1390
+ default:
1391
+ return undefined;
1392
+ }
1393
+ }
1394
+ dispatchArtifactMessage(msg) {
1395
+ switch (msg.type) {
1396
+ case "artifact.list.request":
1397
+ return this.artifactSession.handleArtifactListRequest(msg);
1398
+ case "artifact.create.request":
1399
+ return this.artifactSession.handleArtifactCreateRequest(msg);
1400
+ case "artifact.update.request":
1401
+ return this.artifactSession.handleArtifactUpdateRequest(msg);
1402
+ case "artifact.regenerate.request":
1403
+ return this.artifactSession.handleArtifactRegenerateRequest(msg);
1404
+ case "artifact.cancel.request":
1405
+ return this.artifactSession.handleArtifactCancelRequest(msg);
1406
+ case "artifact.delete.request":
1407
+ return this.artifactSession.handleArtifactDeleteRequest(msg);
1408
+ case "artifact.star.request":
1409
+ return this.artifactSession.handleArtifactStarRequest(msg);
1410
+ case "artifact.get-content.request":
1411
+ return this.artifactSession.handleArtifactGetContentRequest(msg);
1412
+ default:
1413
+ return undefined;
1414
+ }
1415
+ }
1416
+ async dispatchMiscMessage(msg) {
1417
+ switch (msg.type) {
1418
+ case "list_commands_request":
1419
+ await this.handleListCommandsRequest(msg);
1420
+ return;
1421
+ case "agent.context.get_usage.request":
1422
+ await this.handleAgentContextGetUsageRequest(msg);
1423
+ return;
1424
+ case "register_push_token":
1425
+ this.handleRegisterPushToken(msg.token);
1426
+ return;
1427
+ }
1428
+ }
1429
+ resetPeakInflight() {
1430
+ this.peakInflightRequests = this.inflightRequests;
1431
+ }
1432
+ getSessionId() {
1433
+ return this.sessionId;
1434
+ }
1435
+ async handleBinaryFrame(binaryFrame) {
1436
+ if (binaryFrame.kind === "file_transfer") {
1437
+ await this.workspaceFilesSession.handleFileTransferFrame(binaryFrame.frame);
1438
+ return;
1439
+ }
1440
+ this.terminalController.handleBinaryFrame(binaryFrame.frame);
1441
+ }
1442
+ async handleRestartServerRequest(requestId, reason) {
1443
+ const lifecycleReason = normalizeClientRestartRpcReason(reason);
1444
+ const payload = {
1445
+ status: "restart_requested",
1446
+ clientId: this.clientId,
1447
+ };
1448
+ if (reason && reason.trim().length > 0) {
1449
+ payload.reason = reason;
1450
+ }
1451
+ payload.requestId = requestId;
1452
+ this.sessionLogger.warn({ reason: lifecycleReason }, "Restart requested via websocket");
1453
+ this.emit({
1454
+ type: "status",
1455
+ payload,
1456
+ });
1457
+ this.emitLifecycleIntent({
1458
+ type: "restart",
1459
+ clientId: this.clientId,
1460
+ requestId,
1461
+ reason: lifecycleReason,
1462
+ });
1463
+ }
1464
+ async handleShutdownServerRequest(requestId) {
1465
+ const reason = CLIENT_SHUTDOWN_RPC_REASON;
1466
+ this.sessionLogger.warn({ reason }, "Shutdown requested via websocket");
1467
+ this.emit({
1468
+ type: "status",
1469
+ payload: {
1470
+ status: "shutdown_requested",
1471
+ clientId: this.clientId,
1472
+ requestId,
1473
+ },
1474
+ });
1475
+ this.emitLifecycleIntent({
1476
+ type: "shutdown",
1477
+ clientId: this.clientId,
1478
+ requestId,
1479
+ reason,
1480
+ });
1481
+ }
1482
+ emitLifecycleIntent(intent) {
1483
+ if (!this.onLifecycleIntent) {
1484
+ return;
1485
+ }
1486
+ try {
1487
+ this.onLifecycleIntent(intent);
1488
+ }
1489
+ catch (error) {
1490
+ this.sessionLogger.error({ err: error, intent }, "Lifecycle intent handler failed");
1491
+ }
1492
+ }
1493
+ async handleDeleteAgentRequest(agentId, requestId) {
1494
+ this.sessionLogger.info({ agentId }, `Deleting agent ${agentId} from registry`);
1495
+ const knownWorkspaceId = this.agentManager.getAgent(agentId)?.workspaceId ??
1496
+ (await this.agentStorage.get(agentId))?.workspaceId ??
1497
+ null;
1498
+ // File-backed storage still needs an early delete fence before closeAgent().
1499
+ beginAgentDeleteIfSupported(this.agentStorage, agentId);
1500
+ try {
1501
+ await closeAgentCommand({ agentManager: this.agentManager }, agentId);
1502
+ }
1503
+ catch (error) {
1504
+ this.sessionLogger.warn({ err: error, agentId }, `Failed to close agent ${agentId} during delete`);
1505
+ }
1506
+ // Drain queued persistence from the just-closed agent before removing its
1507
+ // durable snapshot, otherwise an in-flight background write can recreate it.
1508
+ await this.agentManager.flush();
1509
+ try {
1510
+ await this.agentStorage.remove(agentId);
1511
+ await this.agentManager.deleteCommittedTimeline(agentId);
1512
+ }
1513
+ catch (error) {
1514
+ this.sessionLogger.error({ err: error, agentId }, `Failed to fully delete agent ${agentId}`);
1515
+ }
1516
+ this.emit({
1517
+ type: "agent_deleted",
1518
+ payload: {
1519
+ agentId,
1520
+ requestId,
1521
+ },
1522
+ });
1523
+ this.agentUpdates.removeAgent(agentId);
1524
+ if (knownWorkspaceId) {
1525
+ await this.emitWorkspaceUpdateForWorkspaceId(knownWorkspaceId);
1526
+ }
1527
+ }
1528
+ async handleArchiveAgentRequest(agentId, requestId) {
1529
+ this.sessionLogger.info({ agentId }, `Archiving agent ${agentId}`);
1530
+ const { archivedAt } = await this.archiveAgentForClose(agentId);
1531
+ this.emit({
1532
+ type: "agent_archived",
1533
+ payload: {
1534
+ agentId,
1535
+ archivedAt,
1536
+ requestId,
1537
+ },
1538
+ });
1539
+ }
1540
+ async archiveAgentForClose(agentId) {
1541
+ const { archivedAt, record: archivedRecord } = await archiveAgentCommand({
1542
+ agentManager: this.agentManager,
1543
+ agentStorage: this.agentStorage,
1544
+ logger: this.sessionLogger,
1545
+ }, agentId);
1546
+ if (this.agentUpdates.hasSubscription()) {
1547
+ const payload = await this.agentUpdates.emitStoredRecord(archivedRecord);
1548
+ if (payload.workspaceId) {
1549
+ await this.emitWorkspaceUpdateForWorkspaceId(payload.workspaceId);
1550
+ }
1551
+ }
1552
+ return { agentId, archivedAt };
1553
+ }
1554
+ async handleDetachAgentRequest(agentId, requestId) {
1555
+ this.sessionLogger.info({ agentId, requestId }, "Detaching agent from parent");
1556
+ try {
1557
+ const result = await detachAgentCommand({ agentManager: this.agentManager }, agentId);
1558
+ const affectedWorkspaceIds = new Set();
1559
+ if (!result.live) {
1560
+ const payload = await this.agentUpdates.emitStoredRecord(result.record);
1561
+ if (payload.workspaceId) {
1562
+ affectedWorkspaceIds.add(payload.workspaceId);
1563
+ }
1564
+ }
1565
+ else if (result.record.workspaceId) {
1566
+ affectedWorkspaceIds.add(result.record.workspaceId);
1567
+ }
1568
+ if (result.previousParentAgentId) {
1569
+ const rootWorkspaceId = await this.resolveDelegationRootWorkspaceId(result.previousParentAgentId);
1570
+ if (rootWorkspaceId) {
1571
+ affectedWorkspaceIds.add(rootWorkspaceId);
1572
+ }
1573
+ }
1574
+ await this.emitWorkspaceUpdatesForWorkspaceIds(affectedWorkspaceIds, {
1575
+ skipReconcile: true,
1576
+ });
1577
+ this.emit({
1578
+ type: "agent.detach.response",
1579
+ payload: {
1580
+ requestId,
1581
+ agentId,
1582
+ accepted: true,
1583
+ error: null,
1584
+ },
1585
+ });
1586
+ }
1587
+ catch (error) {
1588
+ const message = getErrorMessageOr(error, "Failed to detach agent");
1589
+ this.sessionLogger.error({ err: error, agentId, requestId }, "Failed to detach agent");
1590
+ this.emit({
1591
+ type: "agent.detach.response",
1592
+ payload: {
1593
+ requestId,
1594
+ agentId,
1595
+ accepted: false,
1596
+ error: message,
1597
+ },
1598
+ });
1599
+ }
1600
+ }
1601
+ async handleCloseItemsRequest(msg) {
1602
+ const archiveResults = await Promise.allSettled(msg.agentIds.map((agentId) => this.archiveAgentForClose(agentId)));
1603
+ const agents = [];
1604
+ for (let i = 0; i < archiveResults.length; i += 1) {
1605
+ const result = archiveResults[i];
1606
+ if (result.status === "fulfilled") {
1607
+ agents.push(result.value);
1608
+ }
1609
+ else {
1610
+ this.sessionLogger.warn({ err: result.reason, agentId: msg.agentIds[i], requestId: msg.requestId }, "Failed to archive agent during close_items batch");
1611
+ }
1612
+ }
1613
+ const terminals = [];
1614
+ for (const terminalId of msg.terminalIds) {
1615
+ try {
1616
+ terminals.push(this.terminalController.killTerminalForClose(terminalId));
1617
+ }
1618
+ catch (error) {
1619
+ this.sessionLogger.warn({ err: error, terminalId, requestId: msg.requestId }, "Failed to kill terminal during close_items batch");
1620
+ terminals.push({
1621
+ terminalId,
1622
+ success: false,
1623
+ });
1624
+ }
1625
+ }
1626
+ this.emit({
1627
+ type: "close_items_response",
1628
+ payload: {
1629
+ agents,
1630
+ terminals,
1631
+ requestId: msg.requestId,
1632
+ },
1633
+ });
1634
+ }
1635
+ async unarchiveAgentByHandle(handle) {
1636
+ const records = await this.agentStorage.list();
1637
+ const matched = records.find((record) => record.persistence?.provider === handle.provider &&
1638
+ record.persistence?.sessionId === handle.sessionId);
1639
+ if (!matched) {
1640
+ return;
1641
+ }
1642
+ await unarchiveAgentState(this.agentStorage, this.agentManager, matched.id);
1643
+ }
1644
+ async handleUpdateAgentRequest(agentId, name, labels, requestId) {
1645
+ this.sessionLogger.info({
1646
+ agentId,
1647
+ requestId,
1648
+ hasName: typeof name === "string",
1649
+ labelCount: labels ? Object.keys(labels).length : 0,
1650
+ }, "session: update_agent_request");
1651
+ try {
1652
+ const result = await updateAgentCommand({ agentManager: this.agentManager }, { agentId, name, labels });
1653
+ if (!result.accepted) {
1654
+ this.emit({
1655
+ type: "update_agent_response",
1656
+ payload: {
1657
+ requestId,
1658
+ agentId,
1659
+ accepted: false,
1660
+ error: result.error,
1661
+ },
1662
+ });
1663
+ return;
1664
+ }
1665
+ this.emit({
1666
+ type: "update_agent_response",
1667
+ payload: { requestId, agentId, accepted: true, error: null },
1668
+ });
1669
+ }
1670
+ catch (error) {
1671
+ this.sessionLogger.error({ err: error, agentId, requestId }, "session: update_agent_request error");
1672
+ this.emit({
1673
+ type: "activity_log",
1674
+ payload: {
1675
+ id: uuidv4(),
1676
+ timestamp: new Date(),
1677
+ type: "error",
1678
+ content: `Failed to update agent: ${getErrorMessage(error)}`,
1679
+ },
1680
+ });
1681
+ this.emit({
1682
+ type: "update_agent_response",
1683
+ payload: {
1684
+ requestId,
1685
+ agentId,
1686
+ accepted: false,
1687
+ error: getErrorMessageOr(error, "Failed to update agent"),
1688
+ },
1689
+ });
1690
+ }
1691
+ }
1692
+ async handleProjectRenameRequest(projectId, customName, requestId) {
1693
+ this.sessionLogger.info({ projectId, requestId, hasCustomName: typeof customName === "string" }, "session: project.rename.request");
1694
+ try {
1695
+ const existing = await this.projectRegistry.get(projectId);
1696
+ if (!existing) {
1697
+ this.emit({
1698
+ type: "project.rename.response",
1699
+ payload: {
1700
+ requestId,
1701
+ projectId,
1702
+ accepted: false,
1703
+ customName: null,
1704
+ error: "Project not found",
1705
+ },
1706
+ });
1707
+ return;
1708
+ }
1709
+ const trimmed = customName?.trim() ?? "";
1710
+ const nextCustomName = trimmed.length === 0 ? null : trimmed;
1711
+ await this.projectRegistry.upsert({
1712
+ ...existing,
1713
+ customName: nextCustomName,
1714
+ updatedAt: new Date().toISOString(),
1715
+ });
1716
+ this.emit({
1717
+ type: "project.rename.response",
1718
+ payload: {
1719
+ requestId,
1720
+ projectId,
1721
+ accepted: true,
1722
+ customName: nextCustomName,
1723
+ error: null,
1724
+ },
1725
+ });
1726
+ // Re-emit descriptors for every workspace under this project so the new
1727
+ // resolved name lands in the UI immediately.
1728
+ const workspaces = await this.workspaceRegistry.list();
1729
+ const affectedWorkspaceIds = workspaces
1730
+ .filter((workspace) => workspace.projectId === projectId)
1731
+ .map((workspace) => workspace.workspaceId);
1732
+ if (affectedWorkspaceIds.length > 0) {
1733
+ await this.emitWorkspaceUpdatesForWorkspaceIds(affectedWorkspaceIds, {
1734
+ skipReconcile: true,
1735
+ });
1736
+ }
1737
+ }
1738
+ catch (error) {
1739
+ this.sessionLogger.error({ err: error, projectId, requestId }, "session: project.rename.request error");
1740
+ this.emit({
1741
+ type: "activity_log",
1742
+ payload: {
1743
+ id: uuidv4(),
1744
+ timestamp: new Date(),
1745
+ type: "error",
1746
+ content: `Failed to rename project: ${getErrorMessage(error)}`,
1747
+ },
1748
+ });
1749
+ this.emit({
1750
+ type: "project.rename.response",
1751
+ payload: {
1752
+ requestId,
1753
+ projectId,
1754
+ accepted: false,
1755
+ customName: null,
1756
+ error: getErrorMessageOr(error, "Failed to rename project"),
1757
+ },
1758
+ });
1759
+ }
1760
+ }
1761
+ async handleProjectRemoveRequest(request) {
1762
+ const { projectId, requestId } = request;
1763
+ this.sessionLogger.info({ projectId, requestId }, "session: project.remove.request");
1764
+ try {
1765
+ const projectWorkspaces = (await this.workspaceRegistry.list()).filter((workspace) => workspace.projectId === projectId);
1766
+ const activeWorkspaceIds = projectWorkspaces
1767
+ .filter((workspace) => !workspace.archivedAt)
1768
+ .map((workspace) => workspace.workspaceId);
1769
+ if (activeWorkspaceIds.length > 0) {
1770
+ this.markWorkspaceArchiving(activeWorkspaceIds, new Date().toISOString());
1771
+ await this.emitWorkspaceUpdatesForWorkspaceIds(activeWorkspaceIds, {
1772
+ skipReconcile: true,
1773
+ });
1774
+ }
1775
+ const removedWorkspaceIds = [];
1776
+ try {
1777
+ for (const workspaceId of activeWorkspaceIds) {
1778
+ await archiveWorkspaceContents({
1779
+ agentManager: this.agentManager,
1780
+ agentStorage: this.agentStorage,
1781
+ killTerminalsForWorkspace: (id) => this.terminalController.killTerminalsForWorkspace(id),
1782
+ sessionLogger: this.sessionLogger,
1783
+ }, workspaceId);
1784
+ await this.archiveWorkspaceRecord(workspaceId);
1785
+ removedWorkspaceIds.push(workspaceId);
1786
+ }
1787
+ await this.projectRegistry.remove(projectId);
1788
+ }
1789
+ finally {
1790
+ if (activeWorkspaceIds.length > 0) {
1791
+ this.clearWorkspaceArchiving(activeWorkspaceIds);
1792
+ }
1793
+ }
1794
+ const updateIds = removedWorkspaceIds.length > 0
1795
+ ? removedWorkspaceIds
1796
+ : [projectWorkspaces[0]?.workspaceId ?? projectId];
1797
+ await this.emitWorkspaceUpdatesForWorkspaceIds(updateIds, {
1798
+ skipReconcile: true,
1799
+ removedProjectId: projectId,
1800
+ });
1801
+ this.emit({
1802
+ type: "project.remove.response",
1803
+ payload: {
1804
+ requestId,
1805
+ projectId,
1806
+ accepted: true,
1807
+ removedWorkspaceIds,
1808
+ error: null,
1809
+ },
1810
+ });
1811
+ }
1812
+ catch (error) {
1813
+ this.sessionLogger.error({ err: error, projectId, requestId }, "session: project.remove.request error");
1814
+ this.emit({
1815
+ type: "activity_log",
1816
+ payload: {
1817
+ id: uuidv4(),
1818
+ timestamp: new Date(),
1819
+ type: "error",
1820
+ content: `Failed to remove project: ${getErrorMessage(error)}`,
1821
+ },
1822
+ });
1823
+ this.emit({
1824
+ type: "project.remove.response",
1825
+ payload: {
1826
+ requestId,
1827
+ projectId,
1828
+ accepted: false,
1829
+ removedWorkspaceIds: [],
1830
+ error: getErrorMessageOr(error, "Failed to remove project"),
1831
+ },
1832
+ });
1833
+ }
1834
+ }
1835
+ async handleWorkspaceTitleSetRequest(workspaceId, title, requestId) {
1836
+ this.sessionLogger.info({ workspaceId, requestId, hasTitle: typeof title === "string" }, "session: workspace.title.set.request");
1837
+ try {
1838
+ const existing = await this.workspaceRegistry.get(workspaceId);
1839
+ if (!existing) {
1840
+ this.emit({
1841
+ type: "workspace.title.set.response",
1842
+ payload: {
1843
+ requestId,
1844
+ workspaceId,
1845
+ accepted: false,
1846
+ title: null,
1847
+ error: "Workspace not found",
1848
+ },
1849
+ });
1850
+ return;
1851
+ }
1852
+ const trimmed = title?.trim() ?? "";
1853
+ const nextTitle = trimmed.length === 0 ? null : trimmed;
1854
+ await this.workspaceRegistry.upsert({
1855
+ ...existing,
1856
+ title: nextTitle,
1857
+ updatedAt: new Date().toISOString(),
1858
+ });
1859
+ this.emit({
1860
+ type: "workspace.title.set.response",
1861
+ payload: {
1862
+ requestId,
1863
+ workspaceId,
1864
+ accepted: true,
1865
+ title: nextTitle,
1866
+ error: null,
1867
+ },
1868
+ });
1869
+ await this.emitWorkspaceUpdatesForWorkspaceIds([workspaceId], {
1870
+ skipReconcile: true,
1871
+ });
1872
+ }
1873
+ catch (error) {
1874
+ this.sessionLogger.error({ err: error, workspaceId, requestId }, "session: workspace.title.set.request error");
1875
+ this.emit({
1876
+ type: "activity_log",
1877
+ payload: {
1878
+ id: uuidv4(),
1879
+ timestamp: new Date(),
1880
+ type: "error",
1881
+ content: `Failed to set workspace title: ${getErrorMessage(error)}`,
1882
+ },
1883
+ });
1884
+ this.emit({
1885
+ type: "workspace.title.set.response",
1886
+ payload: {
1887
+ requestId,
1888
+ workspaceId,
1889
+ accepted: false,
1890
+ title: null,
1891
+ error: getErrorMessageOr(error, "Failed to set workspace title"),
1892
+ },
1893
+ });
1894
+ }
1895
+ }
1896
+ /**
1897
+ * UI-initiated preview RPCs behind the Preview toolbar button. Mirrors what
1898
+ * the agent-facing preview_* tools do (packages/server/src/server/preview/preview-tools.ts)
1899
+ * but is driven by the app directly instead of an agent tool call — the
1900
+ * caller creates and places the browser tab itself, then reports the
1901
+ * binding back via preview.bind_tab so a later agent preview_start call
1902
+ * finds the same designated tab.
1903
+ */
1904
+ async handlePreviewListConfigRequest(cwd, requestId) {
1905
+ try {
1906
+ const config = await readLaunchConfig(cwd);
1907
+ // Collect running servers from DevServerManager
1908
+ let runningServers = [];
1909
+ if (this.previewDevServers) {
1910
+ // Reconcile against reality (port probes), not just the in-memory map:
1911
+ // the map is wiped on daemon restart while the detached dev-server child
1912
+ // keeps serving, so a plain list() would report a live preview as "not
1913
+ // started" after every tsx-watch restart in dev. See reconcileRunning.
1914
+ const reconciled = await this.previewDevServers.reconcileRunning({
1915
+ cwd,
1916
+ configured: config?.configurations.map((entry) => ({
1917
+ name: entry.name,
1918
+ port: entry.port,
1919
+ })) ?? [],
1920
+ });
1921
+ runningServers = reconciled.map((s) => ({
1922
+ serverId: s.serverId,
1923
+ name: s.name,
1924
+ url: s.url,
1925
+ port: s.port,
1926
+ status: s.status,
1927
+ }));
1928
+ }
1929
+ this.emit({
1930
+ type: "preview.list_config.response",
1931
+ payload: {
1932
+ requestId,
1933
+ cwd,
1934
+ configured: config !== null,
1935
+ servers: config?.configurations.map((entry) => ({ name: entry.name, port: entry.port })) ?? [],
1936
+ runningServers,
1937
+ error: null,
1938
+ },
1939
+ });
1940
+ }
1941
+ catch (error) {
1942
+ this.emit({
1943
+ type: "preview.list_config.response",
1944
+ payload: {
1945
+ requestId,
1946
+ cwd,
1947
+ configured: false,
1948
+ servers: [],
1949
+ error: error instanceof LaunchConfigError ? error.message : getErrorMessage(error),
1950
+ },
1951
+ });
1952
+ }
1953
+ }
1954
+ async handlePreviewStartRequest(cwd, name, requestId) {
1955
+ if (!this.previewDevServers) {
1956
+ this.emit({
1957
+ type: "preview.start.response",
1958
+ payload: {
1959
+ requestId,
1960
+ cwd,
1961
+ success: false,
1962
+ server: null,
1963
+ reused: false,
1964
+ error: "Preview servers are disabled on this daemon.",
1965
+ },
1966
+ });
1967
+ return;
1968
+ }
1969
+ try {
1970
+ const started = await this.previewDevServers.start({ cwd, name });
1971
+ this.emit({
1972
+ type: "preview.start.response",
1973
+ payload: {
1974
+ requestId,
1975
+ cwd,
1976
+ success: true,
1977
+ reused: started.reused,
1978
+ server: {
1979
+ serverId: started.server.serverId,
1980
+ name: started.server.name,
1981
+ url: started.server.url,
1982
+ port: started.server.port,
1983
+ status: started.server.status,
1984
+ boundBrowserId: started.server.boundBrowserId,
1985
+ },
1986
+ error: null,
1987
+ },
1988
+ });
1989
+ }
1990
+ catch (error) {
1991
+ this.emit({
1992
+ type: "preview.start.response",
1993
+ payload: {
1994
+ requestId,
1995
+ cwd,
1996
+ success: false,
1997
+ server: null,
1998
+ reused: false,
1999
+ error: getErrorMessage(error),
2000
+ },
2001
+ });
2002
+ }
2003
+ }
2004
+ async handlePreviewBindTabRequest(serverId, browserId, requestId) {
2005
+ if (!this.previewDevServers) {
2006
+ this.emit({
2007
+ type: "preview.bind_tab.response",
2008
+ payload: {
2009
+ requestId,
2010
+ success: false,
2011
+ error: "Preview servers are disabled on this daemon.",
2012
+ },
2013
+ });
2014
+ return;
2015
+ }
2016
+ try {
2017
+ this.previewDevServers.bindTab(serverId, browserId);
2018
+ this.emit({
2019
+ type: "preview.bind_tab.response",
2020
+ payload: { requestId, success: true, error: null },
2021
+ });
2022
+ }
2023
+ catch (error) {
2024
+ this.emit({
2025
+ type: "preview.bind_tab.response",
2026
+ payload: { requestId, success: false, error: getErrorMessage(error) },
2027
+ });
2028
+ }
2029
+ }
2030
+ async handlePreviewStopRequest(serverId, requestId) {
2031
+ if (!this.previewDevServers) {
2032
+ this.emit({
2033
+ type: "preview.stop.response",
2034
+ payload: {
2035
+ requestId,
2036
+ success: false,
2037
+ error: "Preview servers are disabled on this daemon.",
2038
+ },
2039
+ });
2040
+ return;
2041
+ }
2042
+ try {
2043
+ await this.previewDevServers.stop(serverId);
2044
+ this.emit({
2045
+ type: "preview.stop.response",
2046
+ payload: { requestId, success: true, error: null },
2047
+ });
2048
+ }
2049
+ catch (error) {
2050
+ this.emit({
2051
+ type: "preview.stop.response",
2052
+ payload: { requestId, success: false, error: getErrorMessage(error) },
2053
+ });
2054
+ }
2055
+ }
2056
+ /**
2057
+ * Handle text message to agent (with optional image attachments)
2058
+ */
2059
+ async handleSendAgentMessage(agentId, text, messageId, images, attachments, runOptions, options) {
2060
+ this.sessionLogger.info({
2061
+ agentId,
2062
+ textPreview: text.substring(0, 50),
2063
+ imageCount: images?.length ?? 0,
2064
+ attachmentCount: attachments?.length ?? 0,
2065
+ }, `Sending text to agent ${agentId}${images && images.length > 0 ? ` with ${images.length} image attachment(s)` : ""}${attachments && attachments.length > 0
2066
+ ? ` and ${attachments.length} structured attachment(s)`
2067
+ : ""}`);
2068
+ const promptText = options?.spokenInput ? wrapSpokenInput(text) : text;
2069
+ const prompt = this.buildAgentPrompt(promptText, images, attachments);
2070
+ try {
2071
+ await sendPromptToAgent({
2072
+ agentManager: this.agentManager,
2073
+ agentStorage: this.agentStorage,
2074
+ agentId,
2075
+ prompt,
2076
+ messageId,
2077
+ runOptions,
2078
+ logger: this.sessionLogger,
2079
+ });
2080
+ return { ok: true };
2081
+ }
2082
+ catch (error) {
2083
+ this.handleAgentRunError(agentId, error, "Failed to send agent message");
2084
+ return {
2085
+ ok: false,
2086
+ error: error instanceof Error ? error.message : String(error),
2087
+ };
2088
+ }
2089
+ }
2090
+ /**
2091
+ * Handle create agent request
2092
+ */
2093
+ /**
2094
+ * Resolve an optional personality id against the roster + this cwd's provider
2095
+ * snapshot and fold its identity onto the create config. Availability is
2096
+ * re-checked here (the cwd may differ from where the picker resolved it); an
2097
+ * unavailable or unknown personality is skipped with a warning rather than
2098
+ * failing the create — the agent still runs with the chosen brain, just
2099
+ * without personality identity. The brain fields are never overridden; only
2100
+ * `personalitySnapshot` and (when the caller set none) `systemPrompt` are added.
2101
+ */
2102
+ async applyPersonalityIdentityToConfig(config, personalityId) {
2103
+ if (!personalityId) {
2104
+ return config;
2105
+ }
2106
+ const roster = this.daemonConfigStore.get().agentPersonalities?.personalities ?? [];
2107
+ const personality = roster.find((entry) => entry.id === personalityId);
2108
+ if (!personality) {
2109
+ this.sessionLogger.warn({ personalityId }, "create_agent: personality id not found in roster; spawning without personality identity");
2110
+ return config;
2111
+ }
2112
+ const entries = await this.providerSnapshotManager.listProviders({
2113
+ cwd: config.cwd,
2114
+ wait: true,
2115
+ });
2116
+ const resolution = resolvePersonality(personality, entries);
2117
+ if (resolution.status === "unavailable") {
2118
+ this.sessionLogger.warn({ personalityId, reason: resolution.reason }, "create_agent: personality unavailable for cwd; spawning without personality identity");
2119
+ return config;
2120
+ }
2121
+ const snapshot = resolution.snapshot;
2122
+ return {
2123
+ ...config,
2124
+ personalitySnapshot: snapshot,
2125
+ ...(config.systemPrompt === undefined && snapshot.systemPrompt !== undefined
2126
+ ? { systemPrompt: snapshot.systemPrompt }
2127
+ : {}),
2128
+ };
2129
+ }
2130
+ /**
2131
+ * Resolve a roster personality against a *running* agent's cwd for a live
2132
+ * switch (agent.personality.set). Unlike the spawn-time soft-skip above, an
2133
+ * unknown or unavailable personality throws — the RPC rejects with the reason
2134
+ * instead of half-applying.
2135
+ */
2136
+ async resolvePersonalitySnapshotForAgent(agentId, personalityId) {
2137
+ const agent = this.agentManager.getAgent(agentId);
2138
+ if (!agent) {
2139
+ throw new Error(`Agent not found: ${agentId}`);
2140
+ }
2141
+ const roster = this.daemonConfigStore.get().agentPersonalities?.personalities ?? [];
2142
+ const personality = roster.find((entry) => entry.id === personalityId);
2143
+ if (!personality) {
2144
+ throw new Error(`Personality not found: ${personalityId}`);
2145
+ }
2146
+ // Warm only the personality's own provider — a cold workspace snapshot
2147
+ // would otherwise fan out to every registered provider (network probes)
2148
+ // and stall the switch for seconds.
2149
+ const entries = await this.providerSnapshotManager.listProviders({
2150
+ cwd: agent.config.cwd,
2151
+ providers: [personality.provider],
2152
+ wait: true,
2153
+ });
2154
+ const resolution = resolvePersonality(personality, entries);
2155
+ if (resolution.status === "unavailable") {
2156
+ throw new Error(resolution.reason);
2157
+ }
2158
+ return resolution.snapshot;
2159
+ }
2160
+ async handleCreateAgentRequest(msg) {
2161
+ const { config, personality, worktreeName, requestId, initialPrompt, clientMessageId, outputSchema, git, worktree, autoArchive, images, attachments, labels, env, } = msg;
2162
+ this.sessionLogger.info({ cwd: config.cwd, provider: config.provider, worktreeName }, `Creating agent in ${config.cwd} (${config.provider})${worktreeName ? ` with worktree ${worktreeName}` : ""}`);
2163
+ let createdWorktreeForCleanup = null;
2164
+ let createdAgentId = null;
2165
+ try {
2166
+ const trimmedPrompt = initialPrompt?.trim();
2167
+ const { provisionalTitle } = resolveCreateAgentTitles({
2168
+ configTitle: config.title,
2169
+ initialPrompt: trimmedPrompt,
2170
+ });
2171
+ const firstAgentContext = {
2172
+ ...(trimmedPrompt ? { prompt: trimmedPrompt } : {}),
2173
+ ...(attachments && attachments.length > 0 ? { attachments } : {}),
2174
+ };
2175
+ const workspacePromptTitle = resolveFirstAgentPromptTitle(firstAgentContext);
2176
+ const createdWorktree = await this.createAgentLifecycleDispatch.createWorktreeForRequest({
2177
+ cwd: config.cwd,
2178
+ target: worktree,
2179
+ firstAgentContext,
2180
+ hasLegacyGitOptions: Boolean(git),
2181
+ });
2182
+ createdWorktreeForCleanup = createdWorktree;
2183
+ const baseCreateAgentConfig = createdWorktree
2184
+ ? { ...config, cwd: createdWorktree.worktree.worktreePath }
2185
+ : config;
2186
+ // A composer-selected personality arrives by id; resolve it against the
2187
+ // (possibly worktree) cwd and snapshot its identity onto the agent so the
2188
+ // live spinner, voice, and prompt read as that personality. The brain in
2189
+ // `config` (provider/model/mode/effort) is authoritative — hand-deviations
2190
+ // in the picker keep the identity but override the settings.
2191
+ const createAgentConfig = await this.applyPersonalityIdentityToConfig(baseCreateAgentConfig, personality);
2192
+ const workspaceId = await this.workspaceProvisioning.resolveOrCreateWorkspaceIdForCreateAgent({
2193
+ createdWorktree,
2194
+ requestedWorkspaceId: msg.workspaceId,
2195
+ cwd: createAgentConfig.cwd,
2196
+ initialTitle: workspacePromptTitle,
2197
+ });
2198
+ const createdDirectoryWorkspaceForAgent = !createdWorktree && !msg.workspaceId;
2199
+ const { snapshot, liveSnapshot } = await createAgentCommand({
2200
+ agentManager: this.agentManager,
2201
+ agentStorage: this.agentStorage,
2202
+ logger: this.sessionLogger,
2203
+ ottoHome: this.ottoHome,
2204
+ worktreesRoot: this.worktreesRoot,
2205
+ providerSnapshotManager: this.providerSnapshotManager,
2206
+ }, {
2207
+ kind: "session",
2208
+ config: createAgentConfig,
2209
+ workspaceId,
2210
+ worktreeName,
2211
+ initialPrompt,
2212
+ clientMessageId,
2213
+ outputSchema,
2214
+ images,
2215
+ attachments,
2216
+ git,
2217
+ labels,
2218
+ env,
2219
+ provisionalTitle,
2220
+ firstAgentContext,
2221
+ buildSessionConfig: (sessionConfig, gitOptions, legacyWorktreeName, ctx) => this.buildAgentSessionConfig(sessionConfig, gitOptions, legacyWorktreeName, ctx),
2222
+ });
2223
+ createdAgentId = snapshot.id;
2224
+ await this.agentUpdates.forwardLiveAgent(snapshot);
2225
+ if (createdDirectoryWorkspaceForAgent && trimmedPrompt) {
2226
+ this.workspaceAutoName.scheduleForDirectory({
2227
+ workspaceId,
2228
+ cwd: createAgentConfig.cwd,
2229
+ firstAgentContext,
2230
+ }, { currentSelection: this.getFocusedAgentSelectionForCwd(createAgentConfig.cwd) });
2231
+ }
2232
+ this.createAgentLifecycleDispatch.registerAutoArchiveIfRequested({
2233
+ autoArchive,
2234
+ agentId: snapshot.id,
2235
+ createdWorktree,
2236
+ });
2237
+ if (requestId) {
2238
+ const agentPayload = await this.buildAgentPayload(liveSnapshot);
2239
+ this.emit({
2240
+ type: "status",
2241
+ payload: {
2242
+ status: "agent_created",
2243
+ agentId: liveSnapshot.id,
2244
+ requestId,
2245
+ agent: agentPayload,
2246
+ },
2247
+ });
2248
+ }
2249
+ this.sessionLogger.info({ agentId: snapshot.id, provider: snapshot.provider }, `Created agent ${snapshot.id} (${snapshot.provider})`);
2250
+ }
2251
+ catch (error) {
2252
+ await this.createAgentLifecycleDispatch.cleanupCreatedWorktreeAfterFailedAgentCreate({
2253
+ createdWorktree: createdWorktreeForCleanup,
2254
+ createdAgentId,
2255
+ });
2256
+ const wireError = toWorktreeWireError(error);
2257
+ this.sessionLogger.error({ err: error }, "Failed to create agent");
2258
+ if (requestId) {
2259
+ this.emit({
2260
+ type: "status",
2261
+ payload: {
2262
+ status: "agent_create_failed",
2263
+ requestId,
2264
+ error: wireError.message,
2265
+ errorCode: wireError.code,
2266
+ },
2267
+ });
2268
+ }
2269
+ this.emit({
2270
+ type: "activity_log",
2271
+ payload: {
2272
+ id: uuidv4(),
2273
+ timestamp: new Date(),
2274
+ type: "error",
2275
+ content: `Failed to create agent: ${wireError.message}`,
2276
+ },
2277
+ });
2278
+ }
2279
+ }
2280
+ async handleResumeAgentRequest(msg) {
2281
+ const { handle, overrides, requestId } = msg;
2282
+ if (!handle) {
2283
+ this.sessionLogger.warn("Resume request missing persistence handle");
2284
+ if (requestId) {
2285
+ this.emit({
2286
+ type: "rpc_error",
2287
+ payload: {
2288
+ requestId,
2289
+ requestType: msg.type,
2290
+ error: "Unable to resume agent: missing persistence handle",
2291
+ code: "agent_resume_failed",
2292
+ },
2293
+ });
2294
+ }
2295
+ this.emit({
2296
+ type: "activity_log",
2297
+ payload: {
2298
+ id: uuidv4(),
2299
+ timestamp: new Date(),
2300
+ type: "error",
2301
+ content: "Unable to resume agent: missing persistence handle",
2302
+ },
2303
+ });
2304
+ return;
2305
+ }
2306
+ this.sessionLogger.info({ sessionId: handle.sessionId, provider: handle.provider }, `Resuming agent ${handle.sessionId} (${handle.provider})`);
2307
+ try {
2308
+ await this.unarchiveAgentByHandle(handle);
2309
+ const snapshot = await this.agentManager.resumeAgentFromPersistence(handle, overrides);
2310
+ await unarchiveAgentState(this.agentStorage, this.agentManager, snapshot.id);
2311
+ await this.agentManager.hydrateTimelineFromProvider(snapshot.id);
2312
+ await this.agentUpdates.forwardLiveAgent(snapshot);
2313
+ const timelineSize = this.agentManager.getTimeline(snapshot.id).length;
2314
+ if (requestId) {
2315
+ const agentPayload = await this.buildAgentPayload(snapshot);
2316
+ this.emit({
2317
+ type: "status",
2318
+ payload: {
2319
+ status: "agent_resumed",
2320
+ agentId: snapshot.id,
2321
+ requestId,
2322
+ timelineSize,
2323
+ agent: agentPayload,
2324
+ },
2325
+ });
2326
+ }
2327
+ }
2328
+ catch (error) {
2329
+ const message = getErrorMessage(error);
2330
+ this.sessionLogger.error({ err: error }, "Failed to resume agent");
2331
+ if (requestId) {
2332
+ this.emit({
2333
+ type: "rpc_error",
2334
+ payload: {
2335
+ requestId,
2336
+ requestType: msg.type,
2337
+ error: message,
2338
+ code: "agent_resume_failed",
2339
+ },
2340
+ });
2341
+ }
2342
+ this.emit({
2343
+ type: "activity_log",
2344
+ payload: {
2345
+ id: uuidv4(),
2346
+ timestamp: new Date(),
2347
+ type: "error",
2348
+ content: `Failed to resume agent: ${message}`,
2349
+ },
2350
+ });
2351
+ }
2352
+ }
2353
+ async handleImportAgentRequest(msg) {
2354
+ const normalized = normalizeImportAgentRequest(msg);
2355
+ if ("error" in normalized) {
2356
+ this.emit({
2357
+ type: "status",
2358
+ payload: {
2359
+ status: "agent_create_failed",
2360
+ requestId: msg.requestId,
2361
+ error: normalized.error,
2362
+ },
2363
+ });
2364
+ return;
2365
+ }
2366
+ const { provider, providerHandleId, requestId } = normalized;
2367
+ this.sessionLogger.info({ providerHandleId, provider }, `Importing agent ${providerHandleId} (${provider})`);
2368
+ try {
2369
+ if (!normalized.cwd) {
2370
+ throw new Error("Import requires cwd from the selected provider session");
2371
+ }
2372
+ // An imported agent mints its own workspace; ownership is its workspaceId,
2373
+ // never an existing same-cwd workspace resolved by path.
2374
+ const workspace = await this.workspaceProvisioning.createWorkspaceForDirectory(normalized.cwd);
2375
+ const { snapshot, timelineSize } = await importProviderSession({
2376
+ request: normalized,
2377
+ workspaceId: workspace.workspaceId,
2378
+ agentManager: this.agentManager,
2379
+ agentStorage: this.agentStorage,
2380
+ logger: this.sessionLogger,
2381
+ });
2382
+ await this.registerWorkspaceForImportedAgent(workspace);
2383
+ const agentPayload = await this.buildAgentPayload(snapshot);
2384
+ this.emit({
2385
+ type: "status",
2386
+ payload: {
2387
+ status: "agent_resumed",
2388
+ agentId: snapshot.id,
2389
+ requestId,
2390
+ timelineSize,
2391
+ agent: agentPayload,
2392
+ },
2393
+ });
2394
+ }
2395
+ catch (error) {
2396
+ const message = error instanceof Error ? error.message : String(error);
2397
+ this.sessionLogger.error({ err: error }, "Failed to import agent");
2398
+ this.emit({
2399
+ type: "status",
2400
+ payload: {
2401
+ status: "agent_create_failed",
2402
+ requestId,
2403
+ error: message,
2404
+ },
2405
+ });
2406
+ this.emit({
2407
+ type: "activity_log",
2408
+ payload: {
2409
+ id: uuidv4(),
2410
+ timestamp: new Date(),
2411
+ type: "error",
2412
+ content: `Failed to import agent: ${message}`,
2413
+ },
2414
+ });
2415
+ }
2416
+ }
2417
+ async handleRefreshAgentRequest(msg) {
2418
+ const { agentId, requestId } = msg;
2419
+ this.sessionLogger.info({ agentId }, `Refreshing agent ${agentId} from persistence`);
2420
+ try {
2421
+ await unarchiveAgentState(this.agentStorage, this.agentManager, agentId);
2422
+ await this.unarchiveOwningWorkspaceForAgent(agentId);
2423
+ let snapshot;
2424
+ const existing = this.agentManager.getAgent(agentId);
2425
+ if (existing) {
2426
+ await this.interruptAgentIfRunning(agentId);
2427
+ snapshot = await this.agentManager.reloadAgentSession(agentId, undefined, {
2428
+ rehydrateFromDisk: true,
2429
+ });
2430
+ }
2431
+ else {
2432
+ const record = await this.agentStorage.get(agentId);
2433
+ if (!record) {
2434
+ throw new Error(`Agent not found: ${agentId}`);
2435
+ }
2436
+ const registeredProviderIds = this.providerSnapshotManager.listRegisteredProviderIds();
2437
+ if (!isStoredAgentProviderAvailable(record, registeredProviderIds)) {
2438
+ throw new Error(`Agent ${agentId} references unavailable provider '${record.provider}'`);
2439
+ }
2440
+ const handle = toAgentPersistenceHandle(registeredProviderIds, record.persistence);
2441
+ if (!handle) {
2442
+ throw new Error(`Agent ${agentId} cannot be refreshed because it lacks persistence`);
2443
+ }
2444
+ snapshot = await this.agentManager.resumeAgentFromPersistence(handle, buildConfigOverrides(record), agentId, extractTimestamps(record));
2445
+ }
2446
+ await this.agentManager.hydrateTimelineFromProvider(agentId);
2447
+ await this.agentUpdates.forwardLiveAgent(snapshot);
2448
+ const timelineSize = this.agentManager.getTimeline(agentId).length;
2449
+ if (requestId) {
2450
+ this.emit({
2451
+ type: "status",
2452
+ payload: {
2453
+ status: "agent_refreshed",
2454
+ agentId,
2455
+ requestId,
2456
+ timelineSize,
2457
+ },
2458
+ });
2459
+ }
2460
+ }
2461
+ catch (error) {
2462
+ const message = getErrorMessage(error);
2463
+ this.sessionLogger.error({ err: error, agentId }, `Failed to refresh agent ${agentId}`);
2464
+ if (requestId) {
2465
+ this.emit({
2466
+ type: "rpc_error",
2467
+ payload: {
2468
+ requestId,
2469
+ requestType: msg.type,
2470
+ error: message,
2471
+ code: error instanceof WorktreeRequestError ? error.code : "agent_refresh_failed",
2472
+ },
2473
+ });
2474
+ }
2475
+ this.emit({
2476
+ type: "activity_log",
2477
+ payload: {
2478
+ id: uuidv4(),
2479
+ timestamp: new Date(),
2480
+ type: "error",
2481
+ content: `Failed to refresh agent: ${message}`,
2482
+ },
2483
+ });
2484
+ }
2485
+ }
2486
+ async handleCancelAgentRequest(agentId, requestId) {
2487
+ this.sessionLogger.info({ agentId }, `Cancel request received for agent ${agentId}`);
2488
+ try {
2489
+ await cancelAgentRunCommand({ agentManager: this.agentManager, logger: this.sessionLogger }, agentId);
2490
+ if (requestId) {
2491
+ const agent = this.agentManager.getAgent(agentId);
2492
+ const payload = agent ? await this.buildAgentPayload(agent) : null;
2493
+ this.emit({
2494
+ type: "cancel_agent_response",
2495
+ payload: {
2496
+ requestId,
2497
+ agentId,
2498
+ agent: payload,
2499
+ },
2500
+ });
2501
+ }
2502
+ }
2503
+ catch (error) {
2504
+ this.handleAgentRunError(agentId, error, "Failed to cancel running agent on request");
2505
+ }
2506
+ }
2507
+ async handleAgentRewindRequest(msg) {
2508
+ try {
2509
+ await this.agentManager.rewind(msg.agentId, msg.messageId, msg.mode);
2510
+ this.emit({
2511
+ type: "agent.rewind.response",
2512
+ payload: {
2513
+ requestId: msg.requestId,
2514
+ agentId: msg.agentId,
2515
+ ok: true,
2516
+ error: null,
2517
+ },
2518
+ });
2519
+ }
2520
+ catch (error) {
2521
+ this.emit({
2522
+ type: "agent.rewind.response",
2523
+ payload: {
2524
+ requestId: msg.requestId,
2525
+ agentId: msg.agentId,
2526
+ ok: false,
2527
+ error: error instanceof Error ? error.message : "Failed to rewind agent",
2528
+ },
2529
+ });
2530
+ }
2531
+ }
2532
+ async buildAgentSessionConfig(config, gitOptions, legacyWorktreeName, firstAgentContext) {
2533
+ return buildWorktreeAgentSessionConfig({
2534
+ ottoHome: this.ottoHome,
2535
+ worktreesRoot: this.worktreesRoot,
2536
+ sessionLogger: this.sessionLogger,
2537
+ workspaceGitService: this.workspaceGitService,
2538
+ createOttoWorktree: (input, serviceOptions) => this.createOttoWorktreeWorkflow(input, {
2539
+ ...serviceOptions,
2540
+ setupContinuation: {
2541
+ kind: "agent",
2542
+ terminalManager: this.terminalManager,
2543
+ appendTimelineItem: ({ agentId, item }) => appendTimelineItemIfAgentKnown({
2544
+ agentManager: this.agentManager,
2545
+ agentId,
2546
+ item,
2547
+ }),
2548
+ emitLiveTimelineItem: ({ agentId, item }) => emitLiveTimelineItemIfAgentKnown({
2549
+ agentManager: this.agentManager,
2550
+ agentId,
2551
+ item,
2552
+ }),
2553
+ logger: this.sessionLogger,
2554
+ },
2555
+ }),
2556
+ checkoutExistingBranch: (cwd, branch) => this.gitMutation.checkoutExistingBranch(cwd, branch),
2557
+ createBranchFromBase: (params) => this.gitMutation.createBranchFromBase(params),
2558
+ github: this.github,
2559
+ }, config, gitOptions, legacyWorktreeName, firstAgentContext);
2560
+ }
2561
+ isPathWithinRoot(rootPath, candidatePath) {
2562
+ const resolvedRoot = resolve(rootPath);
2563
+ const resolvedCandidate = resolve(candidatePath);
2564
+ if (resolvedCandidate === resolvedRoot) {
2565
+ return true;
2566
+ }
2567
+ return resolvedCandidate.startsWith(resolvedRoot + sep);
2568
+ }
2569
+ /**
2570
+ * Handle clearing agent attention flag
2571
+ */
2572
+ async handleClearAgentAttention(agentId, requestId) {
2573
+ const agentIds = Array.isArray(agentId) ? agentId : [agentId];
2574
+ try {
2575
+ await Promise.all(agentIds.map((id) => this.agentManager.clearAgentAttention(id)));
2576
+ if (requestId) {
2577
+ const agents = (await Promise.all(agentIds.map(async (id) => {
2578
+ const agent = this.agentManager.getAgent(id);
2579
+ return agent ? this.buildAgentPayload(agent) : null;
2580
+ }))).filter((payload) => payload !== null);
2581
+ this.emit({
2582
+ type: "clear_agent_attention_response",
2583
+ payload: {
2584
+ requestId,
2585
+ agentId,
2586
+ agents,
2587
+ },
2588
+ });
2589
+ }
2590
+ }
2591
+ catch (error) {
2592
+ this.sessionLogger.error({ err: error, agentIds }, "Failed to clear agent attention");
2593
+ // Don't throw - this is not critical
2594
+ }
2595
+ }
2596
+ /**
2597
+ * Handle client heartbeat for activity tracking
2598
+ */
2599
+ handleClientHeartbeat(msg) {
2600
+ const focusedTerminalId = msg.focusedTerminalId?.trim() || null;
2601
+ const appVisibilityChangedAt = msg.appVisibilityChangedAt
2602
+ ? new Date(msg.appVisibilityChangedAt)
2603
+ : new Date(msg.lastActivityAt);
2604
+ this.clientActivity = {
2605
+ deviceType: msg.deviceType,
2606
+ focusedAgentId: msg.focusedAgentId,
2607
+ focusedTerminalId,
2608
+ lastActivityAt: new Date(msg.lastActivityAt),
2609
+ appVisible: msg.appVisible,
2610
+ appVisibilityChangedAt,
2611
+ };
2612
+ if (msg.appVisible && focusedTerminalId) {
2613
+ void this.clearFocusedTerminalAttention(focusedTerminalId);
2614
+ }
2615
+ }
2616
+ async clearFocusedTerminalAttention(terminalId) {
2617
+ const terminalManager = this.terminalManager;
2618
+ if (!terminalManager) {
2619
+ return;
2620
+ }
2621
+ try {
2622
+ await terminalManager.clearTerminalAttention(terminalId);
2623
+ }
2624
+ catch (error) {
2625
+ this.sessionLogger.warn({ err: error, terminalId }, "Failed to clear terminal attention");
2626
+ }
2627
+ }
2628
+ /**
2629
+ * Handle push token registration
2630
+ */
2631
+ /**
2632
+ * Per-category context window breakdown for the client's context popup.
2633
+ * `usage: null` (not an error) when the agent is gone, its provider doesn't
2634
+ * implement the read, or the provider has no live handle to report from.
2635
+ */
2636
+ async handleAgentContextGetUsageRequest(msg) {
2637
+ const { agentId, requestId } = msg;
2638
+ try {
2639
+ const agent = this.agentManager.listAgents().find((a) => a.id === agentId);
2640
+ const usage = agent?.session?.getContextUsage ? await agent.session.getContextUsage() : null;
2641
+ this.emit({
2642
+ type: "agent.context.get_usage.response",
2643
+ payload: { requestId, agentId, usage },
2644
+ });
2645
+ }
2646
+ catch (error) {
2647
+ const err = error instanceof Error ? error : new Error(String(error));
2648
+ this.sessionLogger.error({ err, agentId }, "Failed to get agent context usage");
2649
+ this.emit({
2650
+ type: "rpc_error",
2651
+ payload: {
2652
+ requestId,
2653
+ requestType: msg.type,
2654
+ error: `Failed to get agent context usage: ${err.message}`,
2655
+ code: "agent_context_get_usage_failed",
2656
+ },
2657
+ });
2658
+ }
2659
+ }
2660
+ handleRegisterPushToken(token) {
2661
+ this.pushTokenStore.addToken(token);
2662
+ this.sessionLogger.info("Registered push token");
2663
+ }
2664
+ /**
2665
+ * Handle list commands request for an agent
2666
+ */
2667
+ async handleListCommandsRequest(msg) {
2668
+ const { agentId, requestId, draftConfig } = msg;
2669
+ this.sessionLogger.debug({ agentId, draftConfig }, `Handling list commands request for agent ${agentId}`);
2670
+ try {
2671
+ const agents = this.agentManager.listAgents();
2672
+ const agent = agents.find((a) => a.id === agentId);
2673
+ if (agent?.session?.listCommands) {
2674
+ const commands = await agent.session.listCommands();
2675
+ this.emit({
2676
+ type: "list_commands_response",
2677
+ payload: {
2678
+ agentId,
2679
+ commands,
2680
+ error: null,
2681
+ requestId,
2682
+ },
2683
+ });
2684
+ return;
2685
+ }
2686
+ if (!agent && draftConfig) {
2687
+ const sessionConfig = {
2688
+ provider: draftConfig.provider,
2689
+ cwd: expandTilde(draftConfig.cwd),
2690
+ ...(draftConfig.modeId ? { modeId: draftConfig.modeId } : {}),
2691
+ ...(draftConfig.model ? { model: draftConfig.model } : {}),
2692
+ ...(draftConfig.thinkingOptionId
2693
+ ? { thinkingOptionId: draftConfig.thinkingOptionId }
2694
+ : {}),
2695
+ };
2696
+ const commands = await this.agentManager.listDraftCommands(sessionConfig);
2697
+ this.emit({
2698
+ type: "list_commands_response",
2699
+ payload: {
2700
+ agentId,
2701
+ commands,
2702
+ error: null,
2703
+ requestId,
2704
+ },
2705
+ });
2706
+ return;
2707
+ }
2708
+ this.emit({
2709
+ type: "list_commands_response",
2710
+ payload: {
2711
+ agentId,
2712
+ commands: [],
2713
+ error: agent ? `Agent does not support listing commands` : `Agent not found: ${agentId}`,
2714
+ requestId,
2715
+ },
2716
+ });
2717
+ }
2718
+ catch (error) {
2719
+ this.sessionLogger.error({ err: error, agentId, draftConfig }, "Failed to list commands");
2720
+ this.emit({
2721
+ type: "list_commands_response",
2722
+ payload: {
2723
+ agentId,
2724
+ commands: [],
2725
+ error: getErrorMessage(error),
2726
+ requestId,
2727
+ },
2728
+ });
2729
+ }
2730
+ }
2731
+ /**
2732
+ * Handle agent permission response from user
2733
+ */
2734
+ async handleAgentPermissionResponse(agentId, requestId, response) {
2735
+ try {
2736
+ await respondToAgentPermission({
2737
+ agentManager: this.agentManager,
2738
+ agentId,
2739
+ requestId,
2740
+ response,
2741
+ logger: this.sessionLogger,
2742
+ });
2743
+ }
2744
+ catch (error) {
2745
+ this.sessionLogger.error({ err: error, agentId, requestId }, "Failed to respond to permission");
2746
+ this.emit({
2747
+ type: "activity_log",
2748
+ payload: {
2749
+ id: uuidv4(),
2750
+ timestamp: new Date(),
2751
+ type: "error",
2752
+ content: `Failed to respond to permission: ${getErrorMessage(error)}`,
2753
+ },
2754
+ });
2755
+ throw error;
2756
+ }
2757
+ }
2758
+ async handleDirectorySuggestionsRequest(msg) {
2759
+ const { query, limit, requestId, cwd, includeFiles, includeDirectories, matchMode } = msg;
2760
+ try {
2761
+ const workspaceCwd = cwd?.trim();
2762
+ const entries = workspaceCwd
2763
+ ? await searchWorkspaceEntries({
2764
+ cwd: expandTilde(workspaceCwd),
2765
+ query,
2766
+ limit,
2767
+ includeFiles,
2768
+ includeDirectories,
2769
+ matchMode,
2770
+ })
2771
+ : (await searchHomeDirectories({
2772
+ homeDir: process.env.HOME ?? homedir(),
2773
+ query,
2774
+ limit,
2775
+ })).map((path) => ({ path, kind: "directory" }));
2776
+ const directories = entries
2777
+ .filter((entry) => entry.kind === "directory")
2778
+ .map((entry) => entry.path);
2779
+ this.emit({
2780
+ type: "directory_suggestions_response",
2781
+ payload: {
2782
+ directories,
2783
+ entries,
2784
+ error: null,
2785
+ requestId,
2786
+ },
2787
+ });
2788
+ }
2789
+ catch (error) {
2790
+ this.emit({
2791
+ type: "directory_suggestions_response",
2792
+ payload: {
2793
+ directories: [],
2794
+ entries: [],
2795
+ error: error instanceof Error ? error.message : String(error),
2796
+ requestId,
2797
+ },
2798
+ });
2799
+ }
2800
+ }
2801
+ async handleOttoWorktreeListRequest(msg) {
2802
+ return handleWorktreeListRequest({
2803
+ emit: (message) => this.emit(message),
2804
+ ottoHome: this.ottoHome,
2805
+ workspaceGitService: this.workspaceGitService,
2806
+ }, msg);
2807
+ }
2808
+ async handleOttoWorktreeArchiveRequest(msg) {
2809
+ return handleWorktreeArchiveRequest({
2810
+ ottoHome: this.ottoHome,
2811
+ ottoWorktreesBaseRoot: this.worktreesRoot,
2812
+ github: this.github,
2813
+ workspaceGitService: this.workspaceGitService,
2814
+ agentManager: this.agentManager,
2815
+ agentStorage: this.agentStorage,
2816
+ findWorkspaceIdForCwd: (cwd) => this.findWorkspaceIdForCwd(cwd),
2817
+ listActiveWorkspaces: () => this.listActiveWorkspaceRefs(),
2818
+ archiveWorkspaceRecord: (workspaceId) => this.archiveWorkspaceRecord(workspaceId),
2819
+ emit: (message) => this.emit(message),
2820
+ emitWorkspaceUpdatesForWorkspaceIds: (workspaceIds) => this.emitWorkspaceUpdatesForWorkspaceIds(workspaceIds),
2821
+ markWorkspaceArchiving: (workspaceIds, archivingAt) => this.markWorkspaceArchiving(workspaceIds, archivingAt),
2822
+ clearWorkspaceArchiving: (workspaceIds) => this.clearWorkspaceArchiving(workspaceIds),
2823
+ killTerminalsForWorkspace: (workspaceId) => this.terminalController.killTerminalsForWorkspace(workspaceId),
2824
+ sessionLogger: this.sessionLogger,
2825
+ }, msg);
2826
+ }
2827
+ async listTerminalActivityContributions() {
2828
+ const terminalManager = this.terminalManager;
2829
+ if (!terminalManager) {
2830
+ return [];
2831
+ }
2832
+ const directories = terminalManager.listDirectories();
2833
+ const terminalsByDirectory = await Promise.all(directories.map((cwd) => terminalManager.getTerminals(cwd)));
2834
+ return terminalsByDirectory.flat().map((session) => {
2835
+ const contribution = {
2836
+ cwd: session.cwd,
2837
+ activity: session.getActivity(),
2838
+ };
2839
+ if (session.workspaceId) {
2840
+ contribution.workspaceId = session.workspaceId;
2841
+ }
2842
+ return contribution;
2843
+ });
2844
+ }
2845
+ /**
2846
+ * Build the current agent list payload (live + persisted), optionally filtered by labels.
2847
+ */
2848
+ async listAgentPayloads(filter) {
2849
+ const includeArchived = filter?.includeArchived === true;
2850
+ const labelEntries = filter?.labels ? Object.entries(filter.labels) : [];
2851
+ // Get live agents with session modes
2852
+ const agentSnapshots = this.agentManager.listAgents();
2853
+ const liveAgents = await Promise.all(agentSnapshots.map((agent) => this.buildAgentPayload(agent)));
2854
+ // Add persisted agents that have not been lazily initialized yet
2855
+ // (excluding internal agents which are for ephemeral system tasks)
2856
+ const registryRecords = await this.agentStorage.list();
2857
+ const liveIds = new Set(agentSnapshots.map((a) => a.id));
2858
+ const registeredProviderIds = new Set(this.providerSnapshotManager.listRegisteredProviderIds());
2859
+ const persistedAgents = registryRecords
2860
+ .filter((record) => !liveIds.has(record.id) && !record.internal)
2861
+ // Keep raw-record filters ahead of projection; seeded homes can carry thousands of archived agents.
2862
+ .filter((record) => includeArchived || !record.archivedAt)
2863
+ .filter((record) => labelEntries.every(([key, value]) => record.labels?.[key] === value))
2864
+ .filter((record) => filter?.includeUnavailablePersisted === true ||
2865
+ isStoredAgentProviderAvailable(record, registeredProviderIds))
2866
+ .map((record) => this.buildStoredAgentPayload(record, registeredProviderIds));
2867
+ let agents = [...liveAgents, ...persistedAgents];
2868
+ agents = agents.filter((agent) => this.isProviderVisibleToClient(agent.provider));
2869
+ if (!includeArchived) {
2870
+ agents = agents.filter((agent) => !agent.archivedAt);
2871
+ }
2872
+ // Filter by labels if filter provided
2873
+ if (labelEntries.length > 0) {
2874
+ agents = agents.filter((agent) => labelEntries.every(([key, value]) => agent.labels[key] === value));
2875
+ }
2876
+ return agents;
2877
+ }
2878
+ async resolveAgentIdentifier(identifier) {
2879
+ const trimmed = identifier.trim();
2880
+ if (!trimmed) {
2881
+ return { ok: false, error: "Agent identifier cannot be empty" };
2882
+ }
2883
+ // Exact live-agent lookup bypasses the internal-agent exclusion below.
2884
+ // Internal agents (e.g. artifact generation) are deliberately hidden from
2885
+ // the fuzzy prefix/title resolution and general listings further down,
2886
+ // but a caller that already has the literal agentId (not a prefix or
2887
+ // title guess) should still be able to resolve it while it's alive.
2888
+ if (this.agentManager.getAgent(trimmed)) {
2889
+ return { ok: true, agentId: trimmed };
2890
+ }
2891
+ const stored = await this.agentStorage.list();
2892
+ const storedRecords = stored.filter((record) => !record.internal);
2893
+ const knownIds = new Set();
2894
+ for (const record of storedRecords) {
2895
+ knownIds.add(record.id);
2896
+ }
2897
+ for (const agent of this.agentManager.listAgents()) {
2898
+ knownIds.add(agent.id);
2899
+ }
2900
+ if (knownIds.has(trimmed)) {
2901
+ return { ok: true, agentId: trimmed };
2902
+ }
2903
+ const prefixMatches = Array.from(knownIds).filter((id) => id.startsWith(trimmed));
2904
+ if (prefixMatches.length === 1) {
2905
+ return { ok: true, agentId: prefixMatches[0] };
2906
+ }
2907
+ if (prefixMatches.length > 1) {
2908
+ return {
2909
+ ok: false,
2910
+ error: `Agent identifier "${trimmed}" is ambiguous (${prefixMatches
2911
+ .slice(0, 5)
2912
+ .map((id) => id.slice(0, 8))
2913
+ .join(", ")}${prefixMatches.length > 5 ? ", …" : ""})`,
2914
+ };
2915
+ }
2916
+ const titleMatches = storedRecords.filter((record) => record.title === trimmed);
2917
+ if (titleMatches.length === 1) {
2918
+ return { ok: true, agentId: titleMatches[0].id };
2919
+ }
2920
+ if (titleMatches.length > 1) {
2921
+ return {
2922
+ ok: false,
2923
+ error: `Agent title "${trimmed}" is ambiguous (${titleMatches
2924
+ .slice(0, 5)
2925
+ .map((r) => r.id.slice(0, 8))
2926
+ .join(", ")}${titleMatches.length > 5 ? ", …" : ""})`,
2927
+ };
2928
+ }
2929
+ return { ok: false, error: `Agent not found: ${trimmed}` };
2930
+ }
2931
+ async getAgentPayloadById(agentId) {
2932
+ const live = this.agentManager.getAgent(agentId);
2933
+ if (live) {
2934
+ const payload = await this.buildAgentPayload(live);
2935
+ return this.isProviderVisibleToClient(payload.provider) ? payload : null;
2936
+ }
2937
+ const record = await this.agentStorage.get(agentId);
2938
+ if (!record || record.internal) {
2939
+ return null;
2940
+ }
2941
+ const payload = this.buildStoredAgentPayload(record);
2942
+ return this.isProviderVisibleToClient(payload.provider) ? payload : null;
2943
+ }
2944
+ async resolveDelegationRootWorkspaceId(agentId) {
2945
+ const seen = new Set();
2946
+ let currentAgentId = agentId;
2947
+ while (true) {
2948
+ if (seen.has(currentAgentId)) {
2949
+ return null;
2950
+ }
2951
+ seen.add(currentAgentId);
2952
+ const live = this.agentManager.getAgent(currentAgentId);
2953
+ const source = live ?? (await this.agentStorage.get(currentAgentId));
2954
+ if (!source) {
2955
+ return null;
2956
+ }
2957
+ if ("archivedAt" in source && source.archivedAt) {
2958
+ return null;
2959
+ }
2960
+ const parentAgentId = getParentAgentIdFromLabels(source.labels);
2961
+ if (!parentAgentId) {
2962
+ return source.workspaceId ?? null;
2963
+ }
2964
+ currentAgentId = parentAgentId;
2965
+ }
2966
+ }
2967
+ async buildActiveProjectPlacementsByWorkspaceId() {
2968
+ const [persistedWorkspaces, persistedProjects] = await Promise.all([
2969
+ this.workspaceRegistry.list(),
2970
+ this.projectRegistry.list(),
2971
+ ]);
2972
+ const activeProjects = new Map(persistedProjects
2973
+ .filter((project) => !project.archivedAt)
2974
+ .map((project) => [project.projectId, project]));
2975
+ const placementsByWorkspaceId = new Map();
2976
+ const pairs = persistedWorkspaces.flatMap((workspace) => {
2977
+ if (workspace.archivedAt)
2978
+ return [];
2979
+ const project = activeProjects.get(workspace.projectId);
2980
+ if (!project)
2981
+ return [];
2982
+ return [{ workspace, project }];
2983
+ });
2984
+ const placements = await Promise.all(pairs.map(({ workspace, project }) => this.buildProjectPlacementForWorkspace(workspace, project)));
2985
+ for (let i = 0; i < pairs.length; i += 1) {
2986
+ placementsByWorkspaceId.set(pairs[i].workspace.workspaceId, placements[i]);
2987
+ }
2988
+ return placementsByWorkspaceId;
2989
+ }
2990
+ async collectFetchAgentsEntries(params) {
2991
+ const { candidates, limit, getPlacement, filter } = params;
2992
+ const matchedEntries = [];
2993
+ const batchSize = 25;
2994
+ for (let start = 0; start < candidates.length && matchedEntries.length <= limit; start += batchSize) {
2995
+ const batch = candidates.slice(start, start + batchSize);
2996
+ const batchEntries = await Promise.all(batch.map(async (agent) => {
2997
+ const project = await getPlacement(agent.workspaceId);
2998
+ return project ? { agent, project } : null;
2999
+ }));
3000
+ for (const entry of batchEntries) {
3001
+ if (!entry) {
3002
+ continue;
3003
+ }
3004
+ if (!matchesAgentUpdatesFilter({
3005
+ agent: entry.agent,
3006
+ project: entry.project,
3007
+ filter,
3008
+ })) {
3009
+ continue;
3010
+ }
3011
+ matchedEntries.push(entry);
3012
+ if (matchedEntries.length > limit) {
3013
+ break;
3014
+ }
3015
+ }
3016
+ }
3017
+ return matchedEntries;
3018
+ }
3019
+ async listFetchAgentsEntries(request) {
3020
+ const filter = request.type === "fetch_agent_history_request" &&
3021
+ request.filter?.includeArchived === undefined
3022
+ ? { ...request.filter, includeArchived: true }
3023
+ : request.filter;
3024
+ const scope = request.type === "fetch_agents_request" ? request.scope : undefined;
3025
+ const sort = this.agentsPager.normalizeSort(request.sort);
3026
+ let agents = await this.listAgentPayloads({
3027
+ labels: filter?.labels,
3028
+ includeArchived: filter?.includeArchived,
3029
+ includeUnavailablePersisted: request.type === "fetch_agent_history_request",
3030
+ });
3031
+ const activePlacementsByWorkspaceId = scope === "active" ? await this.buildActiveProjectPlacementsByWorkspaceId() : null;
3032
+ if (activePlacementsByWorkspaceId) {
3033
+ agents = agents.filter((agent) => !agent.archivedAt &&
3034
+ agent.workspaceId != null &&
3035
+ activePlacementsByWorkspaceId.has(agent.workspaceId));
3036
+ }
3037
+ const placementByWorkspaceId = new Map();
3038
+ const getPlacement = (workspaceId) => {
3039
+ if (!workspaceId) {
3040
+ return Promise.resolve(null);
3041
+ }
3042
+ if (activePlacementsByWorkspaceId) {
3043
+ return Promise.resolve(activePlacementsByWorkspaceId.get(workspaceId) ?? null);
3044
+ }
3045
+ const existing = placementByWorkspaceId.get(workspaceId);
3046
+ if (existing) {
3047
+ return existing;
3048
+ }
3049
+ const placementPromise = this.buildProjectPlacementForWorkspaceId(workspaceId);
3050
+ placementByWorkspaceId.set(workspaceId, placementPromise);
3051
+ return placementPromise;
3052
+ };
3053
+ let candidates = [...agents];
3054
+ candidates.sort((left, right) => this.agentsPager.compare(left, right, sort));
3055
+ const cursorToken = request.page?.cursor;
3056
+ if (cursorToken) {
3057
+ const cursor = this.decodeAgentCursor(cursorToken, sort);
3058
+ candidates = candidates.filter((agent) => this.agentsPager.compareWithCursor(agent, cursor, sort) > 0);
3059
+ }
3060
+ const limit = request.page?.limit ?? 200;
3061
+ const matchedEntries = await this.collectFetchAgentsEntries({
3062
+ candidates,
3063
+ limit,
3064
+ getPlacement,
3065
+ filter,
3066
+ });
3067
+ const pagedEntries = matchedEntries.slice(0, limit);
3068
+ const hasMore = matchedEntries.length > limit;
3069
+ const nextCursor = hasMore && pagedEntries.length > 0
3070
+ ? this.agentsPager.encode(pagedEntries[pagedEntries.length - 1].agent, sort)
3071
+ : null;
3072
+ return {
3073
+ entries: pagedEntries,
3074
+ pageInfo: {
3075
+ nextCursor,
3076
+ prevCursor: request.page?.cursor ?? null,
3077
+ hasMore,
3078
+ },
3079
+ };
3080
+ }
3081
+ decodeAgentCursor(token, sort) {
3082
+ try {
3083
+ return this.agentsPager.decode(token, sort);
3084
+ }
3085
+ catch (error) {
3086
+ if (error instanceof CursorError) {
3087
+ throw new SessionRequestError("invalid_cursor", error.message);
3088
+ }
3089
+ throw error;
3090
+ }
3091
+ }
3092
+ async describeWorkspaceRecord(workspace, projectRecord) {
3093
+ const resolvedProjectRecord = projectRecord ?? (await this.projectRegistry.get(workspace.projectId));
3094
+ let diffStat = null;
3095
+ const snapshot = this.workspaceGitService.peekSnapshot(workspace.cwd);
3096
+ if (snapshot?.git.diffStat) {
3097
+ diffStat = snapshot.git.diffStat;
3098
+ }
3099
+ return {
3100
+ id: workspace.workspaceId,
3101
+ projectId: workspace.projectId,
3102
+ projectDisplayName: resolvedProjectRecord
3103
+ ? resolveProjectDisplayName(resolvedProjectRecord)
3104
+ : workspace.projectId,
3105
+ projectCustomName: resolvedProjectRecord?.customName ?? null,
3106
+ projectRootPath: resolvedProjectRecord?.rootPath ?? workspace.cwd,
3107
+ workspaceDirectory: workspace.cwd,
3108
+ projectKind: (resolvedProjectRecord?.kind ?? "directory") === "git" ? "git" : "non_git",
3109
+ workspaceKind: workspace.kind,
3110
+ name: resolveWorkspaceDisplayName(workspace),
3111
+ title: workspace.title,
3112
+ archivingAt: null,
3113
+ status: "done",
3114
+ statusEnteredAt: null,
3115
+ activityAt: null,
3116
+ diffStat,
3117
+ scripts: this.buildWorkspaceScriptPayloadSnapshot(workspace.workspaceId, workspace.cwd),
3118
+ ...(resolvedProjectRecord
3119
+ ? {
3120
+ project: await this.buildProjectPlacementForWorkspace(workspace, resolvedProjectRecord),
3121
+ }
3122
+ : {}),
3123
+ };
3124
+ }
3125
+ buildWorkspaceGitRuntimePayload(snapshot) {
3126
+ if (!snapshot.git.isGit) {
3127
+ return null;
3128
+ }
3129
+ return {
3130
+ currentBranch: snapshot.git.currentBranch,
3131
+ remoteUrl: snapshot.git.remoteUrl,
3132
+ isOttoOwnedWorktree: snapshot.git.isOttoOwnedWorktree,
3133
+ isDirty: snapshot.git.isDirty,
3134
+ aheadBehind: snapshot.git.aheadBehind,
3135
+ aheadOfOrigin: snapshot.git.aheadOfOrigin,
3136
+ behindOfOrigin: snapshot.git.behindOfOrigin,
3137
+ };
3138
+ }
3139
+ buildWorkspaceGitHubRuntimePayload(snapshot) {
3140
+ return {
3141
+ featuresEnabled: snapshot.github.featuresEnabled,
3142
+ pullRequest: snapshot.github.pullRequest,
3143
+ error: snapshot.github.error,
3144
+ };
3145
+ }
3146
+ async describeWorkspaceRecordWithGitData(workspace, projectRecord) {
3147
+ const base = await this.describeWorkspaceRecord(workspace, projectRecord);
3148
+ const snapshot = this.workspaceGitService.peekSnapshot(workspace.cwd);
3149
+ if (!snapshot) {
3150
+ return base;
3151
+ }
3152
+ const checkout = checkoutLiteFromGitSnapshot(workspace.cwd, snapshot.git);
3153
+ const displayName = deriveWorkspaceDisplayName({ cwd: workspace.cwd, checkout });
3154
+ return {
3155
+ ...base,
3156
+ name: resolveWorkspaceName({ title: workspace.title, derivedDisplayName: displayName }),
3157
+ diffStat: snapshot.git.diffStat ?? null,
3158
+ gitRuntime: this.buildWorkspaceGitRuntimePayload(snapshot) ?? undefined,
3159
+ githubRuntime: this.buildWorkspaceGitHubRuntimePayload(snapshot),
3160
+ };
3161
+ }
3162
+ async describeCreatedWorktreeWorkspace(result) {
3163
+ const projectRecord = await this.projectRegistry.get(result.workspace.projectId);
3164
+ return {
3165
+ id: result.workspace.workspaceId,
3166
+ projectId: result.workspace.projectId,
3167
+ projectDisplayName: projectRecord
3168
+ ? resolveProjectDisplayName(projectRecord)
3169
+ : result.workspace.projectId,
3170
+ projectCustomName: projectRecord?.customName ?? null,
3171
+ projectRootPath: projectRecord?.rootPath ?? result.repoRoot,
3172
+ workspaceDirectory: result.workspace.cwd,
3173
+ projectKind: "git",
3174
+ workspaceKind: result.workspace.kind,
3175
+ name: resolveWorkspaceName({
3176
+ title: result.workspace.title,
3177
+ derivedDisplayName: result.worktree.branchName || result.workspace.displayName,
3178
+ }),
3179
+ title: result.workspace.title,
3180
+ archivingAt: null,
3181
+ status: "done",
3182
+ statusEnteredAt: result.workspace.createdAt,
3183
+ activityAt: null,
3184
+ diffStat: { additions: 0, deletions: 0 },
3185
+ scripts: [],
3186
+ gitRuntime: {
3187
+ currentBranch: result.worktree.branchName || null,
3188
+ remoteUrl: null,
3189
+ isOttoOwnedWorktree: true,
3190
+ isDirty: false,
3191
+ aheadBehind: null,
3192
+ aheadOfOrigin: null,
3193
+ behindOfOrigin: null,
3194
+ },
3195
+ githubRuntime: null,
3196
+ };
3197
+ }
3198
+ async buildWorkspaceDescriptor(input) {
3199
+ if (input.includeGitData && input.projectRecord?.kind === "git") {
3200
+ return this.describeWorkspaceRecordWithGitData(input.workspace, input.projectRecord);
3201
+ }
3202
+ return this.describeWorkspaceRecord(input.workspace, input.projectRecord);
3203
+ }
3204
+ markWorkspaceArchiving(workspaceIds, archivingAt) {
3205
+ this.workspaceDirectory.markArchiving(workspaceIds, archivingAt);
3206
+ }
3207
+ clearWorkspaceArchiving(workspaceIds) {
3208
+ this.workspaceDirectory.clearArchiving(workspaceIds);
3209
+ }
3210
+ async buildWorkspaceDescriptorMap(options) {
3211
+ return this.workspaceDirectory.buildDescriptorMap(options);
3212
+ }
3213
+ // external path→workspace adapter, not ownership. Used by archive-by-path flows
3214
+ // where the request carries a worktree path (unique to one workspace) rather
3215
+ // than a workspaceId. This is a directory lookup for an archive target, not a
3216
+ // status/ownership attribution.
3217
+ async findWorkspaceIdForCwd(cwd) {
3218
+ const workspaces = await this.workspaceRegistry.list();
3219
+ return resolveWorkspaceIdForPath(cwd, workspaces);
3220
+ }
3221
+ matchesWorkspaceFilter(input) {
3222
+ return this.workspaceDirectory.matchesFilter(input);
3223
+ }
3224
+ async listFetchWorkspacesEntries(request) {
3225
+ try {
3226
+ return await this.workspaceDirectory.listFetchEntries(request);
3227
+ }
3228
+ catch (error) {
3229
+ if (error instanceof CursorError) {
3230
+ throw new SessionRequestError("invalid_cursor", error.message);
3231
+ }
3232
+ throw error;
3233
+ }
3234
+ }
3235
+ bufferOrEmitWorkspaceUpdate(subscription, payload) {
3236
+ if (subscription.isBootstrapping) {
3237
+ const workspaceId = payload.kind === "upsert" ? payload.workspace.id : payload.id;
3238
+ subscription.pendingUpdatesByWorkspaceId.set(workspaceId, payload);
3239
+ return;
3240
+ }
3241
+ const workspaceId = payload.kind === "upsert" ? payload.workspace.id : payload.id;
3242
+ subscription.lastEmittedByWorkspaceId.set(workspaceId, payload);
3243
+ this.emit({
3244
+ type: "workspace_update",
3245
+ payload,
3246
+ });
3247
+ }
3248
+ flushBootstrappedWorkspaceUpdates(options) {
3249
+ const subscription = this.workspaceUpdatesSubscription;
3250
+ if (!subscription || !subscription.isBootstrapping) {
3251
+ return;
3252
+ }
3253
+ subscription.isBootstrapping = false;
3254
+ const pending = Array.from(subscription.pendingUpdatesByWorkspaceId.values());
3255
+ subscription.pendingUpdatesByWorkspaceId.clear();
3256
+ for (const payload of pending) {
3257
+ if (payload.kind === "upsert") {
3258
+ const snapshot = options?.snapshotByWorkspaceId?.get(payload.workspace.id);
3259
+ const updateActivityAtMs = payload.workspace.activityAt
3260
+ ? Date.parse(payload.workspace.activityAt)
3261
+ : null;
3262
+ const shouldEmit = shouldEmitPendingBootstrapUpdate({
3263
+ snapshot: snapshot
3264
+ ? {
3265
+ status: snapshot.status,
3266
+ statusEnteredAt: snapshot.statusEnteredAt,
3267
+ activityAtMs: snapshot.activityAtMs,
3268
+ }
3269
+ : null,
3270
+ update: {
3271
+ status: payload.workspace.status,
3272
+ statusEnteredAt: payload.workspace.statusEnteredAt ?? null,
3273
+ activityAtMs: Number.isNaN(updateActivityAtMs) ? null : updateActivityAtMs,
3274
+ },
3275
+ });
3276
+ if (!shouldEmit) {
3277
+ continue;
3278
+ }
3279
+ }
3280
+ this.emit({
3281
+ type: "workspace_update",
3282
+ payload,
3283
+ });
3284
+ }
3285
+ }
3286
+ buildProjectDescriptor(project) {
3287
+ return {
3288
+ projectId: project.projectId,
3289
+ projectDisplayName: resolveProjectDisplayName(project),
3290
+ projectCustomName: project.customName ?? null,
3291
+ projectRootPath: project.rootPath,
3292
+ projectKind: project.kind,
3293
+ };
3294
+ }
3295
+ async unarchiveOwningWorkspaceForAgent(agentId) {
3296
+ const record = await this.agentStorage.get(agentId);
3297
+ if (!record?.workspaceId) {
3298
+ return;
3299
+ }
3300
+ const workspace = await this.workspaceRegistry.get(record.workspaceId);
3301
+ if (!workspace?.archivedAt) {
3302
+ return;
3303
+ }
3304
+ const directoryExists = await this.filesystem.isDirectory(record.cwd).catch(() => false);
3305
+ if (!directoryExists) {
3306
+ if (workspace.kind !== "worktree" || !workspace.branch) {
3307
+ return;
3308
+ }
3309
+ // Recreate the worktree directory from its kept branch BEFORE clearing
3310
+ // archivedAt — the reconciler re-archives workspaces whose directory is
3311
+ // missing, so the record must point at a real directory first.
3312
+ await this.recreateOwningWorktreeForRestore(workspace, workspace.branch);
3313
+ }
3314
+ await this.workspaceProvisioning.ensureWorkspaceRecordUnarchived(workspace);
3315
+ await this.emitWorkspaceUpdatesForWorkspaceIds([workspace.workspaceId]);
3316
+ }
3317
+ async recreateOwningWorktreeForRestore(workspace, branch) {
3318
+ const project = await this.projectRegistry.get(workspace.projectId);
3319
+ if (!project) {
3320
+ throw new WorktreeRequestError({
3321
+ code: "unknown",
3322
+ message: `Project ${workspace.projectId} not found for workspace ${workspace.workspaceId}`,
3323
+ });
3324
+ }
3325
+ const projectRootExists = await this.filesystem
3326
+ .isDirectory(project.rootPath)
3327
+ .catch(() => false);
3328
+ if (!projectRootExists) {
3329
+ throw new WorktreeRequestError({
3330
+ code: "unknown",
3331
+ message: `Project root is missing for ${workspace.projectId}: ${project.rootPath}`,
3332
+ });
3333
+ }
3334
+ // Archiving through the default path (scope "workspace", worktreePath only)
3335
+ // resolves repoRoot=null, so deleteOttoWorktree's `git worktree remove`/
3336
+ // `prune` is skipped and the admin registration survives — pinning the
3337
+ // branch as "already checked out". Prune here frees any stale registration
3338
+ // whose working dir is missing (a no-op for live worktrees) so the recreate
3339
+ // below succeeds regardless of how the worktree was archived.
3340
+ try {
3341
+ await runGitCommand(["worktree", "prune"], { cwd: project.rootPath, timeout: 30000 });
3342
+ }
3343
+ catch {
3344
+ // not critical; git will prune lazily
3345
+ }
3346
+ let result;
3347
+ try {
3348
+ result = await createWorktree({
3349
+ cwd: project.rootPath,
3350
+ worktreeSlug: basename(workspace.cwd),
3351
+ source: { kind: "checkout-branch", branchName: branch },
3352
+ runSetup: false,
3353
+ ottoHome: this.ottoHome,
3354
+ worktreesRoot: this.worktreesRoot,
3355
+ });
3356
+ }
3357
+ catch (error) {
3358
+ throw toWorktreeRequestError(error);
3359
+ }
3360
+ if (normalize(result.worktreePath) !== normalize(workspace.cwd)) {
3361
+ throw new WorktreeRequestError({
3362
+ code: "unknown",
3363
+ message: `Recreated worktree diverged from ${workspace.cwd}: ${result.worktreePath}`,
3364
+ });
3365
+ }
3366
+ }
3367
+ async createOttoWorktree(input, options) {
3368
+ const result = await createOttoWorktree(input, {
3369
+ github: this.github,
3370
+ ...(options?.resolveDefaultBranch
3371
+ ? { resolveDefaultBranch: options.resolveDefaultBranch }
3372
+ : {}),
3373
+ projectRegistry: this.projectRegistry,
3374
+ workspaceRegistry: this.workspaceRegistry,
3375
+ workspaceGitService: this.workspaceGitService,
3376
+ });
3377
+ void Promise.all([
3378
+ this.gitMutation.notifyGitMutation(input.cwd, "create-worktree"),
3379
+ this.gitMutation.notifyGitMutation(result.worktree.worktreePath, "create-worktree"),
3380
+ ]).catch((error) => {
3381
+ this.sessionLogger.warn({ err: error, cwd: input.cwd, worktreePath: result.worktree.worktreePath }, "Failed to warm git snapshots after creating worktree");
3382
+ });
3383
+ return result;
3384
+ }
3385
+ async listActiveWorkspaceRefs() {
3386
+ const workspaces = await this.workspaceRegistry.list();
3387
+ return workspaces
3388
+ .filter((workspace) => !workspace.archivedAt)
3389
+ .map((workspace) => ({
3390
+ workspaceId: workspace.workspaceId,
3391
+ cwd: workspace.cwd,
3392
+ kind: workspace.kind,
3393
+ }));
3394
+ }
3395
+ async archiveWorkspaceRecord(workspaceId, archivedAt) {
3396
+ const archiveTimestamp = archivedAt ?? new Date().toISOString();
3397
+ const existingWorkspace = await archivePersistedWorkspaceRecord({
3398
+ workspaceId,
3399
+ archivedAt: archiveTimestamp,
3400
+ workspaceRegistry: this.workspaceRegistry,
3401
+ });
3402
+ if (!existingWorkspace) {
3403
+ this.workspaceGitObserver.removeForWorkspaceId(workspaceId);
3404
+ return;
3405
+ }
3406
+ if (!existingWorkspace.archivedAt) {
3407
+ const activeSiblings = (await this.workspaceRegistry.list()).filter((workspace) => workspace.projectId === existingWorkspace.projectId && !workspace.archivedAt);
3408
+ this.sessionLogger.info({
3409
+ workspaceId,
3410
+ workspaceCwd: existingWorkspace.cwd,
3411
+ projectId: existingWorkspace.projectId,
3412
+ projectArchived: activeSiblings.length === 0,
3413
+ archivedAt: archiveTimestamp,
3414
+ }, "Workspace archived");
3415
+ }
3416
+ await this.teardownArchivedWorkspace({
3417
+ workspaceId: existingWorkspace.workspaceId,
3418
+ cwd: existingWorkspace.cwd,
3419
+ });
3420
+ }
3421
+ // Git watch and subscription state is keyed by directory; the script runtime
3422
+ // store is keyed by the opaque workspace id. Each cleanup uses its own key so an
3423
+ // opaque id is never resolved as a filesystem path.
3424
+ async teardownArchivedWorkspace(input) {
3425
+ this.workspaceGitObserver.removeForCwd(input.cwd);
3426
+ this.scriptRuntimeStore?.removeForWorkspace(input.workspaceId);
3427
+ }
3428
+ async reconcileAndEmitWorkspaceUpdates() {
3429
+ if (!this.workspaceUpdatesSubscription) {
3430
+ return;
3431
+ }
3432
+ try {
3433
+ const changedWorkspaceIds = await this.reconcileActiveWorkspaceRecords();
3434
+ if (changedWorkspaceIds.size === 0) {
3435
+ return;
3436
+ }
3437
+ await this.emitWorkspaceUpdatesForWorkspaceIds(changedWorkspaceIds, {
3438
+ skipReconcile: true,
3439
+ });
3440
+ }
3441
+ catch (error) {
3442
+ this.sessionLogger.error({ err: error }, "Background workspace reconciliation failed");
3443
+ }
3444
+ }
3445
+ async reconcileActiveWorkspaceRecords() {
3446
+ const service = new WorkspaceReconciliationService({
3447
+ projectRegistry: this.projectRegistry,
3448
+ workspaceRegistry: this.workspaceRegistry,
3449
+ logger: this.sessionLogger,
3450
+ workspaceGitService: this.workspaceGitService,
3451
+ });
3452
+ const result = await service.runOnce();
3453
+ const changedWorkspaceIds = new Set();
3454
+ const changedProjectIds = new Set();
3455
+ await Promise.all(result.changesApplied.map(async (change) => {
3456
+ switch (change.kind) {
3457
+ case "workspace_archived":
3458
+ await this.teardownArchivedWorkspace({
3459
+ workspaceId: change.workspaceId,
3460
+ cwd: change.directory,
3461
+ });
3462
+ changedWorkspaceIds.add(change.workspaceId);
3463
+ break;
3464
+ case "workspace_updated":
3465
+ changedWorkspaceIds.add(change.workspaceId);
3466
+ break;
3467
+ case "project_archived":
3468
+ case "project_updated":
3469
+ changedProjectIds.add(change.projectId);
3470
+ break;
3471
+ }
3472
+ }));
3473
+ if (changedProjectIds.size > 0) {
3474
+ for (const workspace of await this.workspaceRegistry.list()) {
3475
+ if (changedProjectIds.has(workspace.projectId)) {
3476
+ changedWorkspaceIds.add(workspace.workspaceId);
3477
+ }
3478
+ }
3479
+ }
3480
+ return changedWorkspaceIds;
3481
+ }
3482
+ async emitWorkspaceUpdatesForWorkspaceIds(workspaceIds, options) {
3483
+ const subscription = this.workspaceUpdatesSubscription;
3484
+ if (!subscription) {
3485
+ return;
3486
+ }
3487
+ const uniqueWorkspaceIds = new Set(Array.from(workspaceIds));
3488
+ if (uniqueWorkspaceIds.size === 0) {
3489
+ return;
3490
+ }
3491
+ const descriptorsByWorkspaceId = await this.buildWorkspaceDescriptorMap({
3492
+ workspaceIds: uniqueWorkspaceIds,
3493
+ includeGitData: true,
3494
+ });
3495
+ for (const workspaceId of uniqueWorkspaceIds) {
3496
+ const workspace = descriptorsByWorkspaceId.get(workspaceId);
3497
+ const nextWorkspace = workspace && this.matchesWorkspaceFilter({ workspace, filter: subscription.filter })
3498
+ ? workspace
3499
+ : null;
3500
+ if (options?.dedupeGitState &&
3501
+ this.workspaceGitObserver.shouldSkipUpdate(workspaceId, nextWorkspace)) {
3502
+ continue;
3503
+ }
3504
+ this.workspaceGitObserver.recordDescriptorState(workspaceId, nextWorkspace);
3505
+ if (!nextWorkspace) {
3506
+ subscription.lastEmittedByWorkspaceId.delete(workspaceId);
3507
+ this.bufferOrEmitWorkspaceUpdate(subscription, await this.buildWorkspaceRemoveUpdatePayload(workspaceId, options?.removedProjectId));
3508
+ continue;
3509
+ }
3510
+ const nextPayload = {
3511
+ kind: "upsert",
3512
+ workspace: nextWorkspace,
3513
+ };
3514
+ const lastEmitted = subscription.lastEmittedByWorkspaceId.get(workspaceId);
3515
+ if (lastEmitted &&
3516
+ lastEmitted.kind === "upsert" &&
3517
+ equal(lastEmitted.workspace, nextWorkspace)) {
3518
+ continue;
3519
+ }
3520
+ this.bufferOrEmitWorkspaceUpdate(subscription, nextPayload);
3521
+ }
3522
+ if (!options?.skipReconcile) {
3523
+ void this.reconcileAndEmitWorkspaceUpdates();
3524
+ }
3525
+ }
3526
+ async buildWorkspaceRemoveUpdatePayload(workspaceId, removedProjectId) {
3527
+ if (removedProjectId) {
3528
+ return { kind: "remove", id: workspaceId, removedProjectId };
3529
+ }
3530
+ return {
3531
+ kind: "remove",
3532
+ id: workspaceId,
3533
+ ...(await this.resolveProjectWithoutActiveWorkspacesForArchivedWorkspace(workspaceId)),
3534
+ };
3535
+ }
3536
+ // When a workspace is archived its project may have no active workspaces left.
3537
+ // Resolve that project parent so the `remove` update can carry it, keeping the
3538
+ // sidebar in sync without a full re-hydration.
3539
+ async resolveProjectWithoutActiveWorkspacesForArchivedWorkspace(workspaceId) {
3540
+ const archivedWorkspace = await this.workspaceRegistry.get(workspaceId);
3541
+ if (!archivedWorkspace) {
3542
+ return null;
3543
+ }
3544
+ const projectWithoutActiveWorkspaces = (await this.workspaceDirectory.listEmptyProjects()).find((project) => project.projectId === archivedWorkspace.projectId);
3545
+ return projectWithoutActiveWorkspaces ? { emptyProject: projectWithoutActiveWorkspaces } : null;
3546
+ }
3547
+ async emitWorkspaceUpdateForTerminalContribution(event) {
3548
+ // A terminal's activity contributes only to the workspace it carries. A
3549
+ // terminal with no workspaceId attributes to nothing — status is per-id.
3550
+ if (!event.workspaceId) {
3551
+ return;
3552
+ }
3553
+ await this.emitWorkspaceUpdatesForWorkspaceIds([event.workspaceId], {
3554
+ skipReconcile: true,
3555
+ });
3556
+ }
3557
+ // A git fact (branch, diff, dirty, PR) changed at `cwd`. Every workspace whose
3558
+ // OWN cwd is this folder re-derives its git facts from that folder (id → cwd)
3559
+ // and emits its own per-id descriptor. This is a deliberate same-folder fan,
3560
+ // not a cwd → id ownership lookup: git never resolves which workspace owns a
3561
+ // path. See `workspaceIdsOnCheckout`.
3562
+ async emitWorkspaceUpdateForCwd(cwd, options) {
3563
+ const workspaceIds = workspaceIdsOnCheckout(await this.workspaceRegistry.list(), cwd);
3564
+ if (workspaceIds.length === 0) {
3565
+ return;
3566
+ }
3567
+ await this.emitWorkspaceUpdatesForWorkspaceIds(workspaceIds, options);
3568
+ }
3569
+ async handleFetchAgents(request) {
3570
+ const requestedSubscriptionId = request.subscribe?.subscriptionId?.trim();
3571
+ const subscriptionId = resolveSubscriptionId(request.subscribe, requestedSubscriptionId);
3572
+ try {
3573
+ if (subscriptionId) {
3574
+ this.agentUpdates.beginSubscription({
3575
+ subscriptionId,
3576
+ filter: request.filter,
3577
+ });
3578
+ }
3579
+ const payload = await this.listFetchAgentsEntries(request);
3580
+ const snapshotUpdatedAtByAgentId = new Map();
3581
+ for (const entry of payload.entries) {
3582
+ const parsedUpdatedAt = Date.parse(entry.agent.updatedAt);
3583
+ if (!Number.isNaN(parsedUpdatedAt)) {
3584
+ snapshotUpdatedAtByAgentId.set(entry.agent.id, parsedUpdatedAt);
3585
+ }
3586
+ }
3587
+ this.emit({
3588
+ type: "fetch_agents_response",
3589
+ payload: {
3590
+ requestId: request.requestId,
3591
+ ...(subscriptionId ? { subscriptionId } : {}),
3592
+ ...payload,
3593
+ },
3594
+ });
3595
+ if (subscriptionId) {
3596
+ this.agentUpdates.flushBootstrapped(subscriptionId, { snapshotUpdatedAtByAgentId });
3597
+ }
3598
+ }
3599
+ catch (error) {
3600
+ if (subscriptionId) {
3601
+ this.agentUpdates.clearSubscription(subscriptionId);
3602
+ }
3603
+ const code = error instanceof SessionRequestError ? error.code : "fetch_agents_failed";
3604
+ const message = error instanceof Error ? error.message : "Failed to fetch agents";
3605
+ this.sessionLogger.error({ err: error }, "Failed to handle fetch_agents_request");
3606
+ this.emit({
3607
+ type: "rpc_error",
3608
+ payload: {
3609
+ requestId: request.requestId,
3610
+ requestType: request.type,
3611
+ error: message,
3612
+ code,
3613
+ },
3614
+ });
3615
+ }
3616
+ }
3617
+ async handleFetchAgentHistory(request) {
3618
+ try {
3619
+ const payload = await this.listFetchAgentsEntries(request);
3620
+ this.emit({
3621
+ type: "fetch_agent_history_response",
3622
+ payload: {
3623
+ requestId: request.requestId,
3624
+ ...payload,
3625
+ },
3626
+ });
3627
+ }
3628
+ catch (error) {
3629
+ const code = error instanceof SessionRequestError ? error.code : "fetch_agent_history_failed";
3630
+ const message = error instanceof Error ? error.message : "Failed to fetch agent history";
3631
+ this.sessionLogger.error({ err: error }, "Failed to handle fetch_agent_history_request");
3632
+ this.emit({
3633
+ type: "rpc_error",
3634
+ payload: {
3635
+ requestId: request.requestId,
3636
+ requestType: request.type,
3637
+ error: message,
3638
+ code,
3639
+ },
3640
+ });
3641
+ }
3642
+ }
3643
+ async handleFetchRecentProviderSessions(request) {
3644
+ try {
3645
+ const result = await listImportableProviderSessions({
3646
+ request,
3647
+ agentManager: this.agentManager,
3648
+ agentStorage: this.agentStorage,
3649
+ providerSnapshotManager: this.providerSnapshotManager,
3650
+ });
3651
+ this.emit({
3652
+ type: "fetch_recent_provider_sessions_response",
3653
+ payload: {
3654
+ requestId: request.requestId,
3655
+ entries: result.entries,
3656
+ ...(result.filteredAlreadyImportedCount > 0
3657
+ ? { filteredAlreadyImportedCount: result.filteredAlreadyImportedCount }
3658
+ : {}),
3659
+ },
3660
+ });
3661
+ }
3662
+ catch (error) {
3663
+ const code = error instanceof ImportSessionsRequestError
3664
+ ? error.code
3665
+ : "fetch_recent_provider_sessions_failed";
3666
+ const message = error instanceof Error ? error.message : "Failed to fetch recent provider sessions";
3667
+ this.sessionLogger.error({ err: error }, "Failed to handle fetch_recent_provider_sessions_request");
3668
+ this.emit({
3669
+ type: "rpc_error",
3670
+ payload: {
3671
+ requestId: request.requestId,
3672
+ requestType: request.type,
3673
+ error: message,
3674
+ code,
3675
+ },
3676
+ });
3677
+ }
3678
+ }
3679
+ async handleFetchWorkspacesRequest(request) {
3680
+ const requestedSubscriptionId = request.subscribe?.subscriptionId?.trim();
3681
+ const subscriptionId = resolveSubscriptionId(request.subscribe, requestedSubscriptionId);
3682
+ try {
3683
+ this.sessionLogger.debug({
3684
+ requestId: request.requestId,
3685
+ subscribeRequested: Boolean(request.subscribe),
3686
+ filter: request.filter ?? null,
3687
+ sort: request.sort ?? null,
3688
+ page: request.page ?? null,
3689
+ }, "fetch_workspaces_request_received");
3690
+ if (subscriptionId) {
3691
+ this.workspaceUpdatesSubscription = {
3692
+ subscriptionId,
3693
+ filter: request.filter,
3694
+ isBootstrapping: true,
3695
+ pendingUpdatesByWorkspaceId: new Map(),
3696
+ lastEmittedByWorkspaceId: new Map(),
3697
+ };
3698
+ }
3699
+ const payload = await this.listFetchWorkspacesEntries(request);
3700
+ this.workspaceGitObserver.syncObservers(payload.entries);
3701
+ this.sessionLogger.debug({
3702
+ requestId: request.requestId,
3703
+ subscriptionId,
3704
+ pageInfo: payload.pageInfo,
3705
+ payload: summarizeFetchWorkspacesEntries(payload.entries),
3706
+ }, "fetch_workspaces_response_ready");
3707
+ const snapshot = this.buildBootstrapSnapshot(payload.entries);
3708
+ this.emit({
3709
+ type: "fetch_workspaces_response",
3710
+ payload: {
3711
+ requestId: request.requestId,
3712
+ ...(subscriptionId ? { subscriptionId } : {}),
3713
+ ...payload,
3714
+ },
3715
+ });
3716
+ if (subscriptionId && this.workspaceUpdatesSubscription?.subscriptionId === subscriptionId) {
3717
+ this.flushBootstrappedWorkspaceUpdates(snapshot);
3718
+ void this.reconcileAndEmitWorkspaceUpdates();
3719
+ }
3720
+ }
3721
+ catch (error) {
3722
+ if (subscriptionId && this.workspaceUpdatesSubscription?.subscriptionId === subscriptionId) {
3723
+ this.workspaceUpdatesSubscription = null;
3724
+ }
3725
+ const code = error instanceof SessionRequestError ? error.code : "fetch_workspaces_failed";
3726
+ const message = error instanceof Error ? error.message : "Failed to fetch workspaces";
3727
+ this.sessionLogger.error({ err: error }, "Failed to handle fetch_workspaces_request");
3728
+ this.emit({
3729
+ type: "rpc_error",
3730
+ payload: {
3731
+ requestId: request.requestId,
3732
+ requestType: request.type,
3733
+ error: message,
3734
+ code,
3735
+ },
3736
+ });
3737
+ }
3738
+ }
3739
+ // Build the bootstrap snapshot used by `flushBootstrappedWorkspaceUpdates`
3740
+ // to decide which pending updates to drop. Captures the status,
3741
+ // statusEnteredAt, and activityAt (parsed to ms) for each workspace entry
3742
+ // so a status-only change (e.g. the unmask case), a statusEnteredAt-only
3743
+ // change (e.g. a fresh unmask time), AND a fresher activity all still
3744
+ // ship to the client.
3745
+ buildBootstrapSnapshot(entries) {
3746
+ const snapshotByWorkspaceId = new Map();
3747
+ for (const entry of entries) {
3748
+ const parsedActivity = entry.activityAt ? Date.parse(entry.activityAt) : null;
3749
+ snapshotByWorkspaceId.set(entry.id, {
3750
+ status: entry.status,
3751
+ statusEnteredAt: entry.statusEnteredAt ?? null,
3752
+ activityAtMs: Number.isNaN(parsedActivity) ? null : parsedActivity,
3753
+ });
3754
+ }
3755
+ return { snapshotByWorkspaceId };
3756
+ }
3757
+ async registerWorkspaceForImportedAgent(workspace) {
3758
+ try {
3759
+ await this.syncWorkspaceGitObserverForWorkspace(workspace);
3760
+ await this.describeWorkspaceRecord(workspace);
3761
+ await this.emitWorkspaceUpdateForWorkspaceId(workspace.workspaceId);
3762
+ }
3763
+ catch (error) {
3764
+ this.sessionLogger.warn({ err: error, workspaceId: workspace.workspaceId, cwd: workspace.cwd }, "Failed to register workspace for imported agent");
3765
+ }
3766
+ }
3767
+ async handleWorkspaceCreateRequest(request) {
3768
+ try {
3769
+ if (request.source.kind === "directory") {
3770
+ await this.handleWorkspaceCreateLocal(request);
3771
+ return;
3772
+ }
3773
+ await this.handleWorkspaceCreateWorktree(request);
3774
+ }
3775
+ catch (error) {
3776
+ const message = error instanceof Error ? error.message : "Failed to create workspace";
3777
+ this.sessionLogger.error({ err: error, sourceKind: request.source.kind, requestId: request.requestId }, "Failed to create workspace");
3778
+ this.emit({
3779
+ type: "workspace.create.response",
3780
+ payload: {
3781
+ requestId: request.requestId,
3782
+ workspace: null,
3783
+ setupTerminalId: null,
3784
+ error: message,
3785
+ },
3786
+ });
3787
+ }
3788
+ }
3789
+ async handleWorkspaceCreateLocal(request) {
3790
+ if (request.source.kind !== "directory") {
3791
+ return;
3792
+ }
3793
+ const cwd = expandTilde(request.source.path);
3794
+ const directoryExists = await this.filesystem.isDirectory(cwd).catch(() => false);
3795
+ if (!directoryExists) {
3796
+ this.emit({
3797
+ type: "workspace.create.response",
3798
+ payload: {
3799
+ requestId: request.requestId,
3800
+ workspace: null,
3801
+ setupTerminalId: null,
3802
+ error: `Directory not found: ${cwd}`,
3803
+ errorCode: "directory_not_found",
3804
+ },
3805
+ });
3806
+ return;
3807
+ }
3808
+ const explicitTitle = request.title?.trim() || null;
3809
+ const promptTitle = resolveFirstAgentPromptTitle(request.firstAgentContext);
3810
+ const workspace = await createLocalCheckoutWorkspace({ cwd, title: explicitTitle ?? promptTitle }, {
3811
+ projectRegistry: this.projectRegistry,
3812
+ workspaceRegistry: this.workspaceRegistry,
3813
+ workspaceGitService: this.workspaceGitService,
3814
+ });
3815
+ await this.syncWorkspaceGitObserverForWorkspace(workspace);
3816
+ const descriptor = await this.describeWorkspaceRecord(workspace);
3817
+ this.emit({
3818
+ type: "workspace.create.response",
3819
+ payload: {
3820
+ requestId: request.requestId,
3821
+ workspace: descriptor,
3822
+ setupTerminalId: null,
3823
+ error: null,
3824
+ },
3825
+ });
3826
+ await this.emitWorkspaceUpdateForWorkspaceId(workspace.workspaceId);
3827
+ void this.workspaceGitService
3828
+ .getSnapshot(workspace.cwd, { force: true, includeGitHub: true, reason: "open_project" })
3829
+ .catch((error) => {
3830
+ this.sessionLogger.warn({ err: error, cwd: workspace.cwd }, "Background snapshot refresh failed after workspace.create");
3831
+ });
3832
+ if (request.firstAgentContext) {
3833
+ const firstAgentContext = request.firstAgentContext;
3834
+ this.workspaceAutoName.scheduleForDirectory({
3835
+ workspaceId: workspace.workspaceId,
3836
+ cwd: workspace.cwd,
3837
+ firstAgentContext,
3838
+ }, { currentSelection: this.getFocusedAgentSelectionForCwd(workspace.cwd) });
3839
+ }
3840
+ }
3841
+ async handleWorkspaceCreateWorktree(request) {
3842
+ if (request.source.kind !== "worktree") {
3843
+ return;
3844
+ }
3845
+ const source = request.source;
3846
+ if (!source.cwd && !source.projectId) {
3847
+ this.emit({
3848
+ type: "workspace.create.response",
3849
+ payload: {
3850
+ requestId: request.requestId,
3851
+ workspace: null,
3852
+ setupTerminalId: null,
3853
+ error: "cwd or projectId is required for a worktree-backed workspace",
3854
+ errorCode: "source_required",
3855
+ },
3856
+ });
3857
+ return;
3858
+ }
3859
+ const sourceCwd = await this.resolveWorktreeSourceCwd({
3860
+ cwd: source.cwd,
3861
+ projectId: source.projectId,
3862
+ });
3863
+ const result = await this.createOttoWorktreeWorkflow({
3864
+ cwd: sourceCwd,
3865
+ projectId: source.projectId,
3866
+ worktreeSlug: source.worktreeSlug,
3867
+ action: source.action,
3868
+ refName: source.refName,
3869
+ githubPrNumber: source.githubPrNumber,
3870
+ firstAgentContext: request.firstAgentContext,
3871
+ }, source.baseBranch
3872
+ ? { resolveDefaultBranch: async () => source.baseBranch }
3873
+ : undefined);
3874
+ if (request.title?.trim()) {
3875
+ await this.workspaceRegistry.upsert({
3876
+ ...result.workspace,
3877
+ title: request.title.trim(),
3878
+ updatedAt: new Date().toISOString(),
3879
+ });
3880
+ result.workspace.title = request.title.trim();
3881
+ }
3882
+ const descriptor = await this.describeCreatedWorktreeWorkspace(result);
3883
+ this.emit({
3884
+ type: "workspace.create.response",
3885
+ payload: {
3886
+ requestId: request.requestId,
3887
+ workspace: descriptor,
3888
+ setupTerminalId: null,
3889
+ error: null,
3890
+ },
3891
+ });
3892
+ this.emit({
3893
+ type: "workspace_update",
3894
+ payload: { kind: "upsert", workspace: descriptor },
3895
+ });
3896
+ }
3897
+ async resolveWorktreeSourceCwd(input) {
3898
+ if (input.cwd) {
3899
+ return expandTilde(input.cwd);
3900
+ }
3901
+ const project = await this.projectRegistry.get(input.projectId);
3902
+ if (!project || project.archivedAt) {
3903
+ throw new Error(`Project not found: ${input.projectId}`);
3904
+ }
3905
+ return project.rootPath;
3906
+ }
3907
+ async handleOpenProjectRequest(request) {
3908
+ const requestedCwd = request.cwd;
3909
+ const cwd = expandTilde(requestedCwd);
3910
+ const directoryExists = await this.filesystem.isDirectory(cwd).catch(() => false);
3911
+ if (!directoryExists) {
3912
+ this.sessionLogger.info({ requestedCwd, resolvedCwd: cwd, reason: "directory_not_found" }, "Open project rejected");
3913
+ this.emit({
3914
+ type: "open_project_response",
3915
+ payload: {
3916
+ requestId: request.requestId,
3917
+ workspace: null,
3918
+ error: `Directory not found: ${cwd}`,
3919
+ errorCode: "directory_not_found",
3920
+ },
3921
+ });
3922
+ return;
3923
+ }
3924
+ try {
3925
+ const projectsBefore = new Map();
3926
+ for (const project of await this.projectRegistry.list()) {
3927
+ projectsBefore.set(project.projectId, project);
3928
+ }
3929
+ const workspacesBefore = new Map();
3930
+ for (const workspaceRecord of await this.workspaceRegistry.list()) {
3931
+ workspacesBefore.set(workspaceRecord.workspaceId, workspaceRecord);
3932
+ }
3933
+ const workspace = await this.workspaceProvisioning.findOrCreateWorkspaceForDirectory(cwd);
3934
+ const project = await this.projectRegistry.get(workspace.projectId);
3935
+ await this.syncWorkspaceGitObserverForWorkspace(workspace);
3936
+ const descriptor = await this.describeWorkspaceRecord(workspace);
3937
+ await this.emitWorkspaceUpdateForWorkspaceId(workspace.workspaceId);
3938
+ this.sessionLogger.info({
3939
+ requestedCwd,
3940
+ resolvedCwd: cwd,
3941
+ workspaceCwd: workspace.cwd,
3942
+ workspaceId: workspace.workspaceId,
3943
+ workspaceKind: workspace.kind,
3944
+ workspaceTransition: describeRegistryTransition(workspacesBefore.get(workspace.workspaceId) ?? null),
3945
+ projectId: workspace.projectId,
3946
+ projectKind: project?.kind ?? null,
3947
+ projectTransition: describeRegistryTransition(projectsBefore.get(workspace.projectId) ?? null),
3948
+ }, "Project opened");
3949
+ this.emit({
3950
+ type: "open_project_response",
3951
+ payload: {
3952
+ requestId: request.requestId,
3953
+ workspace: descriptor,
3954
+ error: null,
3955
+ },
3956
+ });
3957
+ void this.workspaceGitService
3958
+ .getSnapshot(workspace.cwd, {
3959
+ force: true,
3960
+ includeGitHub: true,
3961
+ reason: "open_project",
3962
+ })
3963
+ .catch((error) => {
3964
+ this.sessionLogger.warn({ err: error, cwd: workspace.cwd }, "Background snapshot refresh failed after open_project");
3965
+ });
3966
+ }
3967
+ catch (error) {
3968
+ const message = error instanceof Error ? error.message : "Failed to open project";
3969
+ this.sessionLogger.error({ err: error, cwd }, "Failed to open project");
3970
+ this.emit({
3971
+ type: "open_project_response",
3972
+ payload: {
3973
+ requestId: request.requestId,
3974
+ workspace: null,
3975
+ error: message,
3976
+ },
3977
+ });
3978
+ }
3979
+ }
3980
+ async handleProjectAddRequest(request) {
3981
+ const requestedCwd = request.cwd;
3982
+ const cwd = expandTilde(requestedCwd);
3983
+ const directoryExists = await this.filesystem.isDirectory(cwd).catch(() => false);
3984
+ if (!directoryExists) {
3985
+ this.sessionLogger.info({ requestedCwd, resolvedCwd: cwd, reason: "directory_not_found" }, "Add project rejected");
3986
+ this.emit({
3987
+ type: "project.add.response",
3988
+ payload: {
3989
+ requestId: request.requestId,
3990
+ project: null,
3991
+ error: `Directory not found: ${cwd}`,
3992
+ errorCode: "directory_not_found",
3993
+ },
3994
+ });
3995
+ return;
3996
+ }
3997
+ try {
3998
+ const projectsBefore = new Map();
3999
+ for (const project of await this.projectRegistry.list()) {
4000
+ projectsBefore.set(project.projectId, project);
4001
+ }
4002
+ const project = await this.workspaceProvisioning.findOrCreateProjectForDirectory(cwd);
4003
+ this.sessionLogger.info({
4004
+ requestedCwd,
4005
+ resolvedCwd: cwd,
4006
+ projectId: project.projectId,
4007
+ projectKind: project.kind,
4008
+ projectTransition: describeRegistryTransition(projectsBefore.get(project.projectId) ?? null),
4009
+ }, "Project added");
4010
+ this.emit({
4011
+ type: "project.add.response",
4012
+ payload: {
4013
+ requestId: request.requestId,
4014
+ project: this.buildProjectDescriptor(project),
4015
+ error: null,
4016
+ },
4017
+ });
4018
+ }
4019
+ catch (error) {
4020
+ const message = error instanceof Error ? error.message : "Failed to add project";
4021
+ this.sessionLogger.error({ err: error, cwd }, "Failed to add project");
4022
+ this.emit({
4023
+ type: "project.add.response",
4024
+ payload: {
4025
+ requestId: request.requestId,
4026
+ project: null,
4027
+ error: message,
4028
+ },
4029
+ });
4030
+ }
4031
+ }
4032
+ // Named accessor: the workspace descriptor builder and the git-watch test both read a workspace's
4033
+ // scripts snapshot through here; the workspace-scripts module owns the payload assembly.
4034
+ buildWorkspaceScriptPayloadSnapshot(workspaceId, workspaceDirectory) {
4035
+ return this.workspaceScripts.buildSnapshot(workspaceId, workspaceDirectory);
4036
+ }
4037
+ handleStartWorkspaceScriptRequest(request) {
4038
+ return this.workspaceScripts.start(request);
4039
+ }
4040
+ // COMPAT(desktopEditorBridge): added in v0.1.88, remove after 2026-12-03 once old clients no longer call daemon editor RPCs.
4041
+ async handleLegacyListAvailableEditorsRequest(request) {
4042
+ this.emit({
4043
+ type: "list_available_editors_response",
4044
+ payload: {
4045
+ requestId: request.requestId,
4046
+ editors: [],
4047
+ error: "Editor opening moved to the desktop app and is no longer supported by the daemon",
4048
+ },
4049
+ });
4050
+ }
4051
+ async handleLegacyOpenInEditorRequest(request) {
4052
+ this.emit({
4053
+ type: "open_in_editor_response",
4054
+ payload: {
4055
+ requestId: request.requestId,
4056
+ error: "Editor opening moved to the desktop app and is no longer supported by the daemon",
4057
+ },
4058
+ });
4059
+ }
4060
+ async handleCreateOttoWorktreeRequest(request) {
4061
+ return handleCreateWorktreeRequest({
4062
+ ottoHome: this.ottoHome,
4063
+ worktreesRoot: this.worktreesRoot,
4064
+ describeWorkspaceRecord: (result) => this.describeCreatedWorktreeWorkspace(result),
4065
+ emit: (message) => this.emit(message),
4066
+ sessionLogger: this.sessionLogger,
4067
+ createOttoWorktreeWorkflow: (input) => this.createOttoWorktreeWorkflow(input),
4068
+ }, request);
4069
+ }
4070
+ async createOttoWorktreeWorkflow(input, options) {
4071
+ return createWorktreeWorkflow({
4072
+ ottoHome: this.ottoHome,
4073
+ worktreesRoot: this.worktreesRoot,
4074
+ createOttoWorktree: (workflowInput, serviceOptions) => this.createOttoWorktree(workflowInput, serviceOptions),
4075
+ warmWorkspaceGitData: (workspace) => this.warmWorkspaceGitDataForWorkspace(workspace),
4076
+ autoNameWorkspaceBranchForFirstAgent: (autoNameInput) => this.workspaceAutoName.scheduleForWorktree(autoNameInput, {
4077
+ currentSelection: this.getFocusedAgentSelectionForCwd(autoNameInput.workspace.cwd),
4078
+ }),
4079
+ emitWorkspaceUpdateForWorkspaceId: (workspaceId) => this.emitWorkspaceUpdateForWorkspaceId(workspaceId),
4080
+ cacheWorkspaceSetupSnapshot: (workspaceId, snapshot) => {
4081
+ this.workspaceSetupSnapshots.set(workspaceId, snapshot);
4082
+ },
4083
+ emit: (message) => this.emit(message),
4084
+ sessionLogger: this.sessionLogger,
4085
+ terminalManager: this.terminalManager,
4086
+ archiveWorkspaceRecord: (workspaceId) => this.archiveWorkspaceRecord(workspaceId),
4087
+ serviceProxy: this.serviceProxy,
4088
+ scriptRuntimeStore: this.scriptRuntimeStore,
4089
+ getDaemonTcpPort: this.getDaemonTcpPort,
4090
+ getDaemonTcpHost: this.getDaemonTcpHost,
4091
+ serviceProxyPublicBaseUrl: this.serviceProxyPublicBaseUrl,
4092
+ onScriptsChanged: (workspaceId, workspaceDirectory) => {
4093
+ this.workspaceScripts.emitStatusUpdate(workspaceId, workspaceDirectory);
4094
+ },
4095
+ }, input, options);
4096
+ }
4097
+ async handleWorkspaceSetupStatusRequest(request) {
4098
+ return handleWorkspaceSetupStatusRequestMessage({
4099
+ emit: (message) => this.emit(message),
4100
+ workspaceSetupSnapshots: this.workspaceSetupSnapshots,
4101
+ }, request);
4102
+ }
4103
+ async handleArchiveWorkspaceRequest(request) {
4104
+ try {
4105
+ const existing = await this.workspaceRegistry.get(request.workspaceId);
4106
+ if (!existing) {
4107
+ throw new Error(`Workspace not found: ${request.workspaceId}`);
4108
+ }
4109
+ const gitSnapshot = await this.workspaceGitService
4110
+ .getSnapshot(existing.cwd)
4111
+ .catch(() => null);
4112
+ const repoRoot = gitSnapshot?.git?.repoRoot ?? null;
4113
+ await archiveByScope({
4114
+ ottoHome: this.ottoHome,
4115
+ ottoWorktreesBaseRoot: this.worktreesRoot,
4116
+ github: this.github,
4117
+ workspaceGitService: this.workspaceGitService,
4118
+ agentManager: this.agentManager,
4119
+ agentStorage: this.agentStorage,
4120
+ findWorkspaceIdForCwd: (cwd) => this.findWorkspaceIdForCwd(cwd),
4121
+ listActiveWorkspaces: () => this.listActiveWorkspaceRefs(),
4122
+ archiveWorkspaceRecord: (workspaceId) => this.archiveWorkspaceRecord(workspaceId),
4123
+ emitWorkspaceUpdatesForWorkspaceIds: (workspaceIds) => this.emitWorkspaceUpdatesForWorkspaceIds(workspaceIds),
4124
+ markWorkspaceArchiving: (workspaceIds, archivingAt) => this.markWorkspaceArchiving(workspaceIds, archivingAt),
4125
+ clearWorkspaceArchiving: (workspaceIds) => this.clearWorkspaceArchiving(workspaceIds),
4126
+ killTerminalsForWorkspace: (workspaceId) => this.terminalController.killTerminalsForWorkspace(workspaceId),
4127
+ sessionLogger: this.sessionLogger,
4128
+ }, {
4129
+ scope: { kind: "workspace", workspaceId: existing.workspaceId },
4130
+ repoRoot,
4131
+ ottoWorktreesBaseRoot: this.worktreesRoot,
4132
+ requestId: request.requestId,
4133
+ });
4134
+ const archivedWorkspace = await this.workspaceRegistry.get(request.workspaceId);
4135
+ const archivedAt = archivedWorkspace?.archivedAt ?? new Date().toISOString();
4136
+ this.emit({
4137
+ type: "archive_workspace_response",
4138
+ payload: {
4139
+ requestId: request.requestId,
4140
+ workspaceId: request.workspaceId,
4141
+ archivedAt,
4142
+ error: null,
4143
+ },
4144
+ });
4145
+ }
4146
+ catch (error) {
4147
+ const message = error instanceof Error ? error.message : "Failed to archive workspace";
4148
+ this.sessionLogger.error({ err: error, workspaceId: request.workspaceId }, "Failed to archive workspace");
4149
+ this.emit({
4150
+ type: "archive_workspace_response",
4151
+ payload: {
4152
+ requestId: request.requestId,
4153
+ workspaceId: request.workspaceId,
4154
+ archivedAt: null,
4155
+ error: message,
4156
+ },
4157
+ });
4158
+ }
4159
+ }
4160
+ async handleWorkspaceClearAttentionRequest(request) {
4161
+ const { requestId, workspaceId } = request;
4162
+ const requestedWorkspaceIds = Array.isArray(workspaceId) ? workspaceId : [workspaceId];
4163
+ let agents;
4164
+ try {
4165
+ agents = await this.listAgentPayloads();
4166
+ }
4167
+ catch (error) {
4168
+ const message = getErrorMessage(error);
4169
+ const results = requestedWorkspaceIds.map((requestedWorkspaceId) => ({
4170
+ workspaceId: requestedWorkspaceId,
4171
+ clearedAgentIds: [],
4172
+ success: false,
4173
+ error: message,
4174
+ }));
4175
+ this.emit({
4176
+ type: "workspace.clear_attention.response",
4177
+ payload: {
4178
+ requestId,
4179
+ workspaceId,
4180
+ clearedAgentIds: [],
4181
+ results,
4182
+ success: false,
4183
+ error: message,
4184
+ },
4185
+ });
4186
+ return;
4187
+ }
4188
+ const results = [];
4189
+ for (const requestedWorkspaceId of requestedWorkspaceIds) {
4190
+ const clearedAgentIds = [];
4191
+ try {
4192
+ const workspace = await this.workspaceRegistry.get(requestedWorkspaceId);
4193
+ if (!workspace || workspace.archivedAt) {
4194
+ throw new Error(`Workspace not found: ${requestedWorkspaceId}`);
4195
+ }
4196
+ // Clearing attention is scoped to the workspace that OWNS the agent, by
4197
+ // workspaceId — never by comparing cwd strings. A sibling workspace
4198
+ // sharing the same directory keeps its own agents' attention.
4199
+ const clearableAgentIds = agents
4200
+ .filter((agent) => !agent.archivedAt)
4201
+ .filter((agent) => agent.workspaceId === workspace.workspaceId)
4202
+ .filter((agent) => agent.requiresAttention === true)
4203
+ .filter((agent) => (agent.pendingPermissions?.length ?? 0) === 0)
4204
+ .filter((agent) => agent.attentionReason !== "permission")
4205
+ .map((agent) => agent.id);
4206
+ for (const agentId of clearableAgentIds) {
4207
+ const liveAgent = this.agentManager.getAgent(agentId);
4208
+ if (liveAgent) {
4209
+ await this.agentManager.clearAgentAttention(agentId);
4210
+ clearedAgentIds.push(agentId);
4211
+ continue;
4212
+ }
4213
+ const record = await this.agentStorage.get(agentId);
4214
+ if (!record ||
4215
+ record.internal ||
4216
+ record.archivedAt ||
4217
+ record.requiresAttention !== true) {
4218
+ continue;
4219
+ }
4220
+ const nextRecord = {
4221
+ ...record,
4222
+ updatedAt: new Date().toISOString(),
4223
+ requiresAttention: false,
4224
+ attentionReason: null,
4225
+ attentionTimestamp: null,
4226
+ };
4227
+ await this.agentStorage.upsert(nextRecord);
4228
+ const agent = this.buildStoredAgentPayload(nextRecord);
4229
+ const project = await this.buildProjectPlacementForWorkspace(workspace);
4230
+ this.emit({
4231
+ type: "agent_update",
4232
+ payload: {
4233
+ kind: "upsert",
4234
+ agent,
4235
+ project,
4236
+ },
4237
+ });
4238
+ clearedAgentIds.push(agentId);
4239
+ }
4240
+ await this.emitWorkspaceUpdateForWorkspaceId(workspace.workspaceId);
4241
+ results.push({
4242
+ workspaceId: requestedWorkspaceId,
4243
+ clearedAgentIds,
4244
+ success: true,
4245
+ error: null,
4246
+ });
4247
+ }
4248
+ catch (error) {
4249
+ const message = getErrorMessage(error);
4250
+ this.sessionLogger.error({ err: error, workspaceId: requestedWorkspaceId }, "Failed to clear workspace attention");
4251
+ results.push({
4252
+ workspaceId: requestedWorkspaceId,
4253
+ clearedAgentIds,
4254
+ success: false,
4255
+ error: message,
4256
+ });
4257
+ }
4258
+ }
4259
+ const clearedAgentIds = results.flatMap((result) => result.clearedAgentIds);
4260
+ const failedResults = results.filter((result) => !result.success);
4261
+ this.emit({
4262
+ type: "workspace.clear_attention.response",
4263
+ payload: {
4264
+ requestId,
4265
+ workspaceId,
4266
+ clearedAgentIds,
4267
+ results,
4268
+ success: failedResults.length === 0,
4269
+ error: failedResults.length === 0
4270
+ ? null
4271
+ : failedResults
4272
+ .map((result) => result.error)
4273
+ .filter((error) => error !== null)
4274
+ .join("; "),
4275
+ },
4276
+ });
4277
+ }
4278
+ async handleFetchAgent(agentIdOrIdentifier, requestId) {
4279
+ const resolved = await this.resolveAgentIdentifier(agentIdOrIdentifier);
4280
+ if (!resolved.ok) {
4281
+ this.emit({
4282
+ type: "fetch_agent_response",
4283
+ payload: { requestId, agent: null, project: null, error: resolved.error },
4284
+ });
4285
+ return;
4286
+ }
4287
+ const agent = await this.getAgentPayloadById(resolved.agentId);
4288
+ if (!agent) {
4289
+ this.emit({
4290
+ type: "fetch_agent_response",
4291
+ payload: {
4292
+ requestId,
4293
+ agent: null,
4294
+ project: null,
4295
+ error: `Agent not found: ${resolved.agentId}`,
4296
+ },
4297
+ });
4298
+ return;
4299
+ }
4300
+ const project = agent.workspaceId
4301
+ ? await this.buildProjectPlacementForWorkspaceId(agent.workspaceId)
4302
+ : null;
4303
+ this.emit({
4304
+ type: "fetch_agent_response",
4305
+ payload: { requestId, agent, project, error: null },
4306
+ });
4307
+ }
4308
+ shouldUseFullTimelineForProjectedPage(input) {
4309
+ const { timeline } = input;
4310
+ if (timeline.reset || timeline.rows.length === 0 || !timeline.hasOlder) {
4311
+ return false;
4312
+ }
4313
+ const firstRow = timeline.rows[0];
4314
+ if (firstRow?.item.type === "assistant_message" ||
4315
+ firstRow?.item.type === "reasoning" ||
4316
+ firstRow?.item.type === "tool_call") {
4317
+ return true;
4318
+ }
4319
+ return timeline.rows.some((row) => row.item.type === "tool_call");
4320
+ }
4321
+ selectCanonicalTimelineProjection(input) {
4322
+ const entries = projectTimelineRows({ rows: input.timeline.rows, mode: "canonical" });
4323
+ return {
4324
+ timeline: input.timeline,
4325
+ entries,
4326
+ startSeq: entries[0]?.seqStart ?? null,
4327
+ endSeq: entries[entries.length - 1]?.seqEnd ?? null,
4328
+ hasOlder: input.timeline.hasOlder,
4329
+ hasNewer: input.timeline.hasNewer,
4330
+ };
4331
+ }
4332
+ selectProjectedTimelineProjection(input) {
4333
+ const timeline = this.shouldUseFullTimelineForProjectedPage({
4334
+ timeline: input.controlTimeline,
4335
+ })
4336
+ ? this.agentManager.fetchTimeline(input.agentId, { direction: "tail", limit: 0 })
4337
+ : input.controlTimeline;
4338
+ const page = selectProjectedTimelinePage({
4339
+ rows: timeline.rows,
4340
+ bounds: timeline.window,
4341
+ direction: input.controlTimeline.reset ? "tail" : input.direction,
4342
+ ...(input.cursor ? { cursorSeq: input.cursor.seq } : {}),
4343
+ limit: input.pageLimit,
4344
+ });
4345
+ return {
4346
+ timeline,
4347
+ entries: page.entries,
4348
+ startSeq: page.startSeq,
4349
+ endSeq: page.endSeq,
4350
+ hasOlder: page.hasOlder || (page.startSeq !== null && page.startSeq > timeline.window.minSeq),
4351
+ hasNewer: page.hasNewer,
4352
+ };
4353
+ }
4354
+ selectTimelineProjection(input) {
4355
+ if (input.projection === "canonical") {
4356
+ return this.selectCanonicalTimelineProjection({ timeline: input.controlTimeline });
4357
+ }
4358
+ return this.selectProjectedTimelineProjection(input);
4359
+ }
4360
+ async handleFetchAgentTimelineRequest(msg) {
4361
+ const direction = msg.direction ?? (msg.cursor ? "after" : "tail");
4362
+ const projection = msg.projection ?? "projected";
4363
+ const requestedLimit = msg.limit;
4364
+ const pageLimit = requestedLimit ?? (direction === "after" ? 0 : 200);
4365
+ const cursor = msg.cursor
4366
+ ? {
4367
+ epoch: msg.cursor.epoch,
4368
+ seq: msg.cursor.seq,
4369
+ }
4370
+ : undefined;
4371
+ try {
4372
+ // Observed subagents have no ManagedAgent or stored record; serve their
4373
+ // last emitted snapshot instead. See projects/observed-subagents/observed-subagents.md.
4374
+ const observedPayload = this.agentManager.getObservedSubagentPayload(msg.agentId);
4375
+ const agentPayload = observedPayload ??
4376
+ (await this.buildAgentPayload(await ensureAgentLoaded(msg.agentId, {
4377
+ agentManager: this.agentManager,
4378
+ agentStorage: this.agentStorage,
4379
+ logger: this.sessionLogger,
4380
+ })));
4381
+ const controlTimeline = this.agentManager.fetchTimeline(msg.agentId, {
4382
+ direction,
4383
+ cursor,
4384
+ limit: pageLimit,
4385
+ });
4386
+ const selectedTimeline = this.selectTimelineProjection({
4387
+ agentId: msg.agentId,
4388
+ projection,
4389
+ controlTimeline,
4390
+ direction,
4391
+ ...(cursor ? { cursor } : {}),
4392
+ pageLimit,
4393
+ });
4394
+ const startCursor = selectedTimeline.startSeq !== null
4395
+ ? { epoch: selectedTimeline.timeline.epoch, seq: selectedTimeline.startSeq }
4396
+ : null;
4397
+ const endCursor = selectedTimeline.endSeq !== null
4398
+ ? { epoch: selectedTimeline.timeline.epoch, seq: selectedTimeline.endSeq }
4399
+ : null;
4400
+ this.emit({
4401
+ type: "fetch_agent_timeline_response",
4402
+ payload: {
4403
+ requestId: msg.requestId,
4404
+ agentId: msg.agentId,
4405
+ agent: agentPayload,
4406
+ direction,
4407
+ projection,
4408
+ epoch: selectedTimeline.timeline.epoch,
4409
+ reset: controlTimeline.reset,
4410
+ staleCursor: controlTimeline.staleCursor,
4411
+ gap: controlTimeline.gap,
4412
+ window: selectedTimeline.timeline.window,
4413
+ startCursor,
4414
+ endCursor,
4415
+ hasOlder: selectedTimeline.hasOlder,
4416
+ hasNewer: selectedTimeline.hasNewer,
4417
+ entries: selectedTimeline.entries.map((entry) => ({
4418
+ provider: agentPayload.provider,
4419
+ item: entry.item,
4420
+ timestamp: entry.timestamp,
4421
+ seqStart: entry.seqStart,
4422
+ seqEnd: entry.seqEnd,
4423
+ sourceSeqRanges: entry.sourceSeqRanges,
4424
+ collapsed: this.supports(CLIENT_CAPS.reasoningMergeEnum)
4425
+ ? entry.collapsed
4426
+ : entry.collapsed.filter((value) => value !== "reasoning_merge"),
4427
+ })),
4428
+ error: null,
4429
+ },
4430
+ });
4431
+ }
4432
+ catch (error) {
4433
+ this.sessionLogger.error({ err: error, agentId: msg.agentId }, "Failed to handle fetch_agent_timeline_request");
4434
+ this.emit({
4435
+ type: "fetch_agent_timeline_response",
4436
+ payload: {
4437
+ requestId: msg.requestId,
4438
+ agentId: msg.agentId,
4439
+ agent: null,
4440
+ direction,
4441
+ projection,
4442
+ epoch: "",
4443
+ reset: false,
4444
+ staleCursor: false,
4445
+ gap: false,
4446
+ window: { minSeq: 0, maxSeq: 0, nextSeq: 0 },
4447
+ startCursor: null,
4448
+ endCursor: null,
4449
+ hasOlder: false,
4450
+ hasNewer: false,
4451
+ entries: [],
4452
+ error: error instanceof Error ? error.message : String(error),
4453
+ },
4454
+ });
4455
+ }
4456
+ }
4457
+ async handleAgentForkContextRequest(msg) {
4458
+ try {
4459
+ const snapshot = await ensureAgentLoaded(msg.agentId, {
4460
+ agentManager: this.agentManager,
4461
+ agentStorage: this.agentStorage,
4462
+ logger: this.sessionLogger,
4463
+ });
4464
+ const agentPayload = await this.buildAgentPayload(snapshot);
4465
+ const rows = this.agentManager.fetchTimeline(msg.agentId, {
4466
+ direction: "tail",
4467
+ limit: 0,
4468
+ }).rows;
4469
+ const forkContext = buildAgentForkContextAttachment({
4470
+ rows,
4471
+ boundaryMessageId: msg.boundaryMessageId,
4472
+ agentTitle: agentPayload.title,
4473
+ cwd: snapshot.cwd,
4474
+ });
4475
+ this.emit({
4476
+ type: "agent.fork_context.response",
4477
+ payload: {
4478
+ requestId: msg.requestId,
4479
+ agentId: msg.agentId,
4480
+ attachment: forkContext.attachment,
4481
+ itemCount: forkContext.itemCount,
4482
+ boundaryMessageId: forkContext.boundaryMessageId,
4483
+ error: null,
4484
+ },
4485
+ });
4486
+ }
4487
+ catch (error) {
4488
+ this.sessionLogger.error({ err: error, agentId: msg.agentId }, "Failed to handle agent.fork_context.request");
4489
+ this.emit({
4490
+ type: "agent.fork_context.response",
4491
+ payload: {
4492
+ requestId: msg.requestId,
4493
+ agentId: msg.agentId,
4494
+ attachment: null,
4495
+ itemCount: 0,
4496
+ boundaryMessageId: msg.boundaryMessageId ?? null,
4497
+ error: error instanceof Error ? error.message : String(error),
4498
+ },
4499
+ });
4500
+ }
4501
+ }
4502
+ async handleSendAgentMessageRequest(msg) {
4503
+ const resolved = await this.resolveAgentIdentifier(msg.agentId);
4504
+ if (!resolved.ok) {
4505
+ this.emit({
4506
+ type: "send_agent_message_response",
4507
+ payload: {
4508
+ requestId: msg.requestId,
4509
+ agentId: msg.agentId,
4510
+ accepted: false,
4511
+ error: resolved.error,
4512
+ },
4513
+ });
4514
+ return;
4515
+ }
4516
+ try {
4517
+ const agentId = resolved.agentId;
4518
+ const prompt = this.buildAgentPrompt(msg.text, msg.images, msg.attachments);
4519
+ this.sessionLogger.trace({
4520
+ agentId,
4521
+ messageId: msg.messageId,
4522
+ textPrefix: msg.text.slice(0, 80),
4523
+ }, "agent.session.send_agent_message");
4524
+ let dispatchResult;
4525
+ try {
4526
+ dispatchResult = await sendPromptToAgent({
4527
+ agentManager: this.agentManager,
4528
+ agentStorage: this.agentStorage,
4529
+ agentId,
4530
+ prompt,
4531
+ messageId: msg.messageId,
4532
+ logger: this.sessionLogger,
4533
+ });
4534
+ }
4535
+ catch (error) {
4536
+ const message = error instanceof Error ? error.message : String(error);
4537
+ this.handleAgentRunError(agentId, error, "Failed to send agent message");
4538
+ this.emit({
4539
+ type: "send_agent_message_response",
4540
+ payload: {
4541
+ requestId: msg.requestId,
4542
+ agentId,
4543
+ accepted: false,
4544
+ error: message,
4545
+ },
4546
+ });
4547
+ return;
4548
+ }
4549
+ if (dispatchResult.outOfBand) {
4550
+ this.emit({
4551
+ type: "send_agent_message_response",
4552
+ payload: {
4553
+ requestId: msg.requestId,
4554
+ agentId,
4555
+ accepted: true,
4556
+ error: null,
4557
+ },
4558
+ });
4559
+ return;
4560
+ }
4561
+ try {
4562
+ await waitForAgentRunStartWithTimeout(this.agentManager, agentId);
4563
+ }
4564
+ catch (error) {
4565
+ this.emit({
4566
+ type: "send_agent_message_response",
4567
+ payload: {
4568
+ requestId: msg.requestId,
4569
+ agentId,
4570
+ accepted: false,
4571
+ error: errorToFriendlyMessage(error),
4572
+ },
4573
+ });
4574
+ return;
4575
+ }
4576
+ this.emit({
4577
+ type: "send_agent_message_response",
4578
+ payload: {
4579
+ requestId: msg.requestId,
4580
+ agentId,
4581
+ accepted: true,
4582
+ error: null,
4583
+ },
4584
+ });
4585
+ }
4586
+ catch (error) {
4587
+ this.emit({
4588
+ type: "send_agent_message_response",
4589
+ payload: {
4590
+ requestId: msg.requestId,
4591
+ agentId: resolved.agentId,
4592
+ accepted: false,
4593
+ error: errorToFriendlyMessage(error),
4594
+ },
4595
+ });
4596
+ }
4597
+ }
4598
+ async handleWaitForFinish(agentIdOrIdentifier, requestId, timeoutMs) {
4599
+ const resolved = await this.resolveAgentIdentifier(agentIdOrIdentifier);
4600
+ if (!resolved.ok) {
4601
+ this.emit({
4602
+ type: "wait_for_finish_response",
4603
+ payload: {
4604
+ requestId,
4605
+ status: "error",
4606
+ final: null,
4607
+ error: resolved.error,
4608
+ lastMessage: null,
4609
+ },
4610
+ });
4611
+ return;
4612
+ }
4613
+ const agentId = resolved.agentId;
4614
+ const live = this.agentManager.getAgent(agentId);
4615
+ if (!live) {
4616
+ const record = await this.agentStorage.get(agentId);
4617
+ if (!record || record.internal) {
4618
+ this.emit({
4619
+ type: "wait_for_finish_response",
4620
+ payload: {
4621
+ requestId,
4622
+ status: "error",
4623
+ final: null,
4624
+ error: `Agent not found: ${agentId}`,
4625
+ lastMessage: null,
4626
+ },
4627
+ });
4628
+ return;
4629
+ }
4630
+ const final = this.buildStoredAgentPayload(record);
4631
+ let status;
4632
+ if (record.attentionReason === "permission") {
4633
+ status = "permission";
4634
+ }
4635
+ else if (record.lastStatus === "error") {
4636
+ status = "error";
4637
+ }
4638
+ else {
4639
+ status = "idle";
4640
+ }
4641
+ const error = resolveWaitForFinishError({ status, final });
4642
+ this.emit({
4643
+ type: "wait_for_finish_response",
4644
+ payload: { requestId, status, final, error, lastMessage: null },
4645
+ });
4646
+ return;
4647
+ }
4648
+ const abortController = new AbortController();
4649
+ const hasTimeout = typeof timeoutMs === "number" && timeoutMs > 0;
4650
+ const timeoutHandle = hasTimeout
4651
+ ? setTimeout(() => {
4652
+ abortController.abort("timeout");
4653
+ }, timeoutMs)
4654
+ : null;
4655
+ try {
4656
+ let result = await this.agentManager.waitForAgentEvent(agentId, {
4657
+ signal: abortController.signal,
4658
+ waitForActive: true,
4659
+ });
4660
+ let final = await this.getAgentPayloadById(agentId);
4661
+ if (!final) {
4662
+ throw new Error(`Agent ${agentId} disappeared while waiting`);
4663
+ }
4664
+ let status;
4665
+ if (result.permission) {
4666
+ status = "permission";
4667
+ }
4668
+ else if (result.status === "error") {
4669
+ status = "error";
4670
+ }
4671
+ else {
4672
+ status = "idle";
4673
+ }
4674
+ const error = resolveWaitForFinishError({ status, final });
4675
+ this.emit({
4676
+ type: "wait_for_finish_response",
4677
+ payload: { requestId, status, final, error, lastMessage: result.lastMessage },
4678
+ });
4679
+ }
4680
+ catch (error) {
4681
+ const isAbort = error instanceof Error &&
4682
+ (error.name === "AbortError" || error.message.toLowerCase().includes("aborted"));
4683
+ if (!isAbort) {
4684
+ const message = errorToFriendlyMessage(error);
4685
+ this.sessionLogger.error({ err: error, agentId }, "wait_for_finish_request failed");
4686
+ const final = await this.getAgentPayloadById(agentId);
4687
+ this.emit({
4688
+ type: "wait_for_finish_response",
4689
+ payload: {
4690
+ requestId,
4691
+ status: "error",
4692
+ final,
4693
+ error: message,
4694
+ lastMessage: null,
4695
+ },
4696
+ });
4697
+ return;
4698
+ }
4699
+ const final = await this.getAgentPayloadById(agentId);
4700
+ if (!final) {
4701
+ throw new Error(`Agent ${agentId} disappeared while waiting`, { cause: error });
4702
+ }
4703
+ this.emit({
4704
+ type: "wait_for_finish_response",
4705
+ payload: { requestId, status: "timeout", final, error: null, lastMessage: null },
4706
+ });
4707
+ }
4708
+ finally {
4709
+ if (timeoutHandle) {
4710
+ clearTimeout(timeoutHandle);
4711
+ }
4712
+ }
4713
+ }
4714
+ /**
4715
+ * Emit a message to the client
4716
+ */
4717
+ emit(msg) {
4718
+ // JSON.stringify(msg) is only computed when trace is enabled — it runs for
4719
+ // every outbound message otherwise, and trace is disabled by default.
4720
+ // Optional-chained because test logger stubs don't implement isLevelEnabled.
4721
+ if (this.sessionLogger.isLevelEnabled?.("trace")) {
4722
+ this.sessionLogger.trace({
4723
+ messageType: msg.type,
4724
+ payloadBytes: JSON.stringify(msg).length,
4725
+ }, "agent.session.outbound");
4726
+ }
4727
+ this.onMessage(msg);
4728
+ }
4729
+ emitBinary(frame) {
4730
+ if (!this.onBinaryMessage) {
4731
+ return;
4732
+ }
4733
+ try {
4734
+ this.onBinaryMessage(frame);
4735
+ }
4736
+ catch (error) {
4737
+ this.sessionLogger.error({ err: error }, "Failed to emit binary frame");
4738
+ }
4739
+ }
4740
+ /**
4741
+ * Clean up session resources
4742
+ */
4743
+ async cleanup() {
4744
+ this.sessionLogger.trace({}, "agent.session.lifecycle.cleanup");
4745
+ if (this.unsubscribeAgentEvents) {
4746
+ this.unsubscribeAgentEvents();
4747
+ this.unsubscribeAgentEvents = null;
4748
+ }
4749
+ this.agentUpdates.dispose();
4750
+ if (this.unsubscribeTerminalWorkspaceContributionEvents) {
4751
+ this.unsubscribeTerminalWorkspaceContributionEvents();
4752
+ this.unsubscribeTerminalWorkspaceContributionEvents = null;
4753
+ }
4754
+ this.providerCatalogSession.dispose();
4755
+ await this.voiceSession.cleanup();
4756
+ this.terminalController.dispose();
4757
+ this.checkoutSession.cleanup();
4758
+ this.workspaceGitObserver.dispose();
4759
+ this.workspaceFilesSession.dispose();
4760
+ this.artifactSession.stop();
4761
+ }
4762
+ }
4763
+ //# sourceMappingURL=session.js.map