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
- Guard the call: read the claim and require claim.contestInfo && !claim.contestInfo.resolution before resolving
- Fix orchestration ordering so resolveContest only fires after contestSteal has succeeded
- When replaying events, rebuild full claim state (including the steal) before applying resolution events
- 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
- Sequence orchestration so resolve only fires after contestSteal commits
- When replaying event logs, apply events in causal order (use the recorded timestamps)
- Expose contest state in the UI so 'resolve' is only offered for contested claims
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
- Only the original claimant can contest the steal
- HANDOFF_PENDING
- Issue is not stealable or not claimed
- NOT_CLAIMED
- Agent at maximum concurrent task capacity
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)