mastra-ai/mastra · warning · StaleFileError

ESTALE

ESTALE

Error message

File was modified externally: ${path} (expected mtime ${expectedMtime.toISOString()}, actual ${actualMtime.toISOString()})

What it means

writeFile throws StaleFileError (code ESTALE) when options.expectedMtime is provided and the file's current modification time differs from the expected value. This is optimistic concurrency control: it detects that the file was modified by someone else between your read and your write, preventing silent lost updates. If the file does not exist, no conflict is possible and the write proceeds.

Source

Thrown at packages/core/src/workspace/filesystem/local-filesystem.ts:448

          throw new DirectoryNotFoundError(parentPath);
        }
        throw error;
      }
    }

    if (options?.recursive !== false) {
      const dir = nodePath.dirname(absolutePath);
      await fs.mkdir(dir, { recursive: true });
    }

    // Optimistic concurrency: reject if file was modified since caller last read it
    if (options?.expectedMtime) {
      try {
        const currentStat = await fs.stat(absolutePath);
        // Compare via Date objects — Node's stats.mtime applies internal
        // rounding that can diverge from Math.floor(stats.mtimeMs).
        if (currentStat.mtime.getTime() !== options.expectedMtime.getTime()) {
          throw new StaleFileError(inputPath, options.expectedMtime, currentStat.mtime);
        }
      } catch (error: unknown) {
        if (error instanceof StaleFileError) throw error;
        // File doesn't exist yet — no conflict possible, proceed with write
        if (!isEnoentError(error)) throw error;
      }
    }

    // Use 'wx' flag for atomic overwrite check (avoids TOCTOU race)
    const writeFlag = options?.overwrite === false ? 'wx' : 'w';
    try {
      await fs.writeFile(absolutePath, this.toBuffer(content), { flag: writeFlag });
    } catch (error: unknown) {
      if (options?.overwrite === false && isEexistError(error)) {
        throw new FileExistsError(inputPath);
      }
      throw error;
    }

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Re-read the file (getting fresh content and mtime via stat), merge/re-apply your change, and retry the write with the new expectedMtime.
  2. If last-writer-wins is acceptable, omit expectedMtime entirely.
  3. Use the file-write-lock utilities (file-write-lock.ts) or an external lock to serialize writers.
  4. Catch StaleFileError and surface a conflict so the calling agent can re-base instead of overwriting.

Example fix

// before
await fs.writeFile('state.json', next, { expectedMtime: oldMtime }); // StaleFileError if changed
// after
let success = false;
while (!success) {
  const { content, mtime } = await readWithMtime('state.json');
  try {
    await fs.writeFile('state.json', merge(content, next), { expectedMtime: mtime });
    success = true;
  } catch (e) {
    if (!(e instanceof StaleFileError)) throw e;
  }
}
Defensive patterns

Strategy: retry

Validate before calling

const s = await ws.stat(p);
// use s.modifiedAt (fresh) as expectedMtime immediately before writing;
// if the file changed since your read, re-read and merge first

Type guard

import { StaleFileError } from '@mastra/core/workspace/errors';
function isStaleFileError(e: unknown): e is StaleFileError {
  return e instanceof StaleFileError ||
    (e instanceof Error && 'code' in e && (e as { code?: string }).code === 'ESTALE');
}

Try / catch

for (let attempt = 0; attempt < 3; attempt++) {
  const s = await ws.stat(p);
  const fresh = await ws.readFile(p, { encoding: 'utf8' });
  try {
    await ws.writeFile(p, merge(fresh, myChange), { expectedMtime: s.modifiedAt });
    break;
  } catch (e) {
    if (!isStaleFileError(e) || attempt === 2) throw e;
  }
}

Prevention

When it happens

Trigger: Two agents/processes edit the same file concurrently; writeFile carries an expectedMtime captured from an earlier stat/read but another writer committed first; clock/filesystem rounding causes mtime mismatch on filesystems with coarse timestamps; caller reuses a stale mtime from a cached FileStat.

Common situations: Multi-agent workflows where several agents write shared state files; a human editor saved the file while an agent run was in flight; long-running job re-tries a write with an old expectedMtime after the file legitimately changed.

Related errors


AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30). Data as JSON: /api/errors/f802f3d748c236ac. Report an issue: GitHub.