CherryHQ/cherry-studio · error · Error

Agent memory file must be a real file: ${filePath}

Error message

Agent memory file must be a real file: ${filePath}

What it means

assertRegularFileOrMissing() is called before writing FACT.md or JOURNAL.jsonl. If the target exists and lstat shows it is not a regular file or is a symlink, it throws. This is a TOCTOU defense: it re-validates the file type immediately before the write, complementing the earlier resolveFileCI check.

Source

Thrown at src/main/ai/mcp/servers/agentMemory.ts:199

    }
    return assertedPath
  }

  private async assertMemoryDirectory(): Promise<string> {
    const agentDataPath = await this.getAgentDataPath()
    const memoryDir = path.join(agentDataPath, 'memory')
    const memoryStat = await lstat(memoryDir)
    if (!memoryStat.isDirectory() || memoryStat.isSymbolicLink()) {
      throw new Error(`Agent memory directory must be a real directory: ${memoryDir}`)
    }
    return memoryDir
  }

  private async assertRegularFileOrMissing(filePath: string): Promise<void> {
    try {
      const fileStat = await lstat(filePath)
      if (!fileStat.isFile() || fileStat.isSymbolicLink()) {
        throw new Error(`Agent memory file must be a real file: ${filePath}`)
      }
    } catch (error) {
      if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
    }
  }

  private async memoryUpdate(args: Record<string, string | undefined>) {
    const content = args.content
    if (!content) throw new McpError(ErrorCode.InvalidParams, "'content' is required for update action")

    const memoryDir = await this.assertMemoryDirectory()
    const factPath = await resolveFileCI(memoryDir, 'FACT.md')
    await this.assertRegularFileOrMissing(factPath)

    // Atomic write via temp file + rename
    const tmpPath = path.join(memoryDir, `.FACT.md.${randomUUID()}.tmp`)
    const handle = await open(tmpPath, 'wx', 0o600)
    try {

View on GitHub (pinned to 726446b54c)

Solutions

  1. Investigate what process replaced the file between resolution and write.
  2. Remove the offending non-regular entry so the write can recreate a normal file.
  3. Run the memory tool again after cleanup.

Example fix

# before: FACT.md swapped to symlink mid-flight
ls -la memory/FACT.md  # symlink

# after
rm memory/FACT.md
# re-run the update action; temp file + rename will recreate a regular file
Defensive patterns

Strategy: validation

Validate before calling

import { lstat } from 'node:fs/promises'

async function isRegularOrMissing(p: string): Promise<boolean> {
  try {
    const s = await lstat(p)
    return s.isFile() && !s.isSymbolicLink()
  } catch (err) {
    return (err as NodeJS.ErrnoException).code === 'ENOENT'
  }
}

Prevention

When it happens

Trigger: Between resolveFileCI and the write, the target file was replaced with a symlink, directory, or special file. The pre-write check at line 197-200 catches the swap.

Common situations: A race condition where another process swaps FACT.md for a symlink mid-operation; an adversarial local process attempting a TOCTOU attack; a sync tool creating symlinks during operation.

Related errors


AI-assisted analysis of CherryHQ/cherry-studio@726446b54c (2026-08-12). Data as JSON: /api/errors/426ec2fceea48bd7. Report an issue: GitHub.