/** * @fileoverview AI-Powered Idle Checker for Respawn Controller * * Spawns a fresh Claude CLI session in a tmux session to analyze terminal output * and provide a definitive IDLE/WORKING verdict. This replaces the "Worked for Xm Xs" * pattern as the primary idle detection signal. * * ## How It Works * * 1. Generate temp file path for output capture * 2. Spawn tmux: `tmux new-session -d -s codeman-aicheck- bash -c 'claude -p ...'` * 3. Poll the temp file every 500ms for `__AICHECK_DONE__` marker * 4. Parse the file content for IDLE/WORKING on the first word * 5. Kill tmux session and delete temp file * * ## Error Handling * * - Tmux spawn fails: 1-min cooldown, increment error counter * - Check times out (90s): Kill session, 1-min cooldown * - Can't parse IDLE/WORKING: Treat as WORKING, 1-min cooldown * - 3 consecutive errors: Disable AI check, fall back to noOutputTimeoutMs * - Claude CLI not found: Disable permanently * * @module ai-idle-checker */ import { AiCheckerBase, type AiCheckerConfigBase, type AiCheckerResultBase, type AiCheckerStateBase, } from './ai-checker-base.js'; import { AI_CHECK_MODEL, AI_IDLE_CHECK_MAX_CONTEXT, AI_IDLE_CHECK_TIMEOUT_MS, AI_IDLE_CHECK_COOLDOWN_MS, AI_IDLE_CHECK_ERROR_COOLDOWN_MS, AI_CHECK_MAX_CONSECUTIVE_ERRORS, } from './config/ai-defaults.js'; // ========== Types ========== export type AiIdleCheckConfig = AiCheckerConfigBase; export type AiCheckVerdict = 'IDLE' | 'WORKING' | 'ERROR'; export type AiCheckResult = AiCheckerResultBase; export type AiCheckState = AiCheckerStateBase; // ========== Constants ========== const DEFAULT_AI_CHECK_CONFIG: AiIdleCheckConfig = { enabled: true, model: AI_CHECK_MODEL, maxContextChars: AI_IDLE_CHECK_MAX_CONTEXT, checkTimeoutMs: AI_IDLE_CHECK_TIMEOUT_MS, cooldownMs: AI_IDLE_CHECK_COOLDOWN_MS, errorCooldownMs: AI_IDLE_CHECK_ERROR_COOLDOWN_MS, maxConsecutiveErrors: AI_CHECK_MAX_CONSECUTIVE_ERRORS, }; /** Pattern to match IDLE or WORKING as the first word of output */ const VERDICT_PATTERN = /^\s*(IDLE|WORKING)\b/i; /** * The prompt sent to the AI idle checker. * * P1-005: Enhanced with more specific working pattern examples and clearer structure. */ const AI_CHECK_PROMPT = `You are analyzing terminal output from a Claude Code CLI session. Determine if Claude has FINISHED working (IDLE) or is STILL WORKING (WORKING). CRITICAL RULE: When in doubt, ALWAYS answer WORKING. False positives (saying IDLE when Claude is working) cause session interruptions. It's safer to wait longer than to interrupt active work. ## IDLE Indicators (need AT LEAST 2 of these together) 1. **Completion Summary** - The most reliable signal: - "✻ Worked for Xm Ys" (e.g., "✻ Worked for 2m 46s") - "Worked for Xs" (e.g., "Worked for 5s") - Cost summary: "$X.XX spent" or "X tokens used" 2. **Input Prompt Visible**: - The ❯ prompt character at the very end - Empty line after completion summary - Waiting cursor position 3. **Task Completion Language**: - "All done", "Finished", "Completed successfully" - Explicit "waiting for input" or similar ## WORKING Indicators (ANY ONE of these = answer WORKING) ### Active Processing Indicators: - **Spinners**: ⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏ (Braille), ◐ ◓ ◑ ◒ (quarter), ⣾ ⣽ ⣻ ⢿ ⡿ ⣟ ⣯ ⣷ - **Activity Words**: Thinking, Writing, Reading, Running, Searching, Editing, Creating, Deleting, Analyzing, Executing, Synthesizing, Compiling, Building, Processing, Loading, Generating, Testing, Checking, Validating, Brewing, Formatting, Linting, Installing, Fetching, Downloading ### Tool Execution in Progress: - Bash commands with no result shown yet - "Running: npm test", "Executing command..." - File read/write operations incomplete - Progress bars or percentage indicators - Test suite running (dots appearing, "Test Suites: X passed") ### Output Structure Issues: - Truncated lines without completion - JSON/code blocks not closed - Multi-line output clearly incomplete - "..." indicating more to come - Output ending mid-sentence or mid-word ### Claude Planning/Thinking: - "Let me...", "I'll...", "Now I need to..." - TodoWrite updates without completion - Plan mode approval prompts (numbered options) ## Terminal Output to Analyze --- {TERMINAL_BUFFER} --- ## Your Response First line: EXACTLY "IDLE" or "WORKING" (nothing else) Second line onwards: Brief explanation of your reasoning. Remember: When uncertain, answer WORKING.`; // ========== AiIdleChecker Class ========== /** * Manages AI-powered idle detection by spawning a fresh Claude CLI session * to analyze terminal output and provide a definitive IDLE/WORKING verdict. */ export class AiIdleChecker extends AiCheckerBase { protected readonly muxNamePrefix = 'codeman-aicheck-'; protected readonly doneMarker = '__AICHECK_DONE__'; protected readonly tempFilePrefix = 'codeman-aicheck'; protected readonly logPrefix = '[AiIdleChecker]'; protected readonly checkDescription = 'AI idle check'; constructor(sessionId: string, config: Partial = {}) { super(sessionId, DEFAULT_AI_CHECK_CONFIG, config); } protected buildPrompt(terminalBuffer: string): string { return AI_CHECK_PROMPT.replace('{TERMINAL_BUFFER}', terminalBuffer); } protected parseVerdict(output: string): { verdict: AiCheckVerdict; reasoning: string } | null { const match = output.match(VERDICT_PATTERN); if (!match) return null; const verdict = match[1].toUpperCase() as 'IDLE' | 'WORKING'; const lines = output.split('\n'); const reasoning = lines.slice(1).join('\n').trim() || `AI determined: ${verdict}`; return { verdict, reasoning }; } protected getPositiveVerdict(): AiCheckVerdict { return 'IDLE'; } protected getNegativeVerdict(): AiCheckVerdict { return 'WORKING'; } protected getErrorVerdict(): AiCheckVerdict { return 'ERROR'; } protected createErrorResult(reasoning: string, durationMs: number): AiCheckResult { return { verdict: 'ERROR', reasoning, durationMs }; } protected createResult(verdict: AiCheckVerdict, reasoning: string, durationMs: number): AiCheckResult { return { verdict, reasoning, durationMs }; } }