{"record":{"id":"c8722437fb16e5e3","repo":"Hmbown/CodeWhale","slug":"pet-recorder-is-closed-or-already-writing","errorCode":null,"errorMessage":"Pet recorder is closed or already writing.","messagePattern":"Pet recorder is closed or already writing\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"pet/scripts/lib/pet-recorder.mjs","lineNumber":99,"sourceCode":"        throw new Error('The previous pet recording changed while reading; it was preserved.');\n      bytes = length; restart = true; expectedMtime = original.mtimeNs;\n      // Continue archive numbering without collecting a growing directory list.\n      const prefix = `${basename(path)}.segment-`;\n      for await (const entry of await opendir(dirname(path))) {\n        if (!entry.name.startsWith(prefix)) continue;\n        const suffix = entry.name.slice(prefix.length);\n        if (!/^[0-9]{6,}\\.jsonl$/.test(suffix)) continue;\n        const number = Number(suffix.slice(0, -6));\n        if (!Number.isSafeInteger(number) || number >= Number.MAX_SAFE_INTEGER)\n          throw new Error('Pet archive numbering is exhausted; existing files were preserved.');\n        segment = Math.max(segment, number);\n      }\n    }\n    expectedMtime ??= (await output.stat({ bigint: true })).mtimeNs;\n  } catch (error) { try { await output?.close(); } finally { lock.close(); } throw error; }\n  return {\n    async append(bucket) {\n      if (!output || busy) throw new Error('Pet recorder is closed or already writing.');\n      validatePetBucket(bucket);\n      busy = true;\n      try {\n        await lock.check();\n        const held = await output.stat({ bigint: true }), current = await lstat(path, { bigint: true });\n        if (!current.isFile() || held.dev !== current.dev || held.ino !== current.ino || held.size !== BigInt(bytes) || held.mtimeNs !== expectedMtime)\n          throw new Error('The live pet recording was changed or replaced externally; existing files were preserved.');\n        const encode = seq => encodePetJSONL([{ ...bucket, sequence: seq, simTimeMs: seq * PET_BIN_MS }]);\n        let row = encode(sequence), size = Buffer.byteLength(row);\n        if (restart || sequence >= maxBuckets || bytes + size > maxBytes) {\n          row = encode(0); size = Buffer.byteLength(row);\n          if (size > maxBytes) throw new Error('Pet bucket exceeds the recording segment byte limit.');\n          const temporary = resolve(dirname(path), `.${basename(path)}.next-${randomUUID()}`);\n          const archive = `${path}.segment-${String(segment + 1).padStart(6, '0')}.jsonl`;\n          let next, installed = false, created = false;\n          try {\n            next = await open(temporary, 'wx', 0o600); created = true;\n            await next.writeFile(row); await next.sync();","sourceCodeStart":81,"sourceCodeEnd":117,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/pet/scripts/lib/pet-recorder.mjs#L81-L117","documentation":"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.","triggerScenarios":"Calling append() after close() returned; calling append() again without awaiting the previous append() promise; calling append() while a rotation write is in progress.","commonSituations":"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.","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."],"exampleFix":"// before\nrecorder.append(bucket); recorder.append(bucket2);\n// after\nawait recorder.append(bucket);\nawait recorder.append(bucket2);","handlingStrategy":"type-guard","validationCode":"function canAppend(recorder) { return recorder !== null && recorderClosed !== true; }","typeGuard":"function isWritable(rec) {\n  return rec && typeof rec.append === 'function' && !closed && !pendingWrite;\n}","tryCatchPattern":"try {\n  await recorder.append(bucket);\n} catch (e) {\n  if (e.message.includes('closed or already writing')) {\n    // serialize: wait for prior write or treat recorder as closed\n    await pendingWrite?.catch(() => {});\n  } else throw e;\n}","preventionTips":["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"],"tags":["async","lifecycle","concurrency"],"backgroundTag":"invalid-state-transition","analyzedSha":"433685b2024e7bc4c99e1e2e326bcad39b4d9d65","analyzedAt":"2026-09-15T12:24:24.634Z","contentChangedAt":"2026-09-15T12:24:24.634Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}