{"record":{"id":"d895fd7cf9411c55","repo":"nexu-io/open-design","slug":"live-artifact-refresh-already-active","errorCode":null,"errorMessage":"live artifact refresh already active","messagePattern":"live artifact refresh already active","errorType":"exception","errorClass":"LiveArtifactRefreshLockError","httpStatus":null,"severity":"error","filePath":"apps/daemon/src/live-artifacts/store.ts","lineNumber":800,"sourceCode":"  const paths = await assertLiveArtifactRefreshLockScope(options.projectsRoot, options.projectId, artifactId);\n  const state = await readLiveArtifactRefreshState(paths, options.projectId, artifactId);\n  const refreshOrdinal = state.nextRefreshOrdinal;\n  const refreshId = formatRefreshId(refreshOrdinal);\n  const metadata: LiveArtifactRefreshLockMetadata = {\n    schemaVersion: 1,\n    projectId: options.projectId,\n    artifactId,\n    refreshId,\n    refreshOrdinal,\n    acquiredAt: (options.now ?? new Date()).toISOString(),\n    lockId: randomBytes(12).toString('hex'),\n  };\n\n  try {\n    await writeFile(paths.refreshLockPath, stableJson(metadata), { encoding: 'utf8', flag: 'wx' });\n  } catch (error) {\n    if (error && typeof error === 'object' && 'code' in error && error.code === 'EEXIST') {\n      throw new LiveArtifactRefreshLockError('live artifact refresh already active', {\n        projectId: options.projectId,\n        artifactId,\n        lockPath: paths.refreshLockPath,\n      });\n    }\n    throw error;\n  }\n\n  try {\n    await writeLiveArtifactRefreshState(paths, {\n      ...state,\n      nextRefreshOrdinal: refreshOrdinal + 1,\n    });\n  } catch (error) {\n    await rm(paths.refreshLockPath, { force: true });\n    throw error;\n  }\n","sourceCodeStart":782,"sourceCodeEnd":818,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/apps/daemon/src/live-artifacts/store.ts#L782-L818","documentation":"Thrown as LiveArtifactRefreshLockError by acquireLiveArtifactRefreshLock() when writing refresh.lock.json with flag 'wx' (exclusive create) fails with EEXIST. The lock file already exists, meaning another refresh is already running for that artifact. This is a concurrency guard: only one refresh may run at a time per artifact. The custom error class carries projectId, artifactId, and lockPath so callers can surface scope.","triggerScenarios":"Two concurrent calls to acquireLiveArtifactRefreshLock for the same artifact (e.g. two browser tabs, or the UI and a scheduled refresh racing); a previous refresh crashed or timed out without releasing its lock, leaving refresh.lock.json behind; a refresh is still genuinely in progress.","commonSituations":"Daemon restart after a crash that left a stale lock; user double-clicks a 'refresh' button; automation fires a refresh while another is mid-flight; long-running refresh whose worker died silently.","solutions":["Use withLiveArtifactRefreshLock() which acquires and always releases the lock in a finally block.","If the lock is genuinely stale (older than DEFAULT_LIVE_ARTIFACT_TOTAL_TIMEOUT_MS), call recoverStaleLiveArtifactRefreshes() to reap it.","Manually delete <artifactDir>/refresh.lock.json only after confirming no refresh is actually running.","Serialize refresh requests per-artifact in the calling layer (queue or mutex) to avoid racing the exclusive-create."],"exampleFix":"// before\nconst lock = await acquireLiveArtifactRefreshLock({ projectsRoot, projectId, artifactId });\n// ...work... (crash here leaves the lock)\n\n// after — guaranteed release\nawait withLiveArtifactRefreshLock(\n  { projectsRoot, projectId, artifactId },\n  async (lock) => { /* refresh work; lock released in finally */ },\n);","handlingStrategy":"try-catch","validationCode":"import { stat } from 'node:fs/promises';\n\nasync function isRefreshActive(paths): Promise<boolean> {\n  try { await stat(paths.refreshLockPath); return true; } catch { return false; }\n}\n\nif (await isRefreshActive(paths)) {\n  // surface 'refresh already in progress' to the user instead of attempting acquire\n}","typeGuard":"import { LiveArtifactRefreshLockError } from './store';\n\nfunction isRefreshLockBusy(error: unknown): boolean {\n  return error instanceof LiveArtifactRefreshLockError;\n}","tryCatchPattern":"import { acquireLiveArtifactRefreshLock, LiveArtifactRefreshLockError, recoverStaleLiveArtifactRefreshes } from './store';\n\ntry {\n  const lock = await acquireLiveArtifactRefreshLock({ projectsRoot, projectId, artifactId });\n  // ... refresh work ...\n} catch (error) {\n  if (error instanceof LiveArtifactRefreshLockError) {\n    // Surface 'refresh already in progress' to the user; optionally recover stale locks\n    await recoverStaleLiveArtifactRefreshes({ projectsRoot });\n    return;\n  }\n  throw error;\n}","preventionTips":["Always acquire through withLiveArtifactRefreshLock so release happens in finally.","Run recoverStaleLiveArtifactRefreshes on daemon startup to reap crashed locks.","Serialize refresh requests per artifact in the calling layer."],"tags":["live-artifacts","refresh","concurrency","locking","filesystem"],"backgroundTag":null,"analyzedSha":"5be4028344c2eb4c667c5a97bda8f750c5597ef7","analyzedAt":"2026-08-12T12:03:58.812Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}