{"record":{"id":"f6afe3ea1ee0169d","repo":"ruvnet/ruflo","slug":"can-only-recover-from-error-state","errorCode":null,"errorMessage":"Can only recover from error state","messagePattern":"Can only recover from error state","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/swarm/src/domain/entities/agent.ts","lineNumber":238,"sourceCode":"\n  /**\n   * Mark agent as having an error\n   */\n  setError(errorMessage?: string): void {\n    this._status = 'error';\n    if (errorMessage) {\n      this._metadata['lastError'] = errorMessage;\n      this._metadata['lastErrorAt'] = new Date().toISOString();\n    }\n    this._updatedAt = new Date();\n  }\n\n  /**\n   * Recover from error state\n   */\n  recover(): void {\n    if (this._status !== 'error') {\n      throw new Error('Can only recover from error state');\n    }\n    this._status = 'idle';\n    delete this._metadata['lastError'];\n    this._updatedAt = new Date();\n  }\n\n  /**\n   * Assign a task to this agent\n   */\n  assignTask(taskId: string): void {\n    if (this._status === 'terminated') {\n      throw new Error('Cannot assign task to terminated agent');\n    }\n    if (this._currentTaskIds.size >= this._maxConcurrentTasks) {\n      throw new Error('Agent at maximum concurrent task capacity');\n    }\n\n    this._currentTaskIds.add(taskId);","sourceCodeStart":220,"sourceCodeEnd":256,"githubUrl":"https://github.com/ruvnet/ruflo/blob/fa13ee4ad60ac2090b1480656eb233521790d640/v3/@claude-flow/swarm/src/domain/entities/agent.ts#L220-L256","documentation":"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.","triggerScenarios":"Calling recover() on an 'active'/'busy' agent as a defensive reset before it ever failed\nDouble recovery: first recover() succeeds (status now 'idle'), a second recover() throws\nRecovery sweeps over all agents that don't filter on status === 'error'\nCalling recover() on a 'terminated' agent instead of creating a new one","commonSituations":"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\nUI 'reset agent' buttons mapped to recover() regardless of state","solutions":["Filter to status === 'error' before calling recover() (fail() is what puts an agent into 'error')\nAfter recover(), the agent is 'idle' — call start() to make it active\nDo not use recover() as a generic reset; the valid pre-states are exactly ['error']\nFor terminated agents, spawn a replacement instead"],"exampleFix":"// before\nsetInterval(() => agents.forEach(a => a.recover()), 30_000); // throws on healthy agents\n\n// after\nsetInterval(() => {\n  agents.filter(a => a.status === 'error').forEach(a => { a.recover(); a.start(); });\n}, 30_000);","handlingStrategy":"type-guard","validationCode":"if (agent.status === 'error') { agent.recover(); agent.start(); }","typeGuard":"function isRecoverable(a) { return a.status === 'error'; }","tryCatchPattern":"try { agent.recover(); }\ncatch (e) {\n  if (e instanceof Error && e.message === 'Can only recover from error state') return; // healthy — nothing to do\n  throw e;\n}","preventionTips":["Only enter recovery from monitoring that observed status === 'error' (set by fail())\nDon't use recover() as a generic reset button\nFollow recover() with start() — it lands the agent in 'idle'"],"tags":["agent","state-machine","error-recovery","domain-entity","typescript"],"backgroundTag":"state-machine-invalid-transition","analyzedSha":"fa13ee4ad60ac2090b1480656eb233521790d640","analyzedAt":"2026-08-18T21:34:22.708Z","schemaVersion":2},"datasetVersion":"2026-08-22T04:17:13.399Z"}