Hmbown/CodeWhale · error · Error
The live pet recording was replaced externally after…
Error message
The live pet recording was replaced externally after rotation.
What it means
After a successful rotation rename, the recorder reopens the path and verifies the opened file's identity matches the freshly installed segment. If something replaced the new recording between rename and reopen (or between rename and the identity stat), it throws, leaving the first row published but signaling that the recorder no longer owns the path.
Solutions
- Deduplicate: ensure exactly one recorder instance targets this path (process lock or supervisor).
- Check the on-disk state: the archived segment and new live file exist; decide whether to keep them and restart, or delete and re-record.
- Disable atomic-save/edit-on-change tools for the recording directory.
- Retry with a fresh path if ownership of the directory is contested.
Example fix
// before: two recorders, same path const r1 = await createPetRecorder(p); const r2 = await createPetRecorder(p); // after const recorder = await createPetRecorder(p); // single owner only
Defensive patterns
Strategy: try-catch
Try / catch
try {
await recorder.append(bucket);
} catch (e) {
if (e.message.includes('replaced externally after rotation')) {
// archived segment exists; inspect dir, decide keep-or-restart, then re-create recorder
} else throw e;
} Prevention
- Enforce single-owner via a process-level lock on the recording path
- Exclude the directory from IDE atomic-save and deploy tooling
- Start all recorders from one supervisor, never ad hoc
- After this error, audit the directory before restarting
When it happens
Trigger: A competing writer renamed its own file over the path in the tiny window after the recorder's rename and before/while reopening; an atomic-save editor swapped the file; another recorder instance raced the same path.
Common situations: Two processes starting recorders on the same path simultaneously; IDE atomic-save plugins reacting to the directory; deployment tooling managing the same directory.
Understand the failure class
Background: "failed to write file", "Could not save figure", "Error saving remote file" — file write failed: causes and fixes across languages and libraries — this error's family across 38 libraries.
Related errors
- ; additionally the Codewhale-owned legacy secret slot…
- telemetry permission changed before arming
- The live pet recording changed while rotating; existing…
- The live pet recording was changed or replaced externally…
- The previous pet recording changed while reading; it was…
AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15).
Data as JSON: /api/errors/1137a7df40a82611.
Report an issue: GitHub.
Appendix: source
Thrown at pet/scripts/lib/pet-recorder.mjs:149
|| destination.size !== held.size || destination.mtimeNs !== held.mtimeNs)
throw new Error('The live pet recording changed while rotating; existing files were preserved.');
try { await rename(temporary, path); break; }
catch (error) {
// A reader or file scanner can briefly deny replacement on
// Windows. Retry for under two seconds, checking identity each
// time; persistent denial still stops without deleting history.
if (process.platform !== 'win32' || !['EPERM', 'EBUSY'].includes(error.code) || attempt >= 20) throw error;
await delay(Math.min(100, (attempt + 1) * 25));
}
}
installed = true;
// The first row is already published. Account for it before any
// fallible cleanup/report so a later append sees the actual file.
sequence = 1; bytes = size; segment++; restart = false;
output = await open(path, constants.O_WRONLY | constants.O_APPEND);
const reopened = await output.stat({ bigint: true });
if (reopened.dev !== nextIdentity.dev || reopened.ino !== nextIdentity.ino)
throw new Error('The live pet recording was replaced externally after rotation.');
expectedMtime = reopened.mtimeNs;
await syncDirectory(dirname(path));
report(`Archived pet recording: ${archive}`);
} finally {
await next?.close();
if (created && !installed) await unlink(temporary);
}
return;
} else {
await output.writeFile(row);
expectedMtime = (await output.stat({ bigint: true })).mtimeNs;
}
bytes += size; sequence++;
} finally { busy = false; }
},
async close() {
if (busy) throw new Error('Wait for the pet recorder write before closing.');
const current = output, heldLock = lock; output = undefined; lock = undefined;View on GitHub (pinned to 433685b202)