{"record":{"id":"8ab2c7b5b91a6206","repo":"Hmbown/CodeWhale","slug":"the-previous-pet-recording-has-an-incomplete-final-row-it","errorCode":null,"errorMessage":"The previous pet recording has an incomplete final row; it was preserved.","messagePattern":"The previous pet recording has an incomplete final row; it was preserved\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pet/scripts/lib/pet-recorder.mjs","lineNumber":77,"sourceCode":"      if (!resume || error.code !== 'EEXIST') throw error;\n      const original = await lstat(path, { bigint: true });\n      if (!original.isFile() || original.size > 64n * 1024n * 1024n)\n        throw new Error('The previous pet recording is not a bounded regular file; it was preserved.');\n      output = await open(path, constants.O_RDWR | constants.O_APPEND | constants.O_NOFOLLOW | constants.O_NONBLOCK);\n      const held = await output.stat({ bigint: true });\n      if (held.dev !== original.dev || held.ino !== original.ino || held.size !== original.size)\n        throw new Error('The previous pet recording changed while opening; it was preserved.');\n      // Read at most the size already checked, including a single growth byte.\n      const contents = Buffer.alloc(Number(held.size) + 1);\n      let length = 0;\n      while (length < contents.length) {\n        const { bytesRead } = await output.read(contents, length, contents.length - length, length);\n        if (!bytesRead) break;\n        length += bytesRead;\n      }\n      if (length !== Number(held.size)) throw new Error('The previous pet recording changed while reading; it was preserved.');\n      const text = new TextDecoder('utf-8', { fatal: true }).decode(contents.subarray(0, length));\n      if (text && !text.endsWith('\\n')) throw new Error('The previous pet recording has an incomplete final row; it was preserved.');\n      decodePetJSONL(text);\n      const unchanged = await output.stat({ bigint: true });\n      if (unchanged.size !== original.size || unchanged.mtimeNs !== original.mtimeNs)\n        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;","sourceCodeStart":59,"sourceCodeEnd":95,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/pet/scripts/lib/pet-recorder.mjs#L59-L95","documentation":"After decoding the resumed bytes as strict UTF-8, the library requires that any non-empty content ends with a newline, meaning the last JSONL row is complete. A file whose final line is truncated (crash or kill mid-write of the previous run) throws this error and preserves the recording instead of resuming onto a partial row that would corrupt the replayable-segment guarantee.","triggerScenarios":"createPetRecorder(path, { resume: true }) on a file whose last write was cut off mid-row: previous process killed with SIGKILL mid-write, disk-full truncation, or power loss leaving a partial JSON line without a trailing \\n.","commonSituations":"Resuming after a hard crash of the recording CLI; an earlier run terminated by OOM/timeout while serializing a large event; manually edited or piped output missing the final newline.","solutions":["Repair the file by appending a newline only if the partial tail is safe to discard, or truncate the incomplete final row so the file ends with \\n, then resume.","Keep the corrupt recording as an archive copy and start a new recording at a fresh path instead of resuming.","Prevent recurrence by ensuring the previous writer exits cleanly (flush and close) and is not SIGKILLed mid-write; the recorder's bounded segments make full-segment loss cheaper than tail corruption."],"exampleFix":"// before\nawait createPetRecorder(path, { resume: true }); // throws: incomplete final row\n// after\nlet text = await readFile(path, 'utf8');\nif (text.length && !text.endsWith('\\n')) {\n  text = text.slice(0, text.lastIndexOf('\\n') + 1);\n  await writeFile(path, text); // drop the partial row\n}\nawait createPetRecorder(path, { resume: true });","handlingStrategy":"fallback","validationCode":"import { readFile, writeFile } from 'node:fs/promises';\nasync function tailIsComplete(p) {\n  let text = await readFile(p, 'utf8').catch(e => e.code === 'ENOENT' ? '' : Promise.reject(e));\n  return !text || text.endsWith('\\n');\n}\nasync function repairTail(p) {\n  let text = await readFile(p, 'utf8');\n  if (text && !text.endsWith('\\n')) await writeFile(p, text.slice(0, text.lastIndexOf('\\n') + 1));\n}","typeGuard":null,"tryCatchPattern":"try { await createPetRecorder(p, { resume: true }); } catch (e) { if (/incomplete final row/.test(e.message)) { await archiveCopy(p); await truncateIncompleteTail(p); return createPetRecorder(p, { resume: true }); } throw e; }","preventionTips":["Terminate recorder processes with graceful shutdown (SIGINT/SIGTERM + flush/close), not SIGKILL, to keep rows complete.","Before resuming after a crash, check the file ends with \\n and truncate any partial tail row.","Validate the tail JSON line parses with decodePetJSONL-equivalent logic before appending to a crashed recording."],"tags":["data-integrity","jsonl","resume","crash-recovery"],"backgroundTag":"incomplete-final-row","analyzedSha":"433685b2024e7bc4c99e1e2e326bcad39b4d9d65","analyzedAt":"2026-09-15T12:24:24.634Z","contentChangedAt":"2026-09-15T12:24:24.634Z","schemaVersion":2},"datasetVersion":"2026-09-23T02:17:17.105Z"}