/** * Loop Detector * * Detects "stuck in loop" patterns that indicate: * - Repeated failed attempts on same problem * - Same file being edited multiple times without progress * - User frustration signals * * Used by: * - MetacognitiveVerifier (triggers verification on loop detection) * - ContextPressureMonitor (elevates pressure on loop detection) * * Copyright 2025 Tractatus Project * Licensed under Apache License 2.0 */ const fs = require('fs'); const path = require('path'); class LoopDetector { constructor(options = {}) { this.silent = options.silent || false; this.actionPatternsPath = path.join(__dirname, '../../.claude/action-patterns.json'); this.sessionStatePath = path.join(__dirname, '../../.claude/session-state.json'); // Thresholds this.thresholds = { same_file_edits: 3, // Alert after 3 edits to same file consecutive_actions: 5, // Within last 5 actions user_frustration_phrases: [ 'you ignored me', 'i told you', 'not working', 'still broken', 'same error', 'again', 'why are you' ] }; } /** * Load action patterns */ loadActionPatterns() { try { if (fs.existsSync(this.actionPatternsPath)) { return JSON.parse(fs.readFileSync(this.actionPatternsPath, 'utf8')); } } catch (err) { if (!this.silent) { console.warn(`LoopDetector: Could not load action patterns: ${err.message}`); } } return null; } /** * Detect loops based on action patterns */ detectLoop() { const patterns = this.loadActionPatterns(); if (!patterns || !patterns.actions || patterns.actions.length === 0) { return { detected: false, type: null, severity: 'NONE', details: null }; } // Check 1: Repeated file edits const fileEditLoop = this.detectRepeatedFileEdits(patterns); if (fileEditLoop.detected) { return fileEditLoop; } // Check 2: Same action type repeated const actionLoop = this.detectRepeatedActionType(patterns); if (actionLoop.detected) { return actionLoop; } // Check 3: User frustration signals (would need message history) // TODO: Implement if message history tracking is added return { detected: false, type: null, severity: 'NONE', details: null }; } /** * Detect repeated edits to same file */ detectRepeatedFileEdits(patterns) { const recentActions = patterns.actions.slice(-this.thresholds.consecutive_actions); // Group by file const fileGroups = {}; recentActions.forEach(action => { if (action.type === 'edit' || action.type === 'write') { if (!fileGroups[action.file]) { fileGroups[action.file] = []; } fileGroups[action.file].push(action); } }); // Check for files edited multiple times for (const [file, actions] of Object.entries(fileGroups)) { if (actions.length >= this.thresholds.same_file_edits) { return { detected: true, type: 'repeated_file_edit', severity: 'MEDIUM', details: { file: file, count: actions.length, recent_count: actions.length, message: `File edited ${actions.length} times in last ${this.thresholds.consecutive_actions} actions`, recommendation: 'Pause and run MetacognitiveVerifier to assess approach' } }; } } return { detected: false }; } /** * Detect repeated action type (e.g., multiple failed bash commands) */ detectRepeatedActionType(patterns) { const recentActions = patterns.actions.slice(-this.thresholds.consecutive_actions); // Count consecutive actions of same type let consecutiveCount = 1; let currentType = null; for (let i = recentActions.length - 1; i >= 0; i--) { const action = recentActions[i]; if (currentType === null) { currentType = action.type; } else if (action.type === currentType) { consecutiveCount++; } else { break; } } if (consecutiveCount >= 3) { return { detected: true, type: 'repeated_action_type', severity: 'LOW', details: { action_type: currentType, count: consecutiveCount, message: `${consecutiveCount} consecutive ${currentType} actions`, recommendation: 'Consider if current approach is effective' } }; } return { detected: false }; } /** * Get loop status for MetacognitiveVerifier */ getLoopStatus() { const loop = this.detectLoop(); if (!loop.detected) { return { in_loop: false, should_verify: false }; } return { in_loop: true, should_verify: loop.severity === 'MEDIUM' || loop.severity === 'HIGH', loop_type: loop.type, severity: loop.severity, details: loop.details }; } /** * Calculate pressure contribution for ContextPressureMonitor */ calculatePressureContribution() { const loop = this.detectLoop(); if (!loop.detected) { return 0; } // Pressure contribution based on severity const pressureMap = { 'LOW': 5, 'MEDIUM': 15, 'HIGH': 25, 'CRITICAL': 40 }; return pressureMap[loop.severity] || 0; } /** * Log loop detection result */ log(message, level = 'info') { if (this.silent) return; const colors = { info: '\x1b[36m', warn: '\x1b[33m', error: '\x1b[31m', success: '\x1b[32m', reset: '\x1b[0m' }; const color = colors[level] || colors.reset; console.log(`${color}[LoopDetector] ${message}${colors.reset}`); } } module.exports = LoopDetector;