tractatus/scripts/framework-components/LoopDetector.js
TheFlow ac2db33732 fix(submissions): restructure Economist package and fix article display
- Create Economist SubmissionTracking package correctly:
  * mainArticle = full blog post content
  * coverLetter = 216-word SIR— letter
  * Links to blog post via blogPostId
- Archive 'Letter to The Economist' from blog posts (it's the cover letter)
- Fix date display on article cards (use published_at)
- Target publication already displaying via blue badge

Database changes:
- Make blogPostId optional in SubmissionTracking model
- Economist package ID: 68fa85ae49d4900e7f2ecd83
- Le Monde package ID: 68fa2abd2e6acd5691932150

Next: Enhanced modal with tabs, validation, export

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-24 08:47:42 +13:00

233 lines
5.8 KiB
JavaScript

/**
* 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;