mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-06 07:29:42 +02:00
feat: add RALPH_STATUS block parsing and circuit breaker pattern
Phase 1 Ralph enhancement implementation: - Parse RALPH_STATUS blocks from Claude output (status, tasks, files, tests) - Circuit breaker state machine (CLOSED → HALF_OPEN → OPEN) for stuck detection - Dual-condition exit gate (completion indicators >= 2 AND EXIT_SIGNAL) - Frontend: circuit breaker badge (yellow warning, red stuck), status block display - API: circuit breaker reset endpoint, ralph status endpoint - Notifications when circuit breaker opens or exit gate is met Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -118,6 +118,24 @@ const CLAUDE_BANNER_PATTERN = /\x1b\[1mClaud/;
|
||||
const CTRL_L_PATTERN = /\x0c/g;
|
||||
const LEADING_WHITESPACE_PATTERN = /^[\s\r\n]+/;
|
||||
|
||||
/**
|
||||
* Formats uptime in seconds to a human-readable string.
|
||||
*/
|
||||
function formatUptime(seconds: number): string {
|
||||
const days = Math.floor(seconds / 86400);
|
||||
const hours = Math.floor((seconds % 86400) / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
|
||||
const parts: string[] = [];
|
||||
if (days > 0) parts.push(`${days}d`);
|
||||
if (hours > 0) parts.push(`${hours}h`);
|
||||
if (minutes > 0) parts.push(`${minutes}m`);
|
||||
if (secs > 0 || parts.length === 0) parts.push(`${secs}s`);
|
||||
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes hook event data before broadcasting via SSE.
|
||||
* Extracts only relevant fields and limits total size to prevent
|
||||
@@ -444,6 +462,50 @@ export class WebServer extends EventEmitter {
|
||||
return { success: true, config: this.store.getConfig() };
|
||||
});
|
||||
|
||||
// Debug/monitoring endpoint - lightweight, only runs when called
|
||||
this.app.get('/api/debug/memory', async () => {
|
||||
const mem = process.memoryUsage();
|
||||
const spawnState = this.spawnOrchestrator.getState();
|
||||
return {
|
||||
memory: {
|
||||
rss: mem.rss,
|
||||
rssMB: Math.round(mem.rss / 1024 / 1024 * 10) / 10,
|
||||
heapUsed: mem.heapUsed,
|
||||
heapUsedMB: Math.round(mem.heapUsed / 1024 / 1024 * 10) / 10,
|
||||
heapTotal: mem.heapTotal,
|
||||
heapTotalMB: Math.round(mem.heapTotal / 1024 / 1024 * 10) / 10,
|
||||
external: mem.external,
|
||||
externalMB: Math.round(mem.external / 1024 / 1024 * 10) / 10,
|
||||
arrayBuffers: mem.arrayBuffers,
|
||||
arrayBuffersMB: Math.round(mem.arrayBuffers / 1024 / 1024 * 10) / 10,
|
||||
},
|
||||
counts: {
|
||||
sessions: this.sessions.size,
|
||||
sseClients: this.sseClients.size,
|
||||
respawnControllers: this.respawnControllers.size,
|
||||
runSummaryTrackers: this.runSummaryTrackers.size,
|
||||
scheduledRuns: this.scheduledRuns.size,
|
||||
terminalBatches: this.terminalBatches.size,
|
||||
outputBatches: this.outputBatches.size,
|
||||
pendingRespawnStarts: this.pendingRespawnStarts.size,
|
||||
respawnTimers: this.respawnTimers.size,
|
||||
subagents: subagentWatcher.getSubagents().length,
|
||||
},
|
||||
spawn: {
|
||||
activeAgents: spawnState.activeCount,
|
||||
queuedAgents: spawnState.queuedCount,
|
||||
totalSpawned: spawnState.totalSpawned,
|
||||
totalCompleted: spawnState.totalCompleted,
|
||||
totalFailed: spawnState.totalFailed,
|
||||
},
|
||||
uptime: {
|
||||
seconds: Math.round(process.uptime()),
|
||||
formatted: formatUptime(process.uptime()),
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
});
|
||||
|
||||
// Session management
|
||||
this.app.get('/api/sessions', async () => this.getSessionsState());
|
||||
|
||||
@@ -759,6 +821,39 @@ export class WebServer extends EventEmitter {
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
// Reset circuit breaker for Ralph tracker
|
||||
this.app.post('/api/sessions/:id/ralph-circuit-breaker/reset', async (req) => {
|
||||
const { id } = req.params as { id: string };
|
||||
const session = this.sessions.get(id);
|
||||
|
||||
if (!session) {
|
||||
return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
|
||||
}
|
||||
|
||||
session.ralphTracker.resetCircuitBreaker();
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
// Get Ralph status block and circuit breaker state
|
||||
this.app.get('/api/sessions/:id/ralph-status', async (req) => {
|
||||
const { id } = req.params as { id: string };
|
||||
const session = this.sessions.get(id);
|
||||
|
||||
if (!session) {
|
||||
return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
lastStatusBlock: session.ralphTracker.lastStatusBlock,
|
||||
circuitBreaker: session.ralphTracker.circuitBreakerStatus,
|
||||
cumulativeStats: session.ralphTracker.cumulativeStats,
|
||||
exitGateMet: session.ralphTracker.exitGateMet,
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// Run prompt in session
|
||||
this.app.post('/api/sessions/:id/run', async (req): Promise<ApiResponse> => {
|
||||
const { id } = req.params as { id: string };
|
||||
@@ -2348,6 +2443,49 @@ export class WebServer extends EventEmitter {
|
||||
if (tracker) tracker.recordRalphCompletion(phrase);
|
||||
});
|
||||
|
||||
// RALPH_STATUS block events
|
||||
session.on('ralphStatusBlockDetected', (block: import('../types.js').RalphStatusBlock) => {
|
||||
this.broadcast('session:ralphStatusUpdate', { sessionId: session.id, block });
|
||||
// Track in run summary
|
||||
const tracker = this.runSummaryTrackers.get(session.id);
|
||||
if (tracker) {
|
||||
tracker.addEvent(
|
||||
block.status === 'BLOCKED' ? 'warning' : 'idle_detected',
|
||||
block.status === 'BLOCKED' ? 'warning' : 'info',
|
||||
`Ralph Status: ${block.status}`,
|
||||
`Tasks: ${block.tasksCompletedThisLoop}, Files: ${block.filesModified}, Tests: ${block.testsStatus}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
session.on('ralphCircuitBreakerUpdate', (status: import('../types.js').CircuitBreakerStatus) => {
|
||||
this.broadcast('session:circuitBreakerUpdate', { sessionId: session.id, status });
|
||||
// Track state changes in run summary
|
||||
const tracker = this.runSummaryTrackers.get(session.id);
|
||||
if (tracker && status.state === 'OPEN') {
|
||||
tracker.addEvent(
|
||||
'warning',
|
||||
'warning',
|
||||
'Circuit Breaker Opened',
|
||||
status.reason
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
session.on('ralphExitGateMet', (data: { completionIndicators: number; exitSignal: boolean }) => {
|
||||
this.broadcast('session:exitGateMet', { sessionId: session.id, ...data });
|
||||
// Track in run summary
|
||||
const tracker = this.runSummaryTrackers.get(session.id);
|
||||
if (tracker) {
|
||||
tracker.addEvent(
|
||||
'ralph_completion',
|
||||
'success',
|
||||
'Exit Gate Met',
|
||||
`Indicators: ${data.completionIndicators}, EXIT_SIGNAL: ${data.exitSignal}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Bash tool tracking events (for clickable file paths)
|
||||
session.on('bashToolStart', (tool: ActiveBashTool) => {
|
||||
this.broadcast('session:bashToolStart', { sessionId: session.id, tool });
|
||||
|
||||
Reference in New Issue
Block a user