Implement auto-recovery for consensus failures

- Add iteration tracking and stuck detection to orchestrator
- Add triggerAutoRecovery function for automatic pipeline respawn
- Store structured failure context (proposals, conflicts, reason)
- Force GAMMA agent on recovery attempts for conflict resolution
- Limit auto-recovery to 3 attempts to prevent infinite loops
- Add UI status badges for rebooting/aborted states
- Add failure-context API endpoint for orchestrator handoff
- Add test_auto_recovery.py with 6 passing tests

Exit codes: 0=success, 1=error, 2=consensus failure, 3=aborted

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
profit
2026-01-24 19:28:27 -05:00
co-authored by Claude Opus 4.5
parent c6554a8b3d
commit a19535b580
3 changed files with 892 additions and 33 deletions
+161 -10
View File
@@ -77,6 +77,10 @@ export class MultiAgentOrchestrator {
private startTime!: number;
private monitorInterval?: ReturnType<typeof setInterval>;
private errorCount: number = 0;
private iterationCount: number = 0;
private maxIterations: number = 10;
private lastProgressTime: number = 0;
private progressTimeout: number = 60000; // 60 seconds without progress = stuck
constructor(model: string = "anthropic/claude-sonnet-4") {
// Use environment variable for task ID if provided
@@ -93,6 +97,85 @@ export class MultiAgentOrchestrator {
this.log(`ERROR [${severity}] ${errorType}: ${details}`);
}
private updateProgress(): void {
this.lastProgressTime = Date.now();
this.iterationCount++;
}
private isStuck(): boolean {
if (this.lastProgressTime === 0) return false;
return (Date.now() - this.lastProgressTime) > this.progressTimeout;
}
private isIterationLimitExceeded(): boolean {
return this.iterationCount >= this.maxIterations;
}
// Request token revocation for stuck agents
private async revokeStuckAgentTokens(): Promise<void> {
if (!this.pipelineId) return;
try {
await fetch("http://localhost:3000/api/pipeline/revoke", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
pipeline_id: this.pipelineId,
reason: "iteration_timeout",
details: `Agents stuck after ${this.iterationCount} iterations`
})
});
this.log("Token revocation requested for stuck agents");
} catch (e: any) {
this.log(`Failed to revoke tokens: ${e.message}`);
}
}
// Record structured failure context for auto-recovery
private async recordFailureContext(
reason: "stuck" | "iteration_limit" | "consensus_failed",
metrics: CoordinationMetrics
): Promise<void> {
if (!this.pipelineId) return;
const failureContext = {
pipeline_id: this.pipelineId,
task_id: this.taskId,
failure_reason: reason,
failure_time: new Date().toISOString(),
iteration_count: this.iterationCount,
elapsed_ms: Date.now() - this.startTime,
metrics: metrics,
gamma_spawned: this.gammaAgent !== undefined,
error_count: this.errorCount,
recovery_hint: this.getRecoveryHint(reason)
};
try {
await fetch("http://localhost:3000/api/pipeline/failure-context", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(failureContext)
});
this.log(`Failure context recorded: ${reason}`);
} catch (e: any) {
this.log(`Failed to record failure context: ${e.message}`);
}
}
private getRecoveryHint(reason: string): string {
switch (reason) {
case "stuck":
return "Agents became unresponsive. Try with fresh agents or GAMMA mediator.";
case "iteration_limit":
return "Max iterations reached without consensus. Consider simplifying objective.";
case "consensus_failed":
return "Agents completed but disagreed. GAMMA mediator may help resolve conflicts.";
default:
return "Unknown failure. Review logs for details.";
}
}
private log(msg: string) {
const elapsed = this.startTime ? ((Date.now() - this.startTime) / 1000).toFixed(1) : "0.0";
console.log(`[${elapsed}s] [ORCHESTRATOR] ${msg}`);
@@ -178,7 +261,24 @@ export class MultiAgentOrchestrator {
this.log("GAMMA agent spawned and initialized");
}
private async monitorConditions(): Promise<void> {
private async monitorConditions(): Promise<{ abort: boolean; reason?: string }> {
// Track progress
this.updateProgress();
// Check for iteration timeout (stuck without progress)
if (this.isStuck()) {
this.log("TIMEOUT: No progress detected - triggering auto-recovery");
await this.reportError("iteration_timeout", "high", "Agents stuck without progress");
return { abort: true, reason: "stuck" };
}
// Check for iteration limit exceeded
if (this.isIterationLimitExceeded()) {
this.log("LIMIT: Maximum iterations exceeded - triggering auto-recovery");
await this.reportError("iteration_limit", "high", `Max iterations (${this.maxIterations}) exceeded`);
return { abort: true, reason: "iteration_limit" };
}
// Check stuck condition
const stuckAgents = await this.stateManager.detectStuckAgents(30);
if (stuckAgents.length > 0) {
@@ -218,17 +318,31 @@ export class MultiAgentOrchestrator {
// Log current state
const states = await this.stateManager.getAllStates();
const statesSummary = states.map(s => `${s.role}:${s.status}(${(s.progress * 100).toFixed(0)}%)`).join(", ");
this.log(`Status: ${statesSummary} | Messages: ${metricsData.total_messages} | Conflicts: ${unresolvedConflicts}`);
this.log(`Status: ${statesSummary} | Messages: ${metricsData.total_messages} | Conflicts: ${unresolvedConflicts} | Iter: ${this.iterationCount}`);
return { abort: false };
}
async runTask(task: TaskDefinition): Promise<CoordinationMetrics> {
async runTask(task: TaskDefinition): Promise<CoordinationMetrics & { abort_reason?: string }> {
this.log(`Starting task: ${task.objective.slice(0, 60)}...`);
this.lastProgressTime = Date.now();
// Write task to blackboard
await this.blackboard.write("problem", "task_definition", task, "ALPHA");
// Start monitoring
this.monitorInterval = setInterval(() => this.monitorConditions(), 2000);
// Track if we need to abort due to timeout/iteration limit
let abortReason: string | undefined;
// Start monitoring with abort detection
this.monitorInterval = setInterval(async () => {
const result = await this.monitorConditions();
if (result.abort && result.reason) {
abortReason = result.reason;
if (this.monitorInterval) {
clearInterval(this.monitorInterval);
}
}
}, 2000);
// Run agents in parallel
this.log("Launching ALPHA and BETA in parallel...");
@@ -250,6 +364,23 @@ export class MultiAgentOrchestrator {
timeoutPromise,
]);
// Check if we were aborted during execution
if (abortReason) {
this.log(`Task aborted: ${abortReason}`);
if (this.monitorInterval) {
clearInterval(this.monitorInterval);
}
// Revoke tokens for stuck agents
await this.revokeStuckAgentTokens();
// Get partial metrics and mark as failed
const partialMetrics = await this.metrics.finalize(false);
await this.recordFailureContext(abortReason as any, partialMetrics);
return { ...partialMetrics, abort_reason: abortReason };
}
this.log("Initial agents completed or timeout reached");
// Check if GAMMA needs to be spawned for success validation
@@ -287,6 +418,11 @@ export class MultiAgentOrchestrator {
// Finalize metrics
const finalMetrics = await this.metrics.finalize(consensusAchieved);
// If consensus failed, record structured failure context
if (!consensusAchieved) {
await this.recordFailureContext("consensus_failed", finalMetrics);
}
return finalMetrics;
}
@@ -449,16 +585,31 @@ The solution should consider fault tolerance, data consistency, and cost optimiz
analyzePerformance(metrics);
// Output special marker for server to parse consensus status
// Format: ORCHESTRATION_RESULT:{"consensus":true/false,"metrics":{...}}
// Format: ORCHESTRATION_RESULT:{"consensus":true/false,"metrics":{...},"abort_reason":...}
console.log("\nORCHESTRATION_RESULT:" + JSON.stringify({
consensus: metrics.final_consensus,
task_id: metrics.task_id,
metrics: metrics
metrics: metrics,
abort_reason: (metrics as any).abort_reason || null,
requires_auto_recovery: !metrics.final_consensus || !!(metrics as any).abort_reason
}));
// Exit with code 2 for consensus failure (distinct from error=1, success=0)
if (!metrics.final_consensus) {
console.log("\n[ORCHESTRATOR] Consensus NOT achieved - exiting with code 2");
// Exit codes:
// 0 = Success (consensus achieved)
// 1 = Error (crash or exception)
// 2 = Consensus failure (agents completed but no agreement)
// 3 = Aborted (timeout/stuck/iteration limit - auto-recovery needed)
if ((metrics as any).abort_reason) {
console.log(`\n[ORCHESTRATOR] Task aborted: ${(metrics as any).abort_reason} - exiting with code 3 (auto-recovery needed)`);
exitCode = 3;
const pipelineId = process.env.PIPELINE_ID;
if (pipelineId) {
await reportErrorToObservability(pipelineId, "orchestrator_aborted", "high",
`Orchestration aborted: ${(metrics as any).abort_reason}. Auto-recovery required.`);
}
} else if (!metrics.final_consensus) {
console.log("\n[ORCHESTRATOR] Consensus NOT achieved - exiting with code 2 (auto-recovery needed)");
exitCode = 2;
// Report consensus failure to observability