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
- Pass the exact array object returned by reserveReportNumbers: await releaseReportNumbers(reservedIds) — do not copy it first.
- 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.
- For administrative cleanup, pass { force: true }: await releaseReportNumbers([42], { force: true }).
- 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
- Pass the exact array object returned by reserveReportNumbers — never [...ids] or ids.map().
- For CLI/admin cleanup, always pass { force: true } explicitly.
- Store the reservation token out-of-band if you plan to release from a different code path.
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
- Could not claim ${count} report slot(s) after ${MAX_RETRIES}
- LOCK_TIMEOUT
- pipeline lock timeout: ${lockDir} held > ${timeoutMs}ms
- comeet: cannot derive API URL for ${entry.name} (set api: to
- csod: no anonymous token on ${cfg.homeUrl}
AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13).
Data as JSON: /api/errors/e02986e30f99db04.
Report an issue: GitHub.