ruvnet/ruflo · error · Error
No steal to contest - issue was not recently stolen
Error message
No steal to contest - issue was not recently stolen
What it means
Plain Error thrown by contestSteal when claim.contestInfo is undefined. contestInfo is only populated by the steal() operation; if it is absent, the issue was never stolen (it may have been handed off normally, or is still owned by its original claimant), so there is no contest window to open.
Source
Thrown at v3/@claude-flow/claims/src/application/work-stealing-service.ts:367
}
// ===========================================================================
// Contest Steal
// ===========================================================================
/**
* Contest a steal (original owner wants the work back)
*/
async contestSteal(issueId: IssueId, originalClaimant: Claimant, reason: string): Promise<void> {
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 reasonView on GitHub (pinned to 6b01dc5a68)
Solutions
- Inspect the claim (findByIssueId/getStealable) and confirm contestInfo is present before contesting.
- If the transfer was a handoff, use the handoff accept/reject APIs instead of the contest API.
- Drop stale or misrouted notifications rather than contesting a non-stolen claim.
- Disambiguate IssueStolen events from HandoffRequested events in your event handler.
Example fix
// before
await workStealing.contestSteal(issueId, originalClaimant, reason); // not stolen -> throws
// after
const claim = await repo.findByIssueId(issueId);
if (claim?.contestInfo && !claim.contestInfo.resolution) {
await workStealing.contestSteal(issueId, originalClaimant, reason);
} Defensive patterns
Strategy: validation
Validate before calling
const claim = await repository.findByIssueId(issueId);
if (!claim?.contestInfo) {
throw new Error('Cannot contest: issue was not stolen');
}
await workStealing.contestSteal(issueId, originalClaimant, reason); Type guard
function hasOpenContest(claim: IssueClaimWithStealing | null): claim is IssueClaimWithStealing {
return !!claim && !!claim.contestInfo && !claim.contestInfo.resolution;
} Try / catch
try {
await workStealing.contestSteal(issueId, originalClaimant, reason);
} catch (e) {
if (e instanceof Error && /not recently stolen/.test(e.message)) {
// route to handoff accept/reject instead, or drop
return;
}
throw e;
} Prevention
- Only invoke contestSteal in response to a verified IssueStolen event.
- Route HandoffRequested events to the handoff APIs, not the contest API.
- Inspect contestInfo at the boundary before attempting a contest.
When it happens
Trigger: Calling contestSteal after a normal handoff rather than a steal; on an issue that was never stolen; after contestInfo was cleared following resolution; confusing a HandoffRequested event with an IssueStolen event.
Common situations: Event routing bug delivering handoff notifications to the steal-contest handler; replay of an old notification; caller assumes any ownership change is a steal.
Related errors
- Contest has already been resolved
- Contest window has expired
- 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/fdaa5992936f189b.
Report an issue: GitHub.