nexu-io/open-design · error · LiveArtifactStaleRefreshError

live artifact refresh is older than the latest committed ref

Error message

live artifact refresh is older than the latest committed refresh

What it means

Thrown as LiveArtifactStaleRefreshError by markLiveArtifactRefreshCommitted() when the supplied refreshId's ordinal is <= the last successfully committed ordinal. Once a newer refresh has been committed, older allocated-but-uncommitted ids are superseded and must not be committed — doing so would roll the artifact backwards. The error exposes the stale refreshId and the lastCommittedRefreshId so callers can re-sync.

Source

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

export async function markLiveArtifactRefreshCommitted(
  options: MarkLiveArtifactRefreshCommittedOptions,
): Promise<LiveArtifactRefreshState> {
  const artifactId = validateLiveArtifactStorageId(options.artifactId);
  const paths = await assertLiveArtifactRefreshLockScope(options.projectsRoot, options.projectId, artifactId);
  const refreshOrdinal = parseRefreshOrdinal(options.refreshId);
  const state = await readLiveArtifactRefreshState(paths, options.projectId, artifactId);
  if (refreshOrdinal >= state.nextRefreshOrdinal) {
    throw validationError('refreshId', 'live artifact refresh id has not been allocated');
  }
  if ((state.lastCommittedRefreshOrdinal ?? 0) >= refreshOrdinal) {
    const staleOptions: { projectId: string; artifactId: string; refreshId: string; lastCommittedRefreshId?: string } = {
      projectId: options.projectId,
      artifactId,
      refreshId: options.refreshId,
    };
    if (state.lastCommittedRefreshId !== undefined) staleOptions.lastCommittedRefreshId = state.lastCommittedRefreshId;
    throw new LiveArtifactStaleRefreshError('live artifact refresh is older than the latest committed refresh', staleOptions);
  }

  const nextState: LiveArtifactRefreshState = {
    ...state,
    nextRefreshOrdinal: Math.max(state.nextRefreshOrdinal, refreshOrdinal + 1),
    lastCommittedRefreshId: options.refreshId,
    lastCommittedRefreshOrdinal: refreshOrdinal,
  };
  await writeLiveArtifactRefreshState(paths, nextState);
  return nextState;
}

export async function markLiveArtifactRefreshRunning(
  options: MarkLiveArtifactRefreshRunningOptions,
): Promise<LiveArtifactStoreRecord> {
  const artifactId = validateLiveArtifactStorageId(options.artifactId);
  const paths = await assertLiveArtifactRefreshLockScope(options.projectsRoot, options.projectId, artifactId);
  const current = await readPersistedLiveArtifact(paths);

View on GitHub (pinned to 5be4028344)

Solutions

  1. Acquire a fresh refresh lock to get a new refreshId, then commit that — never reuse an ordinal older than the latest committed one.
  2. Make commit calls idempotent at the caller by tracking the latest committed refreshId and short-circuiting retries that match it.
  3. Inspect error.lastCommittedRefreshId to reconcile local state before retrying.
  4. Serialize commits per artifact so a newer refresh cannot overtake an older in-flight one.

Example fix

// before — reusing a stale id after a newer commit
await markLiveArtifactRefreshCommitted({ ..., refreshId: 'refresh-000002' }); // throws if 000003 committed

// after — acquire a fresh lock first
const lock = await acquireLiveArtifactRefreshLock({ ... });
await markLiveArtifactRefreshCommitted({ ..., refreshId: lock.metadata.refreshId });
Defensive patterns

Strategy: try-catch

Type guard

import { LiveArtifactStaleRefreshError } from './store';

function isStaleRefresh(error: unknown): boolean {
  return error instanceof LiveArtifactStaleRefreshError;
}

Try / catch

import { markLiveArtifactRefreshCommitted, acquireLiveArtifactRefreshLock, LiveArtifactStaleRefreshError } from './store';

try {
  await markLiveArtifactRefreshCommitted({ projectsRoot, projectId, artifactId, refreshId });
} catch (error) {
  if (error instanceof LiveArtifactStaleRefreshError) {
    // Do NOT retry with the same refreshId — acquire a fresh lock and recommit.
    const lock = await acquireLiveArtifactRefreshLock({ projectsRoot, projectId, artifactId });
    await markLiveArtifactRefreshCommitted({ projectsRoot, projectId, artifactId, refreshId: lock.metadata.refreshId });
    return;
  }
  throw error;
}

Prevention

When it happens

Trigger: Caller acquires refresh-000002, then separately acquires and commits refresh-000003, then tries to commit the now-stale refresh-000002. Also when a retry fires after the original call already succeeded but the response was lost.

Common situations: At-least-once retry logic that re-sends a commit after a network blip; two workers both attempting to commit different refreshes of the same artifact; UI re-submitting a stale form after a newer refresh completed.

Related errors


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