ruvnet/ruflo · error

Can only fail running or assigned tasks

Error message

Can only fail running or assigned tasks

What it means

Task.fail(error) accepts only 'running' or 'assigned' tasks. It records the error, increments the retry count, and either marks the task 'failed' (retries exhausted) or resets it to 'queued' for another attempt. This throw means fail() was invoked on a task outside those states — e.g., double-failing, or failing pending/finished tasks.

Source

Thrown at v3/@claude-flow/swarm/src/domain/entities/task.ts:218

  /**
   * Complete the task successfully
   */
  complete(output?: unknown): void {
    if (this._status !== 'running') {
      throw new Error('Can only complete running tasks');
    }
    this._status = 'completed';
    this._output = output;
    this._completedAt = new Date();
  }

  /**
   * Mark task as failed
   */
  fail(error: string): void {
    if (this._status !== 'running' && this._status !== 'assigned') {
      throw new Error('Can only fail running or assigned tasks');
    }
    this._error = error;
    this._retryCount++;

    if (this._retryCount >= this._maxRetries) {
      this._status = 'failed';
      this._completedAt = new Date();
    } else {
      // Reset for retry
      this._status = 'queued';
      this._assignedAgentId = undefined;
    }
  }

  /**
   * Cancel the task
   */
  cancel(): void {

View on GitHub (pinned to fa13ee4ad6)

Solutions

  1. Gate on task.status === 'running' || task.status === 'assigned' before fail() For pre-queue validation errors, reject at the boundary (don't create the task, or use cancel()) instead of fail() Make failure reporting idempotent — treat a second fail() on the same attempt as a no-op Break complete/fail races with single-flight state transitions per task (lock or version number)

Example fix

// before
catch (e) { task.fail(e.message); } // throws if already failed/queued/cancelled

// after
catch (e) {
  if (task.status === 'running' || task.status === 'assigned') task.fail(e.message);
  else log.warn('stale failure for task %s in state %s', task.id, task.status);
}
Defensive patterns

Strategy: type-guard

Validate before calling

if (task.status === 'running' || task.status === 'assigned') task.fail(err.message);
else if (task.status === 'pending' || task.status === 'queued') task.cancel(); // intake-time error path
else logStaleFailure(task);

Type guard

function isFailable(t) { return t.status === 'running' || t.status === 'assigned'; }

Try / catch

try { task.fail(err.message); }
catch (e) {
  if (e instanceof Error && e.message === 'Can only fail running or assigned tasks') return; // stale/dup failure
  throw e;
}

Prevention

When it happens

Trigger: Reporting failure for a task that is still 'pending'/'queued' (failure before assignment/start, e.g., validation errors at intake) Double fail(): the first fail() reset the task to 'queued' (or 'failed'), the second throws Failing a task after it was cancelled or already completed Retry timer racing a completion: fail() arrives after complete() succeeded

Common situations: Intake-time errors routed into task.fail() instead of a pre-queue rejection path At-least-once error events producing duplicate fail() calls Timeout supervisors failing tasks whose workers already completed them Reconciliation jobs sweeping and failing every task older than X regardless of status

Related errors


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