ruvnet/ruflo · error

No contest to resolve

Error message

No contest to resolve

What it means

Thrown by resolveContest when the claim exists but claim.contestInfo is undefined — no steal contest was ever opened for it. resolveContest only applies to claims whose steal produced contestInfo (a contestedBy record, window, and future resolution); calling it on a normally-held or never-stolen claim is an invalid state transition.

Solutions

  1. Guard the call: read the claim and require claim.contestInfo && !claim.contestInfo.resolution before resolving
  2. Fix orchestration ordering so resolveContest only fires after contestSteal has succeeded
  3. When replaying events, rebuild full claim state (including the steal) before applying resolution events
  4. Treat the missing-contest case as an idempotent no-op if your delivery layer may reorder

Example fix

// before
await svc.resolveContest(issueId, winner, reason);

// after
const claim = await svc.repository.findByIssueId(issueId);
if (claim?.contestInfo && !claim.contestInfo.resolution) {
  await svc.resolveContest(issueId, winner, reason);
}
Defensive patterns

Strategy: try-catch

Validate before calling

const claim = await repository.findByIssueId(issueId);
if (claim?.contestInfo && !claim.contestInfo.resolution) {
  await svc.resolveContest(issueId, winner, reason);
}

Type guard

function hasOpenContest(claim: IssueClaim | null): boolean {
  return !!claim?.contestInfo && !claim.contestInfo.resolution;
}

Try / catch

try { await svc.resolveContest(issueId, winner, reason); }
catch (e) { if (e instanceof Error && e.message === 'No contest to resolve') { /* ordering race: re-deliver later or no-op */ } else throw e; }

Prevention

When it happens

Trigger: resolveContest invoked before anyone called contestSteal (or before a steal created contestInfo); resolving a claim that was acquired without stealing; a claim row recreated after a previous contest finished, wiping contestInfo.

Common situations: Event-ordering races where a resolution command arrives before the steal/contest event; admin UIs that offer 'resolve' on every claim; replaying recorded resolution events against a fresh repository that never saw the steal.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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

Appendix: source

Thrown at v3/@claude-flow/claims/src/application/work-stealing-service.ts:410

    await this.emitContestEvent(claim);
  }

  // ===========================================================================
  // Resolve Contest
  // ===========================================================================

  /**
   * Resolve a contest (queen or human decides the winner)
   */
  async resolveContest(issueId: IssueId, winner: Claimant, reason: string): Promise<void> {
    const claim = await this.repository.findByIssueId(issueId);

    if (!claim) {
      throw new Error(`Claim not found for issue: ${issueId}`);
    }

    if (!claim.contestInfo) {
      throw new Error('No contest to resolve');
    }

    if (claim.contestInfo.resolution) {
      throw new Error('Contest has already been resolved');
    }

    const now = new Date();
    const resolvedBy = this.determineResolver(winner, claim.contestInfo);

    // Create resolution
    const resolution: ContestResolution = {
      resolvedAt: now,
      winner,
      resolvedBy,
      reason,
    };

    claim.contestInfo.resolution = resolution;

View on GitHub (pinned to fa13ee4ad6)