mstro-app 0.5.0 → 0.5.5

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 (649) hide show
  1. package/LICENSE +129 -190
  2. package/PRIVACY.md +11 -11
  3. package/README.md +75 -28
  4. package/bin/commands/config.js +1 -2
  5. package/bin/mstro.js +55 -5
  6. package/bin/postinstall.js +0 -1
  7. package/dist/server/cli/eta-estimator.d.ts +55 -0
  8. package/dist/server/cli/eta-estimator.d.ts.map +1 -0
  9. package/dist/server/cli/eta-estimator.js +222 -0
  10. package/dist/server/cli/eta-estimator.js.map +1 -0
  11. package/dist/server/cli/headless/claude-invoker-process.d.ts.map +1 -1
  12. package/dist/server/cli/headless/claude-invoker-process.js +0 -1
  13. package/dist/server/cli/headless/claude-invoker-process.js.map +1 -1
  14. package/dist/server/cli/headless/claude-invoker-stall.d.ts.map +1 -1
  15. package/dist/server/cli/headless/claude-invoker-stall.js +0 -1
  16. package/dist/server/cli/headless/claude-invoker-stall.js.map +1 -1
  17. package/dist/server/cli/headless/claude-invoker-stream.d.ts.map +1 -1
  18. package/dist/server/cli/headless/claude-invoker-stream.js +0 -1
  19. package/dist/server/cli/headless/claude-invoker-stream.js.map +1 -1
  20. package/dist/server/cli/headless/claude-invoker-tools.d.ts.map +1 -1
  21. package/dist/server/cli/headless/claude-invoker-tools.js +0 -1
  22. package/dist/server/cli/headless/claude-invoker-tools.js.map +1 -1
  23. package/dist/server/cli/headless/claude-invoker.d.ts.map +1 -1
  24. package/dist/server/cli/headless/claude-invoker.js +0 -1
  25. package/dist/server/cli/headless/claude-invoker.js.map +1 -1
  26. package/dist/server/cli/headless/haiku-assessments.d.ts.map +1 -1
  27. package/dist/server/cli/headless/haiku-assessments.js +0 -1
  28. package/dist/server/cli/headless/haiku-assessments.js.map +1 -1
  29. package/dist/server/cli/headless/headless-logger.d.ts.map +1 -1
  30. package/dist/server/cli/headless/headless-logger.js +0 -1
  31. package/dist/server/cli/headless/headless-logger.js.map +1 -1
  32. package/dist/server/cli/headless/index.d.ts.map +1 -1
  33. package/dist/server/cli/headless/index.js +0 -1
  34. package/dist/server/cli/headless/index.js.map +1 -1
  35. package/dist/server/cli/headless/native-timeout-detector.d.ts.map +1 -1
  36. package/dist/server/cli/headless/native-timeout-detector.js +0 -1
  37. package/dist/server/cli/headless/native-timeout-detector.js.map +1 -1
  38. package/dist/server/cli/headless/output-utils.d.ts.map +1 -1
  39. package/dist/server/cli/headless/output-utils.js +0 -1
  40. package/dist/server/cli/headless/output-utils.js.map +1 -1
  41. package/dist/server/cli/headless/prompt-utils.d.ts.map +1 -1
  42. package/dist/server/cli/headless/prompt-utils.js +0 -1
  43. package/dist/server/cli/headless/prompt-utils.js.map +1 -1
  44. package/dist/server/cli/headless/resilient-runner.d.ts.map +1 -1
  45. package/dist/server/cli/headless/resilient-runner.js +0 -1
  46. package/dist/server/cli/headless/resilient-runner.js.map +1 -1
  47. package/dist/server/cli/headless/retry-strategies.d.ts.map +1 -1
  48. package/dist/server/cli/headless/retry-strategies.js +0 -1
  49. package/dist/server/cli/headless/retry-strategies.js.map +1 -1
  50. package/dist/server/cli/headless/runner.d.ts.map +1 -1
  51. package/dist/server/cli/headless/runner.js +0 -1
  52. package/dist/server/cli/headless/runner.js.map +1 -1
  53. package/dist/server/cli/headless/stall-assessor.d.ts +50 -0
  54. package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
  55. package/dist/server/cli/headless/stall-assessor.js +64 -10
  56. package/dist/server/cli/headless/stall-assessor.js.map +1 -1
  57. package/dist/server/cli/headless/tool-watchdog.d.ts +21 -0
  58. package/dist/server/cli/headless/tool-watchdog.d.ts.map +1 -1
  59. package/dist/server/cli/headless/tool-watchdog.js +19 -13
  60. package/dist/server/cli/headless/tool-watchdog.js.map +1 -1
  61. package/dist/server/cli/headless/types.d.ts.map +1 -1
  62. package/dist/server/cli/headless/types.js +0 -1
  63. package/dist/server/cli/headless/types.js.map +1 -1
  64. package/dist/server/cli/improvisation-attachments.d.ts.map +1 -1
  65. package/dist/server/cli/improvisation-attachments.js +0 -1
  66. package/dist/server/cli/improvisation-attachments.js.map +1 -1
  67. package/dist/server/cli/improvisation-history-store.d.ts.map +1 -1
  68. package/dist/server/cli/improvisation-history-store.js +5 -2
  69. package/dist/server/cli/improvisation-history-store.js.map +1 -1
  70. package/dist/server/cli/improvisation-movements.d.ts.map +1 -1
  71. package/dist/server/cli/improvisation-movements.js +0 -1
  72. package/dist/server/cli/improvisation-movements.js.map +1 -1
  73. package/dist/server/cli/improvisation-output-queue.d.ts +5 -1
  74. package/dist/server/cli/improvisation-output-queue.d.ts.map +1 -1
  75. package/dist/server/cli/improvisation-output-queue.js +30 -8
  76. package/dist/server/cli/improvisation-output-queue.js.map +1 -1
  77. package/dist/server/cli/improvisation-retry.d.ts.map +1 -1
  78. package/dist/server/cli/improvisation-retry.js +0 -1
  79. package/dist/server/cli/improvisation-retry.js.map +1 -1
  80. package/dist/server/cli/improvisation-session-manager.d.ts +29 -0
  81. package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
  82. package/dist/server/cli/improvisation-session-manager.js +50 -2
  83. package/dist/server/cli/improvisation-session-manager.js.map +1 -1
  84. package/dist/server/cli/improvisation-types.d.ts +2 -0
  85. package/dist/server/cli/improvisation-types.d.ts.map +1 -1
  86. package/dist/server/cli/improvisation-types.js +0 -1
  87. package/dist/server/cli/improvisation-types.js.map +1 -1
  88. package/dist/server/cli/retry/retry-best-result.d.ts.map +1 -1
  89. package/dist/server/cli/retry/retry-best-result.js +0 -1
  90. package/dist/server/cli/retry/retry-best-result.js.map +1 -1
  91. package/dist/server/cli/retry/retry-context-loss.d.ts.map +1 -1
  92. package/dist/server/cli/retry/retry-context-loss.js +0 -1
  93. package/dist/server/cli/retry/retry-context-loss.js.map +1 -1
  94. package/dist/server/cli/retry/retry-premature-completion.d.ts.map +1 -1
  95. package/dist/server/cli/retry/retry-premature-completion.js +1 -2
  96. package/dist/server/cli/retry/retry-premature-completion.js.map +1 -1
  97. package/dist/server/cli/retry/retry-recovery-strategies.d.ts.map +1 -1
  98. package/dist/server/cli/retry/retry-recovery-strategies.js +0 -1
  99. package/dist/server/cli/retry/retry-recovery-strategies.js.map +1 -1
  100. package/dist/server/cli/retry/retry-resume-strategy.d.ts.map +1 -1
  101. package/dist/server/cli/retry/retry-resume-strategy.js +0 -1
  102. package/dist/server/cli/retry/retry-resume-strategy.js.map +1 -1
  103. package/dist/server/cli/retry/retry-runner-factory.d.ts.map +1 -1
  104. package/dist/server/cli/retry/retry-runner-factory.js +0 -1
  105. package/dist/server/cli/retry/retry-runner-factory.js.map +1 -1
  106. package/dist/server/cli/retry/retry-tool-results.d.ts.map +1 -1
  107. package/dist/server/cli/retry/retry-tool-results.js +0 -1
  108. package/dist/server/cli/retry/retry-tool-results.js.map +1 -1
  109. package/dist/server/cli/retry/retry-types.d.ts.map +1 -1
  110. package/dist/server/cli/retry/retry-types.js +0 -1
  111. package/dist/server/cli/retry/retry-types.js.map +1 -1
  112. package/dist/server/engines/EngineEvent.d.ts +126 -0
  113. package/dist/server/engines/EngineEvent.d.ts.map +1 -0
  114. package/dist/server/engines/EngineEvent.js +11 -0
  115. package/dist/server/engines/EngineEvent.js.map +1 -0
  116. package/dist/server/engines/claude/ClaudeCodeEngine.d.ts +47 -0
  117. package/dist/server/engines/claude/ClaudeCodeEngine.d.ts.map +1 -0
  118. package/dist/server/engines/claude/ClaudeCodeEngine.js +338 -0
  119. package/dist/server/engines/claude/ClaudeCodeEngine.js.map +1 -0
  120. package/dist/server/engines/factory.d.ts +21 -0
  121. package/dist/server/engines/factory.d.ts.map +1 -0
  122. package/dist/server/engines/factory.js +152 -0
  123. package/dist/server/engines/factory.js.map +1 -0
  124. package/dist/server/engines/opencode/OpenCodeEngine.d.ts +148 -0
  125. package/dist/server/engines/opencode/OpenCodeEngine.d.ts.map +1 -0
  126. package/dist/server/engines/opencode/OpenCodeEngine.js +630 -0
  127. package/dist/server/engines/opencode/OpenCodeEngine.js.map +1 -0
  128. package/dist/server/engines/opencode/OpenCodeServerManager.d.ts +172 -0
  129. package/dist/server/engines/opencode/OpenCodeServerManager.d.ts.map +1 -0
  130. package/dist/server/engines/opencode/OpenCodeServerManager.js +390 -0
  131. package/dist/server/engines/opencode/OpenCodeServerManager.js.map +1 -0
  132. package/dist/server/engines/opencode/model-catalog.d.ts +94 -0
  133. package/dist/server/engines/opencode/model-catalog.d.ts.map +1 -0
  134. package/dist/server/engines/opencode/model-catalog.js +141 -0
  135. package/dist/server/engines/opencode/model-catalog.js.map +1 -0
  136. package/dist/server/engines/types.d.ts +146 -0
  137. package/dist/server/engines/types.d.ts.map +1 -0
  138. package/dist/server/engines/types.js +4 -0
  139. package/dist/server/engines/types.js.map +1 -0
  140. package/dist/server/index.js +1 -2
  141. package/dist/server/index.js.map +1 -1
  142. package/dist/server/mcp/bouncer-cli.js +0 -1
  143. package/dist/server/mcp/bouncer-cli.js.map +1 -1
  144. package/dist/server/mcp/bouncer-haiku.d.ts +17 -4
  145. package/dist/server/mcp/bouncer-haiku.d.ts.map +1 -1
  146. package/dist/server/mcp/bouncer-haiku.js +8 -125
  147. package/dist/server/mcp/bouncer-haiku.js.map +1 -1
  148. package/dist/server/mcp/bouncer-integration.d.ts +45 -0
  149. package/dist/server/mcp/bouncer-integration.d.ts.map +1 -1
  150. package/dist/server/mcp/bouncer-integration.js +69 -6
  151. package/dist/server/mcp/bouncer-integration.js.map +1 -1
  152. package/dist/server/mcp/classifier/BouncerClassifier.d.ts +34 -0
  153. package/dist/server/mcp/classifier/BouncerClassifier.d.ts.map +1 -0
  154. package/dist/server/mcp/classifier/BouncerClassifier.js +4 -0
  155. package/dist/server/mcp/classifier/BouncerClassifier.js.map +1 -0
  156. package/dist/server/mcp/classifier/ClaudeBouncerClassifier.d.ts +17 -0
  157. package/dist/server/mcp/classifier/ClaudeBouncerClassifier.d.ts.map +1 -0
  158. package/dist/server/mcp/classifier/ClaudeBouncerClassifier.js +142 -0
  159. package/dist/server/mcp/classifier/ClaudeBouncerClassifier.js.map +1 -0
  160. package/dist/server/mcp/classifier/OpenCodeBouncerClassifier.d.ts +68 -0
  161. package/dist/server/mcp/classifier/OpenCodeBouncerClassifier.d.ts.map +1 -0
  162. package/dist/server/mcp/classifier/OpenCodeBouncerClassifier.js +182 -0
  163. package/dist/server/mcp/classifier/OpenCodeBouncerClassifier.js.map +1 -0
  164. package/dist/server/mcp/classifier/factory.d.ts +70 -0
  165. package/dist/server/mcp/classifier/factory.d.ts.map +1 -0
  166. package/dist/server/mcp/classifier/factory.js +155 -0
  167. package/dist/server/mcp/classifier/factory.js.map +1 -0
  168. package/dist/server/mcp/security-analysis.d.ts.map +1 -1
  169. package/dist/server/mcp/security-analysis.js +0 -1
  170. package/dist/server/mcp/security-analysis.js.map +1 -1
  171. package/dist/server/mcp/security-audit.d.ts.map +1 -1
  172. package/dist/server/mcp/security-audit.js +0 -1
  173. package/dist/server/mcp/security-audit.js.map +1 -1
  174. package/dist/server/mcp/security-patterns.d.ts.map +1 -1
  175. package/dist/server/mcp/security-patterns.js +0 -1
  176. package/dist/server/mcp/security-patterns.js.map +1 -1
  177. package/dist/server/mcp/server.js +0 -1
  178. package/dist/server/mcp/server.js.map +1 -1
  179. package/dist/server/routes/files.d.ts.map +1 -1
  180. package/dist/server/routes/files.js +0 -1
  181. package/dist/server/routes/files.js.map +1 -1
  182. package/dist/server/routes/improvise.d.ts.map +1 -1
  183. package/dist/server/routes/improvise.js +0 -1
  184. package/dist/server/routes/improvise.js.map +1 -1
  185. package/dist/server/routes/index.d.ts.map +1 -1
  186. package/dist/server/routes/index.js +0 -1
  187. package/dist/server/routes/index.js.map +1 -1
  188. package/dist/server/routes/instances.d.ts.map +1 -1
  189. package/dist/server/routes/instances.js +0 -1
  190. package/dist/server/routes/instances.js.map +1 -1
  191. package/dist/server/routes/notifications.d.ts.map +1 -1
  192. package/dist/server/routes/notifications.js +0 -1
  193. package/dist/server/routes/notifications.js.map +1 -1
  194. package/dist/server/server-setup.d.ts.map +1 -1
  195. package/dist/server/server-setup.js +0 -1
  196. package/dist/server/server-setup.js.map +1 -1
  197. package/dist/server/services/analytics.d.ts.map +1 -1
  198. package/dist/server/services/analytics.js +0 -1
  199. package/dist/server/services/analytics.js.map +1 -1
  200. package/dist/server/services/auth.d.ts.map +1 -1
  201. package/dist/server/services/auth.js +0 -1
  202. package/dist/server/services/auth.js.map +1 -1
  203. package/dist/server/services/client-id.d.ts.map +1 -1
  204. package/dist/server/services/client-id.js +0 -1
  205. package/dist/server/services/client-id.js.map +1 -1
  206. package/dist/server/services/file-explorer-ops.d.ts.map +1 -1
  207. package/dist/server/services/file-explorer-ops.js +0 -1
  208. package/dist/server/services/file-explorer-ops.js.map +1 -1
  209. package/dist/server/services/files.d.ts.map +1 -1
  210. package/dist/server/services/files.js +0 -1
  211. package/dist/server/services/files.js.map +1 -1
  212. package/dist/server/services/instances.d.ts.map +1 -1
  213. package/dist/server/services/instances.js +0 -1
  214. package/dist/server/services/instances.js.map +1 -1
  215. package/dist/server/services/pathUtils.d.ts.map +1 -1
  216. package/dist/server/services/pathUtils.js +0 -1
  217. package/dist/server/services/pathUtils.js.map +1 -1
  218. package/dist/server/services/plan/agent-loader.d.ts.map +1 -1
  219. package/dist/server/services/plan/agent-loader.js +0 -1
  220. package/dist/server/services/plan/agent-loader.js.map +1 -1
  221. package/dist/server/services/plan/agent-resolver.d.ts +26 -0
  222. package/dist/server/services/plan/agent-resolver.d.ts.map +1 -0
  223. package/dist/server/services/plan/agent-resolver.js +102 -0
  224. package/dist/server/services/plan/agent-resolver.js.map +1 -0
  225. package/dist/server/services/plan/board-config.d.ts.map +1 -1
  226. package/dist/server/services/plan/board-config.js +0 -1
  227. package/dist/server/services/plan/board-config.js.map +1 -1
  228. package/dist/server/services/plan/composer.d.ts.map +1 -1
  229. package/dist/server/services/plan/composer.js +59 -12
  230. package/dist/server/services/plan/composer.js.map +1 -1
  231. package/dist/server/services/plan/config-installer.d.ts.map +1 -1
  232. package/dist/server/services/plan/config-installer.js +0 -1
  233. package/dist/server/services/plan/config-installer.js.map +1 -1
  234. package/dist/server/services/plan/dependency-resolver.d.ts.map +1 -1
  235. package/dist/server/services/plan/dependency-resolver.js +0 -1
  236. package/dist/server/services/plan/dependency-resolver.js.map +1 -1
  237. package/dist/server/services/plan/executor.d.ts.map +1 -1
  238. package/dist/server/services/plan/executor.js +48 -4
  239. package/dist/server/services/plan/executor.js.map +1 -1
  240. package/dist/server/services/plan/front-matter.d.ts.map +1 -1
  241. package/dist/server/services/plan/front-matter.js +0 -1
  242. package/dist/server/services/plan/front-matter.js.map +1 -1
  243. package/dist/server/services/plan/issue-classification.d.ts.map +1 -1
  244. package/dist/server/services/plan/issue-classification.js +0 -1
  245. package/dist/server/services/plan/issue-classification.js.map +1 -1
  246. package/dist/server/services/plan/issue-loader.d.ts.map +1 -1
  247. package/dist/server/services/plan/issue-loader.js +0 -1
  248. package/dist/server/services/plan/issue-loader.js.map +1 -1
  249. package/dist/server/services/plan/issue-prompt-builder.d.ts.map +1 -1
  250. package/dist/server/services/plan/issue-prompt-builder.js +33 -2
  251. package/dist/server/services/plan/issue-prompt-builder.js.map +1 -1
  252. package/dist/server/services/plan/issue-retry.d.ts +3 -1
  253. package/dist/server/services/plan/issue-retry.d.ts.map +1 -1
  254. package/dist/server/services/plan/issue-retry.js +2 -1
  255. package/dist/server/services/plan/issue-retry.js.map +1 -1
  256. package/dist/server/services/plan/issue-writer.d.ts.map +1 -1
  257. package/dist/server/services/plan/issue-writer.js +0 -1
  258. package/dist/server/services/plan/issue-writer.js.map +1 -1
  259. package/dist/server/services/plan/output-manager.d.ts.map +1 -1
  260. package/dist/server/services/plan/output-manager.js +0 -1
  261. package/dist/server/services/plan/output-manager.js.map +1 -1
  262. package/dist/server/services/plan/parser-core.d.ts.map +1 -1
  263. package/dist/server/services/plan/parser-core.js +1 -1
  264. package/dist/server/services/plan/parser-core.js.map +1 -1
  265. package/dist/server/services/plan/parser-migration.d.ts.map +1 -1
  266. package/dist/server/services/plan/parser-migration.js +0 -1
  267. package/dist/server/services/plan/parser-migration.js.map +1 -1
  268. package/dist/server/services/plan/parser.d.ts.map +1 -1
  269. package/dist/server/services/plan/parser.js +0 -1
  270. package/dist/server/services/plan/parser.js.map +1 -1
  271. package/dist/server/services/plan/progress-log.d.ts.map +1 -1
  272. package/dist/server/services/plan/progress-log.js +0 -1
  273. package/dist/server/services/plan/progress-log.js.map +1 -1
  274. package/dist/server/services/plan/prompt-builder.d.ts.map +1 -1
  275. package/dist/server/services/plan/prompt-builder.js +0 -1
  276. package/dist/server/services/plan/prompt-builder.js.map +1 -1
  277. package/dist/server/services/plan/readiness-planner.d.ts.map +1 -1
  278. package/dist/server/services/plan/readiness-planner.js +0 -1
  279. package/dist/server/services/plan/readiness-planner.js.map +1 -1
  280. package/dist/server/services/plan/review-gate.d.ts.map +1 -1
  281. package/dist/server/services/plan/review-gate.js +0 -1
  282. package/dist/server/services/plan/review-gate.js.map +1 -1
  283. package/dist/server/services/plan/state-reconciler.d.ts.map +1 -1
  284. package/dist/server/services/plan/state-reconciler.js +0 -1
  285. package/dist/server/services/plan/state-reconciler.js.map +1 -1
  286. package/dist/server/services/plan/types.d.ts +1 -0
  287. package/dist/server/services/plan/types.d.ts.map +1 -1
  288. package/dist/server/services/plan/types.js +0 -1
  289. package/dist/server/services/plan/types.js.map +1 -1
  290. package/dist/server/services/plan/watcher.d.ts.map +1 -1
  291. package/dist/server/services/plan/watcher.js +0 -1
  292. package/dist/server/services/plan/watcher.js.map +1 -1
  293. package/dist/server/services/platform-credentials.d.ts.map +1 -1
  294. package/dist/server/services/platform-credentials.js +0 -1
  295. package/dist/server/services/platform-credentials.js.map +1 -1
  296. package/dist/server/services/platform-token-lifecycle.d.ts +70 -0
  297. package/dist/server/services/platform-token-lifecycle.d.ts.map +1 -0
  298. package/dist/server/services/platform-token-lifecycle.js +156 -0
  299. package/dist/server/services/platform-token-lifecycle.js.map +1 -0
  300. package/dist/server/services/platform.d.ts +21 -56
  301. package/dist/server/services/platform.d.ts.map +1 -1
  302. package/dist/server/services/platform.js +98 -142
  303. package/dist/server/services/platform.js.map +1 -1
  304. package/dist/server/services/sentry.d.ts.map +1 -1
  305. package/dist/server/services/sentry.js +0 -1
  306. package/dist/server/services/sentry.js.map +1 -1
  307. package/dist/server/services/settings.d.ts +76 -2
  308. package/dist/server/services/settings.d.ts.map +1 -1
  309. package/dist/server/services/settings.js +127 -5
  310. package/dist/server/services/settings.js.map +1 -1
  311. package/dist/server/services/terminal/pty-manager.d.ts.map +1 -1
  312. package/dist/server/services/terminal/pty-manager.js +0 -1
  313. package/dist/server/services/terminal/pty-manager.js.map +1 -1
  314. package/dist/server/services/terminal/pty-utils.d.ts.map +1 -1
  315. package/dist/server/services/terminal/pty-utils.js +0 -1
  316. package/dist/server/services/terminal/pty-utils.js.map +1 -1
  317. package/dist/server/services/websocket/autocomplete.d.ts.map +1 -1
  318. package/dist/server/services/websocket/autocomplete.js +0 -1
  319. package/dist/server/services/websocket/autocomplete.js.map +1 -1
  320. package/dist/server/services/websocket/file-definition-handlers.d.ts.map +1 -1
  321. package/dist/server/services/websocket/file-definition-handlers.js +0 -1
  322. package/dist/server/services/websocket/file-definition-handlers.js.map +1 -1
  323. package/dist/server/services/websocket/file-download-handler.d.ts.map +1 -1
  324. package/dist/server/services/websocket/file-download-handler.js +0 -1
  325. package/dist/server/services/websocket/file-download-handler.js.map +1 -1
  326. package/dist/server/services/websocket/file-explorer-handlers.d.ts.map +1 -1
  327. package/dist/server/services/websocket/file-explorer-handlers.js +0 -1
  328. package/dist/server/services/websocket/file-explorer-handlers.js.map +1 -1
  329. package/dist/server/services/websocket/file-search-handlers.d.ts.map +1 -1
  330. package/dist/server/services/websocket/file-search-handlers.js +0 -1
  331. package/dist/server/services/websocket/file-search-handlers.js.map +1 -1
  332. package/dist/server/services/websocket/file-upload-handler.d.ts +2 -3
  333. package/dist/server/services/websocket/file-upload-handler.d.ts.map +1 -1
  334. package/dist/server/services/websocket/file-upload-handler.js +4 -7
  335. package/dist/server/services/websocket/file-upload-handler.js.map +1 -1
  336. package/dist/server/services/websocket/file-utils.d.ts.map +1 -1
  337. package/dist/server/services/websocket/file-utils.js +0 -1
  338. package/dist/server/services/websocket/file-utils.js.map +1 -1
  339. package/dist/server/services/websocket/git-branch-handlers.d.ts.map +1 -1
  340. package/dist/server/services/websocket/git-branch-handlers.js +19 -7
  341. package/dist/server/services/websocket/git-branch-handlers.js.map +1 -1
  342. package/dist/server/services/websocket/git-diff-handlers.d.ts.map +1 -1
  343. package/dist/server/services/websocket/git-diff-handlers.js +0 -1
  344. package/dist/server/services/websocket/git-diff-handlers.js.map +1 -1
  345. package/dist/server/services/websocket/git-handlers.d.ts.map +1 -1
  346. package/dist/server/services/websocket/git-handlers.js +58 -6
  347. package/dist/server/services/websocket/git-handlers.js.map +1 -1
  348. package/dist/server/services/websocket/git-head-watcher.d.ts.map +1 -1
  349. package/dist/server/services/websocket/git-head-watcher.js +0 -1
  350. package/dist/server/services/websocket/git-head-watcher.js.map +1 -1
  351. package/dist/server/services/websocket/git-log-handlers.d.ts.map +1 -1
  352. package/dist/server/services/websocket/git-log-handlers.js +0 -1
  353. package/dist/server/services/websocket/git-log-handlers.js.map +1 -1
  354. package/dist/server/services/websocket/git-pr-handlers.d.ts.map +1 -1
  355. package/dist/server/services/websocket/git-pr-handlers.js +0 -1
  356. package/dist/server/services/websocket/git-pr-handlers.js.map +1 -1
  357. package/dist/server/services/websocket/git-tag-handlers.d.ts.map +1 -1
  358. package/dist/server/services/websocket/git-tag-handlers.js +0 -1
  359. package/dist/server/services/websocket/git-tag-handlers.js.map +1 -1
  360. package/dist/server/services/websocket/git-utils.d.ts +18 -3
  361. package/dist/server/services/websocket/git-utils.d.ts.map +1 -1
  362. package/dist/server/services/websocket/git-utils.js +58 -8
  363. package/dist/server/services/websocket/git-utils.js.map +1 -1
  364. package/dist/server/services/websocket/git-worktree-handlers.d.ts.map +1 -1
  365. package/dist/server/services/websocket/git-worktree-handlers.js +230 -14
  366. package/dist/server/services/websocket/git-worktree-handlers.js.map +1 -1
  367. package/dist/server/services/websocket/handler-context.d.ts.map +1 -1
  368. package/dist/server/services/websocket/handler-context.js +0 -1
  369. package/dist/server/services/websocket/handler-context.js.map +1 -1
  370. package/dist/server/services/websocket/handler.d.ts +17 -1
  371. package/dist/server/services/websocket/handler.d.ts.map +1 -1
  372. package/dist/server/services/websocket/handler.js +57 -6
  373. package/dist/server/services/websocket/handler.js.map +1 -1
  374. package/dist/server/services/websocket/index.d.ts.map +1 -1
  375. package/dist/server/services/websocket/index.js +0 -1
  376. package/dist/server/services/websocket/index.js.map +1 -1
  377. package/dist/server/services/websocket/msg-id-tracker.d.ts.map +1 -1
  378. package/dist/server/services/websocket/msg-id-tracker.js +0 -1
  379. package/dist/server/services/websocket/msg-id-tracker.js.map +1 -1
  380. package/dist/server/services/websocket/plan-board-handlers.d.ts.map +1 -1
  381. package/dist/server/services/websocket/plan-board-handlers.js +0 -1
  382. package/dist/server/services/websocket/plan-board-handlers.js.map +1 -1
  383. package/dist/server/services/websocket/plan-execution-handlers.d.ts.map +1 -1
  384. package/dist/server/services/websocket/plan-execution-handlers.js +6 -2
  385. package/dist/server/services/websocket/plan-execution-handlers.js.map +1 -1
  386. package/dist/server/services/websocket/plan-handlers.d.ts.map +1 -1
  387. package/dist/server/services/websocket/plan-handlers.js +0 -1
  388. package/dist/server/services/websocket/plan-handlers.js.map +1 -1
  389. package/dist/server/services/websocket/plan-helpers.d.ts.map +1 -1
  390. package/dist/server/services/websocket/plan-helpers.js +0 -1
  391. package/dist/server/services/websocket/plan-helpers.js.map +1 -1
  392. package/dist/server/services/websocket/plan-issue-handlers.d.ts.map +1 -1
  393. package/dist/server/services/websocket/plan-issue-handlers.js +0 -1
  394. package/dist/server/services/websocket/plan-issue-handlers.js.map +1 -1
  395. package/dist/server/services/websocket/plan-sprint-handlers.d.ts.map +1 -1
  396. package/dist/server/services/websocket/plan-sprint-handlers.js +0 -1
  397. package/dist/server/services/websocket/plan-sprint-handlers.js.map +1 -1
  398. package/dist/server/services/websocket/quality-complexity.d.ts.map +1 -1
  399. package/dist/server/services/websocket/quality-complexity.js +78 -27
  400. package/dist/server/services/websocket/quality-complexity.js.map +1 -1
  401. package/dist/server/services/websocket/quality-eta.d.ts +47 -0
  402. package/dist/server/services/websocket/quality-eta.d.ts.map +1 -0
  403. package/dist/server/services/websocket/quality-eta.js +110 -0
  404. package/dist/server/services/websocket/quality-eta.js.map +1 -0
  405. package/dist/server/services/websocket/quality-grading.d.ts +69 -0
  406. package/dist/server/services/websocket/quality-grading.d.ts.map +1 -0
  407. package/dist/server/services/websocket/quality-grading.js +650 -0
  408. package/dist/server/services/websocket/quality-grading.js.map +1 -0
  409. package/dist/server/services/websocket/quality-handlers.d.ts.map +1 -1
  410. package/dist/server/services/websocket/quality-handlers.js +145 -8
  411. package/dist/server/services/websocket/quality-handlers.js.map +1 -1
  412. package/dist/server/services/websocket/quality-linting.d.ts.map +1 -1
  413. package/dist/server/services/websocket/quality-linting.js +0 -1
  414. package/dist/server/services/websocket/quality-linting.js.map +1 -1
  415. package/dist/server/services/websocket/quality-operations.d.ts +34 -0
  416. package/dist/server/services/websocket/quality-operations.d.ts.map +1 -0
  417. package/dist/server/services/websocket/quality-operations.js +47 -0
  418. package/dist/server/services/websocket/quality-operations.js.map +1 -0
  419. package/dist/server/services/websocket/quality-persistence.d.ts +23 -0
  420. package/dist/server/services/websocket/quality-persistence.d.ts.map +1 -1
  421. package/dist/server/services/websocket/quality-persistence.js +38 -12
  422. package/dist/server/services/websocket/quality-persistence.js.map +1 -1
  423. package/dist/server/services/websocket/quality-review-agent.d.ts +1 -1
  424. package/dist/server/services/websocket/quality-review-agent.d.ts.map +1 -1
  425. package/dist/server/services/websocket/quality-review-agent.js +105 -57
  426. package/dist/server/services/websocket/quality-review-agent.js.map +1 -1
  427. package/dist/server/services/websocket/quality-service.d.ts +12 -2
  428. package/dist/server/services/websocket/quality-service.d.ts.map +1 -1
  429. package/dist/server/services/websocket/quality-service.js +387 -72
  430. package/dist/server/services/websocket/quality-service.js.map +1 -1
  431. package/dist/server/services/websocket/quality-tools.d.ts +22 -1
  432. package/dist/server/services/websocket/quality-tools.d.ts.map +1 -1
  433. package/dist/server/services/websocket/quality-tools.js +55 -3
  434. package/dist/server/services/websocket/quality-tools.js.map +1 -1
  435. package/dist/server/services/websocket/quality-types.d.ts +52 -3
  436. package/dist/server/services/websocket/quality-types.d.ts.map +1 -1
  437. package/dist/server/services/websocket/quality-types.js +1 -2
  438. package/dist/server/services/websocket/quality-types.js.map +1 -1
  439. package/dist/server/services/websocket/session-handlers.d.ts +3 -1
  440. package/dist/server/services/websocket/session-handlers.d.ts.map +1 -1
  441. package/dist/server/services/websocket/session-handlers.js +57 -10
  442. package/dist/server/services/websocket/session-handlers.js.map +1 -1
  443. package/dist/server/services/websocket/session-history.d.ts.map +1 -1
  444. package/dist/server/services/websocket/session-history.js +3 -1
  445. package/dist/server/services/websocket/session-history.js.map +1 -1
  446. package/dist/server/services/websocket/session-initialization.d.ts.map +1 -1
  447. package/dist/server/services/websocket/session-initialization.js +158 -43
  448. package/dist/server/services/websocket/session-initialization.js.map +1 -1
  449. package/dist/server/services/websocket/session-registry.d.ts +25 -0
  450. package/dist/server/services/websocket/session-registry.d.ts.map +1 -1
  451. package/dist/server/services/websocket/session-registry.js +19 -1
  452. package/dist/server/services/websocket/session-registry.js.map +1 -1
  453. package/dist/server/services/websocket/settings-handlers.d.ts +1 -1
  454. package/dist/server/services/websocket/settings-handlers.d.ts.map +1 -1
  455. package/dist/server/services/websocket/settings-handlers.js +35 -5
  456. package/dist/server/services/websocket/settings-handlers.js.map +1 -1
  457. package/dist/server/services/websocket/skill-handlers.d.ts.map +1 -1
  458. package/dist/server/services/websocket/skill-handlers.js +0 -1
  459. package/dist/server/services/websocket/skill-handlers.js.map +1 -1
  460. package/dist/server/services/websocket/skill-watcher.d.ts.map +1 -1
  461. package/dist/server/services/websocket/skill-watcher.js +0 -1
  462. package/dist/server/services/websocket/skill-watcher.js.map +1 -1
  463. package/dist/server/services/websocket/tab-broadcast.d.ts +7 -2
  464. package/dist/server/services/websocket/tab-broadcast.d.ts.map +1 -1
  465. package/dist/server/services/websocket/tab-broadcast.js +10 -3
  466. package/dist/server/services/websocket/tab-broadcast.js.map +1 -1
  467. package/dist/server/services/websocket/tab-event-buffer.d.ts +97 -8
  468. package/dist/server/services/websocket/tab-event-buffer.d.ts.map +1 -1
  469. package/dist/server/services/websocket/tab-event-buffer.js +138 -13
  470. package/dist/server/services/websocket/tab-event-buffer.js.map +1 -1
  471. package/dist/server/services/websocket/tab-event-replay.d.ts +29 -13
  472. package/dist/server/services/websocket/tab-event-replay.d.ts.map +1 -1
  473. package/dist/server/services/websocket/tab-event-replay.js +55 -3
  474. package/dist/server/services/websocket/tab-event-replay.js.map +1 -1
  475. package/dist/server/services/websocket/tab-handlers.d.ts +9 -1
  476. package/dist/server/services/websocket/tab-handlers.d.ts.map +1 -1
  477. package/dist/server/services/websocket/tab-handlers.js +47 -3
  478. package/dist/server/services/websocket/tab-handlers.js.map +1 -1
  479. package/dist/server/services/websocket/terminal-handlers.d.ts.map +1 -1
  480. package/dist/server/services/websocket/terminal-handlers.js +39 -4
  481. package/dist/server/services/websocket/terminal-handlers.js.map +1 -1
  482. package/dist/server/services/websocket/types.d.ts +30 -7
  483. package/dist/server/services/websocket/types.d.ts.map +1 -1
  484. package/dist/server/services/websocket/types.js +12 -7
  485. package/dist/server/services/websocket/types.js.map +1 -1
  486. package/dist/server/utils/agent-manager.d.ts.map +1 -1
  487. package/dist/server/utils/agent-manager.js +0 -1
  488. package/dist/server/utils/agent-manager.js.map +1 -1
  489. package/dist/server/utils/paths.d.ts.map +1 -1
  490. package/dist/server/utils/paths.js +0 -1
  491. package/dist/server/utils/paths.js.map +1 -1
  492. package/dist/server/utils/port-manager.d.ts.map +1 -1
  493. package/dist/server/utils/port-manager.js +0 -1
  494. package/dist/server/utils/port-manager.js.map +1 -1
  495. package/dist/server/utils/port.d.ts.map +1 -1
  496. package/dist/server/utils/port.js +0 -1
  497. package/dist/server/utils/port.js.map +1 -1
  498. package/package.json +6 -4
  499. package/server/cli/eta-estimator.ts +249 -0
  500. package/server/cli/headless/claude-invoker-process.ts +0 -1
  501. package/server/cli/headless/claude-invoker-stall.ts +0 -1
  502. package/server/cli/headless/claude-invoker-stream.ts +0 -1
  503. package/server/cli/headless/claude-invoker-tools.ts +0 -1
  504. package/server/cli/headless/claude-invoker.ts +0 -1
  505. package/server/cli/headless/haiku-assessments.ts +0 -1
  506. package/server/cli/headless/headless-logger.ts +0 -1
  507. package/server/cli/headless/index.ts +0 -1
  508. package/server/cli/headless/native-timeout-detector.ts +0 -1
  509. package/server/cli/headless/output-utils.ts +0 -1
  510. package/server/cli/headless/prompt-utils.ts +0 -1
  511. package/server/cli/headless/resilient-runner.ts +0 -1
  512. package/server/cli/headless/retry-strategies.ts +0 -1
  513. package/server/cli/headless/runner.ts +0 -1
  514. package/server/cli/headless/stall-assessor.ts +93 -1
  515. package/server/cli/headless/tool-watchdog.ts +21 -1
  516. package/server/cli/headless/types.ts +0 -1
  517. package/server/cli/improvisation-attachments.ts +0 -1
  518. package/server/cli/improvisation-history-store.ts +4 -2
  519. package/server/cli/improvisation-movements.ts +0 -1
  520. package/server/cli/improvisation-output-queue.ts +29 -8
  521. package/server/cli/improvisation-retry.ts +0 -1
  522. package/server/cli/improvisation-session-manager.ts +54 -2
  523. package/server/cli/improvisation-types.ts +2 -1
  524. package/server/cli/retry/retry-best-result.ts +0 -1
  525. package/server/cli/retry/retry-context-loss.ts +0 -1
  526. package/server/cli/retry/retry-premature-completion.ts +1 -2
  527. package/server/cli/retry/retry-recovery-strategies.ts +0 -1
  528. package/server/cli/retry/retry-resume-strategy.ts +0 -1
  529. package/server/cli/retry/retry-runner-factory.ts +0 -1
  530. package/server/cli/retry/retry-tool-results.ts +0 -1
  531. package/server/cli/retry/retry-types.ts +0 -1
  532. package/server/engines/EngineEvent.ts +156 -0
  533. package/server/engines/claude/ClaudeCodeEngine.ts +404 -0
  534. package/server/engines/factory.ts +176 -0
  535. package/server/engines/opencode/OpenCodeEngine.ts +786 -0
  536. package/server/engines/opencode/OpenCodeServerManager.ts +577 -0
  537. package/server/engines/opencode/model-catalog.ts +217 -0
  538. package/server/engines/types.ts +173 -0
  539. package/server/index.ts +1 -2
  540. package/server/mcp/bouncer-cli.ts +0 -1
  541. package/server/mcp/bouncer-haiku.ts +21 -146
  542. package/server/mcp/bouncer-integration.ts +107 -6
  543. package/server/mcp/classifier/BouncerClassifier.ts +40 -0
  544. package/server/mcp/classifier/ClaudeBouncerClassifier.ts +189 -0
  545. package/server/mcp/classifier/OpenCodeBouncerClassifier.ts +305 -0
  546. package/server/mcp/classifier/factory.ts +195 -0
  547. package/server/mcp/security-analysis.ts +0 -1
  548. package/server/mcp/security-audit.ts +0 -1
  549. package/server/mcp/security-patterns.ts +0 -1
  550. package/server/mcp/server.ts +0 -1
  551. package/server/routes/files.ts +0 -1
  552. package/server/routes/improvise.ts +0 -1
  553. package/server/routes/index.ts +0 -1
  554. package/server/routes/instances.ts +0 -1
  555. package/server/routes/notifications.ts +0 -1
  556. package/server/server-setup.ts +0 -1
  557. package/server/services/analytics.ts +0 -1
  558. package/server/services/auth.ts +0 -1
  559. package/server/services/client-id.ts +0 -1
  560. package/server/services/file-explorer-ops.ts +0 -1
  561. package/server/services/files.ts +0 -1
  562. package/server/services/instances.ts +0 -1
  563. package/server/services/pathUtils.ts +0 -1
  564. package/server/services/plan/agent-loader.ts +0 -1
  565. package/server/services/plan/agent-resolver.ts +115 -0
  566. package/server/services/plan/agents/code-review.md +43 -11
  567. package/server/services/plan/board-config.ts +0 -1
  568. package/server/services/plan/composer.ts +63 -12
  569. package/server/services/plan/config-installer.ts +0 -1
  570. package/server/services/plan/dependency-resolver.ts +0 -1
  571. package/server/services/plan/executor.ts +48 -4
  572. package/server/services/plan/front-matter.ts +0 -1
  573. package/server/services/plan/issue-classification.ts +0 -1
  574. package/server/services/plan/issue-loader.ts +0 -1
  575. package/server/services/plan/issue-prompt-builder.ts +39 -2
  576. package/server/services/plan/issue-retry.ts +5 -2
  577. package/server/services/plan/issue-writer.ts +0 -1
  578. package/server/services/plan/output-manager.ts +0 -1
  579. package/server/services/plan/parser-core.ts +1 -1
  580. package/server/services/plan/parser-migration.ts +0 -1
  581. package/server/services/plan/parser.ts +0 -1
  582. package/server/services/plan/progress-log.ts +0 -1
  583. package/server/services/plan/prompt-builder.ts +0 -1
  584. package/server/services/plan/readiness-planner.ts +0 -1
  585. package/server/services/plan/review-gate.ts +0 -1
  586. package/server/services/plan/state-reconciler.ts +0 -1
  587. package/server/services/plan/types.ts +4 -1
  588. package/server/services/plan/watcher.ts +0 -1
  589. package/server/services/platform-credentials.ts +0 -1
  590. package/server/services/platform-token-lifecycle.ts +171 -0
  591. package/server/services/platform.ts +106 -148
  592. package/server/services/sentry.ts +0 -1
  593. package/server/services/settings.ts +161 -5
  594. package/server/services/terminal/pty-manager.ts +0 -1
  595. package/server/services/terminal/pty-utils.ts +0 -1
  596. package/server/services/websocket/autocomplete.ts +0 -1
  597. package/server/services/websocket/file-definition-handlers.ts +0 -1
  598. package/server/services/websocket/file-download-handler.ts +0 -1
  599. package/server/services/websocket/file-explorer-handlers.ts +0 -1
  600. package/server/services/websocket/file-search-handlers.ts +0 -1
  601. package/server/services/websocket/file-upload-handler.ts +6 -5
  602. package/server/services/websocket/file-utils.ts +0 -1
  603. package/server/services/websocket/git-branch-handlers.ts +20 -7
  604. package/server/services/websocket/git-diff-handlers.ts +0 -1
  605. package/server/services/websocket/git-handlers.ts +66 -10
  606. package/server/services/websocket/git-head-watcher.ts +0 -1
  607. package/server/services/websocket/git-log-handlers.ts +0 -1
  608. package/server/services/websocket/git-pr-handlers.ts +0 -1
  609. package/server/services/websocket/git-tag-handlers.ts +0 -1
  610. package/server/services/websocket/git-utils.ts +69 -9
  611. package/server/services/websocket/git-worktree-handlers.ts +260 -17
  612. package/server/services/websocket/handler-context.ts +0 -1
  613. package/server/services/websocket/handler.ts +62 -6
  614. package/server/services/websocket/index.ts +0 -1
  615. package/server/services/websocket/msg-id-tracker.ts +0 -1
  616. package/server/services/websocket/plan-board-handlers.ts +0 -1
  617. package/server/services/websocket/plan-execution-handlers.ts +6 -2
  618. package/server/services/websocket/plan-handlers.ts +0 -1
  619. package/server/services/websocket/plan-helpers.ts +0 -1
  620. package/server/services/websocket/plan-issue-handlers.ts +0 -1
  621. package/server/services/websocket/plan-sprint-handlers.ts +0 -1
  622. package/server/services/websocket/quality-complexity.ts +80 -27
  623. package/server/services/websocket/quality-eta.ts +155 -0
  624. package/server/services/websocket/quality-grading.ts +834 -0
  625. package/server/services/websocket/quality-handlers.ts +153 -8
  626. package/server/services/websocket/quality-linting.ts +0 -1
  627. package/server/services/websocket/quality-operations.ts +72 -0
  628. package/server/services/websocket/quality-persistence.ts +47 -8
  629. package/server/services/websocket/quality-review-agent.ts +154 -65
  630. package/server/services/websocket/quality-service.ts +415 -68
  631. package/server/services/websocket/quality-tools.ts +62 -3
  632. package/server/services/websocket/quality-types.ts +61 -4
  633. package/server/services/websocket/session-handlers.ts +64 -11
  634. package/server/services/websocket/session-history.ts +3 -1
  635. package/server/services/websocket/session-initialization.ts +189 -47
  636. package/server/services/websocket/session-registry.ts +37 -1
  637. package/server/services/websocket/settings-handlers.ts +41 -5
  638. package/server/services/websocket/skill-handlers.ts +0 -1
  639. package/server/services/websocket/skill-watcher.ts +0 -1
  640. package/server/services/websocket/tab-broadcast.ts +10 -3
  641. package/server/services/websocket/tab-event-buffer.ts +143 -12
  642. package/server/services/websocket/tab-event-replay.ts +70 -4
  643. package/server/services/websocket/tab-handlers.ts +53 -6
  644. package/server/services/websocket/terminal-handlers.ts +39 -3
  645. package/server/services/websocket/types.ts +39 -8
  646. package/server/utils/agent-manager.ts +0 -1
  647. package/server/utils/paths.ts +0 -1
  648. package/server/utils/port-manager.ts +0 -1
  649. package/server/utils/port.ts +0 -1
@@ -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
  /**
5
4
  * Session Registry — Persistent tab-to-session mapping
@@ -13,6 +12,19 @@
13
12
 
14
13
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
15
14
  import { join } from 'node:path'
15
+ import type { EngineId } from './types.js'
16
+
17
+ /**
18
+ * Per-tab engine override persisted alongside the tab record. Survives
19
+ * WebSocket disconnects + process restarts — the web client re-reads it on
20
+ * `tabInitialized` / `activeTabs` to restore the picker state after a
21
+ * reconnect.
22
+ */
23
+ export interface TabEngineOverride {
24
+ engine: EngineId
25
+ model: string
26
+ effortLevel: string
27
+ }
16
28
 
17
29
  export interface RegisteredTab {
18
30
  sessionId: string
@@ -30,6 +42,12 @@ export interface RegisteredTab {
30
42
  hasPersistedHistory?: boolean
31
43
  worktreePath?: string
32
44
  worktreeBranch?: string
45
+ /**
46
+ * Per-tab engine override. Absent when the tab uses the global defaults —
47
+ * authored from the web via `setTabEngine`, cleared by passing `null` to
48
+ * {@link SessionRegistry.updateTabEngineOverride}.
49
+ */
50
+ engineOverride?: TabEngineOverride
33
51
  }
34
52
 
35
53
  interface RegistryData {
@@ -242,6 +260,24 @@ export class SessionRegistry {
242
260
  }
243
261
  }
244
262
 
263
+ /**
264
+ * Update or clear the per-tab engine override. Pass `null` to remove the
265
+ * override and route future prompts through the global defaults. Writes
266
+ * through to disk so the override survives WS disconnects — that's the
267
+ * core persistence guarantee of IS-019.
268
+ */
269
+ updateTabEngineOverride(tabId: string, override: TabEngineOverride | null): void {
270
+ const tab = this.data.tabs[tabId]
271
+ if (!tab) return
272
+ if (override === null) {
273
+ delete tab.engineOverride
274
+ } else {
275
+ tab.engineOverride = { ...override }
276
+ }
277
+ tab.lastActivityAt = new Date().toISOString()
278
+ this.save()
279
+ }
280
+
245
281
  /**
246
282
  * Reorder tabs. Accepts an ordered array of tabIds and reassigns order values.
247
283
  */
@@ -1,25 +1,61 @@
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 { spawn } from 'node:child_process';
5
4
  import { existsSync, mkdirSync, unlinkSync, writeFileSync } from 'node:fs';
6
5
  import { join } from 'node:path';
7
- import { getSettings, setEffortLevel, setModel } from '../settings.js';
6
+ import {
7
+ getSettings,
8
+ isEngineSwapEnabled,
9
+ setBouncerClassifier,
10
+ setEffortLevel,
11
+ setModel,
12
+ } from '../settings.js';
8
13
  import type { HandlerContext } from './handler-context.js';
9
14
  import type { WebSocketMessage, WSContext } from './types.js';
10
15
 
16
+ /**
17
+ * Return the stored settings with the resolved `engineSwap` boolean patched
18
+ * in, so web clients always see the effective flag value (env-var override,
19
+ * NODE_ENV default, etc.) rather than the raw — possibly `undefined` —
20
+ * stored field.
21
+ */
22
+ function getSettingsWithResolvedFlags() {
23
+ return { ...getSettings(), engineSwap: isEngineSwapEnabled() };
24
+ }
25
+
11
26
  export function handleGetSettings(ctx: HandlerContext, ws: WSContext): void {
12
- ctx.send(ws, { type: 'settings', data: getSettings() });
27
+ ctx.send(ws, { type: 'settings', data: getSettingsWithResolvedFlags() });
13
28
  }
14
29
 
15
- export function handleUpdateSettings(ctx: HandlerContext, _ws: WSContext, msg: WebSocketMessage): void {
30
+ export function handleUpdateSettings(ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage): void {
16
31
  if (msg.data?.model !== undefined) {
17
32
  setModel(msg.data.model);
18
33
  }
19
34
  if (msg.data?.effortLevel !== undefined) {
20
35
  setEffortLevel(msg.data.effortLevel);
21
36
  }
22
- ctx.broadcastToAll({ type: 'settingsUpdated', data: getSettings() });
37
+ if (msg.data?.bouncerClassifier !== undefined) {
38
+ try {
39
+ setBouncerClassifier(msg.data.bouncerClassifier);
40
+ } catch (err) {
41
+ // Reject crafted payloads (non-eligible model, bad engine) — surface
42
+ // the reason to the requester and skip the broadcast so other clients
43
+ // keep showing the previous valid config.
44
+ const message = err instanceof Error ? err.message : String(err);
45
+ ctx.send(ws, {
46
+ type: 'error',
47
+ data: {
48
+ scope: 'bouncerClassifier',
49
+ message,
50
+ },
51
+ });
52
+ // Still echo the current settings back to the requester so the UI can
53
+ // revert its optimistic update.
54
+ ctx.send(ws, { type: 'settings', data: getSettingsWithResolvedFlags() });
55
+ return;
56
+ }
57
+ }
58
+ ctx.broadcastToAll({ type: 'settingsUpdated', data: getSettingsWithResolvedFlags() });
23
59
  }
24
60
 
25
61
  export async function generateNotificationSummary(
@@ -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 { execSync } from 'node:child_process';
5
4
  import { existsSync, readdirSync, readFileSync } from 'node:fs';
@@ -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 { type FSWatcher, watch } from 'node:fs';
5
4
  import { findSkillsDir } from '../../utils/paths.js';
@@ -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
  /**
5
4
  * Tab-scoped broadcast helper.
@@ -17,22 +16,30 @@
17
16
  */
18
17
 
19
18
  import type { HandlerContext } from './handler-context.js'
20
- import type { WebSocketResponse } from './types.js'
19
+ import type { EngineId, WebSocketResponse } from './types.js'
21
20
 
22
21
  type TabScopedEventType = WebSocketResponse['type']
23
22
 
24
23
  /**
25
24
  * Record + broadcast a tab-scoped event in one call. Returns the assigned
26
25
  * sequence number purely for logging/tests — callers rarely need it.
26
+ *
27
+ * `engine` is optional: when supplied (typically by session-driven movement
28
+ * events), it rides on the wire envelope so the web client can render
29
+ * engine-specific affordances. Buffer replay preserves it because the engine
30
+ * is part of the envelope produced for each broadcast.
27
31
  */
28
32
  export function broadcastTabEvent(
29
33
  ctx: HandlerContext,
30
34
  tabId: string,
31
35
  type: TabScopedEventType,
32
36
  data: unknown,
37
+ engine?: EngineId,
33
38
  ): number {
34
39
  const buffer = ctx.tabEventBuffers.getOrCreate(tabId)
35
40
  const seq = buffer.record(type, data)
36
- ctx.broadcastToAll({ type, tabId, data, seq })
41
+ const envelope: WebSocketResponse = { type, tabId, data, seq }
42
+ if (engine) envelope.engine = engine
43
+ ctx.broadcastToAll(envelope)
37
44
  return seq
38
45
  }
@@ -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
  /**
5
4
  * Tab event buffer — monotonic, bounded replay log for tab-scoped broadcasts.
@@ -43,22 +42,59 @@ export interface BufferedEvent {
43
42
  data: unknown
44
43
  /** `Date.now()` at record time. Used for age-based eviction. */
45
44
  timestamp: number
45
+ /**
46
+ * Approximate serialized byte size of `data`. Computed once at record
47
+ * time so eviction can enforce a memory cap without re-stringifying on
48
+ * every check. Type and seq overhead is small; we only bill `data` here.
49
+ */
50
+ byteSize: number
46
51
  }
47
52
 
48
53
  /**
49
54
  * Bounded replay log for a single tab.
50
55
  *
51
- * Size/age limits are parameterised for testability but defaulted to values
52
- * that comfortably cover real-world reconnect windows.
56
+ * Size/age/byte limits are parameterised for testability but defaulted to
57
+ * values that comfortably cover real-world reconnect windows for long-running
58
+ * coding-agent tasks (multi-tool, multi-minute).
59
+ *
60
+ * ## Replay-gap detection
61
+ *
62
+ * The buffer tracks `evictedThroughSeq` — the highest seq that has ever been
63
+ * evicted (0 if nothing has been evicted). A web client whose `lastSeenSeq`
64
+ * is below this value has missed events the buffer can no longer supply, and
65
+ * an incremental replay would produce a silent gap. Callers should consult
66
+ * `hasGapSince` before relying on `getSince` for incremental replay; on a
67
+ * gap they should fall back to a full snapshot path (e.g. `outputHistory`).
68
+ *
69
+ * ## Eviction is FIFO with three caps
70
+ *
71
+ * Events are evicted from the front when ANY of these limits is exceeded:
72
+ * - count: `maxEvents` (default 10k)
73
+ * - age: `maxAgeMs` (default 60 min)
74
+ * - bytes: `maxTotalBytes` (default 32 MB)
75
+ *
76
+ * The byte cap is the safety belt against pathological events (e.g. a 50 MB
77
+ * grep result streamed as one event). Without it, count- and age-based caps
78
+ * still allow a single tab to hoard arbitrary memory.
53
79
  */
54
80
  export class TabEventBuffer {
55
81
  private readonly events: BufferedEvent[] = []
56
82
  private nextSeq = 1
83
+ /**
84
+ * Highest seq that has been evicted from the buffer. 0 means nothing has
85
+ * been evicted yet (buffer is operating within its window). Monotonically
86
+ * non-decreasing — eviction always happens from the front of the FIFO, in
87
+ * seq order, so the most recently evicted seq is always the highest.
88
+ */
89
+ private evictedThroughSeq = 0
90
+ /** Approximate sum of `byteSize` over still-resident events. */
91
+ private totalBytes = 0
57
92
 
58
93
  constructor(
59
94
  private readonly maxEvents: number = DEFAULT_MAX_EVENTS,
60
95
  private readonly maxAgeMs: number = DEFAULT_MAX_AGE_MS,
61
96
  private readonly now: () => number = Date.now,
97
+ private readonly maxTotalBytes: number = DEFAULT_MAX_TOTAL_BYTES,
62
98
  ) {}
63
99
 
64
100
  /**
@@ -70,7 +106,9 @@ export class TabEventBuffer {
70
106
  */
71
107
  record(type: string, data: unknown): number {
72
108
  const seq = this.nextSeq++
73
- this.events.push({ seq, type, data, timestamp: this.now() })
109
+ const byteSize = estimateByteSize(data)
110
+ this.events.push({ seq, type, data, timestamp: this.now(), byteSize })
111
+ this.totalBytes += byteSize
74
112
  this.evict()
75
113
  return seq
76
114
  }
@@ -79,6 +117,11 @@ export class TabEventBuffer {
79
117
  * Return all still-buffered events with `seq > afterSeq`, in original
80
118
  * order. Returns an empty array if nothing newer is buffered (either the
81
119
  * web is caught up or the window has rolled past).
120
+ *
121
+ * NOTE: This does not detect or signal replay gaps. Pair with
122
+ * `hasGapSince(afterSeq)` to know whether a returned array is a complete
123
+ * incremental replay or a partial one (events between `afterSeq` and the
124
+ * oldest surviving seq have been evicted and are no longer available).
82
125
  */
83
126
  getSince(afterSeq: number): BufferedEvent[] {
84
127
  this.evict()
@@ -89,6 +132,29 @@ export class TabEventBuffer {
89
132
  return out
90
133
  }
91
134
 
135
+ /**
136
+ * True when an incremental replay starting from `afterSeq` would silently
137
+ * skip events that the buffer has already evicted. Used by the replay
138
+ * orchestrator to decide whether to fall back to a full snapshot rather
139
+ * than emit a partial event stream the web can't reconstruct.
140
+ *
141
+ * `afterSeq < evictedThroughSeq` means the next event the caller expects
142
+ * (`afterSeq + 1`) is at or below the eviction frontier — that event has
143
+ * already been dropped from memory.
144
+ */
145
+ hasGapSince(afterSeq: number): boolean {
146
+ this.evict()
147
+ return afterSeq < this.evictedThroughSeq
148
+ }
149
+
150
+ /**
151
+ * Highest seq that has been evicted from this buffer; 0 if nothing has been
152
+ * evicted yet. Exposed for telemetry and gap-recovery decisions.
153
+ */
154
+ getEvictedThroughSeq(): number {
155
+ return this.evictedThroughSeq
156
+ }
157
+
92
158
  /** Current highest assigned seq (monotonic; not reset by eviction). */
93
159
  currentSeq(): number {
94
160
  return this.nextSeq - 1
@@ -99,20 +165,66 @@ export class TabEventBuffer {
99
165
  return this.events.length
100
166
  }
101
167
 
168
+ /** Approximate bytes held by `data` payloads currently in memory. For tests/telemetry. */
169
+ byteSize(): number {
170
+ return this.totalBytes
171
+ }
172
+
102
173
  /**
103
174
  * Drop events older than `maxAgeMs` from the front, then enforce
104
- * `maxEvents` by trimming the front further if needed. Eviction keeps the
105
- * newest events — they're the ones the web is most likely to still need.
175
+ * `maxEvents` and `maxTotalBytes` by trimming the front further if needed.
176
+ * Eviction keeps the newest events — they're the ones the web is most
177
+ * likely to still need.
178
+ *
179
+ * Each evicted seq advances `evictedThroughSeq` so callers can detect
180
+ * replay gaps. The FIFO ensures we always evict in seq order, so the last
181
+ * evicted seq is always the highest seen so far.
182
+ *
183
+ * The byte cap is enforced LAST so that count- and age-based eviction get
184
+ * a chance first; a chatty-but-small session evicts on age before it ever
185
+ * touches the byte cap, which keeps the usual case predictable.
106
186
  */
107
187
  private evict(): void {
108
188
  const cutoff = this.now() - this.maxAgeMs
109
189
  while (this.events.length > 0 && this.events[0].timestamp < cutoff) {
110
- this.events.shift()
190
+ this.popOldest()
111
191
  }
112
192
  while (this.events.length > this.maxEvents) {
113
- this.events.shift()
193
+ this.popOldest()
194
+ }
195
+ while (this.events.length > 0 && this.totalBytes > this.maxTotalBytes) {
196
+ this.popOldest()
114
197
  }
115
198
  }
199
+
200
+ private popOldest(): void {
201
+ const evicted = this.events.shift()
202
+ if (!evicted) return
203
+ this.evictedThroughSeq = evicted.seq
204
+ this.totalBytes -= evicted.byteSize
205
+ if (this.totalBytes < 0) this.totalBytes = 0
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Estimate `data`'s serialized byte size for the eviction byte cap. Uses
211
+ * `JSON.stringify` because that's what hits the wire; falls back to a small
212
+ * default on circular structures so we don't crash the broadcast path.
213
+ *
214
+ * `Buffer.byteLength` would give us UTF-8 bytes vs UTF-16 code units, but on
215
+ * Node `JSON.stringify(...).length` is close enough (within a small constant
216
+ * factor for ASCII-heavy payloads) and avoids an extra allocation.
217
+ */
218
+ function estimateByteSize(data: unknown): number {
219
+ if (data === undefined || data === null) return 0
220
+ try {
221
+ return JSON.stringify(data).length
222
+ } catch {
223
+ // Circular reference, BigInt, etc. — bill a small fixed cost so the
224
+ // byte cap still has SOME signal. We won't be able to wire-serialize
225
+ // this either, but that's a separate problem.
226
+ return 256
227
+ }
116
228
  }
117
229
 
118
230
  /**
@@ -153,7 +265,26 @@ export class TabEventBufferRegistry {
153
265
  }
154
266
  }
155
267
 
156
- /** 1000 events per tab covers typical reconnect windows comfortably. */
157
- export const DEFAULT_MAX_EVENTS = 1000
158
- /** 15 minutes of history is more than enough for the longest plausible web reconnect. */
159
- export const DEFAULT_MAX_AGE_MS = 15 * 60 * 1000
268
+ /**
269
+ * 10,000 events per tab.
270
+ *
271
+ * Sized for long-running coding-agent tasks (multi-tool, multi-minute) plus
272
+ * laptop sleep/wake reconnect windows. Worst-case observed: a 14-minute
273
+ * session with ~120 tool calls produces ~1.5–3k tab-scoped events; 10× that
274
+ * gives headroom for parallel agents and chatty improvisation. Memory
275
+ * footprint at ~500B/event = ~5MB per tab; the local-only single-tenant
276
+ * deployment makes this a non-issue.
277
+ */
278
+ export const DEFAULT_MAX_EVENTS = 10_000
279
+ /**
280
+ * 60 minutes of history. Covers laptop sleep/wake, long meetings between
281
+ * sessions, and the largest plausible reconnect window that a tab might
282
+ * legitimately want to recover incrementally instead of starting fresh.
283
+ */
284
+ export const DEFAULT_MAX_AGE_MS = 60 * 60 * 1000
285
+ /**
286
+ * 32 MB safety belt against pathological events (large grep results, full
287
+ * file reads streamed inline). Eviction by bytes guarantees a single tab
288
+ * can't hoard arbitrary memory regardless of count/age limits.
289
+ */
290
+ export const DEFAULT_MAX_TOTAL_BYTES = 32 * 1024 * 1024
@@ -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
  /**
5
4
  * Replay tab-scoped events missed during a transport gap.
@@ -14,23 +13,88 @@
14
13
  * events live.
15
14
  */
16
15
 
16
+ import { captureException } from '../sentry.js'
17
17
  import type { HandlerContext } from './handler-context.js'
18
18
  import type { WebSocketResponse, WSContext } from './types.js'
19
19
 
20
+ /** Result of a replay attempt — used by callers (and tests) for telemetry. */
21
+ export interface ReplayResult {
22
+ /** Number of events sent to the web during this replay. */
23
+ sentCount: number
24
+ /**
25
+ * True when the buffer had already evicted events that fell between the
26
+ * web's `lastSeenSeq` and the oldest surviving seq. The replay is partial;
27
+ * the web's incremental state is now provably stale and the caller should
28
+ * fall back to a full snapshot path (e.g. `outputHistory`).
29
+ */
30
+ hadGap: boolean
31
+ /**
32
+ * If `hadGap`, the highest seq that was evicted (so the gap range is
33
+ * `(lastSeenSeq + 1) .. evictedThroughSeq`). Undefined when no gap.
34
+ */
35
+ evictedThroughSeq?: number
36
+ /**
37
+ * If `hadGap`, the seq the web requested replay from. Echoed into
38
+ * telemetry so log entries are self-contained.
39
+ */
40
+ lastSeenSeq?: number
41
+ }
42
+
20
43
  /**
21
44
  * Replay tab events with `seq > lastSeenSeq` to `ws`. Silently no-ops when
22
45
  * the buffer is empty or `lastSeenSeq` is unset (full init, not a resume).
46
+ *
47
+ * Returns a `ReplayResult` so the caller can detect a partial replay (the
48
+ * buffer evicted events the web is asking about) and decide whether to send
49
+ * a recovery snapshot. This is the load-bearing telemetry surface for the
50
+ * "long-running task output disappears mid-stream" failure mode — a `hadGap`
51
+ * here is the smoking gun.
23
52
  */
24
53
  export function replayTabEventsSince(
25
54
  ctx: HandlerContext,
26
55
  ws: WSContext,
27
56
  tabId: string,
28
57
  lastSeenSeq: number | undefined,
29
- ): void {
30
- if (lastSeenSeq === undefined) return
58
+ ): ReplayResult {
59
+ if (lastSeenSeq === undefined) return { sentCount: 0, hadGap: false }
31
60
 
32
61
  const buffer = ctx.tabEventBuffers.get(tabId)
33
- if (!buffer) return
62
+ if (!buffer) return { sentCount: 0, hadGap: false }
63
+
64
+ const hadGap = buffer.hasGapSince(lastSeenSeq)
65
+ const evictedThroughSeq = hadGap ? buffer.getEvictedThroughSeq() : undefined
66
+
67
+ if (hadGap) {
68
+ // Replay is structurally incomplete. Surface a single, structured warning
69
+ // so we can grep/Sentry-search for the failure mode without spamming logs
70
+ // on every event.
71
+ const message =
72
+ `[tab-replay] gap detected for tab=${tabId}: web requested replay from seq=${lastSeenSeq}, ` +
73
+ `but buffer has evicted through seq=${evictedThroughSeq}. ` +
74
+ `Events (${lastSeenSeq + 1}..${evictedThroughSeq}) are unavailable; the web's ` +
75
+ `incremental state is stale and a full snapshot will be sent instead.`
76
+ console.warn(message)
77
+ try {
78
+ captureException(new Error('TabEventBuffer replay gap'), {
79
+ context: 'tab-event-replay',
80
+ tabId,
81
+ lastSeenSeq,
82
+ evictedThroughSeq,
83
+ bufferCurrentSeq: buffer.currentSeq(),
84
+ gapSize: (evictedThroughSeq ?? 0) - lastSeenSeq,
85
+ })
86
+ } catch {
87
+ // Sentry transport errors must not break the replay path.
88
+ }
89
+ // CRITICAL: do NOT emit partial events. If we did, the web would advance
90
+ // its `tabSeqs` past the (lastSeenSeq+1 .. evictedThroughSeq) range and
91
+ // the subsequent snapshot would land in a tab that thinks it's caught up
92
+ // — silently rendering only the post-gap tail. Returning early without
93
+ // events forces the caller (`session-initialization.ts`) into the
94
+ // snapshot-fallback branch, which sends a fresh `outputHistory` payload
95
+ // with `replayGap: true` so the web can replace its tab state cleanly.
96
+ return { sentCount: 0, hadGap: true, evictedThroughSeq, lastSeenSeq }
97
+ }
34
98
 
35
99
  const events = buffer.getSince(lastSeenSeq)
36
100
  for (const event of events) {
@@ -39,4 +103,6 @@ export function replayTabEventsSince(
39
103
  // `WebSocketResponse['type']`. Narrow here without an extra runtime check.
40
104
  ctx.send(ws, { type: event.type as WebSocketResponse['type'], tabId, data: event.data, seq: event.seq })
41
105
  }
106
+
107
+ return { sentCount: events.length, hadGap: false, lastSeenSeq }
42
108
  }
@@ -1,14 +1,14 @@
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
- import { buildOutputHistory, setupSessionListeners } from './session-handlers.js';
8
- import type { WebSocketMessage, WSContext } from './types.js';
6
+ import { buildOutputHistory, resolveEngineForSession, setupSessionListeners } from './session-handlers.js';
7
+ import type { TabEngineOverride } from './session-registry.js';
8
+ import { DEFAULT_ENGINE_ID, type EngineId, type WebSocketMessage, type WSContext } from './types.js';
9
9
 
10
10
  function buildActiveTabData(
11
- regTab: { tabName: string; createdAt: string; order: number; hasUnviewedCompletion?: boolean; sessionId: string },
11
+ regTab: { tabName: string; createdAt: string; order: number; hasUnviewedCompletion?: boolean; sessionId: string; engineOverride?: TabEngineOverride },
12
12
  session: ImprovisationSessionManager,
13
13
  worktreePath: string | undefined,
14
14
  worktreeBranch: string | undefined,
@@ -18,17 +18,19 @@ function buildActiveTabData(
18
18
  createdAt: regTab.createdAt,
19
19
  order: regTab.order,
20
20
  hasUnviewedCompletion: regTab.hasUnviewedCompletion,
21
+ engine: resolveEngineForSession(session),
21
22
  sessionInfo: session.getSessionInfo(),
22
23
  isExecuting: session.isExecuting,
23
24
  outputHistory: buildOutputHistory(session),
24
25
  executionEvents: session.isExecuting ? session.getExecutionEventLog() : undefined,
25
26
  ...(session.isExecuting && session.executionStartTimestamp ? { executionStartTimestamp: session.executionStartTimestamp } : {}),
26
27
  ...(worktreePath ? { worktreePath, worktreeBranch } : {}),
28
+ ...(regTab.engineOverride ? { engineOverride: regTab.engineOverride } : {}),
27
29
  };
28
30
  }
29
31
 
30
32
  function buildInactiveTabData(
31
- regTab: { tabName: string; createdAt: string; order: number; hasUnviewedCompletion?: boolean; sessionId: string },
33
+ regTab: { tabName: string; createdAt: string; order: number; hasUnviewedCompletion?: boolean; sessionId: string; engineOverride?: TabEngineOverride },
32
34
  worktreePath: string | undefined,
33
35
  worktreeBranch: string | undefined,
34
36
  ): Record<string, unknown> {
@@ -37,10 +39,12 @@ function buildInactiveTabData(
37
39
  createdAt: regTab.createdAt,
38
40
  order: regTab.order,
39
41
  hasUnviewedCompletion: regTab.hasUnviewedCompletion,
42
+ engine: DEFAULT_ENGINE_ID,
40
43
  sessionId: regTab.sessionId,
41
44
  isExecuting: false,
42
45
  outputHistory: [],
43
46
  ...(worktreePath ? { worktreePath, worktreeBranch } : {}),
47
+ ...(regTab.engineOverride ? { engineOverride: regTab.engineOverride } : {}),
44
48
  };
45
49
  }
46
50
 
@@ -102,6 +106,41 @@ export function handleMarkTabViewed(ctx: HandlerContext, _ws: WSContext, tabId:
102
106
  });
103
107
  }
104
108
 
109
+ /**
110
+ * Persist a per-tab engine override. `msg.data.override` is either a full
111
+ * `{ engine, model, effortLevel }` payload or `null` to clear the override.
112
+ * Persisted via the session registry so the override survives WebSocket
113
+ * disconnects — the core guarantee of IS-019. Broadcasts the change to all
114
+ * connected clients so multi-device sessions stay in sync.
115
+ */
116
+ export function handleSetTabEngine(ctx: HandlerContext, _ws: WSContext, msg: WebSocketMessage, tabId: string, workingDir: string): void {
117
+ const raw = msg.data?.override;
118
+ let override: TabEngineOverride | null;
119
+ if (raw === null || raw === undefined) {
120
+ override = null;
121
+ } else if (
122
+ typeof raw === 'object' &&
123
+ (raw.engine === 'claude-code' || raw.engine === 'opencode') &&
124
+ typeof raw.model === 'string' && raw.model.length > 0 &&
125
+ typeof raw.effortLevel === 'string' && raw.effortLevel.length > 0
126
+ ) {
127
+ override = { engine: raw.engine, model: raw.model, effortLevel: raw.effortLevel };
128
+ } else {
129
+ // Malformed payload — ignore rather than crash. The client will re-emit
130
+ // from the canonical server-side value on the next reconnect.
131
+ return;
132
+ }
133
+
134
+ const registry = ctx.getRegistry(workingDir);
135
+ registry.updateTabEngineOverride(tabId, override);
136
+
137
+ ctx.broadcastToAll({
138
+ type: 'tabEngineOverride',
139
+ tabId,
140
+ data: { tabId, override },
141
+ });
142
+ }
143
+
105
144
  export async function handleCreateTab(ctx: HandlerContext, ws: WSContext, workingDir: string, tabName?: string, optimisticTabId?: string): Promise<void> {
106
145
  const registry = ctx.getRegistry(workingDir);
107
146
 
@@ -110,14 +149,18 @@ export async function handleCreateTab(ctx: HandlerContext, ws: WSContext, workin
110
149
  const existingSession = registry.getTabSession(tabId);
111
150
  if (existingSession) {
112
151
  const regTab = registry.getTab(tabId);
152
+ const existingSessionObj = ctx.sessions.get(existingSession);
153
+ const engine: EngineId = resolveEngineForSession(existingSessionObj);
113
154
  ctx.broadcastToAll({
114
155
  type: 'tabCreated',
156
+ engine,
115
157
  data: {
116
158
  tabId,
117
159
  tabName: regTab?.tabName || 'Chat',
118
160
  createdAt: regTab?.createdAt,
119
161
  order: regTab?.order,
120
- sessionInfo: ctx.sessions.get(existingSession)?.getSessionInfo(),
162
+ engine,
163
+ sessionInfo: existingSessionObj?.getSessionInfo(),
121
164
  }
122
165
  });
123
166
  return;
@@ -134,14 +177,17 @@ export async function handleCreateTab(ctx: HandlerContext, ws: WSContext, workin
134
177
 
135
178
  registry.registerTab(tabId, sessionId, tabName);
136
179
  const registeredTab = registry.getTab(tabId);
180
+ const engine: EngineId = resolveEngineForSession(session);
137
181
 
138
182
  ctx.broadcastToAll({
139
183
  type: 'tabCreated',
184
+ engine,
140
185
  data: {
141
186
  tabId,
142
187
  tabName: registeredTab?.tabName || 'Chat',
143
188
  createdAt: registeredTab?.createdAt,
144
189
  order: registeredTab?.order,
190
+ engine,
145
191
  sessionInfo: session.getSessionInfo(),
146
192
  }
147
193
  });
@@ -149,6 +195,7 @@ export async function handleCreateTab(ctx: HandlerContext, ws: WSContext, workin
149
195
  ctx.send(ws, {
150
196
  type: 'tabInitialized',
151
197
  tabId,
198
+ engine,
152
199
  data: session.getSessionInfo()
153
200
  });
154
201
  }