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
- Stop the other recorder process using this output path, then rerun.
- Point the new recorder at a different output path if both instances are intentional.
- Check for lingering processes (ps aux | grep recorder) and kill any stale one before retrying.
- 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
- Wrap recorder launches in flock or your scheduler's mutual-exclusion feature.
- Use one output path per recorder instance.
- Kill stale recorder processes after crashes before restarting on the same output.
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
- failed to enable WAL for
- Runtime turn operation is already being claimed; retry
- telemetry privacy lock is held
- The pet recorder lock was replaced; existing files were…
- This runtime is already active in another process. Close…
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)