CherryHQ/cherry-studio · error · Error

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

Error message

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

What it means

resolveFileCI() resolves a filename inside the agent memory directory, preferring an exact case match. After lstat succeeds on the exact path, it verifies the entry is a regular file and NOT a symlink. This is a security invariant: agent memory files (FACT.md, JOURNAL.jsonl) must be real files to prevent symlink-based path traversal or replacement attacks.

Source

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

import type { Tool } from '@modelcontextprotocol/sdk/types.js'
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError } from '@modelcontextprotocol/sdk/types.js'

const logger = loggerService.withContext('McpServer:AgentMemory')

function withNoFollow(flags: number): number {
  return isWin ? flags : flags | constants.O_NOFOLLOW
}

/**
 * Resolve a filename within a directory using case-insensitive matching.
 * Returns the full path if found (preferring exact match), or the canonical path as fallback.
 */
async function resolveFileCI(dir: string, name: string): Promise<string> {
  const exact = path.join(dir, name)
  try {
    const fileStat = await lstat(exact)
    if (!fileStat.isFile() || fileStat.isSymbolicLink()) {
      throw new Error(`Agent memory file must be a real file: ${exact}`)
    }
    return exact
  } catch (err) {
    if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err
    // exact match not found, try case-insensitive
  }

  try {
    const entries = await readdir(dir)
    const target = name.toLowerCase()
    const match = entries.find((e) => e.toLowerCase() === target)
    if (!match) return exact
    const matchedPath = path.join(dir, match)
    const fileStat = await lstat(matchedPath)
    if (!fileStat.isFile() || fileStat.isSymbolicLink()) {
      throw new Error(`Agent memory file must be a real file: ${matchedPath}`)
    }
    return matchedPath

View on GitHub (pinned to 726446b54c)

Solutions

  1. Inspect the offending path with ls -la <agentDataPath>/memory/ and remove or replace the non-regular entry.
  2. If a symlink is intentional, replace it with a real file copy — symlinks are deliberately rejected here.
  3. Restore FACT.md / JOURNAL.jsonl from a backup as regular files.

Example fix

# before: FACT.md is a symlink
ls -la memory/FACT.md  # lrwxrwxrwx ... FACT.md -> /tmp/evil

# after: replace with a real file
rm memory/FACT.md && touch memory/FACT.md
Defensive patterns

Strategy: validation

Validate before calling

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

async function isSafeRegularFile(p: string): Promise<boolean> {
  try {
    const s = await lstat(p)
    return s.isFile() && !s.isSymbolicLink()
  } catch {
    return false
  }
}
// Before resolving a memory file path:
if (await isSafeRegularFile(path.join(memoryDir, 'FACT.md'))) { /* safe to proceed */ }

Type guard

function isRegularFile(stat: import('node:fs').Stats): boolean {
  return stat.isFile() && !stat.isSymbolicLink()
}

Prevention

When it happens

Trigger: Calling any memory tool action when a non-regular file (directory, symlink, FIFO, device node) exists at <agentDataPath>/memory/FACT.md or memory/JOURNAL.jsonl with the exact casing. lstat succeeds but isFile() is false or isSymbolicLink() is true.

Common situations: A user or another process created a symlink at FACT.md pointing elsewhere; the memory directory was corrupted by a partial filesystem restore; a directory named FACT.md was accidentally created; an editor or sync tool (Dropbox, OneDrive) replaced the file with a symlink.

Related errors


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