coleam00/Archon · warning

Workflow run ${runId} was already resolved and is awaiting r

Error message

Workflow run ${runId} was already resolved and is awaiting resume.

What it means

approveWorkflow resolves the approval gate through workflowDb.resolveApprovalGate, a compare-and-swap that atomically commits the resolution plus audit events. If the CAS reports it did not win, another actor already resolved the gate and the run is awaiting resume, so approve throws rather than double-resolving.

Source

Thrown at packages/core/src/operations/workflow-operations.ts:680

        `approveWorkflow: unexpected child_workflow gate reached resolution for run ${runId}`
      );
    default: {
      const unreachable: never = approval.type;
      throw new Error(`approveWorkflow: unhandled gate type '${String(unreachable)}'`);
    }
  }

  // Compare-and-swap: stamp the resolution AND write the audit events ONLY while
  // the gate is still open, all in one transaction. This atomic UPDATE — not the
  // isGateResolved read above — is the real arbiter, so a concurrent second
  // approve loses here (resolved=false) and throws BEFORE any events/telemetry
  // land, eliminating the duplicates (#2113); folding the events into the same
  // transaction means a failed event write rolls the resolution back so a retry
  // can win the still-open gate (#2146). The run stays 'paused'; resume is
  // guarded independently by resumeWorkflowRun's CAS.
  const { resolved: won } = await workflowDb.resolveApprovalGate(runId, metadataPayload, events);
  if (!won) {
    throw new Error(`Workflow run ${runId} was already resolved and is awaiting resume.`);
  }

  // Won the CAS — resolution + audit events already committed atomically.
  // Anonymous telemetry: binary resolution only — no ids/comments/names.
  captureApprovalResolved({ resolution: 'approved' });
  return {
    workflowName: run.workflow_name,
    workingPath: run.working_path,
    userMessage: run.user_message,
    codebaseId: run.codebase_id,
    conversationId: run.conversation_id,
    type: isInteractiveLoop ? 'interactive_loop' : 'approval_gate',
  };
}

/**
 * Reject a paused workflow run.
 *

View on GitHub (pinned to 0773b97458)

Solutions

  1. Treat this as benign: the gate was decided; check which way it went and wait for the resume.
  2. Catch the error and treat 'already resolved' as success in idempotent callers.
  3. Serialize decision commands per run ID (disable the approve button after first click).
  4. If resume never happens after the win, investigate resumeWorkflowRun's CAS/queue.

Example fix

// before
await approveWorkflow(runId, response);
// after
try {
  await approveWorkflow(runId, response);
} catch (e) {
  if (!(e as Error).message.includes('already resolved')) throw e;
  // benign: gate already decided, awaiting resume
}
Defensive patterns

Strategy: try-catch

Validate before calling

if (isGateResolved(run.metadata?.approval)) return; // someone already decided

Type guard

function isGateResolved(a: unknown): boolean {
  return !!a && typeof a === 'object' && (a as any).resolved != null;
}

Try / catch

try { await approveWorkflow(runId, response); }
catch (e) {
  if ((e as Error).message.includes('already resolved')) {
    // benign CAS loss: gate decided concurrently; poll for resume
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: Two approve/reject calls race on the same paused run and the loser's resolveApprovalGate returns { resolved: false }; also a retry of an approve that already succeeded.

Common situations: An operator and a bot both approve the same gate; a double-click fires two approve requests; a client retries after a timeout unaware the first request committed.

Related errors


AI-assisted analysis of coleam00/Archon@0773b97458 (2026-09-01). Data as JSON: /api/errors/f87f1536540e4788. Report an issue: GitHub.