santifer/career-ops · error · Error
apply session not found (it may have expired)
Error message
apply session not found (it may have expired)
What it means
Thrown by `fillSession` when the in-memory session record for the given `id` is not in the `SESSIONS` map. Sessions live in process memory (a `Map`) and are pruned by an idle/age policy, so a session that existed at open time can vanish before the user calls fill. The message explicitly hints expiry as the likely cause.
Source
Thrown at web/src/lib/apply/session.ts:368
export type FillStep = { fieldId: string; label: string; ok: boolean; thumb?: string };
/** True for a file field that wants the candidate's résumé/CV (vs. cover letter,
* portfolio, or a generic attachment we leave for the user). */
function isResumeField(f: ApplyField): boolean {
return f.type === "file" && /resume|résumé|\bcv\b|curriculum|lebenslauf|currículum/i.test(f.label || "");
}
/** Fill the real form with verified answers, screenshotting after each field.
* Attaches the tailored CV PDF to résumé/CV file fields (cvPath). NEVER clicks a
* submit/apply control — only fills/selects/checks/attaches. */
export async function fillSession(
id: string,
answers: Record<string, string>,
fieldsMeta: ApplyField[],
cvPath?: string,
): Promise<{ steps: FillStep[]; navigated: boolean; issues: ApplyIssue[] }> {
const s = SESSIONS.get(id);
if (!s) throw new Error("apply session not found (it may have expired)");
const byId = new Map(fieldsMeta.map((f) => [f.id, f]));
const steps: FillStep[] = [];
// Belt-and-suspenders: if filling ever navigates the page (i.e. something got
// submitted), the URL path changes. We never submit by construction, but we
// report it so the caller can flag it instead of silently "succeeding".
const startPath = (() => {
try {
return new URL(s.frame.url()).pathname;
} catch {
return s.frame.url();
}
})();
const shoot = async () => {
try {
const buf = await s.page.screenshot({ type: "jpeg", quality: 38 });
return `data:image/jpeg;base64,${buf.toString("base64")}`;
} catch {View on GitHub (pinned to 9b17a8ac97)
Solutions
- Re-open the session with `openSession(url)` to get a fresh `id`, then call `fillSession` promptly with the new id.
- Treat the error as recoverable in the UI: show 'session expired, click to reopen' and re-run open+fill in sequence.
- Reduce the gap between open and fill — drive them as one user action so the idle pruner cannot fire in between.
- If sessions expire too aggressively, review the `prune()`/`scheduleIdleClose()` thresholds and raise them to match your UX.
- Persist the answers client-side so a re-open doesn't lose the user's prepared answers.
Example fix
// before — stale id reused across a long pause
const { id } = await openSession(url);
// ...minutes later...
await fillSession(id, answers, fields); // throws
// after — reopen on expiry
try { await fillSession(id, answers, fields); }
catch (e) {
if (/expired/.test(e.message)) {
const fresh = await openSession(url);
await fillSession(fresh.id, answers, fields);
} else throw e;
} Defensive patterns
Strategy: retry
Validate before calling
// Track liveness client-side so you don't call fill on a dead id. const open = await openSession(url); const openedAt = Date.now(); // If user idle > (prune window), reopen before fill: const id = (Date.now() - openedAt > SESSION_TTL_MS) ? (await openSession(url)).id : open.id;
Try / catch
async function fillOrReopen(url, answers, fields) {
let id = currentSessionId;
try {
return await fillSession(id, answers, fields);
} catch (e) {
if (/expired|not found/i.test(e.message)) {
const fresh = await openSession(url);
return await fillSession(fresh.id, answers, fields);
}
throw e;
}
} Prevention
- Drive open → fill as one user action with no long pause.
- Persist answers client-side so reopening doesn't lose data.
- Show 'session expired — reopening' in the UI instead of surfacing the raw error.
- Keep `SESSION_TTL_MS` in sync with the prune threshold in session.ts.
When it happens
Trigger: Calling `fillSession(id, ...)` with an `id` that was never opened, was already pruned by `prune()` (idle expiry), or whose process restarted (in-memory map wiped). Also if the caller mis-keys the id (typo, stale UI state, double-submit after close).
Common situations: Long pause between `openSession` and `fillSession` (user stepped away, idle timer fired); dev server hot-reload restarting the process; multiple tabs / race where one tab closed the session; copy-pasting an old session id from logs; calling fill after `handoffSession` already ended the session.
Related errors
AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13).
Data as JSON: /api/errors/16a2fbacc6b7c8a1.
Report an issue: GitHub.