{"record":{"id":"6810abd0da67c4ba","repo":"can1357/oh-my-pi","slug":"failed-to-replace-session-file-after-eperm-origin","errorCode":null,"errorMessage":"Failed to replace session file after EPERM (original: ${toError(renameError).message}; retry: ${toError(replaceError).message}; rollback: ${rollbackError.message})","messagePattern":"Failed to replace session file after EPERM \\(original: (.+?); retry: (.+?); rollback: (.+?)\\)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"critical","filePath":"packages/coding-agent/src/session/session-storage.ts","lineNumber":401,"sourceCode":"\t\t\t\tthis.renameSync(backupPath, targetPath);\n\t\t\t} catch (restoreErr) {\n\t\t\t\tlogger.warn(\"Failed to restore backup after commitGuard rejection\", {\n\t\t\t\t\tsessionFile: targetPath,\n\t\t\t\t\tbackupPath,\n\t\t\t\t\terror: toError(restoreErr).message,\n\t\t\t\t});\n\t\t\t}\n\t\t\tthis.#discardTemp(tempPath, targetPath);\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tthis.renameSync(tempPath, targetPath);\n\t\t} catch (replaceError) {\n\t\t\ttry {\n\t\t\t\tthis.renameSync(backupPath, targetPath);\n\t\t\t} catch (rollbackErr) {\n\t\t\t\tconst rollbackError = toError(rollbackErr);\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Failed to replace session file after EPERM (original: ${toError(renameError).message}; retry: ${\n\t\t\t\t\t\ttoError(replaceError).message\n\t\t\t\t\t}; rollback: ${rollbackError.message})`,\n\t\t\t\t\t{ cause: toError(renameError) },\n\t\t\t\t);\n\t\t\t}\n\t\t\tthrow toError(replaceError);\n\t\t}\n\t\ttry {\n\t\t\tfs.unlinkSync(backupPath);\n\t\t} catch (err) {\n\t\t\tif (!isEnoent(err)) {\n\t\t\t\tlogger.warn(\"Failed to remove session rewrite backup\", {\n\t\t\t\t\tsessionFile: targetPath,\n\t\t\t\t\tbackupPath,\n\t\t\t\t\terror: toError(err).message,\n\t\t\t\t});\n\t\t\t}","sourceCodeStart":383,"sourceCodeEnd":419,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/session/session-storage.ts#L383-L419","documentation":"writeTextAtomic replaces the session file via a temp-file rename. When the initial rename fails with EPERM (seen on some platforms when the target is held open, e.g. by antivirus/indexers or another process), the code retries and, on failure, attempts to roll back by renaming a backup over the target. If the rollback also fails, this Error is thrown combining all three messages, leaving the replacement incomplete — the original Error is attached as cause.","triggerScenarios":"renameSync of the temp file fails (typically EPERM/EACCES because the target file is locked by another process, a sync client, or Windows AV), the retry rename also fails, and restoring the backup over the target fails too (e.g. backup already moved or target locked).","commonSituations":"Windows file locking by antivirus/Defender or Dropbox/OneDrive syncing the session directory; two omp processes running against the same session file; read-only or permission-restricted session directory after a permission change.","solutions":["Close other processes holding the session file (second omp instance, editors, sync clients) and retry the operation.","Exclude the session directory from antivirus/real-time scanning or pause file-sync software (OneDrive/Dropbox) for ~/.omp.","Check directory permissions: ensure the user can create/rename/delete files in the session directory.","Inspect the error's cause and message trio to determine which stage failed; restore from the .bak/backup file if present, or recreate the session from the last valid JSONL."],"exampleFix":"// before: operation fails while Dropbox syncs the session dir\nawait storage.writeTextAtomic(path, text);\n// after: pause sync client or relocate sessions\nconst dir = path.join(os.homedir(), \".omp\", \"sessions\"); // ensure not inside a synced folder\nawait storage.writeTextAtomic(path, text);","handlingStrategy":"retry","validationCode":"import { access, constants } from \"node:fs/promises\";\nawait access(sessionDir, constants.W_OK); // throws early if dir not writable\n// also ensure no second omp instance holds the file:\n// check for a lock/pid file in the session dir before writing","typeGuard":"function isAtomicReplaceFailure(err: unknown): err is Error & { cause: unknown } {\n  return err instanceof Error &&\n    err.message.startsWith(\"Failed to replace session file after EPERM\");\n}","tryCatchPattern":"try {\n  await storage.writeTextAtomic(target, text);\n} catch (err) {\n  if (isAtomicReplaceFailure(err)) {\n    logger.error(\"Atomic session replace failed (lock/AV/sync client?)\", { cause: err.cause });\n    // look for leftover backup file to restore, then retry after unlocking\n  } else throw err;\n}","preventionTips":["Run only one process against a given session file.","Exclude the session directory from antivirus real-time scanning and file-sync tools (Dropbox/OneDrive).","Keep the session directory on a local writable filesystem, not a synced/network folder.","On Windows, prefer storing sessions outside user-synced profile folders."],"tags":["filesystem","atomic-write","windows","file-locked","session"],"backgroundTag":"atomic-replace-failed","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}