{"record":{"id":"1211e5a89a8e8459","repo":"santifer/career-ops","slug":"lock-timeout","errorCode":"LOCK_TIMEOUT","errorMessage":"Timed out waiting for follow-ups lock at ${lockDir}","messagePattern":"Timed out waiting for follow-ups lock at (.+?)","errorType":"exception","errorClass":"SeedError","httpStatus":null,"severity":"error","filePath":"followup-seed.mjs","lineNumber":366,"sourceCode":"        }\n      }\n\n      if (hasRecoverGuard) {\n        try {\n          if (lockCanRecover(lockDir, staleMs)) {\n            rmSync(lockDir, { recursive: true, force: true });\n            continue;\n          }\n        } finally {\n          rmSync(recoverGuardDir, { recursive: true, force: true });\n        }\n      }\n\n      await sleep(retryMs);\n    }\n  }\n\n  throw new SeedError('LOCK_TIMEOUT', `Timed out waiting for follow-ups lock at ${lockDir}`);\n}\n\n// --- Atomic write (mirrors writeFileAtomic in tracker.mjs / merge-tracker.mjs) --\n\nfunction writeFileAtomic(filePath, content) {\n  const tmpPath = join(dirname(filePath), `.${basename(filePath)}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`);\n  try {\n    writeFileSync(tmpPath, content);\n    renameSync(tmpPath, filePath);\n  } catch (err) {\n    rmSync(tmpPath, { force: true });\n    throw err;\n  }\n}\n\nfunction appendPins(existingContent, pinLines) {\n  const joined = pinLines.join('\\n');\n  if (existingContent == null) {","sourceCodeStart":348,"sourceCodeEnd":384,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/followup-seed.mjs#L348-L384","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["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.","Ensure no other followup-seed / merge-tracker / set-status process is actively writing to the same follow-ups.md.","Increase CAREER_OPS_FOLLOWUPS_LOCK_TIMEOUT_MS (default 60000) if contention is legitimate.","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)."],"exampleFix":null,"handlingStrategy":"retry","validationCode":"import { existsSync, readFileSync } from 'fs';\nimport { join } from 'path';\n\nfunction isLockStale(lockDir, staleMs = 600000) {\n  try {\n    const owner = JSON.parse(readFileSync(join(lockDir, 'owner.json'), 'utf-8'));\n    if (owner?.pid) {\n      try { process.kill(owner.pid, 0); return false; }\n      catch { return true; }\n    }\n  } catch {}\n  return true;\n}\n\n// Before calling seedFollowup, check for a stuck lock\nconst lockDir = resolveLockDir(null, followupsPath);\nif (existsSync(lockDir) && isLockStale(lockDir)) {\n  console.warn(`Removing stale lock at ${lockDir}`);\n  rmSync(lockDir, { recursive: true, force: true });\n}","typeGuard":null,"tryCatchPattern":"import { seedFollowup, SeedError } from './followup-seed.mjs';\n\ntry {\n  await seedFollowup(appNum, { force });\n} catch (err) {\n  if (err instanceof SeedError && err.code === 'LOCK_TIMEOUT') {\n    console.error(`Lock contention on follow-ups. Wait or remove: ${lockDirFromMessage}`);\n    // Optionally retry once with a longer timeout:\n    // await seedFollowup(appNum, { force, lockTimeoutMs: 120000 });\n    process.exit(4);\n  }\n  throw err;\n}","preventionTips":["Do not run followup-seed, merge-tracker, or set-status against the same tracker concurrently.","In CI, clean /tmp lock dirs (career-ops-followups-*) before each run.","If a process is killed, check for and remove its orphaned lock dir manually."],"tags":["locking","concurrency","filesystem","followups","timeout"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}