mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-02 05:29:42 +02:00
Split 3 large files into 11 focused sub-modules via composition: ralph-tracker.ts (3,868 → ~2,400 LOC): - ralph-plan-tracker.ts: plan task tracking, checkpoints, history - ralph-fix-plan-watcher.ts: @fix_plan.md file watching - ralph-stall-detector.ts: iteration stall detection - ralph-status-parser.ts: RALPH_STATUS block parsing, circuit breaker respawn-controller.ts (3,611 → ~3,200 LOC): - respawn-patterns.ts: pure pattern detection functions - respawn-adaptive-timing.ts: adaptive timing with percentile calc - respawn-metrics.ts: cycle metrics tracking + aggregation - respawn-health.ts: pure health scoring functions session.ts (2,418 → ~1,800 LOC): - session-cli-builder.ts: CLI argument construction - session-auto-ops.ts: auto-compact/clear automation - session-task-cache.ts: task description LRU cache All external APIs preserved via delegation. Events forwarded through parent classes. Zero behavioral changes — all 436 tests pass. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
230 lines
7.4 KiB
TypeScript
230 lines
7.4 KiB
TypeScript
/**
|
|
* @fileoverview Pure health scoring functions for respawn controller.
|
|
*
|
|
* Extracted from respawn-controller.ts for modularity. All functions are pure
|
|
* (no side effects, no state) and take a HealthInputs interface that decouples
|
|
* them from direct access to Session, RalphTracker, or AiChecker instances.
|
|
*
|
|
* @module respawn-health
|
|
*/
|
|
|
|
import type { RespawnAggregateMetrics, RalphLoopHealthScore, HealthStatus, CircuitBreakerStatus } from './types.js';
|
|
|
|
/**
|
|
* Input data for health score calculation.
|
|
* Decouples the health calculation from direct access to controller internals.
|
|
*/
|
|
export interface HealthInputs {
|
|
/** Aggregate cycle metrics */
|
|
aggregateMetrics: RespawnAggregateMetrics;
|
|
/** Current circuit breaker status */
|
|
circuitBreakerStatus: CircuitBreakerStatus | null;
|
|
/** Iteration stall metrics, or null if tracker unavailable */
|
|
iterationStallMetrics: {
|
|
stallDurationMs: number;
|
|
warningThresholdMs: number;
|
|
criticalThresholdMs: number;
|
|
} | null;
|
|
/** AI checker state summary */
|
|
aiCheckerState: {
|
|
status: string;
|
|
consecutiveErrors: number;
|
|
};
|
|
/** Number of stuck-state recovery attempts */
|
|
stuckRecoveryCount: number;
|
|
/** Maximum allowed stuck recoveries */
|
|
maxStuckRecoveries: number;
|
|
}
|
|
|
|
/**
|
|
* Calculate a comprehensive health score for the Ralph Loop system.
|
|
* Aggregates multiple health signals into a single score (0-100).
|
|
*
|
|
* @param inputs - Health calculation inputs
|
|
* @returns Health score with component breakdown
|
|
*/
|
|
export function calculateHealthScore(inputs: HealthInputs): RalphLoopHealthScore {
|
|
const now = Date.now();
|
|
const components = {
|
|
cycleSuccess: calculateCycleSuccessScore(inputs.aggregateMetrics),
|
|
circuitBreaker: calculateCircuitBreakerScore(inputs.circuitBreakerStatus),
|
|
iterationProgress: calculateIterationProgressScore(inputs.iterationStallMetrics),
|
|
aiChecker: calculateAiCheckerScore(inputs.aiCheckerState),
|
|
stuckRecovery: calculateStuckRecoveryScore(inputs.stuckRecoveryCount, inputs.maxStuckRecoveries),
|
|
};
|
|
|
|
// Weighted average (cycle success is most important)
|
|
const weights = {
|
|
cycleSuccess: 0.35,
|
|
circuitBreaker: 0.2,
|
|
iterationProgress: 0.2,
|
|
aiChecker: 0.15,
|
|
stuckRecovery: 0.1,
|
|
};
|
|
|
|
const score = Math.round(
|
|
components.cycleSuccess * weights.cycleSuccess +
|
|
components.circuitBreaker * weights.circuitBreaker +
|
|
components.iterationProgress * weights.iterationProgress +
|
|
components.aiChecker * weights.aiChecker +
|
|
components.stuckRecovery * weights.stuckRecovery
|
|
);
|
|
|
|
// Determine status
|
|
let status: HealthStatus;
|
|
if (score >= 90) status = 'excellent';
|
|
else if (score >= 70) status = 'good';
|
|
else if (score >= 50) status = 'degraded';
|
|
else status = 'critical';
|
|
|
|
// Generate recommendations
|
|
const recommendations = generateHealthRecommendations(components);
|
|
|
|
// Generate summary
|
|
const summary = generateHealthSummary(score, status, components);
|
|
|
|
return {
|
|
score,
|
|
status,
|
|
components,
|
|
summary,
|
|
recommendations,
|
|
calculatedAt: now,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Determine whether to skip the /clear step based on current context usage.
|
|
* Skips if token count is below the configured threshold percentage.
|
|
*
|
|
* @param lastTokenCount - Current token count from the session
|
|
* @param skipClearThresholdPercent - Threshold percentage below which to skip /clear
|
|
* @param maxContextTokens - Approximate max context window size
|
|
* @returns True if /clear should be skipped
|
|
*/
|
|
export function shouldSkipClear(
|
|
lastTokenCount: number,
|
|
skipClearThresholdPercent: number,
|
|
maxContextTokens: number
|
|
): boolean {
|
|
if (lastTokenCount === 0) return false; // Can't determine, don't skip
|
|
|
|
const usagePercent = (lastTokenCount / maxContextTokens) * 100;
|
|
return usagePercent < skipClearThresholdPercent;
|
|
}
|
|
|
|
/**
|
|
* Calculate score based on recent cycle success rate.
|
|
*/
|
|
function calculateCycleSuccessScore(aggregateMetrics: RespawnAggregateMetrics): number {
|
|
if (aggregateMetrics.totalCycles === 0) return 100; // No data = assume healthy
|
|
return aggregateMetrics.successRate;
|
|
}
|
|
|
|
/**
|
|
* Calculate score based on circuit breaker state.
|
|
*/
|
|
function calculateCircuitBreakerScore(circuitBreakerStatus: CircuitBreakerStatus | null): number {
|
|
if (!circuitBreakerStatus) return 100;
|
|
|
|
switch (circuitBreakerStatus.state) {
|
|
case 'CLOSED':
|
|
return 100;
|
|
case 'HALF_OPEN':
|
|
return 50;
|
|
case 'OPEN':
|
|
return 0;
|
|
default:
|
|
return 100;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calculate score based on iteration progress.
|
|
*/
|
|
function calculateIterationProgressScore(
|
|
stallMetrics: { stallDurationMs: number; warningThresholdMs: number; criticalThresholdMs: number } | null
|
|
): number {
|
|
if (!stallMetrics) return 100;
|
|
|
|
const { stallDurationMs, warningThresholdMs, criticalThresholdMs } = stallMetrics;
|
|
|
|
if (stallDurationMs >= criticalThresholdMs) return 0;
|
|
if (stallDurationMs >= warningThresholdMs) return 30;
|
|
if (stallDurationMs >= warningThresholdMs / 2) return 70;
|
|
return 100;
|
|
}
|
|
|
|
/**
|
|
* Calculate score based on AI checker health.
|
|
*/
|
|
function calculateAiCheckerScore(aiCheckerState: { status: string; consecutiveErrors: number }): number {
|
|
if (aiCheckerState.status === 'disabled') return 30;
|
|
if (aiCheckerState.status === 'cooldown') return 70;
|
|
if (aiCheckerState.consecutiveErrors > 0) return 50;
|
|
return 100;
|
|
}
|
|
|
|
/**
|
|
* Calculate score based on stuck-state recovery count.
|
|
*/
|
|
function calculateStuckRecoveryScore(stuckRecoveryCount: number, maxStuckRecoveries: number): number {
|
|
if (stuckRecoveryCount === 0) return 100;
|
|
if (stuckRecoveryCount >= maxStuckRecoveries) return 0;
|
|
return Math.round(100 - (stuckRecoveryCount / maxStuckRecoveries) * 100);
|
|
}
|
|
|
|
/**
|
|
* Generate health recommendations based on component scores.
|
|
*/
|
|
function generateHealthRecommendations(components: RalphLoopHealthScore['components']): string[] {
|
|
const recommendations: string[] = [];
|
|
|
|
if (components.cycleSuccess < 70) {
|
|
recommendations.push('Cycle success rate is low. Check for recurring errors or stuck states.');
|
|
}
|
|
if (components.circuitBreaker < 50) {
|
|
recommendations.push('Circuit breaker is open or half-open. Review recent errors and consider manual reset.');
|
|
}
|
|
if (components.iterationProgress < 50) {
|
|
recommendations.push('Iteration progress has stalled. Check if Claude is stuck on a task.');
|
|
}
|
|
if (components.aiChecker < 50) {
|
|
recommendations.push('AI idle checker has errors. May need to check Claude CLI availability.');
|
|
}
|
|
if (components.stuckRecovery < 50) {
|
|
recommendations.push('Multiple stuck-state recoveries occurred. Consider increasing timeouts.');
|
|
}
|
|
|
|
if (recommendations.length === 0) {
|
|
recommendations.push('System is healthy. No action needed.');
|
|
}
|
|
|
|
return recommendations;
|
|
}
|
|
|
|
/**
|
|
* Generate a human-readable health summary.
|
|
*/
|
|
function generateHealthSummary(
|
|
score: number,
|
|
status: HealthStatus,
|
|
components: RalphLoopHealthScore['components']
|
|
): string {
|
|
const lowest = Object.entries(components).reduce((min, [key, val]) => (val < min.val ? { key, val } : min), {
|
|
key: '',
|
|
val: 100,
|
|
});
|
|
|
|
if (status === 'excellent') {
|
|
return `Ralph Loop is operating excellently (${score}/100). All systems healthy.`;
|
|
}
|
|
if (status === 'good') {
|
|
return `Ralph Loop is operating well (${score}/100). Minor issues in ${lowest.key}.`;
|
|
}
|
|
if (status === 'degraded') {
|
|
return `Ralph Loop is degraded (${score}/100). Primary issue: ${lowest.key} (${lowest.val}/100).`;
|
|
}
|
|
return `Ralph Loop is in critical state (${score}/100). Immediate attention needed: ${lowest.key}.`;
|
|
}
|