santifer/career-ops · error · Error

Reservation ownership token is required for release

Error message

Reservation ownership token is required for release

What it means

releaseReportNumbers refuses to release sentinels unless the caller proves ownership via a reservation token or explicitly opts into administrative force mode. The token is attached as a non-enumerable Symbol property (RESERVATION_TOKEN) on the array returned by reserveReportNumbers, so passing that exact array back satisfies the check. Throwing here prevents one caller from clobbering another's reservation by guessing report numbers.

Source

Thrown at reserve-report-num.mjs:224

  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');
  if (!existsSync(reportsDir)) return 0;

  const trackerPath = trackerPathFor(options);
  const lock = await acquireTrackerLock(trackerLockDirFor(trackerPath), {
    timeoutMs: Number(process.env.CAREER_OPS_TRACKER_LOCK_TIMEOUT_MS) || 60_000,
    retryMs: Number(process.env.CAREER_OPS_TRACKER_LOCK_RETRY_MS) || 75,
    staleMs: Number(process.env.CAREER_OPS_TRACKER_LOCK_STALE_MS) || 10 * 60_000,
    tracker: trackerPath,
    ...options.lockOptions,
  });
  try {
    return values.reduce(
      (removed, num) => removed + Number(releaseSlot(reportsDir, num, { token, force })),
      0,
    );
  } finally {
    lock.release();
  }

View on GitHub (pinned to 9b17a8ac97)

Solutions

  1. Pass the exact array object returned by reserveReportNumbers: await releaseReportNumbers(reservedIds) — do not copy it first.
  2. If you must reconstruct, capture the token explicitly: const token = reservedIds[Symbol.for('career-ops-report-reservation-token')] — but note the library uses a private Symbol, so prefer the options.reservationToken path.
  3. For administrative cleanup, pass { force: true }: await releaseReportNumbers([42], { force: true }).
  4. Store and forward the token via options.reservationToken if you obtained it out-of-band.

Example fix

// before: copying the array drops the hidden token
const copy = [...reservedIds];
await releaseReportNumbers(copy); // throws

// after: pass the original array or use force
await releaseReportNumbers(reservedIds);
// or for admin cleanup:
await releaseReportNumbers([42], { force: true });
Defensive patterns

Strategy: type-guard

Validate before calling

function canReleaseSafely(numbers, options = {}) {
  if (options.force === true) return true;
  const token = options.reservationToken || numbers?.[Symbol.for('career-ops-report-reservation-token')];
  return Boolean(token);
}
if (!canReleaseSafely(ids)) { /* need force or token */ }

Type guard

function hasReservationToken(numbers) {
  const sym = Object.getOwnPropertySymbols(numbers).find(
    s => String(s) === 'Symbol(career-ops-report-reservation-token)'
  );
  return Boolean(sym && numbers[sym]);
}

Try / catch

try {
  await releaseReportNumbers(ids);
} catch (err) {
  if (err.message.includes('ownership token')) {
    // administrative fallback
    await releaseReportNumbers(ids, { force: true });
  } else {
    throw err;
  }
}

Prevention

When it happens

Trigger: Calling releaseReportNumbers([42]) without options.reservationToken and without options.force === true; passing a plain array that was not the return value of reserveReportNumbers (so it lacks the Symbol); reconstructing the numbers array via spread or .map() which drops the non-enumerable Symbol property.

Common situations: Copying the reserved array with [...ids] or ids.map(x => x) strips the hidden Symbol token; serializing/deserializing the IDs through JSON loses the token; a cleanup script passing bare numbers from CLI args.

Related errors


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