mstro-app 0.4.52 → 0.5.1

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 (590) hide show
  1. package/LICENSE +129 -190
  2. package/PRIVACY.md +3 -3
  3. package/README.md +15 -6
  4. package/bin/commands/config.js +0 -1
  5. package/bin/mstro.js +1 -2
  6. package/bin/postinstall.js +0 -1
  7. package/dist/server/cli/headless/claude-invoker-process.d.ts.map +1 -1
  8. package/dist/server/cli/headless/claude-invoker-process.js +0 -1
  9. package/dist/server/cli/headless/claude-invoker-process.js.map +1 -1
  10. package/dist/server/cli/headless/claude-invoker-stall.d.ts.map +1 -1
  11. package/dist/server/cli/headless/claude-invoker-stall.js +7 -3
  12. package/dist/server/cli/headless/claude-invoker-stall.js.map +1 -1
  13. package/dist/server/cli/headless/claude-invoker-stream.d.ts.map +1 -1
  14. package/dist/server/cli/headless/claude-invoker-stream.js +0 -1
  15. package/dist/server/cli/headless/claude-invoker-stream.js.map +1 -1
  16. package/dist/server/cli/headless/claude-invoker-tools.d.ts.map +1 -1
  17. package/dist/server/cli/headless/claude-invoker-tools.js +0 -1
  18. package/dist/server/cli/headless/claude-invoker-tools.js.map +1 -1
  19. package/dist/server/cli/headless/claude-invoker.d.ts.map +1 -1
  20. package/dist/server/cli/headless/claude-invoker.js +1 -2
  21. package/dist/server/cli/headless/claude-invoker.js.map +1 -1
  22. package/dist/server/cli/headless/haiku-assessments.d.ts.map +1 -1
  23. package/dist/server/cli/headless/haiku-assessments.js +0 -1
  24. package/dist/server/cli/headless/haiku-assessments.js.map +1 -1
  25. package/dist/server/cli/headless/headless-logger.d.ts.map +1 -1
  26. package/dist/server/cli/headless/headless-logger.js +0 -1
  27. package/dist/server/cli/headless/headless-logger.js.map +1 -1
  28. package/dist/server/cli/headless/index.d.ts.map +1 -1
  29. package/dist/server/cli/headless/index.js +0 -1
  30. package/dist/server/cli/headless/index.js.map +1 -1
  31. package/dist/server/cli/headless/native-timeout-detector.d.ts.map +1 -1
  32. package/dist/server/cli/headless/native-timeout-detector.js +0 -1
  33. package/dist/server/cli/headless/native-timeout-detector.js.map +1 -1
  34. package/dist/server/cli/headless/output-utils.d.ts.map +1 -1
  35. package/dist/server/cli/headless/output-utils.js +0 -1
  36. package/dist/server/cli/headless/output-utils.js.map +1 -1
  37. package/dist/server/cli/headless/prompt-utils.d.ts.map +1 -1
  38. package/dist/server/cli/headless/prompt-utils.js +0 -1
  39. package/dist/server/cli/headless/prompt-utils.js.map +1 -1
  40. package/dist/server/cli/headless/resilient-runner.d.ts.map +1 -1
  41. package/dist/server/cli/headless/resilient-runner.js +0 -1
  42. package/dist/server/cli/headless/resilient-runner.js.map +1 -1
  43. package/dist/server/cli/headless/retry-strategies.d.ts.map +1 -1
  44. package/dist/server/cli/headless/retry-strategies.js +0 -1
  45. package/dist/server/cli/headless/retry-strategies.js.map +1 -1
  46. package/dist/server/cli/headless/runner.d.ts.map +1 -1
  47. package/dist/server/cli/headless/runner.js +63 -68
  48. package/dist/server/cli/headless/runner.js.map +1 -1
  49. package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
  50. package/dist/server/cli/headless/stall-assessor.js +9 -5
  51. package/dist/server/cli/headless/stall-assessor.js.map +1 -1
  52. package/dist/server/cli/headless/tool-watchdog.d.ts.map +1 -1
  53. package/dist/server/cli/headless/tool-watchdog.js +0 -1
  54. package/dist/server/cli/headless/tool-watchdog.js.map +1 -1
  55. package/dist/server/cli/headless/types.d.ts.map +1 -1
  56. package/dist/server/cli/headless/types.js +0 -1
  57. package/dist/server/cli/headless/types.js.map +1 -1
  58. package/dist/server/cli/improvisation-attachments.d.ts.map +1 -1
  59. package/dist/server/cli/improvisation-attachments.js +0 -1
  60. package/dist/server/cli/improvisation-attachments.js.map +1 -1
  61. package/dist/server/cli/improvisation-history-store.d.ts +16 -0
  62. package/dist/server/cli/improvisation-history-store.d.ts.map +1 -0
  63. package/dist/server/cli/improvisation-history-store.js +51 -0
  64. package/dist/server/cli/improvisation-history-store.js.map +1 -0
  65. package/dist/server/cli/improvisation-movements.d.ts +31 -0
  66. package/dist/server/cli/improvisation-movements.d.ts.map +1 -0
  67. package/dist/server/cli/improvisation-movements.js +92 -0
  68. package/dist/server/cli/improvisation-movements.js.map +1 -0
  69. package/dist/server/cli/improvisation-output-queue.d.ts +13 -0
  70. package/dist/server/cli/improvisation-output-queue.d.ts.map +1 -0
  71. package/dist/server/cli/improvisation-output-queue.js +39 -0
  72. package/dist/server/cli/improvisation-output-queue.js.map +1 -0
  73. package/dist/server/cli/improvisation-retry.d.ts +21 -51
  74. package/dist/server/cli/improvisation-retry.d.ts.map +1 -1
  75. package/dist/server/cli/improvisation-retry.js +18 -434
  76. package/dist/server/cli/improvisation-retry.js.map +1 -1
  77. package/dist/server/cli/improvisation-session-manager.d.ts +10 -8
  78. package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
  79. package/dist/server/cli/improvisation-session-manager.js +53 -149
  80. package/dist/server/cli/improvisation-session-manager.js.map +1 -1
  81. package/dist/server/cli/improvisation-types.d.ts.map +1 -1
  82. package/dist/server/cli/improvisation-types.js +0 -1
  83. package/dist/server/cli/improvisation-types.js.map +1 -1
  84. package/dist/server/cli/retry/retry-best-result.d.ts +4 -0
  85. package/dist/server/cli/retry/retry-best-result.d.ts.map +1 -0
  86. package/dist/server/cli/retry/retry-best-result.js +60 -0
  87. package/dist/server/cli/retry/retry-best-result.js.map +1 -0
  88. package/dist/server/cli/retry/retry-context-loss.d.ts +6 -0
  89. package/dist/server/cli/retry/retry-context-loss.d.ts.map +1 -0
  90. package/dist/server/cli/retry/retry-context-loss.js +67 -0
  91. package/dist/server/cli/retry/retry-context-loss.js.map +1 -0
  92. package/dist/server/cli/retry/retry-premature-completion.d.ts +5 -0
  93. package/dist/server/cli/retry/retry-premature-completion.d.ts.map +1 -0
  94. package/dist/server/cli/retry/retry-premature-completion.js +80 -0
  95. package/dist/server/cli/retry/retry-premature-completion.js.map +1 -0
  96. package/dist/server/cli/retry/retry-recovery-strategies.d.ts +13 -0
  97. package/dist/server/cli/retry/retry-recovery-strategies.d.ts.map +1 -0
  98. package/dist/server/cli/retry/retry-recovery-strategies.js +165 -0
  99. package/dist/server/cli/retry/retry-recovery-strategies.js.map +1 -0
  100. package/dist/server/cli/retry/retry-resume-strategy.d.ts +12 -0
  101. package/dist/server/cli/retry/retry-resume-strategy.d.ts.map +1 -0
  102. package/dist/server/cli/retry/retry-resume-strategy.js +21 -0
  103. package/dist/server/cli/retry/retry-resume-strategy.js.map +1 -0
  104. package/dist/server/cli/retry/retry-runner-factory.d.ts +11 -0
  105. package/dist/server/cli/retry/retry-runner-factory.d.ts.map +1 -0
  106. package/dist/server/cli/retry/retry-runner-factory.js +59 -0
  107. package/dist/server/cli/retry/retry-runner-factory.js.map +1 -0
  108. package/dist/server/cli/retry/retry-tool-results.d.ts +9 -0
  109. package/dist/server/cli/retry/retry-tool-results.d.ts.map +1 -0
  110. package/dist/server/cli/retry/retry-tool-results.js +23 -0
  111. package/dist/server/cli/retry/retry-tool-results.js.map +1 -0
  112. package/dist/server/cli/retry/retry-types.d.ts +30 -0
  113. package/dist/server/cli/retry/retry-types.d.ts.map +1 -0
  114. package/dist/server/cli/retry/retry-types.js +3 -0
  115. package/dist/server/cli/retry/retry-types.js.map +1 -0
  116. package/dist/server/index.js +21 -110
  117. package/dist/server/index.js.map +1 -1
  118. package/dist/server/mcp/bouncer-cli.js +0 -1
  119. package/dist/server/mcp/bouncer-cli.js.map +1 -1
  120. package/dist/server/mcp/bouncer-haiku.d.ts.map +1 -1
  121. package/dist/server/mcp/bouncer-haiku.js +0 -1
  122. package/dist/server/mcp/bouncer-haiku.js.map +1 -1
  123. package/dist/server/mcp/bouncer-integration.d.ts.map +1 -1
  124. package/dist/server/mcp/bouncer-integration.js +0 -1
  125. package/dist/server/mcp/bouncer-integration.js.map +1 -1
  126. package/dist/server/mcp/security-analysis.d.ts.map +1 -1
  127. package/dist/server/mcp/security-analysis.js +0 -1
  128. package/dist/server/mcp/security-analysis.js.map +1 -1
  129. package/dist/server/mcp/security-audit.d.ts.map +1 -1
  130. package/dist/server/mcp/security-audit.js +0 -1
  131. package/dist/server/mcp/security-audit.js.map +1 -1
  132. package/dist/server/mcp/security-patterns.d.ts.map +1 -1
  133. package/dist/server/mcp/security-patterns.js +0 -1
  134. package/dist/server/mcp/security-patterns.js.map +1 -1
  135. package/dist/server/mcp/server.js +0 -1
  136. package/dist/server/mcp/server.js.map +1 -1
  137. package/dist/server/routes/files.d.ts.map +1 -1
  138. package/dist/server/routes/files.js +0 -1
  139. package/dist/server/routes/files.js.map +1 -1
  140. package/dist/server/routes/improvise.d.ts.map +1 -1
  141. package/dist/server/routes/improvise.js +0 -1
  142. package/dist/server/routes/improvise.js.map +1 -1
  143. package/dist/server/routes/index.d.ts.map +1 -1
  144. package/dist/server/routes/index.js +0 -1
  145. package/dist/server/routes/index.js.map +1 -1
  146. package/dist/server/routes/instances.d.ts.map +1 -1
  147. package/dist/server/routes/instances.js +0 -1
  148. package/dist/server/routes/instances.js.map +1 -1
  149. package/dist/server/routes/notifications.d.ts.map +1 -1
  150. package/dist/server/routes/notifications.js +0 -1
  151. package/dist/server/routes/notifications.js.map +1 -1
  152. package/dist/server/server-setup.d.ts +16 -1
  153. package/dist/server/server-setup.d.ts.map +1 -1
  154. package/dist/server/server-setup.js +107 -1
  155. package/dist/server/server-setup.js.map +1 -1
  156. package/dist/server/services/analytics.d.ts.map +1 -1
  157. package/dist/server/services/analytics.js +0 -1
  158. package/dist/server/services/analytics.js.map +1 -1
  159. package/dist/server/services/auth.d.ts.map +1 -1
  160. package/dist/server/services/auth.js +0 -1
  161. package/dist/server/services/auth.js.map +1 -1
  162. package/dist/server/services/client-id.d.ts.map +1 -1
  163. package/dist/server/services/client-id.js +0 -1
  164. package/dist/server/services/client-id.js.map +1 -1
  165. package/dist/server/services/file-explorer-ops.d.ts.map +1 -1
  166. package/dist/server/services/file-explorer-ops.js +0 -1
  167. package/dist/server/services/file-explorer-ops.js.map +1 -1
  168. package/dist/server/services/files.d.ts.map +1 -1
  169. package/dist/server/services/files.js +0 -1
  170. package/dist/server/services/files.js.map +1 -1
  171. package/dist/server/services/instances.d.ts.map +1 -1
  172. package/dist/server/services/instances.js +0 -1
  173. package/dist/server/services/instances.js.map +1 -1
  174. package/dist/server/services/pathUtils.d.ts.map +1 -1
  175. package/dist/server/services/pathUtils.js +0 -1
  176. package/dist/server/services/pathUtils.js.map +1 -1
  177. package/dist/server/services/plan/agent-loader.d.ts.map +1 -1
  178. package/dist/server/services/plan/agent-loader.js +0 -1
  179. package/dist/server/services/plan/agent-loader.js.map +1 -1
  180. package/dist/server/services/plan/board-config.d.ts +21 -0
  181. package/dist/server/services/plan/board-config.d.ts.map +1 -0
  182. package/dist/server/services/plan/board-config.js +111 -0
  183. package/dist/server/services/plan/board-config.js.map +1 -0
  184. package/dist/server/services/plan/composer.d.ts +1 -1
  185. package/dist/server/services/plan/composer.d.ts.map +1 -1
  186. package/dist/server/services/plan/composer.js +7 -6
  187. package/dist/server/services/plan/composer.js.map +1 -1
  188. package/dist/server/services/plan/config-installer.d.ts.map +1 -1
  189. package/dist/server/services/plan/config-installer.js +0 -1
  190. package/dist/server/services/plan/config-installer.js.map +1 -1
  191. package/dist/server/services/plan/dependency-resolver.d.ts.map +1 -1
  192. package/dist/server/services/plan/dependency-resolver.js +0 -1
  193. package/dist/server/services/plan/dependency-resolver.js.map +1 -1
  194. package/dist/server/services/plan/executor.d.ts +48 -48
  195. package/dist/server/services/plan/executor.d.ts.map +1 -1
  196. package/dist/server/services/plan/executor.js +202 -458
  197. package/dist/server/services/plan/executor.js.map +1 -1
  198. package/dist/server/services/plan/front-matter.d.ts.map +1 -1
  199. package/dist/server/services/plan/front-matter.js +0 -1
  200. package/dist/server/services/plan/front-matter.js.map +1 -1
  201. package/dist/server/services/plan/issue-classification.d.ts.map +1 -1
  202. package/dist/server/services/plan/issue-classification.js +0 -1
  203. package/dist/server/services/plan/issue-classification.js.map +1 -1
  204. package/dist/server/services/plan/issue-loader.d.ts +16 -0
  205. package/dist/server/services/plan/issue-loader.d.ts.map +1 -0
  206. package/dist/server/services/plan/issue-loader.js +45 -0
  207. package/dist/server/services/plan/issue-loader.js.map +1 -0
  208. package/dist/server/services/plan/issue-prompt-builder.d.ts.map +1 -1
  209. package/dist/server/services/plan/issue-prompt-builder.js +0 -1
  210. package/dist/server/services/plan/issue-prompt-builder.js.map +1 -1
  211. package/dist/server/services/plan/issue-retry.d.ts +3 -1
  212. package/dist/server/services/plan/issue-retry.d.ts.map +1 -1
  213. package/dist/server/services/plan/issue-retry.js +2 -1
  214. package/dist/server/services/plan/issue-retry.js.map +1 -1
  215. package/dist/server/services/plan/issue-writer.d.ts +34 -0
  216. package/dist/server/services/plan/issue-writer.d.ts.map +1 -0
  217. package/dist/server/services/plan/issue-writer.js +109 -0
  218. package/dist/server/services/plan/issue-writer.js.map +1 -0
  219. package/dist/server/services/plan/output-manager.js +2 -2
  220. package/dist/server/services/plan/output-manager.js.map +1 -1
  221. package/dist/server/services/plan/parser-core.d.ts.map +1 -1
  222. package/dist/server/services/plan/parser-core.js +0 -1
  223. package/dist/server/services/plan/parser-core.js.map +1 -1
  224. package/dist/server/services/plan/parser-migration.d.ts.map +1 -1
  225. package/dist/server/services/plan/parser-migration.js +0 -1
  226. package/dist/server/services/plan/parser-migration.js.map +1 -1
  227. package/dist/server/services/plan/parser.d.ts.map +1 -1
  228. package/dist/server/services/plan/parser.js +0 -1
  229. package/dist/server/services/plan/parser.js.map +1 -1
  230. package/dist/server/services/plan/progress-log.d.ts +11 -0
  231. package/dist/server/services/plan/progress-log.d.ts.map +1 -0
  232. package/dist/server/services/plan/progress-log.js +80 -0
  233. package/dist/server/services/plan/progress-log.js.map +1 -0
  234. package/dist/server/services/plan/prompt-builder.d.ts.map +1 -1
  235. package/dist/server/services/plan/prompt-builder.js +48 -32
  236. package/dist/server/services/plan/prompt-builder.js.map +1 -1
  237. package/dist/server/services/plan/readiness-planner.d.ts +15 -0
  238. package/dist/server/services/plan/readiness-planner.d.ts.map +1 -0
  239. package/dist/server/services/plan/readiness-planner.js +40 -0
  240. package/dist/server/services/plan/readiness-planner.js.map +1 -0
  241. package/dist/server/services/plan/review-gate.d.ts +31 -0
  242. package/dist/server/services/plan/review-gate.d.ts.map +1 -1
  243. package/dist/server/services/plan/review-gate.js +52 -3
  244. package/dist/server/services/plan/review-gate.js.map +1 -1
  245. package/dist/server/services/plan/state-reconciler.d.ts.map +1 -1
  246. package/dist/server/services/plan/state-reconciler.js +0 -1
  247. package/dist/server/services/plan/state-reconciler.js.map +1 -1
  248. package/dist/server/services/plan/types.d.ts.map +1 -1
  249. package/dist/server/services/plan/types.js +0 -1
  250. package/dist/server/services/plan/types.js.map +1 -1
  251. package/dist/server/services/plan/watcher.d.ts.map +1 -1
  252. package/dist/server/services/plan/watcher.js +0 -1
  253. package/dist/server/services/plan/watcher.js.map +1 -1
  254. package/dist/server/services/platform-credentials.d.ts.map +1 -1
  255. package/dist/server/services/platform-credentials.js +0 -1
  256. package/dist/server/services/platform-credentials.js.map +1 -1
  257. package/dist/server/services/platform-token-lifecycle.d.ts +70 -0
  258. package/dist/server/services/platform-token-lifecycle.d.ts.map +1 -0
  259. package/dist/server/services/platform-token-lifecycle.js +156 -0
  260. package/dist/server/services/platform-token-lifecycle.js.map +1 -0
  261. package/dist/server/services/platform.d.ts +25 -4
  262. package/dist/server/services/platform.d.ts.map +1 -1
  263. package/dist/server/services/platform.js +150 -92
  264. package/dist/server/services/platform.js.map +1 -1
  265. package/dist/server/services/sentry.d.ts.map +1 -1
  266. package/dist/server/services/sentry.js +0 -1
  267. package/dist/server/services/sentry.js.map +1 -1
  268. package/dist/server/services/settings.d.ts.map +1 -1
  269. package/dist/server/services/settings.js +0 -1
  270. package/dist/server/services/settings.js.map +1 -1
  271. package/dist/server/services/terminal/pty-manager.d.ts.map +1 -1
  272. package/dist/server/services/terminal/pty-manager.js +0 -1
  273. package/dist/server/services/terminal/pty-manager.js.map +1 -1
  274. package/dist/server/services/terminal/pty-utils.d.ts.map +1 -1
  275. package/dist/server/services/terminal/pty-utils.js +0 -1
  276. package/dist/server/services/terminal/pty-utils.js.map +1 -1
  277. package/dist/server/services/websocket/autocomplete.d.ts.map +1 -1
  278. package/dist/server/services/websocket/autocomplete.js +0 -1
  279. package/dist/server/services/websocket/autocomplete.js.map +1 -1
  280. package/dist/server/services/websocket/file-definition-handlers.d.ts.map +1 -1
  281. package/dist/server/services/websocket/file-definition-handlers.js +0 -1
  282. package/dist/server/services/websocket/file-definition-handlers.js.map +1 -1
  283. package/dist/server/services/websocket/file-download-handler.d.ts +17 -0
  284. package/dist/server/services/websocket/file-download-handler.d.ts.map +1 -0
  285. package/dist/server/services/websocket/file-download-handler.js +164 -0
  286. package/dist/server/services/websocket/file-download-handler.js.map +1 -0
  287. package/dist/server/services/websocket/file-explorer-handlers.d.ts.map +1 -1
  288. package/dist/server/services/websocket/file-explorer-handlers.js +0 -1
  289. package/dist/server/services/websocket/file-explorer-handlers.js.map +1 -1
  290. package/dist/server/services/websocket/file-search-handlers.d.ts.map +1 -1
  291. package/dist/server/services/websocket/file-search-handlers.js +0 -1
  292. package/dist/server/services/websocket/file-search-handlers.js.map +1 -1
  293. package/dist/server/services/websocket/file-upload-handler.d.ts +2 -3
  294. package/dist/server/services/websocket/file-upload-handler.d.ts.map +1 -1
  295. package/dist/server/services/websocket/file-upload-handler.js +4 -7
  296. package/dist/server/services/websocket/file-upload-handler.js.map +1 -1
  297. package/dist/server/services/websocket/file-utils.d.ts.map +1 -1
  298. package/dist/server/services/websocket/file-utils.js +0 -1
  299. package/dist/server/services/websocket/file-utils.js.map +1 -1
  300. package/dist/server/services/websocket/git-branch-handlers.d.ts.map +1 -1
  301. package/dist/server/services/websocket/git-branch-handlers.js +0 -1
  302. package/dist/server/services/websocket/git-branch-handlers.js.map +1 -1
  303. package/dist/server/services/websocket/git-diff-handlers.d.ts.map +1 -1
  304. package/dist/server/services/websocket/git-diff-handlers.js +0 -1
  305. package/dist/server/services/websocket/git-diff-handlers.js.map +1 -1
  306. package/dist/server/services/websocket/git-handlers.d.ts.map +1 -1
  307. package/dist/server/services/websocket/git-handlers.js +58 -6
  308. package/dist/server/services/websocket/git-handlers.js.map +1 -1
  309. package/dist/server/services/websocket/git-head-watcher.d.ts.map +1 -1
  310. package/dist/server/services/websocket/git-head-watcher.js +0 -1
  311. package/dist/server/services/websocket/git-head-watcher.js.map +1 -1
  312. package/dist/server/services/websocket/git-log-handlers.d.ts.map +1 -1
  313. package/dist/server/services/websocket/git-log-handlers.js +0 -1
  314. package/dist/server/services/websocket/git-log-handlers.js.map +1 -1
  315. package/dist/server/services/websocket/git-pr-handlers.d.ts.map +1 -1
  316. package/dist/server/services/websocket/git-pr-handlers.js +0 -1
  317. package/dist/server/services/websocket/git-pr-handlers.js.map +1 -1
  318. package/dist/server/services/websocket/git-tag-handlers.d.ts.map +1 -1
  319. package/dist/server/services/websocket/git-tag-handlers.js +0 -1
  320. package/dist/server/services/websocket/git-tag-handlers.js.map +1 -1
  321. package/dist/server/services/websocket/git-utils.d.ts +18 -3
  322. package/dist/server/services/websocket/git-utils.d.ts.map +1 -1
  323. package/dist/server/services/websocket/git-utils.js +58 -8
  324. package/dist/server/services/websocket/git-utils.js.map +1 -1
  325. package/dist/server/services/websocket/git-worktree-handlers.d.ts.map +1 -1
  326. package/dist/server/services/websocket/git-worktree-handlers.js +258 -16
  327. package/dist/server/services/websocket/git-worktree-handlers.js.map +1 -1
  328. package/dist/server/services/websocket/handler-context.d.ts +15 -0
  329. package/dist/server/services/websocket/handler-context.d.ts.map +1 -1
  330. package/dist/server/services/websocket/handler-context.js +0 -1
  331. package/dist/server/services/websocket/handler-context.js.map +1 -1
  332. package/dist/server/services/websocket/handler.d.ts +7 -0
  333. package/dist/server/services/websocket/handler.d.ts.map +1 -1
  334. package/dist/server/services/websocket/handler.js +76 -15
  335. package/dist/server/services/websocket/handler.js.map +1 -1
  336. package/dist/server/services/websocket/index.d.ts.map +1 -1
  337. package/dist/server/services/websocket/index.js +0 -1
  338. package/dist/server/services/websocket/index.js.map +1 -1
  339. package/dist/server/services/websocket/msg-id-tracker.d.ts +21 -0
  340. package/dist/server/services/websocket/msg-id-tracker.d.ts.map +1 -0
  341. package/dist/server/services/websocket/msg-id-tracker.js +76 -0
  342. package/dist/server/services/websocket/msg-id-tracker.js.map +1 -0
  343. package/dist/server/services/websocket/plan-board-handlers.d.ts.map +1 -1
  344. package/dist/server/services/websocket/plan-board-handlers.js +0 -1
  345. package/dist/server/services/websocket/plan-board-handlers.js.map +1 -1
  346. package/dist/server/services/websocket/plan-execution-handlers.d.ts.map +1 -1
  347. package/dist/server/services/websocket/plan-execution-handlers.js +6 -2
  348. package/dist/server/services/websocket/plan-execution-handlers.js.map +1 -1
  349. package/dist/server/services/websocket/plan-handlers.d.ts.map +1 -1
  350. package/dist/server/services/websocket/plan-handlers.js +0 -1
  351. package/dist/server/services/websocket/plan-handlers.js.map +1 -1
  352. package/dist/server/services/websocket/plan-helpers.d.ts.map +1 -1
  353. package/dist/server/services/websocket/plan-helpers.js +0 -1
  354. package/dist/server/services/websocket/plan-helpers.js.map +1 -1
  355. package/dist/server/services/websocket/plan-issue-handlers.d.ts.map +1 -1
  356. package/dist/server/services/websocket/plan-issue-handlers.js +0 -1
  357. package/dist/server/services/websocket/plan-issue-handlers.js.map +1 -1
  358. package/dist/server/services/websocket/plan-sprint-handlers.d.ts.map +1 -1
  359. package/dist/server/services/websocket/plan-sprint-handlers.js +0 -1
  360. package/dist/server/services/websocket/plan-sprint-handlers.js.map +1 -1
  361. package/dist/server/services/websocket/quality-complexity.d.ts.map +1 -1
  362. package/dist/server/services/websocket/quality-complexity.js +0 -1
  363. package/dist/server/services/websocket/quality-complexity.js.map +1 -1
  364. package/dist/server/services/websocket/quality-grading.d.ts +46 -0
  365. package/dist/server/services/websocket/quality-grading.d.ts.map +1 -0
  366. package/dist/server/services/websocket/quality-grading.js +482 -0
  367. package/dist/server/services/websocket/quality-grading.js.map +1 -0
  368. package/dist/server/services/websocket/quality-handlers.d.ts.map +1 -1
  369. package/dist/server/services/websocket/quality-handlers.js +15 -4
  370. package/dist/server/services/websocket/quality-handlers.js.map +1 -1
  371. package/dist/server/services/websocket/quality-linting.d.ts.map +1 -1
  372. package/dist/server/services/websocket/quality-linting.js +0 -1
  373. package/dist/server/services/websocket/quality-linting.js.map +1 -1
  374. package/dist/server/services/websocket/quality-persistence.d.ts +14 -0
  375. package/dist/server/services/websocket/quality-persistence.d.ts.map +1 -1
  376. package/dist/server/services/websocket/quality-persistence.js +28 -12
  377. package/dist/server/services/websocket/quality-persistence.js.map +1 -1
  378. package/dist/server/services/websocket/quality-review-agent.d.ts.map +1 -1
  379. package/dist/server/services/websocket/quality-review-agent.js +2 -3
  380. package/dist/server/services/websocket/quality-review-agent.js.map +1 -1
  381. package/dist/server/services/websocket/quality-service.d.ts +3 -1
  382. package/dist/server/services/websocket/quality-service.d.ts.map +1 -1
  383. package/dist/server/services/websocket/quality-service.js +53 -58
  384. package/dist/server/services/websocket/quality-service.js.map +1 -1
  385. package/dist/server/services/websocket/quality-tools.d.ts +1 -1
  386. package/dist/server/services/websocket/quality-tools.d.ts.map +1 -1
  387. package/dist/server/services/websocket/quality-tools.js +6 -3
  388. package/dist/server/services/websocket/quality-tools.js.map +1 -1
  389. package/dist/server/services/websocket/quality-types.d.ts +18 -2
  390. package/dist/server/services/websocket/quality-types.d.ts.map +1 -1
  391. package/dist/server/services/websocket/quality-types.js +0 -1
  392. package/dist/server/services/websocket/quality-types.js.map +1 -1
  393. package/dist/server/services/websocket/session-handlers.d.ts +48 -2
  394. package/dist/server/services/websocket/session-handlers.d.ts.map +1 -1
  395. package/dist/server/services/websocket/session-handlers.js +204 -66
  396. package/dist/server/services/websocket/session-handlers.js.map +1 -1
  397. package/dist/server/services/websocket/session-history.d.ts.map +1 -1
  398. package/dist/server/services/websocket/session-history.js +0 -1
  399. package/dist/server/services/websocket/session-history.js.map +1 -1
  400. package/dist/server/services/websocket/session-initialization.d.ts +2 -2
  401. package/dist/server/services/websocket/session-initialization.d.ts.map +1 -1
  402. package/dist/server/services/websocket/session-initialization.js +75 -18
  403. package/dist/server/services/websocket/session-initialization.js.map +1 -1
  404. package/dist/server/services/websocket/session-registry.d.ts +29 -1
  405. package/dist/server/services/websocket/session-registry.d.ts.map +1 -1
  406. package/dist/server/services/websocket/session-registry.js +53 -5
  407. package/dist/server/services/websocket/session-registry.js.map +1 -1
  408. package/dist/server/services/websocket/settings-handlers.d.ts.map +1 -1
  409. package/dist/server/services/websocket/settings-handlers.js +0 -1
  410. package/dist/server/services/websocket/settings-handlers.js.map +1 -1
  411. package/dist/server/services/websocket/skill-handlers.d.ts.map +1 -1
  412. package/dist/server/services/websocket/skill-handlers.js +0 -1
  413. package/dist/server/services/websocket/skill-handlers.js.map +1 -1
  414. package/dist/server/services/websocket/skill-watcher.d.ts.map +1 -1
  415. package/dist/server/services/websocket/skill-watcher.js +0 -1
  416. package/dist/server/services/websocket/skill-watcher.js.map +1 -1
  417. package/dist/server/services/websocket/tab-broadcast.d.ts +24 -0
  418. package/dist/server/services/websocket/tab-broadcast.d.ts.map +1 -0
  419. package/dist/server/services/websocket/tab-broadcast.js +12 -0
  420. package/dist/server/services/websocket/tab-broadcast.js.map +1 -0
  421. package/dist/server/services/websocket/tab-event-buffer.d.ts +103 -0
  422. package/dist/server/services/websocket/tab-event-buffer.d.ts.map +1 -0
  423. package/dist/server/services/websocket/tab-event-buffer.js +106 -0
  424. package/dist/server/services/websocket/tab-event-buffer.js.map +1 -0
  425. package/dist/server/services/websocket/tab-event-replay.d.ts +20 -0
  426. package/dist/server/services/websocket/tab-event-replay.d.ts.map +1 -0
  427. package/dist/server/services/websocket/tab-event-replay.js +20 -0
  428. package/dist/server/services/websocket/tab-event-replay.js.map +1 -0
  429. package/dist/server/services/websocket/tab-handlers.d.ts +0 -1
  430. package/dist/server/services/websocket/tab-handlers.d.ts.map +1 -1
  431. package/dist/server/services/websocket/tab-handlers.js +2 -10
  432. package/dist/server/services/websocket/tab-handlers.js.map +1 -1
  433. package/dist/server/services/websocket/terminal-handlers.d.ts.map +1 -1
  434. package/dist/server/services/websocket/terminal-handlers.js +39 -4
  435. package/dist/server/services/websocket/terminal-handlers.js.map +1 -1
  436. package/dist/server/services/websocket/types.d.ts +17 -8
  437. package/dist/server/services/websocket/types.d.ts.map +1 -1
  438. package/dist/server/services/websocket/types.js +8 -7
  439. package/dist/server/services/websocket/types.js.map +1 -1
  440. package/dist/server/utils/agent-manager.d.ts.map +1 -1
  441. package/dist/server/utils/agent-manager.js +0 -1
  442. package/dist/server/utils/agent-manager.js.map +1 -1
  443. package/dist/server/utils/paths.d.ts.map +1 -1
  444. package/dist/server/utils/paths.js +0 -1
  445. package/dist/server/utils/paths.js.map +1 -1
  446. package/dist/server/utils/port-manager.d.ts.map +1 -1
  447. package/dist/server/utils/port-manager.js +0 -1
  448. package/dist/server/utils/port-manager.js.map +1 -1
  449. package/dist/server/utils/port.d.ts.map +1 -1
  450. package/dist/server/utils/port.js +0 -1
  451. package/dist/server/utils/port.js.map +1 -1
  452. package/package.json +2 -2
  453. package/server/README.md +1 -1
  454. package/server/cli/headless/claude-invoker-process.ts +0 -1
  455. package/server/cli/headless/claude-invoker-stall.ts +7 -3
  456. package/server/cli/headless/claude-invoker-stream.ts +0 -1
  457. package/server/cli/headless/claude-invoker-tools.ts +0 -1
  458. package/server/cli/headless/claude-invoker.ts +1 -2
  459. package/server/cli/headless/haiku-assessments.ts +0 -1
  460. package/server/cli/headless/headless-logger.ts +0 -1
  461. package/server/cli/headless/index.ts +0 -1
  462. package/server/cli/headless/native-timeout-detector.ts +0 -1
  463. package/server/cli/headless/output-utils.ts +0 -1
  464. package/server/cli/headless/prompt-utils.ts +0 -1
  465. package/server/cli/headless/resilient-runner.ts +0 -1
  466. package/server/cli/headless/retry-strategies.ts +0 -1
  467. package/server/cli/headless/runner.ts +67 -73
  468. package/server/cli/headless/stall-assessor.ts +9 -5
  469. package/server/cli/headless/tool-watchdog.ts +0 -1
  470. package/server/cli/headless/types.ts +1 -2
  471. package/server/cli/improvisation-attachments.ts +0 -1
  472. package/server/cli/improvisation-history-store.ts +61 -0
  473. package/server/cli/improvisation-movements.ts +119 -0
  474. package/server/cli/improvisation-output-queue.ts +41 -0
  475. package/server/cli/improvisation-retry.ts +25 -601
  476. package/server/cli/improvisation-session-manager.ts +74 -161
  477. package/server/cli/improvisation-types.ts +0 -1
  478. package/server/cli/retry/retry-best-result.ts +69 -0
  479. package/server/cli/retry/retry-context-loss.ts +86 -0
  480. package/server/cli/retry/retry-premature-completion.ts +112 -0
  481. package/server/cli/retry/retry-recovery-strategies.ts +246 -0
  482. package/server/cli/retry/retry-resume-strategy.ts +32 -0
  483. package/server/cli/retry/retry-runner-factory.ts +69 -0
  484. package/server/cli/retry/retry-tool-results.ts +30 -0
  485. package/server/cli/retry/retry-types.ts +31 -0
  486. package/server/index.ts +37 -124
  487. package/server/mcp/bouncer-cli.ts +0 -1
  488. package/server/mcp/bouncer-haiku.ts +0 -1
  489. package/server/mcp/bouncer-integration.ts +0 -1
  490. package/server/mcp/security-analysis.ts +0 -1
  491. package/server/mcp/security-audit.ts +0 -1
  492. package/server/mcp/security-patterns.ts +0 -1
  493. package/server/mcp/server.ts +0 -1
  494. package/server/routes/files.ts +0 -1
  495. package/server/routes/improvise.ts +0 -1
  496. package/server/routes/index.ts +0 -1
  497. package/server/routes/instances.ts +0 -1
  498. package/server/routes/notifications.ts +0 -1
  499. package/server/server-setup.ts +126 -2
  500. package/server/services/analytics.ts +0 -1
  501. package/server/services/auth.ts +0 -1
  502. package/server/services/client-id.ts +0 -1
  503. package/server/services/file-explorer-ops.ts +0 -1
  504. package/server/services/files.ts +0 -1
  505. package/server/services/instances.ts +0 -1
  506. package/server/services/pathUtils.ts +0 -1
  507. package/server/services/plan/agent-loader.ts +0 -1
  508. package/server/services/plan/agents/assess-stall.md +11 -4
  509. package/server/services/plan/agents/code-review.md +13 -11
  510. package/server/services/plan/board-config.ts +121 -0
  511. package/server/services/plan/composer.ts +7 -6
  512. package/server/services/plan/config-installer.ts +0 -1
  513. package/server/services/plan/dependency-resolver.ts +0 -1
  514. package/server/services/plan/executor.ts +259 -470
  515. package/server/services/plan/front-matter.ts +0 -1
  516. package/server/services/plan/issue-classification.ts +0 -1
  517. package/server/services/plan/issue-loader.ts +63 -0
  518. package/server/services/plan/issue-prompt-builder.ts +0 -1
  519. package/server/services/plan/issue-retry.ts +5 -2
  520. package/server/services/plan/issue-writer.ts +136 -0
  521. package/server/services/plan/output-manager.ts +2 -2
  522. package/server/services/plan/parser-core.ts +0 -1
  523. package/server/services/plan/parser-migration.ts +0 -1
  524. package/server/services/plan/parser.ts +0 -1
  525. package/server/services/plan/progress-log.ts +91 -0
  526. package/server/services/plan/prompt-builder.ts +73 -36
  527. package/server/services/plan/readiness-planner.ts +49 -0
  528. package/server/services/plan/review-gate.ts +102 -3
  529. package/server/services/plan/state-reconciler.ts +0 -1
  530. package/server/services/plan/types.ts +0 -1
  531. package/server/services/plan/watcher.ts +0 -1
  532. package/server/services/platform-credentials.ts +0 -1
  533. package/server/services/platform-token-lifecycle.ts +171 -0
  534. package/server/services/platform.ts +168 -105
  535. package/server/services/sentry.ts +0 -1
  536. package/server/services/settings.ts +0 -1
  537. package/server/services/terminal/pty-manager.ts +0 -1
  538. package/server/services/terminal/pty-utils.ts +0 -1
  539. package/server/services/websocket/autocomplete.ts +0 -1
  540. package/server/services/websocket/file-definition-handlers.ts +0 -1
  541. package/server/services/websocket/file-download-handler.ts +190 -0
  542. package/server/services/websocket/file-explorer-handlers.ts +0 -1
  543. package/server/services/websocket/file-search-handlers.ts +0 -1
  544. package/server/services/websocket/file-upload-handler.ts +6 -5
  545. package/server/services/websocket/file-utils.ts +0 -1
  546. package/server/services/websocket/git-branch-handlers.ts +0 -1
  547. package/server/services/websocket/git-diff-handlers.ts +0 -1
  548. package/server/services/websocket/git-handlers.ts +66 -10
  549. package/server/services/websocket/git-head-watcher.ts +0 -1
  550. package/server/services/websocket/git-log-handlers.ts +0 -1
  551. package/server/services/websocket/git-pr-handlers.ts +0 -1
  552. package/server/services/websocket/git-tag-handlers.ts +0 -1
  553. package/server/services/websocket/git-utils.ts +69 -9
  554. package/server/services/websocket/git-worktree-handlers.ts +289 -19
  555. package/server/services/websocket/handler-context.ts +15 -1
  556. package/server/services/websocket/handler.ts +79 -16
  557. package/server/services/websocket/index.ts +0 -1
  558. package/server/services/websocket/msg-id-tracker.ts +83 -0
  559. package/server/services/websocket/plan-board-handlers.ts +0 -1
  560. package/server/services/websocket/plan-execution-handlers.ts +6 -2
  561. package/server/services/websocket/plan-handlers.ts +0 -1
  562. package/server/services/websocket/plan-helpers.ts +0 -1
  563. package/server/services/websocket/plan-issue-handlers.ts +0 -1
  564. package/server/services/websocket/plan-sprint-handlers.ts +0 -1
  565. package/server/services/websocket/quality-complexity.ts +0 -1
  566. package/server/services/websocket/quality-grading.ts +611 -0
  567. package/server/services/websocket/quality-handlers.ts +16 -4
  568. package/server/services/websocket/quality-linting.ts +0 -1
  569. package/server/services/websocket/quality-persistence.ts +30 -8
  570. package/server/services/websocket/quality-review-agent.ts +2 -3
  571. package/server/services/websocket/quality-service.ts +54 -55
  572. package/server/services/websocket/quality-tools.ts +11 -3
  573. package/server/services/websocket/quality-types.ts +21 -3
  574. package/server/services/websocket/session-handlers.ts +213 -69
  575. package/server/services/websocket/session-history.ts +0 -1
  576. package/server/services/websocket/session-initialization.ts +83 -20
  577. package/server/services/websocket/session-registry.ts +61 -5
  578. package/server/services/websocket/settings-handlers.ts +0 -1
  579. package/server/services/websocket/skill-handlers.ts +0 -1
  580. package/server/services/websocket/skill-watcher.ts +0 -1
  581. package/server/services/websocket/tab-broadcast.ts +37 -0
  582. package/server/services/websocket/tab-event-buffer.ts +158 -0
  583. package/server/services/websocket/tab-event-replay.ts +41 -0
  584. package/server/services/websocket/tab-handlers.ts +2 -10
  585. package/server/services/websocket/terminal-handlers.ts +39 -3
  586. package/server/services/websocket/types.ts +19 -7
  587. package/server/utils/agent-manager.ts +0 -1
  588. package/server/utils/paths.ts +0 -1
  589. package/server/utils/port-manager.ts +0 -1
  590. package/server/utils/port.ts +0 -1
@@ -1,11 +1,12 @@
1
1
  // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
- // Licensed under the MIT License. See LICENSE file for details.
3
2
 
4
- import type { FileAttachment, ImprovisationSessionManager } from '../../cli/improvisation-session-manager.js';
3
+ import { type FileAttachment, ImprovisationSessionManager } from '../../cli/improvisation-session-manager.js';
5
4
  import { getEffortLevel, getModel } from '../settings.js';
6
5
  import type { HandlerContext } from './handler-context.js';
7
6
  import { runQualityScan } from './quality-service.js';
7
+ import type { SessionRegistry } from './session-registry.js';
8
8
  import { resolveSkillPrompt } from './skill-handlers.js';
9
+ import { broadcastTabEvent } from './tab-broadcast.js';
9
10
  import type { WebSocketMessage, WSContext } from './types.js';
10
11
 
11
12
  // Re-export from extracted modules for backward compatibility
@@ -43,6 +44,17 @@ function convertToolHistoryToLines(tools: Array<{ toolName: string; toolInput?:
43
44
  return lines;
44
45
  }
45
46
 
47
+ function formatElapsedDuration(totalSeconds: number): string {
48
+ const seconds = Math.floor(totalSeconds) % 60;
49
+ const minutes = Math.floor(totalSeconds / 60) % 60;
50
+ const hours = Math.floor(totalSeconds / 3600) % 24;
51
+ const days = Math.floor(totalSeconds / 86400);
52
+ if (days > 0) return `${days}d ${hours}h ${minutes}m ${seconds}s`;
53
+ if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`;
54
+ if (minutes > 0) return `${minutes}m ${seconds}s`;
55
+ return `${seconds}s`;
56
+ }
57
+
46
58
  /** Convert a single movement record into OutputLine-compatible entries */
47
59
  function convertMovementToLines(movement: { userPrompt: string; timestamp: string; thinkingOutput?: string; toolUseHistory?: Array<{ toolName: string; toolInput?: Record<string, unknown>; result?: string; isError?: boolean }>; assistantResponse?: string; errorOutput?: string; tokensUsed: number; durationMs?: number }): Array<Record<string, unknown>> {
48
60
  const lines: Array<Record<string, unknown>> = [];
@@ -67,26 +79,105 @@ function convertMovementToLines(movement: { userPrompt: string; timestamp: strin
67
79
  }
68
80
 
69
81
  const durationText = movement.durationMs
70
- ? `Completed in ${(movement.durationMs / 1000).toFixed(2)}s`
82
+ ? `Completed in ${formatElapsedDuration(movement.durationMs / 1000)}`
71
83
  : 'Completed';
72
84
  lines.push({ type: 'system', text: durationText, timestamp: ts });
73
85
  return lines;
74
86
  }
75
87
 
76
88
  function requireSession(ctx: HandlerContext, ws: WSContext, tabId: string): ImprovisationSessionManager {
77
- const session = getSession(ctx, ws, tabId);
89
+ const session = resolveTabSession(ctx, ws, tabId);
78
90
  if (!session) throw new Error(`No session found for tab ${tabId}`);
79
91
  return session;
80
92
  }
81
93
 
82
- function getSession(ctx: HandlerContext, ws: WSContext, tabId: string): ImprovisationSessionManager | null {
94
+ /**
95
+ * Canonical tab → session resolver.
96
+ *
97
+ * Returns the `ImprovisationSessionManager` for `tabId`, attaching it to `ws`
98
+ * if needed. Contract: after a successful return, the session is mapped in
99
+ * `ctx.connections.get(ws)` and its event listeners are wired to this `ws`.
100
+ *
101
+ * Resolution order (cheapest first, each step caches for subsequent calls):
102
+ * 1. Per-connection `tabMap` — the session is already attached to this `ws`.
103
+ * 2. Registry + in-memory — another connection has the session loaded;
104
+ * re-attach listeners to this `ws` without re-reading disk.
105
+ * 3. Registry + disk — session is persisted but not in memory (e.g. after
106
+ * a CLI restart); construct the manager from history and cache it.
107
+ *
108
+ * Returns `null` only when the tab is truly unknown (no registry entry AND
109
+ * no history file). That is the only case where handlers should surface an
110
+ * error to the caller — everything else self-heals so session ops never
111
+ * race the `initTab` handshake.
112
+ *
113
+ * Also restores worktree bindings from the registry on miss so git/file ops
114
+ * against this tab route to the correct directory even without initTab.
115
+ */
116
+ export function resolveTabSession(ctx: HandlerContext, ws: WSContext, tabId: string): ImprovisationSessionManager | null {
83
117
  const tabMap = ctx.connections.get(ws);
84
- if (!tabMap) return null;
85
118
 
86
- const sessionId = tabMap.get(tabId);
87
- if (!sessionId) return null;
119
+ const mappedSessionId = tabMap?.get(tabId);
120
+ if (mappedSessionId) {
121
+ const session = ctx.sessions.get(mappedSessionId);
122
+ if (session) return session;
123
+ }
124
+
125
+ const workingDir = ws._workingDir;
126
+ if (!workingDir) return null;
127
+
128
+ const registry = ctx.getRegistry(workingDir);
129
+ const registrySessionId = registry.getTabSession(tabId);
130
+ if (!registrySessionId) return null;
131
+
132
+ const inMemorySession = ctx.sessions.get(registrySessionId);
133
+ if (inMemorySession) {
134
+ return attachSessionToConnection(ctx, ws, tabId, inMemorySession, registry);
135
+ }
136
+
137
+ try {
138
+ const diskSession = ImprovisationSessionManager.resumeFromHistory(workingDir, registrySessionId);
139
+ ctx.sessions.set(diskSession.getSessionInfo().sessionId, diskSession);
140
+ registry.markTabPersisted(tabId);
141
+ return attachSessionToConnection(ctx, ws, tabId, diskSession, registry);
142
+ } catch {
143
+ // History file doesn't exist — either the tab has never had a first
144
+ // prompt (lazy persistence) or the file was deleted and the registry
145
+ // hasn't been swept yet. Either way, construct a fresh session bound to
146
+ // the registered sessionId so the tab keeps its identity.
147
+ const freshSession = new ImprovisationSessionManager({
148
+ workingDir,
149
+ sessionId: registrySessionId,
150
+ model: getModel(),
151
+ effortLevel: getEffortLevel(),
152
+ });
153
+ ctx.sessions.set(freshSession.getSessionInfo().sessionId, freshSession);
154
+ return attachSessionToConnection(ctx, ws, tabId, freshSession, registry);
155
+ }
156
+ }
157
+
158
+ /** Wire listeners + update caches when a resolved session first attaches to this ws. */
159
+ function attachSessionToConnection(
160
+ ctx: HandlerContext,
161
+ ws: WSContext,
162
+ tabId: string,
163
+ session: ImprovisationSessionManager,
164
+ registry: SessionRegistry,
165
+ ): ImprovisationSessionManager {
166
+ setupSessionListeners(ctx, session, ws, tabId);
167
+ const tabMap = ctx.connections.get(ws);
168
+ if (tabMap) tabMap.set(tabId, session.getSessionInfo().sessionId);
169
+ registry.touchTab(tabId);
170
+ restoreWorktreeFromRegistry(ctx, registry, tabId);
171
+ return session;
172
+ }
88
173
 
89
- return ctx.sessions.get(sessionId) || null;
174
+ /** Copy worktree bindings from the persistent registry into the live context. */
175
+ export function restoreWorktreeFromRegistry(ctx: HandlerContext, registry: SessionRegistry, tabId: string): void {
176
+ if (ctx.gitDirectories.has(tabId)) return;
177
+ const regTab = registry.getTab(tabId);
178
+ if (!regTab?.worktreePath) return;
179
+ ctx.gitDirectories.set(tabId, regTab.worktreePath);
180
+ if (regTab.worktreeBranch) ctx.gitBranches.set(tabId, regTab.worktreeBranch);
90
181
  }
91
182
 
92
183
  export function buildOutputHistory(session: ImprovisationSessionManager): Array<Record<string, unknown>> {
@@ -100,24 +191,49 @@ export function buildOutputHistory(session: ImprovisationSessionManager): Array<
100
191
  .flatMap(convertMovementToLines);
101
192
  }
102
193
 
103
- export function setupSessionListeners(ctx: HandlerContext, session: ImprovisationSessionManager, ws: WSContext, tabId: string): void {
194
+ /**
195
+ * Wire session events to the WebSocket fan-out.
196
+ *
197
+ * All session-driven messages broadcast to `allConnections` rather than the
198
+ * `ws` that called `initTab`/`execute`. The CLI has exactly one live socket
199
+ * at a time (the platform relay), and `allConnections` is maintained by
200
+ * `handleConnection`/`handleClose` — so a broadcast always lands on the
201
+ * *current* relay socket, even after a reconnect, and is fanned out to every
202
+ * paired web client by the platform.
203
+ *
204
+ * Sending to the captured `ws` was the prior shape and silently dropped
205
+ * streaming output for any tab whose `setupSessionListeners` hadn't been
206
+ * re-run after a relay reconnect (i.e. background tabs the user wasn't
207
+ * actively viewing). The `tabStateChanged` events still fired — they were
208
+ * already broadcast — so the tab's executing dot showed up but the actual
209
+ * stream content (`output`/`thinking`/`toolUse`/...) went nowhere.
210
+ *
211
+ * `ws` is retained in the signature for symmetry with other handlers and to
212
+ * keep the call sites unchanged.
213
+ */
214
+ export function setupSessionListeners(ctx: HandlerContext, session: ImprovisationSessionManager, _ws: WSContext, tabId: string): void {
104
215
  session.removeAllListeners();
105
216
 
217
+ session.on('onHistoryPersisted', () => {
218
+ const registry = ctx.getRegistry('');
219
+ try { registry.markTabPersisted(tabId); } catch { /* ignore */ }
220
+ });
221
+
106
222
  session.on('onOutput', (text: string) => {
107
- ctx.send(ws, { type: 'output', tabId, data: { text, timestamp: Date.now() } });
223
+ broadcastTabEvent(ctx, tabId, 'output', { text, timestamp: Date.now() });
108
224
  });
109
225
 
110
226
  session.on('onThinking', (text: string) => {
111
- ctx.send(ws, { type: 'thinking', tabId, data: { text } });
227
+ broadcastTabEvent(ctx, tabId, 'thinking', { text });
112
228
  });
113
229
 
114
230
  session.on('onMovementStart', (sequenceNumber: number, prompt: string, isAutoContinue?: boolean) => {
115
- ctx.send(ws, { type: 'movementStart', tabId, data: { sequenceNumber, prompt, timestamp: Date.now(), executionStartTimestamp: session.executionStartTimestamp, isAutoContinue } });
231
+ broadcastTabEvent(ctx, tabId, 'movementStart', { sequenceNumber, prompt, timestamp: Date.now(), executionStartTimestamp: session.executionStartTimestamp, isAutoContinue });
116
232
  ctx.broadcastToAll({ type: 'tabStateChanged', data: { tabId, isExecuting: true, executionStartTimestamp: session.executionStartTimestamp } });
117
233
  });
118
234
 
119
235
  session.on('onMovementComplete', (movement: Record<string, unknown>) => {
120
- ctx.send(ws, { type: 'movementComplete', tabId, data: movement });
236
+ broadcastTabEvent(ctx, tabId, 'movementComplete', movement);
121
237
 
122
238
  const registry = ctx.getRegistry('');
123
239
  // Use a try/catch since getRegistry may not have been initialized with the right workingDir
@@ -141,24 +257,24 @@ export function setupSessionListeners(ctx: HandlerContext, session: Improvisatio
141
257
  });
142
258
 
143
259
  session.on('onMovementError', (error: Error) => {
144
- ctx.send(ws, { type: 'movementError', tabId, data: { message: error.message } });
260
+ broadcastTabEvent(ctx, tabId, 'movementError', { message: error.message });
145
261
  ctx.broadcastToAll({ type: 'tabStateChanged', data: { tabId, isExecuting: false } });
146
262
  });
147
263
 
148
264
  session.on('onSessionUpdate', (history: Record<string, unknown>) => {
149
- ctx.send(ws, { type: 'sessionUpdate', tabId, data: history });
265
+ broadcastTabEvent(ctx, tabId, 'sessionUpdate', history);
150
266
  });
151
267
 
152
268
  session.on('onPlanNeedsConfirmation', (plan: Record<string, unknown>) => {
153
- ctx.send(ws, { type: 'approvalRequired', tabId, data: plan });
269
+ broadcastTabEvent(ctx, tabId, 'approvalRequired', plan);
154
270
  });
155
271
 
156
272
  session.on('onToolUse', (event: Record<string, unknown>) => {
157
- ctx.send(ws, { type: 'toolUse', tabId, data: { ...event, timestamp: Date.now() } });
273
+ broadcastTabEvent(ctx, tabId, 'toolUse', { ...event, timestamp: Date.now() });
158
274
  });
159
275
 
160
276
  session.on('onTokenUsage', (usage: { inputTokens: number; outputTokens: number }) => {
161
- ctx.send(ws, { type: 'streamingTokens', tabId, data: usage });
277
+ broadcastTabEvent(ctx, tabId, 'streamingTokens', usage);
162
278
  });
163
279
  }
164
280
 
@@ -187,46 +303,87 @@ export function mergePreUploadedAttachments(ctx: HandlerContext, tabId: string,
187
303
 
188
304
  const WRITE_OPS = new Set(['execute', 'cancel', 'new', 'approve', 'reject']);
189
305
 
306
+ function emitExecuteAck(ctx: HandlerContext, tabId: string, sessionId: string, msgId: string, duplicate = false): void {
307
+ ctx.broadcastToAll({
308
+ type: 'executeAck',
309
+ tabId,
310
+ data: duplicate ? { msgId, sessionId, duplicate: true } : { msgId, sessionId },
311
+ });
312
+ }
313
+
314
+ /**
315
+ * Handle an `execute` request: validate, dedupe on msgId, run the prompt,
316
+ * and ack every paired web so their outbox drains.
317
+ *
318
+ * Extracted from `handleSessionMessage` to keep its cyclomatic complexity
319
+ * within the biome threshold; the switch-body pattern was pushing past 15.
320
+ */
321
+ function handleExecuteMessage(ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage, tabId: string): void {
322
+ if (!msg.data?.prompt) throw new Error('Prompt is required');
323
+ const session = requireSession(ctx, ws, tabId);
324
+ const { sessionId } = session.getSessionInfo();
325
+ const msgId = typeof msg.data.msgId === 'string' ? msg.data.msgId as string : undefined;
326
+
327
+ // Idempotency: a web reconnect may replay the same msgId. Re-ack so its
328
+ // outbox drains, but don't run the prompt a second time.
329
+ if (msgId && !ctx.msgIdTracker.recordIfFirst(tabId, msgId)) {
330
+ console.log(`[session] execute duplicate msgId=${msgId} tabId=${tabId} — re-acking without re-run`);
331
+ emitExecuteAck(ctx, tabId, sessionId, msgId, /* duplicate */ true);
332
+ return;
333
+ }
334
+
335
+ if (msgId) {
336
+ console.log(`[session] execute accepted msgId=${msgId} tabId=${tabId} sessionId=${sessionId}`);
337
+ }
338
+
339
+ const worktreeDir = ctx.gitDirectories.get(tabId);
340
+ const attachments = mergePreUploadedAttachments(ctx, tabId, msg.data.attachments);
341
+
342
+ // Resolve slash commands (e.g. "/code-review") to their SKILL.md content.
343
+ // Claude Code's -p headless mode doesn't support skills natively, so we
344
+ // load the skill's instructions and pass them as the actual prompt.
345
+ const rawPrompt = msg.data.prompt as string;
346
+ const effectiveDir = worktreeDir || session.getSessionInfo().workingDir;
347
+ const resolved = resolveSkillPrompt(rawPrompt, effectiveDir);
348
+
349
+ session.executePrompt(
350
+ resolved ? resolved.prompt : rawPrompt,
351
+ attachments,
352
+ {
353
+ workingDir: worktreeDir,
354
+ displayPrompt: resolved ? rawPrompt : undefined,
355
+ },
356
+ );
357
+
358
+ // Ack AFTER enqueue so the web knows the CLI accepted the work.
359
+ if (msgId) emitExecuteAck(ctx, tabId, sessionId, msgId);
360
+ }
361
+
362
+ function handleNewSessionMessage(ctx: HandlerContext, ws: WSContext, tabId: string): void {
363
+ const oldSession = requireSession(ctx, ws, tabId);
364
+ const oldSessionId = oldSession.getSessionInfo().sessionId;
365
+ const newSession = oldSession.startNewSession({ model: getModel(), effortLevel: getEffortLevel() });
366
+ oldSession.destroy();
367
+ ctx.sessions.delete(oldSessionId);
368
+ setupSessionListeners(ctx, newSession, ws, tabId);
369
+ const newSessionId = newSession.getSessionInfo().sessionId;
370
+ ctx.sessions.set(newSessionId, newSession);
371
+ const tabMap = ctx.connections.get(ws);
372
+ if (tabMap) tabMap.set(tabId, newSessionId);
373
+ const registry = ctx.getRegistry('');
374
+ try { registry.updateTabSession(tabId, newSessionId); } catch { /* ignore */ }
375
+ ctx.send(ws, { type: 'newSession', tabId, data: newSession.getSessionInfo() });
376
+ }
377
+
190
378
  export function handleSessionMessage(ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage, tabId: string, permission?: 'view'): void {
191
379
  if (permission === 'view' && WRITE_OPS.has(msg.type)) {
192
380
  throw new Error('View-only users cannot perform write operations');
193
381
  }
194
382
 
195
383
  switch (msg.type) {
196
- case 'execute': {
197
- if (!msg.data?.prompt) throw new Error('Prompt is required');
198
- const session = requireSession(ctx, ws, tabId);
199
- const worktreeDir = ctx.gitDirectories.get(tabId);
200
- const attachments = mergePreUploadedAttachments(ctx, tabId, msg.data.attachments);
201
-
202
- // Resolve slash commands (e.g. "/code-review") to their SKILL.md prompt content.
203
- // Claude Code's -p headless mode doesn't support skills natively, so we load
204
- // the skill's instructions and pass them as the actual prompt.
205
- const rawPrompt = msg.data.prompt as string;
206
- const effectiveDir = worktreeDir || session.getSessionInfo().workingDir;
207
- const resolved = resolveSkillPrompt(rawPrompt, effectiveDir);
208
-
209
- // Authoritative prompt-input clear for all connected devices. The
210
- // submitter already cleared locally; this guarantees other devices
211
- // clear even if the submitter's debounced syncPromptText never fires
212
- // (e.g. mobile tab suspended after Send). Clients suppress this via
213
- // locallyEditingTabs if the user is actively typing a new prompt.
214
- ctx.broadcastToAll({
215
- type: 'promptTextSync',
216
- tabId,
217
- data: { tabId, text: '' },
218
- });
219
-
220
- session.executePrompt(
221
- resolved ? resolved.prompt : rawPrompt,
222
- attachments,
223
- {
224
- workingDir: worktreeDir,
225
- displayPrompt: resolved ? rawPrompt : undefined,
226
- },
227
- );
384
+ case 'execute':
385
+ handleExecuteMessage(ctx, ws, msg, tabId);
228
386
  break;
229
- }
230
387
  case 'cancel': {
231
388
  const session = requireSession(ctx, ws, tabId);
232
389
  session.cancel();
@@ -237,32 +394,19 @@ export function handleSessionMessage(ctx: HandlerContext, ws: WSContext, msg: We
237
394
  ctx.send(ws, { type: 'history', tabId, data: session.getHistory() });
238
395
  break;
239
396
  }
240
- case 'new': {
241
- const oldSession = requireSession(ctx, ws, tabId);
242
- const oldSessionId = oldSession.getSessionInfo().sessionId;
243
- const newSession = oldSession.startNewSession({ model: getModel(), effortLevel: getEffortLevel() });
244
- oldSession.destroy();
245
- ctx.sessions.delete(oldSessionId);
246
- setupSessionListeners(ctx, newSession, ws, tabId);
247
- const newSessionId = newSession.getSessionInfo().sessionId;
248
- ctx.sessions.set(newSessionId, newSession);
249
- const tabMap = ctx.connections.get(ws);
250
- if (tabMap) tabMap.set(tabId, newSessionId);
251
- const registry = ctx.getRegistry('');
252
- try { registry.updateTabSession(tabId, newSessionId); } catch { /* ignore */ }
253
- ctx.send(ws, { type: 'newSession', tabId, data: newSession.getSessionInfo() });
397
+ case 'new':
398
+ handleNewSessionMessage(ctx, ws, tabId);
254
399
  break;
255
- }
256
400
  case 'approve': {
257
401
  const session = requireSession(ctx, ws, tabId);
258
402
  session.respondToApproval(true);
259
- ctx.send(ws, { type: 'output', tabId, data: { text: '\n✅ Approved - proceeding with operation\n' } });
403
+ broadcastTabEvent(ctx, tabId, 'output', { text: '\n✅ Approved - proceeding with operation\n' });
260
404
  break;
261
405
  }
262
406
  case 'reject': {
263
407
  const session = requireSession(ctx, ws, tabId);
264
408
  session.respondToApproval(false);
265
- ctx.send(ws, { type: 'output', tabId, data: { text: '\n🚫 Rejected - operation cancelled\n' } });
409
+ broadcastTabEvent(ctx, tabId, 'output', { text: '\n🚫 Rejected - operation cancelled\n' });
266
410
  break;
267
411
  }
268
412
  }
@@ -1,5 +1,4 @@
1
1
  // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
- // Licensed under the MIT License. See LICENSE file for details.
3
2
 
4
3
  import { existsSync, readdirSync, readFileSync, unlinkSync } from 'node:fs';
5
4
  import { readFile } from 'node:fs/promises';
@@ -1,13 +1,27 @@
1
1
  // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
- // Licensed under the MIT License. See LICENSE file for details.
3
2
 
4
3
  import { ImprovisationSessionManager } from '../../cli/improvisation-session-manager.js';
5
4
  import { getEffortLevel, getModel } from '../settings.js';
6
5
  import type { HandlerContext } from './handler-context.js';
7
6
  import { buildOutputHistory, setupSessionListeners } from './session-handlers.js';
8
7
  import type { SessionRegistry } from './session-registry.js';
8
+ import { replayTabEventsSince } from './tab-event-replay.js';
9
9
  import type { WSContext } from './types.js';
10
10
 
11
+ /**
12
+ * Extract `lastSeenSeq` from an initTab/resumeSession data payload.
13
+ *
14
+ * Keeps the narrow-typing scoped to the initialization module instead of
15
+ * leaking into the broader `HandlerContext`. Returns `undefined` for first
16
+ * init (no replay needed) or malformed payloads (treated as first init —
17
+ * safer than surfacing an error the user can't act on).
18
+ */
19
+ function extractLastSeenSeq(data: unknown): number | undefined {
20
+ if (!data || typeof data !== 'object') return undefined;
21
+ const candidate = (data as { lastSeenSeq?: unknown }).lastSeenSeq;
22
+ return typeof candidate === 'number' && Number.isFinite(candidate) ? candidate : undefined;
23
+ }
24
+
11
25
  function tryResumeFromDisk(
12
26
  ctx: HandlerContext,
13
27
  ws: WSContext,
@@ -15,7 +29,8 @@ function tryResumeFromDisk(
15
29
  workingDir: string,
16
30
  registrySessionId: string,
17
31
  tabMap: Map<string, string> | undefined,
18
- registry: SessionRegistry
32
+ registry: SessionRegistry,
33
+ lastSeenSeq: number | undefined,
19
34
  ): boolean {
20
35
  try {
21
36
  const diskSession = ImprovisationSessionManager.resumeFromHistory(workingDir, registrySessionId);
@@ -24,6 +39,7 @@ function tryResumeFromDisk(
24
39
  ctx.sessions.set(diskSessionId, diskSession);
25
40
  if (tabMap) tabMap.set(tabId, diskSessionId);
26
41
  registry.touchTab(tabId);
42
+ registry.markTabPersisted(tabId);
27
43
 
28
44
  // Restore worktree state from registry
29
45
  const regTab = registry.getTab(tabId);
@@ -34,12 +50,18 @@ function tryResumeFromDisk(
34
50
  const worktreePath = ctx.gitDirectories.get(tabId);
35
51
  const worktreeBranch = ctx.gitBranches.get(tabId);
36
52
 
53
+ // Replay any tab-scoped events the web missed during the transport gap
54
+ // BEFORE tabInitialized so they arrive in the right order. Web-side
55
+ // handlers append; `tabInitialized` does NOT reset when `resumedFromSeq`
56
+ // is set, preserving the replayed additions.
57
+ replayTabEventsSince(ctx, ws, tabId, lastSeenSeq);
58
+
37
59
  ctx.send(ws, {
38
60
  type: 'tabInitialized',
39
61
  tabId,
40
62
  data: {
41
63
  ...diskSession.getSessionInfo(),
42
- outputHistory: buildOutputHistory(diskSession),
64
+ ...(lastSeenSeq === undefined ? { outputHistory: buildOutputHistory(diskSession) } : { resumedFromSeq: true }),
43
65
  ...(worktreePath ? { worktreePath, worktreeBranch } : {}),
44
66
  }
45
67
  });
@@ -49,16 +71,17 @@ function tryResumeFromDisk(
49
71
  }
50
72
  }
51
73
 
52
- export async function initializeTab(ctx: HandlerContext, ws: WSContext, tabId: string, workingDir: string, tabName?: string): Promise<void> {
74
+ export async function initializeTab(ctx: HandlerContext, ws: WSContext, tabId: string, workingDir: string, tabName?: string, rawData?: unknown): Promise<void> {
53
75
  const tabMap = ctx.connections.get(ws);
54
76
  const registry = ctx.getRegistry(workingDir);
77
+ const lastSeenSeq = extractLastSeenSeq(rawData);
55
78
 
56
79
  // 1. Check per-connection map (same WS reconnect)
57
80
  const existingSessionId = tabMap?.get(tabId);
58
81
  if (existingSessionId) {
59
82
  const existingSession = ctx.sessions.get(existingSessionId);
60
83
  if (existingSession) {
61
- reattachSession(ctx, existingSession, ws, tabId, registry);
84
+ reattachSession(ctx, existingSession, ws, tabId, registry, lastSeenSeq);
62
85
  return;
63
86
  }
64
87
  }
@@ -68,17 +91,25 @@ export async function initializeTab(ctx: HandlerContext, ws: WSContext, tabId: s
68
91
  if (registrySessionId) {
69
92
  const inMemorySession = ctx.sessions.get(registrySessionId);
70
93
  if (inMemorySession) {
71
- reattachSession(ctx, inMemorySession, ws, tabId, registry);
94
+ reattachSession(ctx, inMemorySession, ws, tabId, registry, lastSeenSeq);
72
95
  return;
73
96
  }
74
97
 
75
- if (tryResumeFromDisk(ctx, ws, tabId, workingDir, registrySessionId, tabMap, registry)) {
98
+ if (tryResumeFromDisk(ctx, ws, tabId, workingDir, registrySessionId, tabMap, registry, lastSeenSeq)) {
76
99
  return;
77
100
  }
78
101
  }
79
102
 
80
- // 3. Create new session
81
- const session = new ImprovisationSessionManager({ workingDir, model: getModel(), effortLevel: getEffortLevel() });
103
+ // 3. Create new session. If the tab is already registered (no file on
104
+ // disk tab is pending first prompt or file was deleted), reuse its
105
+ // sessionId so the tab keeps its identity across restarts.
106
+ const existingTab = registry.getTab(tabId);
107
+ const session = new ImprovisationSessionManager({
108
+ workingDir,
109
+ ...(registrySessionId ? { sessionId: registrySessionId } : {}),
110
+ model: getModel(),
111
+ effortLevel: getEffortLevel(),
112
+ });
82
113
  setupSessionListeners(ctx, session, ws, tabId);
83
114
 
84
115
  const sessionId = session.getSessionInfo().sessionId;
@@ -88,17 +119,24 @@ export async function initializeTab(ctx: HandlerContext, ws: WSContext, tabId: s
88
119
  tabMap.set(tabId, sessionId);
89
120
  }
90
121
 
91
- registry.registerTab(tabId, sessionId, tabName);
122
+ registry.registerTab(tabId, sessionId, tabName || existingTab?.tabName);
92
123
  const registeredTab = registry.getTab(tabId);
93
124
  ctx.broadcastToAll({
94
125
  type: 'tabCreated',
95
126
  data: { tabId, tabName: registeredTab?.tabName || 'Chat', createdAt: registeredTab?.createdAt, order: registeredTab?.order, sessionInfo: session.getSessionInfo() }
96
127
  });
97
128
 
129
+ // Fresh session (no disk/memory predecessor) has nothing to replay,
130
+ // but we still pass lastSeenSeq through so the web flag is consistent.
131
+ replayTabEventsSince(ctx, ws, tabId, lastSeenSeq);
132
+
98
133
  ctx.send(ws, {
99
134
  type: 'tabInitialized',
100
135
  tabId,
101
- data: session.getSessionInfo()
136
+ data: {
137
+ ...session.getSessionInfo(),
138
+ ...(lastSeenSeq !== undefined ? { resumedFromSeq: true } : {}),
139
+ }
102
140
  });
103
141
  }
104
142
 
@@ -107,16 +145,18 @@ export async function resumeHistoricalSession(
107
145
  ws: WSContext,
108
146
  tabId: string,
109
147
  workingDir: string,
110
- historicalSessionId: string
148
+ historicalSessionId: string,
149
+ rawData?: unknown,
111
150
  ): Promise<void> {
112
151
  const tabMap = ctx.connections.get(ws);
113
152
  const registry = ctx.getRegistry(workingDir);
153
+ const lastSeenSeq = extractLastSeenSeq(rawData);
114
154
 
115
155
  const existingSessionId = tabMap?.get(tabId);
116
156
  if (existingSessionId) {
117
157
  const existingSession = ctx.sessions.get(existingSessionId);
118
158
  if (existingSession) {
119
- reattachSession(ctx, existingSession, ws, tabId, registry);
159
+ reattachSession(ctx, existingSession, ws, tabId, registry, lastSeenSeq);
120
160
  return;
121
161
  }
122
162
  }
@@ -125,7 +165,7 @@ export async function resumeHistoricalSession(
125
165
  if (registrySessionId) {
126
166
  const inMemorySession = ctx.sessions.get(registrySessionId);
127
167
  if (inMemorySession) {
128
- reattachSession(ctx, inMemorySession, ws, tabId, registry);
168
+ reattachSession(ctx, inMemorySession, ws, tabId, registry, lastSeenSeq);
129
169
  return;
130
170
  }
131
171
  }
@@ -152,12 +192,14 @@ export async function resumeHistoricalSession(
152
192
 
153
193
  registry.registerTab(tabId, sessionId);
154
194
 
195
+ replayTabEventsSince(ctx, ws, tabId, lastSeenSeq);
196
+
155
197
  ctx.send(ws, {
156
198
  type: 'tabInitialized',
157
199
  tabId,
158
200
  data: {
159
201
  ...session.getSessionInfo(),
160
- outputHistory: buildOutputHistory(session),
202
+ ...(lastSeenSeq === undefined ? { outputHistory: buildOutputHistory(session) } : { resumedFromSeq: true }),
161
203
  resumeFailed: isNewSession,
162
204
  originalSessionId: isNewSession ? historicalSessionId : undefined
163
205
  }
@@ -169,7 +211,8 @@ function reattachSession(
169
211
  session: ImprovisationSessionManager,
170
212
  ws: WSContext,
171
213
  tabId: string,
172
- registry: SessionRegistry
214
+ registry: SessionRegistry,
215
+ lastSeenSeq: number | undefined,
173
216
  ): void {
174
217
  setupSessionListeners(ctx, session, ws, tabId);
175
218
 
@@ -185,15 +228,35 @@ function reattachSession(
185
228
  if (regTab.worktreeBranch) ctx.gitBranches.set(tabId, regTab.worktreeBranch);
186
229
  }
187
230
 
188
- const outputHistory = buildOutputHistory(session);
231
+ const worktreePath = ctx.gitDirectories.get(tabId);
232
+ const worktreeBranch = ctx.gitBranches.get(tabId);
189
233
 
234
+ // Fast path: the web already has local state (via Zustand), so just replay
235
+ // anything newer than `lastSeenSeq` and tell the client to skip the
236
+ // destructive reset in its tabInitialized handler.
237
+ if (lastSeenSeq !== undefined) {
238
+ replayTabEventsSince(ctx, ws, tabId, lastSeenSeq);
239
+ ctx.send(ws, {
240
+ type: 'tabInitialized',
241
+ tabId,
242
+ data: {
243
+ ...session.getSessionInfo(),
244
+ resumedFromSeq: true,
245
+ isExecuting: session.isExecuting,
246
+ ...(session.isExecuting && session.executionStartTimestamp ? { executionStartTimestamp: session.executionStartTimestamp } : {}),
247
+ ...(worktreePath ? { worktreePath, worktreeBranch } : {}),
248
+ }
249
+ });
250
+ return;
251
+ }
252
+
253
+ // Cold-start reattach (no prior seq): send the full snapshot so the web
254
+ // can rebuild from scratch.
255
+ const outputHistory = buildOutputHistory(session);
190
256
  const executionEvents = session.isExecuting
191
257
  ? session.getExecutionEventLog()
192
258
  : undefined;
193
259
 
194
- const worktreePath = ctx.gitDirectories.get(tabId);
195
- const worktreeBranch = ctx.gitBranches.get(tabId);
196
-
197
260
  ctx.send(ws, {
198
261
  type: 'tabInitialized',
199
262
  tabId,