fix: rock-solid idle detection in respawn controller

- Add 300-char rolling window to catch working patterns split across PTY chunks
- Check completion message BEFORE working patterns (priority fix)
- Clear rolling window on completion message (transition point)
- Increase working pattern absence threshold from 3s to 8s
- Add Session.isWorking safety check before confirming idle
- Add 20+ more working patterns (Compiling, Building, Processing, etc.)
- Make AI idle checker prompt more conservative (err toward WORKING)
- Update documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-26 00:55:29 +01:00
co-authored by Claude Opus 4.5
parent 5b87a41aac
commit 9aa5973970
21 changed files with 2561 additions and 360 deletions
+17
View File
@@ -0,0 +1,17 @@
#!/bin/bash
# Generates timestamped data to a file for testing
# Usage: ./scripts/data-generator.sh [output_file] [interval_seconds]
# Then: tail -f /tmp/test-data.log
OUTPUT="${1:-/tmp/test-data.log}"
INTERVAL="${2:-1}"
echo "Writing to $OUTPUT every ${INTERVAL}s (Ctrl+C to stop)"
echo "Run: tail -f $OUTPUT"
i=0
while true; do
((i++))
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Line $i - Random: $RANDOM" >> "$OUTPUT"
sleep "$INTERVAL"
done
+124
View File
@@ -0,0 +1,124 @@
#!/bin/bash
# Respawn Controller Monitor - 12 hour monitoring script
# Usage: ./scripts/monitor-respawn.sh <session_id> [duration_hours]
SESSION_ID="${1:-5ecdf859-3605-42d8-b0a2-f8f0a1b5ff98}"
DURATION_HOURS="${2:-12}"
ISSUES_FILE="/home/arkon/default/claudeman/respawn-monitor-issues.md"
FIXES_FILE="/home/arkon/default/claudeman/respawn-monitor-fixes.md"
API_URL="https://localhost:3000"
POLL_INTERVAL=60 # seconds
# Calculate end time
END_TIME=$(($(date +%s) + DURATION_HOURS * 3600))
log_issue() {
local severity="$1"
local message="$2"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "" >> "$ISSUES_FILE"
echo "### [$severity] $timestamp" >> "$ISSUES_FILE"
echo "$message" >> "$ISSUES_FILE"
echo "" >> "$ISSUES_FILE"
}
add_fix() {
local issue="$1"
local fix="$2"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "" >> "$FIXES_FILE"
echo "### Issue: $issue" >> "$FIXES_FILE"
echo "**Detected:** $timestamp" >> "$FIXES_FILE"
echo "" >> "$FIXES_FILE"
echo "**Proposed Fix:**" >> "$FIXES_FILE"
echo "$fix" >> "$FIXES_FILE"
echo "" >> "$FIXES_FILE"
echo "---" >> "$FIXES_FILE"
}
# State tracking
PREV_STATE=""
STATE_STUCK_COUNT=0
PREV_CYCLE=0
LAST_ACTIVITY=0
AI_CHECK_ERRORS=0
echo "Starting respawn monitoring for session $SESSION_ID"
echo "Duration: $DURATION_HOURS hours"
echo "Issues logged to: $ISSUES_FILE"
echo "Fixes logged to: $FIXES_FILE"
while [ $(date +%s) -lt $END_TIME ]; do
# Get respawn status
RESP=$(curl -sk "$API_URL/api/sessions/$SESSION_ID" 2>/dev/null)
if [ -z "$RESP" ]; then
log_issue "ERROR" "API not responding - server may be down"
add_fix "API not responding" "Check if claudeman-web service is running:\n\`\`\`bash\nsystemctl --user status claudeman-web\nsystemctl --user restart claudeman-web\n\`\`\`"
sleep $POLL_INTERVAL
continue
fi
# Parse response
STATE=$(echo "$RESP" | jq -r '.respawn.state // "null"')
CYCLE=$(echo "$RESP" | jq -r '.respawn.cycleCount // 0')
ENABLED=$(echo "$RESP" | jq -r '.respawnEnabled // false')
TOKENS=$(echo "$RESP" | jq -r '.respawn.config.lastTokenCount // 0')
AI_STATUS=$(echo "$RESP" | jq -r '.respawn.detection.aiCheck.status // "unknown"')
AI_ERRORS=$(echo "$RESP" | jq -r '.respawn.detection.aiCheck.consecutiveErrors // 0')
TIME_SINCE_ACTIVITY=$(echo "$RESP" | jq -r '.respawn.timeSinceActivity // 0')
CONFIDENCE=$(echo "$RESP" | jq -r '.respawn.detection.confidenceLevel // 0')
SESSION_STATUS=$(echo "$RESP" | jq -r '.status // "unknown"')
# Check for issues
# 1. Session not running
if [ "$SESSION_STATUS" = "stopped" ] || [ "$SESSION_STATUS" = "error" ]; then
log_issue "CRITICAL" "Session status is '$SESSION_STATUS' - respawn may not function correctly"
add_fix "Session stopped/error" "Check session health and restart if needed:\n\`\`\`bash\ncurl -sk $API_URL/api/sessions/$SESSION_ID\n# If needed, create new session or restart\n\`\`\`"
fi
# 2. Respawn disabled unexpectedly
if [ "$ENABLED" = "false" ] && [ "$PREV_STATE" != "" ] && [ "$PREV_STATE" != "null" ]; then
log_issue "WARNING" "Respawn appears to have been disabled (was in state: $PREV_STATE)"
add_fix "Respawn disabled" "Re-enable respawn controller:\n\`\`\`bash\ncurl -sk -X POST $API_URL/api/sessions/$SESSION_ID/respawn/enable\n\`\`\`"
fi
# 3. State stuck for too long (>30 minutes in same state)
if [ "$STATE" = "$PREV_STATE" ] && [ "$STATE" != "watching" ]; then
STATE_STUCK_COUNT=$((STATE_STUCK_COUNT + 1))
if [ $STATE_STUCK_COUNT -ge 30 ]; then # 30 minutes
log_issue "WARNING" "State stuck in '$STATE' for $STATE_STUCK_COUNT minutes. Detection: confidence=$CONFIDENCE, timeSinceActivity=${TIME_SINCE_ACTIVITY}ms"
add_fix "State stuck in $STATE" "The respawn controller may be stuck. Options:\n1. Check terminal for blocking prompts\n2. Send input to unstick:\n\`\`\`bash\ncurl -sk -X POST $API_URL/api/sessions/$SESSION_ID/input -d '{\"input\": \"\\\\r\"}'\n\`\`\`\n3. Restart respawn:\n\`\`\`bash\ncurl -sk -X POST $API_URL/api/sessions/$SESSION_ID/respawn/stop\ncurl -sk -X POST $API_URL/api/sessions/$SESSION_ID/respawn/start\n\`\`\`"
STATE_STUCK_COUNT=0 # Reset to avoid spam
fi
else
STATE_STUCK_COUNT=0
fi
# 4. AI check errors
if [ "$AI_ERRORS" -gt 2 ] && [ "$AI_ERRORS" -gt "$AI_CHECK_ERRORS" ]; then
log_issue "WARNING" "AI idle check has $AI_ERRORS consecutive errors (status: $AI_STATUS)"
add_fix "AI check errors" "AI idle checker is failing. Check:\n1. Claude CLI is working\n2. Sufficient context available\n3. Consider disabling AI check temporarily:\n\`\`\`bash\ncurl -sk -X POST $API_URL/api/sessions/$SESSION_ID/respawn/config -H 'Content-Type: application/json' -d '{\"aiIdleCheckEnabled\": false}'\n\`\`\`"
AI_CHECK_ERRORS=$AI_ERRORS
fi
# 5. Cycle completed - log success
if [ "$CYCLE" -gt "$PREV_CYCLE" ] && [ "$PREV_CYCLE" -gt 0 ]; then
log_issue "INFO" "Respawn cycle $CYCLE completed successfully (tokens: $TOKENS)"
fi
# 6. Very high time since activity (>10 minutes) while not in watching state
if [ "$TIME_SINCE_ACTIVITY" -gt 600000 ] && [ "$STATE" != "watching" ]; then
log_issue "WARNING" "No activity for ${TIME_SINCE_ACTIVITY}ms while in state '$STATE' - may be stuck"
fi
# Update tracking vars
PREV_STATE="$STATE"
PREV_CYCLE="$CYCLE"
sleep $POLL_INTERVAL
done
log_issue "INFO" "Monitoring completed after $DURATION_HOURS hours"
echo "Monitoring complete. Check $ISSUES_FILE and $FIXES_FILE"
+166
View File
@@ -0,0 +1,166 @@
#!/usr/bin/env node
/**
* Browser test for clickable file links in Claudeman terminal
* Uses Playwright directly to ignore HTTPS errors
*/
import { chromium } from 'playwright';
const BASE_URL = 'https://localhost:3000';
const TEST_LOG_FILE = '/tmp/test-link-click.log';
async function main() {
console.log('Starting browser test for clickable links...\n');
// Create test log file
const { writeFileSync, appendFileSync } = await import('fs');
writeFileSync(TEST_LOG_FILE, `=== Test started at ${new Date().toISOString()} ===\n`);
for (let i = 1; i <= 10; i++) {
appendFileSync(TEST_LOG_FILE, `Entry #${i} - ${Date.now()}\n`);
}
console.log(`Created test file: ${TEST_LOG_FILE}\n`);
// Launch browser ignoring HTTPS errors
const browser = await chromium.launch({
headless: true,
args: ['--ignore-certificate-errors', '--ignore-ssl-errors']
});
const context = await browser.newContext({
ignoreHTTPSErrors: true
});
const page = await context.newPage();
try {
// Navigate to Claudeman
console.log('Navigating to Claudeman...');
await page.goto(BASE_URL, { waitUntil: 'networkidle', timeout: 30000 });
console.log('Page loaded: ' + await page.title());
// Wait for terminal to be ready
await page.waitForSelector('.xterm-screen', { timeout: 10000 });
console.log('Terminal found\n');
// Get list of sessions
const sessions = await page.evaluate(async () => {
const res = await fetch('/api/sessions');
return res.json();
});
console.log(`Found ${sessions.length} session(s)`);
if (sessions.length === 0) {
console.log('No sessions found. Creating a shell session...');
const newSession = await page.evaluate(async () => {
const res = await fetch('/api/quick-start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ caseName: 'link-test', mode: 'shell' })
});
return res.json();
});
console.log('Created session:', newSession.session?.id);
await page.waitForTimeout(2000);
}
// Get active session (use claudeman session if exists)
const claudemanSession = sessions.find(s => s.workingDir?.includes('claudeman'));
const sessionId = claudemanSession?.id || sessions[0]?.id;
console.log(`Using session: ${sessionId}\n`);
// Send a command that outputs file paths
console.log('Sending command with file path...');
await page.evaluate(async (sid, logFile) => {
await fetch(`/api/sessions/${sid}/input`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ input: `echo "Monitor: tail -f ${logFile}"\r` })
});
}, sessionId, TEST_LOG_FILE);
await page.waitForTimeout(2000);
// Take screenshot
await page.screenshot({ path: '/tmp/claudeman-link-test-1.png' });
console.log('Screenshot saved: /tmp/claudeman-link-test-1.png\n');
// Check if terminal contains our file path
const terminalContent = await page.evaluate(() => {
const screen = document.querySelector('.xterm-screen');
return screen?.textContent || '';
});
if (terminalContent.includes(TEST_LOG_FILE)) {
console.log('File path appears in terminal output');
} else {
console.log('WARNING: File path not found in terminal. Content preview:');
console.log(terminalContent.substring(0, 500));
}
// Try to find clickable links in xterm
const linkCount = await page.evaluate(() => {
// xterm link detection happens on mouse move, so we need to trigger it
const links = document.querySelectorAll('.xterm-link');
return links.length;
});
console.log(`\nClickable links found: ${linkCount}`);
// Try hovering over the terminal to trigger link detection
console.log('\nHovering over terminal to trigger link detection...');
const termRect = await page.locator('.xterm-screen').boundingBox();
if (termRect) {
// Move mouse across the terminal
for (let y = termRect.y + 50; y < termRect.y + termRect.height - 50; y += 20) {
for (let x = termRect.x + 50; x < termRect.x + termRect.width - 50; x += 50) {
await page.mouse.move(x, y);
await page.waitForTimeout(50);
}
}
}
// Check for tooltip (shows when hovering over link)
const tooltipVisible = await page.evaluate(() => {
const tooltip = document.getElementById('file-link-tooltip');
return tooltip?.style.display !== 'none' && tooltip?.style.display !== '';
});
console.log('Tooltip visible:', tooltipVisible);
// Check for log viewer windows
const logViewerCount = await page.evaluate(() => {
return document.querySelectorAll('.log-viewer-window').length;
});
console.log('Log viewer windows:', logViewerCount);
// Take final screenshot
await page.screenshot({ path: '/tmp/claudeman-link-test-2.png' });
console.log('\nFinal screenshot: /tmp/claudeman-link-test-2.png');
// Test the link provider registration
console.log('\n--- Link Provider Debug ---');
const debugInfo = await page.evaluate(() => {
// Check if app has link provider registered
const app = window.app;
if (!app) return { error: 'No app object found' };
const terminal = app.terminals?.get(app.activeSessionId);
if (!terminal) return { error: 'No active terminal', activeSessionId: app.activeSessionId };
return {
hasTerminal: true,
activeSessionId: app.activeSessionId,
terminalBuffer: terminal.buffer?.active?.length || 0
};
});
console.log('Debug info:', debugInfo);
} catch (error) {
console.error('Error:', error.message);
await page.screenshot({ path: '/tmp/claudeman-link-test-error.png' });
console.log('Error screenshot saved to /tmp/claudeman-link-test-error.png');
} finally {
await browser.close();
console.log('\nBrowser closed');
}
}
main().catch(console.error);
+73
View File
@@ -0,0 +1,73 @@
#!/usr/bin/env node
/**
* Test script for link detection patterns
*/
const patterns = {
cmdPattern: /(tail|cat|head|less|grep|watch|multitail)\s+(?:[^\/\n]+?\s+)?(\/[^\s"'<>|;&\n\)]+)/g,
bashPathPattern: /Bash\([^)]*?(\/(?:var|tmp|home|usr|etc|opt|log|logs)[^\s"'<>|;&\)\n]+)/g,
generalPathPattern: /(\/(?:home|tmp|var|etc|usr|opt)[^\s"'<>|;&\n\)\]]+\.(?:log|txt|json|md|ts|js|sh|py|yaml|yml|csv|xml|html|css))/g,
logPathPattern: /(\/var\/log\/[^\s"'<>|;&\n\)\]]+)/g,
claudeOutputPattern: /(?:Created|Writing|Saved|File|Output|Streaming|Monitoring|Log)(?:\s+(?:file|to|at))?[:\s]+([\/~][^\s"'<>|;&\n\)]+)/gi
};
const tests = [
'tail -f /tmp/test.log',
'tail -n 100 /var/log/syslog',
'cat /home/user/file.txt',
'head -n 10 /etc/passwd',
'Bash(tail -f /tmp/test.log)',
'Created file: /tmp/newfile.log',
'Writing to /home/user/output.txt',
'Monitoring file: /var/log/syslog',
'Streaming /home/user/logs/app.log',
'/home/arkon/project/file.log',
'/tmp/test-output.txt',
'echo "tail -f /tmp/test.log"',
'The output is in /home/user/results.csv',
'plain text without paths'
];
console.log('Link Pattern Test Results\n');
for (const [name, pattern] of Object.entries(patterns)) {
console.log(`\n=== ${name} ===`);
console.log(`Pattern: ${pattern.source}\n`);
for (const test of tests) {
pattern.lastIndex = 0;
const matches = [];
let m;
while ((m = pattern.exec(test)) !== null) {
// Get the captured group (varies by pattern)
matches.push(m[1] || m[2]);
}
if (matches.length > 0) {
console.log(` ✓ "${test}"`);
console.log(` → Found: ${matches.join(', ')}`);
}
}
}
console.log('\n\n=== Combined Test ===');
console.log('Testing all patterns on each input:\n');
for (const test of tests) {
const allMatches = [];
for (const [name, pattern] of Object.entries(patterns)) {
pattern.lastIndex = 0;
let m;
while ((m = pattern.exec(test)) !== null) {
const path = m[1] || m[2];
if (path && !allMatches.includes(path)) {
allMatches.push(path);
}
}
}
if (allMatches.length > 0) {
console.log(`✓ "${test}"`);
console.log(` → ${allMatches.join(', ')}`);
} else {
console.log(`✗ "${test}" - no match`);
}
}
+30
View File
@@ -0,0 +1,30 @@
#!/bin/bash
# Test script for clickable file links in Claudeman terminal
# Creates a log file and continuously writes timestamped data
LOGFILE="/tmp/claudeman-test-$(date +%s).log"
echo "Created log file: $LOGFILE"
echo ""
echo "You can monitor this file with:"
echo " tail -f $LOGFILE"
echo ""
echo "Starting to write data every 2 seconds..."
echo ""
# Write initial content
echo "=== Test Log Started at $(date) ===" > "$LOGFILE"
# Write data continuously
count=0
while true; do
count=$((count + 1))
echo "[$(date '+%H:%M:%S')] Entry #$count - Random: $RANDOM" >> "$LOGFILE"
# Print status every 5 entries
if [ $((count % 5)) -eq 0 ]; then
echo "Wrote $count entries to $LOGFILE"
fi
sleep 2
done