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

  1. Re-open the session with `openSession(url)` to get a fresh `id`, then call `fillSession` promptly with the new id.
  2. Treat the error as recoverable in the UI: show 'session expired, click to reopen' and re-run open+fill in sequence.
  3. Reduce the gap between open and fill — drive them as one user action so the idle pruner cannot fire in between.
  4. If sessions expire too aggressively, review the `prune()`/`scheduleIdleClose()` thresholds and raise them to match your UX.
  5. 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

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.