mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-15 02:49:33 +01:00
Found with knip (exports, types, class members) and confirmed by a text search of src, test, scripts and packages before each removal. - Per-CLI get<Cli>NotFoundMessage() (nine of them) with their message constants: missingCliMessage() builds that text from the registry now. - Registry leftovers: renderCliCommand, asCliId, cliIds, isCliRunnable, createCliResolverForTest, profileExists, and two name lists that only repeated schema enums. - A task-description cache in Session that was written on every chunk of terminal output (one regex pass per line) and never read since the subagent watcher takes descriptions from transcripts. The cache module, its pattern and both parse methods go with it. - Session.parentAgentId / childAgentIds: never set by anything, so the two optional SessionState fields were never on the wire. - Unreferenced methods on TmuxManager (pipe-pane pair, syncMouseMode), RespawnController, StateStore.recoverFromBackup (load() already falls back to the .bak file), TeamWatcher, SseStreamManager and the AI checkers' getErrorVerdict. - updateCaseEnvVars: the settings.local.json env path that envOverrides replaced. - Re-exports in the utils, cli-registry, routes and prompts barrels that no importer used, and unused test helpers (MockStateStore, the mobile throttle helpers and others). Eight mobile tests imported the WebServer type from a path that does not exist (../src instead of ../../src); type-only, so nothing failed. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
176 lines
6.1 KiB
TypeScript
176 lines
6.1 KiB
TypeScript
/**
|
||
* @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-<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 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 ==========
|
||
|
||
type AiIdleCheckConfig = AiCheckerConfigBase;
|
||
|
||
export type AiCheckVerdict = 'IDLE' | 'WORKING' | 'ERROR';
|
||
|
||
export type AiCheckResult = AiCheckerResultBase<AiCheckVerdict>;
|
||
|
||
export type AiCheckState = AiCheckerStateBase<AiCheckVerdict>;
|
||
|
||
// ========== 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<AiCheckVerdict, AiIdleCheckConfig, AiCheckResult, AiCheckState> {
|
||
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<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 createErrorResult(reasoning: string, durationMs: number): AiCheckResult {
|
||
return { verdict: 'ERROR', reasoning, durationMs };
|
||
}
|
||
|
||
protected createResult(verdict: AiCheckVerdict, reasoning: string, durationMs: number): AiCheckResult {
|
||
return { verdict, reasoning, durationMs };
|
||
}
|
||
}
|