Hmbown/CodeWhale · error

Invalid pet recorder lock; existing files were preserved.

Error message

Invalid pet recorder lock; existing files were preserved.

What it means

lockRecorder creates an exclusive `<path>.writer-lock` file with open(..., 'wx') and then verifies with lstat that what exists is really a regular file, hard-link count 1, and size 0. If the existing lock file fails that identity check (it was left behind by a crashed writer that wrote data to it, is hard-linked elsewhere, or is a symlink/special file), this error is thrown so the caller's existing output files are never clobbered. It is a stale-lock hygiene guard, not a concurrency error.

Solutions

  1. Inspect `<path>.writer-lock`: if it is a stale leftover from a dead process, delete it with rm and rerun.
  2. Never reuse the lock file for data; if your tooling writes to it, point that tooling elsewhere.
  3. Check for hard links (ls -l link count) and remove the extra links or the file.
  4. If it is a symlink, remove it and investigate what created it before retrying.

Example fix

// before: blind delete then rerun
rm -f output.db.writer-lock && node recorder.js
// after: verify it is stale (owning pid gone / not linked) before deleting
ls -li output.db.writer-lock   # check nlink, size
[ -s output.db.writer-lock ] && cat output.db.writer-lock  # inspect contents
rm -- output.db.writer-lock    # only after confirming no live recorder holds it
Defensive patterns

Strategy: try-catch

Validate before calling

import { lstat } from 'node:fs/promises';
// caller-side stale lock pre-check
try {
  const st = await lstat(path + '.writer-lock', { bigint: true });
  if (st.isFile() && st.nlink === 1n && st.size === 0n) throw new Error('live-looking lock present; verify holder before proceeding');
  // otherwise inspect/delete the anomalous lock before calling lockRecorder
} catch (e) { if (e.code !== 'ENOENT') throw e; }

Try / catch

try {
  const lock = await lockRecorder(path);
} catch (e) {
  if (e.message.includes('Invalid pet recorder lock')) {
    // anomalous leftover lock — confirm no live writer, then remove and retry once
    await confirmNoLiveWriter(path);
    await rm(path + '.writer-lock');
    return lockRecorder(path);
  }
  throw e;
}

Prevention

When it happens

Trigger: A `<path>.writer-lock` file already exists at lock time AND is not a plain empty file with nlink==1: a previous run wrote bytes into the lock file, the file was hard-linked, or it is a symlink/device node that 'wx' happened not to create (e.g. pre-existing).

Common situations: A crashed recorder left a corrupted or non-empty lock file; a user manually created or copied the lock path; a backup/restore tool hard-linked output files including the lock; running on a filesystem where the lock path pre-exists from an older format.

Understand the failure class

Background: "already exists" / EEXIST / FileAlreadyExistsException: what the 'file already exists' error means and how to fix it — this error's family across 37 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15). Data as JSON: /api/errors/1b696c6619bce3c4. Report an issue: GitHub.

Appendix: source

Thrown at pet/scripts/lib/pet-recorder.mjs:28

  let directory;
  try { directory = await open(path, 'r'); await directory.sync(); }
  catch (error) {
    // Node cannot open/sync directory handles on Windows. File data is still
    // synced before its atomic replacement on that platform.
    if (process.platform !== 'win32' || !['EPERM', 'EISDIR', 'EINVAL', 'ENOTSUP'].includes(error.code)) throw error;
  } finally { await directory?.close(); }
}

// SQLite's OS lock is released even after process death. This empty sidecar
// contains no events or recorder state; keep its pathname so later processes
// coordinate on the same inode. No PID files or stale-lock deletion are needed.
async function lockRecorder(path) {
  const name = `${path}.writer-lock`;
  try { const created = await open(name, 'wx', 0o600); await created.close(); }
  catch (error) { if (error.code !== 'EEXIST') throw error; }
  const identity = await lstat(name, { bigint: true });
  if (!identity.isFile() || identity.nlink !== 1n || identity.size !== 0n)
    throw new Error('Invalid pet recorder lock; existing files were preserved.');
  let database;
  const check = async () => {
    const current = await lstat(name, { bigint: true });
    if (!current.isFile() || current.nlink !== 1n || current.size !== 0n
      || current.dev !== identity.dev || current.ino !== identity.ino)
      throw new Error('The pet recorder lock was replaced; existing files were preserved.');
  };
  try {
    database = new DatabaseSync(name);
    database.exec('PRAGMA busy_timeout = 0; BEGIN EXCLUSIVE');
    await check();
    return { check, close: () => { database.close(); } };
  } catch (error) {
    database?.close();
    if (error.errcode === 5 || error.errcode === 6) throw new Error('Another pet recorder is using this output.');
    throw error;
  }
}

View on GitHub (pinned to 433685b202)