{"record":{"id":"cf1a790bcc2282aa","repo":"paperclipai/paperclip","slug":"timed-out-waiting-for-workspace-build-lock-at-lockdir","errorCode":null,"errorMessage":"Timed out waiting for workspace build lock at ${lockDir}. Another build may still be running.","messagePattern":"Timed out waiting for workspace build lock at (.+?)\\. Another build may still be running\\.","errorType":"console","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"scripts/ensure-plugin-build-deps.mjs","lineNumber":182,"sourceCode":"  try {\n    while (!stoppingSignal) {\n      // Do not replace a fresh empty lock held by an older script.\n      recoverAbandonedLock();\n      if (!fs.existsSync(lockDir)) {\n        try {\n          fs.renameSync(candidate, lockDir);\n          holdsLock = true;\n          return;\n        } catch (error) {\n          if (![\"ENOTEMPTY\", \"EEXIST\", \"EPERM\"].includes(error.code)) throw error;\n        }\n      }\n      if (!reportedWait) {\n        console.log(`[paperclip] Waiting for another workspace build (${lockDir})...`);\n        reportedWait = true;\n      }\n      if (Date.now() - startedAt >= lockTimeoutMs) {\n        throw new Error(`Timed out waiting for workspace build lock at ${lockDir}. Another build may still be running.`);\n      }\n      await sleep(lockPollMs);\n    }\n  } finally {\n    fs.rmSync(candidate, { recursive: true, force: true });\n  }\n}\n\nasync function build(target) {\n  console.log(`[paperclip] Building ${target.name}...`);\n  // A hard kill bypasses cleanup. Only a completed compile may restore this\n  // marker, so recovery never trusts index.js emitted partway through a build.\n  fs.rmSync(target.completion, { force: true });\n  const sources = sourceFingerprint(target);\n  const code = await new Promise((resolve, reject) => {\n    child = spawn(process.execPath, [tscCliPath, \"-p\", target.tsconfig], {\n      cwd: rootDir,\n      stdio: \"inherit\",","sourceCodeStart":164,"sourceCodeEnd":200,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/scripts/ensure-plugin-build-deps.mjs#L164-L200","documentation":"The script serializes workspace builds with a filesystem lock directory under node_modules/.cache. acquireLock polls for the lock and, if it cannot acquire it within lockTimeoutMs (60s), throws this error because another build process is presumed to still be running (or a stale lock was left behind).","triggerScenarios":"Two ensure-plugin-build-deps invocations overlap (parallel plugin installs, concurrent builds in CI matrix jobs), or a previous run crashed after creating the lock directory but before its finally-cleanup, leaving a stale lock that never expires.","commonSituations":"CI running build and dev server concurrently; a killed (SIGKILL) prior build leaving node_modules/.cache/paperclip-plugin-build-deps.lock behind; a genuinely hung build holding the lock longer than 60 seconds.","solutions":["Wait for the other build to finish and rerun; the winner removes its lock in its finally block.","If no build is running, delete the stale lock: `rm -rf node_modules/.cache/paperclip-plugin-build-deps.lock` and rerun.","Serialize the callers: run plugin builds sequentially or with a job-level mutex in CI.","If builds legitimately exceed 60s, raise lockTimeoutMs in scripts/ensure-plugin-build-deps.mjs:14."],"exampleFix":"// before\nrm -rf node_modules/.cache/paperclip-plugin-build-deps.lock   # stale lock from killed build\nnode scripts/ensure-plugin-build-deps.mjs  # now acquires lock\n// after\ncleanup done; script proceeds normally","handlingStrategy":"retry","validationCode":"const lockDir = \"node_modules/.cache/paperclip-plugin-build-deps.lock\";\nif (fs.existsSync(lockDir)) console.warn(\"Stale build lock present; remove it if no build is running:\", lockDir);","typeGuard":null,"tryCatchPattern":"try {\n  await ensurePluginBuildDeps();\n} catch (e) {\n  if (String(e.message).includes(\"Timed out waiting for workspace build lock\")) {\n    fs.rmSync(\"node_modules/.cache/paperclip-plugin-build-deps.lock\", { recursive: true, force: true });\n    await ensurePluginBuildDeps(); // retry once after clearing stale lock\n  } else throw e;\n}","preventionTips":["Serialize build steps that invoke this script; never run it concurrently in the same workspace.","Use a per-job workspace copy in CI matrix runs instead of a shared checkout.","Clean node_modules/.cache on CI start to clear locks from killed runs.","If builds can legitimately exceed 60s, raise lockTimeoutMs."],"tags":["build","concurrency","lock","timeout"],"backgroundTag":"request-timeout","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}