diff --git a/gitnexus/src/core/shared-store-analyze.ts b/gitnexus/src/core/shared-store-analyze.ts index 1817d72ae..646f5bd86 100644 --- a/gitnexus/src/core/shared-store-analyze.ts +++ b/gitnexus/src/core/shared-store-analyze.ts @@ -370,6 +370,9 @@ export const publishSharedGraph = async ( meta.lastCommit === currentCommit && !meta.incrementalInProgress && builtClean && + // Embeddings still owed stay with this checkout, which finishes them; a + // commit graph never changes, so a shared copy would stay short for good. + !meta.embeddingCheckpoint && // A sparse or partial checkout builds a graph missing the files it hides. isWorkingTreePristine(repoPath); // Every pointer change and the reclaim that follows run under one publish @@ -379,7 +382,23 @@ export const publishSharedGraph = async ( const target = commitGraphDir(layout, currentCommit, featureKeyOf(meta)); const targetGraph = path.join(target, LBUG_DIRECTORY); let published = await exists(targetGraph); - if (published) { + // A commit graph published before embedding checkpoints were kept + // private may hold fewer embeddings than this checkout's own graph. + // Keep the better private graph; the commit graph stays as it is, since + // other checkouts may read it. + const targetMeta = published ? await loadMeta(target) : null; + const ownIsBetter = + published && + (!targetMeta || + !!targetMeta.embeddingCheckpoint || + (targetMeta.stats?.embeddings ?? 0) < (meta.stats?.embeddings ?? 0)) && + (await exists(own)); + if (ownIsBetter) { + published = false; + log( + `Shared store: commit graph ${currentCommit.slice(0, 12)} is less complete; keeping the private graph.`, + ); + } else if (published) { try { await wipeLbugDbFiles(own); } catch (err) { diff --git a/gitnexus/test/integration/shared-store-analyze.test.ts b/gitnexus/test/integration/shared-store-analyze.test.ts index 4ef69c003..c3c8fbcd0 100644 --- a/gitnexus/test/integration/shared-store-analyze.test.ts +++ b/gitnexus/test/integration/shared-store-analyze.test.ts @@ -390,4 +390,63 @@ describe('publishSharedGraph race (#3352)', () => { expect((await loadMeta(slot))?.graphPath).toBe(shared); expect(existsSync(path.join(slot, 'lbug'))).toBe(false); }); + + const checkpoint: NonNullable = { + at: '2026-01-01T00:00:00.000Z', + nodesProcessed: 1, + totalNodes: 2, + chunksProcessed: 1, + model: 'm', + dimensions: 4, + provider: 'local', + kind: 'partial', + pendingNodeIds: ['n2'], + }; + + // #3374: a graph with embeddings still owed would become every checkout's + // graph, and its checkpoint-free copy would look complete forever. + it('keeps a graph with pending embeddings private', async () => { + const { checkouts, head } = await setup(); + const [main] = checkouts; + const slot = layoutOf(main).checkoutSlot; + const meta = (await loadMeta(slot)) as RepoMeta; + await saveMeta(slot, { ...meta, embeddingCheckpoint: checkpoint }); + await publishSharedGraph(layoutOf(main), main, head, () => {}); + expect(await listCommitDirs(layoutOf(main))).toEqual([]); + expect(existsSync(path.join(slot, 'lbug'))).toBe(true); + expect((await loadMeta(slot))?.embeddingCheckpoint).toEqual(checkpoint); + }); + + // A commit graph published before that rule may still be the weaker one; + // the checkout keeps its own graph rather than trading down to it. + it.each<[string, Partial]>([ + ['records pending embeddings', { embeddingCheckpoint: checkpoint, stats: { embeddings: 5 } }], + ['has fewer embeddings', { stats: { embeddings: 2 } }], + ])('keeps the private graph when the published one %s', async (_label, targetDelta) => { + const { checkouts, head } = await setup(); + const [main, wt] = checkouts; + const layout = layoutOf(main); + const slot = layout.checkoutSlot; + const own: RepoMeta = { ...((await loadMeta(slot)) as RepoMeta), stats: { embeddings: 5 } }; + await saveMeta(slot, own); + const target = commitGraphDir(layout, head, featureKeyOf(own)); + await fs.mkdir(target, { recursive: true }); + await fs.writeFile(path.join(target, 'lbug'), 'older published graph'); + const targetMeta: Partial = { + lastCommit: head, + indexedAt: own.indexedAt, + ...targetDelta, + }; + await fs.writeFile(path.join(target, 'gitnexus.json'), JSON.stringify(targetMeta)); + // Another checkout reads the published graph. + const wtSlot = layoutOf(wt).checkoutSlot; + await fs.rm(path.join(wtSlot, 'lbug')); + const wtMeta = (await loadMeta(wtSlot)) as RepoMeta; + await saveMeta(wtSlot, { ...wtMeta, graphPath: path.join(target, 'lbug') }); + await publishSharedGraph(layout, main, head, () => {}); + expect(await fs.readFile(path.join(slot, 'lbug'), 'utf-8')).toBe(`graph from ${main}`); + expect((await loadMeta(slot))?.graphPath).toBeUndefined(); + // The published graph is immutable: other checkouts may point at it. + expect(await fs.readFile(path.join(target, 'lbug'), 'utf-8')).toBe('older published graph'); + }); });