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

  1. Reduce concurrent skill-materialization across processes (serialize adapter startup, or give each its own instance home).
  2. Manually remove the lock directory printed in the message if you are sure no process is actively materializing.
  3. Increase MATERIALIZED_SKILL_LOCK_STALE_MS if legitimate materialization is slow on your storage.
  4. 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

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

Related errors


AI-assisted analysis of paperclipai/paperclip@67001ec6eb (2026-08-12). Data as JSON: /api/errors/1dd4a9b6326305f7. Report an issue: GitHub.