aicodeman 0.2.9 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (374) hide show
  1. package/README.md +118 -4
  2. package/dist/ai-idle-checker.d.ts.map +1 -1
  3. package/dist/ai-idle-checker.js +3 -2
  4. package/dist/ai-idle-checker.js.map +1 -1
  5. package/dist/ai-plan-checker.d.ts.map +1 -1
  6. package/dist/ai-plan-checker.js +3 -2
  7. package/dist/ai-plan-checker.js.map +1 -1
  8. package/dist/bash-tool-parser.d.ts +2 -3
  9. package/dist/bash-tool-parser.d.ts.map +1 -1
  10. package/dist/bash-tool-parser.js +14 -31
  11. package/dist/bash-tool-parser.js.map +1 -1
  12. package/dist/config/ai-defaults.d.ts +16 -0
  13. package/dist/config/ai-defaults.d.ts.map +1 -0
  14. package/dist/config/ai-defaults.js +16 -0
  15. package/dist/config/ai-defaults.js.map +1 -0
  16. package/dist/config/auth-config.d.ts +19 -0
  17. package/dist/config/auth-config.d.ts.map +1 -0
  18. package/dist/config/auth-config.js +28 -0
  19. package/dist/config/auth-config.js.map +1 -0
  20. package/dist/config/exec-timeout.d.ts +10 -0
  21. package/dist/config/exec-timeout.d.ts.map +1 -0
  22. package/dist/config/exec-timeout.js +10 -0
  23. package/dist/config/exec-timeout.js.map +1 -0
  24. package/dist/config/map-limits.d.ts +4 -0
  25. package/dist/config/map-limits.d.ts.map +1 -1
  26. package/dist/config/map-limits.js +7 -0
  27. package/dist/config/map-limits.js.map +1 -1
  28. package/dist/config/server-timing.d.ts +42 -0
  29. package/dist/config/server-timing.d.ts.map +1 -0
  30. package/dist/config/server-timing.js +57 -0
  31. package/dist/config/server-timing.js.map +1 -0
  32. package/dist/config/team-config.d.ts +16 -0
  33. package/dist/config/team-config.d.ts.map +1 -0
  34. package/dist/config/team-config.js +16 -0
  35. package/dist/config/team-config.js.map +1 -0
  36. package/dist/config/terminal-limits.d.ts +18 -0
  37. package/dist/config/terminal-limits.d.ts.map +1 -0
  38. package/dist/config/terminal-limits.js +18 -0
  39. package/dist/config/terminal-limits.js.map +1 -0
  40. package/dist/config/tunnel-config.d.ts +27 -0
  41. package/dist/config/tunnel-config.d.ts.map +1 -0
  42. package/dist/config/tunnel-config.js +36 -0
  43. package/dist/config/tunnel-config.js.map +1 -0
  44. package/dist/hooks-config.d.ts +21 -6
  45. package/dist/hooks-config.d.ts.map +1 -1
  46. package/dist/hooks-config.js +28 -12
  47. package/dist/hooks-config.js.map +1 -1
  48. package/dist/image-watcher.d.ts +4 -4
  49. package/dist/image-watcher.d.ts.map +1 -1
  50. package/dist/image-watcher.js +17 -30
  51. package/dist/image-watcher.js.map +1 -1
  52. package/dist/index.js +1 -2
  53. package/dist/index.js.map +1 -1
  54. package/dist/plan-orchestrator.d.ts +2 -24
  55. package/dist/plan-orchestrator.d.ts.map +1 -1
  56. package/dist/plan-orchestrator.js.map +1 -1
  57. package/dist/prompts/planner.d.ts +7 -8
  58. package/dist/prompts/planner.d.ts.map +1 -1
  59. package/dist/prompts/planner.js +7 -8
  60. package/dist/prompts/planner.js.map +1 -1
  61. package/dist/prompts/research-agent.d.ts +6 -4
  62. package/dist/prompts/research-agent.d.ts.map +1 -1
  63. package/dist/prompts/research-agent.js +6 -4
  64. package/dist/prompts/research-agent.js.map +1 -1
  65. package/dist/push-store.d.ts +1 -1
  66. package/dist/push-store.d.ts.map +1 -1
  67. package/dist/push-store.js +4 -12
  68. package/dist/push-store.js.map +1 -1
  69. package/dist/ralph-fix-plan-watcher.d.ts +91 -0
  70. package/dist/ralph-fix-plan-watcher.d.ts.map +1 -0
  71. package/dist/ralph-fix-plan-watcher.js +326 -0
  72. package/dist/ralph-fix-plan-watcher.js.map +1 -0
  73. package/dist/ralph-loop.d.ts +14 -4
  74. package/dist/ralph-loop.d.ts.map +1 -1
  75. package/dist/ralph-loop.js +14 -4
  76. package/dist/ralph-loop.js.map +1 -1
  77. package/dist/ralph-plan-tracker.d.ts +201 -0
  78. package/dist/ralph-plan-tracker.d.ts.map +1 -0
  79. package/dist/ralph-plan-tracker.js +325 -0
  80. package/dist/ralph-plan-tracker.js.map +1 -0
  81. package/dist/ralph-stall-detector.d.ts +84 -0
  82. package/dist/ralph-stall-detector.d.ts.map +1 -0
  83. package/dist/ralph-stall-detector.js +139 -0
  84. package/dist/ralph-stall-detector.js.map +1 -0
  85. package/dist/ralph-status-parser.d.ts +141 -0
  86. package/dist/ralph-status-parser.d.ts.map +1 -0
  87. package/dist/ralph-status-parser.js +478 -0
  88. package/dist/ralph-status-parser.js.map +1 -0
  89. package/dist/ralph-tracker.d.ts +218 -692
  90. package/dist/ralph-tracker.d.ts.map +1 -1
  91. package/dist/ralph-tracker.js +389 -1723
  92. package/dist/ralph-tracker.js.map +1 -1
  93. package/dist/respawn-adaptive-timing.d.ts +61 -0
  94. package/dist/respawn-adaptive-timing.d.ts.map +1 -0
  95. package/dist/respawn-adaptive-timing.js +105 -0
  96. package/dist/respawn-adaptive-timing.js.map +1 -0
  97. package/dist/respawn-controller.d.ts +35 -115
  98. package/dist/respawn-controller.d.ts.map +1 -1
  99. package/dist/respawn-controller.js +167 -607
  100. package/dist/respawn-controller.js.map +1 -1
  101. package/dist/respawn-health.d.ts +54 -0
  102. package/dist/respawn-health.d.ts.map +1 -0
  103. package/dist/respawn-health.js +183 -0
  104. package/dist/respawn-health.js.map +1 -0
  105. package/dist/respawn-metrics.d.ts +81 -0
  106. package/dist/respawn-metrics.d.ts.map +1 -0
  107. package/dist/respawn-metrics.js +198 -0
  108. package/dist/respawn-metrics.js.map +1 -0
  109. package/dist/respawn-patterns.d.ts +45 -0
  110. package/dist/respawn-patterns.d.ts.map +1 -0
  111. package/dist/respawn-patterns.js +125 -0
  112. package/dist/respawn-patterns.js.map +1 -0
  113. package/dist/session-auto-ops.d.ts +89 -0
  114. package/dist/session-auto-ops.d.ts.map +1 -0
  115. package/dist/session-auto-ops.js +224 -0
  116. package/dist/session-auto-ops.js.map +1 -0
  117. package/dist/session-cli-builder.d.ts +62 -0
  118. package/dist/session-cli-builder.d.ts.map +1 -0
  119. package/dist/session-cli-builder.js +121 -0
  120. package/dist/session-cli-builder.js.map +1 -0
  121. package/dist/session-manager.d.ts +17 -5
  122. package/dist/session-manager.d.ts.map +1 -1
  123. package/dist/session-manager.js +17 -5
  124. package/dist/session-manager.js.map +1 -1
  125. package/dist/session-task-cache.d.ts +52 -0
  126. package/dist/session-task-cache.d.ts.map +1 -0
  127. package/dist/session-task-cache.js +90 -0
  128. package/dist/session-task-cache.js.map +1 -0
  129. package/dist/session.d.ts +23 -41
  130. package/dist/session.d.ts.map +1 -1
  131. package/dist/session.js +79 -317
  132. package/dist/session.js.map +1 -1
  133. package/dist/state-store.d.ts +19 -9
  134. package/dist/state-store.d.ts.map +1 -1
  135. package/dist/state-store.js +29 -30
  136. package/dist/state-store.js.map +1 -1
  137. package/dist/subagent-watcher.d.ts +26 -7
  138. package/dist/subagent-watcher.d.ts.map +1 -1
  139. package/dist/subagent-watcher.js +47 -64
  140. package/dist/subagent-watcher.js.map +1 -1
  141. package/dist/team-watcher.d.ts.map +1 -1
  142. package/dist/team-watcher.js +2 -5
  143. package/dist/team-watcher.js.map +1 -1
  144. package/dist/tmux-manager.d.ts.map +1 -1
  145. package/dist/tmux-manager.js +1 -2
  146. package/dist/tmux-manager.js.map +1 -1
  147. package/dist/tunnel-manager.d.ts +26 -0
  148. package/dist/tunnel-manager.d.ts.map +1 -1
  149. package/dist/tunnel-manager.js +126 -7
  150. package/dist/tunnel-manager.js.map +1 -1
  151. package/dist/types/api.d.ts +108 -0
  152. package/dist/types/api.d.ts.map +1 -0
  153. package/dist/types/api.js +98 -0
  154. package/dist/types/api.js.map +1 -0
  155. package/dist/types/app-state.d.ts +117 -0
  156. package/dist/types/app-state.d.ts.map +1 -0
  157. package/dist/types/app-state.js +76 -0
  158. package/dist/types/app-state.js.map +1 -0
  159. package/dist/types/common.d.ts +79 -0
  160. package/dist/types/common.d.ts.map +1 -0
  161. package/dist/types/common.js +17 -0
  162. package/dist/types/common.js.map +1 -0
  163. package/dist/types/index.d.ts +66 -0
  164. package/dist/types/index.d.ts.map +1 -0
  165. package/dist/types/index.js +66 -0
  166. package/dist/types/index.js.map +1 -0
  167. package/dist/types/lifecycle.d.ts +28 -0
  168. package/dist/types/lifecycle.d.ts.map +1 -0
  169. package/dist/types/lifecycle.js +16 -0
  170. package/dist/types/lifecycle.js.map +1 -0
  171. package/dist/types/plan.d.ts +45 -0
  172. package/dist/types/plan.d.ts.map +1 -0
  173. package/dist/types/plan.js +18 -0
  174. package/dist/types/plan.js.map +1 -0
  175. package/dist/types/push.d.ts +36 -0
  176. package/dist/types/push.d.ts.map +1 -0
  177. package/dist/types/push.js +18 -0
  178. package/dist/types/push.js.map +1 -0
  179. package/dist/types/ralph.d.ts +262 -0
  180. package/dist/types/ralph.d.ts.map +1 -0
  181. package/dist/types/ralph.js +70 -0
  182. package/dist/types/ralph.js.map +1 -0
  183. package/dist/types/respawn.d.ts +271 -0
  184. package/dist/types/respawn.d.ts.map +1 -0
  185. package/dist/types/respawn.js +26 -0
  186. package/dist/types/respawn.js.map +1 -0
  187. package/dist/types/run-summary.d.ts +96 -0
  188. package/dist/types/run-summary.d.ts.map +1 -0
  189. package/dist/types/run-summary.js +37 -0
  190. package/dist/types/run-summary.js.map +1 -0
  191. package/dist/types/session.d.ts +152 -0
  192. package/dist/types/session.d.ts.map +1 -0
  193. package/dist/types/session.js +27 -0
  194. package/dist/types/session.js.map +1 -0
  195. package/dist/types/task.d.ts +72 -0
  196. package/dist/types/task.d.ts.map +1 -0
  197. package/dist/types/task.js +19 -0
  198. package/dist/types/task.js.map +1 -0
  199. package/dist/types/teams.d.ts +73 -0
  200. package/dist/types/teams.d.ts.map +1 -0
  201. package/dist/types/teams.js +23 -0
  202. package/dist/types/teams.js.map +1 -0
  203. package/dist/types/tools.d.ts +61 -0
  204. package/dist/types/tools.d.ts.map +1 -0
  205. package/dist/types/tools.js +20 -0
  206. package/dist/types/tools.js.map +1 -0
  207. package/dist/types.d.ts +8 -1134
  208. package/dist/types.d.ts.map +1 -1
  209. package/dist/types.js +8 -210
  210. package/dist/types.js.map +1 -1
  211. package/dist/utils/claude-cli-resolver.d.ts.map +1 -1
  212. package/dist/utils/claude-cli-resolver.js +1 -2
  213. package/dist/utils/claude-cli-resolver.js.map +1 -1
  214. package/dist/utils/debouncer.d.ts +111 -0
  215. package/dist/utils/debouncer.d.ts.map +1 -0
  216. package/dist/utils/debouncer.js +162 -0
  217. package/dist/utils/debouncer.js.map +1 -0
  218. package/dist/utils/index.d.ts +3 -2
  219. package/dist/utils/index.d.ts.map +1 -1
  220. package/dist/utils/index.js +3 -2
  221. package/dist/utils/index.js.map +1 -1
  222. package/dist/utils/opencode-cli-resolver.d.ts.map +1 -1
  223. package/dist/utils/opencode-cli-resolver.js +1 -2
  224. package/dist/utils/opencode-cli-resolver.js.map +1 -1
  225. package/dist/utils/string-similarity.d.ts +0 -57
  226. package/dist/utils/string-similarity.d.ts.map +1 -1
  227. package/dist/utils/string-similarity.js +3 -18
  228. package/dist/utils/string-similarity.js.map +1 -1
  229. package/dist/web/middleware/auth.d.ts +31 -0
  230. package/dist/web/middleware/auth.d.ts.map +1 -0
  231. package/dist/web/middleware/auth.js +154 -0
  232. package/dist/web/middleware/auth.js.map +1 -0
  233. package/dist/web/ports/auth-port.d.ts +18 -0
  234. package/dist/web/ports/auth-port.d.ts.map +1 -0
  235. package/dist/web/ports/auth-port.js +6 -0
  236. package/dist/web/ports/auth-port.js.map +1 -0
  237. package/dist/web/ports/config-port.d.ts +28 -0
  238. package/dist/web/ports/config-port.d.ts.map +1 -0
  239. package/dist/web/ports/config-port.js +6 -0
  240. package/dist/web/ports/config-port.js.map +1 -0
  241. package/dist/web/ports/event-port.d.ts +13 -0
  242. package/dist/web/ports/event-port.d.ts.map +1 -0
  243. package/dist/web/ports/event-port.js +6 -0
  244. package/dist/web/ports/event-port.js.map +1 -0
  245. package/dist/web/ports/index.d.ts +14 -0
  246. package/dist/web/ports/index.d.ts.map +1 -0
  247. package/dist/web/ports/index.js +9 -0
  248. package/dist/web/ports/index.js.map +1 -0
  249. package/dist/web/ports/infra-port.d.ts +36 -0
  250. package/dist/web/ports/infra-port.d.ts.map +1 -0
  251. package/dist/web/ports/infra-port.js +6 -0
  252. package/dist/web/ports/infra-port.js.map +1 -0
  253. package/dist/web/ports/respawn-port.d.ts +20 -0
  254. package/dist/web/ports/respawn-port.d.ts.map +1 -0
  255. package/dist/web/ports/respawn-port.js +6 -0
  256. package/dist/web/ports/respawn-port.js.map +1 -0
  257. package/dist/web/ports/session-port.d.ts +15 -0
  258. package/dist/web/ports/session-port.d.ts.map +1 -0
  259. package/dist/web/ports/session-port.js +6 -0
  260. package/dist/web/ports/session-port.js.map +1 -0
  261. package/dist/web/public/api-client.js +82 -0
  262. package/dist/web/public/api-client.js.br +0 -0
  263. package/dist/web/public/api-client.js.gz +0 -0
  264. package/dist/web/public/app.js +117 -201
  265. package/dist/web/public/app.js.br +0 -0
  266. package/dist/web/public/app.js.gz +0 -0
  267. package/dist/web/public/constants.js +365 -0
  268. package/dist/web/public/constants.js.br +0 -0
  269. package/dist/web/public/constants.js.gz +0 -0
  270. package/dist/web/public/index.html +15 -3
  271. package/dist/web/public/index.html.br +0 -0
  272. package/dist/web/public/index.html.gz +0 -0
  273. package/dist/web/public/keyboard-accessory.js +302 -0
  274. package/dist/web/public/keyboard-accessory.js.br +0 -0
  275. package/dist/web/public/keyboard-accessory.js.gz +0 -0
  276. package/dist/web/public/mobile-handlers.js +491 -0
  277. package/dist/web/public/mobile-handlers.js.br +0 -0
  278. package/dist/web/public/mobile-handlers.js.gz +0 -0
  279. package/dist/web/public/mobile.css.gz +0 -0
  280. package/dist/web/public/notification-manager.js +472 -0
  281. package/dist/web/public/notification-manager.js.br +0 -0
  282. package/dist/web/public/notification-manager.js.gz +0 -0
  283. package/dist/web/public/ralph-wizard.js +33 -9
  284. package/dist/web/public/ralph-wizard.js.br +0 -0
  285. package/dist/web/public/ralph-wizard.js.gz +0 -0
  286. package/dist/web/public/styles.css.gz +0 -0
  287. package/dist/web/public/subagent-windows.js +1149 -0
  288. package/dist/web/public/subagent-windows.js.br +0 -0
  289. package/dist/web/public/subagent-windows.js.gz +0 -0
  290. package/dist/web/public/sw.js +15 -0
  291. package/dist/web/public/sw.js.br +0 -0
  292. package/dist/web/public/sw.js.gz +0 -0
  293. package/dist/web/public/upload.html.gz +0 -0
  294. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  295. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  296. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  297. package/dist/web/public/vendor/xterm-zerolag-input.js +4 -0
  298. package/dist/web/public/vendor/xterm-zerolag-input.js.br +0 -0
  299. package/dist/web/public/vendor/xterm-zerolag-input.js.gz +0 -0
  300. package/dist/web/public/vendor/xterm.css.gz +0 -0
  301. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  302. package/dist/web/public/voice-input.js +882 -0
  303. package/dist/web/public/voice-input.js.br +0 -0
  304. package/dist/web/public/voice-input.js.gz +0 -0
  305. package/dist/web/route-helpers.d.ts +38 -0
  306. package/dist/web/route-helpers.d.ts.map +1 -0
  307. package/dist/web/route-helpers.js +144 -0
  308. package/dist/web/route-helpers.js.map +1 -0
  309. package/dist/web/routes/case-routes.d.ts +9 -0
  310. package/dist/web/routes/case-routes.d.ts.map +1 -0
  311. package/dist/web/routes/case-routes.js +426 -0
  312. package/dist/web/routes/case-routes.js.map +1 -0
  313. package/dist/web/routes/file-routes.d.ts +8 -0
  314. package/dist/web/routes/file-routes.d.ts.map +1 -0
  315. package/dist/web/routes/file-routes.js +337 -0
  316. package/dist/web/routes/file-routes.js.map +1 -0
  317. package/dist/web/routes/hook-event-routes.d.ts +9 -0
  318. package/dist/web/routes/hook-event-routes.d.ts.map +1 -0
  319. package/dist/web/routes/hook-event-routes.js +57 -0
  320. package/dist/web/routes/hook-event-routes.js.map +1 -0
  321. package/dist/web/routes/index.d.ts +16 -0
  322. package/dist/web/routes/index.d.ts.map +1 -0
  323. package/dist/web/routes/index.js +16 -0
  324. package/dist/web/routes/index.js.map +1 -0
  325. package/dist/web/routes/mux-routes.d.ts +8 -0
  326. package/dist/web/routes/mux-routes.d.ts.map +1 -0
  327. package/dist/web/routes/mux-routes.js +32 -0
  328. package/dist/web/routes/mux-routes.js.map +1 -0
  329. package/dist/web/routes/plan-routes.d.ts +9 -0
  330. package/dist/web/routes/plan-routes.d.ts.map +1 -0
  331. package/dist/web/routes/plan-routes.js +385 -0
  332. package/dist/web/routes/plan-routes.js.map +1 -0
  333. package/dist/web/routes/push-routes.d.ts +8 -0
  334. package/dist/web/routes/push-routes.d.ts.map +1 -0
  335. package/dist/web/routes/push-routes.js +49 -0
  336. package/dist/web/routes/push-routes.js.map +1 -0
  337. package/dist/web/routes/ralph-routes.d.ts +9 -0
  338. package/dist/web/routes/ralph-routes.d.ts.map +1 -0
  339. package/dist/web/routes/ralph-routes.js +485 -0
  340. package/dist/web/routes/ralph-routes.js.map +1 -0
  341. package/dist/web/routes/respawn-routes.d.ts +8 -0
  342. package/dist/web/routes/respawn-routes.d.ts.map +1 -0
  343. package/dist/web/routes/respawn-routes.js +270 -0
  344. package/dist/web/routes/respawn-routes.js.map +1 -0
  345. package/dist/web/routes/scheduled-routes.d.ts +8 -0
  346. package/dist/web/routes/scheduled-routes.d.ts.map +1 -0
  347. package/dist/web/routes/scheduled-routes.js +51 -0
  348. package/dist/web/routes/scheduled-routes.js.map +1 -0
  349. package/dist/web/routes/session-routes.d.ts +9 -0
  350. package/dist/web/routes/session-routes.d.ts.map +1 -0
  351. package/dist/web/routes/session-routes.js +751 -0
  352. package/dist/web/routes/session-routes.js.map +1 -0
  353. package/dist/web/routes/system-routes.d.ts +9 -0
  354. package/dist/web/routes/system-routes.d.ts.map +1 -0
  355. package/dist/web/routes/system-routes.js +699 -0
  356. package/dist/web/routes/system-routes.js.map +1 -0
  357. package/dist/web/routes/team-routes.d.ts +8 -0
  358. package/dist/web/routes/team-routes.d.ts.map +1 -0
  359. package/dist/web/routes/team-routes.js +14 -0
  360. package/dist/web/routes/team-routes.js.map +1 -0
  361. package/dist/web/schemas.d.ts +43 -3
  362. package/dist/web/schemas.d.ts.map +1 -1
  363. package/dist/web/schemas.js +6 -2
  364. package/dist/web/schemas.js.map +1 -1
  365. package/dist/web/server.d.ts +35 -15
  366. package/dist/web/server.d.ts.map +1 -1
  367. package/dist/web/server.js +563 -3971
  368. package/dist/web/server.js.map +1 -1
  369. package/dist/web/sse-events.d.ts +361 -0
  370. package/dist/web/sse-events.d.ts.map +1 -0
  371. package/dist/web/sse-events.js +396 -0
  372. package/dist/web/sse-events.js.map +1 -0
  373. package/package.json +2 -1
  374. package/scripts/postinstall.js +58 -0
@@ -1,9 +1,9 @@
1
1
  /**
2
- * @fileoverview Respawn Controller for autonomous Claude Code session cycling
2
+ * @fileoverview Respawn Controller for autonomous Claude Code session cycling.
3
3
  *
4
- * The RespawnController manages automatic respawning of Claude Code sessions.
5
- * When Claude finishes working (detected by completion message + output silence),
6
- * it automatically cycles through update → clear → init steps to keep the session productive.
4
+ * Manages automatic respawning of Claude Code sessions. When Claude finishes
5
+ * working (detected by completion message + output silence), it cycles through
6
+ * update → clear → init steps to keep the session productive.
7
7
  *
8
8
  * ## State Machine
9
9
  *
@@ -17,19 +17,28 @@
17
17
  * └──────────────────────── SENDING_KICKSTART → WAITING_KICKSTART ──┘
18
18
  * ```
19
19
  *
20
- * ## Idle Detection (Updated for Claude Code 2024+)
20
+ * ## Idle Detection (multi-layer)
21
+ * - Layer 0: Stop hook / idle_prompt notification (definitive)
22
+ * - Layer 1: Completion message pattern "for Xm Xs"
23
+ * - Layer 2: AI idle check via `AiIdleChecker` (optional)
24
+ * - Layer 3: No-output timeout fallback
21
25
  *
22
- * Primary detection: Completion message pattern "for Xm Xs" (e.g., "✻ Worked for 2m 46s")
23
- * Confirmation: No new output for configurable duration (default 5s)
24
- * Fallback: No output at all for extended period (default 30s)
26
+ * Key exports:
27
+ * - `RespawnController` class state machine, extends EventEmitter
28
+ * - `RespawnConfig` interface all configuration options
29
+ * - `RespawnState` type — union of all state machine states
30
+ * - `DetectionStatus`, `ActiveTimerInfo`, `RespawnEvents` — status/event types
25
31
  *
26
- * ## Configuration
32
+ * Key methods: `start()`, `stop()`, `getStatus()`, `getConfig()`,
33
+ * `getDetectionStatus()`, `getActiveTimers()`, `getAggregateMetrics()`,
34
+ * `getTimingHistory()`, `getHealthScore()`
27
35
  *
28
- * - `sendClear`: Whether to send /clear after update (default: true)
29
- * - `sendInit`: Whether to send /init after clear (default: true)
30
- * - `kickstartPrompt`: Optional prompt if /init doesn't trigger work
31
- * - `completionConfirmMs`: Time to wait after completion message (default: 10000)
32
- * - `noOutputTimeoutMs`: Fallback timeout with no output at all (default: 30000)
36
+ * @dependencies session (PTY output), ai-idle-checker, ai-plan-checker,
37
+ * respawn-patterns, respawn-adaptive-timing, respawn-metrics, respawn-health,
38
+ * team-watcher (blocks respawn if teammates active)
39
+ * @consumedby web/server (respawn routes, SSE), ralph-loop
40
+ * @emits respawn:stateChanged, respawn:started, respawn:stopped, respawn:cycleStarted,
41
+ * respawn:cycleCompleted, respawn:detectionUpdate, respawn:aiCheck*, respawn:log
33
42
  *
34
43
  * @module respawn-controller
35
44
  */
@@ -38,18 +47,15 @@ import { randomUUID } from 'node:crypto';
38
47
  import { AiIdleChecker } from './ai-idle-checker.js';
39
48
  import { AiPlanChecker } from './ai-plan-checker.js';
40
49
  import { BufferAccumulator } from './utils/buffer-accumulator.js';
41
- import { ANSI_ESCAPE_PATTERN_SIMPLE, TOKEN_PATTERN, assertNever } from './utils/index.js';
50
+ import { ANSI_ESCAPE_PATTERN_SIMPLE, assertNever, CleanupManager } from './utils/index.js';
42
51
  import { MAX_RESPAWN_BUFFER_SIZE, TRIM_RESPAWN_BUFFER_TO as RESPAWN_BUFFER_TRIM_SIZE } from './config/buffer-limits.js';
52
+ import { isCompletionMessage, hasWorkingPattern, extractTokenCount, PROMPT_PATTERNS, WORKING_PATTERNS, } from './respawn-patterns.js';
53
+ import { RespawnAdaptiveTiming } from './respawn-adaptive-timing.js';
54
+ import { RespawnCycleMetricsTracker } from './respawn-metrics.js';
55
+ import { calculateHealthScore, shouldSkipClear } from './respawn-health.js';
56
+ import { AI_CHECK_MODEL, AI_IDLE_CHECK_MAX_CONTEXT, AI_PLAN_CHECK_MAX_CONTEXT } from './config/ai-defaults.js';
43
57
  // ========== Constants ==========
44
- /**
45
- * Pattern to detect completion messages from Claude Code.
46
- * Requires "Worked for" prefix to avoid false positives from bare time durations
47
- * in regular text (e.g., "wait for 5s", "run for 2m").
48
- *
49
- * Matches: "✻ Worked for 2m 46s", "Worked for 46s", "Worked for 1h 2m 3s"
50
- * Does NOT match: "wait for 5s", "run for 2m", "for 3s the system..."
51
- */
52
- const COMPLETION_TIME_PATTERN = /\bWorked\s+for\s+\d+[hms](\s*\d+[hms])*/i;
58
+ // COMPLETION_TIME_PATTERN moved to ./respawn-patterns.ts
53
59
  /** Pre-filter: numbered option pattern for plan mode detection */
54
60
  const PLAN_MODE_OPTION_PATTERN = /\d+\.\s+(Yes|No|Type|Cancel|Skip|Proceed|Approve|Reject)/i;
55
61
  /** Pre-filter: selection indicator arrow for plan mode detection */
@@ -67,13 +73,13 @@ const DEFAULT_CONFIG = {
67
73
  autoAcceptPrompts: true, // auto-accept plan mode prompts (not questions)
68
74
  autoAcceptDelayMs: 8000, // 8 seconds before auto-accepting
69
75
  aiIdleCheckEnabled: true, // use AI to confirm idle state
70
- aiIdleCheckModel: 'claude-opus-4-5-20251101',
71
- aiIdleCheckMaxContext: 16000, // ~4k tokens
76
+ aiIdleCheckModel: AI_CHECK_MODEL,
77
+ aiIdleCheckMaxContext: AI_IDLE_CHECK_MAX_CONTEXT,
72
78
  aiIdleCheckTimeoutMs: 90000, // 90 seconds (thinking can be slow)
73
79
  aiIdleCheckCooldownMs: 180000, // 3 minutes after WORKING verdict
74
80
  aiPlanCheckEnabled: true, // use AI to confirm plan mode before auto-accept
75
- aiPlanCheckModel: 'claude-opus-4-5-20251101',
76
- aiPlanCheckMaxContext: 8000, // ~2k tokens (plan mode UI is compact)
81
+ aiPlanCheckModel: AI_CHECK_MODEL,
82
+ aiPlanCheckMaxContext: AI_PLAN_CHECK_MAX_CONTEXT,
77
83
  aiPlanCheckTimeoutMs: 60000, // 60 seconds (thinking can be slow)
78
84
  aiPlanCheckCooldownMs: 30000, // 30 seconds after NOT_PLAN_MODE
79
85
  stuckStateDetectionEnabled: true, // detect stuck states
@@ -157,20 +163,12 @@ export class RespawnController extends EventEmitter {
157
163
  config;
158
164
  /** Current state machine state */
159
165
  _state = 'stopped';
160
- /** Timer for step delays */
161
- stepTimer = null;
162
- /** Timer for completion confirmation (Layer 2) */
163
- completionConfirmTimer = null;
164
- /** Timer for no-output fallback (Layer 5) */
165
- noOutputTimer = null;
166
- /** Timer for periodic detection status updates */
167
- detectionUpdateTimer = null;
166
+ /** Centralized timer lifecycle manager — disposed and recreated on clearTimers() */
167
+ cleanup = new CleanupManager();
168
+ /** Maps timer names to CleanupManager registration IDs (for individual cancel) */
169
+ timerIds = new Map();
168
170
  /** Cached key fields from last emitted detection status (for dedup) */
169
171
  lastEmittedDetectionKey = '';
170
- /** Timer for auto-accepting plan mode prompts */
171
- autoAcceptTimer = null;
172
- /** Timer for pre-filter silence detection (triggers AI check) */
173
- preFilterTimer = null;
174
172
  /** Whether any terminal output has been received since start/last-auto-accept */
175
173
  hasReceivedOutput = false;
176
174
  /** Whether an elicitation dialog (AskUserQuestion) was detected via hook signal */
@@ -184,8 +182,6 @@ export class RespawnController extends EventEmitter {
184
182
  idlePromptReceived = false;
185
183
  /** Timestamp when idle_prompt was received */
186
184
  idlePromptTime = null;
187
- /** Timer for short confirmation after hook signal (handles race conditions) */
188
- hookConfirmTimer = null;
189
185
  /** Confirmation delay after hook signal before confirming idle (ms) */
190
186
  static HOOK_CONFIRM_DELAY_MS = 3000;
191
187
  /** Number of completed respawn cycles */
@@ -208,10 +204,6 @@ export class RespawnController extends EventEmitter {
208
204
  planCheckStartTime = 0;
209
205
  /** Unique ID for current AI check request (to detect stale results) */
210
206
  _currentAiCheckId = null;
211
- /** Timer for /clear step fallback (sends /init if no prompt detected) */
212
- clearFallbackTimer = null;
213
- /** Timer for step completion confirmation (waits for silence after completion) */
214
- stepConfirmTimer = null;
215
207
  /** Fallback timeout for /clear step (ms) - sends /init without waiting for prompt */
216
208
  static CLEAR_FALLBACK_TIMEOUT_MS = 10000;
217
209
  // ========== Timer Tracking for UI Countdown Display ==========
@@ -222,44 +214,18 @@ export class RespawnController extends EventEmitter {
222
214
  // ========== Stuck-State Detection State ==========
223
215
  /** Timestamp when the current state was entered */
224
216
  stateEnteredAt = 0;
225
- /** Timer for stuck-state detection */
226
- stuckStateTimer = null;
227
217
  /** Whether a stuck-state warning has been emitted for current state */
228
218
  stuckStateWarned = false;
229
219
  /** Number of stuck-state recovery attempts */
230
220
  stuckRecoveryCount = 0;
231
- // ========== P2-001: Adaptive Timing State ==========
232
- /** Historical timing data for adaptive adjustments */
233
- timingHistory = {
234
- recentIdleDetectionMs: [],
235
- recentCycleDurationMs: [],
236
- adaptiveCompletionConfirmMs: 10000, // Start with default
237
- sampleCount: 0,
238
- maxSamples: 20, // Keep last 20 samples for rolling average
239
- lastUpdatedAt: Date.now(),
240
- };
241
- // ========== P2-004: Cycle Metrics State ==========
242
- /** Current cycle being tracked */
243
- currentCycleMetrics = null;
221
+ // ========== P2-001: Adaptive Timing (delegated to RespawnAdaptiveTiming) ==========
222
+ /** Adaptive timing controller */
223
+ adaptiveTiming;
224
+ // ========== P2-004: Cycle Metrics (delegated to RespawnCycleMetricsTracker) ==========
225
+ /** Cycle metrics tracker */
226
+ cycleMetrics = new RespawnCycleMetricsTracker();
244
227
  /** Timestamp when idle detection started for current cycle */
245
228
  idleDetectionStartTime = 0;
246
- /** Recent cycle metrics (rolling window for aggregate calculation) */
247
- recentCycleMetrics = [];
248
- /** Maximum number of cycle metrics to keep in memory */
249
- static MAX_CYCLE_METRICS_IN_MEMORY = 100;
250
- /** Aggregate metrics across all tracked cycles */
251
- aggregateMetrics = {
252
- totalCycles: 0,
253
- successfulCycles: 0,
254
- stuckRecoveryCycles: 0,
255
- blockedCycles: 0,
256
- errorCycles: 0,
257
- avgCycleDurationMs: 0,
258
- avgIdleDetectionMs: 0,
259
- p90CycleDurationMs: 0,
260
- successRate: 100,
261
- lastUpdatedAt: Date.now(),
262
- };
263
229
  // ========== Multi-Layer Detection State ==========
264
230
  /** Layer 1: Timestamp when completion message was detected */
265
231
  completionMessageTime = null;
@@ -271,71 +237,7 @@ export class RespawnController extends EventEmitter {
271
237
  lastTokenChangeTime = 0;
272
238
  /** Layer 4: Timestamp when last working pattern was seen */
273
239
  lastWorkingPatternTime = 0;
274
- /**
275
- * Patterns indicating Claude is ready for input (legacy fallback).
276
- * Used as secondary signals, not primary detection.
277
- */
278
- PROMPT_PATTERNS = [
279
- '❯', // Standard prompt
280
- '\u276f', // Unicode variant
281
- '⏵', // Claude Code prompt variant
282
- ];
283
- /**
284
- * Patterns indicating Claude is actively working.
285
- * When detected, resets all idle detection timers.
286
- * Note: ✻ and ✽ removed - they appear in completion messages too.
287
- */
288
- WORKING_PATTERNS = [
289
- 'Thinking',
290
- 'Writing',
291
- 'Reading',
292
- 'Running',
293
- 'Searching',
294
- 'Editing',
295
- 'Creating',
296
- 'Deleting',
297
- 'Analyzing',
298
- 'Executing',
299
- 'Synthesizing',
300
- 'Brewing', // Claude's processing indicators
301
- 'Compiling',
302
- 'Building',
303
- 'Installing',
304
- 'Fetching',
305
- 'Downloading',
306
- 'Processing',
307
- 'Generating',
308
- 'Loading',
309
- 'Starting',
310
- 'Updating',
311
- 'Checking',
312
- 'Validating',
313
- 'Testing',
314
- 'Formatting',
315
- 'Linting',
316
- '⠋',
317
- '⠙',
318
- '⠹',
319
- '⠸',
320
- '⠼',
321
- '⠴',
322
- '⠦',
323
- '⠧',
324
- '⠇',
325
- '⠏', // Spinner chars
326
- '◐',
327
- '◓',
328
- '◑',
329
- '◒', // Alternative spinners
330
- '⣾',
331
- '⣽',
332
- '⣻',
333
- '⢿',
334
- '⡿',
335
- '⣟',
336
- '⣯',
337
- '⣷', // Braille spinners
338
- ];
240
+ // PROMPT_PATTERNS and WORKING_PATTERNS are now imported from ./respawn-patterns.js
339
241
  /**
340
242
  * Rolling window buffer for working pattern detection.
341
243
  * Prevents split-chunk issues where "Thinking" arrives as "Thin" + "king".
@@ -362,6 +264,11 @@ export class RespawnController extends EventEmitter {
362
264
  this.config = { ...DEFAULT_CONFIG, ...filteredConfig };
363
265
  // Validate configuration values
364
266
  this.validateConfig();
267
+ // Initialize sub-modules
268
+ this.adaptiveTiming = new RespawnAdaptiveTiming({
269
+ adaptiveMinConfirmMs: this.config.adaptiveMinConfirmMs ?? 5000,
270
+ adaptiveMaxConfirmMs: this.config.adaptiveMaxConfirmMs ?? 30000,
271
+ });
365
272
  this.aiChecker = new AiIdleChecker(session.id, {
366
273
  enabled: this.config.aiIdleCheckEnabled,
367
274
  model: this.config.aiIdleCheckModel,
@@ -655,7 +562,7 @@ export class RespawnController extends EventEmitter {
655
562
  if (this._state === 'stopped')
656
563
  return;
657
564
  this.lastEmittedDetectionKey = '';
658
- this.detectionUpdateTimer = setInterval(() => {
565
+ const id = this.cleanup.setInterval(() => {
659
566
  try {
660
567
  if (this._state !== 'stopped') {
661
568
  const status = this.getDetectionStatus();
@@ -671,16 +578,14 @@ export class RespawnController extends EventEmitter {
671
578
  catch (err) {
672
579
  console.error(`[RespawnController] Error in detectionUpdateTimer:`, err);
673
580
  }
674
- }, 2000);
581
+ }, 2000, { description: 'detection-update' });
582
+ this.timerIds.set('detection-update', id);
675
583
  }
676
584
  /**
677
585
  * Stop periodic detection status updates.
678
586
  */
679
587
  stopDetectionUpdates() {
680
- if (this.detectionUpdateTimer) {
681
- clearInterval(this.detectionUpdateTimer);
682
- this.detectionUpdateTimer = null;
683
- }
588
+ this.cancelTrackedTimer('detection-update');
684
589
  }
685
590
  /**
686
591
  * Transition to a new state.
@@ -791,7 +696,6 @@ export class RespawnController extends EventEmitter {
791
696
  this.aiChecker.removeAllListeners();
792
697
  this.planChecker.removeAllListeners();
793
698
  this.clearTimers();
794
- this.stopDetectionUpdates();
795
699
  this.recentActions.length = 0;
796
700
  this.setState('stopped');
797
701
  if (this.terminalHandler) {
@@ -880,7 +784,7 @@ export class RespawnController extends EventEmitter {
880
784
  this.planChecker.cancel();
881
785
  }
882
786
  // Track token count (Layer 3)
883
- const tokenCount = this.extractTokenCount(data);
787
+ const tokenCount = extractTokenCount(data);
884
788
  if (tokenCount !== null && tokenCount !== this.lastTokenCount) {
885
789
  this.lastTokenCount = tokenCount;
886
790
  this.lastTokenChangeTime = now;
@@ -888,7 +792,7 @@ export class RespawnController extends EventEmitter {
888
792
  // Detect completion message FIRST (Layer 1) - PRIMARY DETECTION
889
793
  // Check this before working patterns because completion message indicates
890
794
  // the work is done, even if working patterns are still in the rolling window
891
- if (this.isCompletionMessage(data)) {
795
+ if (isCompletionMessage(data)) {
892
796
  // Clear the rolling window - completion marks a transition point
893
797
  this.clearWorkingPatternWindow();
894
798
  this.workingDetected = false;
@@ -933,7 +837,7 @@ export class RespawnController extends EventEmitter {
933
837
  return;
934
838
  }
935
839
  // Detect working patterns (Layer 4)
936
- const isWorking = this.hasWorkingPattern(data);
840
+ const isWorking = this.checkWorkingPattern(data);
937
841
  if (isWorking) {
938
842
  this.workingDetected = true;
939
843
  this.promptDetected = false;
@@ -941,8 +845,7 @@ export class RespawnController extends EventEmitter {
941
845
  this.resetHookState(); // Clear hook signals on new work
942
846
  this.lastWorkingPatternTime = now;
943
847
  // Cancel hook confirmation timer if running
944
- this.cancelTrackedTimer('hook-confirm', this.hookConfirmTimer, 'working patterns detected');
945
- this.hookConfirmTimer = null;
848
+ this.cancelTrackedTimer('hook-confirm', 'working patterns detected');
946
849
  // Cancel any pending completion confirmation
947
850
  this.cancelCompletionConfirm();
948
851
  // Cancel any pending step confirmation (Claude is still working)
@@ -987,7 +890,7 @@ export class RespawnController extends EventEmitter {
987
890
  }
988
891
  }
989
892
  // Legacy fallback: detect prompt characters (still useful for waiting_* states)
990
- const hasPrompt = this.PROMPT_PATTERNS.some((pattern) => data.includes(pattern));
893
+ const hasPrompt = PROMPT_PATTERNS.some((pattern) => data.includes(pattern));
991
894
  if (hasPrompt) {
992
895
  this.promptDetected = true;
993
896
  this.workingDetected = false;
@@ -1037,10 +940,8 @@ export class RespawnController extends EventEmitter {
1037
940
  this.recordCycleStep('update');
1038
941
  if (this.config.sendClear) {
1039
942
  // P2-002: Check if we should skip /clear
1040
- if (this.shouldSkipClear()) {
1041
- if (this.currentCycleMetrics) {
1042
- this.currentCycleMetrics.clearSkipped = true;
1043
- }
943
+ if (this.checkShouldSkipClear()) {
944
+ this.cycleMetrics.markClearSkipped();
1044
945
  // Skip /clear, go directly to /init or complete
1045
946
  if (this.config.sendInit) {
1046
947
  this.sendInit();
@@ -1067,8 +968,7 @@ export class RespawnController extends EventEmitter {
1067
968
  */
1068
969
  checkClearComplete() {
1069
970
  // Clear the fallback timer since we got prompt detection
1070
- this.cancelTrackedTimer('clear-fallback', this.clearFallbackTimer, 'prompt detected');
1071
- this.clearFallbackTimer = null;
971
+ this.cancelTrackedTimer('clear-fallback', 'prompt detected');
1072
972
  this.logAction('step', '/clear completed');
1073
973
  this.emit('stepCompleted', 'clear');
1074
974
  // P2-004: Record step completion
@@ -1111,8 +1011,7 @@ export class RespawnController extends EventEmitter {
1111
1011
  this.workingDetected = false;
1112
1012
  this.logAction('step', 'Monitoring if /init triggered work...');
1113
1013
  // Give Claude a moment to start working before checking for idle
1114
- this.stepTimer = this.startTrackedTimer('init-monitor', 3000, () => {
1115
- this.stepTimer = null;
1014
+ this.startTrackedTimer('init-monitor', 3000, () => {
1116
1015
  // If still in monitoring state and no work detected, consider it idle
1117
1016
  if (this._state === 'monitoring_init' && !this.workingDetected) {
1118
1017
  this.checkMonitoringInitIdle();
@@ -1125,10 +1024,7 @@ export class RespawnController extends EventEmitter {
1125
1024
  * @fires stepCompleted - With step 'init'
1126
1025
  */
1127
1026
  checkMonitoringInitIdle() {
1128
- if (this.stepTimer) {
1129
- clearTimeout(this.stepTimer);
1130
- this.stepTimer = null;
1131
- }
1027
+ this.cancelTrackedTimer('init-monitor');
1132
1028
  this.log('/init did not trigger work, sending kickstart prompt');
1133
1029
  this.emit('stepCompleted', 'init');
1134
1030
  this.sendKickstart();
@@ -1141,8 +1037,7 @@ export class RespawnController extends EventEmitter {
1141
1037
  this.setState('sending_kickstart');
1142
1038
  this.terminalBuffer.clear();
1143
1039
  this.clearWorkingPatternWindow();
1144
- this.stepTimer = this.startTrackedTimer('step-delay', this.config.interStepDelayMs, async () => {
1145
- this.stepTimer = null;
1040
+ this.startTrackedTimer('step-delay', this.config.interStepDelayMs, async () => {
1146
1041
  if (this._state === 'stopped')
1147
1042
  return;
1148
1043
  const prompt = this.config.kickstartPrompt;
@@ -1167,48 +1062,10 @@ export class RespawnController extends EventEmitter {
1167
1062
  }
1168
1063
  /** Clear all timers (step, completion confirm, no-output, pre-filter, step confirm, auto-accept, hook confirm, and clear fallback) */
1169
1064
  clearTimers() {
1170
- // Clear tracked timers map first to avoid stale entries during individual cleanup
1171
1065
  this.activeTimers.clear();
1172
- if (this.stepTimer) {
1173
- clearTimeout(this.stepTimer);
1174
- this.stepTimer = null;
1175
- }
1176
- if (this.clearFallbackTimer) {
1177
- clearTimeout(this.clearFallbackTimer);
1178
- this.clearFallbackTimer = null;
1179
- }
1180
- if (this.completionConfirmTimer) {
1181
- clearTimeout(this.completionConfirmTimer);
1182
- this.completionConfirmTimer = null;
1183
- }
1184
- if (this.stepConfirmTimer) {
1185
- clearTimeout(this.stepConfirmTimer);
1186
- this.stepConfirmTimer = null;
1187
- }
1188
- if (this.autoAcceptTimer) {
1189
- clearTimeout(this.autoAcceptTimer);
1190
- this.autoAcceptTimer = null;
1191
- }
1192
- if (this.preFilterTimer) {
1193
- clearTimeout(this.preFilterTimer);
1194
- this.preFilterTimer = null;
1195
- }
1196
- if (this.noOutputTimer) {
1197
- clearTimeout(this.noOutputTimer);
1198
- this.noOutputTimer = null;
1199
- }
1200
- if (this.hookConfirmTimer) {
1201
- clearTimeout(this.hookConfirmTimer);
1202
- this.hookConfirmTimer = null;
1203
- }
1204
- if (this.stuckStateTimer) {
1205
- clearInterval(this.stuckStateTimer);
1206
- this.stuckStateTimer = null;
1207
- }
1208
- if (this.detectionUpdateTimer) {
1209
- clearInterval(this.detectionUpdateTimer);
1210
- this.detectionUpdateTimer = null;
1211
- }
1066
+ this.timerIds.clear();
1067
+ this.cleanup.dispose();
1068
+ this.cleanup = new CleanupManager();
1212
1069
  }
1213
1070
  // ========== Stuck-State Detection Methods ==========
1214
1071
  /**
@@ -1221,20 +1078,18 @@ export class RespawnController extends EventEmitter {
1221
1078
  if (this._state === 'stopped')
1222
1079
  return;
1223
1080
  // Clear existing timer
1224
- if (this.stuckStateTimer) {
1225
- clearInterval(this.stuckStateTimer);
1226
- this.stuckStateTimer = null;
1227
- }
1081
+ this.cancelTrackedTimer('stuck-state');
1228
1082
  // Check interval for stuck state
1229
1083
  const checkIntervalMs = Math.min(this.config.stuckStateWarningMs, 60000); // Check every minute max
1230
- this.stuckStateTimer = setInterval(() => {
1084
+ const id = this.cleanup.setInterval(() => {
1231
1085
  try {
1232
1086
  this.checkStuckState();
1233
1087
  }
1234
1088
  catch (err) {
1235
1089
  console.error(`[RespawnController] Error in stuckStateTimer:`, err);
1236
1090
  }
1237
- }, checkIntervalMs);
1091
+ }, checkIntervalMs, { description: 'stuck-state' });
1092
+ this.timerIds.set('stuck-state', id);
1238
1093
  }
1239
1094
  /**
1240
1095
  * Check if the controller is stuck in the current state.
@@ -1274,7 +1129,7 @@ export class RespawnController extends EventEmitter {
1274
1129
  handleStuckStateRecovery() {
1275
1130
  const currentState = this._state;
1276
1131
  // P2-004: Complete current cycle metrics with stuck_recovery outcome
1277
- if (this.currentCycleMetrics) {
1132
+ if (this.cycleMetrics.getCurrentCycle()) {
1278
1133
  this.completeCycleMetrics('stuck_recovery', `Stuck in state: ${currentState}`);
1279
1134
  }
1280
1135
  // Cancel any running AI checks
@@ -1351,23 +1206,29 @@ export class RespawnController extends EventEmitter {
1351
1206
  * Emits timerStarted event and tracks the timer for UI display.
1352
1207
  */
1353
1208
  startTrackedTimer(name, durationMs, callback, reason) {
1209
+ // Cancel any existing timer with this name
1210
+ this.cancelTrackedTimer(name);
1354
1211
  const now = Date.now();
1355
1212
  const endsAt = now + durationMs;
1356
1213
  this.activeTimers.set(name, { name, startedAt: now, durationMs, endsAt });
1357
1214
  this.emit('timerStarted', { name, durationMs, endsAt, reason });
1358
1215
  this.logAction('timer', `Started ${name}: ${Math.round(durationMs / 1000)}s${reason ? ` (${reason})` : ''}`);
1359
- return setTimeout(() => {
1216
+ const id = this.cleanup.setTimeout(() => {
1217
+ this.timerIds.delete(name);
1360
1218
  this.activeTimers.delete(name);
1361
1219
  this.emit('timerCompleted', name);
1362
1220
  callback();
1363
- }, durationMs);
1221
+ }, durationMs, { description: name });
1222
+ this.timerIds.set(name, id);
1364
1223
  }
1365
1224
  /**
1366
1225
  * Cancel a tracked timer and emit cancellation event.
1367
1226
  */
1368
- cancelTrackedTimer(name, timerRef, reason) {
1369
- if (timerRef) {
1370
- clearTimeout(timerRef);
1227
+ cancelTrackedTimer(name, reason) {
1228
+ const id = this.timerIds.get(name);
1229
+ if (id) {
1230
+ this.cleanup.unregister(id);
1231
+ this.timerIds.delete(name);
1371
1232
  if (this.activeTimers.has(name)) {
1372
1233
  this.activeTimers.delete(name);
1373
1234
  this.emit('timerCancelled', name, reason);
@@ -1405,25 +1266,19 @@ export class RespawnController extends EventEmitter {
1405
1266
  return [...this.recentActions];
1406
1267
  }
1407
1268
  // ========== Multi-Layer Detection Methods ==========
1269
+ // Pattern detection delegated to ./respawn-patterns.js (isCompletionMessage, hasWorkingPattern, extractTokenCount)
1408
1270
  /**
1409
- * Check if data contains a completion message pattern.
1410
- * Matches "for Xh Xm Xs" time duration patterns.
1411
- */
1412
- isCompletionMessage(data) {
1413
- return COMPLETION_TIME_PATTERN.test(data);
1414
- }
1415
- /**
1416
- * Check if data contains working patterns.
1417
- * Uses rolling window to catch patterns split across chunks (e.g., "Thin" + "king").
1271
+ * Check if data contains working patterns using the rolling window.
1272
+ * Updates the window and delegates to the pure function from respawn-patterns.
1418
1273
  */
1419
- hasWorkingPattern(data) {
1274
+ checkWorkingPattern(data) {
1420
1275
  // Always update the rolling window first to maintain continuity
1421
1276
  this.workingPatternWindow += data;
1422
1277
  if (this.workingPatternWindow.length > RespawnController.WORKING_PATTERN_WINDOW_SIZE) {
1423
1278
  this.workingPatternWindow = this.workingPatternWindow.slice(-RespawnController.WORKING_PATTERN_WINDOW_SIZE);
1424
1279
  }
1425
- // Check the rolling window (includes current data, catches both complete and split patterns)
1426
- return this.WORKING_PATTERNS.some((pattern) => this.workingPatternWindow.includes(pattern));
1280
+ // Delegate to pure function
1281
+ return hasWorkingPattern(this.workingPatternWindow);
1427
1282
  }
1428
1283
  /**
1429
1284
  * Clear the working pattern rolling window.
@@ -1432,32 +1287,14 @@ export class RespawnController extends EventEmitter {
1432
1287
  clearWorkingPatternWindow() {
1433
1288
  this.workingPatternWindow = '';
1434
1289
  }
1435
- /**
1436
- * Extract token count from data if present.
1437
- * Returns null if no token pattern found.
1438
- */
1439
- extractTokenCount(data) {
1440
- const match = data.match(TOKEN_PATTERN);
1441
- if (!match)
1442
- return null;
1443
- let count = parseFloat(match[1]);
1444
- const suffix = match[2]?.toLowerCase();
1445
- if (suffix === 'k')
1446
- count *= 1000;
1447
- else if (suffix === 'm')
1448
- count *= 1000000;
1449
- return Math.round(count);
1450
- }
1451
1290
  /**
1452
1291
  * Start the no-output fallback timer.
1453
1292
  * If no output for noOutputTimeoutMs, triggers idle detection as safety net
1454
1293
  * (used when AI check is disabled or has too many errors).
1455
1294
  */
1456
1295
  startNoOutputTimer() {
1457
- this.cancelTrackedTimer('no-output-fallback', this.noOutputTimer, 'restarting');
1458
- this.noOutputTimer = null;
1459
- this.noOutputTimer = this.startTrackedTimer('no-output-fallback', this.config.noOutputTimeoutMs, () => {
1460
- this.noOutputTimer = null;
1296
+ this.cancelTrackedTimer('no-output-fallback', 'restarting');
1297
+ this.startTrackedTimer('no-output-fallback', this.config.noOutputTimeoutMs, () => {
1461
1298
  if (this._state === 'watching' || this._state === 'confirming_idle') {
1462
1299
  const msSinceOutput = Date.now() - this.lastOutputTime;
1463
1300
  this.logAction('detection', `No-output fallback: ${Math.round(msSinceOutput / 1000)}s silence`);
@@ -1486,13 +1323,11 @@ export class RespawnController extends EventEmitter {
1486
1323
  * This provides an additional path to AI check even without a completion message.
1487
1324
  */
1488
1325
  startPreFilterTimer() {
1489
- this.cancelTrackedTimer('pre-filter', this.preFilterTimer, 'restarting');
1490
- this.preFilterTimer = null;
1326
+ this.cancelTrackedTimer('pre-filter', 'restarting');
1491
1327
  // Only set up pre-filter when AI check is enabled
1492
1328
  if (!this.config.aiIdleCheckEnabled)
1493
1329
  return;
1494
- this.preFilterTimer = this.startTrackedTimer('pre-filter', this.config.completionConfirmMs, () => {
1495
- this.preFilterTimer = null;
1330
+ this.startTrackedTimer('pre-filter', this.config.completionConfirmMs, () => {
1496
1331
  if (this._state === 'watching') {
1497
1332
  const now = Date.now();
1498
1333
  const msSinceOutput = now - this.lastOutputTime;
@@ -1579,18 +1414,15 @@ export class RespawnController extends EventEmitter {
1579
1414
  }
1580
1415
  if (result.verdict === 'IDLE') {
1581
1416
  // Cancel any pending confirmation timers - AI has spoken
1582
- this.cancelTrackedTimer('completion-confirm', this.completionConfirmTimer, 'AI verdict: IDLE');
1583
- this.completionConfirmTimer = null;
1584
- this.cancelTrackedTimer('pre-filter', this.preFilterTimer, 'AI verdict: IDLE');
1585
- this.preFilterTimer = null;
1417
+ this.cancelTrackedTimer('completion-confirm', 'AI verdict: IDLE');
1418
+ this.cancelTrackedTimer('pre-filter', 'AI verdict: IDLE');
1586
1419
  this.logAction('ai-check', `Verdict: IDLE - ${result.reasoning}`);
1587
1420
  this.emit('aiCheckCompleted', result);
1588
1421
  this.onIdleConfirmed(`ai-check: idle (${result.reasoning})`);
1589
1422
  }
1590
1423
  else if (result.verdict === 'WORKING') {
1591
1424
  // Cancel timers and go to cooldown
1592
- this.cancelTrackedTimer('completion-confirm', this.completionConfirmTimer, 'AI verdict: WORKING');
1593
- this.completionConfirmTimer = null;
1425
+ this.cancelTrackedTimer('completion-confirm', 'AI verdict: WORKING');
1594
1426
  this.logAction('ai-check', `Verdict: WORKING - ${result.reasoning}`);
1595
1427
  this.emit('aiCheckCompleted', result);
1596
1428
  this.setState('watching');
@@ -1645,10 +1477,8 @@ export class RespawnController extends EventEmitter {
1645
1477
  * and no elicitation dialog was detected. Only handles plan mode approvals.
1646
1478
  */
1647
1479
  startAutoAcceptTimer() {
1648
- this.cancelTrackedTimer('auto-accept', this.autoAcceptTimer, 'restarting');
1649
- this.autoAcceptTimer = null;
1650
- this.autoAcceptTimer = this.startTrackedTimer('auto-accept', this.config.autoAcceptDelayMs, () => {
1651
- this.autoAcceptTimer = null;
1480
+ this.cancelTrackedTimer('auto-accept', 'restarting');
1481
+ this.startTrackedTimer('auto-accept', this.config.autoAcceptDelayMs, () => {
1652
1482
  this.tryAutoAccept();
1653
1483
  }, 'plan mode detection');
1654
1484
  }
@@ -1657,8 +1487,7 @@ export class RespawnController extends EventEmitter {
1657
1487
  * Called when a completion message is detected (normal idle flow handles it).
1658
1488
  */
1659
1489
  cancelAutoAcceptTimer() {
1660
- this.cancelTrackedTimer('auto-accept', this.autoAcceptTimer, 'cancelled');
1661
- this.autoAcceptTimer = null;
1490
+ this.cancelTrackedTimer('auto-accept', 'cancelled');
1662
1491
  }
1663
1492
  /**
1664
1493
  * Attempt to auto-accept a plan mode prompt by sending Enter.
@@ -1739,7 +1568,7 @@ export class RespawnController extends EventEmitter {
1739
1568
  // Working patterns before the selector are from earlier work and don't matter.
1740
1569
  const selectorIndex = stripped.lastIndexOf(selectorMatch[0]);
1741
1570
  const afterSelector = stripped.slice(selectorIndex + selectorMatch[0].length);
1742
- const hasWorking = this.WORKING_PATTERNS.some((pattern) => afterSelector.includes(pattern));
1571
+ const hasWorking = WORKING_PATTERNS.some((pattern) => afterSelector.includes(pattern));
1743
1572
  if (hasWorking)
1744
1573
  return false;
1745
1574
  return true;
@@ -1806,8 +1635,7 @@ export class RespawnController extends EventEmitter {
1806
1635
  this.aiChecker.cancel();
1807
1636
  }
1808
1637
  // Cancel completion confirmation - auto-accept takes precedence
1809
- this.cancelTrackedTimer('completion-confirm', this.completionConfirmTimer, 'auto-accept');
1810
- this.completionConfirmTimer = null;
1638
+ this.cancelTrackedTimer('completion-confirm', 'auto-accept');
1811
1639
  this.completionMessageTime = null;
1812
1640
  // Ensure we're in watching state (not confirming_idle or ai_checking)
1813
1641
  if (this._state !== 'watching') {
@@ -1854,11 +1682,9 @@ export class RespawnController extends EventEmitter {
1854
1682
  this.aiChecker.cancel();
1855
1683
  }
1856
1684
  // Cancel completion confirm timer - hook takes precedence
1857
- this.cancelTrackedTimer('completion-confirm', this.completionConfirmTimer, 'Stop hook received');
1858
- this.completionConfirmTimer = null;
1685
+ this.cancelTrackedTimer('completion-confirm', 'Stop hook received');
1859
1686
  // Cancel pre-filter timer - hook takes precedence
1860
- this.cancelTrackedTimer('pre-filter', this.preFilterTimer, 'Stop hook received');
1861
- this.preFilterTimer = null;
1687
+ this.cancelTrackedTimer('pre-filter', 'Stop hook received');
1862
1688
  // Start short confirmation timer to handle race conditions
1863
1689
  // (e.g., Stop hook arrives but Claude immediately starts new work)
1864
1690
  this.startHookConfirmTimer('stop');
@@ -1887,12 +1713,9 @@ export class RespawnController extends EventEmitter {
1887
1713
  this.aiChecker.cancel();
1888
1714
  }
1889
1715
  // Cancel all other detection timers - this is definitive
1890
- this.cancelTrackedTimer('completion-confirm', this.completionConfirmTimer, 'idle_prompt received');
1891
- this.completionConfirmTimer = null;
1892
- this.cancelTrackedTimer('pre-filter', this.preFilterTimer, 'idle_prompt received');
1893
- this.preFilterTimer = null;
1894
- this.cancelTrackedTimer('no-output-fallback', this.noOutputTimer, 'idle_prompt received');
1895
- this.noOutputTimer = null;
1716
+ this.cancelTrackedTimer('completion-confirm', 'idle_prompt received');
1717
+ this.cancelTrackedTimer('pre-filter', 'idle_prompt received');
1718
+ this.cancelTrackedTimer('no-output-fallback', 'idle_prompt received');
1896
1719
  // idle_prompt is an even stronger signal than Stop hook (60s+ idle)
1897
1720
  // Skip confirmation and go directly to idle
1898
1721
  this.onIdleConfirmed('idle_prompt hook (60s+ idle)');
@@ -1904,10 +1727,8 @@ export class RespawnController extends EventEmitter {
1904
1727
  * @param hookType - Which hook triggered this ('stop' or 'idle_prompt')
1905
1728
  */
1906
1729
  startHookConfirmTimer(hookType) {
1907
- this.cancelTrackedTimer('hook-confirm', this.hookConfirmTimer, 'restarting');
1908
- this.hookConfirmTimer = null;
1909
- this.hookConfirmTimer = this.startTrackedTimer('hook-confirm', RespawnController.HOOK_CONFIRM_DELAY_MS, () => {
1910
- this.hookConfirmTimer = null;
1730
+ this.cancelTrackedTimer('hook-confirm', 'restarting');
1731
+ this.startTrackedTimer('hook-confirm', RespawnController.HOOK_CONFIRM_DELAY_MS, () => {
1911
1732
  // Verify we haven't received new output since the hook arrived
1912
1733
  const hookTime = hookType === 'stop' ? this.stopHookTime : this.idlePromptTime;
1913
1734
  if (hookTime && this.lastOutputTime > hookTime) {
@@ -1973,12 +1794,10 @@ export class RespawnController extends EventEmitter {
1973
1794
  * After completion message, waits for output silence then triggers AI check.
1974
1795
  */
1975
1796
  startCompletionConfirmTimer() {
1976
- this.cancelTrackedTimer('completion-confirm', this.completionConfirmTimer, 'restarting');
1977
- this.completionConfirmTimer = null;
1797
+ this.cancelTrackedTimer('completion-confirm', 'restarting');
1978
1798
  this.setState('confirming_idle');
1979
1799
  this.logAction('detection', 'Completion message found in output');
1980
- this.completionConfirmTimer = this.startTrackedTimer('completion-confirm', this.config.completionConfirmMs, () => {
1981
- this.completionConfirmTimer = null;
1800
+ this.startTrackedTimer('completion-confirm', this.config.completionConfirmMs, () => {
1982
1801
  if (this._state === 'stopped')
1983
1802
  return;
1984
1803
  const msSinceOutput = Date.now() - this.lastOutputTime;
@@ -1999,8 +1818,7 @@ export class RespawnController extends EventEmitter {
1999
1818
  * Cancel completion confirmation if new activity detected.
2000
1819
  */
2001
1820
  cancelCompletionConfirm() {
2002
- this.cancelTrackedTimer('completion-confirm', this.completionConfirmTimer, 'activity detected');
2003
- this.completionConfirmTimer = null;
1821
+ this.cancelTrackedTimer('completion-confirm', 'activity detected');
2004
1822
  if (this._state === 'confirming_idle') {
2005
1823
  this.setState('watching');
2006
1824
  this.completionMessageTime = null;
@@ -2012,10 +1830,8 @@ export class RespawnController extends EventEmitter {
2012
1830
  * This ensures Claude has finished processing before we send the next command.
2013
1831
  */
2014
1832
  startStepConfirmTimer(step) {
2015
- this.cancelTrackedTimer('step-confirm', this.stepConfirmTimer, 'restarting');
2016
- this.stepConfirmTimer = null;
2017
- this.stepConfirmTimer = this.startTrackedTimer('step-confirm', this.config.completionConfirmMs, () => {
2018
- this.stepConfirmTimer = null;
1833
+ this.cancelTrackedTimer('step-confirm', 'restarting');
1834
+ this.startTrackedTimer('step-confirm', this.config.completionConfirmMs, () => {
2019
1835
  if (this._state === 'stopped')
2020
1836
  return;
2021
1837
  const msSinceOutput = Date.now() - this.lastOutputTime;
@@ -2045,8 +1861,7 @@ export class RespawnController extends EventEmitter {
2045
1861
  * Cancel step confirmation if working patterns detected.
2046
1862
  */
2047
1863
  cancelStepConfirm() {
2048
- this.cancelTrackedTimer('step-confirm', this.stepConfirmTimer, 'working detected');
2049
- this.stepConfirmTimer = null;
1864
+ this.cancelTrackedTimer('step-confirm', 'working detected');
2050
1865
  }
2051
1866
  /**
2052
1867
  * Called when idle is confirmed through any detection layer.
@@ -2188,8 +2003,7 @@ export class RespawnController extends EventEmitter {
2188
2003
  this.setState('sending_update');
2189
2004
  this.terminalBuffer.clear(); // Clear buffer for fresh detection
2190
2005
  this.clearWorkingPatternWindow(); // Clear rolling window
2191
- this.stepTimer = this.startTrackedTimer('step-delay', this.config.interStepDelayMs, async () => {
2192
- this.stepTimer = null;
2006
+ this.startTrackedTimer('step-delay', this.config.interStepDelayMs, async () => {
2193
2007
  if (this._state === 'stopped')
2194
2008
  return;
2195
2009
  // Use RALPH_STATUS RECOMMENDATION if available, otherwise fall back to config
@@ -2220,8 +2034,7 @@ export class RespawnController extends EventEmitter {
2220
2034
  this.setState('sending_clear');
2221
2035
  this.terminalBuffer.clear();
2222
2036
  this.clearWorkingPatternWindow();
2223
- this.stepTimer = this.startTrackedTimer('step-delay', this.config.interStepDelayMs, async () => {
2224
- this.stepTimer = null;
2037
+ this.startTrackedTimer('step-delay', this.config.interStepDelayMs, async () => {
2225
2038
  if (this._state === 'stopped')
2226
2039
  return;
2227
2040
  this.logAction('command', 'Sending: /clear');
@@ -2230,8 +2043,7 @@ export class RespawnController extends EventEmitter {
2230
2043
  this.setState('waiting_clear');
2231
2044
  this.promptDetected = false;
2232
2045
  // Start fallback timer - if no prompt detected after 10s, proceed to /init anyway
2233
- this.clearFallbackTimer = this.startTrackedTimer('clear-fallback', RespawnController.CLEAR_FALLBACK_TIMEOUT_MS, () => {
2234
- this.clearFallbackTimer = null;
2046
+ this.startTrackedTimer('clear-fallback', RespawnController.CLEAR_FALLBACK_TIMEOUT_MS, () => {
2235
2047
  if (this._state === 'waiting_clear') {
2236
2048
  this.logAction('step', '/clear fallback: proceeding to /init');
2237
2049
  this.emit('stepCompleted', 'clear');
@@ -2253,8 +2065,7 @@ export class RespawnController extends EventEmitter {
2253
2065
  this.setState('sending_init');
2254
2066
  this.terminalBuffer.clear();
2255
2067
  this.clearWorkingPatternWindow();
2256
- this.stepTimer = this.startTrackedTimer('step-delay', this.config.interStepDelayMs, async () => {
2257
- this.stepTimer = null;
2068
+ this.startTrackedTimer('step-delay', this.config.interStepDelayMs, async () => {
2258
2069
  if (this._state === 'stopped')
2259
2070
  return;
2260
2071
  this.logAction('command', 'Sending: /init');
@@ -2381,7 +2192,7 @@ export class RespawnController extends EventEmitter {
2381
2192
  config: this.config,
2382
2193
  };
2383
2194
  }
2384
- // ========== P2-001: Adaptive Timing Methods ==========
2195
+ // ========== P2-001: Adaptive Timing (delegated to RespawnAdaptiveTiming) ==========
2385
2196
  /**
2386
2197
  * Get the current completion confirm timeout, potentially adjusted by adaptive timing.
2387
2198
  * Uses historical idle detection durations to calculate an optimal timeout.
@@ -2393,90 +2204,40 @@ export class RespawnController extends EventEmitter {
2393
2204
  return this.config.completionConfirmMs ?? 10000;
2394
2205
  }
2395
2206
  // Need at least 5 samples before adjusting
2396
- if (this.timingHistory.sampleCount < 5) {
2207
+ const history = this.adaptiveTiming.getTimingHistory();
2208
+ if (history.sampleCount < 5) {
2397
2209
  return this.config.completionConfirmMs ?? 10000;
2398
2210
  }
2399
- return this.timingHistory.adaptiveCompletionConfirmMs;
2400
- }
2401
- /**
2402
- * Record timing data from a completed cycle for adaptive adjustments.
2403
- *
2404
- * @param idleDetectionMs - Time spent detecting idle
2405
- * @param cycleDurationMs - Total cycle duration
2406
- */
2407
- recordTimingData(idleDetectionMs, cycleDurationMs) {
2408
- if (!this.config.adaptiveTimingEnabled)
2409
- return;
2410
- const history = this.timingHistory;
2411
- // Add to rolling windows
2412
- history.recentIdleDetectionMs.push(idleDetectionMs);
2413
- history.recentCycleDurationMs.push(cycleDurationMs);
2414
- // Trim to max samples
2415
- if (history.recentIdleDetectionMs.length > history.maxSamples) {
2416
- history.recentIdleDetectionMs.shift();
2417
- }
2418
- if (history.recentCycleDurationMs.length > history.maxSamples) {
2419
- history.recentCycleDurationMs.shift();
2420
- }
2421
- history.sampleCount = history.recentIdleDetectionMs.length;
2422
- history.lastUpdatedAt = Date.now();
2423
- // Recalculate adaptive timing
2424
- this.updateAdaptiveTiming();
2425
- }
2426
- /**
2427
- * Recalculate the adaptive completion confirm timeout based on historical data.
2428
- * Uses the 75th percentile of recent idle detection times as the new timeout,
2429
- * with a 20% buffer for safety.
2430
- */
2431
- updateAdaptiveTiming() {
2432
- const history = this.timingHistory;
2433
- const minMs = this.config.adaptiveMinConfirmMs ?? 5000;
2434
- const maxMs = this.config.adaptiveMaxConfirmMs ?? 30000;
2435
- if (history.recentIdleDetectionMs.length < 5)
2436
- return;
2437
- // Sort for percentile calculation
2438
- const sorted = [...history.recentIdleDetectionMs].sort((a, b) => a - b);
2439
- // Use 75th percentile with 20% buffer
2440
- const p75Index = Math.floor(sorted.length * 0.75);
2441
- const p75Value = sorted[p75Index];
2442
- const withBuffer = Math.round(p75Value * 1.2);
2443
- // Clamp to configured bounds
2444
- const clamped = Math.max(minMs, Math.min(maxMs, withBuffer));
2445
- history.adaptiveCompletionConfirmMs = clamped;
2446
- this.log(`Adaptive timing updated: ${clamped}ms (p75=${p75Value}ms, samples=${sorted.length})`);
2211
+ return this.adaptiveTiming.getAdaptiveCompletionConfirmMs();
2447
2212
  }
2448
2213
  /**
2449
2214
  * Get the current timing history for monitoring.
2450
2215
  * @returns Copy of timing history
2451
2216
  */
2452
2217
  getTimingHistory() {
2453
- return { ...this.timingHistory };
2218
+ return this.adaptiveTiming.getTimingHistory();
2454
2219
  }
2455
- // ========== P2-002: Skip-Clear Optimization Methods ==========
2220
+ // ========== P2-002: Skip-Clear Optimization (delegated to respawn-health.ts) ==========
2456
2221
  /**
2457
2222
  * Determine whether to skip the /clear step based on current context usage.
2458
2223
  * Skips if token count is below the configured threshold percentage.
2459
2224
  *
2460
2225
  * @returns True if /clear should be skipped
2461
2226
  */
2462
- shouldSkipClear() {
2227
+ checkShouldSkipClear() {
2463
2228
  if (!this.config.skipClearWhenLowContext)
2464
2229
  return false;
2465
2230
  const thresholdPercent = this.config.skipClearThresholdPercent ?? 30;
2466
2231
  const maxContext = 200000; // Approximate max context for Claude
2467
- // Use the session's token count if available
2468
- const currentTokens = this.lastTokenCount;
2469
- if (currentTokens === 0)
2470
- return false; // Can't determine, don't skip
2471
- const usagePercent = (currentTokens / maxContext) * 100;
2472
- if (usagePercent < thresholdPercent) {
2473
- this.log(`Skip-clear optimization: ${usagePercent.toFixed(1)}% < ${thresholdPercent}% threshold`);
2474
- this.logAction('optimization', `Skipping /clear (${usagePercent.toFixed(1)}% context used)`);
2475
- return true;
2232
+ const skip = shouldSkipClear(this.lastTokenCount, thresholdPercent, maxContext);
2233
+ if (skip) {
2234
+ const usagePercent = ((this.lastTokenCount / maxContext) * 100).toFixed(1);
2235
+ this.log(`Skip-clear optimization: ${usagePercent}% < ${thresholdPercent}% threshold`);
2236
+ this.logAction('optimization', `Skipping /clear (${usagePercent}% context used)`);
2476
2237
  }
2477
- return false;
2238
+ return skip;
2478
2239
  }
2479
- // ========== P2-004: Cycle Metrics Methods ==========
2240
+ // ========== P2-004: Cycle Metrics (delegated to RespawnCycleMetricsTracker) ==========
2480
2241
  /**
2481
2242
  * Start tracking metrics for a new cycle.
2482
2243
  * Called when a respawn cycle begins.
@@ -2484,28 +2245,16 @@ export class RespawnController extends EventEmitter {
2484
2245
  startCycleMetrics(idleReason) {
2485
2246
  if (!this.config.trackCycleMetrics)
2486
2247
  return;
2487
- const now = Date.now();
2488
- this.currentCycleMetrics = {
2489
- cycleId: `${this.session.id}:${this.cycleCount}`,
2490
- sessionId: this.session.id,
2491
- cycleNumber: this.cycleCount,
2492
- startedAt: now,
2493
- idleReason,
2494
- idleDetectionMs: now - this.idleDetectionStartTime,
2495
- stepsCompleted: [],
2496
- clearSkipped: false,
2497
- tokenCountAtStart: this.lastTokenCount,
2498
- completionConfirmMsUsed: this.getAdaptiveCompletionConfirmMs(),
2499
- };
2248
+ this.cycleMetrics.startCycle(this.session.id, this.cycleCount, idleReason, this.idleDetectionStartTime, this.lastTokenCount, this.getAdaptiveCompletionConfirmMs());
2500
2249
  }
2501
2250
  /**
2502
2251
  * Record a completed step in the current cycle.
2503
2252
  * @param step - Name of the step (e.g., 'update', 'clear', 'init')
2504
2253
  */
2505
2254
  recordCycleStep(step) {
2506
- if (!this.config.trackCycleMetrics || !this.currentCycleMetrics)
2255
+ if (!this.config.trackCycleMetrics)
2507
2256
  return;
2508
- this.currentCycleMetrics.stepsCompleted?.push(step);
2257
+ this.cycleMetrics.recordStep(step);
2509
2258
  }
2510
2259
  /**
2511
2260
  * Complete the current cycle metrics with outcome.
@@ -2515,78 +2264,23 @@ export class RespawnController extends EventEmitter {
2515
2264
  * @param errorMessage - Optional error message if outcome is 'error'
2516
2265
  */
2517
2266
  completeCycleMetrics(outcome, errorMessage) {
2518
- if (!this.config.trackCycleMetrics || !this.currentCycleMetrics)
2267
+ if (!this.config.trackCycleMetrics)
2519
2268
  return;
2520
- const now = Date.now();
2521
- const metrics = {
2522
- ...this.currentCycleMetrics,
2523
- completedAt: now,
2524
- durationMs: now - (this.currentCycleMetrics.startedAt ?? now),
2525
- outcome,
2526
- errorMessage,
2527
- tokenCountAtEnd: this.lastTokenCount,
2528
- };
2529
- // Add to recent metrics
2530
- this.recentCycleMetrics.push(metrics);
2531
- if (this.recentCycleMetrics.length > RespawnController.MAX_CYCLE_METRICS_IN_MEMORY) {
2532
- this.recentCycleMetrics.shift();
2533
- }
2534
- // Record timing data for adaptive timing
2535
- this.recordTimingData(metrics.idleDetectionMs, metrics.durationMs);
2536
- // Update aggregate metrics
2537
- this.updateAggregateMetrics(metrics);
2538
- // Clear current cycle
2539
- this.currentCycleMetrics = null;
2540
- this.log(`Cycle #${metrics.cycleNumber} metrics: ${outcome}, duration=${metrics.durationMs}ms, idle_detection=${metrics.idleDetectionMs}ms`);
2541
- }
2542
- /**
2543
- * Update aggregate metrics with a new cycle's data.
2544
- * @param metrics - The completed cycle metrics
2545
- */
2546
- updateAggregateMetrics(metrics) {
2547
- const agg = this.aggregateMetrics;
2548
- agg.totalCycles++;
2549
- switch (metrics.outcome) {
2550
- case 'success':
2551
- agg.successfulCycles++;
2552
- break;
2553
- case 'stuck_recovery':
2554
- agg.stuckRecoveryCycles++;
2555
- break;
2556
- case 'blocked':
2557
- agg.blockedCycles++;
2558
- break;
2559
- case 'error':
2560
- agg.errorCycles++;
2561
- break;
2562
- case 'cancelled':
2563
- // Cancelled cycles don't count towards any specific category
2564
- // but are still counted in totalCycles
2565
- break;
2566
- default:
2567
- assertNever(metrics.outcome, `Unhandled CycleOutcome: ${metrics.outcome}`);
2568
- }
2569
- // Recalculate averages using all recent metrics
2570
- const durations = this.recentCycleMetrics.map((m) => m.durationMs);
2571
- const idleTimes = this.recentCycleMetrics.map((m) => m.idleDetectionMs);
2572
- if (durations.length > 0) {
2573
- agg.avgCycleDurationMs = Math.round(durations.reduce((a, b) => a + b, 0) / durations.length);
2574
- agg.avgIdleDetectionMs = Math.round(idleTimes.reduce((a, b) => a + b, 0) / idleTimes.length);
2575
- // Calculate P90
2576
- const sortedDurations = [...durations].sort((a, b) => a - b);
2577
- const p90Index = Math.floor(sortedDurations.length * 0.9);
2578
- agg.p90CycleDurationMs = sortedDurations[p90Index];
2269
+ const metrics = this.cycleMetrics.completeCycle(outcome, this.lastTokenCount, errorMessage);
2270
+ if (metrics) {
2271
+ // Record timing data for adaptive timing
2272
+ if (this.config.adaptiveTimingEnabled) {
2273
+ this.adaptiveTiming.recordTimingData(metrics.idleDetectionMs, metrics.durationMs);
2274
+ }
2275
+ this.log(`Cycle #${metrics.cycleNumber} metrics: ${outcome}, duration=${metrics.durationMs}ms, idle_detection=${metrics.idleDetectionMs}ms`);
2579
2276
  }
2580
- // Calculate success rate
2581
- agg.successRate = agg.totalCycles > 0 ? Math.round((agg.successfulCycles / agg.totalCycles) * 100) : 100;
2582
- agg.lastUpdatedAt = Date.now();
2583
2277
  }
2584
2278
  /**
2585
2279
  * Get aggregate metrics for monitoring.
2586
2280
  * @returns Copy of aggregate metrics
2587
2281
  */
2588
2282
  getAggregateMetrics() {
2589
- return { ...this.aggregateMetrics };
2283
+ return this.cycleMetrics.getAggregate();
2590
2284
  }
2591
2285
  /**
2592
2286
  * Get recent cycle metrics for analysis.
@@ -2594,9 +2288,9 @@ export class RespawnController extends EventEmitter {
2594
2288
  * @returns Recent cycle metrics, newest first
2595
2289
  */
2596
2290
  getRecentCycleMetrics(limit = 20) {
2597
- return this.recentCycleMetrics.slice(-limit).reverse();
2291
+ return this.cycleMetrics.getRecent(limit);
2598
2292
  }
2599
- // ========== P2-005: Health Score Methods ==========
2293
+ // ========== P2-005: Health Score (delegated to respawn-health.ts) ==========
2600
2294
  /**
2601
2295
  * Calculate a comprehensive health score for the Ralph Loop system.
2602
2296
  * Aggregates multiple health signals into a single score (0-100).
@@ -2604,161 +2298,27 @@ export class RespawnController extends EventEmitter {
2604
2298
  * @returns Health score with component breakdown
2605
2299
  */
2606
2300
  calculateHealthScore() {
2607
- const now = Date.now();
2608
- const components = {
2609
- cycleSuccess: this.calculateCycleSuccessScore(),
2610
- circuitBreaker: this.calculateCircuitBreakerScore(),
2611
- iterationProgress: this.calculateIterationProgressScore(),
2612
- aiChecker: this.calculateAiCheckerScore(),
2613
- stuckRecovery: this.calculateStuckRecoveryScore(),
2614
- };
2615
- // Weighted average (cycle success is most important)
2616
- const weights = {
2617
- cycleSuccess: 0.35,
2618
- circuitBreaker: 0.2,
2619
- iterationProgress: 0.2,
2620
- aiChecker: 0.15,
2621
- stuckRecovery: 0.1,
2622
- };
2623
- const score = Math.round(components.cycleSuccess * weights.cycleSuccess +
2624
- components.circuitBreaker * weights.circuitBreaker +
2625
- components.iterationProgress * weights.iterationProgress +
2626
- components.aiChecker * weights.aiChecker +
2627
- components.stuckRecovery * weights.stuckRecovery);
2628
- // Determine status
2629
- let status;
2630
- if (score >= 90)
2631
- status = 'excellent';
2632
- else if (score >= 70)
2633
- status = 'good';
2634
- else if (score >= 50)
2635
- status = 'degraded';
2636
- else
2637
- status = 'critical';
2638
- // Generate recommendations
2639
- const recommendations = this.generateHealthRecommendations(components);
2640
- // Generate summary
2641
- const summary = this.generateHealthSummary(score, status, components);
2642
- return {
2643
- score,
2644
- status,
2645
- components,
2646
- summary,
2647
- recommendations,
2648
- calculatedAt: now,
2649
- };
2650
- }
2651
- /**
2652
- * Calculate score based on recent cycle success rate.
2653
- */
2654
- calculateCycleSuccessScore() {
2655
- if (this.aggregateMetrics.totalCycles === 0)
2656
- return 100; // No data = assume healthy
2657
- return this.aggregateMetrics.successRate;
2658
- }
2659
- /**
2660
- * Calculate score based on circuit breaker state.
2661
- */
2662
- calculateCircuitBreakerScore() {
2663
2301
  const tracker = this.session.ralphTracker;
2664
- if (!tracker)
2665
- return 100;
2666
- const cb = tracker.circuitBreakerStatus;
2667
- switch (cb.state) {
2668
- case 'CLOSED':
2669
- return 100;
2670
- case 'HALF_OPEN':
2671
- return 50;
2672
- case 'OPEN':
2673
- return 0;
2674
- default:
2675
- return 100;
2676
- }
2677
- }
2678
- /**
2679
- * Calculate score based on iteration progress.
2680
- */
2681
- calculateIterationProgressScore() {
2682
- const tracker = this.session.ralphTracker;
2683
- if (!tracker)
2684
- return 100;
2685
- const stallMetrics = tracker.getIterationStallMetrics();
2686
- const { stallDurationMs, warningThresholdMs, criticalThresholdMs } = stallMetrics;
2687
- if (stallDurationMs >= criticalThresholdMs)
2688
- return 0;
2689
- if (stallDurationMs >= warningThresholdMs)
2690
- return 30;
2691
- if (stallDurationMs >= warningThresholdMs / 2)
2692
- return 70;
2693
- return 100;
2694
- }
2695
- /**
2696
- * Calculate score based on AI checker health.
2697
- */
2698
- calculateAiCheckerScore() {
2699
- const state = this.aiChecker.getState();
2700
- if (state.status === 'disabled')
2701
- return 30;
2702
- if (state.status === 'cooldown')
2703
- return 70;
2704
- if (state.consecutiveErrors > 0)
2705
- return 50;
2706
- return 100;
2707
- }
2708
- /**
2709
- * Calculate score based on stuck-state recovery count.
2710
- */
2711
- calculateStuckRecoveryScore() {
2712
- const maxRecoveries = this.config.maxStuckRecoveries ?? 3;
2713
- if (this.stuckRecoveryCount === 0)
2714
- return 100;
2715
- if (this.stuckRecoveryCount >= maxRecoveries)
2716
- return 0;
2717
- return Math.round(100 - (this.stuckRecoveryCount / maxRecoveries) * 100);
2718
- }
2719
- /**
2720
- * Generate health recommendations based on component scores.
2721
- */
2722
- generateHealthRecommendations(components) {
2723
- const recommendations = [];
2724
- if (components.cycleSuccess < 70) {
2725
- recommendations.push('Cycle success rate is low. Check for recurring errors or stuck states.');
2726
- }
2727
- if (components.circuitBreaker < 50) {
2728
- recommendations.push('Circuit breaker is open or half-open. Review recent errors and consider manual reset.');
2729
- }
2730
- if (components.iterationProgress < 50) {
2731
- recommendations.push('Iteration progress has stalled. Check if Claude is stuck on a task.');
2732
- }
2733
- if (components.aiChecker < 50) {
2734
- recommendations.push('AI idle checker has errors. May need to check Claude CLI availability.');
2735
- }
2736
- if (components.stuckRecovery < 50) {
2737
- recommendations.push('Multiple stuck-state recoveries occurred. Consider increasing timeouts.');
2738
- }
2739
- if (recommendations.length === 0) {
2740
- recommendations.push('System is healthy. No action needed.');
2741
- }
2742
- return recommendations;
2743
- }
2744
- /**
2745
- * Generate a human-readable health summary.
2746
- */
2747
- generateHealthSummary(score, status, components) {
2748
- const lowest = Object.entries(components).reduce((min, [key, val]) => (val < min.val ? { key, val } : min), {
2749
- key: '',
2750
- val: 100,
2751
- });
2752
- if (status === 'excellent') {
2753
- return `Ralph Loop is operating excellently (${score}/100). All systems healthy.`;
2754
- }
2755
- if (status === 'good') {
2756
- return `Ralph Loop is operating well (${score}/100). Minor issues in ${lowest.key}.`;
2757
- }
2758
- if (status === 'degraded') {
2759
- return `Ralph Loop is degraded (${score}/100). Primary issue: ${lowest.key} (${lowest.val}/100).`;
2760
- }
2761
- return `Ralph Loop is in critical state (${score}/100). Immediate attention needed: ${lowest.key}.`;
2302
+ const stallMetrics = tracker?.getIterationStallMetrics();
2303
+ const aiState = this.aiChecker.getState();
2304
+ const inputs = {
2305
+ aggregateMetrics: this.cycleMetrics.getAggregate(),
2306
+ circuitBreakerStatus: tracker?.circuitBreakerStatus ?? null,
2307
+ iterationStallMetrics: stallMetrics
2308
+ ? {
2309
+ stallDurationMs: stallMetrics.stallDurationMs,
2310
+ warningThresholdMs: stallMetrics.warningThresholdMs,
2311
+ criticalThresholdMs: stallMetrics.criticalThresholdMs,
2312
+ }
2313
+ : null,
2314
+ aiCheckerState: {
2315
+ status: aiState.status,
2316
+ consecutiveErrors: aiState.consecutiveErrors,
2317
+ },
2318
+ stuckRecoveryCount: this.stuckRecoveryCount,
2319
+ maxStuckRecoveries: this.config.maxStuckRecoveries ?? 3,
2320
+ };
2321
+ return calculateHealthScore(inputs);
2762
2322
  }
2763
2323
  }
2764
2324
  //# sourceMappingURL=respawn-controller.js.map