feat: add hook-based idle detection for respawn controller

Implement multi-phase improvement to respawn controller's idle detection:

Phase 1 - Stop Hook Detection:
- Add signalStopHook() method - definitive signal when Claude finishes
- 3s confirmation timer to handle race conditions
- Skip AI check when hook received (100% confidence)

Phase 2 - idle_prompt Detection:
- Add signalIdlePrompt() method - fires after 60s+ of idle
- Immediately confirms idle (no confirmation timer needed)

Phase 3 - Transcript File Monitoring:
- New TranscriptWatcher class watches session JSONL files
- Detects: completion, tool execution, plan mode, errors
- Signals respawn controller for supporting detection

Web UI Updates:
- Hook indicator with purple styling and pulse animation
- Shows "Stop hook received" or "idle_prompt hook received"
- 100% confidence displayed with hook-confirmed style

This significantly improves idle detection reliability by using
definitive signals from Claude Code rather than parsing terminal output.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-25 21:29:35 +01:00
co-authored by Claude Opus 4.5
parent ddc55b7697
commit 1ff1e300f6
9 changed files with 1527 additions and 13 deletions
+21 -2
View File
@@ -2153,6 +2153,19 @@ class ClaudemanApp {
const waitingEl = this.$('detectionWaiting');
const confidenceEl = this.$('detectionConfidence');
const aiCheckEl = document.getElementById('detectionAiCheck');
const hookEl = document.getElementById('detectionHook');
// Hook-based detection indicator (highest priority signals)
if (hookEl) {
if (detection.stopHookReceived || detection.idlePromptReceived) {
const hookType = detection.idlePromptReceived ? 'idle_prompt' : 'Stop';
hookEl.textContent = `🎯 ${hookType} hook received`;
hookEl.className = 'detection-hook hook-active';
hookEl.style.display = '';
} else {
hookEl.style.display = 'none';
}
}
// Simplified status - only show when meaningful
if (detection.statusText && detection.statusText !== 'Watching...') {
@@ -2171,8 +2184,14 @@ class ClaudemanApp {
confidenceEl.textContent = `${confidence}%`;
confidenceEl.style.display = '';
confidenceEl.className = 'detection-confidence';
if (confidence >= 60) confidenceEl.classList.add('high');
else if (confidence >= 30) confidenceEl.classList.add('medium');
// Hook signals give 100% confidence
if (detection.stopHookReceived || detection.idlePromptReceived) {
confidenceEl.classList.add('hook-confirmed');
} else if (confidence >= 60) {
confidenceEl.classList.add('high');
} else if (confidence >= 30) {
confidenceEl.classList.add('medium');
}
} else {
confidenceEl.style.display = 'none';
}
+1
View File
@@ -73,6 +73,7 @@
<span class="detection-status" id="detectionStatus"></span>
<span class="detection-waiting" id="detectionWaiting" style="display: none;"></span>
<span class="detection-confidence" id="detectionConfidence"></span>
<span class="detection-hook" id="detectionHook" style="display: none;"></span>
<span class="detection-ai-check" id="detectionAiCheck" style="display: none;"></span>
<span class="respawn-spacer"></span>
<span class="respawn-cycles">#<span id="respawnCycleCount">0</span></span>
+25
View File
@@ -671,6 +671,31 @@ body {
color: var(--yellow);
}
.detection-confidence.hook-confirmed {
background: rgba(139, 92, 246, 0.3);
color: var(--purple, #a78bfa);
}
.detection-hook {
font-size: 0.7rem;
padding: 0.1rem 0.4rem;
border-radius: 3px;
background: rgba(139, 92, 246, 0.15);
color: var(--purple, #a78bfa);
}
.detection-hook.hook-active {
background: rgba(139, 92, 246, 0.25);
color: var(--purple, #a78bfa);
font-weight: 500;
animation: pulse-hook 1.5s ease-in-out infinite;
}
@keyframes pulse-hook {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
@keyframes spin {
to { transform: rotate(360deg); }
}