{"record":{"id":"1b696c6619bce3c4","repo":"Hmbown/CodeWhale","slug":"invalid-pet-recorder-lock-existing-files-were-preserved","errorCode":null,"errorMessage":"Invalid pet recorder lock; existing files were preserved.","messagePattern":"Invalid pet recorder lock; existing files were preserved\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pet/scripts/lib/pet-recorder.mjs","lineNumber":28,"sourceCode":"  let directory;\n  try { directory = await open(path, 'r'); await directory.sync(); }\n  catch (error) {\n    // Node cannot open/sync directory handles on Windows. File data is still\n    // synced before its atomic replacement on that platform.\n    if (process.platform !== 'win32' || !['EPERM', 'EISDIR', 'EINVAL', 'ENOTSUP'].includes(error.code)) throw error;\n  } finally { await directory?.close(); }\n}\n\n// SQLite's OS lock is released even after process death. This empty sidecar\n// contains no events or recorder state; keep its pathname so later processes\n// coordinate on the same inode. No PID files or stale-lock deletion are needed.\nasync function lockRecorder(path) {\n  const name = `${path}.writer-lock`;\n  try { const created = await open(name, 'wx', 0o600); await created.close(); }\n  catch (error) { if (error.code !== 'EEXIST') throw error; }\n  const identity = await lstat(name, { bigint: true });\n  if (!identity.isFile() || identity.nlink !== 1n || identity.size !== 0n)\n    throw new Error('Invalid pet recorder lock; existing files were preserved.');\n  let database;\n  const check = async () => {\n    const current = await lstat(name, { bigint: true });\n    if (!current.isFile() || current.nlink !== 1n || current.size !== 0n\n      || current.dev !== identity.dev || current.ino !== identity.ino)\n      throw new Error('The pet recorder lock was replaced; existing files were preserved.');\n  };\n  try {\n    database = new DatabaseSync(name);\n    database.exec('PRAGMA busy_timeout = 0; BEGIN EXCLUSIVE');\n    await check();\n    return { check, close: () => { database.close(); } };\n  } catch (error) {\n    database?.close();\n    if (error.errcode === 5 || error.errcode === 6) throw new Error('Another pet recorder is using this output.');\n    throw error;\n  }\n}","sourceCodeStart":10,"sourceCodeEnd":46,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/pet/scripts/lib/pet-recorder.mjs#L10-L46","documentation":"lockRecorder creates an exclusive `<path>.writer-lock` file with open(..., 'wx') and then verifies with lstat that what exists is really a regular file, hard-link count 1, and size 0. If the existing lock file fails that identity check (it was left behind by a crashed writer that wrote data to it, is hard-linked elsewhere, or is a symlink/special file), this error is thrown so the caller's existing output files are never clobbered. It is a stale-lock hygiene guard, not a concurrency error.","triggerScenarios":"A `<path>.writer-lock` file already exists at lock time AND is not a plain empty file with nlink==1: a previous run wrote bytes into the lock file, the file was hard-linked, or it is a symlink/device node that 'wx' happened not to create (e.g. pre-existing).","commonSituations":"A crashed recorder left a corrupted or non-empty lock file; a user manually created or copied the lock path; a backup/restore tool hard-linked output files including the lock; running on a filesystem where the lock path pre-exists from an older format.","solutions":["Inspect `<path>.writer-lock`: if it is a stale leftover from a dead process, delete it with rm and rerun.","Never reuse the lock file for data; if your tooling writes to it, point that tooling elsewhere.","Check for hard links (ls -l link count) and remove the extra links or the file.","If it is a symlink, remove it and investigate what created it before retrying."],"exampleFix":"// before: blind delete then rerun\nrm -f output.db.writer-lock && node recorder.js\n// after: verify it is stale (owning pid gone / not linked) before deleting\nls -li output.db.writer-lock   # check nlink, size\n[ -s output.db.writer-lock ] && cat output.db.writer-lock  # inspect contents\nrm -- output.db.writer-lock    # only after confirming no live recorder holds it","handlingStrategy":"try-catch","validationCode":"import { lstat } from 'node:fs/promises';\n// caller-side stale lock pre-check\ntry {\n  const st = await lstat(path + '.writer-lock', { bigint: true });\n  if (st.isFile() && st.nlink === 1n && st.size === 0n) throw new Error('live-looking lock present; verify holder before proceeding');\n  // otherwise inspect/delete the anomalous lock before calling lockRecorder\n} catch (e) { if (e.code !== 'ENOENT') throw e; }","typeGuard":null,"tryCatchPattern":"try {\n  const lock = await lockRecorder(path);\n} catch (e) {\n  if (e.message.includes('Invalid pet recorder lock')) {\n    // anomalous leftover lock — confirm no live writer, then remove and retry once\n    await confirmNoLiveWriter(path);\n    await rm(path + '.writer-lock');\n    return lockRecorder(path);\n  }\n  throw e;\n}","preventionTips":["Never write data into the .writer-lock file; it must stay empty.","After a crash, inspect and clean the lock before restarting the recorder.","Exclude *.writer-lock from tools that copy/hard-link output directories."],"tags":["filesystem","locking","stale-lock"],"backgroundTag":"file-already-exists","analyzedSha":"433685b2024e7bc4c99e1e2e326bcad39b4d9d65","analyzedAt":"2026-09-15T12:24:24.634Z","contentChangedAt":"2026-09-15T12:24:24.634Z","schemaVersion":2},"datasetVersion":"2026-09-22T01:17:13.364Z"}