@calmhive/calmhive-cli
Version:
🐝 Intelligent Claude CLI wrapper with rule injection, AFk background processing, voice interface, and adaptive retry
904 lines (771 loc) • 26 kB
JavaScript
/**
* SessionQualityAnalyzer - Evaluates session robustness and reliability
*
* Monitors and analyzes AFk session quality including:
* - Lifecycle integrity (clean startup/shutdown, state transitions)
* - Data consistency (database sync, metadata accuracy, progress tracking)
* - Recovery capability (resume functionality, error state handling)
* - Resource management (process cleanup, file handle management)
* - Concurrency safety (multiple session handling, race conditions)
*/
const EventEmitter = require('events');
const fs = require('fs').promises;
const path = require('path');
class SessionQualityAnalyzer extends EventEmitter {
constructor(sessionDatabase, processMonitor) {
super();
this.sessionDb = sessionDatabase;
this.processMonitor = processMonitor;
this.metrics = {
lifecycleEvents: [],
dataConsistencyChecks: [],
recoveryTests: [],
resourceUsage: [],
concurrencyEvents: []
};
this.isMonitoring = false;
this.monitoringStartTime = null;
this.qualityThresholds = {
maxStateTransitionTime: 5000, // 5 seconds
maxRecoveryTime: 30000, // 30 seconds
minDataConsistencyRate: 95, // percentage
maxOrphanProcesses: 0,
maxMemoryLeakRate: 10 // MB per hour
};
this.setupLifecycleTracking();
}
/**
* Start comprehensive session quality monitoring
*/
async startMonitoring() {
if (this.isMonitoring) {
console.log('⚠️ Session quality monitoring already active');
return;
}
console.log('🔍 Starting session quality monitoring...');
this.isMonitoring = true;
this.monitoringStartTime = Date.now();
// Start different monitoring aspects
this.startLifecycleMonitoring();
this.startDataConsistencyMonitoring();
this.startResourceMonitoring();
this.startConcurrencyMonitoring();
console.log('✅ Session quality monitoring active');
this.emit('monitoringStarted');
}
/**
* Stop session quality monitoring
*/
stopMonitoring() {
if (!this.isMonitoring) {
console.log('⚠️ Session quality monitoring not active');
return;
}
console.log('🛑 Stopping session quality monitoring...');
this.isMonitoring = false;
this.stopLifecycleMonitoring();
this.stopDataConsistencyMonitoring();
this.stopResourceMonitoring();
this.stopConcurrencyMonitoring();
console.log('✅ Session quality monitoring stopped');
this.emit('monitoringStopped');
}
/**
* Set up lifecycle event tracking
*/
setupLifecycleTracking() {
this.lifecycleStates = new Map(); // sessionId -> state info
this.stateTransitions = new Map(); // sessionId -> transition history
}
/**
* Start monitoring session lifecycle integrity
*/
startLifecycleMonitoring() {
this.lifecycleTimer = setInterval(async () => {
if (!this.isMonitoring) {return;}
try {
await this.checkLifecycleIntegrity();
} catch (error) {
this.recordQualityIssue('lifecycle', 'monitoring_error', error);
}
}, 5000); // Every 5 seconds
}
/**
* Stop lifecycle monitoring
*/
stopLifecycleMonitoring() {
if (this.lifecycleTimer) {
clearInterval(this.lifecycleTimer);
this.lifecycleTimer = null;
}
}
/**
* Check lifecycle integrity for all sessions
*/
async checkLifecycleIntegrity() {
const sessions = await this.sessionDb.getAllSessions();
const currentTime = Date.now();
for (const session of sessions) {
const previousState = this.lifecycleStates.get(session.id);
const currentState = this.analyzeSessionState(session);
// Record state transition if changed
if (!previousState || previousState.status !== currentState.status) {
await this.recordStateTransition(session.id, previousState, currentState, currentTime);
}
// Update current state
this.lifecycleStates.set(session.id, currentState);
// Check for lifecycle anomalies
await this.checkLifecycleAnomalies(session, currentState, currentTime);
}
}
/**
* Analyze current session state
*/
analyzeSessionState(session) {
return {
status: session.status,
pid: session.pid,
progress: session.progress,
lastUpdated: session.last_updated || session.created_at,
hasProcess: this.processMonitor ? this.processMonitor.isProcessActive(session.id) : false,
timestamp: Date.now()
};
}
/**
* Record state transition
*/
async recordStateTransition(sessionId, previousState, currentState, timestamp) {
if (!this.stateTransitions.has(sessionId)) {
this.stateTransitions.set(sessionId, []);
}
const transitions = this.stateTransitions.get(sessionId);
const transition = {
timestamp,
from: previousState?.status || 'unknown',
to: currentState.status,
duration: previousState ? timestamp - previousState.timestamp : 0,
healthy: this.isHealthyTransition(previousState?.status, currentState.status)
};
transitions.push(transition);
// Keep only last 50 transitions per session
if (transitions.length > 50) {
transitions.shift();
}
// Record metric
this.metrics.lifecycleEvents.push({
sessionId,
type: 'state_transition',
...transition
});
// Check transition health
if (!transition.healthy) {
this.recordQualityIssue('lifecycle', 'invalid_transition', {
sessionId,
transition
});
}
// Check transition duration
if (transition.duration > this.qualityThresholds.maxStateTransitionTime) {
this.recordQualityIssue('lifecycle', 'slow_transition', {
sessionId,
duration: transition.duration,
threshold: this.qualityThresholds.maxStateTransitionTime
});
}
}
/**
* Check if state transition is healthy
*/
isHealthyTransition(fromStatus, toStatus) {
const validTransitions = {
'unknown': ['created', 'running', 'failed'],
'created': ['starting', 'running', 'failed', 'cancelled'],
'starting': ['running', 'failed', 'cancelled'],
'running': ['completed', 'failed', 'cancelled'],
'completed': [], // Terminal state
'failed': [], // Terminal state
'cancelled': [] // Terminal state
};
const allowedTransitions = validTransitions[fromStatus] || [];
return allowedTransitions.includes(toStatus);
}
/**
* Check for lifecycle anomalies
*/
async checkLifecycleAnomalies(session, currentState, timestamp) {
// Check for stale running sessions
if (session.status === 'running') {
const lastUpdate = session.last_updated || session.started_at || session.created_at;
const staleDuration = timestamp - lastUpdate;
if (staleDuration > 15 * 60 * 1000) { // 15 minutes
this.recordQualityIssue('lifecycle', 'stale_session', {
sessionId: session.id,
staleDuration,
lastUpdate: new Date(lastUpdate).toISOString()
});
}
}
// Check for orphaned database entries
if (session.status === 'running' && session.pid && !currentState.hasProcess) {
this.recordQualityIssue('lifecycle', 'orphaned_db_entry', {
sessionId: session.id,
pid: session.pid
});
}
// Check for progress inconsistencies
if (session.status === 'running' && session.progress > session.currentIteration * (100 / session.iterations) + 10) {
this.recordQualityIssue('lifecycle', 'progress_inconsistency', {
sessionId: session.id,
progress: session.progress,
expectedProgress: session.currentIteration * (100 / session.iterations)
});
}
}
/**
* Start data consistency monitoring
*/
startDataConsistencyMonitoring() {
this.dataConsistencyTimer = setInterval(async () => {
if (!this.isMonitoring) {return;}
try {
await this.checkDataConsistency();
} catch (error) {
this.recordQualityIssue('data_consistency', 'monitoring_error', error);
}
}, 10000); // Every 10 seconds
}
/**
* Stop data consistency monitoring
*/
stopDataConsistencyMonitoring() {
if (this.dataConsistencyTimer) {
clearInterval(this.dataConsistencyTimer);
this.dataConsistencyTimer = null;
}
}
/**
* Check data consistency across system components
*/
async checkDataConsistency() {
const timestamp = Date.now();
const checks = [];
// Check database vs process monitor consistency
const dbSessions = await this.sessionDb.getSessionsByStatus(['running']);
const activePIDs = this.processMonitor ? this.processMonitor.getAllActiveProcesses() : [];
for (const session of dbSessions) {
const hasActiveProcess = activePIDs.some(p => p.sessionId === session.id);
const consistencyCheck = {
sessionId: session.id,
dbStatus: session.status,
hasActiveProcess,
consistent: (session.status === 'running') === hasActiveProcess,
timestamp
};
checks.push(consistencyCheck);
if (!consistencyCheck.consistent) {
this.recordQualityIssue('data_consistency', 'db_process_mismatch', consistencyCheck);
}
}
// Check AFk registry consistency
for (const session of dbSessions) {
const registryConsistent = await this.checkAFkRegistryConsistency(session);
checks.push({
sessionId: session.id,
type: 'afk_registry',
consistent: registryConsistent,
timestamp
});
if (!registryConsistent) {
this.recordQualityIssue('data_consistency', 'afk_registry_mismatch', {
sessionId: session.id
});
}
}
this.metrics.dataConsistencyChecks.push(...checks);
// Keep only last 1000 checks
if (this.metrics.dataConsistencyChecks.length > 1000) {
this.metrics.dataConsistencyChecks = this.metrics.dataConsistencyChecks.slice(-1000);
}
// Calculate consistency rate
const recentChecks = this.metrics.dataConsistencyChecks.slice(-100);
const consistentChecks = recentChecks.filter(c => c.consistent).length;
const consistencyRate = recentChecks.length > 0 ? (consistentChecks / recentChecks.length) * 100 : 100;
if (consistencyRate < this.qualityThresholds.minDataConsistencyRate) {
this.recordQualityIssue('data_consistency', 'low_consistency_rate', {
rate: consistencyRate,
threshold: this.qualityThresholds.minDataConsistencyRate
});
}
}
/**
* Check AFk registry consistency
*/
async checkAFkRegistryConsistency(session) {
try {
const registryPath = path.join(process.env.HOME, '.claude', 'afk_registry', session.id);
await fs.access(registryPath);
// Check for recent activity in context monitor
const contextLogPath = path.join(registryPath, 'context-monitor.log');
try {
const stats = await fs.stat(contextLogPath);
const ageMinutes = (Date.now() - stats.mtime.getTime()) / (1000 * 60);
return ageMinutes < 20; // Consider consistent if updated within 20 minutes
} catch {
return false; // No context log
}
} catch {
return false; // No registry directory
}
}
/**
* Start resource monitoring
*/
startResourceMonitoring() {
this.resourceTimer = setInterval(async () => {
if (!this.isMonitoring) {return;}
try {
await this.checkResourceManagement();
} catch (error) {
this.recordQualityIssue('resource_management', 'monitoring_error', error);
}
}, 15000); // Every 15 seconds
}
/**
* Stop resource monitoring
*/
stopResourceMonitoring() {
if (this.resourceTimer) {
clearInterval(this.resourceTimer);
this.resourceTimer = null;
}
}
/**
* Check resource management quality
*/
async checkResourceManagement() {
const timestamp = Date.now();
// Check for orphaned processes
const orphanProcesses = this.processMonitor ? await this.processMonitor.findOrphanProcesses() : [];
if (orphanProcesses.length > this.qualityThresholds.maxOrphanProcesses) {
this.recordQualityIssue('resource_management', 'orphaned_processes', {
count: orphanProcesses.length,
threshold: this.qualityThresholds.maxOrphanProcesses,
processes: orphanProcesses
});
}
// Check memory usage
const memUsage = process.memoryUsage();
this.metrics.resourceUsage.push({
timestamp,
heapUsed: memUsage.heapUsed,
heapTotal: memUsage.heapTotal,
rss: memUsage.rss,
orphanProcessCount: orphanProcesses.length
});
// Keep only last 100 measurements
if (this.metrics.resourceUsage.length > 100) {
this.metrics.resourceUsage.shift();
}
// Check for memory leaks
this.checkMemoryLeaks();
}
/**
* Check for memory leaks
*/
checkMemoryLeaks() {
if (this.metrics.resourceUsage.length < 10) {return;}
const recent = this.metrics.resourceUsage.slice(-10);
const timeSpan = recent[recent.length - 1].timestamp - recent[0].timestamp;
const memoryGrowth = recent[recent.length - 1].heapUsed - recent[0].heapUsed;
if (timeSpan > 0) {
const hourlyGrowthMB = (memoryGrowth / timeSpan) * 3600000 / (1024 * 1024);
if (hourlyGrowthMB > this.qualityThresholds.maxMemoryLeakRate) {
this.recordQualityIssue('resource_management', 'memory_leak', {
hourlyGrowthMB,
threshold: this.qualityThresholds.maxMemoryLeakRate
});
}
}
}
/**
* Start concurrency monitoring
*/
startConcurrencyMonitoring() {
this.concurrencyTimer = setInterval(async () => {
if (!this.isMonitoring) {return;}
try {
await this.checkConcurrencySafety();
} catch (error) {
this.recordQualityIssue('concurrency', 'monitoring_error', error);
}
}, 8000); // Every 8 seconds
}
/**
* Stop concurrency monitoring
*/
stopConcurrencyMonitoring() {
if (this.concurrencyTimer) {
clearInterval(this.concurrencyTimer);
this.concurrencyTimer = null;
}
}
/**
* Check concurrency safety
*/
async checkConcurrencySafety() {
const timestamp = Date.now();
const activeSessions = await this.sessionDb.getSessionsByStatus(['running', 'starting']);
// Check for race conditions in session creation
const recentSessions = activeSessions.filter(s =>
timestamp - s.created_at < 60000 // Created within last minute
);
const concurrentCreations = recentSessions.length;
this.metrics.concurrencyEvents.push({
timestamp,
activeSessions: activeSessions.length,
concurrentCreations,
type: 'concurrent_check'
});
// Keep only last 100 events
if (this.metrics.concurrencyEvents.length > 100) {
this.metrics.concurrencyEvents.shift();
}
// Check for potential race conditions
if (concurrentCreations > 3) {
this.recordQualityIssue('concurrency', 'high_concurrent_creation', {
count: concurrentCreations,
activeSessions: activeSessions.length
});
}
// Check for database contention indicators
await this.checkDatabaseContention(activeSessions);
}
/**
* Check for database contention issues
*/
async checkDatabaseContention(activeSessions) {
// This is a simplified check - in practice you'd look for database locks,
// transaction timeouts, etc.
const startTime = Date.now();
try {
// Perform a quick database operation
await this.sessionDb.getAllSessions();
const duration = Date.now() - startTime;
if (duration > 1000) { // Slower than 1 second
this.recordQualityIssue('concurrency', 'database_contention', {
queryDuration: duration,
activeSessions: activeSessions.length
});
}
} catch (error) {
this.recordQualityIssue('concurrency', 'database_error', {
error: error.message,
activeSessions: activeSessions.length
});
}
}
/**
* Test session recovery capability
*/
async testRecoveryCapability(sessionId) {
const startTime = Date.now();
const recoveryTest = {
sessionId,
startTime,
endTime: null,
success: false,
error: null,
steps: []
};
try {
// Step 1: Get session info
recoveryTest.steps.push('get_session_info');
const session = await this.sessionDb.getSession(sessionId);
if (!session) {
throw new Error('Session not found');
}
// Step 2: Check process status
recoveryTest.steps.push('check_process_status');
const processActive = this.processMonitor ?
this.processMonitor.isProcessActive(sessionId) : false;
// Step 3: Validate log accessibility
recoveryTest.steps.push('validate_log_access');
const logPath = path.join(process.env.HOME, '.claude', 'calmhive', 'v3', 'logs', `${sessionId}.log`);
try {
await fs.access(logPath);
} catch {
// Log doesn't exist, which might be ok for new sessions
}
// Step 4: Check AFk registry
recoveryTest.steps.push('check_afk_registry');
const registryPath = path.join(process.env.HOME, '.claude', 'afk_registry', sessionId);
try {
await fs.access(registryPath);
} catch {
// Registry doesn't exist for this session
}
recoveryTest.success = true;
recoveryTest.endTime = Date.now();
} catch (error) {
recoveryTest.error = error.message;
recoveryTest.endTime = Date.now();
}
const duration = recoveryTest.endTime - recoveryTest.startTime;
recoveryTest.duration = duration;
this.metrics.recoveryTests.push(recoveryTest);
// Keep only last 50 tests
if (this.metrics.recoveryTests.length > 50) {
this.metrics.recoveryTests.shift();
}
// Check recovery performance
if (duration > this.qualityThresholds.maxRecoveryTime) {
this.recordQualityIssue('recovery', 'slow_recovery', {
sessionId,
duration,
threshold: this.qualityThresholds.maxRecoveryTime
});
}
if (!recoveryTest.success) {
this.recordQualityIssue('recovery', 'recovery_failure', {
sessionId,
error: recoveryTest.error,
steps: recoveryTest.steps
});
}
return recoveryTest;
}
/**
* Record a quality issue
*/
recordQualityIssue(category, type, details) {
const issue = {
timestamp: Date.now(),
category,
type,
details,
severity: this.calculateSeverity(category, type)
};
this.emit('qualityIssue', issue);
// Store in metrics
if (!this.metrics[`${category}_issues`]) {
this.metrics[`${category}_issues`] = [];
}
this.metrics[`${category}_issues`].push(issue);
}
/**
* Calculate issue severity
*/
calculateSeverity(category, type) {
const severityMap = {
lifecycle: {
invalid_transition: 'high',
slow_transition: 'medium',
stale_session: 'high',
orphaned_db_entry: 'high',
progress_inconsistency: 'medium'
},
data_consistency: {
db_process_mismatch: 'high',
afk_registry_mismatch: 'medium',
low_consistency_rate: 'high'
},
resource_management: {
orphaned_processes: 'medium',
memory_leak: 'high'
},
concurrency: {
high_concurrent_creation: 'medium',
database_contention: 'high',
database_error: 'high'
},
recovery: {
slow_recovery: 'medium',
recovery_failure: 'high'
}
};
return severityMap[category]?.[type] || 'low';
}
/**
* Generate comprehensive session quality report
*/
generateQualityReport() {
const now = Date.now();
const monitoringDuration = this.monitoringStartTime ?
(now - this.monitoringStartTime) / 1000 : 0;
const report = {
timestamp: now,
monitoringDuration,
lifecycleIntegrity: this.analyzeLifecycleIntegrity(),
dataConsistency: this.analyzeDataConsistency(),
recoveryCapability: this.analyzeRecoveryCapability(),
resourceManagement: this.analyzeResourceManagement(),
concurrencySafety: this.analyzeConcurrencySafety(),
overallScore: 0,
issues: this.getRecentIssues(),
recommendations: []
};
// Calculate overall score
const scores = [
report.lifecycleIntegrity.score,
report.dataConsistency.score,
report.recoveryCapability.score,
report.resourceManagement.score,
report.concurrencySafety.score
];
report.overallScore = scores.reduce((a, b) => a + b, 0) / scores.length;
// Generate recommendations
report.recommendations = this.generateQualityRecommendations(report);
return report;
}
/**
* Analyze lifecycle integrity
*/
analyzeLifecycleIntegrity() {
const transitions = this.metrics.lifecycleEvents;
const healthyTransitions = transitions.filter(t => t.healthy).length;
const totalTransitions = transitions.length;
let score = totalTransitions > 0 ? (healthyTransitions / totalTransitions) * 100 : 100;
return {
score: Math.max(0, score),
totalTransitions,
healthyTransitions,
invalidTransitions: totalTransitions - healthyTransitions
};
}
/**
* Analyze data consistency
*/
analyzeDataConsistency() {
const checks = this.metrics.dataConsistencyChecks;
const consistentChecks = checks.filter(c => c.consistent).length;
const totalChecks = checks.length;
let score = totalChecks > 0 ? (consistentChecks / totalChecks) * 100 : 100;
return {
score: Math.max(0, score),
totalChecks,
consistentChecks,
inconsistentChecks: totalChecks - consistentChecks
};
}
/**
* Analyze recovery capability
*/
analyzeRecoveryCapability() {
const tests = this.metrics.recoveryTests;
const successfulTests = tests.filter(t => t.success).length;
const totalTests = tests.length;
let score = totalTests > 0 ? (successfulTests / totalTests) * 100 : 100;
const avgRecoveryTime = tests.length > 0 ?
tests.reduce((sum, t) => sum + t.duration, 0) / tests.length : 0;
return {
score: Math.max(0, score),
totalTests,
successfulTests,
failedTests: totalTests - successfulTests,
avgRecoveryTime
};
}
/**
* Analyze resource management
*/
analyzeResourceManagement() {
const usage = this.metrics.resourceUsage;
const recentUsage = usage.slice(-10);
let score = 100;
// Check for orphaned processes
const hasOrphans = recentUsage.some(u => u.orphanProcessCount > 0);
if (hasOrphans) {score -= 30;}
// Check memory growth
if (recentUsage.length > 1) {
const memGrowth = recentUsage[recentUsage.length - 1].heapUsed - recentUsage[0].heapUsed;
if (memGrowth > 50 * 1024 * 1024) {score -= 20;} // 50MB growth
}
return {
score: Math.max(0, score),
memoryMeasurements: usage.length,
currentMemoryMB: usage.length > 0 ? (usage[usage.length - 1].heapUsed / 1024 / 1024).toFixed(2) : 0
};
}
/**
* Analyze concurrency safety
*/
analyzeConcurrencySafety() {
const events = this.metrics.concurrencyEvents;
let score = 100;
// Check for high concurrency issues
const highConcurrencyEvents = events.filter(e => e.concurrentCreations > 3).length;
if (highConcurrencyEvents > 0) {score -= 40;}
return {
score: Math.max(0, score),
totalEvents: events.length,
highConcurrencyEvents
};
}
/**
* Get recent quality issues
*/
getRecentIssues(hours = 1) {
const cutoff = Date.now() - (hours * 60 * 60 * 1000);
const allIssues = [];
// Collect issues from all categories
Object.keys(this.metrics).forEach(key => {
if (key.endsWith('_issues')) {
const issues = this.metrics[key] || [];
allIssues.push(...issues.filter(issue => issue.timestamp > cutoff));
}
});
return allIssues.sort((a, b) => b.timestamp - a.timestamp);
}
/**
* Generate quality improvement recommendations
*/
generateQualityRecommendations(report) {
const recommendations = [];
if (report.lifecycleIntegrity.score < 90) {
recommendations.push({
priority: 'high',
category: 'lifecycle',
issue: 'Lifecycle integrity issues detected',
suggestion: 'Review state transition logic and add validation'
});
}
if (report.dataConsistency.score < 95) {
recommendations.push({
priority: 'high',
category: 'data_consistency',
issue: 'Data consistency problems found',
suggestion: 'Implement database transaction safety and sync validation'
});
}
if (report.recoveryCapability.score < 85) {
recommendations.push({
priority: 'medium',
category: 'recovery',
issue: 'Session recovery failures detected',
suggestion: 'Improve error handling and recovery mechanisms'
});
}
return recommendations;
}
/**
* Get current metrics snapshot
*/
getMetricsSnapshot() {
return {
timestamp: Date.now(),
isMonitoring: this.isMonitoring,
monitoringDuration: this.monitoringStartTime ?
Date.now() - this.monitoringStartTime : 0,
metrics: {
lifecycleEvents: [...this.metrics.lifecycleEvents],
dataConsistencyChecks: [...this.metrics.dataConsistencyChecks],
recoveryTests: [...this.metrics.recoveryTests],
resourceUsage: [...this.metrics.resourceUsage],
concurrencyEvents: [...this.metrics.concurrencyEvents]
},
currentStates: Object.fromEntries(this.lifecycleStates),
stateTransitions: Object.fromEntries(this.stateTransitions)
};
}
}
module.exports = SessionQualityAnalyzer;