affaan-m/ECC · error
Issue # is already claimed by
Error message
Issue #${issue.number} is already claimed by ${state.owner || 'unknown'} What it means
assertIssueClaimable throws this when the coordination state for an issue has status 'claimed', meaning another agent/process already owns the GitHub issue. The claim-lock mechanism prevents duplicate concurrent work on the same issue; applyClaim calls this check before assigning a new owner. The existing owner is embedded in the message (or 'unknown' if state.owner is empty).
Solutions
- Pick a different, unclaimed issue from the queue instead of contending for this one.
- Contact the listed owner to confirm the claim is still active; if abandoned, have the owner release the claim (reset status in the coordination state) and retry.
- Inspect scripts/lib/github-coordination state file for the issue to verify the owner and timestamp; clear stale claims only after confirming the owner is inactive.
- Add a retry loop that re-reads state before claiming to reduce races between simultaneous claim attempts.
Example fix
// before: blindly claim
await applyClaim(repo, issueNumber, myOwner);
// after: check first
const state = readIssueState(repo, issueNumber);
if (state.status === 'claimed') {
console.log(`Skipping #${issueNumber}: owned by ${state.owner}`);
} else {
await applyClaim(repo, issueNumber, myOwner);
} Defensive patterns
Strategy: try-catch
Validate before calling
const state = readIssueState(repo, issueNumber);
if (state && state.status === 'claimed') {
throw new SkipError(`#${issueNumber} claimed by ${state.owner}`);
} Type guard
function isClaimable(state) {
return state != null && typeof state === 'object' && state.status !== 'claimed';
} Try / catch
try {
await applyClaim(repo, issueNumber, owner);
} catch (e) {
if (/is already claimed by/.test(e.message)) {
console.warn(`Skipping: ${e.message}`);
return;
}
throw e;
} Prevention
- Re-read claim state immediately before claiming to narrow race windows.
- Treat 'claimed' as terminal unless the owner explicitly releases it.
- Monitor for stale claims (owner inactive beyond a timeout) with a periodic sweeper.
When it happens
Trigger: Calling applyClaim(repo, issueNumber, owner) while state.js on disk already records { status: 'claimed', owner: <someone> } for that issue; two agents attempting to claim the same issue simultaneously; a stale claim from a previous session that was never released.
Common situations: Parallel CI jobs or multiple agents competing for issues from the same queue; a crashed session left the issue marked claimed; a developer locally re-runs a claim script after another teammate already claimed the issue.
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
- announcement has a pending receipt; inspect Discord before…
- Another ECC process is updating Claude settings
- Another Nasiko lifecycle operation is already in progress…
- Another Nasiko lifecycle operation won lock acquisition
- Another Nasiko lifecycle operation won stale-lock recovery
AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16).
Data as JSON: /api/errors/d8ca6802d84bcc13.
Report an issue: GitHub.
Appendix: source
Thrown at scripts/lib/github-coordination/state.js:215
validation: state.validation || 'pending',
review: state.review || 'not-requested',
project: summarizeProjectProjection(state, policy),
dependencies: Array.isArray(state.dependencies) ? state.dependencies : [],
tasks: Array.isArray(state.tasks) ? state.tasks : [],
labels: normalizeLabels(issue.labels),
workItemId: `github-${slugifySegment(repo)}-epic-${issue.number}`,
lastActionAt: state.lastActionAt || null,
lastSyncAt: state.lastSyncAt || null,
};
}
function assertIssueClaimable(issue, state) {
if (String(issue.state || '').toLowerCase() !== 'open') {
throw new Error(`Issue #${issue.number} is not open`);
}
if (state.status === 'claimed') {
throw new Error(`Issue #${issue.number} is already claimed by ${state.owner || 'unknown'}`);
}
}
function verifyDependenciesClosed(repo, dependencyNumbers, options = {}, allIssues = null) {
if (!Array.isArray(dependencyNumbers) || dependencyNumbers.length === 0) {
return [];
}
const issueList = allIssues || listIssues(repo, { ...options, state: 'all', limit: options.limit || 200 });
const closed = [];
for (const dependencyNumber of dependencyNumbers) {
const issue = findIssueByNumber(issueList, dependencyNumber);
if (!issue) {
process.stderr.write(`[github-coordination] Warning: dependency issue #${dependencyNumber} not found in issue list (may be in a different repo or beyond limit)\n`);
} else if (String(issue.state || '').toLowerCase() === 'closed') {
closed.push(dependencyNumber);
}
}View on GitHub (pinned to 8321021c54)