Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 13 additions & 12 deletions src/queue/processors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
// 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<string, { fullName: string; installationId?: number }>();
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());
Expand Down
21 changes: 21 additions & 0 deletions test/unit/rag-index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down