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