CherryHQ/cherry-studio · error · Error

Agent journal must be a regular file: ${journalPath}

Error message

Agent journal must be a regular file: ${journalPath}

What it means

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.

Source

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

    const memoryDir = await this.assertMemoryDirectory()
    const journalPath = await resolveFileCI(memoryDir, 'JOURNAL.jsonl')
    await this.assertRegularFileOrMissing(journalPath)

    const entry: JournalEntry = {
      ts: new Date().toISOString(),
      tags,
      text
    }

    const handle = await open(
      journalPath,
      withNoFollow(constants.O_APPEND | constants.O_CREAT | constants.O_WRONLY),
      0o600
    )
    try {
      const fileStat = await handle.stat()
      if (!fileStat.isFile()) throw new Error(`Agent journal must be a regular file: ${journalPath}`)
      await handle.appendFile(JSON.stringify(entry) + '\n', 'utf-8')
    } finally {
      await handle.close()
    }

    logger.info('Journal entry appended via tool', { agentId: this.agentId, tags })
    return {
      content: [{ type: 'text' as const, text: `Journal entry added at ${entry.ts}.` }]
    }
  }

  private async memorySearch(args: Record<string, string | undefined>) {
    const query = args.query ?? ''
    const tagFilter = args.tag ?? ''
    const limit = Math.max(1, parseInt(args.limit ?? '20', 10) || 20)

    const memoryDir = await this.assertMemoryDirectory()
    const journalPath = await resolveFileCI(memoryDir, 'JOURNAL.jsonl')

View on GitHub (pinned to 726446b54c)

Solutions

  1. Identify the special file: ls -la <agentDataPath>/memory/JOURNAL.jsonl.
  2. Remove it: rm memory/JOURNAL.jsonl (the append will recreate it via O_CREAT).
  3. Audit for the process that created the non-regular file.

Example fix

# before: JOURNAL.jsonl is a FIFO
ls -la memory/JOURNAL.jsonl  # prw-r--r--

# after
rm memory/JOURNAL.jsonl
# re-run append; O_CREAT recreates a regular file
Defensive patterns

Strategy: try-catch

Validate before calling

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

async function isRegularFile(p: string): Promise<boolean> {
  try {
    const s = await lstat(p)
    return s.isFile() && !s.isSymbolicLink()
  } catch {
    return false
  }
}
// Minimize the TOCTOU window: assert immediately before open.
if (!(await isRegularFile(journalPath)) && !(await fileExists(journalPath))) {
  throw new Error('Journal path is occupied by a non-regular file')
}

Try / catch

try {
  await memoryAppend(args)
} catch (err) {
  if (err instanceof Error && err.message.startsWith('Agent journal must be a regular file')) {
    // Cleanup the offending entry and retry once
    await unlink(journalPath).catch(() => {})
    return memoryAppend(args)
  }
  throw err
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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