# Files changed · PR \#20093 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/20093/diffs)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

[perf\(server\): the Pull Requests page no longer burns GitHub's GraphQL budget while agents run](https://git.cafe/pingdotgg/t3code/pull/20093.md)

Version: 11

Head commit: 3d7b2f06716ff8a392e195f0dfd9f723ddaafc49

Base commit: 70e5e384dab96a1d1e4a253ee5a9415ed5d8ce30

Comparison base: 70e5e384dab96a1d1e4a253ee5a9415ed5d8ce30

Showing up to 20 entries from this revision. If the pull request changes, reopen the first page before continuing.

## apps/server/src/git/refreshPushedPullRequests.ts

Status: modified; +3; −3

```diff
@@ -16,18 +16,18 @@
       const engine = yield* OrchestratorV2.OrchestratorV2;
       const thread = yield* engine.getThreadShell(input.threadId);
       if (thread !== null) {
-        yield* pullRequests.refreshAfterTurn(thread.projectId);
+        yield* pullRequests.refreshAfterTurn(thread.projectId, { immediate: true });
         return;
       }
     }
     if (input.projectId !== undefined) {
-      yield* pullRequests.refreshAfterTurn(input.projectId);
+      yield* pullRequests.refreshAfterTurn(input.projectId, { immediate: true });
       return;
     }
     const projects = yield* (yield* ProjectStore.ProjectStoreV2).listShells();
     yield* Effect.forEach(
       projects.filter((project) => project.workspaceRoot === input.cwd),
+      (project) => pullRequests.refreshAfterTurn(project.id, { immediate: true }),
-      (project) => pullRequests.refreshAfterTurn(project.id),
       { discard: true },
     );
   },
```

## apps/server/src/pullRequest/PullRequestService.test.ts

Status: modified; +290; −62

```diff
@@ -25,6 +25,9 @@
 import * as ServerSettingsService from "../serverSettings.ts";
 import { PullRequestOperationError } from "@t3tools/contracts";
 
+import { refreshPushedPullRequests } from "../git/refreshPushedPullRequests.ts";
+import * as OrchestratorV2 from "../orchestration-v2/Orchestrator.ts";
+import * as ProjectStore from "../orchestration-v2/ProjectStore.ts";
 import * as ProjectService from "../project/ProjectService.ts";
 import * as SqlitePersistence from "../persistence/Sqlite.ts";
 import * as PullRequestFilesViewed from "../persistence/PullRequestFilesViewed.ts";
@@ -1862,71 +1865,124 @@
   }),
 );
 
-it.effect.each(["list", "listStats"] as const)(
-  "retries a replacement %s read that joins a canceled lookup",
-  (operation) =>
-    Effect.gen(function* () {
-      const started = yield* Deferred.make<void>();
-      const stopping = yield* Deferred.make<void>();
-      const finishShutdown = yield* Deferred.make<void>();
-      let hostCalls = 0;
-      const lookup = Effect.gen(function* () {
-        if (++hostCalls !== 1) return;
-        yield* Deferred.succeed(started, undefined);
+it.effect("retries a replacement listStats read that joins a canceled lookup", () =>
+  Effect.gen(function* () {
+    const started = yield* Deferred.make<void>();
-        return yield* Effect.never.pipe(
-          Effect.onInterrupt(() =>
-            Deferred.succeed(stopping, undefined).pipe(
-              Effect.andThen(Deferred.await(finishShutdown)),
-            ),
+    const stopping = yield* Deferred.make<void>();
+    const finishShutdown = yield* Deferred.make<void>();
+    let hostCalls = 0;
+    const lookup = Effect.gen(function* () {
+      if (++hostCalls !== 1) return;
+      yield* Deferred.succeed(started, undefined);
+      return yield* Effect.never.pipe(
+        Effect.onInterrupt(() =>
+          Deferred.succeed(stopping, undefined).pipe(
+            Effect.andThen(Deferred.await(finishShutdown)),
           ),
-        );
-      });
-      const service = yield* makeService({
-        projects: [
-          project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" }),
-        ],
-        providers: [
-          fakeProvider(SourceControlProviderKind.make("github"), {
-            listChangeRequests: () =>
-              lookup.pipe(
-                Effect.as({
-                  items: [changeRequest(1, "2026-07-02T00:00:00Z")],
-                  truncated: false,
-                  continues: false,
-                }),
-              ),
-            listChangeRequestStats: () =>
-              lookup.pipe(
-                Effect.as([{ repository: "acme/web", number: 1, additions: 3, deletions: 1 }]),
-              ),
-          }),
-        ],
-      });
-      const read =
-        operation === "list"
-          ? service
-              .list({ state: "open" })
-              .pipe(Effect.map((result) => result.entries.map((entry) => entry.number)))
-          : service
-              .listStats({
-                refs: [{ projectId: "p1" as ProjectId, repository: "acme/web", number: 1 }],
-              })
-              .pipe(Effect.map((result) => result.stats.map((stat) => stat.number)));
-      const original = yield* Effect.forkScoped(read);
-      yield* Deferred.await(started);
-      const cancellation = yield* Effect.forkScoped(Fiber.interrupt(original));
-      yield* Deferred.await(stopping);
-      const replacement = yield* Effect.forkScoped(read, { startImmediately: true });
-      yield* Deferred.succeed(finishShutdown, undefined);
-      yield* Fiber.join(cancellation);
-      const canceled = yield* Fiber.await(original);
-      assert.isTrue(Exit.isFailure(canceled));
-      if (Exit.isFailure(canceled)) assert.isTrue(Cause.hasInterruptsOnly(canceled.cause));
-      assert.deepStrictEqual(yield* Fiber.join(replacement), [1]);
-      assert.strictEqual(hostCalls, 2);
+        ),
+      );
+    });
+    const service = yield* makeService({
+      projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })],
+      providers: [
+        fakeProvider(SourceControlProviderKind.make("github"), {
+          listChangeRequestStats: () =>
+            lookup.pipe(
-    }),
+              Effect.as([{ repository: "acme/web", number: 1, additions: 3, deletions: 1 }]),
+            ),
+        }),
+      ],
+    });
+    const read = service
+      .listStats({
+        refs: [{ projectId: "p1" as ProjectId, repository: "acme/web", number: 1 }],
+      })
+      .pipe(Effect.map((result) => result.stats.map((stat) => stat.number)));
+    const original = yield* Effect.forkScoped(read);
+    yield* Deferred.await(started);
+    const cancellation = yield* Effect.forkScoped(Fiber.interrupt(original));
+    yield* Deferred.await(stopping);
+    // Join the old entry while its lookup is still finishing cancellation.
+    const replacement = yield* Effect.forkScoped(read, { startImmediately: true });
+    yield* Deferred.succeed(finishShutdown, undefined);
+    yield* Fiber.join(cancellation);
+    const canceled = yield* Fiber.await(original);
+    assert.isTrue(Exit.isFailure(canceled));
+    if (Exit.isFailure(canceled)) assert.isTrue(Cause.hasInterruptsOnly(canceled.cause));
+    assert.deepStrictEqual(yield* Fiber.join(replacement), [1]);
+    assert.strictEqual(hostCalls, 2);
+  }),
 );
 
+it.effect("keeps a listing running for the next reader when its reader goes away", () =>
+  Effect.gen(function* () {
+    const started = yield* Deferred.make<void>();
+    const release = yield* Deferred.make<void>();
+    let hostCalls = 0;
+    const service = yield* makeService({
+      projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })],
+      providers: [
+        fakeProvider(SourceControlProviderKind.make("github"), {
+          listChangeRequests: () =>
+            Effect.gen(function* () {
+              hostCalls += 1;
+              yield* Deferred.succeed(started, undefined);
+              yield* Deferred.await(release);
+              return {
+                items: [changeRequest(1, "2026-07-02T00:00:00Z")],
+                truncated: false,
+                continues: false,
+              };
+            }),
+        }),
+      ],
+    });
+    const read = service
+      .list({ state: "open" })
+      .pipe(Effect.map((result) => result.entries.map((entry) => entry.number)));
+
+    const original = yield* Effect.forkChild(read, { startImmediately: true });
+    yield* Deferred.await(started);
+    yield* Fiber.interrupt(original);
+    const replacement = yield* Effect.forkChild(read, { startImmediately: true });
+    yield* Deferred.succeed(release, undefined);
+
+    assert.deepStrictEqual(yield* Fiber.join(replacement), [1]);
+    assert.strictEqual(hostCalls, 1);
+  }),
+);
+
+it.effect("reads a turn's own project listing fresh while other listings wait", () =>
+  Effect.gen(function* () {
+    const asked: string[] = [];
+    const service = yield* makeService({
+      projects: [
+        project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" }),
+        project({ id: "p2", title: "docs", workspaceRoot: "/b", repository: "acme/docs" }),
+      ],
+      providers: [
+        fakeProvider(SourceControlProviderKind.make("github"), {
+          listChangeRequests: ({ repository }) => {
+            asked.push(repository);
+            return Effect.succeed({ items: [], truncated: false, continues: false });
+          },
+        }),
+      ],
+    });
+    const readBoth = Effect.all([
+      service.list({ state: "open", projectId: "p1" as ProjectId }),
+      service.list({ state: "open", projectId: "p2" as ProjectId }),
+    ]);
+
+    yield* readBoth;
+    yield* service.refreshAfterTurn("p1" as ProjectId);
+    yield* readBoth;
+    yield* service.refreshAfterTurn("p2" as ProjectId);
+    yield* readBoth;
+    assert.deepStrictEqual(asked, ["acme/web", "acme/docs", "acme/web", "acme/docs", "acme/docs"]);
+  }),
+);
+
 it.effect("shares one cold viewer lookup across distinct concurrent lists", () =>
   Effect.gen(function* () {
     let viewerCalls = 0;
@@ -2083,6 +2139,97 @@
   }),
 );
 
+it.effect("coalesces turn refreshes for 30 seconds, then refreshes once the window ends", () =>
+  Effect.gen(function* () {
+    let hostCalls = 0;
+    const service = yield* makeService({
+      projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })],
+      providers: [
+        fakeProvider(SourceControlProviderKind.make("github"), {
+          listChangeRequests: () => {
+            hostCalls += 1;
+            return Effect.succeed({ items: [], truncated: false, continues: false });
+          },
+        }),
+      ],
+    });
+
+    yield* service.refreshAfterTurn("p1" as ProjectId);
+    yield* TestClock.adjust("10 seconds");
+    // Cached until t=40, so only the trailing refresh can make the t=30 read fresh.
+    // `projectIds` is the global page split across environments, so it waits too.
+    const readGlobal = Effect.all([
+      service.list({ state: "open" }),
+      service.list({ state: "open", projectIds: ["p1" as ProjectId] }),
+    ]);
+    yield* readGlobal;
+    yield* service.refreshAfterTurn("p1" as ProjectId);
+    yield* service.refreshAfterTurn("p3" as ProjectId);
+    yield* readGlobal;
+    assert.strictEqual(hostCalls, 2);
+
+    const trailing = yield* service.subscribeRefreshes.pipe(
+      Stream.drop(1),
+      Stream.runHead,
+      Effect.forkChild({ startImmediately: true }),
+    );
+    yield* TestClock.adjust("20 seconds");
+    yield* Fiber.join(trailing);
+    yield* readGlobal;
+    assert.strictEqual(hostCalls, 4);
+  }),
+);
+
+it.effect("pushes and explicit refreshes read fresh inside a turn window", () =>
+  Effect.gen(function* () {
+    let hostCalls = 0;
+    let pushed = false;
+    const web = project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" });
+    const service = yield* makeService({
+      projects: [web],
+      providers: [
+        fakeProvider(SourceControlProviderKind.make("github"), {
+          listChangeRequests: () => {
+            hostCalls += 1;
+            return Effect.succeed({
+              items: pushed ? [changeRequest(42, "2026-07-02T00:00:00Z")] : [],
+              truncated: false,
+              continues: false,
+            });
+          },
+        }),
+      ],
+    });
+
+    yield* service.refreshAfterTurn(web.id);
+    yield* TestClock.adjust("10 seconds");
+    yield* service.list({ state: "open" });
+    yield* service.invalidate({});
+    yield* service.list({ state: "open" });
+    assert.strictEqual(hostCalls, 2);
+
+    pushed = true;
+    yield* refreshPushedPullRequests(
+      { cwd: web.workspaceRoot, projectId: web.id },
+      { push: { status: "pushed" } },
+    ).pipe(
+      Effect.provide(
+        Layer.mergeAll(
+          Layer.succeed(PullRequestService.PullRequestService, service),
+          Layer.mock(OrchestratorV2.OrchestratorV2)({}),
+          Layer.mock(ProjectStore.ProjectStoreV2)({}),
+        ),
+      ),
+    );
+    const result = yield* service.list({ state: "open" });
+    assert.strictEqual(hostCalls, 3);
+    assert.deepStrictEqual(
+      result.entries.map(({ number }) => number),
+      [42],
+    );
+  }),
+);
+
 it.effect("close and reopen notify subscribed readers after invalidating their cached state", () =>
   Effect.scoped(
     Effect.gen(function* () {
@@ -2338,6 +2485,87 @@
     );
   }),
 );
+
+it.effect("trusts a covered silent repository's search for ten minutes", () =>
+  Effect.gen(function* () {
+    const separately: string[] = [];
+    let coverageAsks = 0;
+    const service = yield* makeService({
+      projects: [
+        project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" }),
+        project({ id: "p2", title: "docs", workspaceRoot: "/b", repository: "acme/docs" }),
+      ],
+      providers: [
+        fakeProvider(SourceControlProviderKind.make("github"), {
+          listChangeRequests: ({ repository }) => {
+            separately.push(repository);
+            return Effect.succeed({ items: [], truncated: false, continues: false });
+          },
+          listChangeRequestsAcross: () =>
+            Effect.succeed({
+              items: [batchedChangeRequest(1, "acme/web", "2026-07-03T00:00:00Z")],
+              truncated: false,
+            }),
+          searchCoverage: ({ repositories }) => {
+            coverageAsks += 1;
+            return Effect.succeed(
+              repositories.map((repository) => ({ repository, covered: true })),
+            );
+          },
+        }),
+      ],
+    });
+
+    yield* service.list({ state: "open", involvement: "all" });
+    yield* TestClock.adjust("599 seconds");
+    yield* service.list({ state: "open", involvement: "authored" });
+    assert.strictEqual(coverageAsks, 1);
+
+    yield* TestClock.adjust("2 seconds");
+    yield* service.list({ state: "open", involvement: "all" });
+    assert.strictEqual(coverageAsks, 2);
+    assert.deepStrictEqual(separately, []);
+  }),
+);
+it.effect("reads a silent repository on its own unless search coverage is confirmed", () =>
+  Effect.gen(function* () {
+    const separately: string[] = [];
+    let coverageAsks = 0;
+    const service = yield* makeService({
+      projects: [
+        project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" }),
+        project({ id: "p2", title: "docs", workspaceRoot: "/b", repository: "acme/docs" }),
+        project({ id: "p3", title: "old", workspaceRoot: "/c", repository: "acme/old" }),
+      ],
+      providers: [
+        fakeProvider(SourceControlProviderKind.make("github"), {
+          listChangeRequests: ({ repository }) => {
+            separately.push(repository);
+            return Effect.succeed({ items: [], truncated: false, continues: false });
+          },
+          listChangeRequestsAcross: () =>
+            Effect.succeed({
+              items: [batchedChangeRequest(1, "acme/web", "2026-07-03T00:00:00Z")],
+              truncated: false,
+            }),
+          searchCoverage: () =>
+            ++coverageAsks === 1
+              ? Effect.fail(requestFailed)
+              : Effect.succeed([{ repository: "acme/docs", covered: false }]),
+        }),
+      ],
+    });
+
+    yield* service.list({ state: "open" });
+    assert.deepStrictEqual(separately.splice(0).toSorted(), ["acme/docs", "acme/old"]);
+
+    // A failed probe is not remembered; false and missing answers both stay unconfirmed.
+    yield* service.invalidate({});
+    yield* service.list({ state: "open" });
+    assert.strictEqual(coverageAsks, 2);
+    assert.deepStrictEqual(separately.toSorted(), ["acme/docs", "acme/old"]);
+  }),
+);
 it.effect("fills in the line counts for the rows it is given", () =>
   Effect.gen(function* () {
     const asked: Array<unknown> = [];
```

## apps/server/src/pullRequest/PullRequestService.ts

Status: modified; +147; −74

```diff
@@ -13,6 +13,7 @@
 import * as Duration from "effect/Duration";
 import * as Effect from "effect/Effect";
 import * as Exit from "effect/Exit";
+import * as Fiber from "effect/Fiber";
 import * as Layer from "effect/Layer";
 import * as PubSub from "effect/PubSub";
 import * as Option from "effect/Option";
@@ -123,6 +124,7 @@
 const DEFAULT_REPOSITORY_LIST_LIMIT = 99;
 const REPOSITORY_CONCURRENCY = 12;
 const REPOSITORY_SEARCH_CHUNK = 100;
+const encodeListKey = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown));
 
 /**
  * Every read leaves the process — a CLI per repository, against hosts whose limits are low
@@ -206,7 +208,10 @@
       input: PullRequestReportStateInput,
     ) => Effect.Effect<void, PullRequestError>;
     readonly subscribeRefreshes: Stream.Stream<number>;
+    readonly refreshAfterTurn: (
+      projectId: ProjectId,
+      options?: { readonly immediate: true },
+    ) => Effect.Effect<void>;
-    readonly refreshAfterTurn: (projectId: ProjectId) => Effect.Effect<void>;
     readonly detail: (input: PullRequestRef) => Effect.Effect<PullRequestDetail, PullRequestError>;
     readonly preview: (
       input: PullRequestRef,
@@ -501,6 +506,9 @@
       : {
           listChangeRequestsAcross: wrap("listChangeRequestsAcross", api.listChangeRequestsAcross),
         }),
+    ...(api.searchCoverage === undefined
+      ? {}
+      : { searchCoverage: wrap("searchCoverage", api.searchCoverage) }),
     ...(api.listChangeRequestStats === undefined
       ? {}
       : {
@@ -1079,11 +1087,9 @@
     };
   };
 
-  // A repository that has appeared in a host search is known to be indexed there. Empty
-  // authored/reviewing searches for that same repository are therefore real empty answers, not
-  // a reason to issue the two-command per-repository fallback again.
-  const searchVisibleAt = new Map<string, number>();
-  const searchVisibilityKey = (host: string, repository: string) =>
+  // Search hits and coverage probes share the same host/repository fact, across list filters.
+  const searchCoverageCache = new Map<string, { readonly at: number; readonly covered: boolean }>();
+  const searchCoverageKey = (host: string, repository: string) =>
     `${host}\n${repository.trim().toLowerCase()}`;
 
   const listUncached: PullRequestService["Service"]["list"] = Effect.fn(
@@ -1261,11 +1267,12 @@
       }).pipe(
         observeRead,
         Effect.flatMap(({ value: page, observedAt }) =>
+          Effect.gen(function* () {
+            const now = yield* Clock.currentTimeMillis;
-          Effect.flatMap(Clock.currentTimeMillis, (now) => {
             const rows = new Map<string, Array<ProviderChangeRequest>>();
-            for (const [key, visibleAt] of searchVisibleAt) {
+            for (const [key, held] of searchCoverageCache) {
+              if (now - held.at > Duration.toMillis(SEARCH_VISIBILITY_TTL)) {
+                searchCoverageCache.delete(key);
-              if (now - visibleAt > Duration.toMillis(SEARCH_VISIBILITY_TTL)) {
-                searchVisibleAt.delete(key);
               }
             }
             for (const item of page.items) {
@@ -1273,36 +1280,63 @@
               const held = rows.get(key);
               if (held === undefined) rows.set(key, [item]);
               else held.push(item);
-              searchVisibleAt.set(searchVisibilityKey(first.host, item.repository), now);
+              searchCoverageCache.set(searchCoverageKey(first.host, item.repository), {
+                at: now,
+                covered: true,
+              });
             }
             const boundary = page.items.reduce<string | null>(
               (oldest, item) =>
                 oldest === null || item.updatedAt < oldest ? item.updatedAt : oldest,
               null,
             );
+            // Silence on the first page can mean an unindexed repository. Later pages already
+            // have a search boundary, so their missing rows are older than that boundary.
+            const silent = chunk.filter(
+              (project) =>
+                !rows.has(project.repository.trim().toLowerCase()) &&
+                cursorOf(project) === undefined,
+            );
+            const unasked = silent.filter(
+              (project) =>
+                !searchCoverageCache.has(searchCoverageKey(project.host, project.repository)),
+            );
+            const searchCoverage = first.api.searchCoverage;
+            if (unasked.length > 0 && searchCoverage !== undefined) {
+              const answers = yield* searchCoverage({
+                cwd: first.project.workspaceRoot,
+                host: first.host,
+                repositories: unasked.map((project) => project.repository),
+              }).pipe(Effect.option);
+              // Failed probes stay uncached so the next read can retry. Successful probes
+              // cache missing answers as uncovered to retain the per-repository fallback.
+              if (Option.isSome(answers)) {
+                for (const project of unasked) {
+                  searchCoverageCache.set(searchCoverageKey(project.host, project.repository), {
+                    at: now,
+                    covered: false,
+                  });
+                }
+                for (const answer of answers.value) {
+                  searchCoverageCache.set(searchCoverageKey(first.host, answer.repository), {
+                    at: now,
-            return Effect.forEach(
+                    covered: answer.covered,
+                  });
+                }
+              }
+            }
+            return yield* Effect.forEach(
               chunk,
               (project): Effect.Effect<RepositoryBatch> => {
-                const fetched = rows.get(project.repository.trim().toLowerCase()) ?? [];
-                // GitHub does not index every repository for search — a renamed one answers for
-                // its old name with silence rather than with an error — so a repository the
-                // search said nothing at all about is read on its own, once, before it is
-                // believed. Only on its first slice: after that it has a boundary to carry on
-                // from, and silence past one means the rows are older rather than absent.
-                const lastVisible = searchVisibleAt.get(
-                  searchVisibilityKey(project.host, project.repository),
-                );
-                const searchIsKnownVisible =
-                  !page.truncated &&
-                  lastVisible !== undefined &&
-                  now - lastVisible <= Duration.toMillis(SEARCH_VISIBILITY_TTL);
                 if (
+                  !rows.has(project.repository.trim().toLowerCase()) &&
-                  fetched.length === 0 &&
                   cursorOf(project) === undefined &&
-                  !searchIsKnownVisible
+                  searchCoverageCache.get(searchCoverageKey(project.host, project.repository))
+                    ?.covered !== true
                 ) {
                   return readRepository(project);
                 }
+                const fetched = rows.get(project.repository.trim().toLowerCase()) ?? [];
                 const cursorHere = cursorOf(project);
                 const items =
                   cursorHere === undefined
@@ -1916,7 +1950,7 @@
                     Effect.ensuring(
                       input.stackNumber === undefined
                         ? Effect.void
-                        : refreshAfterTurn(project.project.id),
+                        : refreshAfterTurn(project.project.id, { immediate: true }),
                     ),
                     Effect.mapError(toPullRequestError("runAction")),
                     Effect.as(
@@ -2744,37 +2778,41 @@
       timeToLive: (exit) => (Exit.isSuccess(exit) ? LIST_CACHE_TTL : Duration.zero),
     },
   );
+  const list: PullRequestService["Service"]["list"] = (input) =>
+    Effect.suspend(() => {
+      // A project's own list refreshes immediately for its turns. `projectIds` only splits the
+      // global page across environments, so it waits on the global epoch like that page does.
+      const epoch =
+        input.projectId === undefined
+          ? listingsEpoch
+          : Math.max(listingsEpoch, projectEpochs.get(input.projectId) ?? projectEpochFloor);
+      const key = encodeListKey([
+        epoch,
+        input.state,
+        input.involvement ?? null,
+        input.filters === undefined
+          ? null
+          : [
+              input.filters.draft ?? null,
+              input.filters.review ?? null,
+              input.filters.checks ?? null,
+              input.filters.author ?? null,
+              input.filters.labels ?? null,
+              input.filters.excludedLabels ?? null,
-  const list: PullRequestService["Service"]["list"] = (input) => {
-    const key = JSON.stringify([
-      listingsEpoch,
-      input.state,
-      input.involvement ?? null,
-      input.filters === undefined
-        ? null
-        : [
-            input.filters.draft ?? null,
-            input.filters.review ?? null,
-            input.filters.checks ?? null,
-            input.filters.author ?? null,
-            input.filters.labels ?? null,
-            input.filters.excludedLabels ?? null,
+            ],
+        input.projectId ?? null,
+        input.projectIds === undefined ? null : [...input.projectIds].sort(),
+        input.host ?? null,
+        input.limit ?? null,
+        input.query ?? null,
+        input.cursors === undefined
+          ? null
+          : Object.entries(input.cursors).toSorted(([left], [right]) => left.localeCompare(right)),
+      ]);
+      // Keep paid host reads alive when a page refresh cancels its reader. Reattached readers
+      // join the same cache lookup; the service scope still interrupts it on shutdown.
+      return Effect.forkIn(Cache.get(listCache, key), scope).pipe(Effect.flatMap(Fiber.join));
+    });
-          ],
-      input.projectId ?? null,
-      input.projectIds === undefined ? null : [...input.projectIds].sort(),
-      input.host ?? null,
-      input.limit ?? null,
-      input.query ?? null,
-      input.cursors === undefined
-        ? null
-        : Object.entries(input.cursors).toSorted(([left], [right]) => left.localeCompare(right)),
-    ]);
-    // A replacement reader can join a lookup still finishing its previous reader's cancellation.
-    return Cache.get(listCache, key).pipe(
-      Effect.catchCause((cause) =>
-        Cause.hasInterruptsOnly(cause) ? Cache.get(listCache, key) : Effect.failCause(cause),
-      ),
-    );
-  };
 
   const checksCache = yield* Cache.makeWith(
     (key: string) => {
@@ -3217,22 +3255,57 @@
     }
   });
 
+  // Turns share one global refresh per list TTL, plus a trailing refresh for a busy window.
+  let listingsTurnRefreshAt = Number.NEGATIVE_INFINITY;
+  let listingsTurnRefreshPending = false;
+  const refreshListingsAfterTurn = Effect.gen(function* () {
-  const refreshAfterTurn: PullRequestService["Service"]["refreshAfterTurn"] = (projectId) =>
+    if (listingsTurnRefreshPending) return;
-    Effect.suspend(() => {
+    const now = yield* Clock.currentTimeMillis;
+    const wait = listingsTurnRefreshAt + Duration.toMillis(LIST_CACHE_TTL) - now;
+    if (wait <= 0) {
+      listingsTurnRefreshAt = now;
       listingsEpoch = ++epochCounter;
+      return;
+    }
+    // Scheduling is uninterruptible so cancellation cannot leave a pending flag without a timer.
+    listingsTurnRefreshPending = true;
+    yield* Effect.gen(function* () {
+      yield* Effect.sleep(Duration.millis(wait));
+      listingsTurnRefreshAt = yield* Clock.currentTimeMillis;
+      listingsTurnRefreshPending = false;
+      listingsEpoch = ++epochCounter;
+      yield* SubscriptionRef.set(pullRequestRefreshes, listingsEpoch);
+    }).pipe(Effect.interruptible, Effect.forkIn(scope));
+  }).pipe(Effect.uninterruptible);
+
+  const refreshAfterTurn: PullRequestService["Service"]["refreshAfterTurn"] = (
+    projectId,
+    options,
+  ) =>
+    (options?.immediate
+      ? Effect.sync(() => {
+          listingsEpoch = ++epochCounter;
+        })
+      : refreshListingsAfterTurn
+    ).pipe(
+      Effect.andThen(
+        Effect.suspend(() => {
+          const projectEpoch = ++epochCounter;
+          projectEpochs.delete(projectId);
+          if (projectEpochs.size >= REF_EPOCH_CAPACITY) {
+            const oldest = projectEpochs.keys().next().value;
+            if (oldest !== undefined) {
+              projectEpochFloor = projectEpochs.get(oldest)!;
+              projectEpochs.delete(oldest);
+            }
+          }
+          projectEpochs.set(projectId, projectEpoch);
+          return readCache
+            .invalidate(`project:${projectId}`)
+            .pipe(Effect.andThen(SubscriptionRef.set(pullRequestRefreshes, projectEpoch)));
+        }),
+      ),
+    );
-      projectEpochs.delete(projectId);
-      if (projectEpochs.size >= REF_EPOCH_CAPACITY) {
-        const oldest = projectEpochs.keys().next().value;
-        if (oldest !== undefined) {
-          projectEpochFloor = projectEpochs.get(oldest)!;
-          projectEpochs.delete(oldest);
-        }
-      }
-      projectEpochs.set(projectId, listingsEpoch);
-      return readCache
-        .invalidate(`project:${projectId}`)
-        .pipe(Effect.andThen(SubscriptionRef.set(pullRequestRefreshes, listingsEpoch)));
-    });
 
   const invalidatedByMutation =
     <I extends PullRequestRef>(
```

## packages/source-control-core/src/server/PullRequestProvider.ts

Status: modified; +13; −0

```diff
@@ -331,6 +331,19 @@
     readonly filters?: PullRequestListFilters | undefined;
   }) => Effect.Effect<ProviderBatchedChangeRequestPage, PullRequestProviderError>;
 
+  /**
+   * Whether search covers repositories absent from a search page. False or missing answers
+   * fall back to `listChangeRequests`; hosts without this probe keep that fallback too.
+   */
+  readonly searchCoverage?: (input: {
+    readonly cwd: string;
+    readonly host: string;
+    readonly repositories: ReadonlyArray<string>;
+  }) => Effect.Effect<
+    ReadonlyArray<{ readonly repository: string; readonly covered: boolean }>,
+    PullRequestProviderError
+  >;
+
   readonly listChangeRequestStats?: (input: {
     readonly cwd: string;
     readonly host: string;
```

## packages/source-control-github/src/server/GitHubPullRequestApi.test.ts

Status: modified; +46; −0

```diff
@@ -1456,6 +1456,52 @@
     }),
   );
 
+  it.effect("batches search coverage and isolates a refused repository", () =>
+    Effect.gen(function* () {
+      mockedExecute.mockReturnValue(
+        Effect.succeed(
+          output(
+            encodeJson({
+              data: {
+                _0_search: { issueCount: 3 },
+                _0_repository: { pullRequests: { totalCount: 4 } },
+                _1_search: { issueCount: 0 },
+                _1_repository: { pullRequests: { totalCount: 0 } },
+                _2_search: { issueCount: 0 },
+                _2_repository: { pullRequests: { totalCount: 7 } },
+                _3_search: { issueCount: 0 },
+                _3_repository: null,
+              },
+              errors: [{ type: "FORBIDDEN", path: ["_3_repository"], message: "refused" }],
+            }),
+          ),
+        ),
+      );
+      const cli = yield* GitHubPullRequestApi.GitHubPullRequestApi;
+      const coverage = yield* cli.searchCoverage({
+        cwd: "/w",
+        host: "github.com",
+        repositories: ["acme/web", "acme/empty", "acme/skipped", "acme/gated"],
+      });
+
+      assert.strictEqual(mockedExecute.mock.calls.length, 1);
+      assert.deepStrictEqual(coverage, [
+        { repository: "acme/web", covered: true },
+        { repository: "acme/empty", covered: false },
+        { repository: "acme/skipped", covered: false },
+        { repository: "acme/gated", covered: false },
+      ]);
+      expect(Object.values(varsAt(0))).toEqual(
+        expect.arrayContaining([
+          "is:pr repo:acme/web",
+          "is:pr repo:acme/empty",
+          "is:pr repo:acme/skipped",
+          "is:pr repo:acme/gated",
+        ]),
+      );
+    }),
+  );
+
   it.effect("reads the line counts in chunks, and files them back by position", () =>
     Effect.gen(function* () {
       const changeRequests = Array.from({ length: 26 }, (_, index) => ({
```

## packages/source-control-github/src/server/GitHubPullRequestApi.ts

Status: modified; +57; −1

```diff
@@ -47,6 +47,7 @@
 import * as GitHubRequestPolicy from "./GitHubRequestPolicy.ts";
 import * as GitHubGraphQlClient from "./gitHubGraphQlClient.ts";
 import { PullRequestBatchesGraphQLGroup } from "./pullRequestBatches.graphql.ts";
+import { PullRequestSearchCoverage } from "./pullRequestListings.graphql.ts";
 import { PullRequestMutationsGraphQLGroup } from "./pullRequestMutations.graphql.ts";
 import { PullRequestReadsGraphQLGroup } from "./pullRequestReads.graphql.ts";
 import { PullRequestReviewsGraphQLGroup } from "./pullRequestReviews.graphql.ts";
@@ -468,6 +469,19 @@
       readonly filters?: PullRequestListFilters | undefined;
     }) => Effect.Effect<GitHubPullRequestSearchBatch, GitHubPullRequestApiError>;
 
+    /**
+     * Whether search indexes each repository. An empty repository is uncovered: its first
+     * pull request can lag the search index, so only a search hit proves coverage.
+     */
+    readonly searchCoverage: (input: {
+      readonly cwd: string;
+      readonly host: string;
+      readonly repositories: ReadonlyArray<string>;
+    }) => Effect.Effect<
+      ReadonlyArray<{ readonly repository: string; readonly covered: boolean }>,
+      GitHubPullRequestApiError
+    >;
+
     readonly listPullRequestStats: (input: {
       readonly cwd: string;
       readonly host: string;
@@ -899,7 +913,7 @@
 /** Every GraphQL read and write this module makes, through GitHub's auth and rate limits. */
 const PullRequestGraphQLGroup = GraphQLGroup.merge(
   PullRequestBatchesGraphQLGroup,
+  GraphQLGroup.make(LenientPullRequestSearch, LenientPullRequestList, PullRequestSearchCoverage),
-  GraphQLGroup.make(LenientPullRequestSearch, LenientPullRequestList),
   PullRequestReadsGraphQLGroup,
   PullRequestReviewsGraphQLGroup,
   PullRequestMutationsGraphQLGroup,
@@ -1740,6 +1754,9 @@
   const stackMembershipResolver = GraphQLResolver.aliased(client.PullRequestStackMembership, {
     batchKey: GitHubGraphQlClient.batchKey,
   }).pipe(RequestResolver.batchN(STAT_ALIASES_PER_REQUEST));
+  const searchCoverageResolver = GraphQLResolver.aliased(client.PullRequestSearchCoverage, {
+    batchKey: GitHubGraphQlClient.batchKey,
+  }).pipe(RequestResolver.batchN(STAT_ALIASES_PER_REQUEST));
   // A burst of ticks is one request, each file under its own alias. GitHub has no bulk form of
   // `markFileAsViewed`/`unmarkFileAsViewed`, which each take a single path.
   const setFileViewedResolver = GraphQLResolver.aliased(client.SetFileViewed, {
@@ -1955,6 +1972,45 @@
         );
     },
 
+    searchCoverage: Effect.fn("GitHubPullRequestApi.searchCoverage")(
+      function* (input) {
+        return yield* Effect.forEach(
+          input.repositories,
+          (repository) => {
+            const lookup = pullRequestLookup({ repository, number: 1 });
+            if (lookup === null) return Effect.succeed({ repository, covered: false });
+            return GraphQLResolver.request(
+              {
+                q: `is:pr repo:${lookup.owner}/${lookup.name}`,
+                owner: lookup.owner,
+                name: lookup.name,
+              },
+              searchCoverageResolver,
+            ).pipe(
+              Effect.map((result) => {
+                const total = result.repository?.pullRequests.totalCount;
+                return {
+                  repository,
+                  covered: total !== undefined && result.search.issueCount > 0,
+                };
+              }),
+              // An unreadable alias must not discard coverage answers for the other repositories.
+              Effect.catchTags({
+                GraphQLClientError: (error) =>
+                  error.reason._tag === "ResponseError" &&
+                  error.reason.errors.every((error) => error.path?.[0] === "repository")
+                    ? Effect.succeed({ repository, covered: false })
+                    : Effect.fail(error),
+              }),
+            );
+          },
+          { concurrency: "unbounded" },
+        );
+      },
+      (effect, input) =>
+        effect.pipe((effect) => batchedRead({ ...input, operation: "searchCoverage" }, effect)),
+    ),
+
     listPullRequestStats: Effect.fn("GitHubPullRequestApi.listPullRequestStats")(
       function* (input) {
         const lookups = input.changeRequests.map((changeRequest) => ({
```

## packages/source-control-github/src/server/GitHubPullRequestProvider.ts

Status: modified; +3; −0

```diff
@@ -318,6 +318,9 @@
           })),
         ),
 
+    searchCoverage: (input) =>
+      cli.searchCoverage(input).pipe(Effect.mapError(fail("searchCoverage"))),
+
     listChangeRequestStats: (input) =>
       cli
         .listPullRequestStats({
```

## packages/source-control-github/src/server/pullRequestListings.graphql

Status: modified; +12; −0

```diff
@@ -130,3 +130,15 @@
     }
   }
 }
+
+# Search can omit repositories from its index.
+query PullRequestSearchCoverage($q: String!, $owner: String!, $name: String!) {
+  search(query: $q, type: ISSUE, first: 0) {
+    issueCount
+  }
+  repository(owner: $owner, name: $name) {
+    pullRequests {
+      totalCount
+    }
+  }
+}
```

## packages/source-control-github/src/server/pullRequestListings.graphql.ts

Status: modified; +66; −7

```diff
@@ -241,7 +241,7 @@
  * of matches, a maximum of 1,000 results will be available across all types,
  * potentially split across many pages.
  */
-export const SearchResultItemConnection = Schema.Struct({
+export const PullRequestSearchSearchSearchResultItemConnection = Schema.Struct({
   /** Information to aid in pagination. */
   pageInfo: PageInfo,
   /** A list of nodes. */
@@ -250,7 +250,7 @@
     SearchResultItemOther
   ]))))
 })
+export interface PullRequestSearchSearchSearchResultItemConnection extends Schema.Struct.Type<typeof PullRequestSearchSearchSearchResultItemConnection.fields> {}
-export interface SearchResultItemConnection extends Schema.Struct.Type<typeof SearchResultItemConnection.fields> {}
 
 /** `query PullRequestSearch` */
 export const PullRequestSearch = GraphQL.query("PullRequestSearch", {
@@ -278,7 +278,7 @@
   },
   result: Schema.Struct({
     /** Perform a search across resources, returning a maximum of 1,000 results. */
-    search: SearchResultItemConnection
+    search: PullRequestSearchSearchSearchResultItemConnection
   })
 })
 export declare namespace PullRequestSearch {
@@ -287,18 +287,18 @@
 }
 
 /** The connection type for PullRequest. */
+export const PullRequestListRepositoryPullRequestsPullRequestConnection = Schema.Struct({
-export const PullRequestConnection = Schema.Struct({
   /** Information to aid in pagination. */
   pageInfo: PageInfo,
   /** A list of nodes. */
   nodes: Schema.NullOr(Schema.Array(Schema.NullOr(PullRequestRow)))
 })
-export interface PullRequestConnection extends Schema.Struct.Type<typeof PullRequestConnection.fields> {}
+export interface PullRequestListRepositoryPullRequestsPullRequestConnection extends Schema.Struct.Type<typeof PullRequestListRepositoryPullRequestsPullRequestConnection.fields> {}
 
 /** A repository contains the content for a project. */
 export const PullRequestListRepositoryRepository = Schema.Struct({
   /** A list of pull requests that have been opened in the repository. */
-  pullRequests: PullRequestConnection
+  pullRequests: PullRequestListRepositoryPullRequestsPullRequestConnection
 })
 export interface PullRequestListRepositoryRepository extends Schema.Struct.Type<typeof PullRequestListRepositoryRepository.fields> {}
 
@@ -340,5 +340,64 @@
   export type Result = typeof PullRequestList.result.Type
 }
 
+/**
+ * A list of results that matched against a search query. Regardless of the number
+ * of matches, a maximum of 1,000 results will be available across all types,
+ * potentially split across many pages.
+ */
+export const PullRequestSearchCoverageSearchSearchResultItemConnection = Schema.Struct({
+  /**
+   * The total number of issues that matched the search query. Regardless of the
+   * total number of matches, a maximum of 1,000 results will be available across all types.
+   */
+  issueCount: Shared.Int
+})
+export interface PullRequestSearchCoverageSearchSearchResultItemConnection extends Schema.Struct.Type<typeof PullRequestSearchCoverageSearchSearchResultItemConnection.fields> {}
+
+/** The connection type for PullRequest. */
+export const PullRequestSearchCoverageRepositoryPullRequestsPullRequestConnection = Schema.Struct({
+  /** Identifies the total count of items in the connection. */
+  totalCount: Shared.Int
+})
+export interface PullRequestSearchCoverageRepositoryPullRequestsPullRequestConnection extends Schema.Struct.Type<typeof PullRequestSearchCoverageRepositoryPullRequestsPullRequestConnection.fields> {}
+
+/** A repository contains the content for a project. */
+export const PullRequestSearchCoverageRepositoryRepository = Schema.Struct({
+  /** A list of pull requests that have been opened in the repository. */
+  pullRequests: PullRequestSearchCoverageRepositoryPullRequestsPullRequestConnection
+})
+export interface PullRequestSearchCoverageRepositoryRepository extends Schema.Struct.Type<typeof PullRequestSearchCoverageRepositoryRepository.fields> {}
+
+/** `query PullRequestSearchCoverage` */
+export const PullRequestSearchCoverage = GraphQL.query("PullRequestSearchCoverage", {
+  document: {
+    variables: [
+      { name: "q", body: "String!" },
+      { name: "owner", body: "String!" },
+      { name: "name", body: "String!" }
+    ],
+    selections: [
+      { name: "search", body: ["(query:$", "q", " type:ISSUE first:0){issueCount}"] },
+      { name: "repository", body: ["(owner:$", "owner", " name:$", "name", "){pullRequests{totalCount}}"] }
+    ],
+    fragments: []
+  },
+  variables: {
+    q: Schema.String,
+    owner: Schema.String,
+    name: Schema.String
+  },
+  result: Schema.Struct({
+    /** Perform a search across resources, returning a maximum of 1,000 results. */
+    search: PullRequestSearchCoverageSearchSearchResultItemConnection,
+    /** Lookup a given repository by the owner and repository name. */
+    repository: Schema.NullOr(PullRequestSearchCoverageRepositoryRepository)
+  })
+})
+export declare namespace PullRequestSearchCoverage {
+  export type Variables = typeof PullRequestSearchCoverage.variables.Type
+  export type Result = typeof PullRequestSearchCoverage.result.Type
+}
+
 /** Every operation in pullRequestListings.graphql. */
-export const PullRequestListingsGraphQLGroup = GraphQLGroup.make(PullRequestSearch, PullRequestList)
+export const PullRequestListingsGraphQLGroup = GraphQLGroup.make(PullRequestSearch, PullRequestList, PullRequestSearchCoverage)
```
