nexu-io/open-design · error · LiveArtifactRefreshLockError

REFRESH_LOCKED

REFRESH_LOCKED

Error message

live artifact refresh lock ownership mismatch

What it means

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.

Source

Thrown at apps/daemon/src/live-artifacts/store.ts:1009

  });
  return { artifact, paths };
}

export async function releaseLiveArtifactRefreshLock(lock: LiveArtifactRefreshLock): Promise<void> {
  let current: LiveArtifactRefreshLockMetadata;
  try {
    current = JSON.parse(await readFile(lock.lockPath, 'utf8')) as LiveArtifactRefreshLockMetadata;
  } catch (error) {
    if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') return;
    throw error;
  }

  if (
    current.projectId !== lock.metadata.projectId
    || current.artifactId !== lock.metadata.artifactId
    || current.lockId !== lock.metadata.lockId
  ) {
    throw new LiveArtifactRefreshLockError('live artifact refresh lock ownership mismatch', {
      projectId: lock.metadata.projectId,
      artifactId: lock.metadata.artifactId,
      lockPath: lock.lockPath,
    });
  }

  await rm(lock.lockPath, { force: true });
}

export async function withLiveArtifactRefreshLock<T>(
  options: AcquireLiveArtifactRefreshLockOptions,
  callback: (lock: LiveArtifactRefreshLock) => Promise<T>,
): Promise<T> {
  const lock = await acquireLiveArtifactRefreshLock(options);
  try {
    return await callback(lock);
  } finally {
    await releaseLiveArtifactRefreshLock(lock);

View on GitHub (pinned to 5be4028344)

Solutions

  1. Serialize refreshes per artifact: only one in-flight refresh per artifactId.
  2. Treat REFRESH_LOCKED as meaning someone else owns this refresh and back off.
  3. Ensure withLiveArtifactRefreshLock acquire and release are always paired and not nested across processes.

Example fix

// before: two processes refresh the same artifact concurrently
// after: gate by lock
await withLiveArtifactRefreshLock({ projectId, artifactId }, async () => {
  // refresh
})
Defensive patterns

Strategy: try-catch

Type guard

function isRefreshLockedError(e: unknown): boolean {
  return (e != null && typeof e === 'object' && (e as any)?.code === 'REFRESH_LOCKED')
    || /lock ownership mismatch/i.test((e as Error)?.message ?? '');
}

Try / catch

try {
  await releaseLiveArtifactRefreshLock(lock);
} catch (e) {
  if (isRefreshLockedError(e)) {
    // another owner now holds this lock; back off, do NOT delete the lock file
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12). Data as JSON: /api/errors/0c01b49b7b660724. Report an issue: GitHub.