Skip to content

Brain sync log never compacts on an install with no brain-sync peers #5439

Description

@atomantic

Problem

syncAllPeers() (server/services/syncOrchestrator.js:829-877) compacts the brain sync log only inside if (brainPeers.length > 0) (:870-876). On an install with no enabled brain-sync peer — the default, since federation is opt-in — compactLog is never called, so every create / update / upsertWithId / remove on all ten brain types (brainStorage.js:458, 491, 540, 592, 638, 684, 725) appends a full-record entry to data/brain/sync_log.jsonl that nothing will ever read.

The guard was added in 5217deb (#1077) to avoid Math.min() over an empty array (Infinity), not as a decision to keep an unread log forever — the comment beside it is about a reporting-less peer floor of 0, which is a different case.

Trigger

Any single-machine install writing brain records. The heaviest producer is the Daily Log: DailyLogTab.jsx:20-21 autosaves on a 1.5 s debounce / 10 s max-wait, and each PUT → setJournalContentupsertWithId appends the day's entire content and segments (which duplicate the text, brainJournal.js:361-366) as one log line. An hour of journaling adds hundreds of multi-KB entries; a ChatGPT import adds ~1,400 entries of up to 9.8 KB each. None of it is ever pulled.

Impact

  • sync_log.jsonl grows without bound and lives under data/, so it rides every backup.
  • initSyncLog() (server/services/brainSyncLog.js:27-46) readFiles the whole file into one string and splits it just to read the last line — boot latency and a 2–3× file-size memory spike scale with the user's entire brain edit history.
  • The first peer ever added pulls that whole history 100 entries at a time (syncOrchestrator.js:172), each page re-parsing the full file (see the companion read-path issue), even though the Brain sync is delta-log-only — diverged peers never re-converge (no anti-entropy) #1077 anti-entropy snapshot would converge it in one exchange.

Fix

In syncAllPeers, add an else branch to the brainPeers.length > 0 block:

} else if (brainSyncLog.getCurrentSeq() > 0) {
  // No brain-sync peer will ever pull these entries; a peer added later
  // converges through the reconcile snapshot (#1077). Keep the last entry so
  // initSyncLog still recovers the sequence counter across restarts.
  await brainSyncLog.compactLog(brainSyncLog.getCurrentSeq());
}

compactLog(minSeq) keeps entries with seq >= minSeq (brainSyncLog.js:147), so the newest entry survives and the counter never resets to 0 — which matters because a peer that was removed and later re-added still holds a cursorForYou.brainSeq > 0 and would otherwise skip new entries until reconcile caught it. The > 0 guard keeps a fresh install from rewriting an empty file every minute. compactLog already logs only when it runs; it is a no-op rewrite when nothing is below the floor, so add a cheap dropped === 0 early return inside compactLog (or check lines.length > 1 before rewriting) so an idle install doesn't rewrite the file every 60 s.

Rejected: skipping the appendChange entirely when there are no peers — changes the write contract at seven call sites and leaves a peer added later with no delta log at all. Rejected: compactLog(Infinity) / truncating to empty — resets the sequence counter on the next boot (see above).

Files: server/services/syncOrchestrator.js (~5 lines), server/services/brainSyncLog.js (no-op guard), server/services/syncOrchestrator.test.js, server/services/brainSyncLog.test.js.

Tests: in syncOrchestrator.test.js next to "floors at 0 when a brain-enabled peer has not reported its cursor into us" (~:838): (a) with peers configured but none brain-enabled, compactLog is called with getCurrentSeq(); (b) with getCurrentSeq() === 0, compactLog is not called; (c) the existing two floor tests stay green. In brainSyncLog.test.js: compactLog(currentSeq) keeps exactly the last line and a subsequent initSyncLog recovers that seq.

Acceptance criteria

  • On an install with no enabled brain-sync peer, sync_log.jsonl never holds more than the newest entry after a sync cycle.
  • After that compaction and a restart, getCurrentSeq() equals the pre-restart value (no reset to 0).
  • An idle install (no new entries since the last cycle) does not rewrite the file every cycle.
  • Existing peer-floor behavior (min over cursorForYou.brainSeq, 0 for an unreported peer) is unchanged; cd server && npm test green.

Metadata

Metadata

Labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions