mstro-app 0.4.3 → 0.4.4

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 (306) hide show
  1. package/dist/server/cli/headless/claude-invoker-process.d.ts +11 -0
  2. package/dist/server/cli/headless/claude-invoker-process.d.ts.map +1 -0
  3. package/dist/server/cli/headless/claude-invoker-process.js +140 -0
  4. package/dist/server/cli/headless/claude-invoker-process.js.map +1 -0
  5. package/dist/server/cli/headless/claude-invoker-stall.d.ts +40 -0
  6. package/dist/server/cli/headless/claude-invoker-stall.d.ts.map +1 -0
  7. package/dist/server/cli/headless/claude-invoker-stall.js +98 -0
  8. package/dist/server/cli/headless/claude-invoker-stall.js.map +1 -0
  9. package/dist/server/cli/headless/claude-invoker-stream.d.ts +44 -0
  10. package/dist/server/cli/headless/claude-invoker-stream.d.ts.map +1 -0
  11. package/dist/server/cli/headless/claude-invoker-stream.js +276 -0
  12. package/dist/server/cli/headless/claude-invoker-stream.js.map +1 -0
  13. package/dist/server/cli/headless/claude-invoker-tools.d.ts +21 -0
  14. package/dist/server/cli/headless/claude-invoker-tools.d.ts.map +1 -0
  15. package/dist/server/cli/headless/claude-invoker-tools.js +137 -0
  16. package/dist/server/cli/headless/claude-invoker-tools.js.map +1 -0
  17. package/dist/server/cli/headless/claude-invoker.d.ts +6 -4
  18. package/dist/server/cli/headless/claude-invoker.d.ts.map +1 -1
  19. package/dist/server/cli/headless/claude-invoker.js +10 -807
  20. package/dist/server/cli/headless/claude-invoker.js.map +1 -1
  21. package/dist/server/cli/headless/haiku-assessments.d.ts +62 -0
  22. package/dist/server/cli/headless/haiku-assessments.d.ts.map +1 -0
  23. package/dist/server/cli/headless/haiku-assessments.js +281 -0
  24. package/dist/server/cli/headless/haiku-assessments.js.map +1 -0
  25. package/dist/server/cli/headless/headless-logger.d.ts +3 -2
  26. package/dist/server/cli/headless/headless-logger.d.ts.map +1 -1
  27. package/dist/server/cli/headless/headless-logger.js +28 -5
  28. package/dist/server/cli/headless/headless-logger.js.map +1 -1
  29. package/dist/server/cli/headless/native-timeout-detector.d.ts +44 -0
  30. package/dist/server/cli/headless/native-timeout-detector.d.ts.map +1 -0
  31. package/dist/server/cli/headless/native-timeout-detector.js +99 -0
  32. package/dist/server/cli/headless/native-timeout-detector.js.map +1 -0
  33. package/dist/server/cli/headless/stall-assessor.d.ts +2 -110
  34. package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
  35. package/dist/server/cli/headless/stall-assessor.js +65 -457
  36. package/dist/server/cli/headless/stall-assessor.js.map +1 -1
  37. package/dist/server/cli/improvisation-attachments.d.ts +21 -0
  38. package/dist/server/cli/improvisation-attachments.d.ts.map +1 -0
  39. package/dist/server/cli/improvisation-attachments.js +116 -0
  40. package/dist/server/cli/improvisation-attachments.js.map +1 -0
  41. package/dist/server/cli/improvisation-retry.d.ts +52 -0
  42. package/dist/server/cli/improvisation-retry.d.ts.map +1 -0
  43. package/dist/server/cli/improvisation-retry.js +434 -0
  44. package/dist/server/cli/improvisation-retry.js.map +1 -0
  45. package/dist/server/cli/improvisation-session-manager.d.ts +10 -266
  46. package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
  47. package/dist/server/cli/improvisation-session-manager.js +117 -1079
  48. package/dist/server/cli/improvisation-session-manager.js.map +1 -1
  49. package/dist/server/cli/improvisation-types.d.ts +86 -0
  50. package/dist/server/cli/improvisation-types.d.ts.map +1 -0
  51. package/dist/server/cli/improvisation-types.js +10 -0
  52. package/dist/server/cli/improvisation-types.js.map +1 -0
  53. package/dist/server/cli/prompt-builders.d.ts +68 -0
  54. package/dist/server/cli/prompt-builders.d.ts.map +1 -0
  55. package/dist/server/cli/prompt-builders.js +312 -0
  56. package/dist/server/cli/prompt-builders.js.map +1 -0
  57. package/dist/server/index.js +33 -212
  58. package/dist/server/index.js.map +1 -1
  59. package/dist/server/mcp/bouncer-haiku.d.ts +10 -0
  60. package/dist/server/mcp/bouncer-haiku.d.ts.map +1 -0
  61. package/dist/server/mcp/bouncer-haiku.js +152 -0
  62. package/dist/server/mcp/bouncer-haiku.js.map +1 -0
  63. package/dist/server/mcp/bouncer-integration.d.ts +3 -4
  64. package/dist/server/mcp/bouncer-integration.d.ts.map +1 -1
  65. package/dist/server/mcp/bouncer-integration.js +50 -196
  66. package/dist/server/mcp/bouncer-integration.js.map +1 -1
  67. package/dist/server/mcp/security-analysis.d.ts +38 -0
  68. package/dist/server/mcp/security-analysis.d.ts.map +1 -0
  69. package/dist/server/mcp/security-analysis.js +183 -0
  70. package/dist/server/mcp/security-analysis.js.map +1 -0
  71. package/dist/server/mcp/security-audit.d.ts +1 -1
  72. package/dist/server/mcp/security-audit.d.ts.map +1 -1
  73. package/dist/server/mcp/security-patterns.d.ts +1 -25
  74. package/dist/server/mcp/security-patterns.d.ts.map +1 -1
  75. package/dist/server/mcp/security-patterns.js +55 -260
  76. package/dist/server/mcp/security-patterns.js.map +1 -1
  77. package/dist/server/server-setup.d.ts +22 -0
  78. package/dist/server/server-setup.d.ts.map +1 -0
  79. package/dist/server/server-setup.js +101 -0
  80. package/dist/server/server-setup.js.map +1 -0
  81. package/dist/server/services/file-explorer-ops.d.ts +24 -0
  82. package/dist/server/services/file-explorer-ops.d.ts.map +1 -0
  83. package/dist/server/services/file-explorer-ops.js +211 -0
  84. package/dist/server/services/file-explorer-ops.js.map +1 -0
  85. package/dist/server/services/files.d.ts +2 -85
  86. package/dist/server/services/files.d.ts.map +1 -1
  87. package/dist/server/services/files.js +7 -427
  88. package/dist/server/services/files.js.map +1 -1
  89. package/dist/server/services/plan/composer.d.ts.map +1 -1
  90. package/dist/server/services/plan/composer.js +2 -1
  91. package/dist/server/services/plan/composer.js.map +1 -1
  92. package/dist/server/services/plan/executor.d.ts.map +1 -1
  93. package/dist/server/services/plan/executor.js +3 -1
  94. package/dist/server/services/plan/executor.js.map +1 -1
  95. package/dist/server/services/plan/parser-core.d.ts +20 -0
  96. package/dist/server/services/plan/parser-core.d.ts.map +1 -0
  97. package/dist/server/services/plan/parser-core.js +350 -0
  98. package/dist/server/services/plan/parser-core.js.map +1 -0
  99. package/dist/server/services/plan/parser-migration.d.ts +5 -0
  100. package/dist/server/services/plan/parser-migration.d.ts.map +1 -0
  101. package/dist/server/services/plan/parser-migration.js +124 -0
  102. package/dist/server/services/plan/parser-migration.js.map +1 -0
  103. package/dist/server/services/plan/parser.d.ts +0 -8
  104. package/dist/server/services/plan/parser.d.ts.map +1 -1
  105. package/dist/server/services/plan/parser.js +50 -569
  106. package/dist/server/services/plan/parser.js.map +1 -1
  107. package/dist/server/services/plan/review-gate.d.ts +2 -0
  108. package/dist/server/services/plan/review-gate.d.ts.map +1 -1
  109. package/dist/server/services/plan/review-gate.js +2 -2
  110. package/dist/server/services/plan/review-gate.js.map +1 -1
  111. package/dist/server/services/plan/types.d.ts +2 -0
  112. package/dist/server/services/plan/types.d.ts.map +1 -1
  113. package/dist/server/services/platform-credentials.d.ts +24 -0
  114. package/dist/server/services/platform-credentials.d.ts.map +1 -0
  115. package/dist/server/services/platform-credentials.js +68 -0
  116. package/dist/server/services/platform-credentials.js.map +1 -0
  117. package/dist/server/services/platform.d.ts +1 -31
  118. package/dist/server/services/platform.d.ts.map +1 -1
  119. package/dist/server/services/platform.js +10 -119
  120. package/dist/server/services/platform.js.map +1 -1
  121. package/dist/server/services/terminal/pty-manager.d.ts +7 -97
  122. package/dist/server/services/terminal/pty-manager.d.ts.map +1 -1
  123. package/dist/server/services/terminal/pty-manager.js +53 -266
  124. package/dist/server/services/terminal/pty-manager.js.map +1 -1
  125. package/dist/server/services/terminal/pty-utils.d.ts +57 -0
  126. package/dist/server/services/terminal/pty-utils.d.ts.map +1 -0
  127. package/dist/server/services/terminal/pty-utils.js +141 -0
  128. package/dist/server/services/terminal/pty-utils.js.map +1 -0
  129. package/dist/server/services/websocket/file-definition-handlers.d.ts +4 -0
  130. package/dist/server/services/websocket/file-definition-handlers.d.ts.map +1 -0
  131. package/dist/server/services/websocket/file-definition-handlers.js +153 -0
  132. package/dist/server/services/websocket/file-definition-handlers.js.map +1 -0
  133. package/dist/server/services/websocket/file-explorer-handlers.d.ts.map +1 -1
  134. package/dist/server/services/websocket/file-explorer-handlers.js +52 -391
  135. package/dist/server/services/websocket/file-explorer-handlers.js.map +1 -1
  136. package/dist/server/services/websocket/file-search-handlers.d.ts +5 -0
  137. package/dist/server/services/websocket/file-search-handlers.d.ts.map +1 -0
  138. package/dist/server/services/websocket/file-search-handlers.js +238 -0
  139. package/dist/server/services/websocket/file-search-handlers.js.map +1 -0
  140. package/dist/server/services/websocket/file-utils.js +3 -3
  141. package/dist/server/services/websocket/file-utils.js.map +1 -1
  142. package/dist/server/services/websocket/git-branch-handlers.d.ts +7 -0
  143. package/dist/server/services/websocket/git-branch-handlers.d.ts.map +1 -0
  144. package/dist/server/services/websocket/git-branch-handlers.js +110 -0
  145. package/dist/server/services/websocket/git-branch-handlers.js.map +1 -0
  146. package/dist/server/services/websocket/git-diff-handlers.d.ts +6 -0
  147. package/dist/server/services/websocket/git-diff-handlers.d.ts.map +1 -0
  148. package/dist/server/services/websocket/git-diff-handlers.js +123 -0
  149. package/dist/server/services/websocket/git-diff-handlers.js.map +1 -0
  150. package/dist/server/services/websocket/git-handlers.d.ts +2 -31
  151. package/dist/server/services/websocket/git-handlers.d.ts.map +1 -1
  152. package/dist/server/services/websocket/git-handlers.js +35 -541
  153. package/dist/server/services/websocket/git-handlers.js.map +1 -1
  154. package/dist/server/services/websocket/git-log-handlers.d.ts +6 -0
  155. package/dist/server/services/websocket/git-log-handlers.d.ts.map +1 -0
  156. package/dist/server/services/websocket/git-log-handlers.js +128 -0
  157. package/dist/server/services/websocket/git-log-handlers.js.map +1 -0
  158. package/dist/server/services/websocket/git-pr-handlers.d.ts.map +1 -1
  159. package/dist/server/services/websocket/git-pr-handlers.js +13 -53
  160. package/dist/server/services/websocket/git-pr-handlers.js.map +1 -1
  161. package/dist/server/services/websocket/git-tag-handlers.d.ts +6 -0
  162. package/dist/server/services/websocket/git-tag-handlers.d.ts.map +1 -0
  163. package/dist/server/services/websocket/git-tag-handlers.js +76 -0
  164. package/dist/server/services/websocket/git-tag-handlers.js.map +1 -0
  165. package/dist/server/services/websocket/git-utils.d.ts +43 -0
  166. package/dist/server/services/websocket/git-utils.d.ts.map +1 -0
  167. package/dist/server/services/websocket/git-utils.js +201 -0
  168. package/dist/server/services/websocket/git-utils.js.map +1 -0
  169. package/dist/server/services/websocket/handler.d.ts +2 -0
  170. package/dist/server/services/websocket/handler.d.ts.map +1 -1
  171. package/dist/server/services/websocket/handler.js +37 -126
  172. package/dist/server/services/websocket/handler.js.map +1 -1
  173. package/dist/server/services/websocket/plan-board-handlers.d.ts +11 -0
  174. package/dist/server/services/websocket/plan-board-handlers.d.ts.map +1 -0
  175. package/dist/server/services/websocket/plan-board-handlers.js +218 -0
  176. package/dist/server/services/websocket/plan-board-handlers.js.map +1 -0
  177. package/dist/server/services/websocket/plan-execution-handlers.d.ts +9 -0
  178. package/dist/server/services/websocket/plan-execution-handlers.d.ts.map +1 -0
  179. package/dist/server/services/websocket/plan-execution-handlers.js +142 -0
  180. package/dist/server/services/websocket/plan-execution-handlers.js.map +1 -0
  181. package/dist/server/services/websocket/plan-handlers.d.ts +7 -2
  182. package/dist/server/services/websocket/plan-handlers.d.ts.map +1 -1
  183. package/dist/server/services/websocket/plan-handlers.js +6 -925
  184. package/dist/server/services/websocket/plan-handlers.js.map +1 -1
  185. package/dist/server/services/websocket/plan-helpers.d.ts +19 -0
  186. package/dist/server/services/websocket/plan-helpers.d.ts.map +1 -0
  187. package/dist/server/services/websocket/plan-helpers.js +199 -0
  188. package/dist/server/services/websocket/plan-helpers.js.map +1 -0
  189. package/dist/server/services/websocket/plan-issue-handlers.d.ts +12 -0
  190. package/dist/server/services/websocket/plan-issue-handlers.d.ts.map +1 -0
  191. package/dist/server/services/websocket/plan-issue-handlers.js +162 -0
  192. package/dist/server/services/websocket/plan-issue-handlers.js.map +1 -0
  193. package/dist/server/services/websocket/plan-sprint-handlers.d.ts +7 -0
  194. package/dist/server/services/websocket/plan-sprint-handlers.d.ts.map +1 -0
  195. package/dist/server/services/websocket/plan-sprint-handlers.js +206 -0
  196. package/dist/server/services/websocket/plan-sprint-handlers.js.map +1 -0
  197. package/dist/server/services/websocket/quality-complexity.d.ts +14 -0
  198. package/dist/server/services/websocket/quality-complexity.d.ts.map +1 -0
  199. package/dist/server/services/websocket/quality-complexity.js +262 -0
  200. package/dist/server/services/websocket/quality-complexity.js.map +1 -0
  201. package/dist/server/services/websocket/quality-fix-agent.d.ts +16 -0
  202. package/dist/server/services/websocket/quality-fix-agent.d.ts.map +1 -0
  203. package/dist/server/services/websocket/quality-fix-agent.js +140 -0
  204. package/dist/server/services/websocket/quality-fix-agent.js.map +1 -0
  205. package/dist/server/services/websocket/quality-handlers.d.ts.map +1 -1
  206. package/dist/server/services/websocket/quality-handlers.js +34 -346
  207. package/dist/server/services/websocket/quality-handlers.js.map +1 -1
  208. package/dist/server/services/websocket/quality-linting.d.ts +9 -0
  209. package/dist/server/services/websocket/quality-linting.d.ts.map +1 -0
  210. package/dist/server/services/websocket/quality-linting.js +178 -0
  211. package/dist/server/services/websocket/quality-linting.js.map +1 -0
  212. package/dist/server/services/websocket/quality-review-agent.d.ts +19 -0
  213. package/dist/server/services/websocket/quality-review-agent.d.ts.map +1 -0
  214. package/dist/server/services/websocket/quality-review-agent.js +206 -0
  215. package/dist/server/services/websocket/quality-review-agent.js.map +1 -0
  216. package/dist/server/services/websocket/quality-service.d.ts +3 -51
  217. package/dist/server/services/websocket/quality-service.d.ts.map +1 -1
  218. package/dist/server/services/websocket/quality-service.js +9 -651
  219. package/dist/server/services/websocket/quality-service.js.map +1 -1
  220. package/dist/server/services/websocket/quality-tools.d.ts +23 -0
  221. package/dist/server/services/websocket/quality-tools.d.ts.map +1 -0
  222. package/dist/server/services/websocket/quality-tools.js +208 -0
  223. package/dist/server/services/websocket/quality-tools.js.map +1 -0
  224. package/dist/server/services/websocket/quality-types.d.ts +59 -0
  225. package/dist/server/services/websocket/quality-types.d.ts.map +1 -0
  226. package/dist/server/services/websocket/quality-types.js +101 -0
  227. package/dist/server/services/websocket/quality-types.js.map +1 -0
  228. package/dist/server/services/websocket/session-handlers.d.ts +3 -4
  229. package/dist/server/services/websocket/session-handlers.d.ts.map +1 -1
  230. package/dist/server/services/websocket/session-handlers.js +3 -378
  231. package/dist/server/services/websocket/session-handlers.js.map +1 -1
  232. package/dist/server/services/websocket/session-history.d.ts +4 -0
  233. package/dist/server/services/websocket/session-history.d.ts.map +1 -0
  234. package/dist/server/services/websocket/session-history.js +208 -0
  235. package/dist/server/services/websocket/session-history.js.map +1 -0
  236. package/dist/server/services/websocket/session-initialization.d.ts +5 -0
  237. package/dist/server/services/websocket/session-initialization.d.ts.map +1 -0
  238. package/dist/server/services/websocket/session-initialization.js +163 -0
  239. package/dist/server/services/websocket/session-initialization.js.map +1 -0
  240. package/dist/server/services/websocket/types.d.ts +12 -2
  241. package/dist/server/services/websocket/types.d.ts.map +1 -1
  242. package/package.json +1 -1
  243. package/server/cli/headless/claude-invoker-process.ts +204 -0
  244. package/server/cli/headless/claude-invoker-stall.ts +164 -0
  245. package/server/cli/headless/claude-invoker-stream.ts +353 -0
  246. package/server/cli/headless/claude-invoker-tools.ts +187 -0
  247. package/server/cli/headless/claude-invoker.ts +15 -1096
  248. package/server/cli/headless/haiku-assessments.ts +365 -0
  249. package/server/cli/headless/headless-logger.ts +26 -5
  250. package/server/cli/headless/native-timeout-detector.ts +117 -0
  251. package/server/cli/headless/stall-assessor.ts +65 -618
  252. package/server/cli/improvisation-attachments.ts +148 -0
  253. package/server/cli/improvisation-retry.ts +602 -0
  254. package/server/cli/improvisation-session-manager.ts +140 -1349
  255. package/server/cli/improvisation-types.ts +98 -0
  256. package/server/cli/prompt-builders.ts +370 -0
  257. package/server/index.ts +35 -246
  258. package/server/mcp/bouncer-haiku.ts +182 -0
  259. package/server/mcp/bouncer-integration.ts +87 -248
  260. package/server/mcp/security-analysis.ts +217 -0
  261. package/server/mcp/security-audit.ts +1 -1
  262. package/server/mcp/security-patterns.ts +60 -283
  263. package/server/server-setup.ts +114 -0
  264. package/server/services/file-explorer-ops.ts +293 -0
  265. package/server/services/files.ts +20 -532
  266. package/server/services/plan/composer.ts +2 -1
  267. package/server/services/plan/executor.ts +3 -1
  268. package/server/services/plan/parser-core.ts +406 -0
  269. package/server/services/plan/parser-migration.ts +128 -0
  270. package/server/services/plan/parser.ts +52 -620
  271. package/server/services/plan/review-gate.ts +4 -2
  272. package/server/services/plan/types.ts +2 -0
  273. package/server/services/platform-credentials.ts +83 -0
  274. package/server/services/platform.ts +15 -141
  275. package/server/services/terminal/pty-manager.ts +66 -313
  276. package/server/services/terminal/pty-utils.ts +176 -0
  277. package/server/services/websocket/file-definition-handlers.ts +165 -0
  278. package/server/services/websocket/file-explorer-handlers.ts +37 -452
  279. package/server/services/websocket/file-search-handlers.ts +291 -0
  280. package/server/services/websocket/file-utils.ts +3 -3
  281. package/server/services/websocket/git-branch-handlers.ts +130 -0
  282. package/server/services/websocket/git-diff-handlers.ts +140 -0
  283. package/server/services/websocket/git-handlers.ts +40 -625
  284. package/server/services/websocket/git-log-handlers.ts +149 -0
  285. package/server/services/websocket/git-pr-handlers.ts +17 -62
  286. package/server/services/websocket/git-tag-handlers.ts +91 -0
  287. package/server/services/websocket/git-utils.ts +230 -0
  288. package/server/services/websocket/handler.ts +39 -126
  289. package/server/services/websocket/plan-board-handlers.ts +277 -0
  290. package/server/services/websocket/plan-execution-handlers.ts +184 -0
  291. package/server/services/websocket/plan-handlers.ts +8 -1114
  292. package/server/services/websocket/plan-helpers.ts +215 -0
  293. package/server/services/websocket/plan-issue-handlers.ts +204 -0
  294. package/server/services/websocket/plan-sprint-handlers.ts +252 -0
  295. package/server/services/websocket/quality-complexity.ts +294 -0
  296. package/server/services/websocket/quality-fix-agent.ts +181 -0
  297. package/server/services/websocket/quality-handlers.ts +36 -404
  298. package/server/services/websocket/quality-linting.ts +187 -0
  299. package/server/services/websocket/quality-review-agent.ts +246 -0
  300. package/server/services/websocket/quality-service.ts +11 -762
  301. package/server/services/websocket/quality-tools.ts +209 -0
  302. package/server/services/websocket/quality-types.ts +169 -0
  303. package/server/services/websocket/session-handlers.ts +5 -437
  304. package/server/services/websocket/session-history.ts +222 -0
  305. package/server/services/websocket/session-initialization.ts +209 -0
  306. package/server/services/websocket/types.ts +17 -0
@@ -0,0 +1,602 @@
1
+ // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
+ // Licensed under the MIT License. See LICENSE file for details.
3
+
4
+ /**
5
+ * Retry and recovery logic for improvisation sessions.
6
+ * Handles context loss, tool timeouts, signal crashes, and premature completion.
7
+ */
8
+
9
+ import { AnalyticsEvents, trackEvent } from '../services/analytics.js';
10
+ import { hlog } from './headless/headless-logger.js';
11
+ import { HeadlessRunner } from './headless/index.js';
12
+ import { assessBestResult, assessContextLoss, assessPrematureCompletion, type ContextLossContext } from './headless/stall-assessor.js';
13
+ import type { ExecutionCheckpoint } from './headless/types.js';
14
+ import type { FileAttachment, HeadlessRunResult, ImprovisationOptions, MovementRecord, RetryLoopState, SessionHistory } from './improvisation-types.js';
15
+ import { scoreRunResult } from './improvisation-types.js';
16
+ import {
17
+ buildContextRecoveryPrompt,
18
+ buildFreshRecoveryPrompt,
19
+ buildHistoricalContext,
20
+ buildInterMovementRecoveryPrompt,
21
+ buildResumeRetryPrompt,
22
+ buildRetryPrompt,
23
+ buildSignalCrashRecoveryPrompt,
24
+ extractHistoricalToolResults,
25
+ } from './prompt-builders.js';
26
+
27
+ /** Callbacks the retry logic needs from the session manager */
28
+ export interface RetryCallbacks {
29
+ isCancelled: () => boolean;
30
+ queueOutput: (text: string) => void;
31
+ flushOutputQueue: () => void;
32
+ emit: (event: string, ...args: unknown[]) => void;
33
+ addEventLog: (entry: { type: string; data: unknown; timestamp: number }) => void;
34
+ setRunner: (runner: HeadlessRunner | null) => void;
35
+ }
36
+
37
+ /** Session state the retry logic reads/writes */
38
+ export interface RetrySessionState {
39
+ options: ImprovisationOptions;
40
+ claudeSessionId: string | undefined;
41
+ isFirstPrompt: boolean;
42
+ isResumedSession: boolean;
43
+ history: SessionHistory;
44
+ executionStartTimestamp: number | undefined;
45
+ }
46
+
47
+ // ========== Resume Strategy ==========
48
+
49
+ /** Determine whether to use --resume and which session ID */
50
+ export function determineResumeStrategy(
51
+ state: RetryLoopState,
52
+ session: RetrySessionState,
53
+ ): { useResume: boolean; resumeSessionId: string | undefined } {
54
+ if (state.freshRecoveryMode) {
55
+ state.freshRecoveryMode = false;
56
+ return { useResume: false, resumeSessionId: undefined };
57
+ }
58
+ if (state.contextRecoverySessionId) {
59
+ const id = state.contextRecoverySessionId;
60
+ state.contextRecoverySessionId = undefined;
61
+ return { useResume: true, resumeSessionId: id };
62
+ }
63
+ if (state.retryNumber === 0) {
64
+ return { useResume: !session.isFirstPrompt, resumeSessionId: session.claudeSessionId };
65
+ }
66
+ if (state.lastWatchdogCheckpoint?.inProgressTools.length === 0 && state.lastWatchdogCheckpoint.claudeSessionId) {
67
+ return { useResume: true, resumeSessionId: state.lastWatchdogCheckpoint.claudeSessionId };
68
+ }
69
+ return { useResume: false, resumeSessionId: undefined };
70
+ }
71
+
72
+ // ========== Runner Creation ==========
73
+
74
+ /** Create HeadlessRunner for one retry iteration */
75
+ export function createExecutionRunner(
76
+ state: RetryLoopState,
77
+ session: RetrySessionState,
78
+ callbacks: RetryCallbacks,
79
+ sequenceNumber: number,
80
+ useResume: boolean,
81
+ resumeSessionId: string | undefined,
82
+ imageAttachments: FileAttachment[] | undefined,
83
+ sandboxed: boolean | undefined,
84
+ workingDirOverride?: string,
85
+ ): HeadlessRunner {
86
+ return new HeadlessRunner({
87
+ workingDir: workingDirOverride || session.options.workingDir,
88
+ tokenBudgetThreshold: session.options.tokenBudgetThreshold,
89
+ maxSessions: session.options.maxSessions,
90
+ verbose: session.options.verbose,
91
+ noColor: session.options.noColor,
92
+ model: session.options.model,
93
+ improvisationMode: true,
94
+ movementNumber: sequenceNumber,
95
+ continueSession: useResume,
96
+ claudeSessionId: resumeSessionId,
97
+ outputCallback: (text: string) => {
98
+ if (callbacks.isCancelled()) return;
99
+ callbacks.addEventLog({ type: 'output', data: { text, timestamp: Date.now() }, timestamp: Date.now() });
100
+ callbacks.queueOutput(text);
101
+ callbacks.flushOutputQueue();
102
+ },
103
+ thinkingCallback: (text: string) => {
104
+ if (callbacks.isCancelled()) return;
105
+ callbacks.addEventLog({ type: 'thinking', data: { text }, timestamp: Date.now() });
106
+ callbacks.emit('onThinking', text);
107
+ callbacks.flushOutputQueue();
108
+ },
109
+ toolUseCallback: (event) => {
110
+ if (callbacks.isCancelled()) return;
111
+ callbacks.addEventLog({ type: 'toolUse', data: { ...event, timestamp: Date.now() }, timestamp: Date.now() });
112
+ callbacks.emit('onToolUse', event);
113
+ callbacks.flushOutputQueue();
114
+ },
115
+ tokenUsageCallback: (usage) => {
116
+ if (callbacks.isCancelled()) return;
117
+ callbacks.emit('onTokenUsage', usage);
118
+ },
119
+ directPrompt: state.currentPrompt,
120
+ imageAttachments,
121
+ promptContext: (state.retryNumber === 0 && session.isResumedSession && session.isFirstPrompt)
122
+ ? { accumulatedKnowledge: buildHistoricalContext(session.history.movements), filesModified: [] }
123
+ : undefined,
124
+ onToolTimeout: (checkpoint: ExecutionCheckpoint) => {
125
+ state.checkpointRef.value = checkpoint;
126
+ },
127
+ sandboxed,
128
+ });
129
+ }
130
+
131
+ // ========== Context Loss Detection ==========
132
+
133
+ /** Detect resume context loss (Path 1): session expired on --resume */
134
+ export function detectResumeContextLoss(
135
+ result: HeadlessRunResult,
136
+ state: RetryLoopState,
137
+ useResume: boolean,
138
+ maxRetries: number,
139
+ nativeTimeouts: number,
140
+ verbose: boolean,
141
+ ): void {
142
+ if (!useResume || state.checkpointRef.value || state.retryNumber >= maxRetries || nativeTimeouts > 0) {
143
+ return;
144
+ }
145
+ if (!result.assistantResponse || result.assistantResponse.trim().length === 0) {
146
+ state.contextLost = true;
147
+ if (verbose) hlog('[CONTEXT-RECOVERY] Resume context loss: null/empty response');
148
+ } else if (result.resumeBufferedOutput !== undefined) {
149
+ state.contextLost = true;
150
+ if (verbose) hlog('[CONTEXT-RECOVERY] Resume context loss: buffer never flushed (no thinking/tools)');
151
+ } else if (
152
+ (!result.toolUseHistory || result.toolUseHistory.length === 0) &&
153
+ !result.thinkingOutput &&
154
+ result.assistantResponse.length < 500
155
+ ) {
156
+ state.contextLost = true;
157
+ if (verbose) hlog('[CONTEXT-RECOVERY] Resume context loss: no tools, no thinking, short response');
158
+ }
159
+ }
160
+
161
+ /** Detect native timeout context loss (Path 2): tool timeouts caused confusion */
162
+ export async function detectNativeTimeoutContextLoss(
163
+ result: HeadlessRunResult,
164
+ state: RetryLoopState,
165
+ maxRetries: number,
166
+ nativeTimeouts: number,
167
+ verbose: boolean,
168
+ ): Promise<void> {
169
+ if (state.contextLost) return;
170
+
171
+ const succeededIds = new Set<string>();
172
+ const allIds = new Set<string>();
173
+ for (const t of result.toolUseHistory ?? []) {
174
+ allIds.add(t.toolId);
175
+ if (t.result !== undefined) succeededIds.add(t.toolId);
176
+ }
177
+ const toolsWithoutResult = [...allIds].filter(id => !succeededIds.has(id)).length;
178
+ const effectiveTimeouts = Math.max(nativeTimeouts, toolsWithoutResult);
179
+
180
+ if (effectiveTimeouts === 0 || !result.assistantResponse || state.checkpointRef.value || state.retryNumber >= maxRetries) {
181
+ return;
182
+ }
183
+
184
+ const writeToolNames = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit']);
185
+ const contextLossCtx: ContextLossContext = {
186
+ assistantResponse: result.assistantResponse,
187
+ effectiveTimeouts,
188
+ nativeTimeoutCount: nativeTimeouts,
189
+ successfulToolCalls: result.toolUseHistory?.filter(t => t.result !== undefined && !t.isError).length ?? 0,
190
+ thinkingOutputLength: result.thinkingOutput?.length ?? 0,
191
+ hasSuccessfulWrite: result.toolUseHistory?.some(
192
+ t => writeToolNames.has(t.toolName) && t.result !== undefined && !t.isError
193
+ ) ?? false,
194
+ };
195
+
196
+ const claudeCmd = process.env.CLAUDE_COMMAND || 'claude';
197
+ const verdict = await assessContextLoss(contextLossCtx, claudeCmd, verbose);
198
+ state.contextLost = verdict.contextLost;
199
+ if (verbose) {
200
+ hlog(`[CONTEXT-RECOVERY] Haiku verdict: ${state.contextLost ? 'LOST' : 'OK'} — ${verdict.reason}`);
201
+ }
202
+ }
203
+
204
+ // ========== Tool Result Accumulation ==========
205
+
206
+ const MAX_ACCUMULATED_RESULTS = 50;
207
+
208
+ /** Accumulate completed tool results from a run into the retry state */
209
+ export function accumulateToolResults(result: HeadlessRunResult, state: RetryLoopState): void {
210
+ if (!result.toolUseHistory) return;
211
+ for (const t of result.toolUseHistory) {
212
+ if (t.result !== undefined) {
213
+ state.accumulatedToolResults.push({
214
+ toolName: t.toolName,
215
+ toolId: t.toolId,
216
+ toolInput: t.toolInput,
217
+ result: t.result,
218
+ isError: t.isError,
219
+ duration: t.duration,
220
+ });
221
+ }
222
+ }
223
+ if (state.accumulatedToolResults.length > MAX_ACCUMULATED_RESULTS) {
224
+ state.accumulatedToolResults = state.accumulatedToolResults.slice(-MAX_ACCUMULATED_RESULTS);
225
+ }
226
+ }
227
+
228
+ // ========== Recovery Strategies ==========
229
+
230
+ /** Handle inter-movement context loss recovery (resume session expired) */
231
+ export function applyInterMovementRecovery(
232
+ state: RetryLoopState,
233
+ promptWithAttachments: string,
234
+ history: MovementRecord[],
235
+ callbacks: RetryCallbacks,
236
+ ): void {
237
+ const historicalResults = extractHistoricalToolResults(history);
238
+ const allResults = [...historicalResults, ...state.accumulatedToolResults];
239
+
240
+ callbacks.emit('onAutoRetry', {
241
+ retryNumber: state.retryNumber,
242
+ maxRetries: 3,
243
+ toolName: 'InterMovementRecovery',
244
+ completedCount: allResults.length,
245
+ });
246
+ callbacks.queueOutput(
247
+ `\n[[MSTRO_CONTEXT_RECOVERY]] Session context expired — continuing with ${allResults.length} preserved results from prior work (retry ${state.retryNumber}/3).\n`
248
+ );
249
+ callbacks.flushOutputQueue();
250
+
251
+ state.freshRecoveryMode = true;
252
+ state.currentPrompt = buildInterMovementRecoveryPrompt(promptWithAttachments, allResults, history);
253
+ }
254
+
255
+ /** Handle native-timeout context loss recovery (tool timeouts caused confusion) */
256
+ export function applyNativeTimeoutRecovery(
257
+ result: HeadlessRunResult,
258
+ state: RetryLoopState,
259
+ promptWithAttachments: string,
260
+ session: RetrySessionState,
261
+ callbacks: RetryCallbacks,
262
+ ): void {
263
+ const completedCount = state.accumulatedToolResults.length;
264
+
265
+ callbacks.emit('onAutoRetry', {
266
+ retryNumber: state.retryNumber,
267
+ maxRetries: 3,
268
+ toolName: 'ContextRecovery',
269
+ completedCount,
270
+ });
271
+
272
+ if (result.claudeSessionId && state.retryNumber === 1) {
273
+ callbacks.queueOutput(
274
+ `\n[[MSTRO_CONTEXT_RECOVERY]] Context loss detected — resuming session with ${completedCount} preserved results (retry ${state.retryNumber}/3).\n`
275
+ );
276
+ callbacks.flushOutputQueue();
277
+ state.contextRecoverySessionId = result.claudeSessionId;
278
+ session.claudeSessionId = result.claudeSessionId;
279
+ state.currentPrompt = buildContextRecoveryPrompt(promptWithAttachments);
280
+ } else {
281
+ callbacks.queueOutput(
282
+ `\n[[MSTRO_CONTEXT_RECOVERY]] Continuing with fresh context — ${completedCount} preserved results injected (retry ${state.retryNumber}/3).\n`
283
+ );
284
+ callbacks.flushOutputQueue();
285
+ state.freshRecoveryMode = true;
286
+ state.currentPrompt = buildFreshRecoveryPrompt(promptWithAttachments, state.accumulatedToolResults, state.timedOutTools);
287
+ }
288
+ }
289
+
290
+ /** Check if context loss recovery should trigger. Returns true if loop should continue. */
291
+ export function shouldRetryContextLoss(
292
+ result: HeadlessRunResult,
293
+ state: RetryLoopState,
294
+ session: RetrySessionState,
295
+ useResume: boolean,
296
+ nativeTimeouts: number,
297
+ maxRetries: number,
298
+ promptWithAttachments: string,
299
+ callbacks: RetryCallbacks,
300
+ ): boolean {
301
+ if (state.checkpointRef.value || state.retryNumber >= maxRetries || !state.contextLost) {
302
+ return false;
303
+ }
304
+ accumulateToolResults(result, state);
305
+ state.retryNumber++;
306
+ const path = (useResume && nativeTimeouts === 0) ? 'InterMovementRecovery' : 'NativeTimeoutRecovery';
307
+ state.retryLog.push({
308
+ retryNumber: state.retryNumber,
309
+ path,
310
+ reason: `Context lost (${nativeTimeouts} timeouts, ${state.accumulatedToolResults.length} tools preserved)`,
311
+ timestamp: Date.now(),
312
+ });
313
+ if (useResume && nativeTimeouts === 0) {
314
+ applyInterMovementRecovery(state, promptWithAttachments, session.history.movements, callbacks);
315
+ } else {
316
+ applyNativeTimeoutRecovery(result, state, promptWithAttachments, session, callbacks);
317
+ }
318
+ return true;
319
+ }
320
+
321
+ /** Handle tool timeout checkpoint. Returns true if loop should continue. */
322
+ export function applyToolTimeoutRetry(
323
+ state: RetryLoopState,
324
+ maxRetries: number,
325
+ promptWithAttachments: string,
326
+ callbacks: RetryCallbacks,
327
+ model: string | undefined,
328
+ ): boolean {
329
+ if (!state.checkpointRef.value || state.retryNumber >= maxRetries) {
330
+ return false;
331
+ }
332
+
333
+ const cp: ExecutionCheckpoint = state.checkpointRef.value;
334
+ state.retryNumber++;
335
+
336
+ state.timedOutTools.push({
337
+ toolName: cp.hungTool.toolName,
338
+ input: cp.hungTool.input ?? {},
339
+ timeoutMs: cp.hungTool.timeoutMs,
340
+ });
341
+
342
+ const canResumeSession = cp.inProgressTools.length === 0 && !!cp.claudeSessionId;
343
+ state.retryLog.push({
344
+ retryNumber: state.retryNumber,
345
+ path: 'ToolTimeout',
346
+ reason: `${cp.hungTool.toolName} timed out after ${cp.hungTool.timeoutMs}ms, ${cp.completedTools.length} tools completed, ${canResumeSession ? 'resuming' : 'fresh start'}`,
347
+ timestamp: Date.now(),
348
+ });
349
+ callbacks.emit('onAutoRetry', {
350
+ retryNumber: state.retryNumber,
351
+ maxRetries,
352
+ toolName: cp.hungTool.toolName,
353
+ url: cp.hungTool.url,
354
+ completedCount: cp.completedTools.length,
355
+ });
356
+
357
+ trackEvent(AnalyticsEvents.IMPROVISE_AUTO_RETRY, {
358
+ retry_number: state.retryNumber,
359
+ hung_tool: cp.hungTool.toolName,
360
+ hung_url: cp.hungTool.url?.slice(0, 200),
361
+ completed_tools: cp.completedTools.length,
362
+ elapsed_ms: cp.elapsedMs,
363
+ resume_attempted: canResumeSession,
364
+ model: model || 'default',
365
+ });
366
+
367
+ state.currentPrompt = canResumeSession
368
+ ? buildResumeRetryPrompt(cp, state.timedOutTools)
369
+ : buildRetryPrompt(cp, promptWithAttachments, state.timedOutTools);
370
+
371
+ callbacks.queueOutput(
372
+ `\n[[MSTRO_AUTO_RETRY]] Auto-retry ${state.retryNumber}/${maxRetries}: ${canResumeSession ? 'Resuming session' : 'Continuing'} with ${cp.completedTools.length} successful results, skipping failed ${cp.hungTool.toolName}.\n`
373
+ );
374
+ callbacks.flushOutputQueue();
375
+
376
+ return true;
377
+ }
378
+
379
+ /** Detect and retry after a signal crash. Returns true if loop should continue. */
380
+ export function shouldRetrySignalCrash(
381
+ result: HeadlessRunResult,
382
+ state: RetryLoopState,
383
+ session: RetrySessionState,
384
+ maxRetries: number,
385
+ promptWithAttachments: string,
386
+ callbacks: RetryCallbacks,
387
+ ): boolean {
388
+ const isSignalCrash = !!result.signalName;
389
+ const exitCodeSignal = !result.completed && !result.signalName && result.error?.match(/exited with code (1[2-9]\d|[2-9]\d{2})/);
390
+ if ((!isSignalCrash && !exitCodeSignal) || state.retryNumber >= maxRetries) {
391
+ return false;
392
+ }
393
+ if (state.checkpointRef.value) {
394
+ return false;
395
+ }
396
+
397
+ accumulateToolResults(result, state);
398
+ state.retryNumber++;
399
+
400
+ const completedCount = state.accumulatedToolResults.length;
401
+ const signalInfo = result.signalName || 'unknown signal';
402
+ const useResume = !!result.claudeSessionId && state.retryNumber === 1;
403
+
404
+ state.retryLog.push({
405
+ retryNumber: state.retryNumber,
406
+ path: 'SignalCrash',
407
+ reason: `Process killed (${signalInfo}), ${completedCount} tools preserved, ${useResume ? 'resuming' : 'fresh start'}`,
408
+ timestamp: Date.now(),
409
+ });
410
+
411
+ callbacks.emit('onAutoRetry', {
412
+ retryNumber: state.retryNumber,
413
+ maxRetries,
414
+ toolName: `SignalCrash(${signalInfo})`,
415
+ completedCount,
416
+ });
417
+
418
+ trackEvent(AnalyticsEvents.IMPROVISE_AUTO_RETRY, {
419
+ retry_number: state.retryNumber,
420
+ hung_tool: `signal_crash:${signalInfo}`,
421
+ completed_tools: completedCount,
422
+ resume_attempted: useResume,
423
+ });
424
+
425
+ if (useResume) {
426
+ callbacks.queueOutput(
427
+ `\n[[MSTRO_SIGNAL_RECOVERY]] Process killed (${signalInfo}) — resuming session with ${completedCount} preserved results (retry ${state.retryNumber}/${maxRetries}).\n`
428
+ );
429
+ callbacks.flushOutputQueue();
430
+ state.contextRecoverySessionId = result.claudeSessionId;
431
+ session.claudeSessionId = result.claudeSessionId;
432
+ state.currentPrompt = buildSignalCrashRecoveryPrompt(promptWithAttachments, true);
433
+ } else {
434
+ callbacks.queueOutput(
435
+ `\n[[MSTRO_SIGNAL_RECOVERY]] Process killed (${signalInfo}) — restarting with ${completedCount} preserved results (retry ${state.retryNumber}/${maxRetries}).\n`
436
+ );
437
+ callbacks.flushOutputQueue();
438
+ state.freshRecoveryMode = true;
439
+ const allResults = [...extractHistoricalToolResults(session.history.movements), ...state.accumulatedToolResults];
440
+ state.currentPrompt = buildSignalCrashRecoveryPrompt(promptWithAttachments, false, allResults);
441
+ }
442
+
443
+ return true;
444
+ }
445
+
446
+ // ========== Premature Completion ==========
447
+
448
+ /** Guard checks for premature completion */
449
+ function isPrematureCompletionCandidate(
450
+ result: HeadlessRunResult,
451
+ state: RetryLoopState,
452
+ maxRetries: number,
453
+ ): boolean {
454
+ if (!result.completed || result.signalName || state.retryNumber >= maxRetries) return false;
455
+ if (state.checkpointRef.value || state.contextLost) return false;
456
+ if (!result.claudeSessionId || !result.stopReason) return false;
457
+ return result.stopReason === 'max_tokens' || result.stopReason === 'end_turn';
458
+ }
459
+
460
+ /** Use Haiku to assess whether an end_turn response is genuinely complete */
461
+ async function assessEndTurnCompletion(result: HeadlessRunResult, verbose: boolean): Promise<boolean> {
462
+ if (!result.assistantResponse) return false;
463
+
464
+ const claudeCmd = process.env.CLAUDE_COMMAND || 'claude';
465
+ const verdict = await assessPrematureCompletion({
466
+ responseTail: result.assistantResponse.slice(-800),
467
+ successfulToolCalls: result.toolUseHistory?.filter(t => t.result !== undefined && !t.isError).length ?? 0,
468
+ hasThinking: !!result.thinkingOutput,
469
+ responseLength: result.assistantResponse.length,
470
+ }, claudeCmd, verbose);
471
+
472
+ if (verbose) {
473
+ hlog(`[PREMATURE-COMPLETION] Haiku verdict: ${verdict.isIncomplete ? 'INCOMPLETE' : 'COMPLETE'} — ${verdict.reason}`);
474
+ }
475
+ return verdict.isIncomplete;
476
+ }
477
+
478
+ /** Apply premature completion retry */
479
+ function applyPrematureCompletionRetry(
480
+ result: HeadlessRunResult,
481
+ state: RetryLoopState,
482
+ session: RetrySessionState,
483
+ maxRetries: number,
484
+ stopReason: string,
485
+ isMaxTokens: boolean,
486
+ callbacks: RetryCallbacks,
487
+ ): void {
488
+ state.retryNumber++;
489
+ const reason = isMaxTokens ? 'Output limit reached' : 'Task appears unfinished (AI assessment)';
490
+
491
+ state.retryLog.push({
492
+ retryNumber: state.retryNumber,
493
+ path: 'PrematureCompletion',
494
+ reason,
495
+ timestamp: Date.now(),
496
+ });
497
+
498
+ callbacks.emit('onAutoRetry', {
499
+ retryNumber: state.retryNumber,
500
+ maxRetries,
501
+ toolName: `PrematureCompletion(${stopReason})`,
502
+ completedCount: result.toolUseHistory?.length ?? 0,
503
+ });
504
+
505
+ trackEvent(AnalyticsEvents.IMPROVISE_AUTO_RETRY, {
506
+ retry_number: state.retryNumber,
507
+ hung_tool: `premature_completion:${stopReason}`,
508
+ completed_tools: result.toolUseHistory?.length ?? 0,
509
+ resume_attempted: true,
510
+ });
511
+
512
+ callbacks.queueOutput(
513
+ `\n${reason} — resuming session (retry ${state.retryNumber}/${maxRetries}).\n`
514
+ );
515
+ callbacks.flushOutputQueue();
516
+
517
+ state.contextRecoverySessionId = result.claudeSessionId;
518
+ session.claudeSessionId = result.claudeSessionId;
519
+ state.currentPrompt = 'continue';
520
+ }
521
+
522
+ /** Detect and retry premature completion. Returns true if loop should continue. */
523
+ export async function shouldRetryPrematureCompletion(
524
+ result: HeadlessRunResult,
525
+ state: RetryLoopState,
526
+ session: RetrySessionState,
527
+ maxRetries: number,
528
+ callbacks: RetryCallbacks,
529
+ ): Promise<boolean> {
530
+ if (!isPrematureCompletionCandidate(result, state, maxRetries)) {
531
+ return false;
532
+ }
533
+
534
+ const stopReason = result.stopReason!;
535
+ const isMaxTokens = stopReason === 'max_tokens';
536
+ const isIncomplete = isMaxTokens || await assessEndTurnCompletion(result, session.options.verbose);
537
+
538
+ if (!isIncomplete) return false;
539
+
540
+ applyPrematureCompletionRetry(result, state, session, maxRetries, stopReason, isMaxTokens, callbacks);
541
+ return true;
542
+ }
543
+
544
+ // ========== Best Result Selection ==========
545
+
546
+ /** Select the best result across retries using Haiku assessment */
547
+ export async function selectBestResult(
548
+ state: RetryLoopState,
549
+ result: HeadlessRunResult,
550
+ userPrompt: string,
551
+ verbose: boolean,
552
+ ): Promise<HeadlessRunResult> {
553
+ if (!state.bestResult || state.bestResult === result || state.retryNumber === 0) {
554
+ return result;
555
+ }
556
+
557
+ const claudeCmd = process.env.CLAUDE_COMMAND || 'claude';
558
+ const bestToolCount = state.bestResult.toolUseHistory?.filter(t => t.result !== undefined && !t.isError).length ?? 0;
559
+ const currentToolCount = result.toolUseHistory?.filter(t => t.result !== undefined && !t.isError).length ?? 0;
560
+
561
+ try {
562
+ const verdict = await assessBestResult({
563
+ originalPrompt: userPrompt,
564
+ resultA: {
565
+ successfulToolCalls: bestToolCount,
566
+ responseLength: state.bestResult.assistantResponse?.length ?? 0,
567
+ hasThinking: !!state.bestResult.thinkingOutput,
568
+ responseTail: (state.bestResult.assistantResponse ?? '').slice(-500),
569
+ },
570
+ resultB: {
571
+ successfulToolCalls: currentToolCount,
572
+ responseLength: result.assistantResponse?.length ?? 0,
573
+ hasThinking: !!result.thinkingOutput,
574
+ responseTail: (result.assistantResponse ?? '').slice(-500),
575
+ },
576
+ }, claudeCmd, verbose);
577
+
578
+ if (verdict.winner === 'A') {
579
+ if (verbose) hlog(`[BEST-RESULT] Haiku picked earlier attempt: ${verdict.reason}`);
580
+ return mergeResultSessionId(state.bestResult, result.claudeSessionId);
581
+ }
582
+ if (verbose) hlog(`[BEST-RESULT] Haiku picked final attempt: ${verdict.reason}`);
583
+ return result;
584
+ } catch {
585
+ return fallbackBestResult(state.bestResult, result, verbose);
586
+ }
587
+ }
588
+
589
+ function mergeResultSessionId(result: HeadlessRunResult, sessionId: string | undefined): HeadlessRunResult {
590
+ if (sessionId) return { ...result, claudeSessionId: sessionId };
591
+ return result;
592
+ }
593
+
594
+ function fallbackBestResult(bestResult: HeadlessRunResult, result: HeadlessRunResult, verbose: boolean): HeadlessRunResult {
595
+ if (scoreRunResult(bestResult) > scoreRunResult(result)) {
596
+ if (verbose) {
597
+ hlog(`[BEST-RESULT] Haiku unavailable, numeric fallback: earlier attempt (score ${scoreRunResult(bestResult)} vs ${scoreRunResult(result)})`);
598
+ }
599
+ return mergeResultSessionId(bestResult, result.claudeSessionId);
600
+ }
601
+ return result;
602
+ }