paperclipai/paperclip · error · Error
Timed out waiting for Paperclip skill materialization lock a
Error message
Timed out waiting for Paperclip skill materialization lock at ${lockDir} What it means
Thrown by acquireMaterializeLock when the skill-materialization lock directory could not be acquired within MATERIALIZED_SKILL_LOCK_STALE_MS. The lock is an mkdir-based exclusive lock; on EEXIST it tries to remove a stale lock (owner pid dead) and retries every 50ms until a deadline. If the deadline passes with the lock still held by a live owner, this error fires.
Source
Thrown at packages/adapter-utils/src/server-utils.ts:3012
await fs.mkdir(path.dirname(lockDir), { recursive: true });
const deadline = Date.now() + MATERIALIZED_SKILL_LOCK_STALE_MS;
while (true) {
try {
await fs.mkdir(lockDir);
await fs.writeFile(
path.join(lockDir, MATERIALIZED_SKILL_LOCK_OWNER),
`${JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() })}\n`,
"utf8",
);
return async () => {
await fs.rm(lockDir, { recursive: true, force: true });
};
} catch (err) {
const code = err && typeof err === "object" ? (err as { code?: unknown }).code : null;
if (code !== "EEXIST") throw err;
if (await removeStaleMaterializeLock(lockDir, MATERIALIZED_SKILL_LOCK_STALE_MS)) continue;
if (Date.now() >= deadline) {
throw new Error(`Timed out waiting for Paperclip skill materialization lock at ${lockDir}`);
}
await new Promise((resolve) => setTimeout(resolve, 50));
}
}
}
function isPidAlive(pid: number): boolean {
if (!Number.isInteger(pid) || pid <= 0) return false;
try {
process.kill(pid, 0);
return true;
} catch (err) {
const code = err && typeof err === "object" ? (err as { code?: unknown }).code : null;
return code === "EPERM";
}
}
async function removeStaleMaterializeLock(lockDir: string, staleMs: number): Promise<boolean> {View on GitHub (pinned to 67001ec6eb)
Solutions
- Reduce concurrent skill-materialization across processes (serialize adapter startup, or give each its own instance home).
- Manually remove the lock directory printed in the message if you are sure no process is actively materializing.
- Increase MATERIALIZED_SKILL_LOCK_STALE_MS if legitimate materialization is slow on your storage.
- Avoid sharing the skills materialization dir across hosts/containers where pid liveness is unreliable.
Defensive patterns
Strategy: retry
Validate before calling
// Before materializing, check for a live lock owner and warn early.
const lockOwner = await readLockOwner(lockDir);
if (lockOwner && isPidAlive(lockOwner.pid) && Date.now() - lockOwner.createdAt < STALE_MS) {
logger.warn('skill materialization lock held', { pid: lockOwner.pid });
} Try / catch
try { await withMaterializeLock(lockDir, () => materialize()); } catch (err) { if (/Timed out waiting for/.test((err as Error).message)) { await fs.rm(lockDir, { recursive: true, force: true }); /* retry once */ } else throw err; } Prevention
- Serialize adapter startup to avoid concurrent skill materialization into one home.
- Give each adapter its own instance home on shared hosts.
- Remove the lock dir only when you confirm no process is materializing.
When it happens
Trigger: Two adapter processes materializing skills concurrently into the same target root; the holding process is alive (isPidAlive true) and runs longer than the stale window; or removeStaleMaterializeLock cannot clear the lock because the owner pid is alive on a different host/container (pid reuse).
Common situations: Multiple adapter instances sharing a home/skills dir on one host; a long-running materialization (large skill set, slow disk) exceeds the stale timeout while a sibling waits; a crashed process left a lock whose pid now belongs to another live process; NFS/shared volume where pid liveness checks are misleading.
Understand the failure class
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- Timed out waiting for workspace restore lock at ${lockDir}
- 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
- Another managed install is already running${ownerLabel}. If
AI-assisted analysis of paperclipai/paperclip@67001ec6eb (2026-08-12).
Data as JSON: /api/errors/1dd4a9b6326305f7.
Report an issue: GitHub.