From 1dd351b765f31debacaff7a7d5166a85b64447c2 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Mon, 13 Jul 2026 19:38:43 -0700 Subject: [PATCH] fix(queue): widen rag-index fan-out to match the regate-sweep candidate set MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fanOutRagIndexJobs still filtered candidates to isRegistered repos unioned with the LOOPOVER_REVIEW_REPOS allowlist, unlike fanOutAgentRegateSweepJobs which unions ALL listRepositories(). An installed-but-never-registered, non-allowlisted repo (the brokered self-host case) was never a fan-out candidate, so a per-repo features.rag override could never resurface it — the repo was still reviewed via the regate sweep, just without codebase- context retrieval. Drop the isRegistered filter so the candidate set matches the regate sweep exactly; convergedFeatureActive still gates the actual indexing spend, so this only widens eligibility. Closes #5024 --- src/queue/processors.ts | 25 +++++++++++++------------ test/unit/rag-index.test.ts | 21 +++++++++++++++++++++ 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/src/queue/processors.ts b/src/queue/processors.ts index ce8cedd011..6abcc2b534 100644 --- a/src/queue/processors.ts +++ b/src/queue/processors.ts @@ -1159,24 +1159,25 @@ export async function runRagIndexJob( await indexRepo(env, project, repo); } -// Enqueue one per-repo FULL re-index job for every registered + cutover-allowlisted repo (mirrors the -// signal-snapshot / agent-regate fan-out: a delayed per-repo queue message so each repo's index runs as its own -// bounded, retryable job rather than one giant tick). Only allowlisted repos are indexed — retrieval is gated the -// same way, so indexing a non-converged repo would only burn the free-tier vector budget for no benefit. +// Enqueue one per-repo FULL re-index job for every RAG-active repo, candidates drawn from ALL known repos plus the +// cutover allowlist (mirrors the agent-regate fan-out: a delayed per-repo queue message so each repo's index runs +// as its own bounded, retryable job rather than one giant tick). Only RAG-active repos are indexed — retrieval is +// gated the same way, so indexing a non-converged repo would only burn the free-tier vector budget for no benefit. async function fanOutRagIndexJobs( env: Env, requestedBy: "schedule" | "api" | "webhook" | "test", ): Promise { - // Candidate repos = the webhook-REGISTERED repos UNION the maintainer's CONFIGURED repos (LOOPOVER_REVIEW_REPOS). - // The union is the fix for the brokered self-host: a maintainer's repos are is_registered=0 (never went through the - // registration webhook), so a registered-only fan-out never indexed them — leaving reviews without codebase context. - // Deduped case-insensitively (a repo can be both registered AND configured). Each is then filtered by whether RAG is - // active for it (`features.rag` override → LOOPOVER_REVIEW_REPOS allowlist default), so nothing extra is indexed. + // Candidate repos = ALL known repos UNION the maintainer's CONFIGURED repos (LOOPOVER_REVIEW_REPOS) — mirrors + // fanOutAgentRegateSweepJobs's own candidate set exactly (#5024). Filtering to isRegistered-only left an + // installed-but-never-registered repo (the brokered self-host case: is_registered=0, never went through the + // registration webhook) out of the candidate pool entirely, so even a per-repo `features.rag` override could never + // resurface it — the regate sweep still reviewed that repo, just without codebase-context retrieval. Deduped + // case-insensitively (a repo can be both known AND configured). Each candidate is then filtered by whether RAG is + // active for it (`features.rag` override → LOOPOVER_REVIEW_REPOS allowlist default) just below, so this widens + // ELIGIBILITY only — the convergedFeatureActive gate below is what actually controls indexing spend. const repositoriesByKey = new Map((await listRepositories(env)).map((repo) => [repo.fullName.toLowerCase(), repo])); const byKey = new Map(); - for (const repo of [...repositoriesByKey.values()].filter( - (r) => r.isRegistered, - )) + for (const repo of repositoriesByKey.values()) byKey.set(repo.fullName.toLowerCase(), { fullName: repo.fullName, ...(typeof repo.installationId === "number" ? { installationId: repo.installationId } : {}) }); for (const fullName of listConvergenceRepos(env)) { const repo = repositoriesByKey.get(fullName.toLowerCase()); diff --git a/test/unit/rag-index.test.ts b/test/unit/rag-index.test.ts index 4c376e1962..72fee852a9 100644 --- a/test/unit/rag-index.test.ts +++ b/test/unit/rag-index.test.ts @@ -906,6 +906,27 @@ describe("rag-index-repo job dispatch (processors.ts wiring)", () => { expect(sent.filter((m) => (m as { repoFullName?: string }).repoFullName === "JSONbored/gittensory").length).toBe(1); }); + it("cron fan-out includes an INSTALLED, non-registered, non-allowlisted repo once a features.rag override opts it in (#5024)", async () => { + const sent: import("../../src/types").JobMessage[] = []; + const env = createTestEnv({ + LOOPOVER_REVIEW_RAG: "true", + LOOPOVER_REVIEW_REPOS: "", // empty allowlist — this repo must be discoverable without it + JOBS: { async send(message: import("../../src/types").JobMessage) { sent.push(message); } } as unknown as Queue, + }); + // Installed via the GitHub App (installationId set ⇒ is_installed=1) but never went through the registration + // webhook — is_registered stays 0 (registerRepo() is deliberately NOT called), and it's not in the allowlist + // either. Before this fix, fanOutRagIndexJobs's candidate pool was isRegistered-repos UNION the static + // allowlist, so this repo was never even a candidate — the per-repo features.rag override below could never + // resurface it, unlike fanOutAgentRegateSweepJobs's candidate set (ALL listRepositories()), which the regate + // sweep already covers this exact repo through. + await upsertRepositoryFromGitHub(env, { name: "widgets", full_name: "owner/widgets", private: false, owner: { login: "owner" } }, 456); + await upsertRepoFocusManifest(env, "owner/widgets", { features: { rag: true } }); + + await processJob(env, { type: "rag-index-repo", requestedBy: "schedule" }); + + expect(sent).toEqual([{ type: "rag-index-repo", requestedBy: "schedule", repoFullName: "owner/widgets", installationId: 456 }]); + }); + it("FLAG-OFF cron fan-out is a no-op (no per-repo jobs enqueued, no fan-out audit)", async () => { const sent: import("../../src/types").JobMessage[] = []; const env = createTestEnv({