{"record":{"id":"02ee27a3199d9f8e","repo":"ruvnet/ruflo","slug":"timed-out-acquiring-workspace-lease-lock","errorCode":null,"errorMessage":"timed out acquiring workspace-lease lock","messagePattern":"timed out acquiring workspace-lease lock","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/cli/src/services/workspace-lease.ts","lineNumber":115,"sourceCode":"      try {\n        const fd = fs.openSync(lockFile, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY, 0o600);\n        fs.writeSync(fd, String(process.pid));\n        fs.closeSync(fd);\n        try {\n          return fn();\n        } finally {\n          try { fs.unlinkSync(lockFile); } catch { /* already gone */ }\n        }\n      } catch (e) {\n        if ((e as NodeJS.ErrnoException).code !== 'EEXIST') throw e;\n        try {\n          const st = fs.lstatSync(lockFile);\n          if (Date.now() - st.mtimeMs > LOCK_STALE_MS) {\n            fs.unlinkSync(lockFile);\n            continue;\n          }\n        } catch { /* raced — retry */ }\n        if (Date.now() > deadline) throw new Error('timed out acquiring workspace-lease lock');\n        await delay(25);\n      }\n    }\n  }\n\n  private readFile(repositoryId: string): LeaseFile {\n    const file = this.fileFor(repositoryId);\n    assertNotSymlink(file);\n    let parsed: LeaseFile = { version: 1, leases: {} };\n    if (fs.existsSync(file)) {\n      try {\n        const raw = JSON.parse(fs.readFileSync(file, 'utf-8'));\n        if (raw && typeof raw === 'object' && raw.leases && typeof raw.leases === 'object') {\n          parsed = { version: 1, leases: raw.leases };\n        }\n      } catch { /* corrupt — start fresh */ }\n    }\n    return parsed;","sourceCodeStart":97,"sourceCodeEnd":133,"githubUrl":"https://github.com/ruvnet/ruflo/blob/fa13ee4ad60ac2090b1480656eb233521790d640/v3/@claude-flow/cli/src/services/workspace-lease.ts#L97-L133","documentation":"WorkspaceLeaseRegistry serializes concurrent access with an O_EXCL lock file: it retries every 25ms against a 2-second deadline (Date.now() + 2000), breaking locks older than LOCK_STALE_MS = 10s. If the lock file still exists and is younger than 10s when the deadline passes, this timeout error is thrown. So the practical cause is another process holding the registry lock for over ~2 seconds.","triggerScenarios":"Two or more claude-flow processes (daemon, hooks, CLI) acquiring/releasing workspace leases on the same repository simultaneously; a long GC pause or hung process holding the lock file; a lock left behind by a crashed process that is younger than the 10s stale threshold when you retry immediately.","commonSituations":"Parallel CI jobs on the same checkout; a session-start hook racing a manually triggered daemon command; retrying immediately after a crash whose lock file is still fresh; NFS/latency where unlink of the lock is slow.","solutions":["Retry the lease operation after a short wait — the lock self-heals: it is force-removed once older than 10s (LOCK_STALE_MS)","Ensure only one claude-flow process touches a given worktree's leases at a time (stop duplicate daemons: daemon stop, check pgrep)","If you know no other process is running and the lock is stale, delete the lock file manually (the *.lock sibling of the registry file)","Serialize lease operations in your own code instead of issuing them concurrently from several async paths"],"exampleFix":"// before\nconst lease = await registry.acquire(worktree, owner); // two processes at once -> timed out acquiring workspace-lease lock\n\n// after\nasync function acquireWithRetry(registry, worktree, owner, attempts = 3) {\n  for (let i = 0; i < attempts; i++) {\n    try { return await registry.acquire(worktree, owner); }\n    catch (e) {\n      if (!/timed out acquiring workspace-lease lock/.test(String(e?.message)) || i === attempts - 1) throw e;\n      await new Promise(r => setTimeout(r, 2000)); // lock goes stale at 10s\n    }\n  }\n}","handlingStrategy":"retry","validationCode":"// Before lease work, confirm no live lock contention from other processes:\nimport * as fs from 'node:fs';\nconst lockFile = `${registryPath}.lock`;\ntry {\n  const st = fs.lstatSync(lockFile);\n  if (Date.now() - st.mtimeMs < 10_000) {\n    console.warn('Workspace-lease lock is fresh — another process likely holds it; deferring');\n  }\n} catch { /* no lock file, uncontended */ }","typeGuard":null,"tryCatchPattern":"async function acquireLeaseWithRetry(acquire: () => Promise<unknown>, attempts = 3): Promise<unknown> {\n  for (let i = 0; i < attempts; i++) {\n    try { return await acquire(); }\n    catch (e) {\n      if (!/timed out acquiring workspace-lease lock/.test(String(e?.message))) throw e;\n      await new Promise(r => setTimeout(r, 2_000)); // lock force-expires at LOCK_STALE_MS = 10s\n    }\n  }\n  throw new Error('workspace-lease lock still held after retries');\n}","preventionTips":["Serialize lease operations per worktree in your own orchestration layer instead of issuing them concurrently","Ensure only one daemon/session is active per checkout (stop stray processes before batch runs)","After a crash, allow ~10s for stale locks to age out, or remove the *.lock file manually when certain no process lives"],"tags":["lock","concurrency","workspace-lease","timeout","file-lock"],"backgroundTag":"lock-acquisition-timeout","analyzedSha":"fa13ee4ad60ac2090b1480656eb233521790d640","analyzedAt":"2026-08-18T21:34:22.708Z","contentChangedAt":"2026-08-18T21:34:22.708Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}