hammoc 1.5.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (372) hide show
  1. package/README.md +12 -4
  2. package/package.json +2 -2
  3. package/packages/client/dist/assets/{agentExampleHighlight-BgwTm15v.js → agentExampleHighlight-7Olvqdpf.js} +1 -1
  4. package/packages/client/dist/assets/{commandTokenHighlight-BljHwnrK.js → commandTokenHighlight-DtKwh4lx.js} +1 -1
  5. package/packages/client/dist/assets/index-B09doO8H.js +139 -0
  6. package/packages/client/dist/assets/index-Bee52CKG.js +1569 -0
  7. package/packages/client/dist/assets/index-CVy582lU.css +32 -0
  8. package/packages/client/dist/assets/{index-D3LxqW3f.js → index-DLHBUw8c.js} +1 -1
  9. package/packages/client/dist/assets/{snippetTokenHighlight-DWsaQXX0.js → snippetTokenHighlight-C8O688f4.js} +1 -1
  10. package/packages/client/dist/index.html +2 -2
  11. package/packages/client/dist/sw.js +2 -2
  12. package/packages/server/dist/_dbg_agent_rows.d.ts +2 -0
  13. package/packages/server/dist/_dbg_agent_rows.d.ts.map +1 -0
  14. package/packages/server/dist/_dbg_agent_rows.js +60 -0
  15. package/packages/server/dist/_dbg_agent_rows.js.map +1 -0
  16. package/packages/server/dist/_dbg_flicker.d.ts +2 -0
  17. package/packages/server/dist/_dbg_flicker.d.ts.map +1 -0
  18. package/packages/server/dist/_dbg_flicker.js +38 -0
  19. package/packages/server/dist/_dbg_flicker.js.map +1 -0
  20. package/packages/server/dist/_harness_cli.d.ts +2 -0
  21. package/packages/server/dist/_harness_cli.d.ts.map +1 -0
  22. package/packages/server/dist/_harness_cli.js +81 -0
  23. package/packages/server/dist/_harness_cli.js.map +1 -0
  24. package/packages/server/dist/_harness_provcard.d.ts +2 -0
  25. package/packages/server/dist/_harness_provcard.d.ts.map +1 -0
  26. package/packages/server/dist/_harness_provcard.js +67 -0
  27. package/packages/server/dist/_harness_provcard.js.map +1 -0
  28. package/packages/server/dist/_replay_dump.d.ts +2 -0
  29. package/packages/server/dist/_replay_dump.d.ts.map +1 -0
  30. package/packages/server/dist/_replay_dump.js +62 -0
  31. package/packages/server/dist/_replay_dump.js.map +1 -0
  32. package/packages/server/dist/app.d.ts.map +1 -1
  33. package/packages/server/dist/app.js +6 -0
  34. package/packages/server/dist/app.js.map +1 -1
  35. package/packages/server/dist/config/index.d.ts.map +1 -1
  36. package/packages/server/dist/config/index.js.map +1 -1
  37. package/packages/server/dist/controllers/bmadCoreConfigController.d.ts +41 -0
  38. package/packages/server/dist/controllers/bmadCoreConfigController.d.ts.map +1 -0
  39. package/packages/server/dist/controllers/bmadCoreConfigController.js +172 -0
  40. package/packages/server/dist/controllers/bmadCoreConfigController.js.map +1 -0
  41. package/packages/server/dist/controllers/contextBuilderController.d.ts +43 -0
  42. package/packages/server/dist/controllers/contextBuilderController.d.ts.map +1 -0
  43. package/packages/server/dist/controllers/contextBuilderController.js +159 -0
  44. package/packages/server/dist/controllers/contextBuilderController.js.map +1 -0
  45. package/packages/server/dist/controllers/harnessAgentController.d.ts +7 -0
  46. package/packages/server/dist/controllers/harnessAgentController.d.ts.map +1 -1
  47. package/packages/server/dist/controllers/harnessAgentController.js +33 -0
  48. package/packages/server/dist/controllers/harnessAgentController.js.map +1 -1
  49. package/packages/server/dist/controllers/harnessBundleController.d.ts +37 -0
  50. package/packages/server/dist/controllers/harnessBundleController.d.ts.map +1 -0
  51. package/packages/server/dist/controllers/harnessBundleController.js +312 -0
  52. package/packages/server/dist/controllers/harnessBundleController.js.map +1 -0
  53. package/packages/server/dist/controllers/harnessCommandController.d.ts +7 -0
  54. package/packages/server/dist/controllers/harnessCommandController.d.ts.map +1 -1
  55. package/packages/server/dist/controllers/harnessCommandController.js +33 -0
  56. package/packages/server/dist/controllers/harnessCommandController.js.map +1 -1
  57. package/packages/server/dist/controllers/harnessHookController.d.ts.map +1 -1
  58. package/packages/server/dist/controllers/harnessHookController.js +44 -1
  59. package/packages/server/dist/controllers/harnessHookController.js.map +1 -1
  60. package/packages/server/dist/controllers/harnessMcpController.d.ts.map +1 -1
  61. package/packages/server/dist/controllers/harnessMcpController.js +62 -1
  62. package/packages/server/dist/controllers/harnessMcpController.js.map +1 -1
  63. package/packages/server/dist/controllers/harnessShareScopeController.d.ts +9 -0
  64. package/packages/server/dist/controllers/harnessShareScopeController.d.ts.map +1 -1
  65. package/packages/server/dist/controllers/harnessShareScopeController.js +48 -1
  66. package/packages/server/dist/controllers/harnessShareScopeController.js.map +1 -1
  67. package/packages/server/dist/controllers/marketplaceController.d.ts +19 -0
  68. package/packages/server/dist/controllers/marketplaceController.d.ts.map +1 -0
  69. package/packages/server/dist/controllers/marketplaceController.js +74 -0
  70. package/packages/server/dist/controllers/marketplaceController.js.map +1 -0
  71. package/packages/server/dist/controllers/observabilityController.d.ts +32 -0
  72. package/packages/server/dist/controllers/observabilityController.d.ts.map +1 -0
  73. package/packages/server/dist/controllers/observabilityController.js +148 -0
  74. package/packages/server/dist/controllers/observabilityController.js.map +1 -0
  75. package/packages/server/dist/controllers/projectController.d.ts.map +1 -1
  76. package/packages/server/dist/controllers/projectController.js +7 -1
  77. package/packages/server/dist/controllers/projectController.js.map +1 -1
  78. package/packages/server/dist/controllers/sessionController.d.ts.map +1 -1
  79. package/packages/server/dist/controllers/sessionController.js +16 -4
  80. package/packages/server/dist/controllers/sessionController.js.map +1 -1
  81. package/packages/server/dist/controllers/systemBrowseController.d.ts +33 -0
  82. package/packages/server/dist/controllers/systemBrowseController.d.ts.map +1 -0
  83. package/packages/server/dist/controllers/systemBrowseController.js +105 -0
  84. package/packages/server/dist/controllers/systemBrowseController.js.map +1 -0
  85. package/packages/server/dist/handlers/streamCallbacks.d.ts +10 -0
  86. package/packages/server/dist/handlers/streamCallbacks.d.ts.map +1 -1
  87. package/packages/server/dist/handlers/streamCallbacks.js +25 -4
  88. package/packages/server/dist/handlers/streamCallbacks.js.map +1 -1
  89. package/packages/server/dist/handlers/websocket.d.ts +43 -3
  90. package/packages/server/dist/handlers/websocket.d.ts.map +1 -1
  91. package/packages/server/dist/handlers/websocket.js +385 -93
  92. package/packages/server/dist/handlers/websocket.js.map +1 -1
  93. package/packages/server/dist/index.js +2 -0
  94. package/packages/server/dist/index.js.map +1 -1
  95. package/packages/server/dist/locales/en/server.json +17 -0
  96. package/packages/server/dist/locales/es/server.json +17 -0
  97. package/packages/server/dist/locales/ja/server.json +17 -0
  98. package/packages/server/dist/locales/ko/server.json +17 -0
  99. package/packages/server/dist/locales/pt/server.json +17 -0
  100. package/packages/server/dist/locales/zh-CN/server.json +17 -0
  101. package/packages/server/dist/routes/harness.d.ts.map +1 -1
  102. package/packages/server/dist/routes/harness.js +58 -0
  103. package/packages/server/dist/routes/harness.js.map +1 -1
  104. package/packages/server/dist/routes/preferences.d.ts.map +1 -1
  105. package/packages/server/dist/routes/preferences.js +12 -2
  106. package/packages/server/dist/routes/preferences.js.map +1 -1
  107. package/packages/server/dist/routes/systemBrowse.d.ts +17 -0
  108. package/packages/server/dist/routes/systemBrowse.d.ts.map +1 -0
  109. package/packages/server/dist/routes/systemBrowse.js +22 -0
  110. package/packages/server/dist/routes/systemBrowse.js.map +1 -0
  111. package/packages/server/dist/services/bmadCoreConfigService.d.ts +86 -0
  112. package/packages/server/dist/services/bmadCoreConfigService.d.ts.map +1 -0
  113. package/packages/server/dist/services/bmadCoreConfigService.js +175 -0
  114. package/packages/server/dist/services/bmadCoreConfigService.js.map +1 -0
  115. package/packages/server/dist/services/bmadStatusService.d.ts +9 -0
  116. package/packages/server/dist/services/bmadStatusService.d.ts.map +1 -1
  117. package/packages/server/dist/services/bmadStatusService.js +77 -13
  118. package/packages/server/dist/services/bmadStatusService.js.map +1 -1
  119. package/packages/server/dist/services/chatEngine.d.ts +81 -0
  120. package/packages/server/dist/services/chatEngine.d.ts.map +1 -0
  121. package/packages/server/dist/services/chatEngine.js +2 -0
  122. package/packages/server/dist/services/chatEngine.js.map +1 -0
  123. package/packages/server/dist/services/chatEngineFactory.d.ts +15 -0
  124. package/packages/server/dist/services/chatEngineFactory.d.ts.map +1 -0
  125. package/packages/server/dist/services/chatEngineFactory.js +28 -0
  126. package/packages/server/dist/services/chatEngineFactory.js.map +1 -0
  127. package/packages/server/dist/services/chatService.d.ts +34 -26
  128. package/packages/server/dist/services/chatService.d.ts.map +1 -1
  129. package/packages/server/dist/services/chatService.js +44 -104
  130. package/packages/server/dist/services/chatService.js.map +1 -1
  131. package/packages/server/dist/services/cliChatEngine.d.ts +198 -0
  132. package/packages/server/dist/services/cliChatEngine.d.ts.map +1 -0
  133. package/packages/server/dist/services/cliChatEngine.js +2977 -0
  134. package/packages/server/dist/services/cliChatEngine.js.map +1 -0
  135. package/packages/server/dist/services/cliGridCards.d.ts +87 -0
  136. package/packages/server/dist/services/cliGridCards.d.ts.map +1 -0
  137. package/packages/server/dist/services/cliGridCards.js +298 -0
  138. package/packages/server/dist/services/cliGridCards.js.map +1 -0
  139. package/packages/server/dist/services/cliGridRegion.d.ts +44 -0
  140. package/packages/server/dist/services/cliGridRegion.d.ts.map +1 -0
  141. package/packages/server/dist/services/cliGridRegion.js +94 -0
  142. package/packages/server/dist/services/cliGridRegion.js.map +1 -0
  143. package/packages/server/dist/services/cliModalDetect.d.ts +299 -0
  144. package/packages/server/dist/services/cliModalDetect.d.ts.map +1 -0
  145. package/packages/server/dist/services/cliModalDetect.js +583 -0
  146. package/packages/server/dist/services/cliModalDetect.js.map +1 -0
  147. package/packages/server/dist/services/cliScreenCache.d.ts +33 -0
  148. package/packages/server/dist/services/cliScreenCache.d.ts.map +1 -0
  149. package/packages/server/dist/services/cliScreenCache.js +48 -0
  150. package/packages/server/dist/services/cliScreenCache.js.map +1 -0
  151. package/packages/server/dist/services/cliScreenModel.d.ts +141 -0
  152. package/packages/server/dist/services/cliScreenModel.d.ts.map +1 -0
  153. package/packages/server/dist/services/cliScreenModel.js +196 -0
  154. package/packages/server/dist/services/cliScreenModel.js.map +1 -0
  155. package/packages/server/dist/services/cliScreenStallWatchdog.d.ts +41 -0
  156. package/packages/server/dist/services/cliScreenStallWatchdog.d.ts.map +1 -0
  157. package/packages/server/dist/services/cliScreenStallWatchdog.js +45 -0
  158. package/packages/server/dist/services/cliScreenStallWatchdog.js.map +1 -0
  159. package/packages/server/dist/services/cliSessionPool.d.ts +97 -0
  160. package/packages/server/dist/services/cliSessionPool.d.ts.map +1 -0
  161. package/packages/server/dist/services/cliSessionPool.js +278 -0
  162. package/packages/server/dist/services/cliSessionPool.js.map +1 -0
  163. package/packages/server/dist/services/cliSpinnerProgress.d.ts +41 -0
  164. package/packages/server/dist/services/cliSpinnerProgress.d.ts.map +1 -0
  165. package/packages/server/dist/services/cliSpinnerProgress.js +131 -0
  166. package/packages/server/dist/services/cliSpinnerProgress.js.map +1 -0
  167. package/packages/server/dist/services/contextBuilderScriptTemplate.d.ts +24 -0
  168. package/packages/server/dist/services/contextBuilderScriptTemplate.d.ts.map +1 -0
  169. package/packages/server/dist/services/contextBuilderScriptTemplate.js +181 -0
  170. package/packages/server/dist/services/contextBuilderScriptTemplate.js.map +1 -0
  171. package/packages/server/dist/services/contextBuilderService.d.ts +68 -0
  172. package/packages/server/dist/services/contextBuilderService.d.ts.map +1 -0
  173. package/packages/server/dist/services/contextBuilderService.js +345 -0
  174. package/packages/server/dist/services/contextBuilderService.js.map +1 -0
  175. package/packages/server/dist/services/fileRewind.d.ts +46 -0
  176. package/packages/server/dist/services/fileRewind.d.ts.map +1 -0
  177. package/packages/server/dist/services/fileRewind.js +77 -0
  178. package/packages/server/dist/services/fileRewind.js.map +1 -0
  179. package/packages/server/dist/services/fileWatcherService.d.ts.map +1 -1
  180. package/packages/server/dist/services/fileWatcherService.js +40 -0
  181. package/packages/server/dist/services/fileWatcherService.js.map +1 -1
  182. package/packages/server/dist/services/harnessAgentService.d.ts +18 -0
  183. package/packages/server/dist/services/harnessAgentService.d.ts.map +1 -1
  184. package/packages/server/dist/services/harnessAgentService.js +55 -0
  185. package/packages/server/dist/services/harnessAgentService.js.map +1 -1
  186. package/packages/server/dist/services/harnessBundleService.d.ts +145 -0
  187. package/packages/server/dist/services/harnessBundleService.d.ts.map +1 -0
  188. package/packages/server/dist/services/harnessBundleService.js +1318 -0
  189. package/packages/server/dist/services/harnessBundleService.js.map +1 -0
  190. package/packages/server/dist/services/harnessCommandService.d.ts +21 -0
  191. package/packages/server/dist/services/harnessCommandService.d.ts.map +1 -1
  192. package/packages/server/dist/services/harnessCommandService.js +64 -0
  193. package/packages/server/dist/services/harnessCommandService.js.map +1 -1
  194. package/packages/server/dist/services/harnessHookService.d.ts +27 -0
  195. package/packages/server/dist/services/harnessHookService.d.ts.map +1 -1
  196. package/packages/server/dist/services/harnessHookService.js +52 -0
  197. package/packages/server/dist/services/harnessHookService.js.map +1 -1
  198. package/packages/server/dist/services/harnessMcpService.d.ts +24 -1
  199. package/packages/server/dist/services/harnessMcpService.d.ts.map +1 -1
  200. package/packages/server/dist/services/harnessMcpService.js +70 -0
  201. package/packages/server/dist/services/harnessMcpService.js.map +1 -1
  202. package/packages/server/dist/services/harnessShareScopeService.d.ts +19 -0
  203. package/packages/server/dist/services/harnessShareScopeService.d.ts.map +1 -1
  204. package/packages/server/dist/services/harnessShareScopeService.js +65 -0
  205. package/packages/server/dist/services/harnessShareScopeService.js.map +1 -1
  206. package/packages/server/dist/services/historyParser.d.ts +11 -0
  207. package/packages/server/dist/services/historyParser.d.ts.map +1 -1
  208. package/packages/server/dist/services/historyParser.js +10 -8
  209. package/packages/server/dist/services/historyParser.js.map +1 -1
  210. package/packages/server/dist/services/imageStorageService.d.ts +12 -0
  211. package/packages/server/dist/services/imageStorageService.d.ts.map +1 -1
  212. package/packages/server/dist/services/imageStorageService.js +22 -0
  213. package/packages/server/dist/services/imageStorageService.js.map +1 -1
  214. package/packages/server/dist/services/issueService.d.ts.map +1 -1
  215. package/packages/server/dist/services/issueService.js +1 -0
  216. package/packages/server/dist/services/issueService.js.map +1 -1
  217. package/packages/server/dist/services/marketplaceService.d.ts +50 -0
  218. package/packages/server/dist/services/marketplaceService.d.ts.map +1 -0
  219. package/packages/server/dist/services/marketplaceService.js +326 -0
  220. package/packages/server/dist/services/marketplaceService.js.map +1 -0
  221. package/packages/server/dist/services/observabilityService.d.ts +87 -0
  222. package/packages/server/dist/services/observabilityService.d.ts.map +1 -0
  223. package/packages/server/dist/services/observabilityService.js +0 -0
  224. package/packages/server/dist/services/observabilityService.js.map +1 -0
  225. package/packages/server/dist/services/projectService.d.ts +13 -1
  226. package/packages/server/dist/services/projectService.d.ts.map +1 -1
  227. package/packages/server/dist/services/projectService.js +32 -0
  228. package/packages/server/dist/services/projectService.js.map +1 -1
  229. package/packages/server/dist/services/queueService.d.ts.map +1 -1
  230. package/packages/server/dist/services/queueService.js +12 -3
  231. package/packages/server/dist/services/queueService.js.map +1 -1
  232. package/packages/server/dist/services/rateLimitProbeService.d.ts +15 -0
  233. package/packages/server/dist/services/rateLimitProbeService.d.ts.map +1 -1
  234. package/packages/server/dist/services/rateLimitProbeService.js +48 -19
  235. package/packages/server/dist/services/rateLimitProbeService.js.map +1 -1
  236. package/packages/server/dist/services/sessionService.d.ts +16 -0
  237. package/packages/server/dist/services/sessionService.d.ts.map +1 -1
  238. package/packages/server/dist/services/sessionService.js +125 -0
  239. package/packages/server/dist/services/sessionService.js.map +1 -1
  240. package/packages/server/dist/services/streamHandler.d.ts +8 -1
  241. package/packages/server/dist/services/streamHandler.d.ts.map +1 -1
  242. package/packages/server/dist/services/streamHandler.js +12 -5
  243. package/packages/server/dist/services/streamHandler.js.map +1 -1
  244. package/packages/server/dist/services/systemBrowseService.d.ts +91 -0
  245. package/packages/server/dist/services/systemBrowseService.d.ts.map +1 -0
  246. package/packages/server/dist/services/systemBrowseService.js +372 -0
  247. package/packages/server/dist/services/systemBrowseService.js.map +1 -0
  248. package/packages/server/dist/services/tokenCountService.d.ts +71 -0
  249. package/packages/server/dist/services/tokenCountService.d.ts.map +1 -0
  250. package/packages/server/dist/services/tokenCountService.js +313 -0
  251. package/packages/server/dist/services/tokenCountService.js.map +1 -0
  252. package/packages/server/dist/services/workspaceContext.d.ts +24 -0
  253. package/packages/server/dist/services/workspaceContext.d.ts.map +1 -0
  254. package/packages/server/dist/services/workspaceContext.js +109 -0
  255. package/packages/server/dist/services/workspaceContext.js.map +1 -0
  256. package/packages/server/dist/snippets/apply-qa-fixes +7 -5
  257. package/packages/server/dist/snippets/qa-review +7 -1
  258. package/packages/server/dist/utils/assertSafeBundlePath.d.ts +29 -0
  259. package/packages/server/dist/utils/assertSafeBundlePath.d.ts.map +1 -0
  260. package/packages/server/dist/utils/assertSafeBundlePath.js +53 -0
  261. package/packages/server/dist/utils/assertSafeBundlePath.js.map +1 -0
  262. package/packages/server/dist/utils/backgroundBlock.d.ts +29 -0
  263. package/packages/server/dist/utils/backgroundBlock.d.ts.map +1 -0
  264. package/packages/server/dist/utils/backgroundBlock.js +33 -0
  265. package/packages/server/dist/utils/backgroundBlock.js.map +1 -0
  266. package/packages/server/dist/utils/backgroundTaskTracker.d.ts +20 -0
  267. package/packages/server/dist/utils/backgroundTaskTracker.d.ts.map +1 -0
  268. package/packages/server/dist/utils/backgroundTaskTracker.js +40 -0
  269. package/packages/server/dist/utils/backgroundTaskTracker.js.map +1 -0
  270. package/packages/server/dist/utils/bundledBinaryModelSupport.d.ts +14 -0
  271. package/packages/server/dist/utils/bundledBinaryModelSupport.d.ts.map +1 -0
  272. package/packages/server/dist/utils/bundledBinaryModelSupport.js +112 -0
  273. package/packages/server/dist/utils/bundledBinaryModelSupport.js.map +1 -0
  274. package/packages/server/dist/utils/cliDebugLog.d.ts +38 -0
  275. package/packages/server/dist/utils/cliDebugLog.d.ts.map +1 -0
  276. package/packages/server/dist/utils/cliDebugLog.js +65 -0
  277. package/packages/server/dist/utils/cliDebugLog.js.map +1 -0
  278. package/packages/server/dist/utils/cliEngineUtils.d.ts +35 -0
  279. package/packages/server/dist/utils/cliEngineUtils.d.ts.map +1 -0
  280. package/packages/server/dist/utils/cliEngineUtils.js +38 -0
  281. package/packages/server/dist/utils/cliEngineUtils.js.map +1 -0
  282. package/packages/server/dist/utils/effortUtils.d.ts +2 -2
  283. package/packages/server/dist/utils/effortUtils.d.ts.map +1 -1
  284. package/packages/server/dist/utils/effortUtils.js +7 -5
  285. package/packages/server/dist/utils/effortUtils.js.map +1 -1
  286. package/packages/server/dist/utils/errors.d.ts +1 -0
  287. package/packages/server/dist/utils/errors.d.ts.map +1 -1
  288. package/packages/server/dist/utils/errors.js +17 -0
  289. package/packages/server/dist/utils/errors.js.map +1 -1
  290. package/packages/server/dist/utils/harnessBundleSchema.d.ts +14 -12
  291. package/packages/server/dist/utils/harnessBundleSchema.d.ts.map +1 -1
  292. package/packages/server/dist/utils/harnessBundleSchema.js +11 -1
  293. package/packages/server/dist/utils/harnessBundleSchema.js.map +1 -1
  294. package/packages/server/dist/utils/harnessPaths.d.ts +40 -0
  295. package/packages/server/dist/utils/harnessPaths.d.ts.map +1 -1
  296. package/packages/server/dist/utils/harnessPaths.js +123 -0
  297. package/packages/server/dist/utils/harnessPaths.js.map +1 -1
  298. package/packages/server/dist/utils/trailingThrottle.d.ts +21 -0
  299. package/packages/server/dist/utils/trailingThrottle.d.ts.map +1 -0
  300. package/packages/server/dist/utils/trailingThrottle.js +36 -0
  301. package/packages/server/dist/utils/trailingThrottle.js.map +1 -0
  302. package/packages/server/package.json +61 -58
  303. package/packages/server/resources/internals/INDEX.md +3 -1
  304. package/packages/server/resources/internals/bmad-qa-fix-marker.md +32 -0
  305. package/packages/server/resources/internals/harness-files.md +22 -0
  306. package/packages/server/resources/internals/image-storage.md +9 -0
  307. package/packages/server/resources/internals/observability-storage.md +23 -0
  308. package/packages/server/resources/manual/02-chat.md +23 -4
  309. package/packages/server/resources/manual/03-sessions.md +1 -0
  310. package/packages/server/resources/manual/05-projects.md +15 -2
  311. package/packages/server/resources/manual/10-project-board.md +4 -3
  312. package/packages/server/resources/manual/11-bmad-method-integration.md +12 -8
  313. package/packages/server/resources/manual/12-harness-workbench.md +82 -1
  314. package/packages/server/resources/manual/13-settings.md +61 -9
  315. package/packages/shared/dist/index.d.ts +7 -0
  316. package/packages/shared/dist/index.d.ts.map +1 -1
  317. package/packages/shared/dist/index.js +14 -0
  318. package/packages/shared/dist/index.js.map +1 -1
  319. package/packages/shared/dist/types/bmadCoreConfig.d.ts +71 -0
  320. package/packages/shared/dist/types/bmadCoreConfig.d.ts.map +1 -0
  321. package/packages/shared/dist/types/bmadCoreConfig.js +30 -0
  322. package/packages/shared/dist/types/bmadCoreConfig.js.map +1 -0
  323. package/packages/shared/dist/types/bmadStatus.d.ts +26 -0
  324. package/packages/shared/dist/types/bmadStatus.d.ts.map +1 -1
  325. package/packages/shared/dist/types/bmadStatus.js.map +1 -1
  326. package/packages/shared/dist/types/board.d.ts +6 -0
  327. package/packages/shared/dist/types/board.d.ts.map +1 -1
  328. package/packages/shared/dist/types/contextBuilder.d.ts +102 -0
  329. package/packages/shared/dist/types/contextBuilder.d.ts.map +1 -0
  330. package/packages/shared/dist/types/contextBuilder.js +55 -0
  331. package/packages/shared/dist/types/contextBuilder.js.map +1 -0
  332. package/packages/shared/dist/types/harnessBundle.d.ts +35 -0
  333. package/packages/shared/dist/types/harnessBundle.d.ts.map +1 -1
  334. package/packages/shared/dist/types/marketplace.d.ts +83 -0
  335. package/packages/shared/dist/types/marketplace.d.ts.map +1 -0
  336. package/packages/shared/dist/types/marketplace.js +18 -0
  337. package/packages/shared/dist/types/marketplace.js.map +1 -0
  338. package/packages/shared/dist/types/observability.d.ts +148 -0
  339. package/packages/shared/dist/types/observability.d.ts.map +1 -0
  340. package/packages/shared/dist/types/observability.js +24 -0
  341. package/packages/shared/dist/types/observability.js.map +1 -0
  342. package/packages/shared/dist/types/preferences.d.ts +16 -2
  343. package/packages/shared/dist/types/preferences.d.ts.map +1 -1
  344. package/packages/shared/dist/types/preferences.js +1 -0
  345. package/packages/shared/dist/types/preferences.js.map +1 -1
  346. package/packages/shared/dist/types/project.d.ts +7 -1
  347. package/packages/shared/dist/types/project.d.ts.map +1 -1
  348. package/packages/shared/dist/types/project.js.map +1 -1
  349. package/packages/shared/dist/types/sdk.d.ts +124 -12
  350. package/packages/shared/dist/types/sdk.d.ts.map +1 -1
  351. package/packages/shared/dist/types/sdk.js +64 -19
  352. package/packages/shared/dist/types/sdk.js.map +1 -1
  353. package/packages/shared/dist/types/streaming.d.ts +2 -2
  354. package/packages/shared/dist/types/streaming.d.ts.map +1 -1
  355. package/packages/shared/dist/types/streaming.js.map +1 -1
  356. package/packages/shared/dist/types/systemBrowse.d.ts +120 -0
  357. package/packages/shared/dist/types/systemBrowse.d.ts.map +1 -0
  358. package/packages/shared/dist/types/systemBrowse.js +57 -0
  359. package/packages/shared/dist/types/systemBrowse.js.map +1 -0
  360. package/packages/shared/dist/types/websocket.d.ts +38 -0
  361. package/packages/shared/dist/types/websocket.d.ts.map +1 -1
  362. package/packages/shared/dist/utils/interruptFiller.d.ts +30 -0
  363. package/packages/shared/dist/utils/interruptFiller.d.ts.map +1 -0
  364. package/packages/shared/dist/utils/interruptFiller.js +38 -0
  365. package/packages/shared/dist/utils/interruptFiller.js.map +1 -0
  366. package/packages/shared/dist/utils/toolResultContent.d.ts +22 -0
  367. package/packages/shared/dist/utils/toolResultContent.d.ts.map +1 -0
  368. package/packages/shared/dist/utils/toolResultContent.js +29 -0
  369. package/packages/shared/dist/utils/toolResultContent.js.map +1 -0
  370. package/scripts/run-integration-test.mjs +362 -362
  371. package/packages/client/dist/assets/index-CjyjnXB8.css +0 -32
  372. package/packages/client/dist/assets/index-NqJdhlek.js +0 -1498
@@ -7,13 +7,12 @@
7
7
  import { Server as SocketIOServer } from 'socket.io';
8
8
  import { existsSync, unlinkSync, readFileSync, readdirSync, statSync } from 'fs';
9
9
  import { randomUUID } from 'crypto';
10
- import { ERROR_CODES, IMAGE_CONSTRAINTS, parseQueueScript, TERMINAL_ERRORS, ROOT_BRANCH_KEY } from '@hammoc/shared';
10
+ import { ERROR_CODES, IMAGE_CONSTRAINTS, parseQueueScript, TERMINAL_ERRORS, ROOT_BRANCH_KEY, effectiveModelIs1M } from '@hammoc/shared';
11
11
  import i18next from '../i18n.js';
12
12
  import { SUPPORTED_LANGUAGES } from '@hammoc/shared';
13
13
  import { isLocalIP, extractClientIP } from '../utils/networkUtils.js';
14
- import { query as sdkQuery } from '@anthropic-ai/claude-agent-sdk';
15
- import { ChatService } from '../services/chatService.js';
16
- import { SessionService } from '../services/sessionService.js';
14
+ import { createChatEngine } from '../services/chatEngineFactory.js';
15
+ import { SessionService, sessionService } from '../services/sessionService.js';
17
16
  import { parseSDKError, AbortedError, SDKErrorCode } from '../utils/errors.js';
18
17
  import { createSessionMiddleware } from '../middleware/session.js';
19
18
  import { config } from '../config/index.js';
@@ -22,7 +21,13 @@ import { preferencesService } from '../services/preferencesService.js';
22
21
  import { getOrCreateQueueService, getQueueInstances } from '../controllers/queueController.js';
23
22
  import { createLogger } from '../utils/logger.js';
24
23
  import { clampEffortForModel, supportsAdaptiveThinking } from '../utils/effortUtils.js';
24
+ import { shouldForwardCliProgress, shouldForwardCliPtyMirror } from '../utils/cliEngineUtils.js';
25
+ import { getCliScreen, getCliScreenStall, setCliScreenStall, deleteCliScreen } from '../services/cliScreenCache.js';
26
+ import { createScreenStallWatchdog } from '../services/cliScreenStallWatchdog.js';
27
+ import { modelMissingNative1MSupport } from '../utils/bundledBinaryModelSupport.js';
25
28
  import { buildStreamCallbacks } from './streamCallbacks.js';
29
+ import { BackgroundTaskTracker } from '../utils/backgroundTaskTracker.js';
30
+ import { createMcpCallRecorder } from '../services/observabilityService.js';
26
31
  import { rateLimitProbeService } from '../services/rateLimitProbeService.js';
27
32
  import { ptyService } from '../services/ptyService.js';
28
33
  import { projectService } from '../services/projectService.js';
@@ -37,6 +42,8 @@ import { isSnippetRef, resolveSnippet, listSnippets, SnippetError } from '../uti
37
42
  const log = createLogger('websocket');
38
43
  // Alias for concise usage in guards
39
44
  const queueInstances = getQueueInstances;
45
+ // Defense B: models already warned about a stale-binary 1M fallback (warn once per model per process)
46
+ const warnedMissing1M = new Set();
40
47
  // Socket-to-terminal mapping: socket.id → Set of terminalIds (Story 17.1)
41
48
  const socketTerminals = new Map();
42
49
  // Story 20.1: Session-to-project mapping for dashboard triggers
@@ -412,6 +419,25 @@ export function getActiveStreamSessionIds() {
412
419
  .filter(([, stream]) => stream.status === 'running')
413
420
  .map(([key]) => key);
414
421
  }
422
+ /**
423
+ * Running streams for a project, with their captured first prompt. Used by the
424
+ * session list to surface a session whose JSONL file does not exist yet (the CLI
425
+ * boot window) and which the client may have already navigated away from — so it
426
+ * is no longer in getJoinedSessionIdsByProject. Prefers the synchronously-captured
427
+ * stream.projectSlug, falling back to the async-populated sessionProjectMap.
428
+ */
429
+ export function getActiveStreamMetaByProject(projectSlug) {
430
+ const out = [];
431
+ for (const [sessionId, stream] of activeStreams.entries()) {
432
+ if (stream.status !== 'running')
433
+ continue;
434
+ const slug = stream.projectSlug ?? sessionProjectMap.get(sessionId);
435
+ if (slug !== projectSlug)
436
+ continue;
437
+ out.push({ sessionId, firstPrompt: stream.firstPrompt ?? '' });
438
+ }
439
+ return out;
440
+ }
415
441
  /** Get active (running) session counts grouped by project slug (in-memory, no I/O) */
416
442
  export function getActiveSessionCountsByProject() {
417
443
  const counts = new Map();
@@ -918,6 +944,13 @@ export async function initializeWebSocket(httpServer) {
918
944
  resumeSessionAt: data.resumeSessionAt,
919
945
  expectedBranchTotal: data.expectedBranchTotal,
920
946
  isFork: !!data.forkSession,
947
+ // Captured up front so a file-less waiting session (esp. the CLI boot window)
948
+ // can be listed for its project with a real preview even if the client has
949
+ // already left the session view (joinedIds would no longer include it).
950
+ projectSlug: data.workingDirectory
951
+ ? sessionService.encodeProjectPath(data.workingDirectory)
952
+ : undefined,
953
+ firstPrompt: data.content || undefined,
921
954
  };
922
955
  activeStreams.set(streamKey, stream);
923
956
  // Bump drain generation so any pending scheduled drain is invalidated
@@ -1064,31 +1097,43 @@ export async function initializeWebSocket(httpServer) {
1064
1097
  return;
1065
1098
  const { mode, projectSlug } = data;
1066
1099
  const stream = activeStreams.get(sessionId);
1067
- // 1) Update SDK permission mode only when stream is actively running
1100
+ // Story 37.5: the CLI engine's setPermissionMode runs a live Shift+Tab closed loop and may
1101
+ // LAND on a different mode than requested (fail-safe / off-cycle). setPermissionMode returns
1102
+ // void (signature unchanged), so we read the *verified* applied mode back via
1103
+ // getPermissionMode() and broadcast/persist THAT — not the inbound request value. Without the
1104
+ // read-back the requested mode would be broadcast even on a fail-safe (the live screen never
1105
+ // reached it), silently violating AC3 ("verified, not assumed"). SDK mode: getPermissionMode()
1106
+ // == requested, so the read-back is a harmless no-op there.
1107
+ let effectiveMode = mode;
1108
+ let modeDiverged = false; // verified mode differed from the request (CLI fail-safe / off-cycle)
1109
+ // 1) Update the engine's permission mode — only when stream is actively running
1068
1110
  if (stream?.chatService && stream.status === 'running') {
1069
1111
  try {
1070
1112
  await stream.chatService.setPermissionMode(mode);
1071
- log.debug(`Permission mode changed to "${mode}" for session ${sessionId}`);
1113
+ effectiveMode = stream.chatService.getPermissionMode(); // verified (closed-loop) mode
1114
+ modeDiverged = effectiveMode !== mode;
1115
+ log.debug(`Permission mode requested "${mode}", applied "${effectiveMode}" for session ${sessionId}`);
1072
1116
  }
1073
1117
  catch (err) {
1074
1118
  log.error('Failed to change permission mode:', err);
1075
1119
  return; // Don't persist or broadcast a mode that failed to apply
1076
1120
  }
1077
1121
  }
1078
- // Update pending chain items so the next drain uses the new mode.
1079
- // Outside the running-stream block because mode can change between turns
1122
+ // Update pending chain items so the next drain uses the new mode. Use the verified mode so a
1123
+ // CLI fail-safe also corrects the next-spawn posture (== request on every non-CLI/non-diverged
1124
+ // path). Outside the running-stream block because mode can change between turns
1080
1125
  // (e.g., during the 1s chain drain delay when no stream is active).
1081
1126
  const chainItems = chainState.get(sessionId);
1082
1127
  if (chainItems) {
1083
1128
  for (const item of chainItems) {
1084
1129
  if (item.status === 'pending' || item.status === 'sending') {
1085
- item.permissionMode = mode;
1130
+ item.permissionMode = effectiveMode;
1086
1131
  }
1087
1132
  }
1088
1133
  }
1089
1134
  // 2) Always persist per-session permission mode (read only when policy is 'always')
1090
1135
  // Use projectSlug from client as fallback when sessionProjectMap entry is gone (stream ended)
1091
- await persistSessionPermissionMode(sessionId, mode, projectSlug);
1136
+ await persistSessionPermissionMode(sessionId, effectiveMode, projectSlug);
1092
1137
  // 3) Broadcast to other viewers based on sync policy
1093
1138
  let syncPolicy = 'always';
1094
1139
  try {
@@ -1100,8 +1145,16 @@ export async function initializeWebSocket(httpServer) {
1100
1145
  }
1101
1146
  if (syncPolicy === 'streaming' && stream?.status !== 'running')
1102
1147
  return;
1103
- // 'always' or ('streaming' + running) → broadcast via Socket.io room
1104
- socket.to(`session:${sessionId}`).emit('permission:mode-change', { mode });
1148
+ // 'always' or ('streaming' + running) → broadcast the VERIFIED mode via Socket.io room.
1149
+ socket.to(`session:${sessionId}`).emit('permission:mode-change', { mode: effectiveMode });
1150
+ // Story 37.5: originator convergence. The broadcast above excludes the sending socket
1151
+ // (socket.to), so on a fail-safe (verified ≠ requested) the SENDER's selector would stay on
1152
+ // the wrong requested value. Echo the verified mode back to the sender ONLY in that case; the
1153
+ // normal path (verified == requested) leaves the sender's optimistic value correct, so no
1154
+ // redundant self-echo is sent.
1155
+ if (modeDiverged) {
1156
+ socket.emit('permission:mode-change', { mode: effectiveMode });
1157
+ }
1105
1158
  });
1106
1159
  // Handle session:join event — attach socket to active running stream (broadcast)
1107
1160
  // Also joins a persistent Socket.io room so future streams auto-include this socket.
@@ -1185,6 +1238,26 @@ export async function initializeWebSocket(httpServer) {
1185
1238
  if (alreadyJoinedSame) {
1186
1239
  return;
1187
1240
  }
1241
+ // Story 37.8: CLI mirror late-join / refresh sync. If this session has a cached screen
1242
+ // frame (refreshed each ~100ms during a turn and at teardown), push it once to THIS
1243
+ // socket so a newly-connected or refreshed browser initializes to the CURRENT claude
1244
+ // screen (color intact), then follows live cli:screen-frame updates. Room-wide broadcast
1245
+ // is unnecessary (sockets already in the room are live-current). A browser refresh
1246
+ // reconnects with a NEW socket id, so it is past the alreadyJoinedSame dedup above and
1247
+ // receives the frame — only a same-socket re-join is deduped. Cache miss (e.g. SDK mode,
1248
+ // or before the first turn ends) → no emit, blank-screen fallback, unchanged from before.
1249
+ const cachedScreen = getCliScreen(sessionId);
1250
+ if (cachedScreen) {
1251
+ socket.emit('cli:screen-frame', { sessionId, frame: cachedScreen });
1252
+ }
1253
+ // Resync the soft screen-stall flag (CLI mode). cli:screen-stall is emitted only on a
1254
+ // stalled↔live transition, so a socket that joined / reconnected AFTER the stall began
1255
+ // (tab switch, mobile sleep/wake) never saw it and would otherwise show a stale card.
1256
+ // Re-send the current flag once so the stall notice matches reality. Only when stalled:
1257
+ // the client resets the flag to false on stream restore, so a false needs no wire traffic.
1258
+ if (getCliScreenStall(sessionId)) {
1259
+ socket.emit('cli:screen-stall', { sessionId, stalled: true });
1260
+ }
1188
1261
  if (!stream || stream.status !== 'running') {
1189
1262
  // Story 27.1: Deliver buffer messages via stream:history (no HTTP fetch needed).
1190
1263
  // Wrapped in a helper that re-checks activeStreams because the async
@@ -1299,6 +1372,32 @@ export async function initializeWebSocket(httpServer) {
1299
1372
  // NOW add to broadcast set — live events flow from here
1300
1373
  stream.sockets.add(socket);
1301
1374
  });
1375
+ // Story 37.8: CLI mirror collapse/expand + remount restore. The mirror panel emits this
1376
+ // (no payload) whenever it (re)mounts; resolve the socket's current session and push a
1377
+ // one-time cli:screen-frame from the screen cache. A collapse/expand or panel remount does
1378
+ // NOT re-fire session:join, so this control channel is how the panel pulls the current
1379
+ // screen. Cache miss (SDK mode / before first turn / no session) → no-op.
1380
+ socket.on('cli:request-screen-frame', () => {
1381
+ const sid = socketToSession.get(socket.id) ?? socketSessionRoom.get(socket.id);
1382
+ if (!sid)
1383
+ return;
1384
+ const cachedFrame = getCliScreen(sid);
1385
+ if (cachedFrame) {
1386
+ socket.emit('cli:screen-frame', { sessionId: sid, frame: cachedFrame });
1387
+ }
1388
+ });
1389
+ // CLI debug log relay — client sends decision entries, server writes them to the
1390
+ // per-session CLI debug log file alongside server-side entries.
1391
+ socket.on('cli:debug-log', (data) => {
1392
+ const sid = socketToSession.get(socket.id) ?? socketSessionRoom.get(socket.id);
1393
+ if (!sid)
1394
+ return;
1395
+ const stream = activeStreams.get(sid);
1396
+ const engine = stream?.chatService;
1397
+ if (engine && 'currentDebugLog' in engine) {
1398
+ engine.currentDebugLog?.clientWithTs(data.ts, data.ev, data.d);
1399
+ }
1400
+ });
1302
1401
  // Handle session:leave event — detach socket from current stream and session room
1303
1402
  // (client navigating away from a session while streaming continues in background)
1304
1403
  socket.on('session:leave', (sessionId) => {
@@ -1562,50 +1661,27 @@ export async function initializeWebSocket(httpServer) {
1562
1661
  return;
1563
1662
  log.info(`session:rewind-files sessionId=${sessionId}, messageUuid=${messageUuid}, dryRun=${!!dryRun}`);
1564
1663
  try {
1565
- const sessionService = new SessionService();
1566
- const _projectSlug = sessionService.encodeProjectPath(workingDirectory);
1567
- const rewindQuery = sdkQuery({
1568
- prompt: '',
1569
- options: {
1570
- resume: sessionId,
1571
- cwd: workingDirectory,
1572
- enableFileCheckpointing: true,
1573
- // Do NOT pass sessionId here — CLI rejects --session-id
1574
- // combined with --resume unless --fork-session is also set.
1575
- // resume: sessionId already identifies the session.
1576
- },
1577
- });
1578
- try {
1579
- const rewindResult = await rewindQuery.rewindFiles(messageUuid, { dryRun: !!dryRun });
1580
- log.info(`rewindFiles result: canRewind=${rewindResult.canRewind}, filesChanged=${rewindResult.filesChanged?.length ?? 0}, insertions=${rewindResult.insertions ?? 0}, deletions=${rewindResult.deletions ?? 0}`);
1581
- if (rewindResult.canRewind) {
1582
- socket.emit('session:rewind-result', {
1583
- success: true,
1584
- dryRun: !!dryRun,
1585
- filesChanged: rewindResult.filesChanged,
1586
- insertions: rewindResult.insertions,
1587
- deletions: rewindResult.deletions,
1588
- });
1589
- }
1590
- else {
1591
- socket.emit('session:rewind-result', {
1592
- success: false,
1593
- dryRun: !!dryRun,
1594
- error: rewindResult.error,
1595
- });
1596
- }
1664
+ // Standalone rewind is now a ChatEngine operation (Story 32.3) — the SDK
1665
+ // mechanism (throwaway resume query + markRewindDirty + close) lives
1666
+ // behind the seam in ChatService.rewindFiles. The handler keeps only the
1667
+ // transport concerns: result → session:rewind-result mapping.
1668
+ const engine = createChatEngine(await projectService.getEffectiveEngineMode(workingDirectory), { workingDirectory });
1669
+ const result = await engine.rewindFiles({ sessionId, messageUuid, dryRun: !!dryRun });
1670
+ if (result.canRewind) {
1671
+ socket.emit('session:rewind-result', {
1672
+ success: true,
1673
+ dryRun: !!dryRun,
1674
+ filesChanged: result.filesChanged,
1675
+ insertions: result.insertions,
1676
+ deletions: result.deletions,
1677
+ });
1597
1678
  }
1598
- finally {
1599
- // Mark session as dirty BEFORE close — SDK query({ prompt: '' })
1600
- // writes an empty user message to the JSONL which causes
1601
- // cache_control 400 errors. chatService will clean the JSONL
1602
- // before the next resume.
1603
- sessionService.markRewindDirty(workingDirectory, sessionId);
1604
- // Clean up the query object
1605
- try {
1606
- rewindQuery.close();
1607
- }
1608
- catch { /* best-effort */ }
1679
+ else {
1680
+ socket.emit('session:rewind-result', {
1681
+ success: false,
1682
+ dryRun: !!dryRun,
1683
+ error: result.error,
1684
+ });
1609
1685
  }
1610
1686
  }
1611
1687
  catch (err) {
@@ -2090,6 +2166,9 @@ export async function initializeWebSocket(httpServer) {
2090
2166
  // Only destroy if no active stream (streaming continues in background)
2091
2167
  if (!activeStreams.has(disconnectSessionId)) {
2092
2168
  sessionBufferManager.destroy(disconnectSessionId);
2169
+ // Story 37.7: drop the CLI mirror screen cache on the same room-empty seam
2170
+ // (no observer left to sync) so the session-lifetime Map does not leak.
2171
+ deleteCliScreen(disconnectSessionId);
2093
2172
  }
2094
2173
  // Notify session list viewers when no sockets remain for this session
2095
2174
  const slug = socketProjectRoom.get(socket.id) || sessionProjectMap.get(disconnectSessionId);
@@ -2183,6 +2262,45 @@ export async function broadcastSnippetList(workingDirectory, originSocketId) {
2183
2262
  log.warn(`broadcastSnippetList failed for socket ${originSocketId}: ${String(err)}`);
2184
2263
  }
2185
2264
  }
2265
+ /**
2266
+ * Broadcast a global-preferences change to every OTHER connected browser
2267
+ * (multi-device settings sync). The originating socket is excluded so it never
2268
+ * receives its own echo — it already applied the change optimistically. With no
2269
+ * origin id (e.g. a non-browser caller) the change goes to everyone.
2270
+ */
2271
+ export function broadcastPreferencesChange(preferences, originSocketId) {
2272
+ try {
2273
+ // Strip credential-bearing fields — telegram/webPush are managed via
2274
+ // dedicated (masked) endpoints and must not be broadcast in plaintext to
2275
+ // other browsers. Clients keep their existing values for these (merge).
2276
+ const safe = { ...preferences };
2277
+ delete safe.telegram;
2278
+ delete safe.webPush;
2279
+ // theme & diffLayout are device-local (each browser's localStorage, not the server) —
2280
+ // broadcasting them would yank another device's theme / diff layout. Never sync them.
2281
+ delete safe.theme;
2282
+ delete safe.diffLayout;
2283
+ const target = originSocketId ? io.except(originSocketId) : io;
2284
+ target.emit('preferences:changed', { preferences: safe });
2285
+ }
2286
+ catch (err) {
2287
+ log.warn(`broadcastPreferencesChange failed: ${String(err)}`);
2288
+ }
2289
+ }
2290
+ /**
2291
+ * Broadcast a project-settings change to OTHER browsers (multi-device sync).
2292
+ * Sent to everyone except the origin socket; the payload is keyed by projectSlug
2293
+ * so each client applies it only while viewing that project's settings.
2294
+ */
2295
+ export function broadcastProjectSettingsChange(projectSlug, settings, originSocketId) {
2296
+ try {
2297
+ const target = originSocketId ? io.except(originSocketId) : io;
2298
+ target.emit('project:settings-changed', { projectSlug, settings });
2299
+ }
2300
+ catch (err) {
2301
+ log.warn(`broadcastProjectSettingsChange failed: ${String(err)}`);
2302
+ }
2303
+ }
2186
2304
  /**
2187
2305
  * Get the current number of connected clients
2188
2306
  * @returns Number of connected clients
@@ -2294,10 +2412,14 @@ async function handleChatSend(stream, data, abortController, lang) {
2294
2412
  const sessionService = new SessionService();
2295
2413
  const projectSlug = sessionService.encodeProjectPath(workingDirectory);
2296
2414
  // Story 27.2: Store images as files, emit URL-based ImageRef[] instead of base64.
2415
+ // CLI engine (image support): also resolve the on-disk absolute paths so the CLI path
2416
+ // can reference attachments by --add-dir + prompt (it can't embed base64 over the PTY).
2417
+ let attachedImagePaths = [];
2297
2418
  if (!forkSession) {
2298
2419
  let imageRefs;
2299
2420
  if (images && images.length > 0) {
2300
2421
  imageRefs = await imageStorageService.storeImages(projectSlug, sessionId || '', images);
2422
+ attachedImagePaths = imageStorageService.resolveImagePaths(projectSlug, sessionId || '', images);
2301
2423
  }
2302
2424
  emit('user:message', {
2303
2425
  content,
@@ -2364,6 +2486,14 @@ async function handleChatSend(stream, data, abortController, lang) {
2364
2486
  }
2365
2487
  }
2366
2488
  let timeoutId = null;
2489
+ // CLI screen-stall watchdog — assigned once prefs/engineMode are known (below). Declared at the
2490
+ // function scope so the finally can dispose() it (a try-block `const` wouldn't be in scope there).
2491
+ let stallWatchdog = null;
2492
+ // Story 35.1: when true the inactivity timer is paused because we're awaiting
2493
+ // user input (permission approval / AskUserQuestion selection). resetTimeout()
2494
+ // short-circuits while this is set so no activity can re-arm the timer, and
2495
+ // canUseTool clears it + re-arms once the user responds.
2496
+ let permissionWaiting = false;
2367
2497
  // Snapshot JSONL files before SDK query to detect phantom checkpoint files.
2368
2498
  // The SDK's file checkpointing creates separate JSONL files (new UUIDs) with
2369
2499
  // only file-history-snapshot entries alongside the real session file. These
@@ -2380,12 +2510,20 @@ async function handleChatSend(stream, data, abortController, lang) {
2380
2510
  // Non-critical — cleanup will be skipped if snapshot fails
2381
2511
  }
2382
2512
  try {
2383
- const chatService = new ChatService({ workingDirectory, permissionMode });
2384
- stream.chatService = chatService;
2385
2513
  // Load preferences early for advanced settings + timeout
2386
2514
  const effectivePrefs = await preferencesService.getEffectivePreferences();
2515
+ // Story 33.3: resolve the effective engine mode (override > global > 'sdk' default).
2516
+ // Captured once into a local so the progress-callback gating below reuses the same
2517
+ // decision (no second resolve, and `engineMode === 'cli'` stays consistent).
2518
+ const engineMode = await projectService.getEffectiveEngineMode(workingDirectory);
2519
+ // Auto-compaction master switch (both engines). Default ON. Gates the engine-internal
2520
+ // auto-compact (forwarded into each engine's settings) AND Hammoc's overflow-triggered
2521
+ // /compact recovery below, so one toggle controls all auto-compaction.
2522
+ const autoCompactEnabled = effectivePrefs.autoCompactEnabled ?? true;
2523
+ const chatService = createChatEngine(engineMode, { workingDirectory, permissionMode, cliBinaryPath: effectivePrefs.cliBinaryPath, cliShowThinkingSummaries: effectivePrefs.cliShowThinkingSummaries, cliResumeChoice: effectivePrefs.cliResumeChoice, autoCompactEnabled, planModeBypassBehavior: effectivePrefs.planModeBypassBehavior });
2524
+ stream.chatService = chatService;
2387
2525
  const effectiveEffort = clampEffortForModel(effort ?? effectivePrefs.defaultEffort, model);
2388
- // Opus 4.7 flipped `thinking.display` default to 'omitted' — ThinkingBlock UI stays blank
2526
+ // Opus 4.7+ flipped `thinking.display` default to 'omitted' — ThinkingBlock UI stays blank
2389
2527
  // unless we explicitly opt in. For adaptive-thinking models, forward an explicit `thinking`
2390
2528
  // config based on the user's showThinkingBlocks preference (Hammoc default: true).
2391
2529
  // Legacy models (Sonnet 4.5, Opus 4.5, Haiku, Sonnet 4) keep the `maxThinkingTokens` path
@@ -2399,11 +2537,17 @@ async function handleChatSend(stream, data, abortController, lang) {
2399
2537
  ...(isResuming ? { resume: sessionId } : { sessionId }),
2400
2538
  abortController,
2401
2539
  model,
2402
- images,
2540
+ // SDK mode embeds base64 image blocks directly; CLI mode can't (the PTY is
2541
+ // text-only), so it instead receives the on-disk paths resolved above and
2542
+ // references them via --add-dir + an explicit prompt instruction. Each engine
2543
+ // ignores the other's field, so we only send the one that applies.
2544
+ ...(engineMode === 'cli'
2545
+ ? (attachedImagePaths.length > 0 ? { attachedImagePaths } : {})
2546
+ : { images }),
2403
2547
  // Advanced settings from preferences
2404
2548
  customSystemPrompt: effectivePrefs.customSystemPrompt,
2405
2549
  // maxThinkingTokens: legacy path; SDK docs say it takes precedence for backward-compat,
2406
- // which conflicts with adaptive mode on Opus 4.7 (rejects enabled+budget). Skip on
2550
+ // which conflicts with adaptive mode on Opus 4.7+ (rejects enabled+budget). Skip on
2407
2551
  // adaptive-capable models so the explicit `thinking` field governs.
2408
2552
  ...(!adaptiveCapable && { maxThinkingTokens: effectivePrefs.maxThinkingTokens }),
2409
2553
  ...(thinkingConfig && { thinking: thinkingConfig }),
@@ -2449,6 +2593,10 @@ async function handleChatSend(stream, data, abortController, lang) {
2449
2593
  sessionId: sessionIdRef.current || '',
2450
2594
  toolCall: { id: requestId, name: toolName, input },
2451
2595
  requiresApproval: true,
2596
+ // CLI engine emits no tool:call, so a normal-tool permission has no tool card to
2597
+ // attach to — tell the client to render it as an independent card. AskUserQuestion
2598
+ // already renders independently (client keys off the tool name), so exclude it.
2599
+ standalone: engineMode === 'cli' && !isAskUserQuestion,
2452
2600
  });
2453
2601
  // Notify via Telegram if no socket connected (or alwaysNotify enabled)
2454
2602
  if (notificationService.shouldNotify(stream.sockets.size)) {
@@ -2457,38 +2605,59 @@ async function handleChatSend(stream, data, abortController, lang) {
2457
2605
  : `${toolName}`;
2458
2606
  notificationService.notifyInputRequired(stream.sessionId, toolName, prompt);
2459
2607
  }
2460
- // Wait for user response Promise stays pending if no socket connected
2461
- const userResponse = await new Promise((resolve) => {
2462
- stream.pendingPermissions.set(requestId, {
2463
- resolve,
2464
- interactionType: isAskUserQuestion ? 'question' : 'permission',
2608
+ // Story 35.1: pause the inactivity timer for the input-wait. Waiting on a human
2609
+ // (permission approval / AskUserQuestion selection) is by-design indefinite —
2610
+ // "respond when you can" — so the activity-based timeout must not fire here. Stop
2611
+ // it on the way in; the finally below resumes it on EVERY exit path. Generation-hang
2612
+ // detection is unaffected: the timer still governs all activity outside this wait.
2613
+ permissionWaiting = true;
2614
+ if (timeoutId) {
2615
+ clearTimeout(timeoutId);
2616
+ timeoutId = null;
2617
+ }
2618
+ try {
2619
+ // Wait for user response — Promise stays pending if no socket connected
2620
+ const userResponse = await new Promise((resolve) => {
2621
+ stream.pendingPermissions.set(requestId, {
2622
+ resolve,
2623
+ interactionType: isAskUserQuestion ? 'question' : 'permission',
2624
+ });
2465
2625
  });
2466
- });
2467
- if (isAskUserQuestion) {
2468
- const questions = input.questions;
2469
- let answers;
2470
- if (typeof userResponse.response === 'object' && !Array.isArray(userResponse.response) && userResponse.response !== null) {
2471
- answers = userResponse.response;
2626
+ if (isAskUserQuestion) {
2627
+ const questions = input.questions;
2628
+ let answers;
2629
+ if (typeof userResponse.response === 'object' && !Array.isArray(userResponse.response) && userResponse.response !== null) {
2630
+ answers = userResponse.response;
2631
+ }
2632
+ else {
2633
+ const answer = typeof userResponse.response === 'string'
2634
+ ? userResponse.response
2635
+ : Array.isArray(userResponse.response) ? userResponse.response.join(', ') : '';
2636
+ answers = { [questions[0].question]: answer };
2637
+ }
2638
+ log.info(`[canUseTool] AskUserQuestion result: questionCount=${questions.length}, answerCount=${Object.keys(answers).length}, responseType=${typeof userResponse.response}, isArray=${Array.isArray(userResponse.response)}, answerKeys=${JSON.stringify(Object.keys(answers))}`);
2639
+ return {
2640
+ behavior: 'allow',
2641
+ updatedInput: {
2642
+ questions,
2643
+ answers,
2644
+ },
2645
+ };
2646
+ }
2647
+ if (userResponse.approved) {
2648
+ return { behavior: 'allow', updatedInput: input };
2472
2649
  }
2473
2650
  else {
2474
- const answer = typeof userResponse.response === 'string'
2475
- ? userResponse.response
2476
- : Array.isArray(userResponse.response) ? userResponse.response.join(', ') : '';
2477
- answers = { [questions[0].question]: answer };
2478
- }
2479
- return {
2480
- behavior: 'allow',
2481
- updatedInput: {
2482
- questions,
2483
- answers,
2484
- },
2485
- };
2486
- }
2487
- if (userResponse.approved) {
2488
- return { behavior: 'allow', updatedInput: input };
2651
+ return { behavior: 'deny', message: 'User denied permission', interrupt: true };
2652
+ }
2489
2653
  }
2490
- else {
2491
- return { behavior: 'deny', message: 'User denied permission', interrupt: true };
2654
+ finally {
2655
+ // Story 35.1: resume on EVERY exit path (resolve / throw / early return). Clear
2656
+ // the pause FIRST so resetTimeout's guard lets this call re-arm the timer; later
2657
+ // SDK activity then resets it normally. Skipping this would leave hang-detection
2658
+ // disabled for the rest of the turn.
2659
+ permissionWaiting = false;
2660
+ resetTimeout('permission-resolved');
2492
2661
  }
2493
2662
  };
2494
2663
  // Activity-based timeout: resets on every SDK callback event
@@ -2496,8 +2665,24 @@ async function handleChatSend(stream, data, abortController, lang) {
2496
2665
  // Timeout value from preferences (with env var override), clamped to 5s–30min range
2497
2666
  const rawTimeoutMs = effectivePrefs.chatTimeoutMs ?? config.chat.timeoutMs;
2498
2667
  const timeoutMs = (rawTimeoutMs >= 5000 && rawTimeoutMs <= 1800000) ? rawTimeoutMs : 300000;
2668
+ // CLI mode has NO inactivity timeout. The activity-based timeout is an SDK-mode safeguard
2669
+ // against a silently-stalled query; in CLI mode it does more harm than good — a turn can be
2670
+ // legitimately quiet (deep thinking, a long tool run) yet the spinner stops repainting, so the
2671
+ // timer would abort healthy work. The genuine stuck case it used to (slowly, confusingly) catch
2672
+ // is now handled directly: the usage-limit notice is detected on the PTY and fails the turn
2673
+ // immediately (cliChatEngine.detectUsageLimit), and a crashed REPL surfaces via pty.onExit.
2674
+ // Anything else ends via end_turn or the user's Stop button. So CLI never arms the timer.
2675
+ const isCliEngine = engineMode === 'cli';
2499
2676
  let lastResetSource = 'initial';
2500
2677
  const resetTimeout = (source) => {
2678
+ if (isCliEngine)
2679
+ return; // CLI mode: no inactivity timeout (see above)
2680
+ // Story 35.1: while awaiting user input (permission / AskUserQuestion) the timer
2681
+ // is paused — ignore every activity-driven reset (onActivity, raw PTY frames,
2682
+ // compact/retry) so the wait stays indefinite. canUseTool clears the pause and
2683
+ // calls this once to re-arm when the user responds.
2684
+ if (permissionWaiting)
2685
+ return;
2501
2686
  if (source)
2502
2687
  lastResetSource = source;
2503
2688
  if (timeoutId)
@@ -2508,6 +2693,71 @@ async function handleChatSend(stream, data, abortController, lang) {
2508
2693
  }, timeoutMs);
2509
2694
  };
2510
2695
  resetTimeout('initial');
2696
+ // CLI screen-stall watchdog (soft, advisory — see cliScreenStallWatchdog). The reconstructed
2697
+ // screen changes ~1×/second while claude is healthy (spinner glyph + ticking clock), so a long
2698
+ // flat-line of NO change is a reliable "claude froze" signal — unlike the removed inactivity
2699
+ // timeout, which watched JSONL activity that legitimately pauses during thinking. We do NOT
2700
+ // auto-abort: emit a soft `cli:screen-stall` so the UI can offer a Stop. CLI mode only, driven by
2701
+ // the mirror frames (so it follows the mirror gate), clamped to 5s–10min (0 = off). Fires only on
2702
+ // a still-running, non-modal turn; disposed at turn end so a stale timer can't leak into the next.
2703
+ const rawStallMs = effectivePrefs.cliScreenStallMs ?? 20000;
2704
+ const screenStallMs = (engineMode !== 'cli' || rawStallMs <= 0)
2705
+ ? 0
2706
+ : Math.min(Math.max(rawStallMs, 5000), 600000);
2707
+ stallWatchdog = createScreenStallWatchdog({
2708
+ stallMs: screenStallMs,
2709
+ isActive: () => stream.status === 'running' && !permissionWaiting,
2710
+ onStallChange: (stalled) => {
2711
+ // Mirror onto the session screen cache so a late-join / reconnect can resync this
2712
+ // transition-only signal (see session:join above). sessionId is set by the time a turn
2713
+ // streams, but it's typed optional at this scope — guard so the cache key is never empty.
2714
+ if (sessionId)
2715
+ setCliScreenStall(sessionId, stalled);
2716
+ emit('cli:screen-stall', { sessionId, stalled });
2717
+ },
2718
+ });
2719
+ // Story 32.7: forward the CLI engine's transient generation-progress counter
2720
+ // ("↓ N tokens · Ns" parsed from the spinner) to the browser. Live-only — the
2721
+ // client skips it on buffer replay (mirrors tool:progress). The SDK engine never
2722
+ // calls this (real token streaming makes it unnecessary). Defined once and passed
2723
+ // as the 6th arg to every sendMessageWithCallbacks below; the queue path omits it
2724
+ // (no live progress UI there — the same asymmetry as onRawMessage).
2725
+ const onGenerationProgress = (progress) => {
2726
+ emit('generation:progress', progress);
2727
+ };
2728
+ // Story 36.2: CLI pre-generation phase (launching/submitting/waiting/null). Same
2729
+ // transient/live-only semantics and the SAME gating as the progress counter below.
2730
+ const onPhase = (phase) => {
2731
+ emit('cli:phase', { phase });
2732
+ };
2733
+ // Story 37.8: full-screen mirror frame. The CLI engine serializes the headless emulator's
2734
+ // CURRENT screen (color intact) and paces it to ~100ms; forward each frame to the room so
2735
+ // the read-only mirror renders the actual claude screen. Self-contained (a whole screen,
2736
+ // not a delta), so the same frame restores a late-join / refresh / collapse-expand. The
2737
+ // session:join + cli:request-screen-frame paths reuse this same event from the cache.
2738
+ const onScreenFrame = (frame) => {
2739
+ stallWatchdog?.noteFrame(frame); // soft screen-stall watchdog (CLI mode; no-op when disabled)
2740
+ emit('cli:screen-frame', { sessionId, frame });
2741
+ };
2742
+ // Story 33.3: gate the progress counter on CLI mode + the user's preference (default
2743
+ // ON, Story 33.2). In SDK mode the engine ignores the callback anyway; passing
2744
+ // `undefined` makes the gate explicit so the 'cli' + pref-OFF case emits nothing.
2745
+ const cliProgress = shouldForwardCliProgress(engineMode, effectivePrefs.cliShowGenerationProgress);
2746
+ const generationProgressCb = cliProgress ? onGenerationProgress : undefined;
2747
+ const cliPhaseCb = cliProgress ? onPhase : undefined;
2748
+ // Mirror gate — a SEPARATE preference (default ON, opt-out), independent of the progress
2749
+ // gate. Forwarded unless the user explicitly turned the mirror off.
2750
+ const screenFrameCb = shouldForwardCliPtyMirror(engineMode, effectivePrefs.cliPtyMirror) ? onScreenFrame : undefined;
2751
+ // Story 37.8: mirror refresh interval (ms) — the user's preference, applied as the engine's
2752
+ // trailing-throttle interval. undefined → engine default (200ms). Harmless when the mirror is off.
2753
+ const mirrorThrottleMs = effectivePrefs.cliMirrorThrottleMs;
2754
+ const backgroundTracker = new BackgroundTaskTracker((state) => {
2755
+ emit('background:waiting', {
2756
+ sessionId: sessionIdRef.current ?? sessionId,
2757
+ waiting: state.waiting,
2758
+ pendingCount: state.pending,
2759
+ });
2760
+ });
2511
2761
  // Build shared callbacks (common logic for browser & queue paths)
2512
2762
  const { callbacks, sessionIdRef } = buildStreamCallbacks({
2513
2763
  emit,
@@ -2518,6 +2768,11 @@ async function handleChatSend(stream, data, abortController, lang) {
2518
2768
  rekeyStream: (sid) => rekeyStream(stream, sid),
2519
2769
  broadcastStreamChange,
2520
2770
  notificationService,
2771
+ // Story 31.3 — browser path resolves projectSlug from the working dir
2772
+ // (async; memoized by the recorder). append is read-only so the lazy
2773
+ // slug resolution cannot disrupt the stream.
2774
+ mcpRecorder: createMcpCallRecorder(() => projectService.findProjectByPath(workingDirectory).then((p) => p?.projectSlug)),
2775
+ backgroundTracker,
2521
2776
  }, {
2522
2777
  onCallbackActivity: (source) => resetTimeout(source),
2523
2778
  onSessionIdResolved: (sid) => {
@@ -2611,9 +2866,28 @@ async function handleChatSend(stream, data, abortController, lang) {
2611
2866
  origOnError?.(error);
2612
2867
  };
2613
2868
  }
2869
+ // Defense B: if the bundled engine doesn't recognize this native-1M model, its
2870
+ // 1M context silently falls back to ~200K (long sessions then compact early and
2871
+ // can break on resume). Warn once per model so the fallback isn't invisible.
2872
+ // Non-blocking: the binary scan must not delay the query.
2873
+ // Only warn when 1M is actually intended (Opus auto, or an explicit `[1m]`
2874
+ // opt-in). A bare Sonnet now runs at 200K by design — warning there would be
2875
+ // a false alarm about a window the user never asked for.
2876
+ const warnModel = model;
2877
+ if (warnModel && effectiveModelIs1M(warnModel) && !warnedMissing1M.has(warnModel)) {
2878
+ void modelMissingNative1MSupport(warnModel).then((missing) => {
2879
+ if (missing && !warnedMissing1M.has(warnModel)) {
2880
+ warnedMissing1M.add(warnModel);
2881
+ log.warn(`Bundled Claude Code engine does not recognize model "${warnModel}" — native 1M context will not apply (falls back to ~200K). Update @anthropic-ai/claude-agent-sdk.`);
2882
+ emit('system:info', { message: `⚠️ "${warnModel}" 1M context is not active — the bundled engine doesn't recognize this model yet, so it's limited to ~200K. Update @anthropic-ai/claude-agent-sdk to enable the full 1M window.` });
2883
+ }
2884
+ }).catch(() => { });
2885
+ }
2614
2886
  try {
2615
2887
  const sendResult = await chatService.sendMessageWithCallbacks(content, callbacks, chatOptions, canUseTool, (messageType) => {
2616
2888
  resetTimeout(`raw:${messageType}`);
2889
+ }, generationProgressCb, cliPhaseCb, screenFrameCb, mirrorThrottleMs, backgroundTracker, (mode) => {
2890
+ emit('permission:mode-change', { mode });
2617
2891
  });
2618
2892
  // SDK may return "No conversation found" as an error result (not a thrown exception).
2619
2893
  // Convert to a thrown error so the retry logic below can handle it.
@@ -2638,6 +2912,7 @@ async function handleChatSend(stream, data, abortController, lang) {
2638
2912
  // /compact is a CLI-level command processed before context evaluation,
2639
2913
  // so it works even when context is overflowing.
2640
2914
  if (isResumeAttempt
2915
+ && autoCompactEnabled
2641
2916
  && parsedError.code === SDKErrorCode.CONTEXT_OVERFLOW
2642
2917
  && !abortController.signal.aborted
2643
2918
  && !hasEmittedOutput) {
@@ -2650,12 +2925,16 @@ async function handleChatSend(stream, data, abortController, lang) {
2650
2925
  // Run /compact to shrink context (preserves session file)
2651
2926
  await chatService.sendMessageWithCallbacks('/compact', callbacks, chatOptions, canUseTool, (messageType) => {
2652
2927
  resetTimeout(`compact:${messageType}`);
2928
+ }, generationProgressCb, cliPhaseCb, screenFrameCb, mirrorThrottleMs, backgroundTracker, (mode) => {
2929
+ emit('permission:mode-change', { mode });
2653
2930
  });
2654
2931
  // Retry the original message after compaction
2655
2932
  log.info(`[AUTO-COMPACT] compaction done, retrying original message: sessionId=${sessionId}`);
2656
2933
  resetTimeout('auto-compact-retry');
2657
2934
  await chatService.sendMessageWithCallbacks(content, callbacks, chatOptions, canUseTool, (messageType) => {
2658
2935
  resetTimeout(`retry:${messageType}`);
2936
+ }, generationProgressCb, cliPhaseCb, screenFrameCb, mirrorThrottleMs, backgroundTracker, (mode) => {
2937
+ emit('permission:mode-change', { mode });
2659
2938
  });
2660
2939
  // Resume failed for other unknown reasons — retry without resume (fresh session).
2661
2940
  // Guards:
@@ -2694,6 +2973,8 @@ async function handleChatSend(stream, data, abortController, lang) {
2694
2973
  resetTimeout('resume-retry');
2695
2974
  await chatService.sendMessageWithCallbacks(content, callbacks, retryOptions, canUseTool, (messageType) => {
2696
2975
  resetTimeout(`raw:${messageType}`);
2976
+ }, generationProgressCb, cliPhaseCb, screenFrameCb, mirrorThrottleMs, backgroundTracker, (mode) => {
2977
+ emit('permission:mode-change', { mode });
2697
2978
  });
2698
2979
  }
2699
2980
  else {
@@ -2712,13 +2993,20 @@ async function handleChatSend(stream, data, abortController, lang) {
2712
2993
  const sdkError = parseSDKError(error, lang);
2713
2994
  log.info(`[CHAIN-DRAIN] handleChatSend catch: sessionId=${stream.sessionId}, aborted=${abortController.signal.aborted}, reason=${abortController.signal.reason}, error=${sdkError.message.slice(0, 120)}`);
2714
2995
  if (sdkError instanceof AbortedError || abortController.signal.aborted) {
2715
- if (abortController.signal.reason === 'user-abort' || abortController.signal.reason === 'another-client') {
2996
+ const abortReason = abortController.signal.reason;
2997
+ if (abortReason === 'user-abort' || abortReason === 'another-client') {
2716
2998
  return false;
2717
2999
  }
2718
- emit('error', {
2719
- code: ERROR_CODES.TIMEOUT_ERROR,
2720
- message: t('ws.error.timeout'),
2721
- });
3000
+ // Only a genuine inactivity timeout (abortController.abort('timeout'), SDK mode) is a "timeout".
3001
+ // Any OTHER abort/interruption surfaces its real message instead of a misleading "응답 시간 초과"
3002
+ // — that catch-all previously masked e.g. a CLI boot-readiness failure across several debugging
3003
+ // cycles (the user saw "timeout" for what was actually a resume-classify abort).
3004
+ if (abortReason === 'timeout') {
3005
+ emit('error', { code: ERROR_CODES.TIMEOUT_ERROR, message: t('ws.error.timeout') });
3006
+ }
3007
+ else {
3008
+ emit('error', { code: ERROR_CODES.CHAT_ERROR, message: sdkError.message });
3009
+ }
2722
3010
  return false;
2723
3011
  }
2724
3012
  emit('error', {
@@ -2731,6 +3019,10 @@ async function handleChatSend(stream, data, abortController, lang) {
2731
3019
  if (timeoutId) {
2732
3020
  clearTimeout(timeoutId);
2733
3021
  }
3022
+ // Screen-stall watchdog teardown: clear the pending timer so a stale fire can't leak into the
3023
+ // NEXT turn (the stream object is reused and its status flips back to 'running'), and drop any
3024
+ // active stall so every viewer's "looks stuck?" affordance clears when the turn ends.
3025
+ stallWatchdog?.dispose();
2734
3026
  // Delete phantom checkpoint files created by SDK file checkpointing.
2735
3027
  // These are separate JSONL files (new UUIDs) containing only
2736
3028
  // file-history-snapshot entries. The same snapshot data already exists