affaan-m/ECC · error · Error

Memory destination changed while it was being created.

Error message

Memory destination changed while it was being created.

What it means

writeCreateOnlyTextFile creates the destination with O_EXCL semantics and re-verifies, after opening, that the path still refers to the exact same regular file it opened (same identity, not a symlink, not replaced). If anything about the destination changed between stat and the post-open check, it throws this error to prevent writing through a swapped path (TOCTOU / symlink attack defense).

Solutions

  1. Re-run the save operation; transient races usually resolve on retry.
  2. Ensure no other process/tool is writing to or reorganizing the memory vault directory concurrently.
  3. Verify no symlink exists at the destination path; remove it and retry.
  4. If EEXIST-like races are common, serialize writes through a single process or use file locking.

Example fix

// before
saveMemory({ id: 'note-1', ... }); // concurrent worker also creating note-1 -> race
// after
// serialize writes per memory id
await withLock(`memory:note-1`, () => saveMemory({ id: 'note-1', ... }));
Defensive patterns

Strategy: retry

Validate before calling

const st = fs.lstatSync(destPath);
if (st.isSymbolicLink()) throw new Error('Destination is a symlink; aborting save.');

Type guard

const isSafeRegularFile = (p) => { try { const st = fs.lstatSync(p); return st.isFile() && !st.isSymbolicLink(); } catch { return true; } }; // true = path absent, safe to create

Try / catch

try {
  saveMemory(memory);
} catch (err) {
  if (err.message.includes('changed while it was being created')) {
    // transient race: wait briefly and retry once; investigate if persistent
  } else throw err;
}

Prevention

When it happens

Trigger: saveMemory or ensureProjectScopeIgnored attempts to create a memory file and, between path creation and the identity check, the destination is replaced, deleted and recreated, or turned into a symbolic link by another process.

Common situations: Concurrent writers (two agents saving the same memory id simultaneously), build/sync tools touching the vault directory, or an attacker planting a symlink at the destination path.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16). Data as JSON: /api/errors/c04bb392613e3975. Report an issue: GitHub.

Appendix: source

Thrown at scripts/lib/memory-vault.js:215

  const flags = fs.constants.O_WRONLY
    | fs.constants.O_CREAT
    | fs.constants.O_EXCL
    | (fs.constants.O_NOFOLLOW || 0);
  let descriptor;
  let operationError;
  let cleanupError;
  try {
    descriptor = fs.openSync(temporaryPath, flags, 0o600);
    const opened = fs.fstatSync(descriptor, { bigint: true });
    const after = fs.lstatSync(temporaryPath, { bigint: true });
    assertWithinTrustedRoot(temporaryPath, trustedRoot, 'write memory');
    if (
      !opened.isFile()
      || after.isSymbolicLink()
      || !after.isFile()
      || !sameFileIdentity(after, opened)
    ) {
      throw new Error('Memory destination changed while it was being created.');
    }
    fs.writeFileSync(descriptor, content, 'utf8');
    fs.fsyncSync(descriptor);
    fs.closeSync(descriptor);
    descriptor = undefined;
    assertWithinTrustedRoot(filePath, trustedRoot, 'write memory');
    fs.linkSync(temporaryPath, filePath);
  } catch (error) {
    operationError = error;
  } finally {
    if (descriptor !== undefined) {
      try {
        fs.closeSync(descriptor);
      } catch (error) {
        cleanupError = error;
      }
    }
    try {

View on GitHub (pinned to 8321021c54)