ruvnet/ruflo · error · Error

Can only recover from error state

Error message

Can only recover from error state

What it means

Agent.recover() clears the error state and returns the agent to 'idle' (deleting the recorded lastError metadata), but it is only legal when status === 'error'. This error means recover() was called on a healthy or otherwise-occupied agent — recovery is the single exit from 'error', not a general reset.

Source

Thrown at v3/@claude-flow/swarm/src/domain/entities/agent.ts:238

  /**
   * Mark agent as having an error
   */
  setError(errorMessage?: string): void {
    this._status = 'error';
    if (errorMessage) {
      this._metadata['lastError'] = errorMessage;
      this._metadata['lastErrorAt'] = new Date().toISOString();
    }
    this._updatedAt = new Date();
  }

  /**
   * Recover from error state
   */
  recover(): void {
    if (this._status !== 'error') {
      throw new Error('Can only recover from error state');
    }
    this._status = 'idle';
    delete this._metadata['lastError'];
    this._updatedAt = new Date();
  }

  /**
   * Assign a task to this agent
   */
  assignTask(taskId: string): void {
    if (this._status === 'terminated') {
      throw new Error('Cannot assign task to terminated agent');
    }
    if (this._currentTaskIds.size >= this._maxConcurrentTasks) {
      throw new Error('Agent at maximum concurrent task capacity');
    }

    this._currentTaskIds.add(taskId);

View on GitHub (pinned to fa13ee4ad6)

Solutions

  1. Filter to status === 'error' before calling recover() (fail() is what puts an agent into 'error') After recover(), the agent is 'idle' — call start() to make it active Do not use recover() as a generic reset; the valid pre-states are exactly ['error'] For terminated agents, spawn a replacement instead

Example fix

// before
setInterval(() => agents.forEach(a => a.recover()), 30_000); // throws on healthy agents

// after
setInterval(() => {
  agents.filter(a => a.status === 'error').forEach(a => { a.recover(); a.start(); });
}, 30_000);
Defensive patterns

Strategy: type-guard

Validate before calling

if (agent.status === 'error') { agent.recover(); agent.start(); }

Type guard

function isRecoverable(a) { return a.status === 'error'; }

Try / catch

try { agent.recover(); }
catch (e) {
  if (e instanceof Error && e.message === 'Can only recover from error state') return; // healthy — nothing to do
  throw e;
}

Prevention

When it happens

Trigger: Calling recover() on an 'active'/'busy' agent as a defensive reset before it ever failed Double recovery: first recover() succeeds (status now 'idle'), a second recover() throws Recovery sweeps over all agents that don't filter on status === 'error' Calling recover() on a 'terminated' agent instead of creating a new one

Common situations: Automated health-check loops that 'recover just in case' on every tick Playbooks running recover() -> start() blindly after incidents, even for agents that never errored UI 'reset agent' buttons mapped to recover() regardless of state

Related errors


AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18). Data as JSON: /api/errors/f6afe3ea1ee0169d. Report an issue: GitHub.