{"record":{"id":"1137a7df40a82611","repo":"Hmbown/CodeWhale","slug":"the-live-pet-recording-was-replaced-externally-after","errorCode":null,"errorMessage":"The live pet recording was replaced externally after rotation.","messagePattern":"The live pet recording was replaced externally after rotation\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"pet/scripts/lib/pet-recorder.mjs","lineNumber":149,"sourceCode":"                || destination.size !== held.size || destination.mtimeNs !== held.mtimeNs)\n                throw new Error('The live pet recording changed while rotating; existing files were preserved.');\n              try { await rename(temporary, path); break; }\n              catch (error) {\n                // A reader or file scanner can briefly deny replacement on\n                // Windows. Retry for under two seconds, checking identity each\n                // time; persistent denial still stops without deleting history.\n                if (process.platform !== 'win32' || !['EPERM', 'EBUSY'].includes(error.code) || attempt >= 20) throw error;\n                await delay(Math.min(100, (attempt + 1) * 25));\n              }\n            }\n            installed = true;\n            // The first row is already published. Account for it before any\n            // fallible cleanup/report so a later append sees the actual file.\n            sequence = 1; bytes = size; segment++; restart = false;\n            output = await open(path, constants.O_WRONLY | constants.O_APPEND);\n            const reopened = await output.stat({ bigint: true });\n            if (reopened.dev !== nextIdentity.dev || reopened.ino !== nextIdentity.ino)\n              throw new Error('The live pet recording was replaced externally after rotation.');\n            expectedMtime = reopened.mtimeNs;\n            await syncDirectory(dirname(path));\n            report(`Archived pet recording: ${archive}`);\n          } finally {\n            await next?.close();\n            if (created && !installed) await unlink(temporary);\n          }\n          return;\n        } else {\n          await output.writeFile(row);\n          expectedMtime = (await output.stat({ bigint: true })).mtimeNs;\n        }\n        bytes += size; sequence++;\n      } finally { busy = false; }\n    },\n    async close() {\n      if (busy) throw new Error('Wait for the pet recorder write before closing.');\n      const current = output, heldLock = lock; output = undefined; lock = undefined;","sourceCodeStart":131,"sourceCodeEnd":167,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/pet/scripts/lib/pet-recorder.mjs#L131-L167","documentation":"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.","triggerScenarios":"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.","commonSituations":"Two processes starting recorders on the same path simultaneously; IDE atomic-save plugins reacting to the directory; deployment tooling managing the same directory.","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."],"exampleFix":"// before: two recorders, same path\nconst r1 = await createPetRecorder(p); const r2 = await createPetRecorder(p);\n// after\nconst recorder = await createPetRecorder(p); // single owner only","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  await recorder.append(bucket);\n} catch (e) {\n  if (e.message.includes('replaced externally after rotation')) {\n    // archived segment exists; inspect dir, decide keep-or-restart, then re-create recorder\n  } else throw e;\n}","preventionTips":["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"],"tags":["concurrency","race-condition","rotation"],"backgroundTag":"file-write-failed","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"}