Hmbown/CodeWhale · error

Another pet recorder is using this output.

Error message

Another pet recorder is using this output.

What it means

Inside lockRecorder, the SQLite database is opened on the lock file and BEGIN EXCLUSIVE is issued with busy_timeout 0. If SQLite reports errcode 5 (SQLITE_BUSY) or 6 (SQLITE_LOCKED), another recorder already holds the database lock, so this friendly error is thrown instead of the raw SQLite code. It is the intended 'single writer' signal: a second recorder instance is pointing at the same output.

Solutions

  1. Stop the other recorder process using this output path, then rerun.
  2. Point the new recorder at a different output path if both instances are intentional.
  3. Check for lingering processes (ps aux | grep recorder) and kill any stale one before retrying.
  4. Add instance coordination (e.g. your scheduler's mutual exclusion) so overlapping runs never start.

Example fix

// before: second overlapping run
node recorder.js --out same.db   # -> Another pet recorder is using this output.
// after: guard with flock so only one instance ever starts
flock -n /tmp/recorder.lock -c 'node recorder.js --out same.db' || echo 'already running'
Defensive patterns

Strategy: try-catch

Validate before calling

// check a live lock exists before even attempting to start a second recorder
import { lstat } from 'node:fs/promises';
try { await lstat(path + '.writer-lock'); console.error('output appears locked; refusing to start'); process.exit(1); }
catch (e) { if (e.code !== 'ENOENT') throw e; }

Try / catch

try {
  const lock = await lockRecorder(path);
} catch (e) {
  if (e.message === 'Another pet recorder is using this output.') {
    console.error('recorder already running for', path);
    process.exit(0); // expected during overlapping runs
  }
  throw e;
}

Prevention

When it happens

Trigger: Starting a second recorder against the same output path while the first still holds the lock: BEGIN EXCLUSIVE returns SQLITE_BUSY (5) or SQLITE_LOCKED (6).

Common situations: Running two recorder processes/terminals on the same output; a cron job overlapping a manual run; the previous recorder is a zombie still holding the DB; container restart racing a lingering old instance.

Related errors


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

Appendix: source

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

  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;
  }
}

/** Replaces the CLI's unbounded append-only output. Each complete segment is
 * replayable on its own; the same live pathname always holds the newest one. */
export async function createPetRecorder(path, { maxBuckets = 216_000, maxBytes = 64 * 1024 * 1024, report = () => {}, resume = false } = {}) {
  if (!Number.isSafeInteger(maxBuckets) || maxBuckets < 1 || maxBuckets > 216_000
    || !Number.isSafeInteger(maxBytes) || maxBytes < 1 || maxBytes > 64 * 1024 * 1024)
    throw new Error('Invalid pet recording segment limit.');
  path = resolve(await realpath(dirname(resolve(path))), basename(path));
  let lock = await lockRecorder(path), output, sequence = 0, bytes = 0, segment = 0, busy = false, restart = false, expectedMtime;
  try {
    try { output = await open(path, 'wx', 0o600); }
    catch (error) {
      if (!resume || error.code !== 'EEXIST') throw error;
      const original = await lstat(path, { bigint: true });
      if (!original.isFile() || original.size > 64n * 1024n * 1024n)

View on GitHub (pinned to 433685b202)