Hmbown/CodeWhale · error
The previous pet recording changed while reading; it was…
Error message
The previous pet recording changed while reading; it was preserved.
What it means
During resume, the library reads the existing file in full, then re-stats the open handle and compares size and mtimeNs against the original lstat values. If the length read back does not equal the statted size, or the file's size/mtime changed since it was first inspected, a concurrent writer modified it during the read. The library throws and preserves the file rather than appending to a moving target.
Solutions
- Stop all other writers to the recording path, verify with lsof/fuser that nothing holds the file open for write, then retry the resume.
- Snapshot/copy the current file to a stable location and resume from the copy, so the live path is untouched.
- If the concurrent writer is another recorder instance, kill it or let it finish and reuse its output rather than double-attaching.
Example fix
// before
await createPetRecorder(activePath, { resume: true }); // other process still appending
// after
await stopOtherRecorder(activePath);
await waitUntilStable(activePath, 1000); // size/mtime unchanged for 1s
await createPetRecorder(activePath, { resume: true }); Defensive patterns
Strategy: retry
Validate before calling
async function quiescent(p, ms = 1000) {
const { stat } = await import('node:fs/promises');
const a = await stat(p, { bigint: true });
await new Promise(r => setTimeout(r, ms));
const b = await stat(p, { bigint: true });
return a.size === b.size && a.mtimeNs === b.mtimeNs;
} Try / catch
try { await createPetRecorder(p, { resume: true }); } catch (e) { if (/changed while (opening|reading)/.test(e.message)) { await waitForQuiescence(p); return createPetRecorder(p, { resume: true }); } throw e; } Prevention
- Ensure no other process holds the file open for append (check lsof) before resuming.
- Quiesce or pause concurrent writers/rotation for the duration of the resume.
- Resume from a frozen copy when the live path must keep receiving writes.
When it happens
Trigger: createPetRecorder(path, { resume: true }) while another process appends to or truncates the recording between the initial stat and the post-read re-stat; reads returning fewer bytes than stat reported mid-read loop; a writer flushing more data after the initial open/stat but before the unchanged check.
Common situations: A second CLI instance or tail-like consumer writing/rotating the file concurrently; a watchdog appending status lines; an interrupted previous run being written to by a recovered process.
Related errors
- The previous pet recording changed while opening; it was…
- ; additionally the Codewhale-owned legacy secret slot…
- external credential path was redirected while opening
- failed to create owned skill directory
- file is not a bounded regular single-link file
AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15).
Data as JSON: /api/errors/0b9e8ad507c409c4.
Report an issue: GitHub.
Appendix: source
Thrown at pet/scripts/lib/pet-recorder.mjs:75
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)
throw new Error('The previous pet recording is not a bounded regular file; it was preserved.');
output = await open(path, constants.O_RDWR | constants.O_APPEND | constants.O_NOFOLLOW | constants.O_NONBLOCK);
const held = await output.stat({ bigint: true });
if (held.dev !== original.dev || held.ino !== original.ino || held.size !== original.size)
throw new Error('The previous pet recording changed while opening; it was preserved.');
// Read at most the size already checked, including a single growth byte.
const contents = Buffer.alloc(Number(held.size) + 1);
let length = 0;
while (length < contents.length) {
const { bytesRead } = await output.read(contents, length, contents.length - length, length);
if (!bytesRead) break;
length += bytesRead;
}
if (length !== Number(held.size)) throw new Error('The previous pet recording changed while reading; it was preserved.');
const text = new TextDecoder('utf-8', { fatal: true }).decode(contents.subarray(0, length));
if (text && !text.endsWith('\n')) throw new Error('The previous pet recording has an incomplete final row; it was preserved.');
decodePetJSONL(text);
const unchanged = await output.stat({ bigint: true });
if (unchanged.size !== original.size || unchanged.mtimeNs !== original.mtimeNs)
throw new Error('The previous pet recording changed while reading; it was preserved.');
bytes = length; restart = true; expectedMtime = original.mtimeNs;
// Continue archive numbering without collecting a growing directory list.
const prefix = `${basename(path)}.segment-`;
for await (const entry of await opendir(dirname(path))) {
if (!entry.name.startsWith(prefix)) continue;
const suffix = entry.name.slice(prefix.length);
if (!/^[0-9]{6,}\.jsonl$/.test(suffix)) continue;
const number = Number(suffix.slice(0, -6));
if (!Number.isSafeInteger(number) || number >= Number.MAX_SAFE_INTEGER)
throw new Error('Pet archive numbering is exhausted; existing files were preserved.');
segment = Math.max(segment, number);
}View on GitHub (pinned to 433685b202)