Hmbown/CodeWhale · error · Error
The live pet recording changed while rotating; existing…
Error message
The live pet recording changed while rotating; existing files were preserved.
What it means
During rotation, after staging the new segment file, the recorder re-stats the destination path and requires it to still be the exact same file (dev, inode, size, mtime) it held before performing the atomic rename. If the live recording changed meanwhile, it refuses to replace it and preserves the on-disk state.
Solutions
- Ensure no other process writes or replaces the recording while the recorder runs.
- Retry the append after the competing writer stops; nothing was destroyed.
- On Windows persistent EPERM/EBUSY, close tools (indexers, editors) holding the file and retry.
- If a job must read the file, have it copy without modifying mtime or use read-only access.
Example fix
// before: a cron job rewrites the live file during rotation * * * * * cp pet-recording.jsonl backup.jsonl && : > pet-recording.jsonl // after: copy only, never modify * * * * * cp pet-recording.jsonl backup.jsonl
Defensive patterns
Strategy: retry
Try / catch
try {
await recorder.append(bucket);
} catch (e) {
if (e.message.includes('changed while rotating')) {
await sleep(500); // competing writer/AV finished; files were preserved
await recorder.append(bucket); // retry once
} else throw e;
} Prevention
- Keep AV/indexers away from the recording directory
- Do not run competing writers during rotation-prone appends
- On Windows, close tools that hold file handles (EPERM/EBUSY)
- Treat this as transient: state on disk is intact, retry
When it happens
Trigger: Another writer appended to or replaced the recording between the append-time identity check and the rename; a reader/scanner tool touched the file; the file was deleted and recreated during the rotation window.
Common situations: Log scanners or antivirus briefly holding/altering the file during rotation; a second recorder instance racing for the same path; monitoring jobs that copy-then-truncate the live file.
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
- The live pet recording was changed or replaced externally…
- The live pet recording was replaced externally after…
- A saved trace identifier collided. Try saving again.
- a window change is already in progress
- another Codewhale process
AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15).
Data as JSON: /api/errors/ccea83f88b11eacc.
Report an issue: GitHub.
Appendix: source
Thrown at pet/scripts/lib/pet-recorder.mjs:132
let next, installed = false, created = false;
try {
next = await open(temporary, 'wx', 0o600); created = true;
await next.writeFile(row); await next.sync();
const nextIdentity = await next.stat({ bigint: true });
await next.close(); next = undefined;
await output.sync();
// link is exclusive: a collision never replaces someone else's
// archive. Persist this name before replacing the live pathname.
await link(path, archive); await syncDirectory(dirname(path));
// Windows can reject replacement while either writer handle is
// open. Both files are synced and the old file is archived first.
await output.close(); output = undefined;
for (let attempt = 0; ; attempt++) {
await lock.check();
const destination = await lstat(path, { bigint: true });
if (!destination.isFile() || destination.dev !== held.dev || destination.ino !== held.ino
|| 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;View on GitHub (pinned to 433685b202)