mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-07 16:09:43 +02:00
adjust formatting
This commit is contained in:
+390
-213
@@ -41,11 +41,7 @@ import { AiIdleChecker, type AiCheckResult, type AiCheckState } from './ai-idle-
|
||||
import { AiPlanChecker, type AiPlanCheckResult } from './ai-plan-checker.js';
|
||||
import type { TeamWatcher } from './team-watcher.js';
|
||||
import { BufferAccumulator } from './utils/buffer-accumulator.js';
|
||||
import {
|
||||
ANSI_ESCAPE_PATTERN_SIMPLE,
|
||||
TOKEN_PATTERN,
|
||||
assertNever,
|
||||
} from './utils/index.js';
|
||||
import { ANSI_ESCAPE_PATTERN_SIMPLE, TOKEN_PATTERN, assertNever } from './utils/index.js';
|
||||
import {
|
||||
MAX_RESPAWN_BUFFER_SIZE,
|
||||
TRIM_RESPAWN_BUFFER_TO as RESPAWN_BUFFER_TRIM_SIZE,
|
||||
@@ -542,45 +538,46 @@ export interface RespawnEvents {
|
||||
|
||||
/** Default configuration values */
|
||||
const DEFAULT_CONFIG: RespawnConfig = {
|
||||
idleTimeoutMs: 10000, // 10 seconds of no activity after prompt (legacy, still used as fallback)
|
||||
updatePrompt: 'write a brief progress summary to CLAUDE.md noting what you accomplished, then continue working.',
|
||||
interStepDelayMs: 1000, // 1 second between steps
|
||||
idleTimeoutMs: 10000, // 10 seconds of no activity after prompt (legacy, still used as fallback)
|
||||
updatePrompt:
|
||||
'write a brief progress summary to CLAUDE.md noting what you accomplished, then continue working.',
|
||||
interStepDelayMs: 1000, // 1 second between steps
|
||||
enabled: true,
|
||||
sendClear: true, // send /clear after update prompt
|
||||
sendInit: true, // send /init after /clear
|
||||
completionConfirmMs: 10000, // 10 seconds of silence after completion message
|
||||
noOutputTimeoutMs: 30000, // 30 seconds fallback if no output at all
|
||||
autoAcceptPrompts: true, // auto-accept plan mode prompts (not questions)
|
||||
autoAcceptDelayMs: 8000, // 8 seconds before auto-accepting
|
||||
aiIdleCheckEnabled: true, // use AI to confirm idle state
|
||||
sendClear: true, // send /clear after update prompt
|
||||
sendInit: true, // send /init after /clear
|
||||
completionConfirmMs: 10000, // 10 seconds of silence after completion message
|
||||
noOutputTimeoutMs: 30000, // 30 seconds fallback if no output at all
|
||||
autoAcceptPrompts: true, // auto-accept plan mode prompts (not questions)
|
||||
autoAcceptDelayMs: 8000, // 8 seconds before auto-accepting
|
||||
aiIdleCheckEnabled: true, // use AI to confirm idle state
|
||||
aiIdleCheckModel: 'claude-opus-4-5-20251101',
|
||||
aiIdleCheckMaxContext: 16000, // ~4k tokens
|
||||
aiIdleCheckTimeoutMs: 90000, // 90 seconds (thinking can be slow)
|
||||
aiIdleCheckMaxContext: 16000, // ~4k tokens
|
||||
aiIdleCheckTimeoutMs: 90000, // 90 seconds (thinking can be slow)
|
||||
aiIdleCheckCooldownMs: 180000, // 3 minutes after WORKING verdict
|
||||
aiPlanCheckEnabled: true, // use AI to confirm plan mode before auto-accept
|
||||
aiPlanCheckEnabled: true, // use AI to confirm plan mode before auto-accept
|
||||
aiPlanCheckModel: 'claude-opus-4-5-20251101',
|
||||
aiPlanCheckMaxContext: 8000, // ~2k tokens (plan mode UI is compact)
|
||||
aiPlanCheckTimeoutMs: 60000, // 60 seconds (thinking can be slow)
|
||||
aiPlanCheckCooldownMs: 30000, // 30 seconds after NOT_PLAN_MODE
|
||||
stuckStateDetectionEnabled: true, // detect stuck states
|
||||
stuckStateWarningMs: 300000, // 5 minutes warning threshold
|
||||
stuckStateRecoveryMs: 600000, // 10 minutes recovery threshold
|
||||
maxStuckRecoveries: 3, // max recovery attempts
|
||||
aiPlanCheckMaxContext: 8000, // ~2k tokens (plan mode UI is compact)
|
||||
aiPlanCheckTimeoutMs: 60000, // 60 seconds (thinking can be slow)
|
||||
aiPlanCheckCooldownMs: 30000, // 30 seconds after NOT_PLAN_MODE
|
||||
stuckStateDetectionEnabled: true, // detect stuck states
|
||||
stuckStateWarningMs: 300000, // 5 minutes warning threshold
|
||||
stuckStateRecoveryMs: 600000, // 10 minutes recovery threshold
|
||||
maxStuckRecoveries: 3, // max recovery attempts
|
||||
// P2-001: Adaptive timing
|
||||
adaptiveTimingEnabled: true, // Use adaptive timing based on historical patterns
|
||||
adaptiveMinConfirmMs: 5000, // Minimum 5 seconds
|
||||
adaptiveMaxConfirmMs: 30000, // Maximum 30 seconds
|
||||
adaptiveTimingEnabled: true, // Use adaptive timing based on historical patterns
|
||||
adaptiveMinConfirmMs: 5000, // Minimum 5 seconds
|
||||
adaptiveMaxConfirmMs: 30000, // Maximum 30 seconds
|
||||
// P2-002: Skip-clear optimization
|
||||
skipClearWhenLowContext: true, // Skip /clear when token count is low
|
||||
skipClearThresholdPercent: 30, // Skip if below 30% of max context
|
||||
skipClearWhenLowContext: true, // Skip /clear when token count is low
|
||||
skipClearThresholdPercent: 30, // Skip if below 30% of max context
|
||||
// P2-004: Cycle metrics
|
||||
trackCycleMetrics: true, // Track and persist cycle metrics
|
||||
trackCycleMetrics: true, // Track and persist cycle metrics
|
||||
// P2-001: Confidence scoring
|
||||
minIdleConfidence: 65, // Minimum confidence to trigger idle (0-100)
|
||||
confidenceWeightCompletion: 40, // Weight for completion message
|
||||
confidenceWeightSilence: 25, // Weight for output silence
|
||||
confidenceWeightTokens: 20, // Weight for token stability
|
||||
confidenceWeightNoWorking: 15, // Weight for working pattern absence
|
||||
minIdleConfidence: 65, // Minimum confidence to trigger idle (0-100)
|
||||
confidenceWeightCompletion: 40, // Weight for completion message
|
||||
confidenceWeightSilence: 25, // Weight for output silence
|
||||
confidenceWeightTokens: 20, // Weight for token stability
|
||||
confidenceWeightNoWorking: 15, // Weight for working pattern absence
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -737,7 +734,10 @@ export class RespawnController extends EventEmitter {
|
||||
// ========== Timer Tracking for UI Countdown Display ==========
|
||||
|
||||
/** Active timers being tracked for UI display */
|
||||
private activeTimers: Map<string, { name: string; startedAt: number; durationMs: number; endsAt: number }> = new Map();
|
||||
private activeTimers: Map<
|
||||
string,
|
||||
{ name: string; startedAt: number; durationMs: number; endsAt: number }
|
||||
> = new Map();
|
||||
|
||||
/** Recent action log entries (for UI display, max 20) */
|
||||
private recentActions: ActionLogEntry[] = [];
|
||||
@@ -818,9 +818,9 @@ export class RespawnController extends EventEmitter {
|
||||
* Used as secondary signals, not primary detection.
|
||||
*/
|
||||
private readonly PROMPT_PATTERNS = [
|
||||
'❯', // Standard prompt
|
||||
'\u276f', // Unicode variant
|
||||
'⏵', // Claude Code prompt variant
|
||||
'❯', // Standard prompt
|
||||
'\u276f', // Unicode variant
|
||||
'⏵', // Claude Code prompt variant
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -829,15 +829,55 @@ export class RespawnController extends EventEmitter {
|
||||
* Note: ✻ and ✽ removed - they appear in completion messages too.
|
||||
*/
|
||||
private readonly WORKING_PATTERNS = [
|
||||
'Thinking', 'Writing', 'Reading', 'Running', 'Searching',
|
||||
'Editing', 'Creating', 'Deleting', 'Analyzing', 'Executing',
|
||||
'Synthesizing', 'Brewing', // Claude's processing indicators
|
||||
'Compiling', 'Building', 'Installing', 'Fetching', 'Downloading',
|
||||
'Processing', 'Generating', 'Loading', 'Starting', 'Updating',
|
||||
'Checking', 'Validating', 'Testing', 'Formatting', 'Linting',
|
||||
'⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏', // Spinner chars
|
||||
'◐', '◓', '◑', '◒', // Alternative spinners
|
||||
'⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷', // Braille spinners
|
||||
'Thinking',
|
||||
'Writing',
|
||||
'Reading',
|
||||
'Running',
|
||||
'Searching',
|
||||
'Editing',
|
||||
'Creating',
|
||||
'Deleting',
|
||||
'Analyzing',
|
||||
'Executing',
|
||||
'Synthesizing',
|
||||
'Brewing', // Claude's processing indicators
|
||||
'Compiling',
|
||||
'Building',
|
||||
'Installing',
|
||||
'Fetching',
|
||||
'Downloading',
|
||||
'Processing',
|
||||
'Generating',
|
||||
'Loading',
|
||||
'Starting',
|
||||
'Updating',
|
||||
'Checking',
|
||||
'Validating',
|
||||
'Testing',
|
||||
'Formatting',
|
||||
'Linting',
|
||||
'⠋',
|
||||
'⠙',
|
||||
'⠹',
|
||||
'⠸',
|
||||
'⠼',
|
||||
'⠴',
|
||||
'⠦',
|
||||
'⠧',
|
||||
'⠇',
|
||||
'⠏', // Spinner chars
|
||||
'◐',
|
||||
'◓',
|
||||
'◑',
|
||||
'◒', // Alternative spinners
|
||||
'⣾',
|
||||
'⣽',
|
||||
'⣻',
|
||||
'⢿',
|
||||
'⡿',
|
||||
'⣟',
|
||||
'⣯',
|
||||
'⣷', // Braille spinners
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -912,12 +952,14 @@ export class RespawnController extends EventEmitter {
|
||||
// Ensure AI check timeouts are positive
|
||||
if (c.aiIdleCheckTimeoutMs <= 0) c.aiIdleCheckTimeoutMs = DEFAULT_CONFIG.aiIdleCheckTimeoutMs;
|
||||
if (c.aiIdleCheckCooldownMs < 0) c.aiIdleCheckCooldownMs = DEFAULT_CONFIG.aiIdleCheckCooldownMs;
|
||||
if (c.aiIdleCheckMaxContext <= 0) c.aiIdleCheckMaxContext = DEFAULT_CONFIG.aiIdleCheckMaxContext;
|
||||
if (c.aiIdleCheckMaxContext <= 0)
|
||||
c.aiIdleCheckMaxContext = DEFAULT_CONFIG.aiIdleCheckMaxContext;
|
||||
|
||||
// Ensure plan check timeouts are positive
|
||||
if (c.aiPlanCheckTimeoutMs <= 0) c.aiPlanCheckTimeoutMs = DEFAULT_CONFIG.aiPlanCheckTimeoutMs;
|
||||
if (c.aiPlanCheckCooldownMs < 0) c.aiPlanCheckCooldownMs = DEFAULT_CONFIG.aiPlanCheckCooldownMs;
|
||||
if (c.aiPlanCheckMaxContext <= 0) c.aiPlanCheckMaxContext = DEFAULT_CONFIG.aiPlanCheckMaxContext;
|
||||
if (c.aiPlanCheckMaxContext <= 0)
|
||||
c.aiPlanCheckMaxContext = DEFAULT_CONFIG.aiPlanCheckMaxContext;
|
||||
}
|
||||
|
||||
/** Wire up AI checker events to controller events (removes existing listeners first to prevent duplicates) */
|
||||
@@ -999,7 +1041,8 @@ export class RespawnController extends EventEmitter {
|
||||
const completionMessageDetected = this.completionMessageTime !== null;
|
||||
const outputSilent = msSinceLastOutput >= this.config.completionConfirmMs;
|
||||
const tokensStable = msSinceTokenChange >= this.config.completionConfirmMs;
|
||||
const workingPatternsAbsent = msSinceLastWorking >= RespawnController.MIN_WORKING_PATTERN_ABSENCE_MS;
|
||||
const workingPatternsAbsent =
|
||||
msSinceLastWorking >= RespawnController.MIN_WORKING_PATTERN_ABSENCE_MS;
|
||||
|
||||
// Calculate confidence level (0-100) using configurable weights
|
||||
// P2-001: Configurable confidence scoring
|
||||
@@ -1200,7 +1243,7 @@ export class RespawnController extends EventEmitter {
|
||||
const prevState = this._state;
|
||||
this._state = newState;
|
||||
this.stateEnteredAt = Date.now();
|
||||
this.stuckStateWarned = false; // Reset warning for new state
|
||||
this.stuckStateWarned = false; // Reset warning for new state
|
||||
this.log(`State: ${prevState} → ${newState}`);
|
||||
this.logAction('state', `${prevState} → ${newState}`);
|
||||
this.emit('stateChanged', newState, prevState);
|
||||
@@ -1458,7 +1501,10 @@ export class RespawnController extends EventEmitter {
|
||||
// Completion message during these states is ignored
|
||||
break;
|
||||
default:
|
||||
assertNever(this._state, `Unhandled RespawnState in completion detection: ${this._state}`);
|
||||
assertNever(
|
||||
this._state,
|
||||
`Unhandled RespawnState in completion detection: ${this._state}`
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1513,12 +1559,16 @@ export class RespawnController extends EventEmitter {
|
||||
const stripped = data.replace(ANSI_ESCAPE_PATTERN_SIMPLE, '').trim();
|
||||
if (stripped.length > 2) {
|
||||
if (this._state === 'ai_checking') {
|
||||
this.log(`Substantial output during AI check ("${stripped.substring(0, 40)}..."), cancelling`);
|
||||
this.log(
|
||||
`Substantial output during AI check ("${stripped.substring(0, 40)}..."), cancelling`
|
||||
);
|
||||
this.aiChecker.cancel();
|
||||
this.setState('watching');
|
||||
} else {
|
||||
// Real content (not just escape codes or single chars) - cancel confirmation
|
||||
this.log(`Substantial output during confirmation ("${stripped.substring(0, 40)}..."), cancelling idle detection`);
|
||||
this.log(
|
||||
`Substantial output during confirmation ("${stripped.substring(0, 40)}..."), cancelling idle detection`
|
||||
);
|
||||
this.cancelCompletionConfirm();
|
||||
}
|
||||
return;
|
||||
@@ -1526,7 +1576,7 @@ export class RespawnController extends EventEmitter {
|
||||
}
|
||||
|
||||
// Legacy fallback: detect prompt characters (still useful for waiting_* states)
|
||||
const hasPrompt = this.PROMPT_PATTERNS.some(pattern => data.includes(pattern));
|
||||
const hasPrompt = this.PROMPT_PATTERNS.some((pattern) => data.includes(pattern));
|
||||
if (hasPrompt) {
|
||||
this.promptDetected = true;
|
||||
this.workingDetected = false;
|
||||
@@ -1706,7 +1756,7 @@ export class RespawnController extends EventEmitter {
|
||||
if (this._state === 'stopped') return;
|
||||
const prompt = this.config.kickstartPrompt!;
|
||||
this.logAction('command', `Sending kickstart: "${prompt.substring(0, 40)}..."`);
|
||||
await this.session.writeViaMux(prompt + '\r'); // \r triggers key.return in Ink/Claude CLI
|
||||
await this.session.writeViaMux(prompt + '\r'); // \r triggers key.return in Ink/Claude CLI
|
||||
this.emit('stepSent', 'kickstart', prompt);
|
||||
this.setState('waiting_kickstart');
|
||||
this.promptDetected = false;
|
||||
@@ -1830,12 +1880,20 @@ export class RespawnController extends EventEmitter {
|
||||
if (durationMs >= this.config.stuckStateRecoveryMs) {
|
||||
if (this.stuckRecoveryCount < this.config.maxStuckRecoveries) {
|
||||
this.stuckRecoveryCount++;
|
||||
this.logAction('stuck', `Recovery attempt ${this.stuckRecoveryCount}/${this.config.maxStuckRecoveries}`);
|
||||
this.log(`Stuck-state recovery triggered (state: ${this._state}, duration: ${Math.round(durationMs / 1000)}s, attempt: ${this.stuckRecoveryCount})`);
|
||||
this.logAction(
|
||||
'stuck',
|
||||
`Recovery attempt ${this.stuckRecoveryCount}/${this.config.maxStuckRecoveries}`
|
||||
);
|
||||
this.log(
|
||||
`Stuck-state recovery triggered (state: ${this._state}, duration: ${Math.round(durationMs / 1000)}s, attempt: ${this.stuckRecoveryCount})`
|
||||
);
|
||||
this.emit('stuckStateRecovery', this._state, durationMs, this.stuckRecoveryCount);
|
||||
this.handleStuckStateRecovery();
|
||||
} else {
|
||||
this.logAction('stuck', `Max recoveries (${this.config.maxStuckRecoveries}) reached - manual intervention needed`);
|
||||
this.logAction(
|
||||
'stuck',
|
||||
`Max recoveries (${this.config.maxStuckRecoveries}) reached - manual intervention needed`
|
||||
);
|
||||
this.log(`Stuck-state: max recoveries reached, manual intervention needed`);
|
||||
}
|
||||
return;
|
||||
@@ -1844,8 +1902,13 @@ export class RespawnController extends EventEmitter {
|
||||
// Check for warning threshold
|
||||
if (durationMs >= this.config.stuckStateWarningMs && !this.stuckStateWarned) {
|
||||
this.stuckStateWarned = true;
|
||||
this.logAction('stuck', `Warning: in state '${this._state}' for ${Math.round(durationMs / 1000)}s`);
|
||||
this.log(`Stuck-state warning: state '${this._state}' for ${Math.round(durationMs / 1000)}s without progress`);
|
||||
this.logAction(
|
||||
'stuck',
|
||||
`Warning: in state '${this._state}' for ${Math.round(durationMs / 1000)}s`
|
||||
);
|
||||
this.log(
|
||||
`Stuck-state warning: state '${this._state}' for ${Math.round(durationMs / 1000)}s without progress`
|
||||
);
|
||||
this.emit('stuckStateWarning', this._state, durationMs);
|
||||
}
|
||||
}
|
||||
@@ -1961,7 +2024,10 @@ export class RespawnController extends EventEmitter {
|
||||
|
||||
this.activeTimers.set(name, { name, startedAt: now, durationMs, endsAt });
|
||||
this.emit('timerStarted', { name, durationMs, endsAt, reason });
|
||||
this.logAction('timer', `Started ${name}: ${Math.round(durationMs / 1000)}s${reason ? ` (${reason})` : ''}`);
|
||||
this.logAction(
|
||||
'timer',
|
||||
`Started ${name}: ${Math.round(durationMs / 1000)}s${reason ? ` (${reason})` : ''}`
|
||||
);
|
||||
|
||||
return setTimeout(() => {
|
||||
this.activeTimers.delete(name);
|
||||
@@ -1989,7 +2055,7 @@ export class RespawnController extends EventEmitter {
|
||||
*/
|
||||
getActiveTimers(): ActiveTimerInfo[] {
|
||||
const now = Date.now();
|
||||
return Array.from(this.activeTimers.values()).map(t => ({
|
||||
return Array.from(this.activeTimers.values()).map((t) => ({
|
||||
name: t.name,
|
||||
remainingMs: Math.max(0, t.endsAt - now),
|
||||
totalMs: t.durationMs,
|
||||
@@ -2034,11 +2100,13 @@ export class RespawnController extends EventEmitter {
|
||||
// Always update the rolling window first to maintain continuity
|
||||
this.workingPatternWindow += data;
|
||||
if (this.workingPatternWindow.length > RespawnController.WORKING_PATTERN_WINDOW_SIZE) {
|
||||
this.workingPatternWindow = this.workingPatternWindow.slice(-RespawnController.WORKING_PATTERN_WINDOW_SIZE);
|
||||
this.workingPatternWindow = this.workingPatternWindow.slice(
|
||||
-RespawnController.WORKING_PATTERN_WINDOW_SIZE
|
||||
);
|
||||
}
|
||||
|
||||
// Check the rolling window (includes current data, catches both complete and split patterns)
|
||||
return this.WORKING_PATTERNS.some(pattern => this.workingPatternWindow.includes(pattern));
|
||||
return this.WORKING_PATTERNS.some((pattern) => this.workingPatternWindow.includes(pattern));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2081,7 +2149,10 @@ export class RespawnController extends EventEmitter {
|
||||
this.noOutputTimer = null;
|
||||
if (this._state === 'watching' || this._state === 'confirming_idle') {
|
||||
const msSinceOutput = Date.now() - this.lastOutputTime;
|
||||
this.logAction('detection', `No-output fallback: ${Math.round(msSinceOutput / 1000)}s silence`);
|
||||
this.logAction(
|
||||
'detection',
|
||||
`No-output fallback: ${Math.round(msSinceOutput / 1000)}s silence`
|
||||
);
|
||||
// If AI check is disabled or errored out, go directly to idle
|
||||
if (!this.config.aiIdleCheckEnabled || this.aiChecker.status === 'disabled') {
|
||||
this.onIdleConfirmed('no-output fallback (AI check disabled)');
|
||||
@@ -2134,7 +2205,10 @@ export class RespawnController extends EventEmitter {
|
||||
const tokensStableMet = msSinceTokenChange >= this.config.completionConfirmMs;
|
||||
|
||||
if (silenceMet && noWorkingMet && tokensStableMet) {
|
||||
this.logAction('detection', `Pre-filter passed: silence=${Math.round(msSinceOutput / 1000)}s`);
|
||||
this.logAction(
|
||||
'detection',
|
||||
`Pre-filter passed: silence=${Math.round(msSinceOutput / 1000)}s`
|
||||
);
|
||||
this.tryStartAiCheck('pre-filter');
|
||||
}
|
||||
}
|
||||
@@ -2168,14 +2242,18 @@ export class RespawnController extends EventEmitter {
|
||||
|
||||
// If AI check is disabled or errored out, fall back to direct idle confirmation
|
||||
if (!this.config.aiIdleCheckEnabled || this.aiChecker.status === 'disabled') {
|
||||
this.log(`AI check unavailable (${this.aiChecker.status}), confirming idle directly via: ${reason}`);
|
||||
this.log(
|
||||
`AI check unavailable (${this.aiChecker.status}), confirming idle directly via: ${reason}`
|
||||
);
|
||||
this.onIdleConfirmed(reason);
|
||||
return;
|
||||
}
|
||||
|
||||
// If on cooldown, don't start check - wait for cooldown to expire
|
||||
if (this.aiChecker.isOnCooldown()) {
|
||||
this.log(`AI check on cooldown (${Math.ceil(this.aiChecker.getCooldownRemainingMs() / 1000)}s remaining), waiting...`);
|
||||
this.log(
|
||||
`AI check on cooldown (${Math.ceil(this.aiChecker.getCooldownRemainingMs() / 1000)}s remaining), waiting...`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2207,67 +2285,80 @@ export class RespawnController extends EventEmitter {
|
||||
// Get the terminal buffer for analysis
|
||||
const buffer = this.terminalBuffer.value;
|
||||
|
||||
this.aiChecker.check(buffer).then((result) => {
|
||||
// If state changed while checking (e.g., cancelled), ignore result
|
||||
if (this._state !== 'ai_checking') {
|
||||
this.log(`AI check result ignored (state is now ${this._state})`);
|
||||
return;
|
||||
}
|
||||
this.aiChecker
|
||||
.check(buffer)
|
||||
.then((result) => {
|
||||
// If state changed while checking (e.g., cancelled), ignore result
|
||||
if (this._state !== 'ai_checking') {
|
||||
this.log(`AI check result ignored (state is now ${this._state})`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate this is the result for the current check (not a stale one)
|
||||
if (this._currentAiCheckId !== checkId) {
|
||||
this.log(`AI check result ignored (stale check ID: ${checkId.substring(0, 8)})`);
|
||||
return;
|
||||
}
|
||||
// Validate this is the result for the current check (not a stale one)
|
||||
if (this._currentAiCheckId !== checkId) {
|
||||
this.log(`AI check result ignored (stale check ID: ${checkId.substring(0, 8)})`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.verdict === 'IDLE') {
|
||||
// Cancel any pending confirmation timers - AI has spoken
|
||||
this.cancelTrackedTimer('completion-confirm', this.completionConfirmTimer, 'AI verdict: IDLE');
|
||||
this.completionConfirmTimer = null;
|
||||
this.cancelTrackedTimer('pre-filter', this.preFilterTimer, 'AI verdict: IDLE');
|
||||
this.preFilterTimer = null;
|
||||
if (result.verdict === 'IDLE') {
|
||||
// Cancel any pending confirmation timers - AI has spoken
|
||||
this.cancelTrackedTimer(
|
||||
'completion-confirm',
|
||||
this.completionConfirmTimer,
|
||||
'AI verdict: IDLE'
|
||||
);
|
||||
this.completionConfirmTimer = null;
|
||||
this.cancelTrackedTimer('pre-filter', this.preFilterTimer, 'AI verdict: IDLE');
|
||||
this.preFilterTimer = null;
|
||||
|
||||
this.logAction('ai-check', `Verdict: IDLE - ${result.reasoning}`);
|
||||
this.emit('aiCheckCompleted', result);
|
||||
this.onIdleConfirmed(`ai-check: idle (${result.reasoning})`);
|
||||
} else if (result.verdict === 'WORKING') {
|
||||
// Cancel timers and go to cooldown
|
||||
this.cancelTrackedTimer('completion-confirm', this.completionConfirmTimer, 'AI verdict: WORKING');
|
||||
this.completionConfirmTimer = null;
|
||||
this.logAction('ai-check', `Verdict: IDLE - ${result.reasoning}`);
|
||||
this.emit('aiCheckCompleted', result);
|
||||
this.onIdleConfirmed(`ai-check: idle (${result.reasoning})`);
|
||||
} else if (result.verdict === 'WORKING') {
|
||||
// Cancel timers and go to cooldown
|
||||
this.cancelTrackedTimer(
|
||||
'completion-confirm',
|
||||
this.completionConfirmTimer,
|
||||
'AI verdict: WORKING'
|
||||
);
|
||||
this.completionConfirmTimer = null;
|
||||
|
||||
this.logAction('ai-check', `Verdict: WORKING - ${result.reasoning}`);
|
||||
this.emit('aiCheckCompleted', result);
|
||||
this.setState('watching');
|
||||
this.log(`AI check says WORKING, returning to watching with ${this.config.aiIdleCheckCooldownMs}ms cooldown`);
|
||||
// Restart timers so the controller retries after cooldown expires
|
||||
this.startNoOutputTimer();
|
||||
this.startPreFilterTimer();
|
||||
} else {
|
||||
// ERROR verdict
|
||||
this.logAction('ai-check', `Error: ${result.reasoning}`);
|
||||
this.emit('aiCheckFailed', result.reasoning);
|
||||
this.setState('watching');
|
||||
// Restart timers to allow retry
|
||||
this.startNoOutputTimer();
|
||||
this.startPreFilterTimer();
|
||||
}
|
||||
}).catch((err) => {
|
||||
// Validate this is the error for the current check
|
||||
if (this._currentAiCheckId !== checkId) {
|
||||
return; // Stale check, ignore error
|
||||
}
|
||||
if (this._state === 'stopped') return; // Guard against stopped state
|
||||
if (this._state === 'ai_checking') {
|
||||
const errorMsg = err instanceof Error ? err.message : String(err);
|
||||
this.logAction('ai-check', `Failed: ${errorMsg.substring(0, 50)}`);
|
||||
this.emit('aiCheckFailed', errorMsg);
|
||||
this.setState('watching');
|
||||
this.log(`AI check error: ${errorMsg}`);
|
||||
// Restart timers to allow retry
|
||||
this.startNoOutputTimer();
|
||||
this.startPreFilterTimer();
|
||||
}
|
||||
});
|
||||
this.logAction('ai-check', `Verdict: WORKING - ${result.reasoning}`);
|
||||
this.emit('aiCheckCompleted', result);
|
||||
this.setState('watching');
|
||||
this.log(
|
||||
`AI check says WORKING, returning to watching with ${this.config.aiIdleCheckCooldownMs}ms cooldown`
|
||||
);
|
||||
// Restart timers so the controller retries after cooldown expires
|
||||
this.startNoOutputTimer();
|
||||
this.startPreFilterTimer();
|
||||
} else {
|
||||
// ERROR verdict
|
||||
this.logAction('ai-check', `Error: ${result.reasoning}`);
|
||||
this.emit('aiCheckFailed', result.reasoning);
|
||||
this.setState('watching');
|
||||
// Restart timers to allow retry
|
||||
this.startNoOutputTimer();
|
||||
this.startPreFilterTimer();
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
// Validate this is the error for the current check
|
||||
if (this._currentAiCheckId !== checkId) {
|
||||
return; // Stale check, ignore error
|
||||
}
|
||||
if (this._state === 'stopped') return; // Guard against stopped state
|
||||
if (this._state === 'ai_checking') {
|
||||
const errorMsg = err instanceof Error ? err.message : String(err);
|
||||
this.logAction('ai-check', `Failed: ${errorMsg.substring(0, 50)}`);
|
||||
this.emit('aiCheckFailed', errorMsg);
|
||||
this.setState('watching');
|
||||
this.log(`AI check error: ${errorMsg}`);
|
||||
// Restart timers to allow retry
|
||||
this.startNoOutputTimer();
|
||||
this.startPreFilterTimer();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ========== Auto-Accept Prompt Methods ==========
|
||||
@@ -2351,7 +2442,9 @@ export class RespawnController extends EventEmitter {
|
||||
// Stage 2: AI confirmation (if enabled and available)
|
||||
if (this.config.aiPlanCheckEnabled && this.planChecker.status !== 'disabled') {
|
||||
if (this.planChecker.isOnCooldown()) {
|
||||
this.log(`Skipping auto-accept: plan checker on cooldown (${Math.ceil(this.planChecker.getCooldownRemainingMs() / 1000)}s remaining)`);
|
||||
this.log(
|
||||
`Skipping auto-accept: plan checker on cooldown (${Math.ceil(this.planChecker.getCooldownRemainingMs() / 1000)}s remaining)`
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (this.planChecker.status === 'checking') {
|
||||
@@ -2396,7 +2489,7 @@ export class RespawnController extends EventEmitter {
|
||||
// Working patterns before the selector are from earlier work and don't matter.
|
||||
const selectorIndex = stripped.lastIndexOf(selectorMatch[0]);
|
||||
const afterSelector = stripped.slice(selectorIndex + selectorMatch[0].length);
|
||||
const hasWorking = this.WORKING_PATTERNS.some(pattern => afterSelector.includes(pattern));
|
||||
const hasWorking = this.WORKING_PATTERNS.some((pattern) => afterSelector.includes(pattern));
|
||||
if (hasWorking) return false;
|
||||
|
||||
return true;
|
||||
@@ -2416,36 +2509,42 @@ export class RespawnController extends EventEmitter {
|
||||
this.logAction('plan-check', 'Spawning AI plan checker');
|
||||
this.emit('planCheckStarted');
|
||||
|
||||
this.planChecker.check(buffer).then((result) => {
|
||||
// Discard stale result if new output arrived during check
|
||||
if (this.lastOutputTime > this.planCheckStartTime) {
|
||||
this.logAction('plan-check', 'Result discarded (output arrived during check)');
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.verdict === 'PLAN_MODE') {
|
||||
// Don't send Enter if state changed (e.g., AI idle check started or respawn cycle began)
|
||||
if (this._state !== 'watching') {
|
||||
this.logAction('plan-check', `Verdict: PLAN_MODE but state is ${this._state}, not sending Enter`);
|
||||
this.planChecker
|
||||
.check(buffer)
|
||||
.then((result) => {
|
||||
// Discard stale result if new output arrived during check
|
||||
if (this.lastOutputTime > this.planCheckStartTime) {
|
||||
this.logAction('plan-check', 'Result discarded (output arrived during check)');
|
||||
return;
|
||||
}
|
||||
this.emit('planCheckCompleted', result);
|
||||
this.logAction('plan-check', 'Verdict: PLAN_MODE - sending Enter immediately');
|
||||
this.sendAutoAcceptEnter();
|
||||
// No cooldown needed - we're taking action
|
||||
} else if (result.verdict === 'NOT_PLAN_MODE') {
|
||||
this.emit('planCheckCompleted', result);
|
||||
this.logAction('plan-check', `Verdict: NOT_PLAN_MODE - ${result.reasoning}`);
|
||||
} else {
|
||||
// ERROR verdict
|
||||
this.emit('planCheckFailed', result.reasoning);
|
||||
this.logAction('plan-check', `Error: ${result.reasoning}`);
|
||||
}
|
||||
}).catch((err) => {
|
||||
const errorMsg = err instanceof Error ? err.message : String(err);
|
||||
this.emit('planCheckFailed', errorMsg);
|
||||
this.logAction('plan-check', `Failed: ${errorMsg.substring(0, 50)}`);
|
||||
});
|
||||
|
||||
if (result.verdict === 'PLAN_MODE') {
|
||||
// Don't send Enter if state changed (e.g., AI idle check started or respawn cycle began)
|
||||
if (this._state !== 'watching') {
|
||||
this.logAction(
|
||||
'plan-check',
|
||||
`Verdict: PLAN_MODE but state is ${this._state}, not sending Enter`
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.emit('planCheckCompleted', result);
|
||||
this.logAction('plan-check', 'Verdict: PLAN_MODE - sending Enter immediately');
|
||||
this.sendAutoAcceptEnter();
|
||||
// No cooldown needed - we're taking action
|
||||
} else if (result.verdict === 'NOT_PLAN_MODE') {
|
||||
this.emit('planCheckCompleted', result);
|
||||
this.logAction('plan-check', `Verdict: NOT_PLAN_MODE - ${result.reasoning}`);
|
||||
} else {
|
||||
// ERROR verdict
|
||||
this.emit('planCheckFailed', result.reasoning);
|
||||
this.logAction('plan-check', `Error: ${result.reasoning}`);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
const errorMsg = err instanceof Error ? err.message : String(err);
|
||||
this.emit('planCheckFailed', errorMsg);
|
||||
this.logAction('plan-check', `Failed: ${errorMsg.substring(0, 50)}`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2455,7 +2554,9 @@ export class RespawnController extends EventEmitter {
|
||||
*/
|
||||
private sendAutoAcceptEnter(): void {
|
||||
const msSinceOutput = Date.now() - this.lastOutputTime;
|
||||
this.log(`Auto-accepting plan mode prompt (${msSinceOutput}ms silence, pre-filter + AI confirmed)`);
|
||||
this.log(
|
||||
`Auto-accepting plan mode prompt (${msSinceOutput}ms silence, pre-filter + AI confirmed)`
|
||||
);
|
||||
|
||||
// Cancel any pending AI idle checks - we're about to make Claude work
|
||||
if (this.aiChecker.status === 'checking') {
|
||||
@@ -2501,7 +2602,11 @@ export class RespawnController extends EventEmitter {
|
||||
*/
|
||||
signalStopHook(): void {
|
||||
// Only process in states where we're watching for idle
|
||||
if (this._state !== 'watching' && this._state !== 'confirming_idle' && this._state !== 'ai_checking') {
|
||||
if (
|
||||
this._state !== 'watching' &&
|
||||
this._state !== 'confirming_idle' &&
|
||||
this._state !== 'ai_checking'
|
||||
) {
|
||||
this.log(`Stop hook received but ignoring (state is ${this._state})`);
|
||||
return;
|
||||
}
|
||||
@@ -2519,7 +2624,11 @@ export class RespawnController extends EventEmitter {
|
||||
}
|
||||
|
||||
// Cancel completion confirm timer - hook takes precedence
|
||||
this.cancelTrackedTimer('completion-confirm', this.completionConfirmTimer, 'Stop hook received');
|
||||
this.cancelTrackedTimer(
|
||||
'completion-confirm',
|
||||
this.completionConfirmTimer,
|
||||
'Stop hook received'
|
||||
);
|
||||
this.completionConfirmTimer = null;
|
||||
|
||||
// Cancel pre-filter timer - hook takes precedence
|
||||
@@ -2540,7 +2649,11 @@ export class RespawnController extends EventEmitter {
|
||||
*/
|
||||
signalIdlePrompt(): void {
|
||||
// Only process in states where we're watching for idle
|
||||
if (this._state !== 'watching' && this._state !== 'confirming_idle' && this._state !== 'ai_checking') {
|
||||
if (
|
||||
this._state !== 'watching' &&
|
||||
this._state !== 'confirming_idle' &&
|
||||
this._state !== 'ai_checking'
|
||||
) {
|
||||
this.log(`idle_prompt received but ignoring (state is ${this._state})`);
|
||||
return;
|
||||
}
|
||||
@@ -2558,7 +2671,11 @@ export class RespawnController extends EventEmitter {
|
||||
}
|
||||
|
||||
// Cancel all other detection timers - this is definitive
|
||||
this.cancelTrackedTimer('completion-confirm', this.completionConfirmTimer, 'idle_prompt received');
|
||||
this.cancelTrackedTimer(
|
||||
'completion-confirm',
|
||||
this.completionConfirmTimer,
|
||||
'idle_prompt received'
|
||||
);
|
||||
this.completionConfirmTimer = null;
|
||||
this.cancelTrackedTimer('pre-filter', this.preFilterTimer, 'idle_prompt received');
|
||||
this.preFilterTimer = null;
|
||||
@@ -2604,7 +2721,10 @@ export class RespawnController extends EventEmitter {
|
||||
}
|
||||
|
||||
// No new output - confirm idle via hook signal
|
||||
this.logAction('hook', `${hookType} confirmed after ${RespawnController.HOOK_CONFIRM_DELAY_MS}ms`);
|
||||
this.logAction(
|
||||
'hook',
|
||||
`${hookType} confirmed after ${RespawnController.HOOK_CONFIRM_DELAY_MS}ms`
|
||||
);
|
||||
this.onIdleConfirmed(`${hookType} hook (confirmed)`);
|
||||
},
|
||||
`confirming ${hookType} hook`
|
||||
@@ -2715,7 +2835,10 @@ export class RespawnController extends EventEmitter {
|
||||
const msSinceOutput = Date.now() - this.lastOutputTime;
|
||||
|
||||
if (msSinceOutput >= this.config.completionConfirmMs) {
|
||||
this.logAction('step', `${step} confirmed after ${Math.round(msSinceOutput / 1000)}s silence`);
|
||||
this.logAction(
|
||||
'step',
|
||||
`${step} confirmed after ${Math.round(msSinceOutput / 1000)}s silence`
|
||||
);
|
||||
|
||||
// Proceed with the step completion
|
||||
switch (step) {
|
||||
@@ -2755,7 +2878,9 @@ export class RespawnController extends EventEmitter {
|
||||
// Safety check: if Session thinks it's still working, don't trigger idle
|
||||
// This catches cases where our detection missed working patterns
|
||||
if (this.session.isWorking) {
|
||||
this.log(`Idle confirmation rejected - Session reports isWorking=true (reason was: ${reason})`);
|
||||
this.log(
|
||||
`Idle confirmation rejected - Session reports isWorking=true (reason was: ${reason})`
|
||||
);
|
||||
this.logAction('detection', 'Rejected: Session still working');
|
||||
this.setState('watching');
|
||||
this.startNoOutputTimer();
|
||||
@@ -2765,11 +2890,13 @@ export class RespawnController extends EventEmitter {
|
||||
|
||||
this.log(`Idle confirmed via: ${reason}`);
|
||||
const status = this.getDetectionStatus();
|
||||
this.log(`Detection status: confidence=${status.confidenceLevel}%, ` +
|
||||
`completion=${status.completionMessageDetected}, ` +
|
||||
`silent=${status.outputSilent}, ` +
|
||||
`tokensStable=${status.tokensStable}, ` +
|
||||
`noWorking=${status.workingPatternsAbsent}`);
|
||||
this.log(
|
||||
`Detection status: confidence=${status.confidenceLevel}%, ` +
|
||||
`completion=${status.completionMessageDetected}, ` +
|
||||
`silent=${status.outputSilent}, ` +
|
||||
`tokensStable=${status.tokensStable}, ` +
|
||||
`noWorking=${status.workingPatternsAbsent}`
|
||||
);
|
||||
|
||||
// ========== Agent Teams Integration ==========
|
||||
// Check if session has active teammates — don't respawn while team is working
|
||||
@@ -2777,7 +2904,10 @@ export class RespawnController extends EventEmitter {
|
||||
const count = this.teamWatcher.getActiveTeammateCount(this.session.id);
|
||||
this.log(`Respawn blocked - ${count} active teammate(s) working`);
|
||||
this.logAction('team', `Active teammates: ${count}`);
|
||||
this.emit('respawnBlocked', { reason: 'active_teammates', details: `${count} teammate(s) still working` });
|
||||
this.emit('respawnBlocked', {
|
||||
reason: 'active_teammates',
|
||||
details: `${count} teammate(s) still working`,
|
||||
});
|
||||
this.setState('watching');
|
||||
this.startNoOutputTimer();
|
||||
this.startPreFilterTimer();
|
||||
@@ -2792,7 +2922,10 @@ export class RespawnController extends EventEmitter {
|
||||
if (circuitBreaker.state === 'OPEN') {
|
||||
this.log(`Respawn blocked - Circuit breaker OPEN: ${circuitBreaker.reason}`);
|
||||
this.logAction('ralph', `Circuit breaker OPEN: ${circuitBreaker.reason}`);
|
||||
this.emit('respawnBlocked', { reason: 'circuit_breaker_open', details: circuitBreaker.reason });
|
||||
this.emit('respawnBlocked', {
|
||||
reason: 'circuit_breaker_open',
|
||||
details: circuitBreaker.reason,
|
||||
});
|
||||
this.setState('watching');
|
||||
// Don't restart timers - wait for manual reset or circuit breaker resolution
|
||||
return;
|
||||
@@ -2803,8 +2936,14 @@ export class RespawnController extends EventEmitter {
|
||||
const statusBlock = ralphTracker?.lastStatusBlock;
|
||||
if (statusBlock?.exitSignal) {
|
||||
this.log(`Respawn paused - RALPH_STATUS EXIT_SIGNAL=true`);
|
||||
this.logAction('ralph', `Exit signal detected: ${statusBlock.recommendation || 'Task complete'}`);
|
||||
this.emit('respawnBlocked', { reason: 'exit_signal', details: statusBlock.recommendation || 'Task complete' });
|
||||
this.logAction(
|
||||
'ralph',
|
||||
`Exit signal detected: ${statusBlock.recommendation || 'Task complete'}`
|
||||
);
|
||||
this.emit('respawnBlocked', {
|
||||
reason: 'exit_signal',
|
||||
details: statusBlock.recommendation || 'Task complete',
|
||||
});
|
||||
this.setState('watching');
|
||||
// Don't restart timers - loop is complete
|
||||
return;
|
||||
@@ -2813,8 +2952,14 @@ export class RespawnController extends EventEmitter {
|
||||
// Check if STATUS=BLOCKED - trigger circuit breaker
|
||||
if (statusBlock?.status === 'BLOCKED') {
|
||||
this.log(`Respawn blocked - RALPH_STATUS reports BLOCKED`);
|
||||
this.logAction('ralph', `Claude reported BLOCKED: ${statusBlock.recommendation || 'Needs human intervention'}`);
|
||||
this.emit('respawnBlocked', { reason: 'status_blocked', details: statusBlock.recommendation || 'Needs human intervention' });
|
||||
this.logAction(
|
||||
'ralph',
|
||||
`Claude reported BLOCKED: ${statusBlock.recommendation || 'Needs human intervention'}`
|
||||
);
|
||||
this.emit('respawnBlocked', {
|
||||
reason: 'status_blocked',
|
||||
details: statusBlock.recommendation || 'Needs human intervention',
|
||||
});
|
||||
this.setState('watching');
|
||||
return;
|
||||
}
|
||||
@@ -2834,7 +2979,11 @@ export class RespawnController extends EventEmitter {
|
||||
*/
|
||||
private onIdleDetected(): void {
|
||||
// Accept watching, confirming_idle, and ai_checking states
|
||||
if (this._state !== 'watching' && this._state !== 'confirming_idle' && this._state !== 'ai_checking') {
|
||||
if (
|
||||
this._state !== 'watching' &&
|
||||
this._state !== 'confirming_idle' &&
|
||||
this._state !== 'ai_checking'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2846,7 +2995,10 @@ export class RespawnController extends EventEmitter {
|
||||
if (this.session.status === 'error') {
|
||||
this.log('Skipping respawn cycle - session is in error state');
|
||||
this.logAction('health', 'Respawn skipped: Session error state');
|
||||
this.emit('respawnBlocked', { reason: 'session_error', details: 'Session is in error state' });
|
||||
this.emit('respawnBlocked', {
|
||||
reason: 'session_error',
|
||||
details: 'Session is in error state',
|
||||
});
|
||||
this.setState('watching');
|
||||
return;
|
||||
}
|
||||
@@ -2907,7 +3059,7 @@ export class RespawnController extends EventEmitter {
|
||||
this.logAction('ralph', `Using RECOMMENDATION: ${rec.substring(0, 50)}...`);
|
||||
}
|
||||
|
||||
const input = updatePrompt + '\r'; // \r triggers Enter in Ink/Claude CLI
|
||||
const input = updatePrompt + '\r'; // \r triggers Enter in Ink/Claude CLI
|
||||
this.logAction('command', `Sending: "${updatePrompt.substring(0, 50)}..."`);
|
||||
await this.session.writeViaMux(input);
|
||||
this.emit('stepSent', 'update', updatePrompt);
|
||||
@@ -2937,7 +3089,7 @@ export class RespawnController extends EventEmitter {
|
||||
this.stepTimer = null;
|
||||
if (this._state === 'stopped') return;
|
||||
this.logAction('command', 'Sending: /clear');
|
||||
await this.session.writeViaMux('/clear\r'); // \r triggers Enter in Ink/Claude CLI
|
||||
await this.session.writeViaMux('/clear\r'); // \r triggers Enter in Ink/Claude CLI
|
||||
this.emit('stepSent', 'clear', '/clear');
|
||||
this.setState('waiting_clear');
|
||||
this.promptDetected = false;
|
||||
@@ -2981,7 +3133,7 @@ export class RespawnController extends EventEmitter {
|
||||
this.stepTimer = null;
|
||||
if (this._state === 'stopped') return;
|
||||
this.logAction('command', 'Sending: /init');
|
||||
await this.session.writeViaMux('/init\r'); // \r triggers Enter in Ink/Claude CLI
|
||||
await this.session.writeViaMux('/init\r'); // \r triggers Enter in Ink/Claude CLI
|
||||
this.emit('stepSent', 'init', '/init');
|
||||
this.setState('waiting_init');
|
||||
this.promptDetected = false;
|
||||
@@ -3052,9 +3204,13 @@ export class RespawnController extends EventEmitter {
|
||||
this.config = { ...this.config, ...filteredConfig };
|
||||
|
||||
// Sync AI checker config if relevant fields changed
|
||||
if (config.aiIdleCheckEnabled !== undefined || config.aiIdleCheckModel !== undefined ||
|
||||
config.aiIdleCheckMaxContext !== undefined || config.aiIdleCheckTimeoutMs !== undefined ||
|
||||
config.aiIdleCheckCooldownMs !== undefined) {
|
||||
if (
|
||||
config.aiIdleCheckEnabled !== undefined ||
|
||||
config.aiIdleCheckModel !== undefined ||
|
||||
config.aiIdleCheckMaxContext !== undefined ||
|
||||
config.aiIdleCheckTimeoutMs !== undefined ||
|
||||
config.aiIdleCheckCooldownMs !== undefined
|
||||
) {
|
||||
this.aiChecker.updateConfig({
|
||||
enabled: this.config.aiIdleCheckEnabled,
|
||||
model: this.config.aiIdleCheckModel,
|
||||
@@ -3065,9 +3221,13 @@ export class RespawnController extends EventEmitter {
|
||||
}
|
||||
|
||||
// Sync plan checker config if relevant fields changed
|
||||
if (config.aiPlanCheckEnabled !== undefined || config.aiPlanCheckModel !== undefined ||
|
||||
config.aiPlanCheckMaxContext !== undefined || config.aiPlanCheckTimeoutMs !== undefined ||
|
||||
config.aiPlanCheckCooldownMs !== undefined) {
|
||||
if (
|
||||
config.aiPlanCheckEnabled !== undefined ||
|
||||
config.aiPlanCheckModel !== undefined ||
|
||||
config.aiPlanCheckMaxContext !== undefined ||
|
||||
config.aiPlanCheckTimeoutMs !== undefined ||
|
||||
config.aiPlanCheckCooldownMs !== undefined
|
||||
) {
|
||||
this.planChecker.updateConfig({
|
||||
enabled: this.config.aiPlanCheckEnabled,
|
||||
model: this.config.aiPlanCheckModel,
|
||||
@@ -3223,7 +3383,9 @@ export class RespawnController extends EventEmitter {
|
||||
const usagePercent = (currentTokens / maxContext) * 100;
|
||||
|
||||
if (usagePercent < thresholdPercent) {
|
||||
this.log(`Skip-clear optimization: ${usagePercent.toFixed(1)}% < ${thresholdPercent}% threshold`);
|
||||
this.log(
|
||||
`Skip-clear optimization: ${usagePercent.toFixed(1)}% < ${thresholdPercent}% threshold`
|
||||
);
|
||||
this.logAction('optimization', `Skipping /clear (${usagePercent.toFixed(1)}% context used)`);
|
||||
return true;
|
||||
}
|
||||
@@ -3276,7 +3438,7 @@ export class RespawnController extends EventEmitter {
|
||||
|
||||
const now = Date.now();
|
||||
const metrics: RespawnCycleMetrics = {
|
||||
...this.currentCycleMetrics as RespawnCycleMetrics,
|
||||
...(this.currentCycleMetrics as RespawnCycleMetrics),
|
||||
completedAt: now,
|
||||
durationMs: now - (this.currentCycleMetrics.startedAt ?? now),
|
||||
outcome,
|
||||
@@ -3299,7 +3461,9 @@ export class RespawnController extends EventEmitter {
|
||||
// Clear current cycle
|
||||
this.currentCycleMetrics = null;
|
||||
|
||||
this.log(`Cycle #${metrics.cycleNumber} metrics: ${outcome}, duration=${metrics.durationMs}ms, idle_detection=${metrics.idleDetectionMs}ms`);
|
||||
this.log(
|
||||
`Cycle #${metrics.cycleNumber} metrics: ${outcome}, duration=${metrics.durationMs}ms, idle_detection=${metrics.idleDetectionMs}ms`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3333,8 +3497,8 @@ export class RespawnController extends EventEmitter {
|
||||
}
|
||||
|
||||
// Recalculate averages using all recent metrics
|
||||
const durations = this.recentCycleMetrics.map(m => m.durationMs);
|
||||
const idleTimes = this.recentCycleMetrics.map(m => m.idleDetectionMs);
|
||||
const durations = this.recentCycleMetrics.map((m) => m.durationMs);
|
||||
const idleTimes = this.recentCycleMetrics.map((m) => m.idleDetectionMs);
|
||||
|
||||
if (durations.length > 0) {
|
||||
agg.avgCycleDurationMs = Math.round(durations.reduce((a, b) => a + b, 0) / durations.length);
|
||||
@@ -3347,9 +3511,8 @@ export class RespawnController extends EventEmitter {
|
||||
}
|
||||
|
||||
// Calculate success rate
|
||||
agg.successRate = agg.totalCycles > 0
|
||||
? Math.round((agg.successfulCycles / agg.totalCycles) * 100)
|
||||
: 100;
|
||||
agg.successRate =
|
||||
agg.totalCycles > 0 ? Math.round((agg.successfulCycles / agg.totalCycles) * 100) : 100;
|
||||
|
||||
agg.lastUpdatedAt = Date.now();
|
||||
}
|
||||
@@ -3392,18 +3555,18 @@ export class RespawnController extends EventEmitter {
|
||||
// Weighted average (cycle success is most important)
|
||||
const weights = {
|
||||
cycleSuccess: 0.35,
|
||||
circuitBreaker: 0.20,
|
||||
iterationProgress: 0.20,
|
||||
circuitBreaker: 0.2,
|
||||
iterationProgress: 0.2,
|
||||
aiChecker: 0.15,
|
||||
stuckRecovery: 0.10,
|
||||
stuckRecovery: 0.1,
|
||||
};
|
||||
|
||||
const score = Math.round(
|
||||
components.cycleSuccess * weights.cycleSuccess +
|
||||
components.circuitBreaker * weights.circuitBreaker +
|
||||
components.iterationProgress * weights.iterationProgress +
|
||||
components.aiChecker * weights.aiChecker +
|
||||
components.stuckRecovery * weights.stuckRecovery
|
||||
components.circuitBreaker * weights.circuitBreaker +
|
||||
components.iterationProgress * weights.iterationProgress +
|
||||
components.aiChecker * weights.aiChecker +
|
||||
components.stuckRecovery * weights.stuckRecovery
|
||||
);
|
||||
|
||||
// Determine status
|
||||
@@ -3446,10 +3609,14 @@ export class RespawnController extends EventEmitter {
|
||||
|
||||
const cb = tracker.circuitBreakerStatus;
|
||||
switch (cb.state) {
|
||||
case 'CLOSED': return 100;
|
||||
case 'HALF_OPEN': return 50;
|
||||
case 'OPEN': return 0;
|
||||
default: return 100;
|
||||
case 'CLOSED':
|
||||
return 100;
|
||||
case 'HALF_OPEN':
|
||||
return 50;
|
||||
case 'OPEN':
|
||||
return 0;
|
||||
default:
|
||||
return 100;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3497,19 +3664,27 @@ export class RespawnController extends EventEmitter {
|
||||
const recommendations: string[] = [];
|
||||
|
||||
if (components.cycleSuccess < 70) {
|
||||
recommendations.push('Cycle success rate is low. Check for recurring errors or stuck states.');
|
||||
recommendations.push(
|
||||
'Cycle success rate is low. Check for recurring errors or stuck states.'
|
||||
);
|
||||
}
|
||||
if (components.circuitBreaker < 50) {
|
||||
recommendations.push('Circuit breaker is open or half-open. Review recent errors and consider manual reset.');
|
||||
recommendations.push(
|
||||
'Circuit breaker is open or half-open. Review recent errors and consider manual reset.'
|
||||
);
|
||||
}
|
||||
if (components.iterationProgress < 50) {
|
||||
recommendations.push('Iteration progress has stalled. Check if Claude is stuck on a task.');
|
||||
}
|
||||
if (components.aiChecker < 50) {
|
||||
recommendations.push('AI idle checker has errors. May need to check Claude CLI availability.');
|
||||
recommendations.push(
|
||||
'AI idle checker has errors. May need to check Claude CLI availability.'
|
||||
);
|
||||
}
|
||||
if (components.stuckRecovery < 50) {
|
||||
recommendations.push('Multiple stuck-state recoveries occurred. Consider increasing timeouts.');
|
||||
recommendations.push(
|
||||
'Multiple stuck-state recoveries occurred. Consider increasing timeouts.'
|
||||
);
|
||||
}
|
||||
|
||||
if (recommendations.length === 0) {
|
||||
@@ -3527,8 +3702,10 @@ export class RespawnController extends EventEmitter {
|
||||
status: HealthStatus,
|
||||
components: RalphLoopHealthScore['components']
|
||||
): string {
|
||||
const lowest = Object.entries(components).reduce((min, [key, val]) =>
|
||||
val < min.val ? { key, val } : min, { key: '', val: 100 });
|
||||
const lowest = Object.entries(components).reduce(
|
||||
(min, [key, val]) => (val < min.val ? { key, val } : min),
|
||||
{ key: '', val: 100 }
|
||||
);
|
||||
|
||||
if (status === 'excellent') {
|
||||
return `Ralph Loop is operating excellently (${score}/100). All systems healthy.`;
|
||||
|
||||
Reference in New Issue
Block a user