nexu-io/open-design · error · FinalizePackageLockedError

CONFLICT

CONFLICT

Error message

finalize is already in progress for project ${projectId}

What it means

FinalizePackageLockedError: fs.openSync(lockPath, 'wx') failed with EEXIST. A `.finalize.lock` file already exists in the project directory, meaning another finalize is running, or a previous run crashed without cleaning up the lock. The route maps this to 409 CONFLICT.

Source

Thrown at apps/daemon/src/design/finalize-design.ts:304

  // For native projects, it lazily creates `.od/projects/<id>`.
  fs.mkdirSync(dir, { recursive: true });
  const finalPath = path.join(dir, OUTPUT_FILENAME);
  const lockPath = path.join(dir, LOCK_FILENAME);
  const tmpPath = path.join(
    dir,
    `${OUTPUT_FILENAME}.tmp.${process.pid}.${randomBytes(4).toString('hex')}`,
  );
  const now = options.now ?? (() => new Date());
  const protocol = options.protocol ?? 'anthropic';
  const baseUrl = options.baseUrl ?? defaultBaseUrlForFinalizeProtocol(protocol);
  const maxTokens = options.maxTokens ?? DEFAULT_MAX_TOKENS;

  let lockFd: number | null = null;
  try {
    lockFd = fs.openSync(lockPath, 'wx');
  } catch (err: unknown) {
    if ((err as NodeJS.ErrnoException)?.code === 'EEXIST') {
      throw new FinalizePackageLockedError(
        `finalize is already in progress for project ${projectId}`,
      );
    }
    throw err;
  }

  try {
    // Phase 3: export transcript via the PR #493 primitive. Returns the
    // disk path; we read the body and run it through the truncation
    // policy so a 4 MB transcript does not blow Anthropic's context.
    const transcriptResult = exportProjectTranscript(db, projectsRoot, projectId, { now });
    const transcriptJsonl = fs.readFileSync(transcriptResult.path, 'utf8');
    const truncatedJsonl = truncateTranscriptForPrompt(transcriptJsonl);

    // Phase 4: design system. Project may not have one selected; readDesignSystem
    // returns null on missing DESIGN.md so the prompt's design-system section
    // gracefully falls back to "(no design system selected for this project)".
    const designSystemId =

View on GitHub (pinned to 5be4028344)

Solutions

  1. Wait for the in-progress finalize to complete, then retry.
  2. If no finalize is running (confirm via process list / a second request still returning 409), remove the stale lock: `rm <projectDir>/.finalize.lock`.
  3. Avoid triggering concurrent finalize on the same project from multiple tabs.
Defensive patterns

Strategy: try-catch

Validate before calling

// Optional pre-check: only helps if no concurrent finalize is known.
import { existsSync } from 'node:fs';
import { join } from 'node:path';
const lockPath = join(projectDir, '.finalize.lock');
if (existsSync(lockPath)) {
  // Either wait or prompt the operator to clear a stale lock.
}

Type guard

import { FinalizePackageLockedError } from './finalize-design.js';
function isFinalizeLocked(err: unknown): err is FinalizePackageLockedError {
  return err instanceof FinalizePackageLockedError;
}

Try / catch

import { FinalizePackageLockedError } from './finalize-design.js';
try {
  await finalizeDesignPackage(db, projectsRoot, dsRoot, projectId, options);
} catch (err) {
  if (err instanceof FinalizePackageLockedError) {
    // HTTP route maps to 409 CONFLICT; retry after the in-progress run,
    // or `rm <projectDir>/.finalize.lock` if it is stale.
    return res.status(409).json({ code: 'CONFLICT', error: err.message });
  }
  throw err;
}

Prevention

When it happens

Trigger: Two concurrent finalize requests for the same project (e.g. double-clicking Finalize, two browser tabs), or a prior finalize that crashed (OOM, kill, daemon restart) leaving a stale `.finalize.lock` behind.

Common situations: Double-clicking the Finalize button; two tabs finalizing the same project; the daemon was killed mid-finalize; an OOM or panic aborted the process before the finally block unlinked the lock.

Related errors


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