santifer/career-ops · error · Error
Could not claim report slot(s) after retries
Error message
Could not claim ${count} report slot(s) after ${MAX_RETRIES} retries What it means
reserveReportNumbers retries up to MAX_RETRIES (50) times: each attempt claims `count` contiguous slots, and on collision releases partial claims, rescans occupied numbers, and retries from the next free base. If all 50 attempts fail, the reservation is abandoned and this generic Error is thrown — meaning another process or pre-existing files keep winning the slots. This is the contention-exhaustion path, reached only after the loop releases every partially claimed sentinel.
Solutions
- Reserve the whole range once before spawning N parallel workers: node reserve-report-num.mjs --count N, and hand each worker its assigned number (see AGENTS.md fan-out guidance).
- Run node reserve-report-num.mjs --gc to clear stale sentinels (older than 4h) that are shrinking the free range.
- Reduce concurrency: chunk the work so fewer processes reserve simultaneously, or add jitter/backoff between workers.
- Increase MAX_RETRIES pressure indirectly by reserving smaller counts per call; large contiguous blocks collide more often.
- Check for rogue processes writing reserved sentinels outside the API and stop them.
Example fix
// before (per-worker race)
workers.forEach(() => spawn('node evaluator.mjs')); // each calls reserveReportNumbers itself
// after
const [lo, hi] = await reserveReportNumbers(workers.length); // reserve up front
workers.forEach((_, i) => spawn('node evaluator.mjs', [String(lo + i)])); Defensive patterns
Strategy: retry
Validate before calling
// before fanning out, reserve the whole range up front (documented pattern #749)
const range = await reserveReportNumbers(workerCount);
console.log('reserved', range[0], '-', range[range.length - 1]); Try / catch
try {
ids = await reserveReportNumbers(count);
} catch (err) {
if (err.message.includes('Could not claim')) {
await new Promise(r => setTimeout(r, 1000)); // backoff
ids = await reserveReportNumbers(count); // one bounded retry after contention settles
} else throw err;
} Prevention
- Reserve a range once before spawning parallel workers; never let each worker compute its own number.
- Run `node reserve-report-num.mjs --gc` periodically to clear stale sentinels.
- Keep per-call counts small; huge contiguous blocks collide more under contention.
- Ensure all writers go through the tracker lock; rogue sentinel writers defeat the retry logic.
- Free reserved slots promptly with releaseReportNumbers after report creation or failure.
When it happens
Trigger: Massively concurrent fan-out where dozens of worker processes reserve simultaneously and each collision forces a rescan+retry, exhausting 50 attempts. A reports/ directory so densely pre-populated that contiguous runs of `count` free numbers keep getting taken. A racing process ignoring the tracker lock (e.g. writing NNN-RESERVED.md sentinels manually).
Common situations: Parallel batch evaluators spawned without first reserving a range via reserve-report-num.mjs (the documented anti-race pattern #749). Long CI pipelines where each worker calls reserveReportNumbers(1) in a hot loop instead of reserving a block once. Leftover sentinels from crashed runs shrinking the free range (run --gc).
Related errors
- merge-tracker concurrent write test flaked
- All active models failed. Last error
- Cannot verify tracker lock ownership at
- concurrent reservation test flaked
- ⚠️ Could not release report reservation
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/98c7873a9b8cc9fd.
Report an issue: GitHub.
Appendix: source
Thrown at reserve-report-num.mjs:220
} else {
failedAt = num;
break;
}
}
if (failedAt == null) {
Object.defineProperty(claimed, RESERVATION_TOKEN, { value: token });
return claimed;
}
for (const num of claimed) releaseSlot(reportsDir, num, { token });
occupied = collectOccupied(reportsDir, trackerPath);
base = Math.max(failedAt + 1, highestNumber(occupied) + 1);
}
} finally {
lock.release();
}
throw new Error(`Could not claim ${count} report slot(s) after ${MAX_RETRIES} retries`);
}
/**
* Release reservation sentinels after report creation or on failure.
* Only the array returned by reserveReportNumbers owns its sentinels. The CLI
* uses force mode as an explicit administrative cleanup path.
*/
export async function releaseReportNumbers(numbers, options = {}) {
const reportsDir = reportsDirFor(options);
const values = Array.isArray(numbers) ? numbers : [numbers];
for (const num of values) {
if (!Number.isSafeInteger(num) || num < 1) {
throw new TypeError(`Report number must be a positive integer, got ${num}`);
}
}
const force = options.force === true;
const token = options.reservationToken || numbers?.[RESERVATION_TOKEN];
if (!force && !token) throw new Error('Reservation ownership token is required for release');View on GitHub (pinned to aac998c7ed)