mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-04 14:39:42 +02:00
feat: add AI-powered idle check for respawn controller
Replace the "Worked for Xm Xs" pattern as the sole primary idle detection signal with a final AI-powered check. When pre-filter conditions are met (output silence, no working patterns, tokens stable), a fresh Claude CLI session is spawned in a screen to analyze terminal output and provide a definitive IDLE/WORKING verdict before proceeding with the respawn cycle. New state in state machine: `ai_checking` (between pre-filter confirmation and `sending_update`). WORKING verdict triggers a 3-minute cooldown. Errors auto-disable after 3 consecutive failures, falling back to the existing noOutputTimeoutMs safety net. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -939,6 +939,36 @@ class ClaudemanApp {
|
||||
}
|
||||
});
|
||||
|
||||
this.eventSource.addEventListener('respawn:aiCheckStarted', (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data.sessionId === this.activeSessionId) {
|
||||
document.getElementById('respawnStep').textContent = 'AI Check: Analyzing...';
|
||||
}
|
||||
});
|
||||
|
||||
this.eventSource.addEventListener('respawn:aiCheckCompleted', (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data.sessionId === this.activeSessionId) {
|
||||
const icon = data.verdict === 'IDLE' ? '✓' : '⏳';
|
||||
document.getElementById('respawnStep').textContent = `AI Check: ${data.verdict} ${icon}`;
|
||||
}
|
||||
});
|
||||
|
||||
this.eventSource.addEventListener('respawn:aiCheckFailed', (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data.sessionId === this.activeSessionId) {
|
||||
document.getElementById('respawnStep').textContent = `AI Check: Error - ${data.error?.substring(0, 50)}`;
|
||||
}
|
||||
});
|
||||
|
||||
this.eventSource.addEventListener('respawn:aiCheckCooldown', (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data.sessionId === this.activeSessionId && data.active) {
|
||||
const remaining = Math.ceil((data.endsAt - Date.now()) / 1000);
|
||||
document.getElementById('respawnStep').textContent = `AI Check: WORKING (cooldown ${remaining}s)`;
|
||||
}
|
||||
});
|
||||
|
||||
// Respawn timer events
|
||||
this.eventSource.addEventListener('respawn:timerStarted', (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
@@ -1904,6 +1934,26 @@ class ClaudemanApp {
|
||||
} else {
|
||||
confidenceEl.style.display = 'none';
|
||||
}
|
||||
|
||||
// Show AI check info if available
|
||||
const aiCheckEl = document.getElementById('detectionAiCheck');
|
||||
if (aiCheckEl && detection.aiCheck) {
|
||||
const ai = detection.aiCheck;
|
||||
let aiText = '';
|
||||
if (ai.status === 'checking') {
|
||||
aiText = 'AI: Analyzing...';
|
||||
} else if (ai.status === 'cooldown' && ai.cooldownEndsAt) {
|
||||
const remaining = Math.ceil((ai.cooldownEndsAt - Date.now()) / 1000);
|
||||
aiText = `AI: WORKING (${remaining}s cooldown)`;
|
||||
} else if (ai.status === 'disabled') {
|
||||
aiText = `AI: Disabled (${ai.disabledReason || 'errors'})`;
|
||||
} else if (ai.lastVerdict) {
|
||||
aiText = `AI: Last=${ai.lastVerdict}`;
|
||||
if (ai.lastCheckDurationMs) aiText += ` (${Math.round(ai.lastCheckDurationMs / 1000)}s)`;
|
||||
}
|
||||
aiCheckEl.textContent = aiText;
|
||||
aiCheckEl.style.display = aiText ? '' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
showRespawnTimer() {
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
<span class="detection-status" id="detectionStatus"></span>
|
||||
<span class="detection-waiting" id="detectionWaiting"></span>
|
||||
<span class="detection-confidence" id="detectionConfidence"></span>
|
||||
<span class="detection-ai-check" id="detectionAiCheck" style="display: none;"></span>
|
||||
</span>
|
||||
<span class="respawn-cycles">Cycle <span id="respawnCycleCount">0</span></span>
|
||||
<span class="respawn-timer" id="respawnTimer" style="display: none;"></span>
|
||||
|
||||
@@ -815,6 +815,11 @@ export class WebServer extends EventEmitter {
|
||||
kickstartPrompt: config.kickstartPrompt ?? currentConfig?.kickstartPrompt,
|
||||
autoAcceptPrompts: config.autoAcceptPrompts ?? currentConfig?.autoAcceptPrompts ?? true,
|
||||
autoAcceptDelayMs: config.autoAcceptDelayMs ?? currentConfig?.autoAcceptDelayMs ?? 8000,
|
||||
aiIdleCheckEnabled: config.aiIdleCheckEnabled ?? currentConfig?.aiIdleCheckEnabled ?? true,
|
||||
aiIdleCheckModel: config.aiIdleCheckModel ?? currentConfig?.aiIdleCheckModel,
|
||||
aiIdleCheckMaxContext: config.aiIdleCheckMaxContext ?? currentConfig?.aiIdleCheckMaxContext,
|
||||
aiIdleCheckTimeoutMs: config.aiIdleCheckTimeoutMs ?? currentConfig?.aiIdleCheckTimeoutMs,
|
||||
aiIdleCheckCooldownMs: config.aiIdleCheckCooldownMs ?? currentConfig?.aiIdleCheckCooldownMs,
|
||||
durationMinutes: currentConfig?.durationMinutes,
|
||||
};
|
||||
this.screenManager.updateRespawnConfig(id, merged);
|
||||
@@ -1566,6 +1571,11 @@ export class WebServer extends EventEmitter {
|
||||
kickstartPrompt: config.kickstartPrompt,
|
||||
autoAcceptPrompts: config.autoAcceptPrompts,
|
||||
autoAcceptDelayMs: config.autoAcceptDelayMs,
|
||||
aiIdleCheckEnabled: config.aiIdleCheckEnabled,
|
||||
aiIdleCheckModel: config.aiIdleCheckModel,
|
||||
aiIdleCheckMaxContext: config.aiIdleCheckMaxContext,
|
||||
aiIdleCheckTimeoutMs: config.aiIdleCheckTimeoutMs,
|
||||
aiIdleCheckCooldownMs: config.aiIdleCheckCooldownMs,
|
||||
durationMinutes,
|
||||
};
|
||||
this.screenManager.updateRespawnConfig(sessionId, persistedConfig);
|
||||
@@ -1818,6 +1828,22 @@ export class WebServer extends EventEmitter {
|
||||
this.broadcast('respawn:autoAcceptSent', { sessionId });
|
||||
});
|
||||
|
||||
controller.on('aiCheckStarted', () => {
|
||||
this.broadcast('respawn:aiCheckStarted', { sessionId });
|
||||
});
|
||||
|
||||
controller.on('aiCheckCompleted', (result: { verdict: string; reasoning: string; durationMs: number }) => {
|
||||
this.broadcast('respawn:aiCheckCompleted', { sessionId, verdict: result.verdict, reasoning: result.reasoning, durationMs: result.durationMs });
|
||||
});
|
||||
|
||||
controller.on('aiCheckFailed', (error: string) => {
|
||||
this.broadcast('respawn:aiCheckFailed', { sessionId, error });
|
||||
});
|
||||
|
||||
controller.on('aiCheckCooldown', (active: boolean, endsAt: number | null) => {
|
||||
this.broadcast('respawn:aiCheckCooldown', { sessionId, active, endsAt });
|
||||
});
|
||||
|
||||
controller.on('log', (message: string) => {
|
||||
this.broadcast('respawn:log', { sessionId, message });
|
||||
});
|
||||
@@ -2401,6 +2427,11 @@ export class WebServer extends EventEmitter {
|
||||
noOutputTimeoutMs: savedState.respawnConfig.noOutputTimeoutMs,
|
||||
autoAcceptPrompts: savedState.respawnConfig.autoAcceptPrompts ?? true,
|
||||
autoAcceptDelayMs: savedState.respawnConfig.autoAcceptDelayMs ?? 8000,
|
||||
aiIdleCheckEnabled: savedState.respawnConfig.aiIdleCheckEnabled ?? true,
|
||||
aiIdleCheckModel: savedState.respawnConfig.aiIdleCheckModel ?? 'claude-opus-4-5-20251101',
|
||||
aiIdleCheckMaxContext: savedState.respawnConfig.aiIdleCheckMaxContext ?? 16000,
|
||||
aiIdleCheckTimeoutMs: savedState.respawnConfig.aiIdleCheckTimeoutMs ?? 90000,
|
||||
aiIdleCheckCooldownMs: savedState.respawnConfig.aiIdleCheckCooldownMs ?? 180000,
|
||||
});
|
||||
this.respawnControllers.set(session.id, controller);
|
||||
this.setupRespawnListeners(session.id, controller);
|
||||
@@ -2430,6 +2461,11 @@ export class WebServer extends EventEmitter {
|
||||
kickstartPrompt: screen.respawnConfig.kickstartPrompt,
|
||||
autoAcceptPrompts: screen.respawnConfig.autoAcceptPrompts ?? true,
|
||||
autoAcceptDelayMs: screen.respawnConfig.autoAcceptDelayMs ?? 8000,
|
||||
aiIdleCheckEnabled: screen.respawnConfig.aiIdleCheckEnabled ?? true,
|
||||
aiIdleCheckModel: screen.respawnConfig.aiIdleCheckModel ?? 'claude-opus-4-5-20251101',
|
||||
aiIdleCheckMaxContext: screen.respawnConfig.aiIdleCheckMaxContext ?? 16000,
|
||||
aiIdleCheckTimeoutMs: screen.respawnConfig.aiIdleCheckTimeoutMs ?? 90000,
|
||||
aiIdleCheckCooldownMs: screen.respawnConfig.aiIdleCheckCooldownMs ?? 180000,
|
||||
});
|
||||
this.respawnControllers.set(session.id, controller);
|
||||
this.setupRespawnListeners(session.id, controller);
|
||||
|
||||
Reference in New Issue
Block a user