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)
Summary
appendJournalEntries()usesfs.appendFileSyncwith no locking:The per-line write is not a transactional boundary. If two writers append concurrently — for example a watcher session (
watcher.ts:96-119appends after each debounced batch) plus a manualcodegraph buildin a second shell — their lines can interleave inside the.codegraph/changes.journalfile. Possible outcomes:DELETEDprefix on one writer's line, causing a path to be parsed as a changed file.\n-less tail if one process exits mid-write.Related: advisory "lock" is purely informational
acquireAdvisoryLock()insrc/db/connection.ts:112-130warns 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-lockfileis already a lightweight option; afs.openSync(path, 'wx')-based lockfile with PID + heartbeat also works and matches the current style.Why it matters in practice
readJournalto returnvalid: 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)