aicodeman 0.2.8 → 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 (348) 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 +14 -101
  85. package/dist/respawn-controller.d.ts.map +1 -1
  86. package/dist/respawn-controller.js +155 -594
  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 +9 -2
  117. package/dist/state-store.d.ts.map +1 -1
  118. package/dist/state-store.js +112 -39
  119. package/dist/state-store.js.map +1 -1
  120. package/dist/subagent-watcher.d.ts +16 -9
  121. package/dist/subagent-watcher.d.ts.map +1 -1
  122. package/dist/subagent-watcher.js +126 -147
  123. package/dist/subagent-watcher.js.map +1 -1
  124. package/dist/team-watcher.d.ts +3 -0
  125. package/dist/team-watcher.d.ts.map +1 -1
  126. package/dist/team-watcher.js +54 -5
  127. package/dist/team-watcher.js.map +1 -1
  128. package/dist/tmux-manager.d.ts.map +1 -1
  129. package/dist/tmux-manager.js +1 -2
  130. package/dist/tmux-manager.js.map +1 -1
  131. package/dist/tunnel-manager.d.ts +26 -0
  132. package/dist/tunnel-manager.d.ts.map +1 -1
  133. package/dist/tunnel-manager.js +127 -7
  134. package/dist/tunnel-manager.js.map +1 -1
  135. package/dist/types/api.d.ts +93 -0
  136. package/dist/types/api.d.ts.map +1 -0
  137. package/dist/types/api.js +83 -0
  138. package/dist/types/api.js.map +1 -0
  139. package/dist/types/app-state.d.ts +100 -0
  140. package/dist/types/app-state.d.ts.map +1 -0
  141. package/dist/types/app-state.js +59 -0
  142. package/dist/types/app-state.js.map +1 -0
  143. package/dist/types/common.d.ts +70 -0
  144. package/dist/types/common.d.ts.map +1 -0
  145. package/dist/types/common.js +8 -0
  146. package/dist/types/common.js.map +1 -0
  147. package/dist/types/index.d.ts +18 -0
  148. package/dist/types/index.d.ts.map +1 -0
  149. package/dist/types/index.js +18 -0
  150. package/dist/types/index.js.map +1 -0
  151. package/dist/types/lifecycle.d.ts +17 -0
  152. package/dist/types/lifecycle.d.ts.map +1 -0
  153. package/dist/types/lifecycle.js +5 -0
  154. package/dist/types/lifecycle.js.map +1 -0
  155. package/dist/types/plan.d.ts +32 -0
  156. package/dist/types/plan.d.ts.map +1 -0
  157. package/dist/types/plan.js +5 -0
  158. package/dist/types/plan.js.map +1 -0
  159. package/dist/types/push.d.ts +23 -0
  160. package/dist/types/push.d.ts.map +1 -0
  161. package/dist/types/push.js +5 -0
  162. package/dist/types/push.js.map +1 -0
  163. package/dist/types/ralph.d.ts +241 -0
  164. package/dist/types/ralph.d.ts.map +1 -0
  165. package/dist/types/ralph.js +49 -0
  166. package/dist/types/ralph.js.map +1 -0
  167. package/dist/types/respawn.d.ts +250 -0
  168. package/dist/types/respawn.d.ts.map +1 -0
  169. package/dist/types/respawn.js +5 -0
  170. package/dist/types/respawn.js.map +1 -0
  171. package/dist/types/run-summary.d.ts +81 -0
  172. package/dist/types/run-summary.d.ts.map +1 -0
  173. package/dist/types/run-summary.js +22 -0
  174. package/dist/types/run-summary.js.map +1 -0
  175. package/dist/types/session.d.ts +130 -0
  176. package/dist/types/session.d.ts.map +1 -0
  177. package/dist/types/session.js +5 -0
  178. package/dist/types/session.js.map +1 -0
  179. package/dist/types/task.d.ts +58 -0
  180. package/dist/types/task.d.ts.map +1 -0
  181. package/dist/types/task.js +5 -0
  182. package/dist/types/task.js.map +1 -0
  183. package/dist/types/teams.d.ts +55 -0
  184. package/dist/types/teams.d.ts.map +1 -0
  185. package/dist/types/teams.js +5 -0
  186. package/dist/types/teams.js.map +1 -0
  187. package/dist/types/tools.d.ts +46 -0
  188. package/dist/types/tools.d.ts.map +1 -0
  189. package/dist/types/tools.js +5 -0
  190. package/dist/types/tools.js.map +1 -0
  191. package/dist/types.d.ts +1 -1138
  192. package/dist/types.d.ts.map +1 -1
  193. package/dist/types.js +1 -214
  194. package/dist/types.js.map +1 -1
  195. package/dist/utils/claude-cli-resolver.d.ts.map +1 -1
  196. package/dist/utils/claude-cli-resolver.js +1 -2
  197. package/dist/utils/claude-cli-resolver.js.map +1 -1
  198. package/dist/utils/debouncer.d.ts +111 -0
  199. package/dist/utils/debouncer.d.ts.map +1 -0
  200. package/dist/utils/debouncer.js +162 -0
  201. package/dist/utils/debouncer.js.map +1 -0
  202. package/dist/utils/index.d.ts +3 -2
  203. package/dist/utils/index.d.ts.map +1 -1
  204. package/dist/utils/index.js +3 -2
  205. package/dist/utils/index.js.map +1 -1
  206. package/dist/utils/opencode-cli-resolver.d.ts.map +1 -1
  207. package/dist/utils/opencode-cli-resolver.js +1 -2
  208. package/dist/utils/opencode-cli-resolver.js.map +1 -1
  209. package/dist/utils/string-similarity.d.ts +0 -57
  210. package/dist/utils/string-similarity.d.ts.map +1 -1
  211. package/dist/utils/string-similarity.js +3 -18
  212. package/dist/utils/string-similarity.js.map +1 -1
  213. package/dist/web/middleware/auth.d.ts +31 -0
  214. package/dist/web/middleware/auth.d.ts.map +1 -0
  215. package/dist/web/middleware/auth.js +154 -0
  216. package/dist/web/middleware/auth.js.map +1 -0
  217. package/dist/web/ports/auth-port.d.ts +18 -0
  218. package/dist/web/ports/auth-port.d.ts.map +1 -0
  219. package/dist/web/ports/auth-port.js +6 -0
  220. package/dist/web/ports/auth-port.js.map +1 -0
  221. package/dist/web/ports/config-port.d.ts +28 -0
  222. package/dist/web/ports/config-port.d.ts.map +1 -0
  223. package/dist/web/ports/config-port.js +6 -0
  224. package/dist/web/ports/config-port.js.map +1 -0
  225. package/dist/web/ports/event-port.d.ts +13 -0
  226. package/dist/web/ports/event-port.d.ts.map +1 -0
  227. package/dist/web/ports/event-port.js +6 -0
  228. package/dist/web/ports/event-port.js.map +1 -0
  229. package/dist/web/ports/index.d.ts +14 -0
  230. package/dist/web/ports/index.d.ts.map +1 -0
  231. package/dist/web/ports/index.js +9 -0
  232. package/dist/web/ports/index.js.map +1 -0
  233. package/dist/web/ports/infra-port.d.ts +36 -0
  234. package/dist/web/ports/infra-port.d.ts.map +1 -0
  235. package/dist/web/ports/infra-port.js +6 -0
  236. package/dist/web/ports/infra-port.js.map +1 -0
  237. package/dist/web/ports/respawn-port.d.ts +20 -0
  238. package/dist/web/ports/respawn-port.d.ts.map +1 -0
  239. package/dist/web/ports/respawn-port.js +6 -0
  240. package/dist/web/ports/respawn-port.js.map +1 -0
  241. package/dist/web/ports/session-port.d.ts +15 -0
  242. package/dist/web/ports/session-port.d.ts.map +1 -0
  243. package/dist/web/ports/session-port.js +6 -0
  244. package/dist/web/ports/session-port.js.map +1 -0
  245. package/dist/web/public/api-client.js +70 -0
  246. package/dist/web/public/api-client.js.br +0 -0
  247. package/dist/web/public/api-client.js.gz +0 -0
  248. package/dist/web/public/app.js +152 -236
  249. package/dist/web/public/app.js.br +0 -0
  250. package/dist/web/public/app.js.gz +0 -0
  251. package/dist/web/public/constants.js +238 -0
  252. package/dist/web/public/constants.js.br +0 -0
  253. package/dist/web/public/constants.js.gz +0 -0
  254. package/dist/web/public/index.html +11 -3
  255. package/dist/web/public/index.html.br +0 -0
  256. package/dist/web/public/index.html.gz +0 -0
  257. package/dist/web/public/keyboard-accessory.js +279 -0
  258. package/dist/web/public/keyboard-accessory.js.br +0 -0
  259. package/dist/web/public/keyboard-accessory.js.gz +0 -0
  260. package/dist/web/public/mobile-handlers.js +467 -0
  261. package/dist/web/public/mobile-handlers.js.br +0 -0
  262. package/dist/web/public/mobile-handlers.js.gz +0 -0
  263. package/dist/web/public/mobile.css.gz +0 -0
  264. package/dist/web/public/notification-manager.js +445 -0
  265. package/dist/web/public/notification-manager.js.br +0 -0
  266. package/dist/web/public/notification-manager.js.gz +0 -0
  267. package/dist/web/public/ralph-wizard.js +3 -3
  268. package/dist/web/public/ralph-wizard.js.br +0 -0
  269. package/dist/web/public/ralph-wizard.js.gz +0 -0
  270. package/dist/web/public/styles.css.gz +0 -0
  271. package/dist/web/public/subagent-windows.js +1115 -0
  272. package/dist/web/public/subagent-windows.js.br +0 -0
  273. package/dist/web/public/subagent-windows.js.gz +0 -0
  274. package/dist/web/public/sw.js.gz +0 -0
  275. package/dist/web/public/upload.html.gz +0 -0
  276. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  277. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  278. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  279. package/dist/web/public/vendor/xterm.css.gz +0 -0
  280. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  281. package/dist/web/public/voice-input.js +858 -0
  282. package/dist/web/public/voice-input.js.br +0 -0
  283. package/dist/web/public/voice-input.js.gz +0 -0
  284. package/dist/web/route-helpers.d.ts +38 -0
  285. package/dist/web/route-helpers.d.ts.map +1 -0
  286. package/dist/web/route-helpers.js +143 -0
  287. package/dist/web/route-helpers.js.map +1 -0
  288. package/dist/web/routes/case-routes.d.ts +9 -0
  289. package/dist/web/routes/case-routes.d.ts.map +1 -0
  290. package/dist/web/routes/case-routes.js +419 -0
  291. package/dist/web/routes/case-routes.js.map +1 -0
  292. package/dist/web/routes/file-routes.d.ts +8 -0
  293. package/dist/web/routes/file-routes.d.ts.map +1 -0
  294. package/dist/web/routes/file-routes.js +337 -0
  295. package/dist/web/routes/file-routes.js.map +1 -0
  296. package/dist/web/routes/hook-event-routes.d.ts +9 -0
  297. package/dist/web/routes/hook-event-routes.d.ts.map +1 -0
  298. package/dist/web/routes/hook-event-routes.js +57 -0
  299. package/dist/web/routes/hook-event-routes.js.map +1 -0
  300. package/dist/web/routes/index.d.ts +16 -0
  301. package/dist/web/routes/index.d.ts.map +1 -0
  302. package/dist/web/routes/index.js +16 -0
  303. package/dist/web/routes/index.js.map +1 -0
  304. package/dist/web/routes/mux-routes.d.ts +8 -0
  305. package/dist/web/routes/mux-routes.d.ts.map +1 -0
  306. package/dist/web/routes/mux-routes.js +32 -0
  307. package/dist/web/routes/mux-routes.js.map +1 -0
  308. package/dist/web/routes/plan-routes.d.ts +9 -0
  309. package/dist/web/routes/plan-routes.d.ts.map +1 -0
  310. package/dist/web/routes/plan-routes.js +381 -0
  311. package/dist/web/routes/plan-routes.js.map +1 -0
  312. package/dist/web/routes/push-routes.d.ts +8 -0
  313. package/dist/web/routes/push-routes.d.ts.map +1 -0
  314. package/dist/web/routes/push-routes.js +49 -0
  315. package/dist/web/routes/push-routes.js.map +1 -0
  316. package/dist/web/routes/ralph-routes.d.ts +9 -0
  317. package/dist/web/routes/ralph-routes.d.ts.map +1 -0
  318. package/dist/web/routes/ralph-routes.js +475 -0
  319. package/dist/web/routes/ralph-routes.js.map +1 -0
  320. package/dist/web/routes/respawn-routes.d.ts +8 -0
  321. package/dist/web/routes/respawn-routes.d.ts.map +1 -0
  322. package/dist/web/routes/respawn-routes.js +260 -0
  323. package/dist/web/routes/respawn-routes.js.map +1 -0
  324. package/dist/web/routes/scheduled-routes.d.ts +8 -0
  325. package/dist/web/routes/scheduled-routes.d.ts.map +1 -0
  326. package/dist/web/routes/scheduled-routes.js +51 -0
  327. package/dist/web/routes/scheduled-routes.js.map +1 -0
  328. package/dist/web/routes/session-routes.d.ts +9 -0
  329. package/dist/web/routes/session-routes.d.ts.map +1 -0
  330. package/dist/web/routes/session-routes.js +729 -0
  331. package/dist/web/routes/session-routes.js.map +1 -0
  332. package/dist/web/routes/system-routes.d.ts +9 -0
  333. package/dist/web/routes/system-routes.d.ts.map +1 -0
  334. package/dist/web/routes/system-routes.js +678 -0
  335. package/dist/web/routes/system-routes.js.map +1 -0
  336. package/dist/web/routes/team-routes.d.ts +8 -0
  337. package/dist/web/routes/team-routes.d.ts.map +1 -0
  338. package/dist/web/routes/team-routes.js +14 -0
  339. package/dist/web/routes/team-routes.js.map +1 -0
  340. package/dist/web/schemas.d.ts +43 -3
  341. package/dist/web/schemas.d.ts.map +1 -1
  342. package/dist/web/schemas.js +6 -2
  343. package/dist/web/schemas.js.map +1 -1
  344. package/dist/web/server.d.ts +10 -9
  345. package/dist/web/server.d.ts.map +1 -1
  346. package/dist/web/server.js +342 -3829
  347. package/dist/web/server.js.map +1 -1
  348. 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,18 +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;
168
- /** Timer for auto-accepting plan mode prompts */
169
- autoAcceptTimer = null;
170
- /** Timer for pre-filter silence detection (triggers AI check) */
171
- preFilterTimer = 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();
161
+ /** Cached key fields from last emitted detection status (for dedup) */
162
+ lastEmittedDetectionKey = '';
172
163
  /** Whether any terminal output has been received since start/last-auto-accept */
173
164
  hasReceivedOutput = false;
174
165
  /** Whether an elicitation dialog (AskUserQuestion) was detected via hook signal */
@@ -182,8 +173,6 @@ export class RespawnController extends EventEmitter {
182
173
  idlePromptReceived = false;
183
174
  /** Timestamp when idle_prompt was received */
184
175
  idlePromptTime = null;
185
- /** Timer for short confirmation after hook signal (handles race conditions) */
186
- hookConfirmTimer = null;
187
176
  /** Confirmation delay after hook signal before confirming idle (ms) */
188
177
  static HOOK_CONFIRM_DELAY_MS = 3000;
189
178
  /** Number of completed respawn cycles */
@@ -206,10 +195,6 @@ export class RespawnController extends EventEmitter {
206
195
  planCheckStartTime = 0;
207
196
  /** Unique ID for current AI check request (to detect stale results) */
208
197
  _currentAiCheckId = null;
209
- /** Timer for /clear step fallback (sends /init if no prompt detected) */
210
- clearFallbackTimer = null;
211
- /** Timer for step completion confirmation (waits for silence after completion) */
212
- stepConfirmTimer = null;
213
198
  /** Fallback timeout for /clear step (ms) - sends /init without waiting for prompt */
214
199
  static CLEAR_FALLBACK_TIMEOUT_MS = 10000;
215
200
  // ========== Timer Tracking for UI Countdown Display ==========
@@ -220,44 +205,18 @@ export class RespawnController extends EventEmitter {
220
205
  // ========== Stuck-State Detection State ==========
221
206
  /** Timestamp when the current state was entered */
222
207
  stateEnteredAt = 0;
223
- /** Timer for stuck-state detection */
224
- stuckStateTimer = null;
225
208
  /** Whether a stuck-state warning has been emitted for current state */
226
209
  stuckStateWarned = false;
227
210
  /** Number of stuck-state recovery attempts */
228
211
  stuckRecoveryCount = 0;
229
- // ========== P2-001: Adaptive Timing State ==========
230
- /** Historical timing data for adaptive adjustments */
231
- timingHistory = {
232
- recentIdleDetectionMs: [],
233
- recentCycleDurationMs: [],
234
- adaptiveCompletionConfirmMs: 10000, // Start with default
235
- sampleCount: 0,
236
- maxSamples: 20, // Keep last 20 samples for rolling average
237
- lastUpdatedAt: Date.now(),
238
- };
239
- // ========== P2-004: Cycle Metrics State ==========
240
- /** Current cycle being tracked */
241
- 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();
242
218
  /** Timestamp when idle detection started for current cycle */
243
219
  idleDetectionStartTime = 0;
244
- /** Recent cycle metrics (rolling window for aggregate calculation) */
245
- recentCycleMetrics = [];
246
- /** Maximum number of cycle metrics to keep in memory */
247
- static MAX_CYCLE_METRICS_IN_MEMORY = 100;
248
- /** Aggregate metrics across all tracked cycles */
249
- aggregateMetrics = {
250
- totalCycles: 0,
251
- successfulCycles: 0,
252
- stuckRecoveryCycles: 0,
253
- blockedCycles: 0,
254
- errorCycles: 0,
255
- avgCycleDurationMs: 0,
256
- avgIdleDetectionMs: 0,
257
- p90CycleDurationMs: 0,
258
- successRate: 100,
259
- lastUpdatedAt: Date.now(),
260
- };
261
220
  // ========== Multi-Layer Detection State ==========
262
221
  /** Layer 1: Timestamp when completion message was detected */
263
222
  completionMessageTime = null;
@@ -269,71 +228,7 @@ export class RespawnController extends EventEmitter {
269
228
  lastTokenChangeTime = 0;
270
229
  /** Layer 4: Timestamp when last working pattern was seen */
271
230
  lastWorkingPatternTime = 0;
272
- /**
273
- * Patterns indicating Claude is ready for input (legacy fallback).
274
- * Used as secondary signals, not primary detection.
275
- */
276
- PROMPT_PATTERNS = [
277
- '❯', // Standard prompt
278
- '\u276f', // Unicode variant
279
- '⏵', // Claude Code prompt variant
280
- ];
281
- /**
282
- * Patterns indicating Claude is actively working.
283
- * When detected, resets all idle detection timers.
284
- * Note: ✻ and ✽ removed - they appear in completion messages too.
285
- */
286
- WORKING_PATTERNS = [
287
- 'Thinking',
288
- 'Writing',
289
- 'Reading',
290
- 'Running',
291
- 'Searching',
292
- 'Editing',
293
- 'Creating',
294
- 'Deleting',
295
- 'Analyzing',
296
- 'Executing',
297
- 'Synthesizing',
298
- 'Brewing', // Claude's processing indicators
299
- 'Compiling',
300
- 'Building',
301
- 'Installing',
302
- 'Fetching',
303
- 'Downloading',
304
- 'Processing',
305
- 'Generating',
306
- 'Loading',
307
- 'Starting',
308
- 'Updating',
309
- 'Checking',
310
- 'Validating',
311
- 'Testing',
312
- 'Formatting',
313
- 'Linting',
314
- '⠋',
315
- '⠙',
316
- '⠹',
317
- '⠸',
318
- '⠼',
319
- '⠴',
320
- '⠦',
321
- '⠧',
322
- '⠇',
323
- '⠏', // Spinner chars
324
- '◐',
325
- '◓',
326
- '◑',
327
- '◒', // Alternative spinners
328
- '⣾',
329
- '⣽',
330
- '⣻',
331
- '⢿',
332
- '⡿',
333
- '⣟',
334
- '⣯',
335
- '⣷', // Braille spinners
336
- ];
231
+ // PROMPT_PATTERNS and WORKING_PATTERNS are now imported from ./respawn-patterns.js
337
232
  /**
338
233
  * Rolling window buffer for working pattern detection.
339
234
  * Prevents split-chunk issues where "Thinking" arrives as "Thin" + "king".
@@ -360,6 +255,11 @@ export class RespawnController extends EventEmitter {
360
255
  this.config = { ...DEFAULT_CONFIG, ...filteredConfig };
361
256
  // Validate configuration values
362
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
+ });
363
263
  this.aiChecker = new AiIdleChecker(session.id, {
364
264
  enabled: this.config.aiIdleCheckEnabled,
365
265
  model: this.config.aiIdleCheckModel,
@@ -652,25 +552,31 @@ export class RespawnController extends EventEmitter {
652
552
  this.stopDetectionUpdates();
653
553
  if (this._state === 'stopped')
654
554
  return;
655
- this.detectionUpdateTimer = setInterval(() => {
555
+ this.lastEmittedDetectionKey = '';
556
+ const id = this.cleanup.setInterval(() => {
656
557
  try {
657
558
  if (this._state !== 'stopped') {
658
- this.emit('detectionUpdate', this.getDetectionStatus());
559
+ const status = this.getDetectionStatus();
560
+ // Only emit when status meaningfully changed (confidence, state text, or timer values)
561
+ // to avoid broadcasting identical data every 2s for stable/idle sessions.
562
+ const key = `${status.confidenceLevel}|${status.statusText}|${this._state}`;
563
+ if (key !== this.lastEmittedDetectionKey) {
564
+ this.lastEmittedDetectionKey = key;
565
+ this.emit('detectionUpdate', status);
566
+ }
659
567
  }
660
568
  }
661
569
  catch (err) {
662
570
  console.error(`[RespawnController] Error in detectionUpdateTimer:`, err);
663
571
  }
664
- }, 2000);
572
+ }, 2000, { description: 'detection-update' });
573
+ this.timerIds.set('detection-update', id);
665
574
  }
666
575
  /**
667
576
  * Stop periodic detection status updates.
668
577
  */
669
578
  stopDetectionUpdates() {
670
- if (this.detectionUpdateTimer) {
671
- clearInterval(this.detectionUpdateTimer);
672
- this.detectionUpdateTimer = null;
673
- }
579
+ this.cancelTrackedTimer('detection-update');
674
580
  }
675
581
  /**
676
582
  * Transition to a new state.
@@ -781,7 +687,6 @@ export class RespawnController extends EventEmitter {
781
687
  this.aiChecker.removeAllListeners();
782
688
  this.planChecker.removeAllListeners();
783
689
  this.clearTimers();
784
- this.stopDetectionUpdates();
785
690
  this.recentActions.length = 0;
786
691
  this.setState('stopped');
787
692
  if (this.terminalHandler) {
@@ -870,7 +775,7 @@ export class RespawnController extends EventEmitter {
870
775
  this.planChecker.cancel();
871
776
  }
872
777
  // Track token count (Layer 3)
873
- const tokenCount = this.extractTokenCount(data);
778
+ const tokenCount = extractTokenCount(data);
874
779
  if (tokenCount !== null && tokenCount !== this.lastTokenCount) {
875
780
  this.lastTokenCount = tokenCount;
876
781
  this.lastTokenChangeTime = now;
@@ -878,7 +783,7 @@ export class RespawnController extends EventEmitter {
878
783
  // Detect completion message FIRST (Layer 1) - PRIMARY DETECTION
879
784
  // Check this before working patterns because completion message indicates
880
785
  // the work is done, even if working patterns are still in the rolling window
881
- if (this.isCompletionMessage(data)) {
786
+ if (isCompletionMessage(data)) {
882
787
  // Clear the rolling window - completion marks a transition point
883
788
  this.clearWorkingPatternWindow();
884
789
  this.workingDetected = false;
@@ -923,7 +828,7 @@ export class RespawnController extends EventEmitter {
923
828
  return;
924
829
  }
925
830
  // Detect working patterns (Layer 4)
926
- const isWorking = this.hasWorkingPattern(data);
831
+ const isWorking = this.checkWorkingPattern(data);
927
832
  if (isWorking) {
928
833
  this.workingDetected = true;
929
834
  this.promptDetected = false;
@@ -931,8 +836,7 @@ export class RespawnController extends EventEmitter {
931
836
  this.resetHookState(); // Clear hook signals on new work
932
837
  this.lastWorkingPatternTime = now;
933
838
  // Cancel hook confirmation timer if running
934
- this.cancelTrackedTimer('hook-confirm', this.hookConfirmTimer, 'working patterns detected');
935
- this.hookConfirmTimer = null;
839
+ this.cancelTrackedTimer('hook-confirm', 'working patterns detected');
936
840
  // Cancel any pending completion confirmation
937
841
  this.cancelCompletionConfirm();
938
842
  // Cancel any pending step confirmation (Claude is still working)
@@ -977,7 +881,7 @@ export class RespawnController extends EventEmitter {
977
881
  }
978
882
  }
979
883
  // Legacy fallback: detect prompt characters (still useful for waiting_* states)
980
- const hasPrompt = this.PROMPT_PATTERNS.some((pattern) => data.includes(pattern));
884
+ const hasPrompt = PROMPT_PATTERNS.some((pattern) => data.includes(pattern));
981
885
  if (hasPrompt) {
982
886
  this.promptDetected = true;
983
887
  this.workingDetected = false;
@@ -1027,10 +931,8 @@ export class RespawnController extends EventEmitter {
1027
931
  this.recordCycleStep('update');
1028
932
  if (this.config.sendClear) {
1029
933
  // P2-002: Check if we should skip /clear
1030
- if (this.shouldSkipClear()) {
1031
- if (this.currentCycleMetrics) {
1032
- this.currentCycleMetrics.clearSkipped = true;
1033
- }
934
+ if (this.checkShouldSkipClear()) {
935
+ this.cycleMetrics.markClearSkipped();
1034
936
  // Skip /clear, go directly to /init or complete
1035
937
  if (this.config.sendInit) {
1036
938
  this.sendInit();
@@ -1057,8 +959,7 @@ export class RespawnController extends EventEmitter {
1057
959
  */
1058
960
  checkClearComplete() {
1059
961
  // Clear the fallback timer since we got prompt detection
1060
- this.cancelTrackedTimer('clear-fallback', this.clearFallbackTimer, 'prompt detected');
1061
- this.clearFallbackTimer = null;
962
+ this.cancelTrackedTimer('clear-fallback', 'prompt detected');
1062
963
  this.logAction('step', '/clear completed');
1063
964
  this.emit('stepCompleted', 'clear');
1064
965
  // P2-004: Record step completion
@@ -1101,8 +1002,7 @@ export class RespawnController extends EventEmitter {
1101
1002
  this.workingDetected = false;
1102
1003
  this.logAction('step', 'Monitoring if /init triggered work...');
1103
1004
  // Give Claude a moment to start working before checking for idle
1104
- this.stepTimer = this.startTrackedTimer('init-monitor', 3000, () => {
1105
- this.stepTimer = null;
1005
+ this.startTrackedTimer('init-monitor', 3000, () => {
1106
1006
  // If still in monitoring state and no work detected, consider it idle
1107
1007
  if (this._state === 'monitoring_init' && !this.workingDetected) {
1108
1008
  this.checkMonitoringInitIdle();
@@ -1115,10 +1015,7 @@ export class RespawnController extends EventEmitter {
1115
1015
  * @fires stepCompleted - With step 'init'
1116
1016
  */
1117
1017
  checkMonitoringInitIdle() {
1118
- if (this.stepTimer) {
1119
- clearTimeout(this.stepTimer);
1120
- this.stepTimer = null;
1121
- }
1018
+ this.cancelTrackedTimer('init-monitor');
1122
1019
  this.log('/init did not trigger work, sending kickstart prompt');
1123
1020
  this.emit('stepCompleted', 'init');
1124
1021
  this.sendKickstart();
@@ -1131,8 +1028,7 @@ export class RespawnController extends EventEmitter {
1131
1028
  this.setState('sending_kickstart');
1132
1029
  this.terminalBuffer.clear();
1133
1030
  this.clearWorkingPatternWindow();
1134
- this.stepTimer = this.startTrackedTimer('step-delay', this.config.interStepDelayMs, async () => {
1135
- this.stepTimer = null;
1031
+ this.startTrackedTimer('step-delay', this.config.interStepDelayMs, async () => {
1136
1032
  if (this._state === 'stopped')
1137
1033
  return;
1138
1034
  const prompt = this.config.kickstartPrompt;
@@ -1157,48 +1053,10 @@ export class RespawnController extends EventEmitter {
1157
1053
  }
1158
1054
  /** Clear all timers (step, completion confirm, no-output, pre-filter, step confirm, auto-accept, hook confirm, and clear fallback) */
1159
1055
  clearTimers() {
1160
- // Clear tracked timers map first to avoid stale entries during individual cleanup
1161
1056
  this.activeTimers.clear();
1162
- if (this.stepTimer) {
1163
- clearTimeout(this.stepTimer);
1164
- this.stepTimer = null;
1165
- }
1166
- if (this.clearFallbackTimer) {
1167
- clearTimeout(this.clearFallbackTimer);
1168
- this.clearFallbackTimer = null;
1169
- }
1170
- if (this.completionConfirmTimer) {
1171
- clearTimeout(this.completionConfirmTimer);
1172
- this.completionConfirmTimer = null;
1173
- }
1174
- if (this.stepConfirmTimer) {
1175
- clearTimeout(this.stepConfirmTimer);
1176
- this.stepConfirmTimer = null;
1177
- }
1178
- if (this.autoAcceptTimer) {
1179
- clearTimeout(this.autoAcceptTimer);
1180
- this.autoAcceptTimer = null;
1181
- }
1182
- if (this.preFilterTimer) {
1183
- clearTimeout(this.preFilterTimer);
1184
- this.preFilterTimer = null;
1185
- }
1186
- if (this.noOutputTimer) {
1187
- clearTimeout(this.noOutputTimer);
1188
- this.noOutputTimer = null;
1189
- }
1190
- if (this.hookConfirmTimer) {
1191
- clearTimeout(this.hookConfirmTimer);
1192
- this.hookConfirmTimer = null;
1193
- }
1194
- if (this.stuckStateTimer) {
1195
- clearInterval(this.stuckStateTimer);
1196
- this.stuckStateTimer = null;
1197
- }
1198
- if (this.detectionUpdateTimer) {
1199
- clearInterval(this.detectionUpdateTimer);
1200
- this.detectionUpdateTimer = null;
1201
- }
1057
+ this.timerIds.clear();
1058
+ this.cleanup.dispose();
1059
+ this.cleanup = new CleanupManager();
1202
1060
  }
1203
1061
  // ========== Stuck-State Detection Methods ==========
1204
1062
  /**
@@ -1211,20 +1069,18 @@ export class RespawnController extends EventEmitter {
1211
1069
  if (this._state === 'stopped')
1212
1070
  return;
1213
1071
  // Clear existing timer
1214
- if (this.stuckStateTimer) {
1215
- clearInterval(this.stuckStateTimer);
1216
- this.stuckStateTimer = null;
1217
- }
1072
+ this.cancelTrackedTimer('stuck-state');
1218
1073
  // Check interval for stuck state
1219
1074
  const checkIntervalMs = Math.min(this.config.stuckStateWarningMs, 60000); // Check every minute max
1220
- this.stuckStateTimer = setInterval(() => {
1075
+ const id = this.cleanup.setInterval(() => {
1221
1076
  try {
1222
1077
  this.checkStuckState();
1223
1078
  }
1224
1079
  catch (err) {
1225
1080
  console.error(`[RespawnController] Error in stuckStateTimer:`, err);
1226
1081
  }
1227
- }, checkIntervalMs);
1082
+ }, checkIntervalMs, { description: 'stuck-state' });
1083
+ this.timerIds.set('stuck-state', id);
1228
1084
  }
1229
1085
  /**
1230
1086
  * Check if the controller is stuck in the current state.
@@ -1264,7 +1120,7 @@ export class RespawnController extends EventEmitter {
1264
1120
  handleStuckStateRecovery() {
1265
1121
  const currentState = this._state;
1266
1122
  // P2-004: Complete current cycle metrics with stuck_recovery outcome
1267
- if (this.currentCycleMetrics) {
1123
+ if (this.cycleMetrics.getCurrentCycle()) {
1268
1124
  this.completeCycleMetrics('stuck_recovery', `Stuck in state: ${currentState}`);
1269
1125
  }
1270
1126
  // Cancel any running AI checks
@@ -1341,23 +1197,29 @@ export class RespawnController extends EventEmitter {
1341
1197
  * Emits timerStarted event and tracks the timer for UI display.
1342
1198
  */
1343
1199
  startTrackedTimer(name, durationMs, callback, reason) {
1200
+ // Cancel any existing timer with this name
1201
+ this.cancelTrackedTimer(name);
1344
1202
  const now = Date.now();
1345
1203
  const endsAt = now + durationMs;
1346
1204
  this.activeTimers.set(name, { name, startedAt: now, durationMs, endsAt });
1347
1205
  this.emit('timerStarted', { name, durationMs, endsAt, reason });
1348
1206
  this.logAction('timer', `Started ${name}: ${Math.round(durationMs / 1000)}s${reason ? ` (${reason})` : ''}`);
1349
- return setTimeout(() => {
1207
+ const id = this.cleanup.setTimeout(() => {
1208
+ this.timerIds.delete(name);
1350
1209
  this.activeTimers.delete(name);
1351
1210
  this.emit('timerCompleted', name);
1352
1211
  callback();
1353
- }, durationMs);
1212
+ }, durationMs, { description: name });
1213
+ this.timerIds.set(name, id);
1354
1214
  }
1355
1215
  /**
1356
1216
  * Cancel a tracked timer and emit cancellation event.
1357
1217
  */
1358
- cancelTrackedTimer(name, timerRef, reason) {
1359
- if (timerRef) {
1360
- 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);
1361
1223
  if (this.activeTimers.has(name)) {
1362
1224
  this.activeTimers.delete(name);
1363
1225
  this.emit('timerCancelled', name, reason);
@@ -1395,25 +1257,19 @@ export class RespawnController extends EventEmitter {
1395
1257
  return [...this.recentActions];
1396
1258
  }
1397
1259
  // ========== Multi-Layer Detection Methods ==========
1260
+ // Pattern detection delegated to ./respawn-patterns.js (isCompletionMessage, hasWorkingPattern, extractTokenCount)
1398
1261
  /**
1399
- * Check if data contains a completion message pattern.
1400
- * Matches "for Xh Xm Xs" time duration patterns.
1401
- */
1402
- isCompletionMessage(data) {
1403
- return COMPLETION_TIME_PATTERN.test(data);
1404
- }
1405
- /**
1406
- * Check if data contains working patterns.
1407
- * 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.
1408
1264
  */
1409
- hasWorkingPattern(data) {
1265
+ checkWorkingPattern(data) {
1410
1266
  // Always update the rolling window first to maintain continuity
1411
1267
  this.workingPatternWindow += data;
1412
1268
  if (this.workingPatternWindow.length > RespawnController.WORKING_PATTERN_WINDOW_SIZE) {
1413
1269
  this.workingPatternWindow = this.workingPatternWindow.slice(-RespawnController.WORKING_PATTERN_WINDOW_SIZE);
1414
1270
  }
1415
- // Check the rolling window (includes current data, catches both complete and split patterns)
1416
- return this.WORKING_PATTERNS.some((pattern) => this.workingPatternWindow.includes(pattern));
1271
+ // Delegate to pure function
1272
+ return hasWorkingPattern(this.workingPatternWindow);
1417
1273
  }
1418
1274
  /**
1419
1275
  * Clear the working pattern rolling window.
@@ -1422,32 +1278,14 @@ export class RespawnController extends EventEmitter {
1422
1278
  clearWorkingPatternWindow() {
1423
1279
  this.workingPatternWindow = '';
1424
1280
  }
1425
- /**
1426
- * Extract token count from data if present.
1427
- * Returns null if no token pattern found.
1428
- */
1429
- extractTokenCount(data) {
1430
- const match = data.match(TOKEN_PATTERN);
1431
- if (!match)
1432
- return null;
1433
- let count = parseFloat(match[1]);
1434
- const suffix = match[2]?.toLowerCase();
1435
- if (suffix === 'k')
1436
- count *= 1000;
1437
- else if (suffix === 'm')
1438
- count *= 1000000;
1439
- return Math.round(count);
1440
- }
1441
1281
  /**
1442
1282
  * Start the no-output fallback timer.
1443
1283
  * If no output for noOutputTimeoutMs, triggers idle detection as safety net
1444
1284
  * (used when AI check is disabled or has too many errors).
1445
1285
  */
1446
1286
  startNoOutputTimer() {
1447
- this.cancelTrackedTimer('no-output-fallback', this.noOutputTimer, 'restarting');
1448
- this.noOutputTimer = null;
1449
- this.noOutputTimer = this.startTrackedTimer('no-output-fallback', this.config.noOutputTimeoutMs, () => {
1450
- this.noOutputTimer = null;
1287
+ this.cancelTrackedTimer('no-output-fallback', 'restarting');
1288
+ this.startTrackedTimer('no-output-fallback', this.config.noOutputTimeoutMs, () => {
1451
1289
  if (this._state === 'watching' || this._state === 'confirming_idle') {
1452
1290
  const msSinceOutput = Date.now() - this.lastOutputTime;
1453
1291
  this.logAction('detection', `No-output fallback: ${Math.round(msSinceOutput / 1000)}s silence`);
@@ -1476,13 +1314,11 @@ export class RespawnController extends EventEmitter {
1476
1314
  * This provides an additional path to AI check even without a completion message.
1477
1315
  */
1478
1316
  startPreFilterTimer() {
1479
- this.cancelTrackedTimer('pre-filter', this.preFilterTimer, 'restarting');
1480
- this.preFilterTimer = null;
1317
+ this.cancelTrackedTimer('pre-filter', 'restarting');
1481
1318
  // Only set up pre-filter when AI check is enabled
1482
1319
  if (!this.config.aiIdleCheckEnabled)
1483
1320
  return;
1484
- this.preFilterTimer = this.startTrackedTimer('pre-filter', this.config.completionConfirmMs, () => {
1485
- this.preFilterTimer = null;
1321
+ this.startTrackedTimer('pre-filter', this.config.completionConfirmMs, () => {
1486
1322
  if (this._state === 'watching') {
1487
1323
  const now = Date.now();
1488
1324
  const msSinceOutput = now - this.lastOutputTime;
@@ -1569,18 +1405,15 @@ export class RespawnController extends EventEmitter {
1569
1405
  }
1570
1406
  if (result.verdict === 'IDLE') {
1571
1407
  // Cancel any pending confirmation timers - AI has spoken
1572
- this.cancelTrackedTimer('completion-confirm', this.completionConfirmTimer, 'AI verdict: IDLE');
1573
- this.completionConfirmTimer = null;
1574
- this.cancelTrackedTimer('pre-filter', this.preFilterTimer, 'AI verdict: IDLE');
1575
- this.preFilterTimer = null;
1408
+ this.cancelTrackedTimer('completion-confirm', 'AI verdict: IDLE');
1409
+ this.cancelTrackedTimer('pre-filter', 'AI verdict: IDLE');
1576
1410
  this.logAction('ai-check', `Verdict: IDLE - ${result.reasoning}`);
1577
1411
  this.emit('aiCheckCompleted', result);
1578
1412
  this.onIdleConfirmed(`ai-check: idle (${result.reasoning})`);
1579
1413
  }
1580
1414
  else if (result.verdict === 'WORKING') {
1581
1415
  // Cancel timers and go to cooldown
1582
- this.cancelTrackedTimer('completion-confirm', this.completionConfirmTimer, 'AI verdict: WORKING');
1583
- this.completionConfirmTimer = null;
1416
+ this.cancelTrackedTimer('completion-confirm', 'AI verdict: WORKING');
1584
1417
  this.logAction('ai-check', `Verdict: WORKING - ${result.reasoning}`);
1585
1418
  this.emit('aiCheckCompleted', result);
1586
1419
  this.setState('watching');
@@ -1635,10 +1468,8 @@ export class RespawnController extends EventEmitter {
1635
1468
  * and no elicitation dialog was detected. Only handles plan mode approvals.
1636
1469
  */
1637
1470
  startAutoAcceptTimer() {
1638
- this.cancelTrackedTimer('auto-accept', this.autoAcceptTimer, 'restarting');
1639
- this.autoAcceptTimer = null;
1640
- this.autoAcceptTimer = this.startTrackedTimer('auto-accept', this.config.autoAcceptDelayMs, () => {
1641
- this.autoAcceptTimer = null;
1471
+ this.cancelTrackedTimer('auto-accept', 'restarting');
1472
+ this.startTrackedTimer('auto-accept', this.config.autoAcceptDelayMs, () => {
1642
1473
  this.tryAutoAccept();
1643
1474
  }, 'plan mode detection');
1644
1475
  }
@@ -1647,8 +1478,7 @@ export class RespawnController extends EventEmitter {
1647
1478
  * Called when a completion message is detected (normal idle flow handles it).
1648
1479
  */
1649
1480
  cancelAutoAcceptTimer() {
1650
- this.cancelTrackedTimer('auto-accept', this.autoAcceptTimer, 'cancelled');
1651
- this.autoAcceptTimer = null;
1481
+ this.cancelTrackedTimer('auto-accept', 'cancelled');
1652
1482
  }
1653
1483
  /**
1654
1484
  * Attempt to auto-accept a plan mode prompt by sending Enter.
@@ -1729,7 +1559,7 @@ export class RespawnController extends EventEmitter {
1729
1559
  // Working patterns before the selector are from earlier work and don't matter.
1730
1560
  const selectorIndex = stripped.lastIndexOf(selectorMatch[0]);
1731
1561
  const afterSelector = stripped.slice(selectorIndex + selectorMatch[0].length);
1732
- const hasWorking = this.WORKING_PATTERNS.some((pattern) => afterSelector.includes(pattern));
1562
+ const hasWorking = WORKING_PATTERNS.some((pattern) => afterSelector.includes(pattern));
1733
1563
  if (hasWorking)
1734
1564
  return false;
1735
1565
  return true;
@@ -1796,8 +1626,7 @@ export class RespawnController extends EventEmitter {
1796
1626
  this.aiChecker.cancel();
1797
1627
  }
1798
1628
  // Cancel completion confirmation - auto-accept takes precedence
1799
- this.cancelTrackedTimer('completion-confirm', this.completionConfirmTimer, 'auto-accept');
1800
- this.completionConfirmTimer = null;
1629
+ this.cancelTrackedTimer('completion-confirm', 'auto-accept');
1801
1630
  this.completionMessageTime = null;
1802
1631
  // Ensure we're in watching state (not confirming_idle or ai_checking)
1803
1632
  if (this._state !== 'watching') {
@@ -1844,11 +1673,9 @@ export class RespawnController extends EventEmitter {
1844
1673
  this.aiChecker.cancel();
1845
1674
  }
1846
1675
  // Cancel completion confirm timer - hook takes precedence
1847
- this.cancelTrackedTimer('completion-confirm', this.completionConfirmTimer, 'Stop hook received');
1848
- this.completionConfirmTimer = null;
1676
+ this.cancelTrackedTimer('completion-confirm', 'Stop hook received');
1849
1677
  // Cancel pre-filter timer - hook takes precedence
1850
- this.cancelTrackedTimer('pre-filter', this.preFilterTimer, 'Stop hook received');
1851
- this.preFilterTimer = null;
1678
+ this.cancelTrackedTimer('pre-filter', 'Stop hook received');
1852
1679
  // Start short confirmation timer to handle race conditions
1853
1680
  // (e.g., Stop hook arrives but Claude immediately starts new work)
1854
1681
  this.startHookConfirmTimer('stop');
@@ -1877,12 +1704,9 @@ export class RespawnController extends EventEmitter {
1877
1704
  this.aiChecker.cancel();
1878
1705
  }
1879
1706
  // Cancel all other detection timers - this is definitive
1880
- this.cancelTrackedTimer('completion-confirm', this.completionConfirmTimer, 'idle_prompt received');
1881
- this.completionConfirmTimer = null;
1882
- this.cancelTrackedTimer('pre-filter', this.preFilterTimer, 'idle_prompt received');
1883
- this.preFilterTimer = null;
1884
- this.cancelTrackedTimer('no-output-fallback', this.noOutputTimer, 'idle_prompt received');
1885
- 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');
1886
1710
  // idle_prompt is an even stronger signal than Stop hook (60s+ idle)
1887
1711
  // Skip confirmation and go directly to idle
1888
1712
  this.onIdleConfirmed('idle_prompt hook (60s+ idle)');
@@ -1894,10 +1718,8 @@ export class RespawnController extends EventEmitter {
1894
1718
  * @param hookType - Which hook triggered this ('stop' or 'idle_prompt')
1895
1719
  */
1896
1720
  startHookConfirmTimer(hookType) {
1897
- this.cancelTrackedTimer('hook-confirm', this.hookConfirmTimer, 'restarting');
1898
- this.hookConfirmTimer = null;
1899
- this.hookConfirmTimer = this.startTrackedTimer('hook-confirm', RespawnController.HOOK_CONFIRM_DELAY_MS, () => {
1900
- this.hookConfirmTimer = null;
1721
+ this.cancelTrackedTimer('hook-confirm', 'restarting');
1722
+ this.startTrackedTimer('hook-confirm', RespawnController.HOOK_CONFIRM_DELAY_MS, () => {
1901
1723
  // Verify we haven't received new output since the hook arrived
1902
1724
  const hookTime = hookType === 'stop' ? this.stopHookTime : this.idlePromptTime;
1903
1725
  if (hookTime && this.lastOutputTime > hookTime) {
@@ -1963,12 +1785,10 @@ export class RespawnController extends EventEmitter {
1963
1785
  * After completion message, waits for output silence then triggers AI check.
1964
1786
  */
1965
1787
  startCompletionConfirmTimer() {
1966
- this.cancelTrackedTimer('completion-confirm', this.completionConfirmTimer, 'restarting');
1967
- this.completionConfirmTimer = null;
1788
+ this.cancelTrackedTimer('completion-confirm', 'restarting');
1968
1789
  this.setState('confirming_idle');
1969
1790
  this.logAction('detection', 'Completion message found in output');
1970
- this.completionConfirmTimer = this.startTrackedTimer('completion-confirm', this.config.completionConfirmMs, () => {
1971
- this.completionConfirmTimer = null;
1791
+ this.startTrackedTimer('completion-confirm', this.config.completionConfirmMs, () => {
1972
1792
  if (this._state === 'stopped')
1973
1793
  return;
1974
1794
  const msSinceOutput = Date.now() - this.lastOutputTime;
@@ -1989,8 +1809,7 @@ export class RespawnController extends EventEmitter {
1989
1809
  * Cancel completion confirmation if new activity detected.
1990
1810
  */
1991
1811
  cancelCompletionConfirm() {
1992
- this.cancelTrackedTimer('completion-confirm', this.completionConfirmTimer, 'activity detected');
1993
- this.completionConfirmTimer = null;
1812
+ this.cancelTrackedTimer('completion-confirm', 'activity detected');
1994
1813
  if (this._state === 'confirming_idle') {
1995
1814
  this.setState('watching');
1996
1815
  this.completionMessageTime = null;
@@ -2002,10 +1821,8 @@ export class RespawnController extends EventEmitter {
2002
1821
  * This ensures Claude has finished processing before we send the next command.
2003
1822
  */
2004
1823
  startStepConfirmTimer(step) {
2005
- this.cancelTrackedTimer('step-confirm', this.stepConfirmTimer, 'restarting');
2006
- this.stepConfirmTimer = null;
2007
- this.stepConfirmTimer = this.startTrackedTimer('step-confirm', this.config.completionConfirmMs, () => {
2008
- this.stepConfirmTimer = null;
1824
+ this.cancelTrackedTimer('step-confirm', 'restarting');
1825
+ this.startTrackedTimer('step-confirm', this.config.completionConfirmMs, () => {
2009
1826
  if (this._state === 'stopped')
2010
1827
  return;
2011
1828
  const msSinceOutput = Date.now() - this.lastOutputTime;
@@ -2035,8 +1852,7 @@ export class RespawnController extends EventEmitter {
2035
1852
  * Cancel step confirmation if working patterns detected.
2036
1853
  */
2037
1854
  cancelStepConfirm() {
2038
- this.cancelTrackedTimer('step-confirm', this.stepConfirmTimer, 'working detected');
2039
- this.stepConfirmTimer = null;
1855
+ this.cancelTrackedTimer('step-confirm', 'working detected');
2040
1856
  }
2041
1857
  /**
2042
1858
  * Called when idle is confirmed through any detection layer.
@@ -2178,8 +1994,7 @@ export class RespawnController extends EventEmitter {
2178
1994
  this.setState('sending_update');
2179
1995
  this.terminalBuffer.clear(); // Clear buffer for fresh detection
2180
1996
  this.clearWorkingPatternWindow(); // Clear rolling window
2181
- this.stepTimer = this.startTrackedTimer('step-delay', this.config.interStepDelayMs, async () => {
2182
- this.stepTimer = null;
1997
+ this.startTrackedTimer('step-delay', this.config.interStepDelayMs, async () => {
2183
1998
  if (this._state === 'stopped')
2184
1999
  return;
2185
2000
  // Use RALPH_STATUS RECOMMENDATION if available, otherwise fall back to config
@@ -2210,8 +2025,7 @@ export class RespawnController extends EventEmitter {
2210
2025
  this.setState('sending_clear');
2211
2026
  this.terminalBuffer.clear();
2212
2027
  this.clearWorkingPatternWindow();
2213
- this.stepTimer = this.startTrackedTimer('step-delay', this.config.interStepDelayMs, async () => {
2214
- this.stepTimer = null;
2028
+ this.startTrackedTimer('step-delay', this.config.interStepDelayMs, async () => {
2215
2029
  if (this._state === 'stopped')
2216
2030
  return;
2217
2031
  this.logAction('command', 'Sending: /clear');
@@ -2220,8 +2034,7 @@ export class RespawnController extends EventEmitter {
2220
2034
  this.setState('waiting_clear');
2221
2035
  this.promptDetected = false;
2222
2036
  // Start fallback timer - if no prompt detected after 10s, proceed to /init anyway
2223
- this.clearFallbackTimer = this.startTrackedTimer('clear-fallback', RespawnController.CLEAR_FALLBACK_TIMEOUT_MS, () => {
2224
- this.clearFallbackTimer = null;
2037
+ this.startTrackedTimer('clear-fallback', RespawnController.CLEAR_FALLBACK_TIMEOUT_MS, () => {
2225
2038
  if (this._state === 'waiting_clear') {
2226
2039
  this.logAction('step', '/clear fallback: proceeding to /init');
2227
2040
  this.emit('stepCompleted', 'clear');
@@ -2243,8 +2056,7 @@ export class RespawnController extends EventEmitter {
2243
2056
  this.setState('sending_init');
2244
2057
  this.terminalBuffer.clear();
2245
2058
  this.clearWorkingPatternWindow();
2246
- this.stepTimer = this.startTrackedTimer('step-delay', this.config.interStepDelayMs, async () => {
2247
- this.stepTimer = null;
2059
+ this.startTrackedTimer('step-delay', this.config.interStepDelayMs, async () => {
2248
2060
  if (this._state === 'stopped')
2249
2061
  return;
2250
2062
  this.logAction('command', 'Sending: /init');
@@ -2371,7 +2183,7 @@ export class RespawnController extends EventEmitter {
2371
2183
  config: this.config,
2372
2184
  };
2373
2185
  }
2374
- // ========== P2-001: Adaptive Timing Methods ==========
2186
+ // ========== P2-001: Adaptive Timing (delegated to RespawnAdaptiveTiming) ==========
2375
2187
  /**
2376
2188
  * Get the current completion confirm timeout, potentially adjusted by adaptive timing.
2377
2189
  * Uses historical idle detection durations to calculate an optimal timeout.
@@ -2383,90 +2195,40 @@ export class RespawnController extends EventEmitter {
2383
2195
  return this.config.completionConfirmMs ?? 10000;
2384
2196
  }
2385
2197
  // Need at least 5 samples before adjusting
2386
- if (this.timingHistory.sampleCount < 5) {
2198
+ const history = this.adaptiveTiming.getTimingHistory();
2199
+ if (history.sampleCount < 5) {
2387
2200
  return this.config.completionConfirmMs ?? 10000;
2388
2201
  }
2389
- return this.timingHistory.adaptiveCompletionConfirmMs;
2390
- }
2391
- /**
2392
- * Record timing data from a completed cycle for adaptive adjustments.
2393
- *
2394
- * @param idleDetectionMs - Time spent detecting idle
2395
- * @param cycleDurationMs - Total cycle duration
2396
- */
2397
- recordTimingData(idleDetectionMs, cycleDurationMs) {
2398
- if (!this.config.adaptiveTimingEnabled)
2399
- return;
2400
- const history = this.timingHistory;
2401
- // Add to rolling windows
2402
- history.recentIdleDetectionMs.push(idleDetectionMs);
2403
- history.recentCycleDurationMs.push(cycleDurationMs);
2404
- // Trim to max samples
2405
- if (history.recentIdleDetectionMs.length > history.maxSamples) {
2406
- history.recentIdleDetectionMs.shift();
2407
- }
2408
- if (history.recentCycleDurationMs.length > history.maxSamples) {
2409
- history.recentCycleDurationMs.shift();
2410
- }
2411
- history.sampleCount = history.recentIdleDetectionMs.length;
2412
- history.lastUpdatedAt = Date.now();
2413
- // Recalculate adaptive timing
2414
- this.updateAdaptiveTiming();
2415
- }
2416
- /**
2417
- * Recalculate the adaptive completion confirm timeout based on historical data.
2418
- * Uses the 75th percentile of recent idle detection times as the new timeout,
2419
- * with a 20% buffer for safety.
2420
- */
2421
- updateAdaptiveTiming() {
2422
- const history = this.timingHistory;
2423
- const minMs = this.config.adaptiveMinConfirmMs ?? 5000;
2424
- const maxMs = this.config.adaptiveMaxConfirmMs ?? 30000;
2425
- if (history.recentIdleDetectionMs.length < 5)
2426
- return;
2427
- // Sort for percentile calculation
2428
- const sorted = [...history.recentIdleDetectionMs].sort((a, b) => a - b);
2429
- // Use 75th percentile with 20% buffer
2430
- const p75Index = Math.floor(sorted.length * 0.75);
2431
- const p75Value = sorted[p75Index];
2432
- const withBuffer = Math.round(p75Value * 1.2);
2433
- // Clamp to configured bounds
2434
- const clamped = Math.max(minMs, Math.min(maxMs, withBuffer));
2435
- history.adaptiveCompletionConfirmMs = clamped;
2436
- this.log(`Adaptive timing updated: ${clamped}ms (p75=${p75Value}ms, samples=${sorted.length})`);
2202
+ return this.adaptiveTiming.getAdaptiveCompletionConfirmMs();
2437
2203
  }
2438
2204
  /**
2439
2205
  * Get the current timing history for monitoring.
2440
2206
  * @returns Copy of timing history
2441
2207
  */
2442
2208
  getTimingHistory() {
2443
- return { ...this.timingHistory };
2209
+ return this.adaptiveTiming.getTimingHistory();
2444
2210
  }
2445
- // ========== P2-002: Skip-Clear Optimization Methods ==========
2211
+ // ========== P2-002: Skip-Clear Optimization (delegated to respawn-health.ts) ==========
2446
2212
  /**
2447
2213
  * Determine whether to skip the /clear step based on current context usage.
2448
2214
  * Skips if token count is below the configured threshold percentage.
2449
2215
  *
2450
2216
  * @returns True if /clear should be skipped
2451
2217
  */
2452
- shouldSkipClear() {
2218
+ checkShouldSkipClear() {
2453
2219
  if (!this.config.skipClearWhenLowContext)
2454
2220
  return false;
2455
2221
  const thresholdPercent = this.config.skipClearThresholdPercent ?? 30;
2456
2222
  const maxContext = 200000; // Approximate max context for Claude
2457
- // Use the session's token count if available
2458
- const currentTokens = this.lastTokenCount;
2459
- if (currentTokens === 0)
2460
- return false; // Can't determine, don't skip
2461
- const usagePercent = (currentTokens / maxContext) * 100;
2462
- if (usagePercent < thresholdPercent) {
2463
- this.log(`Skip-clear optimization: ${usagePercent.toFixed(1)}% < ${thresholdPercent}% threshold`);
2464
- this.logAction('optimization', `Skipping /clear (${usagePercent.toFixed(1)}% context used)`);
2465
- 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)`);
2466
2228
  }
2467
- return false;
2229
+ return skip;
2468
2230
  }
2469
- // ========== P2-004: Cycle Metrics Methods ==========
2231
+ // ========== P2-004: Cycle Metrics (delegated to RespawnCycleMetricsTracker) ==========
2470
2232
  /**
2471
2233
  * Start tracking metrics for a new cycle.
2472
2234
  * Called when a respawn cycle begins.
@@ -2474,28 +2236,16 @@ export class RespawnController extends EventEmitter {
2474
2236
  startCycleMetrics(idleReason) {
2475
2237
  if (!this.config.trackCycleMetrics)
2476
2238
  return;
2477
- const now = Date.now();
2478
- this.currentCycleMetrics = {
2479
- cycleId: `${this.session.id}:${this.cycleCount}`,
2480
- sessionId: this.session.id,
2481
- cycleNumber: this.cycleCount,
2482
- startedAt: now,
2483
- idleReason,
2484
- idleDetectionMs: now - this.idleDetectionStartTime,
2485
- stepsCompleted: [],
2486
- clearSkipped: false,
2487
- tokenCountAtStart: this.lastTokenCount,
2488
- completionConfirmMsUsed: this.getAdaptiveCompletionConfirmMs(),
2489
- };
2239
+ this.cycleMetrics.startCycle(this.session.id, this.cycleCount, idleReason, this.idleDetectionStartTime, this.lastTokenCount, this.getAdaptiveCompletionConfirmMs());
2490
2240
  }
2491
2241
  /**
2492
2242
  * Record a completed step in the current cycle.
2493
2243
  * @param step - Name of the step (e.g., 'update', 'clear', 'init')
2494
2244
  */
2495
2245
  recordCycleStep(step) {
2496
- if (!this.config.trackCycleMetrics || !this.currentCycleMetrics)
2246
+ if (!this.config.trackCycleMetrics)
2497
2247
  return;
2498
- this.currentCycleMetrics.stepsCompleted?.push(step);
2248
+ this.cycleMetrics.recordStep(step);
2499
2249
  }
2500
2250
  /**
2501
2251
  * Complete the current cycle metrics with outcome.
@@ -2505,78 +2255,23 @@ export class RespawnController extends EventEmitter {
2505
2255
  * @param errorMessage - Optional error message if outcome is 'error'
2506
2256
  */
2507
2257
  completeCycleMetrics(outcome, errorMessage) {
2508
- if (!this.config.trackCycleMetrics || !this.currentCycleMetrics)
2258
+ if (!this.config.trackCycleMetrics)
2509
2259
  return;
2510
- const now = Date.now();
2511
- const metrics = {
2512
- ...this.currentCycleMetrics,
2513
- completedAt: now,
2514
- durationMs: now - (this.currentCycleMetrics.startedAt ?? now),
2515
- outcome,
2516
- errorMessage,
2517
- tokenCountAtEnd: this.lastTokenCount,
2518
- };
2519
- // Add to recent metrics
2520
- this.recentCycleMetrics.push(metrics);
2521
- if (this.recentCycleMetrics.length > RespawnController.MAX_CYCLE_METRICS_IN_MEMORY) {
2522
- this.recentCycleMetrics.shift();
2523
- }
2524
- // Record timing data for adaptive timing
2525
- this.recordTimingData(metrics.idleDetectionMs, metrics.durationMs);
2526
- // Update aggregate metrics
2527
- this.updateAggregateMetrics(metrics);
2528
- // Clear current cycle
2529
- this.currentCycleMetrics = null;
2530
- this.log(`Cycle #${metrics.cycleNumber} metrics: ${outcome}, duration=${metrics.durationMs}ms, idle_detection=${metrics.idleDetectionMs}ms`);
2531
- }
2532
- /**
2533
- * Update aggregate metrics with a new cycle's data.
2534
- * @param metrics - The completed cycle metrics
2535
- */
2536
- updateAggregateMetrics(metrics) {
2537
- const agg = this.aggregateMetrics;
2538
- agg.totalCycles++;
2539
- switch (metrics.outcome) {
2540
- case 'success':
2541
- agg.successfulCycles++;
2542
- break;
2543
- case 'stuck_recovery':
2544
- agg.stuckRecoveryCycles++;
2545
- break;
2546
- case 'blocked':
2547
- agg.blockedCycles++;
2548
- break;
2549
- case 'error':
2550
- agg.errorCycles++;
2551
- break;
2552
- case 'cancelled':
2553
- // Cancelled cycles don't count towards any specific category
2554
- // but are still counted in totalCycles
2555
- break;
2556
- default:
2557
- assertNever(metrics.outcome, `Unhandled CycleOutcome: ${metrics.outcome}`);
2558
- }
2559
- // Recalculate averages using all recent metrics
2560
- const durations = this.recentCycleMetrics.map((m) => m.durationMs);
2561
- const idleTimes = this.recentCycleMetrics.map((m) => m.idleDetectionMs);
2562
- if (durations.length > 0) {
2563
- agg.avgCycleDurationMs = Math.round(durations.reduce((a, b) => a + b, 0) / durations.length);
2564
- agg.avgIdleDetectionMs = Math.round(idleTimes.reduce((a, b) => a + b, 0) / idleTimes.length);
2565
- // Calculate P90
2566
- const sortedDurations = [...durations].sort((a, b) => a - b);
2567
- const p90Index = Math.floor(sortedDurations.length * 0.9);
2568
- 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`);
2569
2267
  }
2570
- // Calculate success rate
2571
- agg.successRate = agg.totalCycles > 0 ? Math.round((agg.successfulCycles / agg.totalCycles) * 100) : 100;
2572
- agg.lastUpdatedAt = Date.now();
2573
2268
  }
2574
2269
  /**
2575
2270
  * Get aggregate metrics for monitoring.
2576
2271
  * @returns Copy of aggregate metrics
2577
2272
  */
2578
2273
  getAggregateMetrics() {
2579
- return { ...this.aggregateMetrics };
2274
+ return this.cycleMetrics.getAggregate();
2580
2275
  }
2581
2276
  /**
2582
2277
  * Get recent cycle metrics for analysis.
@@ -2584,9 +2279,9 @@ export class RespawnController extends EventEmitter {
2584
2279
  * @returns Recent cycle metrics, newest first
2585
2280
  */
2586
2281
  getRecentCycleMetrics(limit = 20) {
2587
- return this.recentCycleMetrics.slice(-limit).reverse();
2282
+ return this.cycleMetrics.getRecent(limit);
2588
2283
  }
2589
- // ========== P2-005: Health Score Methods ==========
2284
+ // ========== P2-005: Health Score (delegated to respawn-health.ts) ==========
2590
2285
  /**
2591
2286
  * Calculate a comprehensive health score for the Ralph Loop system.
2592
2287
  * Aggregates multiple health signals into a single score (0-100).
@@ -2594,161 +2289,27 @@ export class RespawnController extends EventEmitter {
2594
2289
  * @returns Health score with component breakdown
2595
2290
  */
2596
2291
  calculateHealthScore() {
2597
- const now = Date.now();
2598
- const components = {
2599
- cycleSuccess: this.calculateCycleSuccessScore(),
2600
- circuitBreaker: this.calculateCircuitBreakerScore(),
2601
- iterationProgress: this.calculateIterationProgressScore(),
2602
- aiChecker: this.calculateAiCheckerScore(),
2603
- stuckRecovery: this.calculateStuckRecoveryScore(),
2604
- };
2605
- // Weighted average (cycle success is most important)
2606
- const weights = {
2607
- cycleSuccess: 0.35,
2608
- circuitBreaker: 0.2,
2609
- iterationProgress: 0.2,
2610
- aiChecker: 0.15,
2611
- stuckRecovery: 0.1,
2612
- };
2613
- const score = Math.round(components.cycleSuccess * weights.cycleSuccess +
2614
- components.circuitBreaker * weights.circuitBreaker +
2615
- components.iterationProgress * weights.iterationProgress +
2616
- components.aiChecker * weights.aiChecker +
2617
- components.stuckRecovery * weights.stuckRecovery);
2618
- // Determine status
2619
- let status;
2620
- if (score >= 90)
2621
- status = 'excellent';
2622
- else if (score >= 70)
2623
- status = 'good';
2624
- else if (score >= 50)
2625
- status = 'degraded';
2626
- else
2627
- status = 'critical';
2628
- // Generate recommendations
2629
- const recommendations = this.generateHealthRecommendations(components);
2630
- // Generate summary
2631
- const summary = this.generateHealthSummary(score, status, components);
2632
- return {
2633
- score,
2634
- status,
2635
- components,
2636
- summary,
2637
- recommendations,
2638
- calculatedAt: now,
2639
- };
2640
- }
2641
- /**
2642
- * Calculate score based on recent cycle success rate.
2643
- */
2644
- calculateCycleSuccessScore() {
2645
- if (this.aggregateMetrics.totalCycles === 0)
2646
- return 100; // No data = assume healthy
2647
- return this.aggregateMetrics.successRate;
2648
- }
2649
- /**
2650
- * Calculate score based on circuit breaker state.
2651
- */
2652
- calculateCircuitBreakerScore() {
2653
2292
  const tracker = this.session.ralphTracker;
2654
- if (!tracker)
2655
- return 100;
2656
- const cb = tracker.circuitBreakerStatus;
2657
- switch (cb.state) {
2658
- case 'CLOSED':
2659
- return 100;
2660
- case 'HALF_OPEN':
2661
- return 50;
2662
- case 'OPEN':
2663
- return 0;
2664
- default:
2665
- return 100;
2666
- }
2667
- }
2668
- /**
2669
- * Calculate score based on iteration progress.
2670
- */
2671
- calculateIterationProgressScore() {
2672
- const tracker = this.session.ralphTracker;
2673
- if (!tracker)
2674
- return 100;
2675
- const stallMetrics = tracker.getIterationStallMetrics();
2676
- const { stallDurationMs, warningThresholdMs, criticalThresholdMs } = stallMetrics;
2677
- if (stallDurationMs >= criticalThresholdMs)
2678
- return 0;
2679
- if (stallDurationMs >= warningThresholdMs)
2680
- return 30;
2681
- if (stallDurationMs >= warningThresholdMs / 2)
2682
- return 70;
2683
- return 100;
2684
- }
2685
- /**
2686
- * Calculate score based on AI checker health.
2687
- */
2688
- calculateAiCheckerScore() {
2689
- const state = this.aiChecker.getState();
2690
- if (state.status === 'disabled')
2691
- return 30;
2692
- if (state.status === 'cooldown')
2693
- return 70;
2694
- if (state.consecutiveErrors > 0)
2695
- return 50;
2696
- return 100;
2697
- }
2698
- /**
2699
- * Calculate score based on stuck-state recovery count.
2700
- */
2701
- calculateStuckRecoveryScore() {
2702
- const maxRecoveries = this.config.maxStuckRecoveries ?? 3;
2703
- if (this.stuckRecoveryCount === 0)
2704
- return 100;
2705
- if (this.stuckRecoveryCount >= maxRecoveries)
2706
- return 0;
2707
- return Math.round(100 - (this.stuckRecoveryCount / maxRecoveries) * 100);
2708
- }
2709
- /**
2710
- * Generate health recommendations based on component scores.
2711
- */
2712
- generateHealthRecommendations(components) {
2713
- const recommendations = [];
2714
- if (components.cycleSuccess < 70) {
2715
- recommendations.push('Cycle success rate is low. Check for recurring errors or stuck states.');
2716
- }
2717
- if (components.circuitBreaker < 50) {
2718
- recommendations.push('Circuit breaker is open or half-open. Review recent errors and consider manual reset.');
2719
- }
2720
- if (components.iterationProgress < 50) {
2721
- recommendations.push('Iteration progress has stalled. Check if Claude is stuck on a task.');
2722
- }
2723
- if (components.aiChecker < 50) {
2724
- recommendations.push('AI idle checker has errors. May need to check Claude CLI availability.');
2725
- }
2726
- if (components.stuckRecovery < 50) {
2727
- recommendations.push('Multiple stuck-state recoveries occurred. Consider increasing timeouts.');
2728
- }
2729
- if (recommendations.length === 0) {
2730
- recommendations.push('System is healthy. No action needed.');
2731
- }
2732
- return recommendations;
2733
- }
2734
- /**
2735
- * Generate a human-readable health summary.
2736
- */
2737
- generateHealthSummary(score, status, components) {
2738
- const lowest = Object.entries(components).reduce((min, [key, val]) => (val < min.val ? { key, val } : min), {
2739
- key: '',
2740
- val: 100,
2741
- });
2742
- if (status === 'excellent') {
2743
- return `Ralph Loop is operating excellently (${score}/100). All systems healthy.`;
2744
- }
2745
- if (status === 'good') {
2746
- return `Ralph Loop is operating well (${score}/100). Minor issues in ${lowest.key}.`;
2747
- }
2748
- if (status === 'degraded') {
2749
- return `Ralph Loop is degraded (${score}/100). Primary issue: ${lowest.key} (${lowest.val}/100).`;
2750
- }
2751
- 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);
2752
2313
  }
2753
2314
  }
2754
2315
  //# sourceMappingURL=respawn-controller.js.map