santifer/career-ops · error · SeedError

LOCK_TIMEOUT

LOCK_TIMEOUT

Error message

Timed out waiting for follow-ups lock at ${lockDir}

What it means

acquireFollowupsLock uses a lock directory with retry/backoff to serialize concurrent mutations of data/follow-ups.md. When it cannot acquire the lock within the timeout window, it throws a SeedError with code LOCK_TIMEOUT. This protects the follow-ups file from concurrent-corruption during writes by seedFollowup or other tooling.

Solutions

  1. Wait briefly and re-run the command — the competing process may finish and release the lock.
  2. Check whether another career-ops process (seed, merge, tracker write) is still running and let it finish or stop it.
  3. If no other process is running, remove the stale lock directory referenced in the message and re-run.
  4. Avoid parallel seedFollowup invocations; run seeding sequentially (locks exist precisely because concurrent writes are unsafe).

Example fix

// before: parallel fan-out causes lock contention
await Promise.all(rows.map(r => seedFollowup(r.num)));
// after: sequential seeding avoids timeout
for (const r of rows) {
  await seedFollowup(r.num);
}
Defensive patterns

Strategy: retry

Validate before calling

import { existsSync } from 'node:fs';
const lockDir = join(process.cwd(), 'data', '.followups.lock');
if (existsSync(lockDir)) {
  const ageMs = Date.now() - (await stat(lockDir)).mtimeMs;
  if (ageMs < 60_000) await new Promise(r => setTimeout(r, Math.min(60_000 - ageMs, 5000)));
}

Try / catch

try {
  await seedFollowup(appNum);
} catch (e) {
  if (e.code === 'LOCK_TIMEOUT') {
    console.error('Follow-ups lock busy; retrying once after a delay...');
    await new Promise(r => setTimeout(r, 5000));
    await seedFollowup(appNum);
  } else throw e;
}

Prevention

When it happens

Trigger: Another process holds the follow-ups lock past the timeout budget; a crashed run left a stale lock directory behind; many seedFollowup invocations run in parallel and each retries until the overall deadline expires.

Common situations: Batch mode fanning out several seedFollowup calls concurrently; a prior run was killed (Ctrl-C or OOM) without releasing the lock, leaving a stale lockdir; an extremely slow filesystem making lock create/check exceed backoff windows.

Understand the failure class

Background: Request timed out: what client-side request timeouts mean across libraries (Request timed out, TIMED_OUT, APITimeoutError) — this error's family across 39 libraries.

Related errors


AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16). Data as JSON: /api/errors/1211e5a89a8e8459. Report an issue: GitHub.

Appendix: source

Thrown at followup-seed.mjs:436

          // STALE only. VANISHED means the lock was absent when we looked, and
          // by the time this line runs another acquirer may have won the mkdir
          // and be partway through writing owner.json — deleting on that answer
          // destroys a live lock and kills its winner with ENOENT.
          if (lockRecoveryVerdict(lockDir, staleMs) === RECOVER_STALE) {
            if (rmLockArtifactSync(lockDir)) continue;
            // rm hit contention: another process is touching the stale lock at
            // this instant — back off instead of treating the collision as fatal.
          }
        } finally {
          rmLockArtifactSync(recoverGuardDir);
        }
      }

      await sleep(backoffMs());
    }
  }

  throw new SeedError('LOCK_TIMEOUT', `Timed out waiting for follow-ups lock at ${lockDir}`);
}

// --- Atomic write (mirrors writeFileAtomic in tracker.mjs / merge-tracker.mjs) --

function writeFileAtomic(filePath, content) {
  const tmpPath = join(dirname(filePath), `.${basename(filePath)}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`);
  try {
    writeFileSync(tmpPath, content);
    renameSyncWithRetry(tmpPath, filePath);
  } catch (err) {
    rmSync(tmpPath, { force: true });
    throw err;
  }
}

function appendPins(existingContent, pinLines) {
  const joined = pinLines.join('\n');
  if (existingContent == null) {

View on GitHub (pinned to aac998c7ed)