santifer/career-ops · error · Error
Could not claim ${count} report slot(s) after ${MAX_RETRIES}
Error message
Could not claim ${count} report slot(s) after ${MAX_RETRIES} retries What it means
After acquiring the tracker lock, reserveReportNumbers retries up to MAX_RETRIES (50) times to claim a contiguous run of `count` slots. If every retry fails — because sentinels keep appearing in the target range from concurrent or stale reservations — it throws a generic Error. Each retry re-reads occupied slots and advances base past the failure point, so this indicates sustained contention or corrupted sentinels that never clear.
Source
Thrown at reserve-report-num.mjs:206
} 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 9b17a8ac97)
Solutions
- Reduce concurrency: reserve report numbers centrally once (node reserve-report-num.mjs --count N) and hand each worker a pre-assigned ID rather than having each worker call reserveReportNumbers itself.
- Clean stale sentinels: node reserve-report-num.mjs --release <range> --force, or raise CAREER_OPS_TRACKER_LOCK_STALE_MS if processes are legitimately long-lived.
- Inspect reports/*-RESERVED.md files: check the pid field with processIsAlive; remove orphans.
- Retry the operation after a short delay — transient heavy contention may clear.
Example fix
// before: each worker reserves its own number (contention)
const ids = await reserveReportNumbers(1);
// after: reserve a shared range up front, distribute
const { execSync } = require('child_process');
const range = execSync('node reserve-report-num.mjs --count 10').toString().trim(); // e.g. '042-051'
const [start, end] = range.split('-').map(Number);
// hand each worker its own id from start..end Defensive patterns
Strategy: retry
Try / catch
let ids;
for (let attempt = 0; attempt < 3; attempt++) {
try {
ids = await reserveReportNumbers(count);
break;
} catch (err) {
if (err.message.includes('Could not claim') && attempt < 2) {
await new Promise(r => setTimeout(r, 500 * (attempt + 1)));
continue;
}
throw err;
}
} Prevention
- Pre-reserve a shared range centrally and distribute IDs to workers instead of having each worker reserve.
- Keep concurrency below the level that saturates the lock; clean stale sentinels between batch runs.
- Monitor reports/*-RESERVED.md for orphaned sentinels whose pid is no longer alive.
When it happens
Trigger: Many parallel workers (more than the lock allows through quickly) all competing for the next contiguous range; stale RESERVED sentinel files whose owning process died but whose staleMs window (10 min default) has not elapsed; a corrupted sentinel that readSentinelOwner cannot parse and that blocks the same slot every iteration.
Common situations: Running a large batch fan-out (>50 concurrent evaluators) without pre-reserving a shared range; sentinel files left behind by a killed process; clock skew causing stale-lock detection to fail; disk-full or permission issues causing wx flag failures.
Related errors
- LOCK_TIMEOUT
- pipeline lock timeout: ${lockDir} held > ${timeoutMs}ms
- Reservation ownership token is required for release
- Cannot verify tracker lock ownership at ${lockDir}
- portal-health lock timeout: ${lockDir} held > ${timeoutMs}ms
AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13).
Data as JSON: /api/errors/98c7873a9b8cc9fd.
Report an issue: GitHub.