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

  1. 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).
  2. Run node reserve-report-num.mjs --gc to clear stale sentinels (older than 4h) that are shrinking the free range.
  3. Reduce concurrency: chunk the work so fewer processes reserve simultaneously, or add jitter/backoff between workers.
  4. Increase MAX_RETRIES pressure indirectly by reserving smaller counts per call; large contiguous blocks collide more often.
  5. 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

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


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)