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.
- package/README.md +91 -0
- 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 +36 -0
- package/dist/config/server-timing.d.ts.map +1 -0
- package/dist/config/server-timing.js +51 -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.map +1 -1
- package/dist/hooks-config.js +7 -6
- 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/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-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 +194 -685
- package/dist/ralph-tracker.d.ts.map +1 -1
- package/dist/ralph-tracker.js +349 -1713
- 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 +14 -101
- package/dist/respawn-controller.d.ts.map +1 -1
- package/dist/respawn-controller.js +155 -594
- 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-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 +2 -33
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +58 -309
- package/dist/session.js.map +1 -1
- package/dist/state-store.d.ts +9 -2
- package/dist/state-store.d.ts.map +1 -1
- package/dist/state-store.js +112 -39
- package/dist/state-store.js.map +1 -1
- package/dist/subagent-watcher.d.ts +16 -9
- package/dist/subagent-watcher.d.ts.map +1 -1
- package/dist/subagent-watcher.js +126 -147
- package/dist/subagent-watcher.js.map +1 -1
- package/dist/team-watcher.d.ts +3 -0
- package/dist/team-watcher.d.ts.map +1 -1
- package/dist/team-watcher.js +54 -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 +127 -7
- package/dist/tunnel-manager.js.map +1 -1
- package/dist/types/api.d.ts +93 -0
- package/dist/types/api.d.ts.map +1 -0
- package/dist/types/api.js +83 -0
- package/dist/types/api.js.map +1 -0
- package/dist/types/app-state.d.ts +100 -0
- package/dist/types/app-state.d.ts.map +1 -0
- package/dist/types/app-state.js +59 -0
- package/dist/types/app-state.js.map +1 -0
- package/dist/types/common.d.ts +70 -0
- package/dist/types/common.d.ts.map +1 -0
- package/dist/types/common.js +8 -0
- package/dist/types/common.js.map +1 -0
- package/dist/types/index.d.ts +18 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +18 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/lifecycle.d.ts +17 -0
- package/dist/types/lifecycle.d.ts.map +1 -0
- package/dist/types/lifecycle.js +5 -0
- package/dist/types/lifecycle.js.map +1 -0
- package/dist/types/plan.d.ts +32 -0
- package/dist/types/plan.d.ts.map +1 -0
- package/dist/types/plan.js +5 -0
- package/dist/types/plan.js.map +1 -0
- package/dist/types/push.d.ts +23 -0
- package/dist/types/push.d.ts.map +1 -0
- package/dist/types/push.js +5 -0
- package/dist/types/push.js.map +1 -0
- package/dist/types/ralph.d.ts +241 -0
- package/dist/types/ralph.d.ts.map +1 -0
- package/dist/types/ralph.js +49 -0
- package/dist/types/ralph.js.map +1 -0
- package/dist/types/respawn.d.ts +250 -0
- package/dist/types/respawn.d.ts.map +1 -0
- package/dist/types/respawn.js +5 -0
- package/dist/types/respawn.js.map +1 -0
- package/dist/types/run-summary.d.ts +81 -0
- package/dist/types/run-summary.d.ts.map +1 -0
- package/dist/types/run-summary.js +22 -0
- package/dist/types/run-summary.js.map +1 -0
- package/dist/types/session.d.ts +130 -0
- package/dist/types/session.d.ts.map +1 -0
- package/dist/types/session.js +5 -0
- package/dist/types/session.js.map +1 -0
- package/dist/types/task.d.ts +58 -0
- package/dist/types/task.d.ts.map +1 -0
- package/dist/types/task.js +5 -0
- package/dist/types/task.js.map +1 -0
- package/dist/types/teams.d.ts +55 -0
- package/dist/types/teams.d.ts.map +1 -0
- package/dist/types/teams.js +5 -0
- package/dist/types/teams.js.map +1 -0
- package/dist/types/tools.d.ts +46 -0
- package/dist/types/tools.d.ts.map +1 -0
- package/dist/types/tools.js +5 -0
- package/dist/types/tools.js.map +1 -0
- package/dist/types.d.ts +1 -1138
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -214
- 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 +70 -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 +152 -236
- package/dist/web/public/app.js.br +0 -0
- package/dist/web/public/app.js.gz +0 -0
- package/dist/web/public/constants.js +238 -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 +11 -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 +279 -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 +467 -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 +445 -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 +3 -3
- 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 +1115 -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.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.css.gz +0 -0
- package/dist/web/public/vendor/xterm.min.js.gz +0 -0
- package/dist/web/public/voice-input.js +858 -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 +143 -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 +419 -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 +381 -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 +475 -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 +260 -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 +729 -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 +678 -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 +10 -9
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +342 -3829
- package/dist/web/server.js.map +1 -1
- 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,
|
|
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:
|
|
71
|
-
aiIdleCheckMaxContext:
|
|
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:
|
|
76
|
-
aiPlanCheckMaxContext:
|
|
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
|
-
/**
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
|
|
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
|
|
230
|
-
/**
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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.
|
|
555
|
+
this.lastEmittedDetectionKey = '';
|
|
556
|
+
const id = this.cleanup.setInterval(() => {
|
|
656
557
|
try {
|
|
657
558
|
if (this._state !== 'stopped') {
|
|
658
|
-
|
|
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
|
-
|
|
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 =
|
|
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 (
|
|
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.
|
|
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',
|
|
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 =
|
|
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.
|
|
1031
|
-
|
|
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',
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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,
|
|
1359
|
-
|
|
1360
|
-
|
|
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
|
|
1400
|
-
*
|
|
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
|
-
|
|
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
|
-
//
|
|
1416
|
-
return
|
|
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',
|
|
1448
|
-
this.
|
|
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',
|
|
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.
|
|
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',
|
|
1573
|
-
this.
|
|
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',
|
|
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',
|
|
1639
|
-
this.
|
|
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',
|
|
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 =
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
1881
|
-
this.
|
|
1882
|
-
this.cancelTrackedTimer('
|
|
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',
|
|
1898
|
-
this.
|
|
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',
|
|
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.
|
|
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',
|
|
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',
|
|
2006
|
-
this.
|
|
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',
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
2198
|
+
const history = this.adaptiveTiming.getTimingHistory();
|
|
2199
|
+
if (history.sampleCount < 5) {
|
|
2387
2200
|
return this.config.completionConfirmMs ?? 10000;
|
|
2388
2201
|
}
|
|
2389
|
-
return this.
|
|
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
|
|
2209
|
+
return this.adaptiveTiming.getTimingHistory();
|
|
2444
2210
|
}
|
|
2445
|
-
// ========== P2-002: Skip-Clear Optimization
|
|
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
|
-
|
|
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
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
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
|
|
2229
|
+
return skip;
|
|
2468
2230
|
}
|
|
2469
|
-
// ========== P2-004: Cycle Metrics
|
|
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
|
-
|
|
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
|
|
2246
|
+
if (!this.config.trackCycleMetrics)
|
|
2497
2247
|
return;
|
|
2498
|
-
this.
|
|
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
|
|
2258
|
+
if (!this.config.trackCycleMetrics)
|
|
2509
2259
|
return;
|
|
2510
|
-
const
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
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
|
|
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.
|
|
2282
|
+
return this.cycleMetrics.getRecent(limit);
|
|
2588
2283
|
}
|
|
2589
|
-
// ========== P2-005: Health Score
|
|
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
|
-
|
|
2655
|
-
|
|
2656
|
-
const
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
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
|