{"record":{"id":"8e7bb04f9ea17b7f","repo":"CherryHQ/cherry-studio","slug":"agent-memory-file-must-be-a-real-file-matchedpa","errorCode":null,"errorMessage":"Agent memory file must be a real file: ${matchedPath}","messagePattern":"Agent memory file must be a real file: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/main/ai/mcp/servers/agentMemory.ts","lineNumber":45,"sourceCode":"    const fileStat = await lstat(exact)\n    if (!fileStat.isFile() || fileStat.isSymbolicLink()) {\n      throw new Error(`Agent memory file must be a real file: ${exact}`)\n    }\n    return exact\n  } catch (err) {\n    if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err\n    // exact match not found, try case-insensitive\n  }\n\n  try {\n    const entries = await readdir(dir)\n    const target = name.toLowerCase()\n    const match = entries.find((e) => e.toLowerCase() === target)\n    if (!match) return exact\n    const matchedPath = path.join(dir, match)\n    const fileStat = await lstat(matchedPath)\n    if (!fileStat.isFile() || fileStat.isSymbolicLink()) {\n      throw new Error(`Agent memory file must be a real file: ${matchedPath}`)\n    }\n    return matchedPath\n  } catch (err) {\n    if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {\n      logger.warn('Unexpected error reading directory', { dir, error: (err as Error).message })\n    }\n    return exact\n  }\n}\n\ntype JournalEntry = {\n  ts: string\n  tags: string[]\n  text: string\n}\n\nconst MEMORY_TOOL: Tool = {\n  name: 'memory',","sourceCodeStart":27,"sourceCodeEnd":63,"githubUrl":"https://github.com/CherryHQ/cherry-studio/blob/726446b54cd69ffe51a276638672f6d95ca0768c/src/main/ai/mcp/servers/agentMemory.ts#L27-L63","documentation":"The second branch of resolveFileCI(): when no exact-case match exists, it scans the directory case-insensitively. If a case-variant match (e.g. 'fact.md' when looking for 'FACT.md') is found but that entry is a symlink or non-regular file, this throws. Same security invariant as the exact-match branch, applied to the fallback resolution.","triggerScenarios":"The exact filename is absent, but a case-different variant exists and is a symlink/directory/special file. Happens on case-insensitive filesystems (macOS APFS, Windows NTFS) where the OS may report a differently-cased entry, or when a user manually created 'fact.md' as a symlink.","commonSituations":"macOS users whose filesystem folds case; a symlink left from a migration; a directory accidentally named with wrong case; cross-platform sync creating case variants.","solutions":["List the memory directory and identify the case-variant entry: ls -la <agentDataPath>/memory/.","Remove the offending case-variant symlink/non-file and create FACT.md or JOURNAL.jsonl as a regular file with the correct case.","On case-insensitive filesystems, move the file to a temp name first to force the correct case."],"exampleFix":"# before: fact.md (lowercase) is a symlink\nls memory/  # fact.md -> /elsewhere\n\n# after\nrm memory/fact.md\nprintf '# Facts\n' > memory/FACT.md","handlingStrategy":"validation","validationCode":"import { lstat, readdir } from 'node:fs/promises'\nimport path from 'node:path'\n\nasync function safeResolveCi(dir: string, name: string): Promise<string | null> {\n  const target = name.toLowerCase()\n  const entries = await readdir(dir).catch(() => [])\n  const match = entries.find((e) => e.toLowerCase() === target)\n  if (!match) return null\n  const p = path.join(dir, match)\n  const s = await lstat(p)\n  return s.isFile() && !s.isSymbolicLink() ? p : null\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["On case-insensitive filesystems (macOS/Windows), always create memory files with the canonical case (FACT.md, JOURNAL.jsonl).","Avoid symlinks anywhere under the agent data directory.","When porting a workspace across OSes, normalize file casing."],"tags":["security","filesystem","symlink-guard","case-insensitive","agent-memory"],"backgroundTag":null,"analyzedSha":"726446b54cd69ffe51a276638672f6d95ca0768c","analyzedAt":"2026-08-12T17:30:37.448Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}