mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
Knip-driven cleanup. All changes verified with tsc --noEmit, lint, and
build.
Removed (zero consumers):
- VERIFICATION_PROMPT constant + its barrel re-export
- createInitialOrchestratorPersistState factory
- transcriptWatcher singleton export
- createAnsiPatternFull / createAnsiPatternSimple factories
- TimerInfo interface + unused AiCheckResult/AiPlanCheckResult imports
in respawn-controller.ts
- 35 unused Zod z.infer \`*Input\` types in schemas.ts
- Dead re-exports: SessionMode from session.ts, AuthSessionRecord from
web/ports/index.ts, EnhancedPlanTask/CheckpointReview from
ralph-tracker.ts, 7 unused entries in utils/index.ts
- 14 event/config interfaces that lived only as JSDoc hints (no TS type
position usage): Session/Respawn/RalphLoop/RalphTracker/
SessionManager/SessionAutoOps/Subagent/TaskQueue/TaskTracker/
TranscriptWatcher/Image/OrchestratorLoop Events + RespawnPreset +
SessionOutput
Narrowed to module scope (kept but no longer exported):
- buildPermissionArgs in session-cli-builder.ts
- 28 type/interface declarations used only within their own file:
Ai{Idle,Plan}Check{Config,State}, BashToolParser{Events,Config},
FileStream/CreateStream{Options,Result}, PlanSubagentEvent,
SubagentCallback, RalphLoopConfig, RalphLoop{Events,Options},
ActiveTimerInfo, DetectionStatus, ActionLogEntry, AutoOpsCallbacks,
TunnelStatus, Timer/LRUMap/StaleExpirationMap Options, AuthState,
SessionListenerDeps, SseStreamManagerDeps, and 8 more
Docs: CLAUDE.md advice for global-regex `lastIndex` now points to the
remaining `execPattern()` helper instead of the deleted factories.
Knip delta: unused files 42→0, unused exports 161→16, unused types 92→0.
The 16 remaining exports are a mobile-test helper toolkit intentionally
kept for upcoming tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
180 lines
6.2 KiB
TypeScript
180 lines
6.2 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 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 };
|
||
}
|
||
}
|