paperclipai/paperclip · error · Error
Timed out waiting for workspace restore lock at ${lockDir}
Error message
Timed out waiting for workspace restore lock at ${lockDir} What it means
Thrown by acquireDirectoryMergeLock when it cannot create the lock directory within the 30-second deadline and the existing lock's owner PID is still alive. The restore merge is serialized per target dir via a mkdir-based lock; a live owner means another restore is legitimately holding it, so this is a contention timeout, not a deadlock.
Source
Thrown at packages/adapter-utils/src/workspace-restore-merge.ts:154
path.join(lockDir, "owner.json"),
`${JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() })}\n`,
"utf8",
);
return async () => {
await fs.rm(lockDir, { recursive: true, force: true }).catch(() => undefined);
};
} catch (error) {
const code = error && typeof error === "object" ? (error as { code?: unknown }).code : null;
if (code !== "EEXIST") throw error;
// Stale-lock detection: if the owner PID is dead (SIGKILL / OOM / crash),
// the lockDir would otherwise persist forever and stall restores. Mirror
// the materializePaperclipSkillCopy lock pattern — remove and retry.
if (!(await isHolderAlive(lockDir))) {
await fs.rm(lockDir, { recursive: true, force: true }).catch(() => undefined);
continue;
}
if (Date.now() >= deadline) {
throw new Error(`Timed out waiting for workspace restore lock at ${lockDir}`);
}
await new Promise((resolve) => setTimeout(resolve, 50));
}
}
}
export async function withDirectoryMergeLock<T>(
targetDir: string,
fn: () => Promise<T>,
): Promise<T> {
const releaseLock = await acquireDirectoryMergeLock(`${targetDir}.paperclip-restore.lock`);
try {
return await fn();
} finally {
await releaseLock();
}
}
View on GitHub (pinned to 67001ec6eb)
Solutions
- Serialize restores per target dir so only one contends the lock at a time.
- If the holder is a legitimately slow restore, wait and retry once it completes (it will release the lock in its finally block).
- If no restore should be running, check for an orphaned lock whose owner.json pid is stale and remove <targetDir>.paperclip-restore.lock manually.
- For large workspaces, investigate why the in-flight merge exceeds 30s (disk throughput, huge file count) and optimize.
Defensive patterns
Strategy: retry
Try / catch
try {
await withDirectoryMergeLock(targetDir, async () => { /* merge */ });
} catch (err) {
if (err.message.startsWith("Timed out waiting for workspace restore lock")) {
// another restore holds the lock; wait for it to finish, then retry once
}
throw err;
} Prevention
- Serialize restores per target directory at the orchestrator level.
- Investigate restores that run longer than 30s — large file counts, slow disks.
- Confirm the lock dir's owner.json pid before assuming a deadlock; never blindly delete a live-owner lock.
When it happens
Trigger: withDirectoryMergeLock / acquireDirectoryMergeLock for targetDir: another process holds <targetDir>.paperclip-restore.lock (owner.json pid responds to kill(pid,0)) for the full 30s window. Happens when two restores target the same workspace dir or one restore takes longer than 30s.
Common situations: Concurrent restore triggers against the same workspace; a slow/large directory merge holding the lock past 30s; a restore running under heavy I/O; orchestrator double-triggering a restore.
Understand the failure class
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- Timed out waiting for Paperclip skill materialization lock a
- Failed to integrate concurrent SSH git history for ${input.i
- Another restart for instance ${instanceId} is still running.
- Worktree seed lock ${lockPath} belongs to exited process ${c
- Worktree seed lock ${lockPath} is stale or malformed. Verify
AI-assisted analysis of paperclipai/paperclip@67001ec6eb (2026-08-12).
Data as JSON: /api/errors/c4f9db16e87f5df9.
Report an issue: GitHub.