Skip to content

bug(journal): concurrent appends are not atomic — corrupted under parallel builds #996

@carlos-alm

Description

@carlos-alm

Summary

appendJournalEntries() uses fs.appendFileSync with no locking:

// src/domain/graph/journal.ts:62-83
export function appendJournalEntries(
  rootDir: string,
  entries: Array<{ file: string; deleted?: boolean }>,
): void {
  // ...
  fs.appendFileSync(journalPath, `${lines.join('\n')}\n`);
}

The per-line write is not a transactional boundary. If two writers append concurrently — for example a watcher session (watcher.ts:96-119 appends after each debounced batch) plus a manual codegraph build in a second shell — their lines can interleave inside the .codegraph/changes.journal file. Possible outcomes:

  • Truncated DELETED prefix on one writer's line, causing a path to be parsed as a changed file.
  • Duplicate or partial entries.
  • A \n-less tail if one process exits mid-write.

Related: advisory "lock" is purely informational

acquireAdvisoryLock() in src/db/connection.ts:112-130 warns if another PID is live and proceeds anyway. It does not serialize file operations outside the SQLite WAL critical section. Journal/snapshot writes get no coverage.

Suggested fix

Serialize journal mutations with a real lock. proper-lockfile is already a lightweight option; a fs.openSync(path, 'wx')-based lockfile with PID + heartbeat also works and matches the current style.

Why it matters in practice

  • Watch mode + parallel worktrees is the documented workflow (CLAUDE.md "Parallel Sessions" section).
  • A corrupted journal header causes readJournal to return valid: false (line 25-28, 31-34), which silently falls through to Tier 1/2 hash scan — a silent performance regression, not a crash.

File refs

  • src/domain/graph/journal.ts:62-83 — append (no lock)
  • src/domain/graph/journal.ts:85-105 — writeJournalHeader (tmp+rename, but not coordinated with appends)
  • src/db/connection.ts:112-130 — advisory lock (warn-only)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions