- 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>
233 lines
5.8 KiB
JavaScript
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;
|