From 2bb5b41e60ee06b1227f52993435291e690c4adb Mon Sep 17 00:00:00 2001 From: prateek18-ai <2102508783@svyasa-sas.edu.in> Date: Sun, 30 Aug 2026 21:58:26 +0530 Subject: [PATCH] fix(brain): compact sync log on installs with no brain-sync peers (#5439) - Compact to getCurrentSeq() in syncAllPeers() when no brain-sync peers are enabled, keeping the last entry so initSyncLog() recovers the sequence counter across restarts - Add early return in brainSyncLog.compactLog() when dropped is 0 to avoid rewriting the log file on idle cycles - Add unit and regression tests in syncOrchestrator.test.js and brainSyncLog.test.js --- server/services/brainSyncLog.js | 2 ++ server/services/brainSyncLog.test.js | 41 ++++++++++++++++++++++++ server/services/syncOrchestrator.js | 5 +++ server/services/syncOrchestrator.test.js | 33 +++++++++++++++++++ 4 files changed, 81 insertions(+) diff --git a/server/services/brainSyncLog.js b/server/services/brainSyncLog.js index e3a00ed9f1..82016ce900 100644 --- a/server/services/brainSyncLog.js +++ b/server/services/brainSyncLog.js @@ -261,6 +261,8 @@ export async function compactLog(minSeq) { kept.push({ line, seq: entry.seq }); } + if (dropped === 0) return 0; + const newContent = kept.length > 0 ? kept.map(k => k.line).join('\n') + '\n' : ''; await atomicWrite(SYNC_LOG_FILE, newContent); diff --git a/server/services/brainSyncLog.test.js b/server/services/brainSyncLog.test.js index 20df7df6c3..c7cbfd877c 100644 --- a/server/services/brainSyncLog.test.js +++ b/server/services/brainSyncLog.test.js @@ -434,5 +434,46 @@ describe('brainSyncLog', () => { expect(lastReadStart()).toBe(lineOffset(2)); expect(result.changes.map(c => c.seq)).toEqual([5]); }); + + it('keeps exactly the newest entry when compactLog(currentSeq) is called and recovers seq across restarts (#5439)', async () => { + for (let i = 0; i < 5; i++) { + await appendChange('create', 'people', `p${i}`, { name: `Person ${i}` }, 'inst-1'); + } + expect(getCurrentSeq()).toBe(5); + + // Compact keeping only seq >= 5 + const dropped = await compactLog(getCurrentSeq()); + expect(dropped).toBe(4); + + // Verify file on disk has only seq 5 + const lines = readFileSync(syncLogPath(), 'utf8').trim().split('\n'); + expect(lines).toHaveLength(1); + const parsed = JSON.parse(lines[0]); + expect(parsed.seq).toBe(5); + expect(parsed.id).toBe('p4'); + + // Simulate restart: re-initialize the log + await initSyncLog(); + expect(getCurrentSeq()).toBe(5); + + // Subsequent appends continue monotonic sequence numbers + const nextEntry = await appendChange('create', 'people', 'p5', { name: 'Person 5' }, 'inst-1'); + expect(nextEntry.seq).toBe(6); + expect(getCurrentSeq()).toBe(6); + }); + + it('returns 0 and does not rewrite file when no entries are dropped (#5439)', async () => { + for (let i = 0; i < 3; i++) { + await appendChange('create', 'people', `p${i}`, { name: `Person ${i}` }, 'inst-1'); + } + const beforeContent = readFileSync(syncLogPath(), 'utf8'); + + // Calling compactLog with minSeq <= 1 drops nothing + const dropped = await compactLog(1); + expect(dropped).toBe(0); + + const afterContent = readFileSync(syncLogPath(), 'utf8'); + expect(afterContent).toBe(beforeContent); + }); }); }); diff --git a/server/services/syncOrchestrator.js b/server/services/syncOrchestrator.js index 5c6f2dcf1a..eb3def54bc 100644 --- a/server/services/syncOrchestrator.js +++ b/server/services/syncOrchestrator.js @@ -874,6 +874,11 @@ export async function syncAllPeers() { }); const minSeq = Math.min(...consumedSeqs); await brainSyncLog.compactLog(minSeq); + } 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()); } } diff --git a/server/services/syncOrchestrator.test.js b/server/services/syncOrchestrator.test.js index 3cce6b4c14..8a6907d493 100644 --- a/server/services/syncOrchestrator.test.js +++ b/server/services/syncOrchestrator.test.js @@ -847,6 +847,39 @@ describe('syncOrchestrator', () => { expect(compactLog).toHaveBeenCalledWith(0); }); + + it('compacts to getCurrentSeq() when peers are configured but none is brain-enabled (#5439)', async () => { + const memoryOnlyPeer = { + ...mockPeer, + instanceId: 'A', + syncCategories: { brain: false, memory: true } + }; + getPeers.mockResolvedValue([memoryOnlyPeer]); + readJSONFile.mockImplementation(async () => ({})); + + await syncAllPeers(); + + expect(compactLog).toHaveBeenCalledWith(200); + }); + + it('compacts to getCurrentSeq() when no peers are configured (#5439)', async () => { + getPeers.mockResolvedValue([]); + readJSONFile.mockImplementation(async () => ({})); + + await syncAllPeers(); + + expect(compactLog).toHaveBeenCalledWith(200); + }); + + it('does not call compactLog when no brain peers exist and getCurrentSeq() is 0 (#5439)', async () => { + getCurrentSeq.mockReturnValue(0); + getPeers.mockResolvedValue([]); + readJSONFile.mockImplementation(async () => ({})); + + await syncAllPeers(); + + expect(compactLog).not.toHaveBeenCalled(); + }); }); describe('initSyncOrchestrator', () => {