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
- 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.
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
- 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.
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.
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- Timed out waiting for worktree port reservation lock at
- ACPX provider ownership admission is closed
- ACPX runtime host already has an active turn
- --agent-name cannot be empty.
- --api-key and --api-key-env are mutually exclusive.
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)