/* * Copyright 2025 John G Stroh * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * Context Pressure Monitor Service * Detects conditions that increase AI error probability * * Core Tractatus Service: Monitors environmental factors that degrade * AI performance and triggers increased verification or human intervention. * * Monitored Conditions: * - Token budget pressure (approaching context limit) * - Conversation length (attention decay over long sessions) * - Task complexity (number of simultaneous objectives) * - Error frequency (recent failures indicate degraded state) * - Instruction density (too many competing directives) */ const { getMemoryProxy } = require('./MemoryProxy.service'); const SessionState = require('../models/SessionState.model'); const logger = require('../utils/logger.util'); /** * Pressure levels and thresholds */ const PRESSURE_LEVELS = { NORMAL: { level: 0, threshold: 0.0, // 0-30%: Normal operations description: 'Normal operating conditions', action: 'PROCEED', verificationMultiplier: 1.0 }, ELEVATED: { level: 1, threshold: 0.3, // 30-50%: Increased verification recommended description: 'Elevated pressure, increased verification recommended', action: 'INCREASE_VERIFICATION', verificationMultiplier: 1.3 }, HIGH: { level: 2, threshold: 0.5, // 50-70%: Mandatory verification required description: 'High pressure, mandatory verification required', action: 'MANDATORY_VERIFICATION', verificationMultiplier: 1.6 }, CRITICAL: { level: 3, threshold: 0.7, // 70-85%: Recommend context refresh description: 'Critical pressure, recommend context refresh', action: 'RECOMMEND_REFRESH', verificationMultiplier: 2.0 }, DANGEROUS: { level: 4, threshold: 0.85, // 85-100%: Require human intervention description: 'Dangerous conditions, require human intervention', action: 'REQUIRE_HUMAN_INTERVENTION', verificationMultiplier: 3.0 } }; /** * Monitored metrics * * UPDATED 2025-10-12: Increased conversation length weight after observing that * compacting events are the PRIMARY cause of session disruption. Each compaction: * - Takes significant time (1-3 minutes) * - Loses critical context * - Requires re-explaining state * - Degrades quality dramatically * * Conversation message count is now MORE important than token count because: * - Compacting happens based on messages, not just tokens * - 60 messages triggers first compaction (observed) * - Multiple compactions in one session = CRITICAL failure condition */ const METRICS = { TOKEN_USAGE: { weight: 0.30, // Reduced from 0.35 - still important but secondary to conversation decay criticalThreshold: 0.8, // 80% of token budget dangerThreshold: 0.95 }, CONVERSATION_LENGTH: { weight: 0.40, // Increased from 0.25 - conversation decay is PRIMARY factor criticalThreshold: 40, // Messages before degradation starts (reduced from 100) dangerThreshold: 60, // Messages at dangerous levels (reduced from 150) compactionMultiplier: { first: 1.5, // 50% pressure boost after first compaction second: 3.0, // 3x pressure after second compaction (CRITICAL) third: 5.0 // 5x pressure after third+ compaction (DANGEROUS) } }, TASK_COMPLEXITY: { weight: 0.15, criticalThreshold: 5, // Simultaneous tasks dangerThreshold: 8 }, ERROR_FREQUENCY: { weight: 0.10, // Reduced from 0.15 - errors less predictive than conversation decay criticalThreshold: 3, // Errors in last 10 actions dangerThreshold: 5 }, INSTRUCTION_DENSITY: { weight: 0.05, // Reduced from 0.10 - least important factor criticalThreshold: 10, // Active instructions dangerThreshold: 15 } }; class ContextPressureMonitor { constructor() { this.pressureLevels = PRESSURE_LEVELS; this.metrics = METRICS; this.errorHistory = []; this.maxErrorHistory = 20; this.pressureHistory = []; this.maxPressureHistory = 50; // Initialize MemoryProxy for governance rules and audit logging this.memoryProxy = getMemoryProxy(); this.governanceRules = []; // Loaded from memory for pressure analysis reference this.memoryProxyInitialized = false; // Session state persistence this.currentSessionId = null; this.sessionState = null; // SessionState model instance // Statistics tracking this.stats = { total_analyses: 0, total_errors: 0, by_level: { NORMAL: 0, ELEVATED: 0, HIGH: 0, CRITICAL: 0, DANGEROUS: 0 }, error_types: {} }; logger.info('ContextPressureMonitor initialized'); } /** * Initialize MemoryProxy and load governance rules * @param {string} sessionId - Optional session ID for state persistence * @returns {Promise} Initialization result */ async initialize(sessionId = null) { try { await this.memoryProxy.initialize(); // Load all governance rules for pressure analysis reference this.governanceRules = await this.memoryProxy.loadGovernanceRules(); this.memoryProxyInitialized = true; // Initialize session state if sessionId provided if (sessionId) { this.currentSessionId = sessionId; this.sessionState = await SessionState.findOrCreate(sessionId); logger.info('[ContextPressureMonitor] Session state loaded', { sessionId, totalAnalyses: this.sessionState.totalAnalyses, currentPressure: this.sessionState.currentPressure.pressureLevel }); } logger.info('[ContextPressureMonitor] MemoryProxy initialized', { governanceRulesLoaded: this.governanceRules.length, sessionPersistence: !!sessionId }); return { success: true, governanceRulesLoaded: this.governanceRules.length, sessionId: this.currentSessionId, sessionPersistence: !!this.sessionState }; } catch (error) { logger.error('[ContextPressureMonitor] Failed to initialize MemoryProxy', { error: error.message }); return { success: false, error: error.message, governanceRulesLoaded: 0 }; } } /** * Calculate current pressure level * @param {Object} context - Current conversation/session context * @returns {Promise} Pressure analysis */ async analyzePressure(context) { try { // Calculate individual metric scores const metricScores = { tokenUsage: this._calculateTokenPressure(context), conversationLength: this._calculateConversationPressure(context), taskComplexity: this._calculateComplexityPressure(context), errorFrequency: this._calculateErrorPressure(context), instructionDensity: this._calculateInstructionPressure(context) }; // Calculate weighted overall pressure score const overallPressure = this._calculateOverallPressure(metricScores); // Determine pressure level (returns string like 'NORMAL') const pressureName = this._determinePressureLevel(overallPressure); const pressureLevel = this.pressureLevels[pressureName]; // Calculate degradation score (async) let degradation = null; try { if (this.memoryProxyInitialized) { degradation = await this.calculateDegradationScore(); } } catch (degradationError) { logger.warn('[ContextPressureMonitor] Degradation calculation failed, continuing without it', { error: degradationError.message }); } // Generate recommendations const recommendations = this._generateRecommendations( pressureLevel, metricScores, context ); // Create simple recommendation strings for test compatibility const recommendationStrings = recommendations.map(r => r.action || r.type); const analysis = { overallPressure, overall_score: overallPressure, pressureLevel: pressureLevel.level, level: pressureName, pressureName, description: pressureLevel.description, action: pressureLevel.action, verificationMultiplier: pressureLevel.verificationMultiplier, metrics: metricScores, recommendations: recommendationStrings, // Simple array for test compatibility detailed_recommendations: recommendations, // Full objects for actual use warnings: recommendations .filter(r => r.severity === 'HIGH' || r.severity === 'CRITICAL') .map(r => r.message), risks: recommendations .filter(r => r.type === 'RISK') .map(r => r.message), timestamp: new Date() }; // Add degradation analysis if available if (degradation) { analysis.degradation = degradation.score; analysis.degradationLevel = degradation.level; analysis.degradationBreakdown = degradation.breakdown; analysis.degradationRecommendation = degradation.recommendation; // Add degradation warnings to main warnings if serious if (degradation.level === 'CRITICAL' || degradation.level === 'HIGH') { analysis.warnings.push(degradation.recommendation); } } // Track statistics this.stats.total_analyses++; this.stats.by_level[pressureName]++; // Add to pressure history this.pressureHistory.unshift(analysis); if (this.pressureHistory.length > this.maxPressureHistory) { this.pressureHistory = this.pressureHistory.slice(0, this.maxPressureHistory); } // Detect trends if (this.pressureHistory.length >= 3) { const recent = this.pressureHistory.slice(0, 3); const scores = recent.map(p => p.overallPressure); if (scores[0] > scores[1] && scores[1] > scores[2]) { analysis.trend = 'escalating'; analysis.warnings.push('Pressure is escalating rapidly'); } else if (scores[0] < scores[1] && scores[1] < scores[2]) { analysis.trend = 'improving'; } else { analysis.trend = 'stable'; } } // Log if pressure is elevated if (pressureLevel.level >= PRESSURE_LEVELS.ELEVATED.level) { logger.warn('Elevated context pressure detected', { level: pressureLevel.level, pressure: overallPressure, topMetric: this._getTopMetric(metricScores) }); } // Audit pressure analysis this._auditPressureAnalysis(analysis, context); // Persist to MongoDB if session state active if (this.sessionState) { this._persistPressureState(analysis).catch(error => { logger.error('[ContextPressureMonitor] Failed to persist pressure state', { error: error.message }); }); } return analysis; } catch (error) { logger.error('Pressure analysis error:', error); return this._defaultPressureAnalysis(); } } /** * Record an error for error frequency tracking */ recordError(error) { const errorType = error.type || 'unknown'; this.errorHistory.push({ timestamp: new Date(), error: error.message || String(error), type: errorType }); // Track error statistics this.stats.total_errors++; if (!this.stats.error_types[errorType]) { this.stats.error_types[errorType] = 0; } this.stats.error_types[errorType]++; // Maintain history limit if (this.errorHistory.length > this.maxErrorHistory) { this.errorHistory.shift(); } logger.debug('Error recorded in pressure monitor', { recentErrors: this.errorHistory.length, type: errorType }); // Persist error to session state if (this.sessionState) { this.sessionState.addError(error).catch(err => { logger.error('[ContextPressureMonitor] Failed to persist error to session state', { error: err.message }); }); } // Check for error clustering const recentErrors = this.errorHistory.filter(e => (new Date() - e.timestamp) < 60000 // Last minute ); if (recentErrors.length >= 5) { logger.warn('Error clustering detected', { count: recentErrors.length, timeWindow: '1 minute' }); } } /** * Check if action should proceed given current pressure */ async shouldProceed(action, context) { const analysis = await this.analyzePressure(context); if (analysis.pressureLevel >= PRESSURE_LEVELS.DANGEROUS.level) { return { proceed: false, reason: 'Dangerous pressure level - human intervention required', analysis }; } if (analysis.pressureLevel >= PRESSURE_LEVELS.CRITICAL.level) { return { proceed: true, requireVerification: true, reason: 'Critical pressure - mandatory verification required', analysis }; } return { proceed: true, requireVerification: analysis.pressureLevel >= PRESSURE_LEVELS.HIGH.level, reason: 'Acceptable pressure level', analysis }; } /** * Private methods */ _calculateTokenPressure(context) { // Support both camelCase and snake_case let tokenUsage = context.tokenUsage || context.token_usage || 0; const tokenBudget = context.tokenBudget || context.token_limit || 200000; // Handle negative values if (tokenUsage < 0) { tokenUsage = 0; } // Determine if tokenUsage is a ratio or absolute count let ratio; if (tokenUsage <= 2.0) { // Values <= 2.0 are treated as ratios (allows for over-budget like 1.5 = 150%) ratio = tokenUsage; } else { // Values > 2.0 are treated as absolute token counts ratio = tokenUsage / tokenBudget; } // Use ratio directly as normalized score (don't divide by criticalThreshold) const normalized = Math.min(1.0, Math.max(0.0, ratio)); return { value: ratio, score: normalized, // Alias for test compatibility normalized, raw: tokenUsage <= 2.0 ? tokenUsage * tokenBudget : tokenUsage, budget: tokenBudget, percentage: (ratio * 100).toFixed(1) }; } _calculateConversationPressure(context) { // Support multiple field names for conversation length const messageCount = context.messageCount || context.messages?.length || context.conversation_length || context.messages_count || 0; // Check for compaction events (CRITICAL indicator) const compactions = context.compactions_count || context.compactions || 0; let ratio = messageCount / this.metrics.CONVERSATION_LENGTH.criticalThreshold; // Apply compaction multiplier (each compaction dramatically increases pressure) let multiplier = 1.0; let compactionNote = null; if (compactions >= 3) { multiplier = this.metrics.CONVERSATION_LENGTH.compactionMultiplier.third; compactionNote = `3+ compactions detected - DANGEROUS conditions`; } else if (compactions === 2) { multiplier = this.metrics.CONVERSATION_LENGTH.compactionMultiplier.second; compactionNote = `2nd compaction detected - CRITICAL pressure`; } else if (compactions === 1) { multiplier = this.metrics.CONVERSATION_LENGTH.compactionMultiplier.first; compactionNote = `1st compaction detected - elevated pressure`; } // Apply multiplier BEFORE normalization to allow pressure > 1.0 ratio = ratio * multiplier; const normalized = Math.min(1.0, ratio); // Cap at 1.0 for scoring const result = { value: ratio, score: normalized, // Alias for test compatibility normalized, raw: messageCount, threshold: this.metrics.CONVERSATION_LENGTH.criticalThreshold, compactions, multiplier }; if (compactionNote) { result.compactionNote = compactionNote; } return result; } _calculateComplexityPressure(context) { // Calculate complexity from multiple factors let complexityScore = 0; const factors = []; // Task depth (how many nested levels) const taskDepth = context.task_depth || 0; if (taskDepth >= 3) { complexityScore += taskDepth * 0.8; factors.push('high task depth'); } else if (taskDepth >= 2) { complexityScore += taskDepth * 0.5; } else { complexityScore += taskDepth * 0.3; } // Dependencies const dependencies = context.dependencies || 0; if (dependencies >= 8) { complexityScore += dependencies * 0.3; factors.push('many dependencies'); } else { complexityScore += dependencies * 0.2; } // File modifications const fileModifications = context.file_modifications || 0; if (fileModifications >= 10) { complexityScore += fileModifications * 0.15; factors.push('many file modifications'); } else { complexityScore += fileModifications * 0.1; } // Concurrent operations const concurrentOps = context.concurrent_operations || 0; if (concurrentOps >= 5) { complexityScore += concurrentOps * 0.4; factors.push('high concurrency'); } else { complexityScore += concurrentOps * 0.2; } // Subtasks pending const subtasks = context.subtasks_pending || 0; if (subtasks >= 10) { complexityScore += subtasks * 0.2; factors.push('many pending subtasks'); } else { complexityScore += subtasks * 0.1; } // Fallback to simple task count if no factors if (complexityScore === 0) { const taskCount = context.activeTasks?.length || context.taskComplexity || 1; complexityScore = taskCount; } const ratio = complexityScore / this.metrics.TASK_COMPLEXITY.criticalThreshold; const normalized = Math.min(1.0, ratio); return { value: ratio, score: normalized, // Alias for test compatibility normalized, raw: complexityScore, threshold: this.metrics.TASK_COMPLEXITY.criticalThreshold, factors: factors.length > 0 ? factors : undefined }; } _calculateErrorPressure(context) { // Check for explicit error count in context first let recentErrors = context.errors_recent || context.errors_last_hour || 0; // If not provided, count from error history (last 10 minutes) if (recentErrors === 0 && this.errorHistory.length > 0) { const tenMinutesAgo = new Date(Date.now() - 10 * 60 * 1000); recentErrors = this.errorHistory.filter( e => new Date(e.timestamp) > tenMinutesAgo ).length; } const ratio = recentErrors / this.metrics.ERROR_FREQUENCY.criticalThreshold; const normalized = Math.min(1.0, ratio); return { value: ratio, score: normalized, // Alias for test compatibility normalized, raw: recentErrors, recent_errors: recentErrors, // Alias for test compatibility threshold: this.metrics.ERROR_FREQUENCY.criticalThreshold, total: this.errorHistory.length }; } _calculateInstructionPressure(context) { const instructionCount = context.activeInstructions?.length || 0; const ratio = instructionCount / this.metrics.INSTRUCTION_DENSITY.criticalThreshold; const normalized = Math.min(1.0, ratio); return { value: ratio, score: normalized, // Alias for test compatibility normalized, raw: instructionCount, threshold: this.metrics.INSTRUCTION_DENSITY.criticalThreshold }; } _calculateOverallPressure(metricScores) { // Calculate weighted average based on configured weights // This properly prioritizes token usage (35%) over other metrics let weightedPressure = 0; weightedPressure += metricScores.tokenUsage.normalized * this.metrics.TOKEN_USAGE.weight; weightedPressure += metricScores.conversationLength.normalized * this.metrics.CONVERSATION_LENGTH.weight; weightedPressure += metricScores.taskComplexity.normalized * this.metrics.TASK_COMPLEXITY.weight; weightedPressure += metricScores.errorFrequency.normalized * this.metrics.ERROR_FREQUENCY.weight; weightedPressure += metricScores.instructionDensity.normalized * this.metrics.INSTRUCTION_DENSITY.weight; // Use weighted average as the pressure score // The configured weights already reflect relative importance of each metric return Math.min(1.0, Math.max(0.0, weightedPressure)); } _generateRecommendations(pressureLevel, metricScores, context) { const recommendations = []; // Add baseline recommendation based on pressure level switch (pressureLevel.level) { case 0: // NORMAL recommendations.push({ type: 'GENERAL', severity: 'NORMAL', message: 'Continue normal operations', action: 'CONTINUE_NORMAL' }); break; case 1: // ELEVATED recommendations.push({ type: 'GENERAL', severity: 'MEDIUM', message: 'Increase verification level', action: 'INCREASE_VERIFICATION' }); break; case 2: // HIGH recommendations.push({ type: 'GENERAL', severity: 'HIGH', message: 'Suggest context refresh', action: 'SUGGEST_CONTEXT_REFRESH' }); break; case 3: // CRITICAL recommendations.push({ type: 'GENERAL', severity: 'CRITICAL', message: 'Mandatory verification required', action: 'MANDATORY_VERIFICATION' }); break; case 4: // DANGEROUS recommendations.push({ type: 'GENERAL', severity: 'CRITICAL', message: 'Immediate halt required', action: 'IMMEDIATE_HALT' }); break; } // Token usage recommendations if (metricScores.tokenUsage.normalized >= 0.95) { // IMMEDIATE_HALT already added above for DANGEROUS level if (pressureLevel.level < 4) { recommendations.push({ type: 'TOKEN_MANAGEMENT', severity: 'CRITICAL', message: 'Token budget at dangerous levels - immediate halt required', action: 'IMMEDIATE_HALT' }); } } else if (metricScores.tokenUsage.normalized > 0.8) { recommendations.push({ type: 'TOKEN_MANAGEMENT', severity: 'HIGH', message: 'Token budget critically low - consider context refresh', action: 'Summarize conversation and start new context window' }); } else if (metricScores.tokenUsage.normalized > 0.6) { recommendations.push({ type: 'TOKEN_MANAGEMENT', severity: 'MEDIUM', message: 'Token usage elevated - monitor carefully', action: 'Be concise in responses, consider pruning context if needed' }); } // Conversation length recommendations if (metricScores.conversationLength.normalized > 0.8) { recommendations.push({ type: 'CONVERSATION_MANAGEMENT', severity: 'HIGH', message: 'Very long conversation - attention may degrade', action: 'Consider summarizing progress and starting fresh session' }); } // Error frequency recommendations if (metricScores.errorFrequency.normalized > 0.6) { recommendations.push({ type: 'ERROR_MANAGEMENT', severity: 'HIGH', message: 'High error frequency detected - operating conditions degraded', action: 'Increase verification, slow down, consider pausing for review' }); } // Task complexity recommendations if (metricScores.taskComplexity.normalized > 0.7) { recommendations.push({ type: 'COMPLEXITY_MANAGEMENT', severity: 'MEDIUM', message: 'High task complexity - risk of context confusion', action: 'Focus on one task at a time, explicitly track task switching' }); } // Instruction density recommendations if (metricScores.instructionDensity.normalized > 0.7) { recommendations.push({ type: 'INSTRUCTION_MANAGEMENT', severity: 'MEDIUM', message: 'Many active instructions - risk of conflicts', action: 'Review and consolidate instructions, resolve conflicts' }); } // Overall pressure recommendations if (pressureLevel.level >= PRESSURE_LEVELS.CRITICAL.level) { recommendations.push({ type: 'GENERAL', severity: 'CRITICAL', message: 'Critical pressure level - degraded performance likely', action: 'Strongly recommend context refresh or human intervention' }); } return recommendations; } _getTopMetric(metricScores) { const scores = [ { name: 'tokenUsage', score: metricScores.tokenUsage.normalized }, { name: 'conversationLength', score: metricScores.conversationLength.normalized }, { name: 'taskComplexity', score: metricScores.taskComplexity.normalized }, { name: 'errorFrequency', score: metricScores.errorFrequency.normalized }, { name: 'instructionDensity', score: metricScores.instructionDensity.normalized } ]; scores.sort((a, b) => b.score - a.score); return scores[0].name; } _defaultPressureAnalysis() { return { overallPressure: 0.5, overall_score: 0.5, pressureLevel: 1, level: 'ELEVATED', pressureName: 'ELEVATED', description: 'Unable to analyze pressure, using safe defaults', action: 'INCREASE_VERIFICATION', verificationMultiplier: 1.5, metrics: {}, recommendations: [{ type: 'ERROR', severity: 'HIGH', message: 'Pressure analysis failed - proceeding with caution', action: 'Increase verification and monitoring' }], warnings: ['Pressure analysis failed - proceeding with caution'], risks: [], timestamp: new Date() }; } /** * Determine pressure level from score (exposed for testing) * @param {number} score - Overall pressure score (0-1) * @returns {string} Pressure level name */ _determinePressureLevel(score) { if (score >= PRESSURE_LEVELS.DANGEROUS.threshold) return 'DANGEROUS'; if (score >= PRESSURE_LEVELS.CRITICAL.threshold) return 'CRITICAL'; if (score >= PRESSURE_LEVELS.HIGH.threshold) return 'HIGH'; if (score >= PRESSURE_LEVELS.ELEVATED.threshold) return 'ELEVATED'; return 'NORMAL'; } /** * Get pressure history * @param {boolean} fromDatabase - Load from database instead of memory * @returns {Promise|Array} Pressure analysis history */ getPressureHistory(fromDatabase = false) { if (fromDatabase && this.sessionState) { return Promise.resolve(this.sessionState.pressureHistory); } return [...this.pressureHistory]; } /** * Load session state from MongoDB * @param {string} sessionId - Session ID to load * @returns {Promise} Loaded session state */ async loadSessionState(sessionId) { try { this.currentSessionId = sessionId; this.sessionState = await SessionState.findActiveSession(sessionId); if (!this.sessionState) { logger.warn('[ContextPressureMonitor] No active session found, creating new', { sessionId }); this.sessionState = await SessionState.findOrCreate(sessionId); } // Restore in-memory state from database this.stats.total_analyses = this.sessionState.totalAnalyses; this.stats.total_errors = this.sessionState.totalErrors; this.stats.by_level = { ...this.sessionState.levelStats }; // Restore error history this.errorHistory = this.sessionState.errorHistory.map(e => ({ timestamp: e.timestamp, error: e.error, type: e.type })); // Restore pressure history this.pressureHistory = this.sessionState.pressureHistory.map(p => ({ overallPressure: p.overallScore, level: p.pressureLevel, trend: p.trend, warnings: p.warnings, timestamp: p.timestamp })); logger.info('[ContextPressureMonitor] Session state loaded from MongoDB', { sessionId, totalAnalyses: this.stats.total_analyses, currentPressure: this.sessionState.currentPressure.pressureLevel }); return this.sessionState.getSummary(); } catch (error) { logger.error('[ContextPressureMonitor] Failed to load session state', { error: error.message, sessionId }); throw error; } } /** * Close current session * @returns {Promise} */ async closeSession() { if (this.sessionState) { await this.sessionState.close(); logger.info('[ContextPressureMonitor] Session closed', { sessionId: this.currentSessionId }); this.sessionState = null; this.currentSessionId = null; } } /** * Persist pressure state to MongoDB (async) * @private */ async _persistPressureState(analysis) { if (!this.sessionState) { return; } try { await this.sessionState.updatePressure(analysis); } catch (error) { logger.error('[ContextPressureMonitor] Failed to update session state', { error: error.message, sessionId: this.currentSessionId }); throw error; } } /** * Audit pressure analysis to MemoryProxy * @private */ _auditPressureAnalysis(analysis, context = {}) { if (!this.memoryProxyInitialized) { return; } const violations = analysis.warnings || []; this.memoryProxy.auditDecision({ sessionId: context.sessionId || 'pressure-monitor', action: 'context_pressure_analysis', service: 'ContextPressureMonitor', rulesChecked: this.governanceRules.map(r => r.id), violations, allowed: analysis.pressureLevel < PRESSURE_LEVELS.DANGEROUS.level, metadata: { overall_pressure: analysis.overallPressure, pressure_level: analysis.pressureName, pressure_level_numeric: analysis.pressureLevel, action_required: analysis.action, verification_multiplier: analysis.verificationMultiplier, trend: analysis.trend, metrics: { token_usage: analysis.metrics.tokenUsage?.normalized, conversation_length: analysis.metrics.conversationLength?.normalized, task_complexity: analysis.metrics.taskComplexity?.normalized, error_frequency: analysis.metrics.errorFrequency?.normalized, instruction_density: analysis.metrics.instructionDensity?.normalized }, top_metric: this._getTopMetric(analysis.metrics), warnings_count: violations.length, recommendations_count: analysis.recommendations?.length || 0, services_involved: context.services_involved || [] // Deep Interlock coordination tracking } }).catch(error => { logger.error('[ContextPressureMonitor] Failed to audit pressure analysis', { error: error.message, pressure: analysis.pressureName }); }); } /** * Calculate degradation score (0-100) * Combines behavioral and quality metrics to detect performance degradation * that may not be captured by standard pressure metrics. * * @returns {Promise} Degradation analysis */ async calculateDegradationScore() { try { const scores = { errorPattern: await this._analyzeErrorPatterns(), // 30% frameworkFade: await this._detectFrameworkFade(), // 25% contextQuality: await this._assessContextQuality(), // 20% behavioral: await this._analyzeBehavior(), // 15% taskCompletion: await this._measureTaskCompletion() // 10% }; const degradationScore = scores.errorPattern * 0.30 + scores.frameworkFade * 0.25 + scores.contextQuality * 0.20 + scores.behavioral * 0.15 + scores.taskCompletion * 0.10; return { score: Math.round(degradationScore), level: this._getDegradationLevel(degradationScore), breakdown: scores, recommendation: this._getDegradationRecommendation(degradationScore) }; } catch (error) { logger.error('[ContextPressureMonitor] Failed to calculate degradation score', { error: error.message }); return { score: 0, level: 'LOW', breakdown: {}, recommendation: 'Unable to calculate degradation score' }; } } /** * Analyze error patterns (returns 0-100) * Detects consecutive errors, clustering, and severity weighting * @private */ async _analyzeErrorPatterns() { try { const recentErrors = await this.memoryProxy.getRecentAuditLogs({ limit: 50, filter: { hasError: true } }); if (!recentErrors || recentErrors.length === 0) { return 0; } // Consecutive errors let maxConsecutive = 0; let currentStreak = 0; recentErrors.forEach((e) => { if (e.decision?.blocked || e.decision?.errors) { currentStreak++; maxConsecutive = Math.max(maxConsecutive, currentStreak); } else { currentStreak = 0; } }); // Error clustering (3+ errors in 10-minute windows) const errorClusters = this._detectErrorClusters(recentErrors, 10 * 60 * 1000); // Error severity weighting const severityScore = recentErrors.reduce((sum, e) => { if (e.decision?.blocked) return sum + 3; if (e.decision?.errors) return sum + 1; return sum; }, 0); // Combine metrics const consecutiveScore = Math.min(maxConsecutive * 10, 100); const clusterScore = Math.min(errorClusters.length * 15, 100); const severityScoreNormalized = Math.min(severityScore * 2, 100); return Math.round((consecutiveScore + clusterScore + severityScoreNormalized) / 3); } catch (error) { logger.error('[ContextPressureMonitor] Error pattern analysis failed', { error: error.message }); return 0; } } /** * Detect error clusters in time windows * @private */ _detectErrorClusters(errors, windowMs) { const clusters = []; const sortedErrors = [...errors].sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp) ); for (let i = 0; i < sortedErrors.length; i++) { const windowStart = new Date(sortedErrors[i].timestamp); const windowEnd = new Date(windowStart.getTime() + windowMs); const errorsInWindow = sortedErrors.filter(e => { const errorTime = new Date(e.timestamp); return errorTime >= windowStart && errorTime <= windowEnd; }); if (errorsInWindow.length >= 3) { clusters.push({ start: windowStart, end: windowEnd, count: errorsInWindow.length }); } } return clusters; } /** * Detect framework fade (returns 0-100) * Measures time since critical components were last used * @private */ async _detectFrameworkFade() { try { const criticalComponents = [ 'MetacognitiveVerifier', 'BoundaryEnforcer', 'PluralisticDeliberationOrchestrator' ]; const componentActivity = await Promise.all( criticalComponents.map(async (service) => { const logs = await this.memoryProxy.getRecentAuditLogs({ limit: 1, filter: { service } }); if (logs.length === 0) return { service, ageMinutes: Infinity }; const age = (Date.now() - new Date(logs[0].timestamp)) / 1000 / 60; return { service, ageMinutes: age }; }) ); // Score: minutes since last use // 0-30 min = 0 points // 30-60 min = 50 points // 60+ min = 100 points const scores = componentActivity.map(c => { if (c.ageMinutes === Infinity) return 100; if (c.ageMinutes < 30) return 0; if (c.ageMinutes < 60) return 50; return 100; }); return Math.round(scores.reduce((a, b) => a + b, 0) / scores.length); } catch (error) { logger.error('[ContextPressureMonitor] Framework fade detection failed', { error: error.message }); return 0; } } /** * Assess context quality (returns 0-100) * Detects post-compaction degradation and session age issues * @private */ async _assessContextQuality() { try { const session = this.sessionState || await this.memoryProxy.getSessionState(); if (!session) return 0; let score = 0; // Post-compaction flag (major degradation indicator) if (session.autoCompactions && session.autoCompactions.length > 0) { const lastCompaction = session.autoCompactions[session.autoCompactions.length - 1]; const timeSinceCompaction = (Date.now() - new Date(lastCompaction.timestamp)) / 1000 / 60; // Within 60 minutes of compaction = high risk if (timeSinceCompaction < 60) { score += 60; } else if (timeSinceCompaction < 120) { score += 30; } } // Session age (very long sessions accumulate drift) const sessionAge = (Date.now() - new Date(session.startTime)) / 1000 / 60 / 60; // hours if (sessionAge > 6) score += 40; else if (sessionAge > 4) score += 20; return Math.min(score, 100); } catch (error) { logger.error('[ContextPressureMonitor] Context quality assessment failed', { error: error.message }); return 0; } } /** * Analyze behavioral indicators (returns 0-100) * Detects tool retry patterns and thrashing * @private */ async _analyzeBehavior() { try { const recentActions = await this.memoryProxy.getRecentAuditLogs({ limit: 50 }); if (!recentActions || recentActions.length === 0) return 0; // Tool retry rate const toolCalls = recentActions.map(a => a.metadata?.tool).filter(Boolean); let retries = 0; for (let i = 2; i < toolCalls.length; i++) { if (toolCalls[i] === toolCalls[i-1] && toolCalls[i] === toolCalls[i-2]) { retries++; } } const retryScore = Math.min(retries * 20, 100); return retryScore; } catch (error) { logger.error('[ContextPressureMonitor] Behavioral analysis failed', { error: error.message }); return 0; } } /** * Measure task completion (returns 0-100) * Simple error rate metric in recent actions * @private */ async _measureTaskCompletion() { try { const recentErrors = await this.memoryProxy.getRecentAuditLogs({ limit: 20, filter: { hasError: true } }); if (!recentErrors) return 0; // Simple metric: error rate in last 20 actions const errorRate = (recentErrors.length / 20) * 100; return Math.round(errorRate); } catch (error) { logger.error('[ContextPressureMonitor] Task completion measurement failed', { error: error.message }); return 0; } } /** * Get degradation level from score * @private */ _getDegradationLevel(score) { if (score >= 60) return 'CRITICAL'; if (score >= 40) return 'HIGH'; if (score >= 20) return 'MODERATE'; return 'LOW'; } /** * Get degradation recommendation * @private */ _getDegradationRecommendation(score) { if (score >= 60) { return 'RECOMMEND SESSION RESTART - Quality severely degraded'; } if (score >= 40) { return 'WARN USER - Performance declining, consider checkpoint review'; } return 'Monitoring - No action needed'; } /** * Reset monitoring state */ reset() { this.errorHistory = []; this.pressureHistory = []; this.stats = { total_analyses: 0, total_errors: 0, by_level: { NORMAL: 0, ELEVATED: 0, HIGH: 0, CRITICAL: 0, DANGEROUS: 0 }, error_types: {} }; logger.info('ContextPressureMonitor state reset'); } /** * Get monitoring statistics * @returns {Object} Statistics object */ getStats() { const recentErrors = this.errorHistory.filter(e => (new Date() - e.timestamp) < 3600000 // Last hour ).length; return { ...this.stats, error_history_size: this.errorHistory.length, pressure_history_size: this.pressureHistory.length, recent_errors_1h: recentErrors, current_pressure: this.pressureHistory.length > 0 ? this.pressureHistory[0].level : 'UNKNOWN', timestamp: new Date() }; } } // Singleton instance const monitor = new ContextPressureMonitor(); module.exports = monitor;