{"record":{"id":"1dd4a9b6326305f7","repo":"paperclipai/paperclip","slug":"timed-out-waiting-for-paperclip-skill-materializat","errorCode":null,"errorMessage":"Timed out waiting for Paperclip skill materialization lock at ${lockDir}","messagePattern":"Timed out waiting for Paperclip skill materialization lock at (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/adapter-utils/src/server-utils.ts","lineNumber":3012,"sourceCode":"  await fs.mkdir(path.dirname(lockDir), { recursive: true });\n  const deadline = Date.now() + MATERIALIZED_SKILL_LOCK_STALE_MS;\n  while (true) {\n    try {\n      await fs.mkdir(lockDir);\n      await fs.writeFile(\n        path.join(lockDir, MATERIALIZED_SKILL_LOCK_OWNER),\n        `${JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() })}\\n`,\n        \"utf8\",\n      );\n      return async () => {\n        await fs.rm(lockDir, { recursive: true, force: true });\n      };\n    } catch (err) {\n      const code = err && typeof err === \"object\" ? (err as { code?: unknown }).code : null;\n      if (code !== \"EEXIST\") throw err;\n      if (await removeStaleMaterializeLock(lockDir, MATERIALIZED_SKILL_LOCK_STALE_MS)) continue;\n      if (Date.now() >= deadline) {\n        throw new Error(`Timed out waiting for Paperclip skill materialization lock at ${lockDir}`);\n      }\n      await new Promise((resolve) => setTimeout(resolve, 50));\n    }\n  }\n}\n\nfunction isPidAlive(pid: number): boolean {\n  if (!Number.isInteger(pid) || pid <= 0) return false;\n  try {\n    process.kill(pid, 0);\n    return true;\n  } catch (err) {\n    const code = err && typeof err === \"object\" ? (err as { code?: unknown }).code : null;\n    return code === \"EPERM\";\n  }\n}\n\nasync function removeStaleMaterializeLock(lockDir: string, staleMs: number): Promise<boolean> {","sourceCodeStart":2994,"sourceCodeEnd":3030,"githubUrl":"https://github.com/paperclipai/paperclip/blob/67001ec6eb96ae601aa27bc91d9b2415d665334a/packages/adapter-utils/src/server-utils.ts#L2994-L3030","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":null,"handlingStrategy":"retry","validationCode":"// Before materializing, check for a live lock owner and warn early.\nconst lockOwner = await readLockOwner(lockDir);\nif (lockOwner && isPidAlive(lockOwner.pid) && Date.now() - lockOwner.createdAt < STALE_MS) {\n  logger.warn('skill materialization lock held', { pid: lockOwner.pid });\n}","typeGuard":null,"tryCatchPattern":"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; }","preventionTips":["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."],"tags":["skills","lock","concurrency","materialization","timeout"],"backgroundTag":null,"analyzedSha":"67001ec6eb96ae601aa27bc91d9b2415d665334a","analyzedAt":"2026-08-12T12:05:45.408Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}