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
- 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.
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
- 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.
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
- live artifact refresh already active
- live artifact refresh is older than the latest committed ref
- automation proposal not found
- stale_generation
- generation_payload_mismatch
AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12).
Data as JSON: /api/errors/0c01b49b7b660724.
Report an issue: GitHub.