Files
Codeman/src/ai-idle-checker.ts
T
arkonandClaude Opus 4.5 9aa5973970 fix: rock-solid idle detection in respawn controller
- Add 300-char rolling window to catch working patterns split across PTY chunks
- Check completion message BEFORE working patterns (priority fix)
- Clear rolling window on completion message (transition point)
- Increase working pattern absence threshold from 3s to 8s
- Add Session.isWorking safety check before confirming idle
- Add 20+ more working patterns (Compiling, Building, Processing, etc.)
- Make AI idle checker prompt more conservative (err toward WORKING)
- Update documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 00:55:29 +01:00

158 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @fileoverview AI-Powered Idle Checker for Respawn Controller
*
* Spawns a fresh Claude CLI session in a screen 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 screen: `screen -dmS claudeman-aicheck-<short> 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 screen and delete temp file
*
* ## Error Handling
*
* - Screen spawn fails: 1-min cooldown, increment error counter
* - Check times out (90s): Kill screen, 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,
type AiCheckerStatus,
} from './ai-checker-base.js';
// ========== Types ==========
export interface AiIdleCheckConfig extends AiCheckerConfigBase {}
// Re-export the status type for backwards compatibility
export type AiCheckStatus = AiCheckerStatus;
export type AiCheckVerdict = 'IDLE' | 'WORKING' | 'ERROR';
export interface AiCheckResult extends AiCheckerResultBase<AiCheckVerdict> {}
export interface AiCheckState extends AiCheckerStateBase<AiCheckVerdict> {}
/** Events emitted by AiIdleChecker */
export interface AiIdleCheckerEvents {
checkStarted: () => void;
checkCompleted: (result: AiCheckResult) => void;
checkFailed: (error: string) => void;
cooldownStarted: (endsAt: number) => void;
cooldownEnded: () => void;
disabled: (reason: string) => void;
log: (message: string) => void;
}
// ========== Constants ==========
const DEFAULT_AI_CHECK_CONFIG: AiIdleCheckConfig = {
enabled: true,
model: 'claude-opus-4-5-20251101',
maxContextChars: 16000,
checkTimeoutMs: 90000,
cooldownMs: 180000,
errorCooldownMs: 60000,
maxConsecutiveErrors: 3,
};
/** 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 checker */
const AI_CHECK_PROMPT = `Analyze this terminal output from a running Claude Code session. Determine if the session is IDLE (done working, waiting for new input) or WORKING (still actively processing).
IMPORTANT: When in doubt, answer WORKING. Brief pauses between tool executions do NOT mean the session is idle. Claude may be processing or about to output more.
IDLE indicators (need MULTIPLE of these to confirm idle):
- Completion summary shown (e.g., "✻ Worked for 2m 46s", "Worked for 5s")
- Prompt character visible at the end (❯ or similar)
- Cost summary displayed (e.g., "$0.12 spent")
- Clear end of output with no pending work
WORKING indicators (ANY of these means WORKING):
- Spinner characters (⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏ or similar)
- Activity text: Thinking, Writing, Reading, Running, Searching, Editing, Creating, Deleting, Analyzing, Executing, Synthesizing, Compiling, Building, Processing, Loading, Generating, Testing, Checking, Validating
- Tool execution in progress (commands being run)
- Truncated or partial lines at the end
- File operations in progress
- Output that appears mid-stream or incomplete
- No completion summary visible yet
Terminal output (most recent at bottom):
---
{TERMINAL_BUFFER}
---
Answer with EXACTLY one word on the first line: IDLE or WORKING
If uncertain, answer WORKING. Then briefly explain why.`;
// ========== 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<
AiCheckVerdict,
AiIdleCheckConfig,
AiCheckResult,
AiCheckState
> {
protected readonly screenNamePrefix = 'claudeman-aicheck-';
protected readonly doneMarker = '__AICHECK_DONE__';
protected readonly tempFilePrefix = 'claudeman-aicheck';
protected readonly logPrefix = '[AiIdleChecker]';
protected readonly checkDescription = 'AI idle check';
constructor(sessionId: string, config: Partial<AiIdleCheckConfig> = {}) {
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 };
}
}