{"record":{"id":"c4f9db16e87f5df9","repo":"paperclipai/paperclip","slug":"timed-out-waiting-for-workspace-restore-lock-at","errorCode":null,"errorMessage":"Timed out waiting for workspace restore lock at ${lockDir}","messagePattern":"Timed out waiting for workspace restore lock at (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/adapter-utils/src/workspace-restore-merge.ts","lineNumber":154,"sourceCode":"        path.join(lockDir, \"owner.json\"),\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 }).catch(() => undefined);\n      };\n    } catch (error) {\n      const code = error && typeof error === \"object\" ? (error as { code?: unknown }).code : null;\n      if (code !== \"EEXIST\") throw error;\n      // Stale-lock detection: if the owner PID is dead (SIGKILL / OOM / crash),\n      // the lockDir would otherwise persist forever and stall restores. Mirror\n      // the materializePaperclipSkillCopy lock pattern — remove and retry.\n      if (!(await isHolderAlive(lockDir))) {\n        await fs.rm(lockDir, { recursive: true, force: true }).catch(() => undefined);\n        continue;\n      }\n      if (Date.now() >= deadline) {\n        throw new Error(`Timed out waiting for workspace restore lock at ${lockDir}`);\n      }\n      await new Promise((resolve) => setTimeout(resolve, 50));\n    }\n  }\n}\n\nexport async function withDirectoryMergeLock<T>(\n  targetDir: string,\n  fn: () => Promise<T>,\n): Promise<T> {\n  const releaseLock = await acquireDirectoryMergeLock(`${targetDir}.paperclip-restore.lock`);\n  try {\n    return await fn();\n  } finally {\n    await releaseLock();\n  }\n}\n","sourceCodeStart":136,"sourceCodeEnd":172,"githubUrl":"https://github.com/paperclipai/paperclip/blob/67001ec6eb96ae601aa27bc91d9b2415d665334a/packages/adapter-utils/src/workspace-restore-merge.ts#L136-L172","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  await withDirectoryMergeLock(targetDir, async () => { /* merge */ });\n} catch (err) {\n  if (err.message.startsWith(\"Timed out waiting for workspace restore lock\")) {\n    // another restore holds the lock; wait for it to finish, then retry once\n  }\n  throw err;\n}","preventionTips":["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."],"tags":["workspace-restore","lock","concurrency","timeout","paperclip"],"backgroundTag":null,"analyzedSha":"67001ec6eb96ae601aa27bc91d9b2415d665334a","analyzedAt":"2026-08-12T12:05:45.408Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}