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

  1. 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.
  2. 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.
  3. Inspect reports/*-RESERVED.md files: check the pid field with processIsAlive; remove orphans.
  4. 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

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


AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13). Data as JSON: /api/errors/98c7873a9b8cc9fd. Report an issue: GitHub.