santifer/career-ops · error · SeedError

LOCK_TIMEOUT

LOCK_TIMEOUT

Error message

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

What it means

followup-seed.mjs writes to data/follow-ups.md under an exclusive filesystem lock (mirrors merge-tracker.mjs). LOCK_TIMEOUT fires when the process cannot acquire that lock within timeoutMs (default 60000ms): neither mkdirSync won the race nor could a stale lock be recovered (owner PID still alive, or ownerless dir younger than staleMs floored at OWNERLESS_GRACE_MS). The lock guards the read-check-append critical section so two writers cannot corrupt follow-ups.md.

Source

Thrown at followup-seed.mjs:366

        }
      }

      if (hasRecoverGuard) {
        try {
          if (lockCanRecover(lockDir, staleMs)) {
            rmSync(lockDir, { recursive: true, force: true });
            continue;
          }
        } finally {
          rmSync(recoverGuardDir, { recursive: true, force: true });
        }
      }

      await sleep(retryMs);
    }
  }

  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);
    renameSync(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 9b17a8ac97)

Solutions

  1. Inspect the lock directory printed in the error (lockDir) — check owner.json for the pid and followups path; if that PID is an unrelated or dead process, remove the lock dir with rm -rf.
  2. Ensure no other followup-seed / merge-tracker / set-status process is actively writing to the same follow-ups.md.
  3. Increase CAREER_OPS_FOLLOWUPS_LOCK_TIMEOUT_MS (default 60000) if contention is legitimate.
  4. If the owner PID is genuinely dead but the lock won't age out, lower CAREER_OPS_FOLLOWUPS_LOCK_STALE_MS (default 600000) so stale recovery triggers sooner — but never below OWNERLESS_GRACE_MS (1000ms).
Defensive patterns

Strategy: retry

Validate before calling

import { existsSync, readFileSync } from 'fs';
import { join } from 'path';

function isLockStale(lockDir, staleMs = 600000) {
  try {
    const owner = JSON.parse(readFileSync(join(lockDir, 'owner.json'), 'utf-8'));
    if (owner?.pid) {
      try { process.kill(owner.pid, 0); return false; }
      catch { return true; }
    }
  } catch {}
  return true;
}

// Before calling seedFollowup, check for a stuck lock
const lockDir = resolveLockDir(null, followupsPath);
if (existsSync(lockDir) && isLockStale(lockDir)) {
  console.warn(`Removing stale lock at ${lockDir}`);
  rmSync(lockDir, { recursive: true, force: true });
}

Try / catch

import { seedFollowup, SeedError } from './followup-seed.mjs';

try {
  await seedFollowup(appNum, { force });
} catch (err) {
  if (err instanceof SeedError && err.code === 'LOCK_TIMEOUT') {
    console.error(`Lock contention on follow-ups. Wait or remove: ${lockDirFromMessage}`);
    // Optionally retry once with a longer timeout:
    // await seedFollowup(appNum, { force, lockTimeoutMs: 120000 });
    process.exit(4);
  }
  throw err;
}

Prevention

When it happens

Trigger: Another followup-seed or merge-tracker process holds the lock for the entire 60s window; a prior process was kill -9'd leaving owner.json whose PID was later reused by an unrelated OS process (so PID-liveness says alive); CAREER_OPS_FOLLOWUPS_LOCK points at a lock dir whose .recover guard is itself wedged and below the staleMs age floor.

Common situations: Running followup-seed concurrently with merge-tracker or a second seedBackfill; a crashed CI runner left an orphaned lock in /tmp whose owner PID now belongs to a different program; an aggressively short CAREER_OPS_FOLLOWUPS_LOCK_TIMEOUT_MS override in CI combined with a legitimately busy lock.

Understand the failure class

Related errors


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