{"record":{"id":"0c01b49b7b660724","repo":"nexu-io/open-design","slug":"refresh-locked","errorCode":"REFRESH_LOCKED","errorMessage":"live artifact refresh lock ownership mismatch","messagePattern":"live artifact refresh lock ownership mismatch","errorType":"http","errorClass":"LiveArtifactRefreshLockError","httpStatus":409,"severity":"error","filePath":"apps/daemon/src/live-artifacts/store.ts","lineNumber":1009,"sourceCode":"  });\n  return { artifact, paths };\n}\n\nexport async function releaseLiveArtifactRefreshLock(lock: LiveArtifactRefreshLock): Promise<void> {\n  let current: LiveArtifactRefreshLockMetadata;\n  try {\n    current = JSON.parse(await readFile(lock.lockPath, 'utf8')) as LiveArtifactRefreshLockMetadata;\n  } catch (error) {\n    if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') return;\n    throw error;\n  }\n\n  if (\n    current.projectId !== lock.metadata.projectId\n    || current.artifactId !== lock.metadata.artifactId\n    || current.lockId !== lock.metadata.lockId\n  ) {\n    throw new LiveArtifactRefreshLockError('live artifact refresh lock ownership mismatch', {\n      projectId: lock.metadata.projectId,\n      artifactId: lock.metadata.artifactId,\n      lockPath: lock.lockPath,\n    });\n  }\n\n  await rm(lock.lockPath, { force: true });\n}\n\nexport async function withLiveArtifactRefreshLock<T>(\n  options: AcquireLiveArtifactRefreshLockOptions,\n  callback: (lock: LiveArtifactRefreshLock) => Promise<T>,\n): Promise<T> {\n  const lock = await acquireLiveArtifactRefreshLock(options);\n  try {\n    return await callback(lock);\n  } finally {\n    await releaseLiveArtifactRefreshLock(lock);","sourceCodeStart":991,"sourceCodeEnd":1027,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/apps/daemon/src/live-artifacts/store.ts#L991-L1027","documentation":"releaseLiveArtifactRefreshLock reads the on-disk lock file and compares projectId, artifactId, and lockId against the lock handle being released. If any differ, the file on disk was overwritten by a different owner between acquire and release, so releasing would unlock someone else's lock; it refuses and throws LiveArtifactRefreshLockError with code REFRESH_LOCKED.","triggerScenarios":"Two refresh operations for the same artifact with different lockId or process racing; an external process replaced the lock file; lockId reused across distinct refreshes.","commonSituations":"Concurrent refresh of one live artifact from two daemon instances or two tabs; a stale lock from a crashed process that a new acquire then overwrote.","solutions":["Serialize refreshes per artifact: only one in-flight refresh per artifactId.","Treat REFRESH_LOCKED as meaning someone else owns this refresh and back off.","Ensure withLiveArtifactRefreshLock acquire and release are always paired and not nested across processes."],"exampleFix":"// before: two processes refresh the same artifact concurrently\n// after: gate by lock\nawait withLiveArtifactRefreshLock({ projectId, artifactId }, async () => {\n  // refresh\n})","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"function isRefreshLockedError(e: unknown): boolean {\n  return (e != null && typeof e === 'object' && (e as any)?.code === 'REFRESH_LOCKED')\n    || /lock ownership mismatch/i.test((e as Error)?.message ?? '');\n}","tryCatchPattern":"try {\n  await releaseLiveArtifactRefreshLock(lock);\n} catch (e) {\n  if (isRefreshLockedError(e)) {\n    // another owner now holds this lock; back off, do NOT delete the lock file\n    return;\n  }\n  throw e;\n}","preventionTips":["Allow only one in-flight refresh per artifactId.","Never manually delete lock files.","Always pair lock acquire with release and never nest them across processes."],"tags":["live-artifacts","locking","concurrency"],"backgroundTag":null,"analyzedSha":"5be4028344c2eb4c667c5a97bda8f750c5597ef7","analyzedAt":"2026-08-12T12:03:58.812Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}