From 3487e4b255e4dbabf688497b7259d84db302abe8 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Wed, 22 Jul 2026 10:59:36 -0700 Subject: [PATCH] =?UTF-8?q?fix(embed):=20thread=20write=20column=20into=20?= =?UTF-8?q?sumStaleChunkChars=20=E2=80=94=20sync=20cost=20gate=20followed?= =?UTF-8?q?=20the=20legacy=20predicate=20(#1262)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review follow-up: the PR threaded the write-side column through countStaleChunks/listStaleChunks but not sumStaleChunkChars, so the sync cost gate counted an alt-column brain's fully-embedded corpus as phantom backlog on every gate (inflating the --full estimate and the deferred-mode backlog note). Both engines already share buildStaleChunkWhere, so this is a type widening + one call-site thread + a contrast assertion. Co-Authored-By: Claude Fable 5 --- src/commands/sync.ts | 9 ++++++++- src/core/engine.ts | 7 ++++++- src/core/pglite-engine.ts | 2 +- src/core/postgres-engine.ts | 2 +- test/e2e/embedding-column-pglite.test.ts | 3 +++ 5 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/commands/sync.ts b/src/commands/sync.ts index 55177421b..ed4448d23 100644 --- a/src/commands/sync.ts +++ b/src/commands/sync.ts @@ -576,9 +576,16 @@ async function runInlineCostGate( // Stale backlog: cheap single SQL; fail-open to 0 so a transient DB hiccup // never blocks the sync. Signature-aware (model/dims swap surfaces here). + // #1262: follow the write-side embedding column — otherwise an alt-column + // brain's fully-embedded corpus counts as phantom backlog on every gate. let staleChars = 0; try { - staleChars = await engine.sumStaleChunkChars({ signature: currentEmbeddingSignature() }); + const { resolveWriteColumnForEngine } = await import('../core/search/embedding-column.ts'); + const embeddingColumn = await resolveWriteColumnForEngine(engine); + staleChars = await engine.sumStaleChunkChars({ + signature: currentEmbeddingSignature(), + ...(embeddingColumn && { embeddingColumn }), + }); } catch { staleChars = 0; } diff --git a/src/core/engine.ts b/src/core/engine.ts index 3cbf6299d..c1ed2be76 100644 --- a/src/core/engine.ts +++ b/src/core/engine.ts @@ -1031,8 +1031,13 @@ export interface BrainEngine { * model signature (a model/dims swap). NULL signature is GRANDFATHERED * (never counted) so the post-migration corpus isn't flagged en masse. * Omit `signature` for the legacy `embedding IS NULL`-only count. + * + * `opts.embeddingColumn` switches the staleness predicate to the resolved + * write-side column (#1262) — same contract as countStaleChunks — so the + * sync cost gate doesn't count an alt-column brain's fully-embedded corpus + * as phantom backlog. */ - sumStaleChunkChars(opts?: { sourceId?: string; signature?: string }): Promise; + sumStaleChunkChars(opts?: { sourceId?: string; signature?: string; embeddingColumn?: ResolvedColumn }): Promise; /** * Stamp `pages.embedding_signature = signature` for one page. Called after * a page's chunks are (re)embedded so a later model swap can detect it as diff --git a/src/core/pglite-engine.ts b/src/core/pglite-engine.ts index b662d7d0e..566ddf9e8 100644 --- a/src/core/pglite-engine.ts +++ b/src/core/pglite-engine.ts @@ -2424,7 +2424,7 @@ export class PGLiteEngine implements BrainEngine { return Number(count); } - async sumStaleChunkChars(opts?: { sourceId?: string; signature?: string }): Promise { + async sumStaleChunkChars(opts?: { sourceId?: string; signature?: string; embeddingColumn?: ResolvedColumn }): Promise { // Sibling of countStaleChunks: same stale predicate, summing chunk_text // length for the sync cost preview. ::bigint guards int4 overflow. const { where, params } = this.buildStaleChunkWhere(opts); diff --git a/src/core/postgres-engine.ts b/src/core/postgres-engine.ts index 59fc70174..363e0bba2 100644 --- a/src/core/postgres-engine.ts +++ b/src/core/postgres-engine.ts @@ -2579,7 +2579,7 @@ export class PostgresEngine implements BrainEngine { }); } - async sumStaleChunkChars(opts?: { sourceId?: string; signature?: string }): Promise { + async sumStaleChunkChars(opts?: { sourceId?: string; signature?: string; embeddingColumn?: ResolvedColumn }): Promise { // Sibling of countStaleChunks: same stale predicate, summing chunk_text // length for the sync cost preview. ::bigint guards int4 overflow. const { where, params } = this.buildStaleChunkWhere(opts); diff --git a/test/e2e/embedding-column-pglite.test.ts b/test/e2e/embedding-column-pglite.test.ts index 7dac210ee..d5fb6a65a 100644 --- a/test/e2e/embedding-column-pglite.test.ts +++ b/test/e2e/embedding-column-pglite.test.ts @@ -355,6 +355,9 @@ describe('PGLite: embed --stale converges on an alt-column brain (#1262)', () => // the alt-column predicate does not. expect(await local.countStaleChunks()).toBe(1); expect(await local.countStaleChunks({ embeddingColumn: descriptor })).toBe(0); + // sumStaleChunkChars feeds the sync cost gate — same predicate contract. + expect(await local.sumStaleChunkChars()).toBeGreaterThan(0); + expect(await local.sumStaleChunkChars({ embeddingColumn: descriptor })).toBe(0); expect(await local.listStaleChunks({ embeddingColumn: descriptor, batchSize: 100 })).toHaveLength(0); expect(await local.listStaleChunks({ batchSize: 100 })).toHaveLength(1);