{"record":{"id":"6ddeca882f0fa6cf","repo":"CherryHQ/cherry-studio","slug":"agent-journal-must-be-a-regular-file-journalpat","errorCode":null,"errorMessage":"Agent journal must be a regular file: ${journalPath}","messagePattern":"Agent journal must be a regular file: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/main/ai/mcp/servers/agentMemory.ts","lineNumber":264,"sourceCode":"\n    const memoryDir = await this.assertMemoryDirectory()\n    const journalPath = await resolveFileCI(memoryDir, 'JOURNAL.jsonl')\n    await this.assertRegularFileOrMissing(journalPath)\n\n    const entry: JournalEntry = {\n      ts: new Date().toISOString(),\n      tags,\n      text\n    }\n\n    const handle = await open(\n      journalPath,\n      withNoFollow(constants.O_APPEND | constants.O_CREAT | constants.O_WRONLY),\n      0o600\n    )\n    try {\n      const fileStat = await handle.stat()\n      if (!fileStat.isFile()) throw new Error(`Agent journal must be a regular file: ${journalPath}`)\n      await handle.appendFile(JSON.stringify(entry) + '\\n', 'utf-8')\n    } finally {\n      await handle.close()\n    }\n\n    logger.info('Journal entry appended via tool', { agentId: this.agentId, tags })\n    return {\n      content: [{ type: 'text' as const, text: `Journal entry added at ${entry.ts}.` }]\n    }\n  }\n\n  private async memorySearch(args: Record<string, string | undefined>) {\n    const query = args.query ?? ''\n    const tagFilter = args.tag ?? ''\n    const limit = Math.max(1, parseInt(args.limit ?? '20', 10) || 20)\n\n    const memoryDir = await this.assertMemoryDirectory()\n    const journalPath = await resolveFileCI(memoryDir, 'JOURNAL.jsonl')","sourceCodeStart":246,"sourceCodeEnd":282,"githubUrl":"https://github.com/CherryHQ/cherry-studio/blob/726446b54cd69ffe51a276638672f6d95ca0768c/src/main/ai/mcp/servers/agentMemory.ts#L246-L282","documentation":"In memoryAppend(), after opening JOURNAL.jsonl with O_APPEND|O_CREAT|O_WRONLY (and O_NOFOLLOW on non-Windows), handle.stat() is checked. If the opened handle is somehow not a regular file (stat().isFile() false), it throws. This catches a swap where a FIFO, device, or non-regular file occupies the journal path between the pre-write assertRegularFileOrMissing check and the open.","triggerScenarios":"A FIFO, device node, or socket exists at JOURNAL.jsonl. O_NOFOLLOW already blocks symlinks (on non-Windows), so this catches the remaining non-regular file types. The TOCTOU window is between assertRegularFileOrMissing and open.","commonSituations":"An adversarial local process creates a named pipe at the journal path to hang or exploit the append; filesystem corruption placed a special file there; a misbehaving backup tool.","solutions":["Identify the special file: ls -la <agentDataPath>/memory/JOURNAL.jsonl.","Remove it: rm memory/JOURNAL.jsonl (the append will recreate it via O_CREAT).","Audit for the process that created the non-regular file."],"exampleFix":"# before: JOURNAL.jsonl is a FIFO\nls -la memory/JOURNAL.jsonl  # prw-r--r--\n\n# after\nrm memory/JOURNAL.jsonl\n# re-run append; O_CREAT recreates a regular file","handlingStrategy":"try-catch","validationCode":"import { lstat } from 'node:fs/promises'\n\nasync function isRegularFile(p: string): Promise<boolean> {\n  try {\n    const s = await lstat(p)\n    return s.isFile() && !s.isSymbolicLink()\n  } catch {\n    return false\n  }\n}\n// Minimize the TOCTOU window: assert immediately before open.\nif (!(await isRegularFile(journalPath)) && !(await fileExists(journalPath))) {\n  throw new Error('Journal path is occupied by a non-regular file')\n}","typeGuard":null,"tryCatchPattern":"try {\n  await memoryAppend(args)\n} catch (err) {\n  if (err instanceof Error && err.message.startsWith('Agent journal must be a regular file')) {\n    // Cleanup the offending entry and retry once\n    await unlink(journalPath).catch(() => {})\n    return memoryAppend(args)\n  }\n  throw err\n}","preventionTips":["Do not create FIFOs/devices inside agent data directories.","If you see this repeatedly, audit for a local process tampering with the journal path.","The O_NOFOLLOW guard already blocks symlinks on non-Windows; this check covers the rest."],"tags":["security","filesystem","toctou","agent-memory"],"backgroundTag":null,"analyzedSha":"726446b54cd69ffe51a276638672f6d95ca0768c","analyzedAt":"2026-08-12T17:30:37.448Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}