diff --git a/src/main/ai-vault-search/session-search-index-pass.test.ts b/src/main/ai-vault-search/session-search-index-pass.test.ts index bd138852e22..5f0fc01f88e 100644 --- a/src/main/ai-vault-search/session-search-index-pass.test.ts +++ b/src/main/ai-vault-search/session-search-index-pass.test.ts @@ -111,6 +111,22 @@ it('leaves what it ran out of time for owed, with nothing written down', async ( expect(store.files()).toHaveLength(2) }) +// A deferred candidate whose row already says `due` is in `stateCounts().due`, +// which the status adds `left` to; counting it here would report it twice. +it('leaves a deferred candidate out of the count when its row already says due', async () => { + await passOverAll() + for (const row of store.files()) { + store.setFileState(row.path, 'due') + } + + const cut = await runSessionSearchIndexPass(store, await candidates(), { + rows: rows(), + overdue: () => true + }) + + expect(cut).toMatchObject({ outOfTime: true, left: 0 }) +}) + // The deadline is never applied before the pass has read anything, so a single // transcript larger than one deadline is read alone rather than starved. it('reads one file even when the deadline has already expired', async () => { diff --git a/src/main/ai-vault-search/session-search-index-pass.ts b/src/main/ai-vault-search/session-search-index-pass.ts index 3ed11cf8c71..fc0ed0bff22 100644 --- a/src/main/ai-vault-search/session-search-index-pass.ts +++ b/src/main/ai-vault-search/session-search-index-pass.ts @@ -33,9 +33,11 @@ export type SessionSearchIndexPassOptions = { * drop. What the reads themselves leave behind is written by the index consumer * onto the rows. * - * `left` is what makes the backlog sayable: a candidate with no row yet is owed - * a read and counted by no query, so without this the status has no way to tell - * an index that holds everything from one that has barely started. + * `left` is what makes the backlog sayable: a candidate with no row yet, or one + * whose row does not say it is owed, is counted by no `due` query, so without + * this the status has no way to tell an index that holds everything from one + * that has barely started. Candidates whose row is already `due` are left out, + * because the status adds `left` to that same count. */ export async function runSessionSearchIndexPass( store: SessionSearchStore, @@ -67,7 +69,11 @@ export async function runSessionSearchIndexPass( // count of what a pass left is worth more than the microseconds. outOfTime ||= read > 0 && options.overdue?.() === true if (outOfTime) { - left += 1 + // A `due` row is already in `stateCounts().due`, which the status adds + // this to; counting it here would report the same file twice. + if (row?.state !== 'due') { + left += 1 + } continue } // The clock the deadline reads is one the owner may close behind: the read