paperclipai/paperclip · error · Error

Timed out waiting for workspace build lock at

Error message

Timed out waiting for workspace build lock at ${lockDir}. Another build may still be running.

What it means

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).

Solutions

  1. Wait for the other build to finish and rerun; the winner removes its lock in its finally block.
  2. If no build is running, delete the stale lock: `rm -rf node_modules/.cache/paperclip-plugin-build-deps.lock` and rerun.
  3. Serialize the callers: run plugin builds sequentially or with a job-level mutex in CI.
  4. If builds legitimately exceed 60s, raise lockTimeoutMs in scripts/ensure-plugin-build-deps.mjs:14.

Example fix

// before
rm -rf node_modules/.cache/paperclip-plugin-build-deps.lock   # stale lock from killed build
node scripts/ensure-plugin-build-deps.mjs  # now acquires lock
// after
cleanup done; script proceeds normally
Defensive patterns

Strategy: retry

Validate before calling

const lockDir = "node_modules/.cache/paperclip-plugin-build-deps.lock";
if (fs.existsSync(lockDir)) console.warn("Stale build lock present; remove it if no build is running:", lockDir);

Try / catch

try {
  await ensurePluginBuildDeps();
} catch (e) {
  if (String(e.message).includes("Timed out waiting for workspace build lock")) {
    fs.rmSync("node_modules/.cache/paperclip-plugin-build-deps.lock", { recursive: true, force: true });
    await ensurePluginBuildDeps(); // retry once after clearing stale lock
  } else throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

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 paperclipai/paperclip@3f1d897a7c (2026-09-18). Data as JSON: /api/errors/cf1a790bcc2282aa. Report an issue: GitHub.

Appendix: source

Thrown at scripts/ensure-plugin-build-deps.mjs:182

  try {
    while (!stoppingSignal) {
      // Do not replace a fresh empty lock held by an older script.
      recoverAbandonedLock();
      if (!fs.existsSync(lockDir)) {
        try {
          fs.renameSync(candidate, lockDir);
          holdsLock = true;
          return;
        } catch (error) {
          if (!["ENOTEMPTY", "EEXIST", "EPERM"].includes(error.code)) throw error;
        }
      }
      if (!reportedWait) {
        console.log(`[paperclip] Waiting for another workspace build (${lockDir})...`);
        reportedWait = true;
      }
      if (Date.now() - startedAt >= lockTimeoutMs) {
        throw new Error(`Timed out waiting for workspace build lock at ${lockDir}. Another build may still be running.`);
      }
      await sleep(lockPollMs);
    }
  } finally {
    fs.rmSync(candidate, { recursive: true, force: true });
  }
}

async function build(target) {
  console.log(`[paperclip] Building ${target.name}...`);
  // A hard kill bypasses cleanup. Only a completed compile may restore this
  // marker, so recovery never trusts index.js emitted partway through a build.
  fs.rmSync(target.completion, { force: true });
  const sources = sourceFingerprint(target);
  const code = await new Promise((resolve, reject) => {
    child = spawn(process.execPath, [tscCliPath, "-p", target.tsconfig], {
      cwd: rootDir,
      stdio: "inherit",

View on GitHub (pinned to 3f1d897a7c)