ruvnet/ruflo · warning · Error
Contest window has expired
Error message
Contest window has expired
What it means
Plain Error thrown by contestSteal when nowMs() exceeds claim.contestInfo.windowEndsAt. The contest window (config.contestWindowMinutes in DEFAULT_WORK_STEALING_CONFIG) starts at steal time; once it elapses, the steal is final and cannot be contested. This is time-based and not retryable, since the window only moves further into the past.
Source
Thrown at v3/@claude-flow/claims/src/application/work-stealing-service.ts:377
const claim = await this.repository.findByIssueId(issueId);
if (!claim) {
throw new Error(`Claim not found for issue: ${issueId}`);
}
// Check if there's a valid contest window
if (!claim.contestInfo) {
throw new Error('No steal to contest - issue was not recently stolen');
}
if (claim.contestInfo.resolution) {
throw new Error('Contest has already been resolved');
}
const nowMs = this.nowMs();
const windowEndsAtMs = new Date(claim.contestInfo.windowEndsAt).getTime();
if (nowMs > windowEndsAtMs) {
throw new Error('Contest window has expired');
}
// Verify the contester was the original owner
if (claim.contestInfo.contestedBy.id !== originalClaimant.id) {
throw new Error('Only the original claimant can contest the steal');
}
// Update contest info with reason
claim.contestInfo.reason = reason;
claim.contestInfo.contestedAt = new Date(nowMs);
await this.repository.update(claim);
// Emit contest event
await this.emitContestEvent(claim);
}
// ===========================================================================View on GitHub (pinned to 6b01dc5a68)
Solutions
- Increase config.contestWindowMinutes to fit your review latency.
- Process IssueStolen events with higher priority so contests are filed within the window.
- Pre-check the window before calling: compare nowMs() to contestInfo.windowEndsAt and skip if expired.
- If expired, accept the steal as final or escalate to a manual override (resolveContest by a coordinator, if still permitted).
Example fix
// before
await workStealing.contestSteal(issueId, originalClaimant, reason); // window passed -> throws
// after (config)
const workStealing = new WorkStealingService(repo, bus, { contestWindowMinutes: 120 });
// after (pre-check)
const claim = await repo.findByIssueId(issueId);
if (claim?.contestInfo && Date.now() <= claim.contestInfo.windowEndsAt.getTime()) {
await workStealing.contestSteal(issueId, originalClaimant, reason);
} Defensive patterns
Strategy: validation
Validate before calling
const claim = await repository.findByIssueId(issueId);
const windowEnd = claim?.contestInfo?.windowEndsAt;
if (!windowEnd || Date.now() > windowEnd.getTime()) {
throw new Error('Contest window expired; steal is final');
}
await workStealing.contestSteal(issueId, originalClaimant, reason); Try / catch
try {
await workStealing.contestSteal(issueId, originalClaimant, reason);
} catch (e) {
if (e instanceof Error && /window has expired/.test(e.message)) {
// not retryable: escalate or accept the steal as final
return escalateExpiredContest(issueId);
}
throw e;
} Prevention
- Set config.contestWindowMinutes large enough for your review latency.
- Pre-check nowMs() vs contestInfo.windowEndsAt before calling.
- Prioritize IssueStolen event processing so contests are filed in time.
When it happens
Trigger: Contesting long after the steal occurred; processing delay (queue backlog, slow human review) longer than the contest window; contestWindowMinutes misconfigured to a value too short for your workflow; clock skew between an HLC and wall clock.
Common situations: Default contest window too short for human-in-the-loop review; notifications queued behind a long backlog; HLC physical clock drifted relative to the wall clock used to compute windowEndsAt.
Related errors
- No steal to contest - issue was not recently stolen
- Contest has already been resolved
- Only the original claimant can contest the steal
- Claim not found for issue: ${issueId}
- Claim is still in grace period
AI-assisted analysis of ruvnet/ruflo@6b01dc5a68 (2026-08-12).
Data as JSON: /api/errors/d7e6bbc8a47bc74e.
Report an issue: GitHub.