aicodeman 0.2.9 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (347) hide show
  1. package/README.md +91 -0
  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 +36 -0
  29. package/dist/config/server-timing.d.ts.map +1 -0
  30. package/dist/config/server-timing.js +51 -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.map +1 -1
  45. package/dist/hooks-config.js +7 -6
  46. package/dist/hooks-config.js.map +1 -1
  47. package/dist/image-watcher.d.ts +4 -4
  48. package/dist/image-watcher.d.ts.map +1 -1
  49. package/dist/image-watcher.js +17 -30
  50. package/dist/image-watcher.js.map +1 -1
  51. package/dist/index.js +1 -2
  52. package/dist/index.js.map +1 -1
  53. package/dist/plan-orchestrator.d.ts +2 -24
  54. package/dist/plan-orchestrator.d.ts.map +1 -1
  55. package/dist/plan-orchestrator.js.map +1 -1
  56. package/dist/push-store.d.ts +1 -1
  57. package/dist/push-store.d.ts.map +1 -1
  58. package/dist/push-store.js +4 -12
  59. package/dist/push-store.js.map +1 -1
  60. package/dist/ralph-fix-plan-watcher.d.ts +91 -0
  61. package/dist/ralph-fix-plan-watcher.d.ts.map +1 -0
  62. package/dist/ralph-fix-plan-watcher.js +326 -0
  63. package/dist/ralph-fix-plan-watcher.js.map +1 -0
  64. package/dist/ralph-plan-tracker.d.ts +201 -0
  65. package/dist/ralph-plan-tracker.d.ts.map +1 -0
  66. package/dist/ralph-plan-tracker.js +325 -0
  67. package/dist/ralph-plan-tracker.js.map +1 -0
  68. package/dist/ralph-stall-detector.d.ts +84 -0
  69. package/dist/ralph-stall-detector.d.ts.map +1 -0
  70. package/dist/ralph-stall-detector.js +139 -0
  71. package/dist/ralph-stall-detector.js.map +1 -0
  72. package/dist/ralph-status-parser.d.ts +141 -0
  73. package/dist/ralph-status-parser.d.ts.map +1 -0
  74. package/dist/ralph-status-parser.js +478 -0
  75. package/dist/ralph-status-parser.js.map +1 -0
  76. package/dist/ralph-tracker.d.ts +194 -685
  77. package/dist/ralph-tracker.d.ts.map +1 -1
  78. package/dist/ralph-tracker.js +349 -1713
  79. package/dist/ralph-tracker.js.map +1 -1
  80. package/dist/respawn-adaptive-timing.d.ts +61 -0
  81. package/dist/respawn-adaptive-timing.d.ts.map +1 -0
  82. package/dist/respawn-adaptive-timing.js +105 -0
  83. package/dist/respawn-adaptive-timing.js.map +1 -0
  84. package/dist/respawn-controller.d.ts +12 -101
  85. package/dist/respawn-controller.d.ts.map +1 -1
  86. package/dist/respawn-controller.js +144 -593
  87. package/dist/respawn-controller.js.map +1 -1
  88. package/dist/respawn-health.d.ts +54 -0
  89. package/dist/respawn-health.d.ts.map +1 -0
  90. package/dist/respawn-health.js +183 -0
  91. package/dist/respawn-health.js.map +1 -0
  92. package/dist/respawn-metrics.d.ts +81 -0
  93. package/dist/respawn-metrics.d.ts.map +1 -0
  94. package/dist/respawn-metrics.js +198 -0
  95. package/dist/respawn-metrics.js.map +1 -0
  96. package/dist/respawn-patterns.d.ts +45 -0
  97. package/dist/respawn-patterns.d.ts.map +1 -0
  98. package/dist/respawn-patterns.js +125 -0
  99. package/dist/respawn-patterns.js.map +1 -0
  100. package/dist/session-auto-ops.d.ts +89 -0
  101. package/dist/session-auto-ops.d.ts.map +1 -0
  102. package/dist/session-auto-ops.js +224 -0
  103. package/dist/session-auto-ops.js.map +1 -0
  104. package/dist/session-cli-builder.d.ts +62 -0
  105. package/dist/session-cli-builder.d.ts.map +1 -0
  106. package/dist/session-cli-builder.js +121 -0
  107. package/dist/session-cli-builder.js.map +1 -0
  108. package/dist/session-task-cache.d.ts +52 -0
  109. package/dist/session-task-cache.d.ts.map +1 -0
  110. package/dist/session-task-cache.js +90 -0
  111. package/dist/session-task-cache.js.map +1 -0
  112. package/dist/session.d.ts +2 -33
  113. package/dist/session.d.ts.map +1 -1
  114. package/dist/session.js +58 -309
  115. package/dist/session.js.map +1 -1
  116. package/dist/state-store.d.ts +2 -2
  117. package/dist/state-store.d.ts.map +1 -1
  118. package/dist/state-store.js +12 -23
  119. package/dist/state-store.js.map +1 -1
  120. package/dist/subagent-watcher.d.ts +3 -4
  121. package/dist/subagent-watcher.d.ts.map +1 -1
  122. package/dist/subagent-watcher.js +24 -61
  123. package/dist/subagent-watcher.js.map +1 -1
  124. package/dist/team-watcher.d.ts.map +1 -1
  125. package/dist/team-watcher.js +2 -5
  126. package/dist/team-watcher.js.map +1 -1
  127. package/dist/tmux-manager.d.ts.map +1 -1
  128. package/dist/tmux-manager.js +1 -2
  129. package/dist/tmux-manager.js.map +1 -1
  130. package/dist/tunnel-manager.d.ts +26 -0
  131. package/dist/tunnel-manager.d.ts.map +1 -1
  132. package/dist/tunnel-manager.js +127 -7
  133. package/dist/tunnel-manager.js.map +1 -1
  134. package/dist/types/api.d.ts +93 -0
  135. package/dist/types/api.d.ts.map +1 -0
  136. package/dist/types/api.js +83 -0
  137. package/dist/types/api.js.map +1 -0
  138. package/dist/types/app-state.d.ts +100 -0
  139. package/dist/types/app-state.d.ts.map +1 -0
  140. package/dist/types/app-state.js +59 -0
  141. package/dist/types/app-state.js.map +1 -0
  142. package/dist/types/common.d.ts +70 -0
  143. package/dist/types/common.d.ts.map +1 -0
  144. package/dist/types/common.js +8 -0
  145. package/dist/types/common.js.map +1 -0
  146. package/dist/types/index.d.ts +18 -0
  147. package/dist/types/index.d.ts.map +1 -0
  148. package/dist/types/index.js +18 -0
  149. package/dist/types/index.js.map +1 -0
  150. package/dist/types/lifecycle.d.ts +17 -0
  151. package/dist/types/lifecycle.d.ts.map +1 -0
  152. package/dist/types/lifecycle.js +5 -0
  153. package/dist/types/lifecycle.js.map +1 -0
  154. package/dist/types/plan.d.ts +32 -0
  155. package/dist/types/plan.d.ts.map +1 -0
  156. package/dist/types/plan.js +5 -0
  157. package/dist/types/plan.js.map +1 -0
  158. package/dist/types/push.d.ts +23 -0
  159. package/dist/types/push.d.ts.map +1 -0
  160. package/dist/types/push.js +5 -0
  161. package/dist/types/push.js.map +1 -0
  162. package/dist/types/ralph.d.ts +241 -0
  163. package/dist/types/ralph.d.ts.map +1 -0
  164. package/dist/types/ralph.js +49 -0
  165. package/dist/types/ralph.js.map +1 -0
  166. package/dist/types/respawn.d.ts +250 -0
  167. package/dist/types/respawn.d.ts.map +1 -0
  168. package/dist/types/respawn.js +5 -0
  169. package/dist/types/respawn.js.map +1 -0
  170. package/dist/types/run-summary.d.ts +81 -0
  171. package/dist/types/run-summary.d.ts.map +1 -0
  172. package/dist/types/run-summary.js +22 -0
  173. package/dist/types/run-summary.js.map +1 -0
  174. package/dist/types/session.d.ts +130 -0
  175. package/dist/types/session.d.ts.map +1 -0
  176. package/dist/types/session.js +5 -0
  177. package/dist/types/session.js.map +1 -0
  178. package/dist/types/task.d.ts +58 -0
  179. package/dist/types/task.d.ts.map +1 -0
  180. package/dist/types/task.js +5 -0
  181. package/dist/types/task.js.map +1 -0
  182. package/dist/types/teams.d.ts +55 -0
  183. package/dist/types/teams.d.ts.map +1 -0
  184. package/dist/types/teams.js +5 -0
  185. package/dist/types/teams.js.map +1 -0
  186. package/dist/types/tools.d.ts +46 -0
  187. package/dist/types/tools.d.ts.map +1 -0
  188. package/dist/types/tools.js +5 -0
  189. package/dist/types/tools.js.map +1 -0
  190. package/dist/types.d.ts +1 -1138
  191. package/dist/types.d.ts.map +1 -1
  192. package/dist/types.js +1 -214
  193. package/dist/types.js.map +1 -1
  194. package/dist/utils/claude-cli-resolver.d.ts.map +1 -1
  195. package/dist/utils/claude-cli-resolver.js +1 -2
  196. package/dist/utils/claude-cli-resolver.js.map +1 -1
  197. package/dist/utils/debouncer.d.ts +111 -0
  198. package/dist/utils/debouncer.d.ts.map +1 -0
  199. package/dist/utils/debouncer.js +162 -0
  200. package/dist/utils/debouncer.js.map +1 -0
  201. package/dist/utils/index.d.ts +3 -2
  202. package/dist/utils/index.d.ts.map +1 -1
  203. package/dist/utils/index.js +3 -2
  204. package/dist/utils/index.js.map +1 -1
  205. package/dist/utils/opencode-cli-resolver.d.ts.map +1 -1
  206. package/dist/utils/opencode-cli-resolver.js +1 -2
  207. package/dist/utils/opencode-cli-resolver.js.map +1 -1
  208. package/dist/utils/string-similarity.d.ts +0 -57
  209. package/dist/utils/string-similarity.d.ts.map +1 -1
  210. package/dist/utils/string-similarity.js +3 -18
  211. package/dist/utils/string-similarity.js.map +1 -1
  212. package/dist/web/middleware/auth.d.ts +31 -0
  213. package/dist/web/middleware/auth.d.ts.map +1 -0
  214. package/dist/web/middleware/auth.js +154 -0
  215. package/dist/web/middleware/auth.js.map +1 -0
  216. package/dist/web/ports/auth-port.d.ts +18 -0
  217. package/dist/web/ports/auth-port.d.ts.map +1 -0
  218. package/dist/web/ports/auth-port.js +6 -0
  219. package/dist/web/ports/auth-port.js.map +1 -0
  220. package/dist/web/ports/config-port.d.ts +28 -0
  221. package/dist/web/ports/config-port.d.ts.map +1 -0
  222. package/dist/web/ports/config-port.js +6 -0
  223. package/dist/web/ports/config-port.js.map +1 -0
  224. package/dist/web/ports/event-port.d.ts +13 -0
  225. package/dist/web/ports/event-port.d.ts.map +1 -0
  226. package/dist/web/ports/event-port.js +6 -0
  227. package/dist/web/ports/event-port.js.map +1 -0
  228. package/dist/web/ports/index.d.ts +14 -0
  229. package/dist/web/ports/index.d.ts.map +1 -0
  230. package/dist/web/ports/index.js +9 -0
  231. package/dist/web/ports/index.js.map +1 -0
  232. package/dist/web/ports/infra-port.d.ts +36 -0
  233. package/dist/web/ports/infra-port.d.ts.map +1 -0
  234. package/dist/web/ports/infra-port.js +6 -0
  235. package/dist/web/ports/infra-port.js.map +1 -0
  236. package/dist/web/ports/respawn-port.d.ts +20 -0
  237. package/dist/web/ports/respawn-port.d.ts.map +1 -0
  238. package/dist/web/ports/respawn-port.js +6 -0
  239. package/dist/web/ports/respawn-port.js.map +1 -0
  240. package/dist/web/ports/session-port.d.ts +15 -0
  241. package/dist/web/ports/session-port.d.ts.map +1 -0
  242. package/dist/web/ports/session-port.js +6 -0
  243. package/dist/web/ports/session-port.js.map +1 -0
  244. package/dist/web/public/api-client.js +70 -0
  245. package/dist/web/public/api-client.js.br +0 -0
  246. package/dist/web/public/api-client.js.gz +0 -0
  247. package/dist/web/public/app.js +151 -235
  248. package/dist/web/public/app.js.br +0 -0
  249. package/dist/web/public/app.js.gz +0 -0
  250. package/dist/web/public/constants.js +238 -0
  251. package/dist/web/public/constants.js.br +0 -0
  252. package/dist/web/public/constants.js.gz +0 -0
  253. package/dist/web/public/index.html +11 -3
  254. package/dist/web/public/index.html.br +0 -0
  255. package/dist/web/public/index.html.gz +0 -0
  256. package/dist/web/public/keyboard-accessory.js +279 -0
  257. package/dist/web/public/keyboard-accessory.js.br +0 -0
  258. package/dist/web/public/keyboard-accessory.js.gz +0 -0
  259. package/dist/web/public/mobile-handlers.js +467 -0
  260. package/dist/web/public/mobile-handlers.js.br +0 -0
  261. package/dist/web/public/mobile-handlers.js.gz +0 -0
  262. package/dist/web/public/mobile.css.gz +0 -0
  263. package/dist/web/public/notification-manager.js +445 -0
  264. package/dist/web/public/notification-manager.js.br +0 -0
  265. package/dist/web/public/notification-manager.js.gz +0 -0
  266. package/dist/web/public/ralph-wizard.js +3 -3
  267. package/dist/web/public/ralph-wizard.js.br +0 -0
  268. package/dist/web/public/ralph-wizard.js.gz +0 -0
  269. package/dist/web/public/styles.css.gz +0 -0
  270. package/dist/web/public/subagent-windows.js +1115 -0
  271. package/dist/web/public/subagent-windows.js.br +0 -0
  272. package/dist/web/public/subagent-windows.js.gz +0 -0
  273. package/dist/web/public/sw.js.gz +0 -0
  274. package/dist/web/public/upload.html.gz +0 -0
  275. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  276. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  277. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  278. package/dist/web/public/vendor/xterm.css.gz +0 -0
  279. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  280. package/dist/web/public/voice-input.js +858 -0
  281. package/dist/web/public/voice-input.js.br +0 -0
  282. package/dist/web/public/voice-input.js.gz +0 -0
  283. package/dist/web/route-helpers.d.ts +38 -0
  284. package/dist/web/route-helpers.d.ts.map +1 -0
  285. package/dist/web/route-helpers.js +143 -0
  286. package/dist/web/route-helpers.js.map +1 -0
  287. package/dist/web/routes/case-routes.d.ts +9 -0
  288. package/dist/web/routes/case-routes.d.ts.map +1 -0
  289. package/dist/web/routes/case-routes.js +419 -0
  290. package/dist/web/routes/case-routes.js.map +1 -0
  291. package/dist/web/routes/file-routes.d.ts +8 -0
  292. package/dist/web/routes/file-routes.d.ts.map +1 -0
  293. package/dist/web/routes/file-routes.js +337 -0
  294. package/dist/web/routes/file-routes.js.map +1 -0
  295. package/dist/web/routes/hook-event-routes.d.ts +9 -0
  296. package/dist/web/routes/hook-event-routes.d.ts.map +1 -0
  297. package/dist/web/routes/hook-event-routes.js +57 -0
  298. package/dist/web/routes/hook-event-routes.js.map +1 -0
  299. package/dist/web/routes/index.d.ts +16 -0
  300. package/dist/web/routes/index.d.ts.map +1 -0
  301. package/dist/web/routes/index.js +16 -0
  302. package/dist/web/routes/index.js.map +1 -0
  303. package/dist/web/routes/mux-routes.d.ts +8 -0
  304. package/dist/web/routes/mux-routes.d.ts.map +1 -0
  305. package/dist/web/routes/mux-routes.js +32 -0
  306. package/dist/web/routes/mux-routes.js.map +1 -0
  307. package/dist/web/routes/plan-routes.d.ts +9 -0
  308. package/dist/web/routes/plan-routes.d.ts.map +1 -0
  309. package/dist/web/routes/plan-routes.js +381 -0
  310. package/dist/web/routes/plan-routes.js.map +1 -0
  311. package/dist/web/routes/push-routes.d.ts +8 -0
  312. package/dist/web/routes/push-routes.d.ts.map +1 -0
  313. package/dist/web/routes/push-routes.js +49 -0
  314. package/dist/web/routes/push-routes.js.map +1 -0
  315. package/dist/web/routes/ralph-routes.d.ts +9 -0
  316. package/dist/web/routes/ralph-routes.d.ts.map +1 -0
  317. package/dist/web/routes/ralph-routes.js +475 -0
  318. package/dist/web/routes/ralph-routes.js.map +1 -0
  319. package/dist/web/routes/respawn-routes.d.ts +8 -0
  320. package/dist/web/routes/respawn-routes.d.ts.map +1 -0
  321. package/dist/web/routes/respawn-routes.js +260 -0
  322. package/dist/web/routes/respawn-routes.js.map +1 -0
  323. package/dist/web/routes/scheduled-routes.d.ts +8 -0
  324. package/dist/web/routes/scheduled-routes.d.ts.map +1 -0
  325. package/dist/web/routes/scheduled-routes.js +51 -0
  326. package/dist/web/routes/scheduled-routes.js.map +1 -0
  327. package/dist/web/routes/session-routes.d.ts +9 -0
  328. package/dist/web/routes/session-routes.d.ts.map +1 -0
  329. package/dist/web/routes/session-routes.js +729 -0
  330. package/dist/web/routes/session-routes.js.map +1 -0
  331. package/dist/web/routes/system-routes.d.ts +9 -0
  332. package/dist/web/routes/system-routes.d.ts.map +1 -0
  333. package/dist/web/routes/system-routes.js +678 -0
  334. package/dist/web/routes/system-routes.js.map +1 -0
  335. package/dist/web/routes/team-routes.d.ts +8 -0
  336. package/dist/web/routes/team-routes.d.ts.map +1 -0
  337. package/dist/web/routes/team-routes.js +14 -0
  338. package/dist/web/routes/team-routes.js.map +1 -0
  339. package/dist/web/schemas.d.ts +43 -3
  340. package/dist/web/schemas.d.ts.map +1 -1
  341. package/dist/web/schemas.js +6 -2
  342. package/dist/web/schemas.js.map +1 -1
  343. package/dist/web/server.d.ts +10 -9
  344. package/dist/web/server.d.ts.map +1 -1
  345. package/dist/web/server.js +335 -3824
  346. package/dist/web/server.js.map +1 -1
  347. package/package.json +1 -1
@@ -38,18 +38,15 @@ import { randomUUID } from 'node:crypto';
38
38
  import { AiIdleChecker } from './ai-idle-checker.js';
39
39
  import { AiPlanChecker } from './ai-plan-checker.js';
40
40
  import { BufferAccumulator } from './utils/buffer-accumulator.js';
41
- import { ANSI_ESCAPE_PATTERN_SIMPLE, TOKEN_PATTERN, assertNever } from './utils/index.js';
41
+ import { ANSI_ESCAPE_PATTERN_SIMPLE, assertNever, CleanupManager } from './utils/index.js';
42
42
  import { MAX_RESPAWN_BUFFER_SIZE, TRIM_RESPAWN_BUFFER_TO as RESPAWN_BUFFER_TRIM_SIZE } from './config/buffer-limits.js';
43
+ import { isCompletionMessage, hasWorkingPattern, extractTokenCount, PROMPT_PATTERNS, WORKING_PATTERNS, } from './respawn-patterns.js';
44
+ import { RespawnAdaptiveTiming } from './respawn-adaptive-timing.js';
45
+ import { RespawnCycleMetricsTracker } from './respawn-metrics.js';
46
+ import { calculateHealthScore, shouldSkipClear } from './respawn-health.js';
47
+ import { AI_CHECK_MODEL, AI_IDLE_CHECK_MAX_CONTEXT, AI_PLAN_CHECK_MAX_CONTEXT } from './config/ai-defaults.js';
43
48
  // ========== 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;
49
+ // COMPLETION_TIME_PATTERN moved to ./respawn-patterns.ts
53
50
  /** Pre-filter: numbered option pattern for plan mode detection */
54
51
  const PLAN_MODE_OPTION_PATTERN = /\d+\.\s+(Yes|No|Type|Cancel|Skip|Proceed|Approve|Reject)/i;
55
52
  /** Pre-filter: selection indicator arrow for plan mode detection */
@@ -67,13 +64,13 @@ const DEFAULT_CONFIG = {
67
64
  autoAcceptPrompts: true, // auto-accept plan mode prompts (not questions)
68
65
  autoAcceptDelayMs: 8000, // 8 seconds before auto-accepting
69
66
  aiIdleCheckEnabled: true, // use AI to confirm idle state
70
- aiIdleCheckModel: 'claude-opus-4-5-20251101',
71
- aiIdleCheckMaxContext: 16000, // ~4k tokens
67
+ aiIdleCheckModel: AI_CHECK_MODEL,
68
+ aiIdleCheckMaxContext: AI_IDLE_CHECK_MAX_CONTEXT,
72
69
  aiIdleCheckTimeoutMs: 90000, // 90 seconds (thinking can be slow)
73
70
  aiIdleCheckCooldownMs: 180000, // 3 minutes after WORKING verdict
74
71
  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)
72
+ aiPlanCheckModel: AI_CHECK_MODEL,
73
+ aiPlanCheckMaxContext: AI_PLAN_CHECK_MAX_CONTEXT,
77
74
  aiPlanCheckTimeoutMs: 60000, // 60 seconds (thinking can be slow)
78
75
  aiPlanCheckCooldownMs: 30000, // 30 seconds after NOT_PLAN_MODE
79
76
  stuckStateDetectionEnabled: true, // detect stuck states
@@ -157,20 +154,12 @@ export class RespawnController extends EventEmitter {
157
154
  config;
158
155
  /** Current state machine state */
159
156
  _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;
157
+ /** Centralized timer lifecycle manager — disposed and recreated on clearTimers() */
158
+ cleanup = new CleanupManager();
159
+ /** Maps timer names to CleanupManager registration IDs (for individual cancel) */
160
+ timerIds = new Map();
168
161
  /** Cached key fields from last emitted detection status (for dedup) */
169
162
  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
163
  /** Whether any terminal output has been received since start/last-auto-accept */
175
164
  hasReceivedOutput = false;
176
165
  /** Whether an elicitation dialog (AskUserQuestion) was detected via hook signal */
@@ -184,8 +173,6 @@ export class RespawnController extends EventEmitter {
184
173
  idlePromptReceived = false;
185
174
  /** Timestamp when idle_prompt was received */
186
175
  idlePromptTime = null;
187
- /** Timer for short confirmation after hook signal (handles race conditions) */
188
- hookConfirmTimer = null;
189
176
  /** Confirmation delay after hook signal before confirming idle (ms) */
190
177
  static HOOK_CONFIRM_DELAY_MS = 3000;
191
178
  /** Number of completed respawn cycles */
@@ -208,10 +195,6 @@ export class RespawnController extends EventEmitter {
208
195
  planCheckStartTime = 0;
209
196
  /** Unique ID for current AI check request (to detect stale results) */
210
197
  _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
198
  /** Fallback timeout for /clear step (ms) - sends /init without waiting for prompt */
216
199
  static CLEAR_FALLBACK_TIMEOUT_MS = 10000;
217
200
  // ========== Timer Tracking for UI Countdown Display ==========
@@ -222,44 +205,18 @@ export class RespawnController extends EventEmitter {
222
205
  // ========== Stuck-State Detection State ==========
223
206
  /** Timestamp when the current state was entered */
224
207
  stateEnteredAt = 0;
225
- /** Timer for stuck-state detection */
226
- stuckStateTimer = null;
227
208
  /** Whether a stuck-state warning has been emitted for current state */
228
209
  stuckStateWarned = false;
229
210
  /** Number of stuck-state recovery attempts */
230
211
  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;
212
+ // ========== P2-001: Adaptive Timing (delegated to RespawnAdaptiveTiming) ==========
213
+ /** Adaptive timing controller */
214
+ adaptiveTiming;
215
+ // ========== P2-004: Cycle Metrics (delegated to RespawnCycleMetricsTracker) ==========
216
+ /** Cycle metrics tracker */
217
+ cycleMetrics = new RespawnCycleMetricsTracker();
244
218
  /** Timestamp when idle detection started for current cycle */
245
219
  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
220
  // ========== Multi-Layer Detection State ==========
264
221
  /** Layer 1: Timestamp when completion message was detected */
265
222
  completionMessageTime = null;
@@ -271,71 +228,7 @@ export class RespawnController extends EventEmitter {
271
228
  lastTokenChangeTime = 0;
272
229
  /** Layer 4: Timestamp when last working pattern was seen */
273
230
  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
- ];
231
+ // PROMPT_PATTERNS and WORKING_PATTERNS are now imported from ./respawn-patterns.js
339
232
  /**
340
233
  * Rolling window buffer for working pattern detection.
341
234
  * Prevents split-chunk issues where "Thinking" arrives as "Thin" + "king".
@@ -362,6 +255,11 @@ export class RespawnController extends EventEmitter {
362
255
  this.config = { ...DEFAULT_CONFIG, ...filteredConfig };
363
256
  // Validate configuration values
364
257
  this.validateConfig();
258
+ // Initialize sub-modules
259
+ this.adaptiveTiming = new RespawnAdaptiveTiming({
260
+ adaptiveMinConfirmMs: this.config.adaptiveMinConfirmMs ?? 5000,
261
+ adaptiveMaxConfirmMs: this.config.adaptiveMaxConfirmMs ?? 30000,
262
+ });
365
263
  this.aiChecker = new AiIdleChecker(session.id, {
366
264
  enabled: this.config.aiIdleCheckEnabled,
367
265
  model: this.config.aiIdleCheckModel,
@@ -655,7 +553,7 @@ export class RespawnController extends EventEmitter {
655
553
  if (this._state === 'stopped')
656
554
  return;
657
555
  this.lastEmittedDetectionKey = '';
658
- this.detectionUpdateTimer = setInterval(() => {
556
+ const id = this.cleanup.setInterval(() => {
659
557
  try {
660
558
  if (this._state !== 'stopped') {
661
559
  const status = this.getDetectionStatus();
@@ -671,16 +569,14 @@ export class RespawnController extends EventEmitter {
671
569
  catch (err) {
672
570
  console.error(`[RespawnController] Error in detectionUpdateTimer:`, err);
673
571
  }
674
- }, 2000);
572
+ }, 2000, { description: 'detection-update' });
573
+ this.timerIds.set('detection-update', id);
675
574
  }
676
575
  /**
677
576
  * Stop periodic detection status updates.
678
577
  */
679
578
  stopDetectionUpdates() {
680
- if (this.detectionUpdateTimer) {
681
- clearInterval(this.detectionUpdateTimer);
682
- this.detectionUpdateTimer = null;
683
- }
579
+ this.cancelTrackedTimer('detection-update');
684
580
  }
685
581
  /**
686
582
  * Transition to a new state.
@@ -791,7 +687,6 @@ export class RespawnController extends EventEmitter {
791
687
  this.aiChecker.removeAllListeners();
792
688
  this.planChecker.removeAllListeners();
793
689
  this.clearTimers();
794
- this.stopDetectionUpdates();
795
690
  this.recentActions.length = 0;
796
691
  this.setState('stopped');
797
692
  if (this.terminalHandler) {
@@ -880,7 +775,7 @@ export class RespawnController extends EventEmitter {
880
775
  this.planChecker.cancel();
881
776
  }
882
777
  // Track token count (Layer 3)
883
- const tokenCount = this.extractTokenCount(data);
778
+ const tokenCount = extractTokenCount(data);
884
779
  if (tokenCount !== null && tokenCount !== this.lastTokenCount) {
885
780
  this.lastTokenCount = tokenCount;
886
781
  this.lastTokenChangeTime = now;
@@ -888,7 +783,7 @@ export class RespawnController extends EventEmitter {
888
783
  // Detect completion message FIRST (Layer 1) - PRIMARY DETECTION
889
784
  // Check this before working patterns because completion message indicates
890
785
  // the work is done, even if working patterns are still in the rolling window
891
- if (this.isCompletionMessage(data)) {
786
+ if (isCompletionMessage(data)) {
892
787
  // Clear the rolling window - completion marks a transition point
893
788
  this.clearWorkingPatternWindow();
894
789
  this.workingDetected = false;
@@ -933,7 +828,7 @@ export class RespawnController extends EventEmitter {
933
828
  return;
934
829
  }
935
830
  // Detect working patterns (Layer 4)
936
- const isWorking = this.hasWorkingPattern(data);
831
+ const isWorking = this.checkWorkingPattern(data);
937
832
  if (isWorking) {
938
833
  this.workingDetected = true;
939
834
  this.promptDetected = false;
@@ -941,8 +836,7 @@ export class RespawnController extends EventEmitter {
941
836
  this.resetHookState(); // Clear hook signals on new work
942
837
  this.lastWorkingPatternTime = now;
943
838
  // Cancel hook confirmation timer if running
944
- this.cancelTrackedTimer('hook-confirm', this.hookConfirmTimer, 'working patterns detected');
945
- this.hookConfirmTimer = null;
839
+ this.cancelTrackedTimer('hook-confirm', 'working patterns detected');
946
840
  // Cancel any pending completion confirmation
947
841
  this.cancelCompletionConfirm();
948
842
  // Cancel any pending step confirmation (Claude is still working)
@@ -987,7 +881,7 @@ export class RespawnController extends EventEmitter {
987
881
  }
988
882
  }
989
883
  // Legacy fallback: detect prompt characters (still useful for waiting_* states)
990
- const hasPrompt = this.PROMPT_PATTERNS.some((pattern) => data.includes(pattern));
884
+ const hasPrompt = PROMPT_PATTERNS.some((pattern) => data.includes(pattern));
991
885
  if (hasPrompt) {
992
886
  this.promptDetected = true;
993
887
  this.workingDetected = false;
@@ -1037,10 +931,8 @@ export class RespawnController extends EventEmitter {
1037
931
  this.recordCycleStep('update');
1038
932
  if (this.config.sendClear) {
1039
933
  // P2-002: Check if we should skip /clear
1040
- if (this.shouldSkipClear()) {
1041
- if (this.currentCycleMetrics) {
1042
- this.currentCycleMetrics.clearSkipped = true;
1043
- }
934
+ if (this.checkShouldSkipClear()) {
935
+ this.cycleMetrics.markClearSkipped();
1044
936
  // Skip /clear, go directly to /init or complete
1045
937
  if (this.config.sendInit) {
1046
938
  this.sendInit();
@@ -1067,8 +959,7 @@ export class RespawnController extends EventEmitter {
1067
959
  */
1068
960
  checkClearComplete() {
1069
961
  // Clear the fallback timer since we got prompt detection
1070
- this.cancelTrackedTimer('clear-fallback', this.clearFallbackTimer, 'prompt detected');
1071
- this.clearFallbackTimer = null;
962
+ this.cancelTrackedTimer('clear-fallback', 'prompt detected');
1072
963
  this.logAction('step', '/clear completed');
1073
964
  this.emit('stepCompleted', 'clear');
1074
965
  // P2-004: Record step completion
@@ -1111,8 +1002,7 @@ export class RespawnController extends EventEmitter {
1111
1002
  this.workingDetected = false;
1112
1003
  this.logAction('step', 'Monitoring if /init triggered work...');
1113
1004
  // Give Claude a moment to start working before checking for idle
1114
- this.stepTimer = this.startTrackedTimer('init-monitor', 3000, () => {
1115
- this.stepTimer = null;
1005
+ this.startTrackedTimer('init-monitor', 3000, () => {
1116
1006
  // If still in monitoring state and no work detected, consider it idle
1117
1007
  if (this._state === 'monitoring_init' && !this.workingDetected) {
1118
1008
  this.checkMonitoringInitIdle();
@@ -1125,10 +1015,7 @@ export class RespawnController extends EventEmitter {
1125
1015
  * @fires stepCompleted - With step 'init'
1126
1016
  */
1127
1017
  checkMonitoringInitIdle() {
1128
- if (this.stepTimer) {
1129
- clearTimeout(this.stepTimer);
1130
- this.stepTimer = null;
1131
- }
1018
+ this.cancelTrackedTimer('init-monitor');
1132
1019
  this.log('/init did not trigger work, sending kickstart prompt');
1133
1020
  this.emit('stepCompleted', 'init');
1134
1021
  this.sendKickstart();
@@ -1141,8 +1028,7 @@ export class RespawnController extends EventEmitter {
1141
1028
  this.setState('sending_kickstart');
1142
1029
  this.terminalBuffer.clear();
1143
1030
  this.clearWorkingPatternWindow();
1144
- this.stepTimer = this.startTrackedTimer('step-delay', this.config.interStepDelayMs, async () => {
1145
- this.stepTimer = null;
1031
+ this.startTrackedTimer('step-delay', this.config.interStepDelayMs, async () => {
1146
1032
  if (this._state === 'stopped')
1147
1033
  return;
1148
1034
  const prompt = this.config.kickstartPrompt;
@@ -1167,48 +1053,10 @@ export class RespawnController extends EventEmitter {
1167
1053
  }
1168
1054
  /** Clear all timers (step, completion confirm, no-output, pre-filter, step confirm, auto-accept, hook confirm, and clear fallback) */
1169
1055
  clearTimers() {
1170
- // Clear tracked timers map first to avoid stale entries during individual cleanup
1171
1056
  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
- }
1057
+ this.timerIds.clear();
1058
+ this.cleanup.dispose();
1059
+ this.cleanup = new CleanupManager();
1212
1060
  }
1213
1061
  // ========== Stuck-State Detection Methods ==========
1214
1062
  /**
@@ -1221,20 +1069,18 @@ export class RespawnController extends EventEmitter {
1221
1069
  if (this._state === 'stopped')
1222
1070
  return;
1223
1071
  // Clear existing timer
1224
- if (this.stuckStateTimer) {
1225
- clearInterval(this.stuckStateTimer);
1226
- this.stuckStateTimer = null;
1227
- }
1072
+ this.cancelTrackedTimer('stuck-state');
1228
1073
  // Check interval for stuck state
1229
1074
  const checkIntervalMs = Math.min(this.config.stuckStateWarningMs, 60000); // Check every minute max
1230
- this.stuckStateTimer = setInterval(() => {
1075
+ const id = this.cleanup.setInterval(() => {
1231
1076
  try {
1232
1077
  this.checkStuckState();
1233
1078
  }
1234
1079
  catch (err) {
1235
1080
  console.error(`[RespawnController] Error in stuckStateTimer:`, err);
1236
1081
  }
1237
- }, checkIntervalMs);
1082
+ }, checkIntervalMs, { description: 'stuck-state' });
1083
+ this.timerIds.set('stuck-state', id);
1238
1084
  }
1239
1085
  /**
1240
1086
  * Check if the controller is stuck in the current state.
@@ -1274,7 +1120,7 @@ export class RespawnController extends EventEmitter {
1274
1120
  handleStuckStateRecovery() {
1275
1121
  const currentState = this._state;
1276
1122
  // P2-004: Complete current cycle metrics with stuck_recovery outcome
1277
- if (this.currentCycleMetrics) {
1123
+ if (this.cycleMetrics.getCurrentCycle()) {
1278
1124
  this.completeCycleMetrics('stuck_recovery', `Stuck in state: ${currentState}`);
1279
1125
  }
1280
1126
  // Cancel any running AI checks
@@ -1351,23 +1197,29 @@ export class RespawnController extends EventEmitter {
1351
1197
  * Emits timerStarted event and tracks the timer for UI display.
1352
1198
  */
1353
1199
  startTrackedTimer(name, durationMs, callback, reason) {
1200
+ // Cancel any existing timer with this name
1201
+ this.cancelTrackedTimer(name);
1354
1202
  const now = Date.now();
1355
1203
  const endsAt = now + durationMs;
1356
1204
  this.activeTimers.set(name, { name, startedAt: now, durationMs, endsAt });
1357
1205
  this.emit('timerStarted', { name, durationMs, endsAt, reason });
1358
1206
  this.logAction('timer', `Started ${name}: ${Math.round(durationMs / 1000)}s${reason ? ` (${reason})` : ''}`);
1359
- return setTimeout(() => {
1207
+ const id = this.cleanup.setTimeout(() => {
1208
+ this.timerIds.delete(name);
1360
1209
  this.activeTimers.delete(name);
1361
1210
  this.emit('timerCompleted', name);
1362
1211
  callback();
1363
- }, durationMs);
1212
+ }, durationMs, { description: name });
1213
+ this.timerIds.set(name, id);
1364
1214
  }
1365
1215
  /**
1366
1216
  * Cancel a tracked timer and emit cancellation event.
1367
1217
  */
1368
- cancelTrackedTimer(name, timerRef, reason) {
1369
- if (timerRef) {
1370
- clearTimeout(timerRef);
1218
+ cancelTrackedTimer(name, reason) {
1219
+ const id = this.timerIds.get(name);
1220
+ if (id) {
1221
+ this.cleanup.unregister(id);
1222
+ this.timerIds.delete(name);
1371
1223
  if (this.activeTimers.has(name)) {
1372
1224
  this.activeTimers.delete(name);
1373
1225
  this.emit('timerCancelled', name, reason);
@@ -1405,25 +1257,19 @@ export class RespawnController extends EventEmitter {
1405
1257
  return [...this.recentActions];
1406
1258
  }
1407
1259
  // ========== Multi-Layer Detection Methods ==========
1260
+ // Pattern detection delegated to ./respawn-patterns.js (isCompletionMessage, hasWorkingPattern, extractTokenCount)
1408
1261
  /**
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").
1262
+ * Check if data contains working patterns using the rolling window.
1263
+ * Updates the window and delegates to the pure function from respawn-patterns.
1418
1264
  */
1419
- hasWorkingPattern(data) {
1265
+ checkWorkingPattern(data) {
1420
1266
  // Always update the rolling window first to maintain continuity
1421
1267
  this.workingPatternWindow += data;
1422
1268
  if (this.workingPatternWindow.length > RespawnController.WORKING_PATTERN_WINDOW_SIZE) {
1423
1269
  this.workingPatternWindow = this.workingPatternWindow.slice(-RespawnController.WORKING_PATTERN_WINDOW_SIZE);
1424
1270
  }
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));
1271
+ // Delegate to pure function
1272
+ return hasWorkingPattern(this.workingPatternWindow);
1427
1273
  }
1428
1274
  /**
1429
1275
  * Clear the working pattern rolling window.
@@ -1432,32 +1278,14 @@ export class RespawnController extends EventEmitter {
1432
1278
  clearWorkingPatternWindow() {
1433
1279
  this.workingPatternWindow = '';
1434
1280
  }
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
1281
  /**
1452
1282
  * Start the no-output fallback timer.
1453
1283
  * If no output for noOutputTimeoutMs, triggers idle detection as safety net
1454
1284
  * (used when AI check is disabled or has too many errors).
1455
1285
  */
1456
1286
  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;
1287
+ this.cancelTrackedTimer('no-output-fallback', 'restarting');
1288
+ this.startTrackedTimer('no-output-fallback', this.config.noOutputTimeoutMs, () => {
1461
1289
  if (this._state === 'watching' || this._state === 'confirming_idle') {
1462
1290
  const msSinceOutput = Date.now() - this.lastOutputTime;
1463
1291
  this.logAction('detection', `No-output fallback: ${Math.round(msSinceOutput / 1000)}s silence`);
@@ -1486,13 +1314,11 @@ export class RespawnController extends EventEmitter {
1486
1314
  * This provides an additional path to AI check even without a completion message.
1487
1315
  */
1488
1316
  startPreFilterTimer() {
1489
- this.cancelTrackedTimer('pre-filter', this.preFilterTimer, 'restarting');
1490
- this.preFilterTimer = null;
1317
+ this.cancelTrackedTimer('pre-filter', 'restarting');
1491
1318
  // Only set up pre-filter when AI check is enabled
1492
1319
  if (!this.config.aiIdleCheckEnabled)
1493
1320
  return;
1494
- this.preFilterTimer = this.startTrackedTimer('pre-filter', this.config.completionConfirmMs, () => {
1495
- this.preFilterTimer = null;
1321
+ this.startTrackedTimer('pre-filter', this.config.completionConfirmMs, () => {
1496
1322
  if (this._state === 'watching') {
1497
1323
  const now = Date.now();
1498
1324
  const msSinceOutput = now - this.lastOutputTime;
@@ -1579,18 +1405,15 @@ export class RespawnController extends EventEmitter {
1579
1405
  }
1580
1406
  if (result.verdict === 'IDLE') {
1581
1407
  // 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;
1408
+ this.cancelTrackedTimer('completion-confirm', 'AI verdict: IDLE');
1409
+ this.cancelTrackedTimer('pre-filter', 'AI verdict: IDLE');
1586
1410
  this.logAction('ai-check', `Verdict: IDLE - ${result.reasoning}`);
1587
1411
  this.emit('aiCheckCompleted', result);
1588
1412
  this.onIdleConfirmed(`ai-check: idle (${result.reasoning})`);
1589
1413
  }
1590
1414
  else if (result.verdict === 'WORKING') {
1591
1415
  // Cancel timers and go to cooldown
1592
- this.cancelTrackedTimer('completion-confirm', this.completionConfirmTimer, 'AI verdict: WORKING');
1593
- this.completionConfirmTimer = null;
1416
+ this.cancelTrackedTimer('completion-confirm', 'AI verdict: WORKING');
1594
1417
  this.logAction('ai-check', `Verdict: WORKING - ${result.reasoning}`);
1595
1418
  this.emit('aiCheckCompleted', result);
1596
1419
  this.setState('watching');
@@ -1645,10 +1468,8 @@ export class RespawnController extends EventEmitter {
1645
1468
  * and no elicitation dialog was detected. Only handles plan mode approvals.
1646
1469
  */
1647
1470
  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;
1471
+ this.cancelTrackedTimer('auto-accept', 'restarting');
1472
+ this.startTrackedTimer('auto-accept', this.config.autoAcceptDelayMs, () => {
1652
1473
  this.tryAutoAccept();
1653
1474
  }, 'plan mode detection');
1654
1475
  }
@@ -1657,8 +1478,7 @@ export class RespawnController extends EventEmitter {
1657
1478
  * Called when a completion message is detected (normal idle flow handles it).
1658
1479
  */
1659
1480
  cancelAutoAcceptTimer() {
1660
- this.cancelTrackedTimer('auto-accept', this.autoAcceptTimer, 'cancelled');
1661
- this.autoAcceptTimer = null;
1481
+ this.cancelTrackedTimer('auto-accept', 'cancelled');
1662
1482
  }
1663
1483
  /**
1664
1484
  * Attempt to auto-accept a plan mode prompt by sending Enter.
@@ -1739,7 +1559,7 @@ export class RespawnController extends EventEmitter {
1739
1559
  // Working patterns before the selector are from earlier work and don't matter.
1740
1560
  const selectorIndex = stripped.lastIndexOf(selectorMatch[0]);
1741
1561
  const afterSelector = stripped.slice(selectorIndex + selectorMatch[0].length);
1742
- const hasWorking = this.WORKING_PATTERNS.some((pattern) => afterSelector.includes(pattern));
1562
+ const hasWorking = WORKING_PATTERNS.some((pattern) => afterSelector.includes(pattern));
1743
1563
  if (hasWorking)
1744
1564
  return false;
1745
1565
  return true;
@@ -1806,8 +1626,7 @@ export class RespawnController extends EventEmitter {
1806
1626
  this.aiChecker.cancel();
1807
1627
  }
1808
1628
  // Cancel completion confirmation - auto-accept takes precedence
1809
- this.cancelTrackedTimer('completion-confirm', this.completionConfirmTimer, 'auto-accept');
1810
- this.completionConfirmTimer = null;
1629
+ this.cancelTrackedTimer('completion-confirm', 'auto-accept');
1811
1630
  this.completionMessageTime = null;
1812
1631
  // Ensure we're in watching state (not confirming_idle or ai_checking)
1813
1632
  if (this._state !== 'watching') {
@@ -1854,11 +1673,9 @@ export class RespawnController extends EventEmitter {
1854
1673
  this.aiChecker.cancel();
1855
1674
  }
1856
1675
  // Cancel completion confirm timer - hook takes precedence
1857
- this.cancelTrackedTimer('completion-confirm', this.completionConfirmTimer, 'Stop hook received');
1858
- this.completionConfirmTimer = null;
1676
+ this.cancelTrackedTimer('completion-confirm', 'Stop hook received');
1859
1677
  // Cancel pre-filter timer - hook takes precedence
1860
- this.cancelTrackedTimer('pre-filter', this.preFilterTimer, 'Stop hook received');
1861
- this.preFilterTimer = null;
1678
+ this.cancelTrackedTimer('pre-filter', 'Stop hook received');
1862
1679
  // Start short confirmation timer to handle race conditions
1863
1680
  // (e.g., Stop hook arrives but Claude immediately starts new work)
1864
1681
  this.startHookConfirmTimer('stop');
@@ -1887,12 +1704,9 @@ export class RespawnController extends EventEmitter {
1887
1704
  this.aiChecker.cancel();
1888
1705
  }
1889
1706
  // 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;
1707
+ this.cancelTrackedTimer('completion-confirm', 'idle_prompt received');
1708
+ this.cancelTrackedTimer('pre-filter', 'idle_prompt received');
1709
+ this.cancelTrackedTimer('no-output-fallback', 'idle_prompt received');
1896
1710
  // idle_prompt is an even stronger signal than Stop hook (60s+ idle)
1897
1711
  // Skip confirmation and go directly to idle
1898
1712
  this.onIdleConfirmed('idle_prompt hook (60s+ idle)');
@@ -1904,10 +1718,8 @@ export class RespawnController extends EventEmitter {
1904
1718
  * @param hookType - Which hook triggered this ('stop' or 'idle_prompt')
1905
1719
  */
1906
1720
  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;
1721
+ this.cancelTrackedTimer('hook-confirm', 'restarting');
1722
+ this.startTrackedTimer('hook-confirm', RespawnController.HOOK_CONFIRM_DELAY_MS, () => {
1911
1723
  // Verify we haven't received new output since the hook arrived
1912
1724
  const hookTime = hookType === 'stop' ? this.stopHookTime : this.idlePromptTime;
1913
1725
  if (hookTime && this.lastOutputTime > hookTime) {
@@ -1973,12 +1785,10 @@ export class RespawnController extends EventEmitter {
1973
1785
  * After completion message, waits for output silence then triggers AI check.
1974
1786
  */
1975
1787
  startCompletionConfirmTimer() {
1976
- this.cancelTrackedTimer('completion-confirm', this.completionConfirmTimer, 'restarting');
1977
- this.completionConfirmTimer = null;
1788
+ this.cancelTrackedTimer('completion-confirm', 'restarting');
1978
1789
  this.setState('confirming_idle');
1979
1790
  this.logAction('detection', 'Completion message found in output');
1980
- this.completionConfirmTimer = this.startTrackedTimer('completion-confirm', this.config.completionConfirmMs, () => {
1981
- this.completionConfirmTimer = null;
1791
+ this.startTrackedTimer('completion-confirm', this.config.completionConfirmMs, () => {
1982
1792
  if (this._state === 'stopped')
1983
1793
  return;
1984
1794
  const msSinceOutput = Date.now() - this.lastOutputTime;
@@ -1999,8 +1809,7 @@ export class RespawnController extends EventEmitter {
1999
1809
  * Cancel completion confirmation if new activity detected.
2000
1810
  */
2001
1811
  cancelCompletionConfirm() {
2002
- this.cancelTrackedTimer('completion-confirm', this.completionConfirmTimer, 'activity detected');
2003
- this.completionConfirmTimer = null;
1812
+ this.cancelTrackedTimer('completion-confirm', 'activity detected');
2004
1813
  if (this._state === 'confirming_idle') {
2005
1814
  this.setState('watching');
2006
1815
  this.completionMessageTime = null;
@@ -2012,10 +1821,8 @@ export class RespawnController extends EventEmitter {
2012
1821
  * This ensures Claude has finished processing before we send the next command.
2013
1822
  */
2014
1823
  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;
1824
+ this.cancelTrackedTimer('step-confirm', 'restarting');
1825
+ this.startTrackedTimer('step-confirm', this.config.completionConfirmMs, () => {
2019
1826
  if (this._state === 'stopped')
2020
1827
  return;
2021
1828
  const msSinceOutput = Date.now() - this.lastOutputTime;
@@ -2045,8 +1852,7 @@ export class RespawnController extends EventEmitter {
2045
1852
  * Cancel step confirmation if working patterns detected.
2046
1853
  */
2047
1854
  cancelStepConfirm() {
2048
- this.cancelTrackedTimer('step-confirm', this.stepConfirmTimer, 'working detected');
2049
- this.stepConfirmTimer = null;
1855
+ this.cancelTrackedTimer('step-confirm', 'working detected');
2050
1856
  }
2051
1857
  /**
2052
1858
  * Called when idle is confirmed through any detection layer.
@@ -2188,8 +1994,7 @@ export class RespawnController extends EventEmitter {
2188
1994
  this.setState('sending_update');
2189
1995
  this.terminalBuffer.clear(); // Clear buffer for fresh detection
2190
1996
  this.clearWorkingPatternWindow(); // Clear rolling window
2191
- this.stepTimer = this.startTrackedTimer('step-delay', this.config.interStepDelayMs, async () => {
2192
- this.stepTimer = null;
1997
+ this.startTrackedTimer('step-delay', this.config.interStepDelayMs, async () => {
2193
1998
  if (this._state === 'stopped')
2194
1999
  return;
2195
2000
  // Use RALPH_STATUS RECOMMENDATION if available, otherwise fall back to config
@@ -2220,8 +2025,7 @@ export class RespawnController extends EventEmitter {
2220
2025
  this.setState('sending_clear');
2221
2026
  this.terminalBuffer.clear();
2222
2027
  this.clearWorkingPatternWindow();
2223
- this.stepTimer = this.startTrackedTimer('step-delay', this.config.interStepDelayMs, async () => {
2224
- this.stepTimer = null;
2028
+ this.startTrackedTimer('step-delay', this.config.interStepDelayMs, async () => {
2225
2029
  if (this._state === 'stopped')
2226
2030
  return;
2227
2031
  this.logAction('command', 'Sending: /clear');
@@ -2230,8 +2034,7 @@ export class RespawnController extends EventEmitter {
2230
2034
  this.setState('waiting_clear');
2231
2035
  this.promptDetected = false;
2232
2036
  // 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;
2037
+ this.startTrackedTimer('clear-fallback', RespawnController.CLEAR_FALLBACK_TIMEOUT_MS, () => {
2235
2038
  if (this._state === 'waiting_clear') {
2236
2039
  this.logAction('step', '/clear fallback: proceeding to /init');
2237
2040
  this.emit('stepCompleted', 'clear');
@@ -2253,8 +2056,7 @@ export class RespawnController extends EventEmitter {
2253
2056
  this.setState('sending_init');
2254
2057
  this.terminalBuffer.clear();
2255
2058
  this.clearWorkingPatternWindow();
2256
- this.stepTimer = this.startTrackedTimer('step-delay', this.config.interStepDelayMs, async () => {
2257
- this.stepTimer = null;
2059
+ this.startTrackedTimer('step-delay', this.config.interStepDelayMs, async () => {
2258
2060
  if (this._state === 'stopped')
2259
2061
  return;
2260
2062
  this.logAction('command', 'Sending: /init');
@@ -2381,7 +2183,7 @@ export class RespawnController extends EventEmitter {
2381
2183
  config: this.config,
2382
2184
  };
2383
2185
  }
2384
- // ========== P2-001: Adaptive Timing Methods ==========
2186
+ // ========== P2-001: Adaptive Timing (delegated to RespawnAdaptiveTiming) ==========
2385
2187
  /**
2386
2188
  * Get the current completion confirm timeout, potentially adjusted by adaptive timing.
2387
2189
  * Uses historical idle detection durations to calculate an optimal timeout.
@@ -2393,90 +2195,40 @@ export class RespawnController extends EventEmitter {
2393
2195
  return this.config.completionConfirmMs ?? 10000;
2394
2196
  }
2395
2197
  // Need at least 5 samples before adjusting
2396
- if (this.timingHistory.sampleCount < 5) {
2198
+ const history = this.adaptiveTiming.getTimingHistory();
2199
+ if (history.sampleCount < 5) {
2397
2200
  return this.config.completionConfirmMs ?? 10000;
2398
2201
  }
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})`);
2202
+ return this.adaptiveTiming.getAdaptiveCompletionConfirmMs();
2447
2203
  }
2448
2204
  /**
2449
2205
  * Get the current timing history for monitoring.
2450
2206
  * @returns Copy of timing history
2451
2207
  */
2452
2208
  getTimingHistory() {
2453
- return { ...this.timingHistory };
2209
+ return this.adaptiveTiming.getTimingHistory();
2454
2210
  }
2455
- // ========== P2-002: Skip-Clear Optimization Methods ==========
2211
+ // ========== P2-002: Skip-Clear Optimization (delegated to respawn-health.ts) ==========
2456
2212
  /**
2457
2213
  * Determine whether to skip the /clear step based on current context usage.
2458
2214
  * Skips if token count is below the configured threshold percentage.
2459
2215
  *
2460
2216
  * @returns True if /clear should be skipped
2461
2217
  */
2462
- shouldSkipClear() {
2218
+ checkShouldSkipClear() {
2463
2219
  if (!this.config.skipClearWhenLowContext)
2464
2220
  return false;
2465
2221
  const thresholdPercent = this.config.skipClearThresholdPercent ?? 30;
2466
2222
  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;
2223
+ const skip = shouldSkipClear(this.lastTokenCount, thresholdPercent, maxContext);
2224
+ if (skip) {
2225
+ const usagePercent = ((this.lastTokenCount / maxContext) * 100).toFixed(1);
2226
+ this.log(`Skip-clear optimization: ${usagePercent}% < ${thresholdPercent}% threshold`);
2227
+ this.logAction('optimization', `Skipping /clear (${usagePercent}% context used)`);
2476
2228
  }
2477
- return false;
2229
+ return skip;
2478
2230
  }
2479
- // ========== P2-004: Cycle Metrics Methods ==========
2231
+ // ========== P2-004: Cycle Metrics (delegated to RespawnCycleMetricsTracker) ==========
2480
2232
  /**
2481
2233
  * Start tracking metrics for a new cycle.
2482
2234
  * Called when a respawn cycle begins.
@@ -2484,28 +2236,16 @@ export class RespawnController extends EventEmitter {
2484
2236
  startCycleMetrics(idleReason) {
2485
2237
  if (!this.config.trackCycleMetrics)
2486
2238
  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
- };
2239
+ this.cycleMetrics.startCycle(this.session.id, this.cycleCount, idleReason, this.idleDetectionStartTime, this.lastTokenCount, this.getAdaptiveCompletionConfirmMs());
2500
2240
  }
2501
2241
  /**
2502
2242
  * Record a completed step in the current cycle.
2503
2243
  * @param step - Name of the step (e.g., 'update', 'clear', 'init')
2504
2244
  */
2505
2245
  recordCycleStep(step) {
2506
- if (!this.config.trackCycleMetrics || !this.currentCycleMetrics)
2246
+ if (!this.config.trackCycleMetrics)
2507
2247
  return;
2508
- this.currentCycleMetrics.stepsCompleted?.push(step);
2248
+ this.cycleMetrics.recordStep(step);
2509
2249
  }
2510
2250
  /**
2511
2251
  * Complete the current cycle metrics with outcome.
@@ -2515,78 +2255,23 @@ export class RespawnController extends EventEmitter {
2515
2255
  * @param errorMessage - Optional error message if outcome is 'error'
2516
2256
  */
2517
2257
  completeCycleMetrics(outcome, errorMessage) {
2518
- if (!this.config.trackCycleMetrics || !this.currentCycleMetrics)
2258
+ if (!this.config.trackCycleMetrics)
2519
2259
  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];
2260
+ const metrics = this.cycleMetrics.completeCycle(outcome, this.lastTokenCount, errorMessage);
2261
+ if (metrics) {
2262
+ // Record timing data for adaptive timing
2263
+ if (this.config.adaptiveTimingEnabled) {
2264
+ this.adaptiveTiming.recordTimingData(metrics.idleDetectionMs, metrics.durationMs);
2265
+ }
2266
+ this.log(`Cycle #${metrics.cycleNumber} metrics: ${outcome}, duration=${metrics.durationMs}ms, idle_detection=${metrics.idleDetectionMs}ms`);
2579
2267
  }
2580
- // Calculate success rate
2581
- agg.successRate = agg.totalCycles > 0 ? Math.round((agg.successfulCycles / agg.totalCycles) * 100) : 100;
2582
- agg.lastUpdatedAt = Date.now();
2583
2268
  }
2584
2269
  /**
2585
2270
  * Get aggregate metrics for monitoring.
2586
2271
  * @returns Copy of aggregate metrics
2587
2272
  */
2588
2273
  getAggregateMetrics() {
2589
- return { ...this.aggregateMetrics };
2274
+ return this.cycleMetrics.getAggregate();
2590
2275
  }
2591
2276
  /**
2592
2277
  * Get recent cycle metrics for analysis.
@@ -2594,9 +2279,9 @@ export class RespawnController extends EventEmitter {
2594
2279
  * @returns Recent cycle metrics, newest first
2595
2280
  */
2596
2281
  getRecentCycleMetrics(limit = 20) {
2597
- return this.recentCycleMetrics.slice(-limit).reverse();
2282
+ return this.cycleMetrics.getRecent(limit);
2598
2283
  }
2599
- // ========== P2-005: Health Score Methods ==========
2284
+ // ========== P2-005: Health Score (delegated to respawn-health.ts) ==========
2600
2285
  /**
2601
2286
  * Calculate a comprehensive health score for the Ralph Loop system.
2602
2287
  * Aggregates multiple health signals into a single score (0-100).
@@ -2604,161 +2289,27 @@ export class RespawnController extends EventEmitter {
2604
2289
  * @returns Health score with component breakdown
2605
2290
  */
2606
2291
  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
2292
  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}.`;
2293
+ const stallMetrics = tracker?.getIterationStallMetrics();
2294
+ const aiState = this.aiChecker.getState();
2295
+ const inputs = {
2296
+ aggregateMetrics: this.cycleMetrics.getAggregate(),
2297
+ circuitBreakerStatus: tracker?.circuitBreakerStatus ?? null,
2298
+ iterationStallMetrics: stallMetrics
2299
+ ? {
2300
+ stallDurationMs: stallMetrics.stallDurationMs,
2301
+ warningThresholdMs: stallMetrics.warningThresholdMs,
2302
+ criticalThresholdMs: stallMetrics.criticalThresholdMs,
2303
+ }
2304
+ : null,
2305
+ aiCheckerState: {
2306
+ status: aiState.status,
2307
+ consecutiveErrors: aiState.consecutiveErrors,
2308
+ },
2309
+ stuckRecoveryCount: this.stuckRecoveryCount,
2310
+ maxStuckRecoveries: this.config.maxStuckRecoveries ?? 3,
2311
+ };
2312
+ return calculateHealthScore(inputs);
2762
2313
  }
2763
2314
  }
2764
2315
  //# sourceMappingURL=respawn-controller.js.map