Hmbown/CodeWhale · error · Error
Pet recorder is closed or already writing.
Error message
Pet recorder is closed or already writing.
What it means
append() throws this when the recorder has been closed (output handle undefined) or when a previous append is still in flight (busy flag set). The recorder allows only one write at a time and no writes after close.
Solutions
- Await every append() call before issuing the next: `await recorder.append(bucket)`.
- Check lifecycle: do not call append() after close(); stop producers before closing.
- Serialize appends through a queue or use one writer task that owns the recorder.
- If closing early is the cause, defer close() until all pending writes resolve.
Example fix
// before recorder.append(bucket); recorder.append(bucket2); // after await recorder.append(bucket); await recorder.append(bucket2);
Defensive patterns
Strategy: type-guard
Validate before calling
function canAppend(recorder) { return recorder !== null && recorderClosed !== true; } Type guard
function isWritable(rec) {
return rec && typeof rec.append === 'function' && !closed && !pendingWrite;
} Try / catch
try {
await recorder.append(bucket);
} catch (e) {
if (e.message.includes('closed or already writing')) {
// serialize: wait for prior write or treat recorder as closed
await pendingWrite?.catch(() => {});
} else throw e;
} Prevention
- Always await append() before the next call
- Close the recorder only after all producers stopped
- Route all writes through one owner task
- Never fire-and-forget append() in event handlers
When it happens
Trigger: Calling append() after close() returned; calling append() again without awaiting the previous append() promise; calling append() while a rotation write is in progress.
Common situations: Fire-and-forget append calls in an event loop without awaiting; a shutdown path closing the recorder while producers still emit buckets; missing async/await on the prior append.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- Failed to steer turn
- Message dispatch belongs to a previous session
- Runtime is shutting down; execution admission is closed
- Steer receipt task ended before acknowledgement
- Wait for the pet recorder write before closing.
AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15).
Data as JSON: /api/errors/c8722437fb16e5e3.
Report an issue: GitHub.
Appendix: source
Thrown at pet/scripts/lib/pet-recorder.mjs:99
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);
}
}
expectedMtime ??= (await output.stat({ bigint: true })).mtimeNs;
} catch (error) { try { await output?.close(); } finally { lock.close(); } throw error; }
return {
async append(bucket) {
if (!output || busy) throw new Error('Pet recorder is closed or already writing.');
validatePetBucket(bucket);
busy = true;
try {
await lock.check();
const held = await output.stat({ bigint: true }), current = await lstat(path, { bigint: true });
if (!current.isFile() || held.dev !== current.dev || held.ino !== current.ino || held.size !== BigInt(bytes) || held.mtimeNs !== expectedMtime)
throw new Error('The live pet recording was changed or replaced externally; existing files were preserved.');
const encode = seq => encodePetJSONL([{ ...bucket, sequence: seq, simTimeMs: seq * PET_BIN_MS }]);
let row = encode(sequence), size = Buffer.byteLength(row);
if (restart || sequence >= maxBuckets || bytes + size > maxBytes) {
row = encode(0); size = Buffer.byteLength(row);
if (size > maxBytes) throw new Error('Pet bucket exceeds the recording segment byte limit.');
const temporary = resolve(dirname(path), `.${basename(path)}.next-${randomUUID()}`);
const archive = `${path}.segment-${String(segment + 1).padStart(6, '0')}.jsonl`;
let next, installed = false, created = false;
try {
next = await open(temporary, 'wx', 0o600); created = true;
await next.writeFile(row); await next.sync();View on GitHub (pinned to 433685b202)