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

  1. Serialize restores per target dir so only one contends the lock at a time.
  2. If the holder is a legitimately slow restore, wait and retry once it completes (it will release the lock in its finally block).
  3. If no restore should be running, check for an orphaned lock whose owner.json pid is stale and remove <targetDir>.paperclip-restore.lock manually.
  4. 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

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

Related errors


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