authorgravatar for 62337003+Bil0000@users.noreply.github.comBilal Bakr <62337003+Bil0000@users.noreply.github.com> 2026-09-12 10:45:54+03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-12 00:45:54-07:00
logb1e223e2b0d87124883b1410ab52dd6a1338e40d
tree6c715b608c2b2dc61deb3f3e91907c516fe8dd54
parent57b23a09f9dff15d57e3fa288c111381259a9a12
signature Signed by PGP key B5690EEEBB952194

perf(server): avoid workspace scans when loading pull requests (#11299)


12 files changed, 114 insertions(+), 15 deletions(-)

apps/server/src/checkpointing/CheckpointDiffQuery.test.ts+5
......@@ -88,6 +88,7 @@ describe("CheckpointDiffQuery.layer", () => {
8888 getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }),
8989 getEventReplayStats: () => Effect.die("unused"),
9090 getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()),
91 getProjectShells: () => Effect.die("unused"),
9192 getProjectShellById: () => Effect.succeed(Option.none()),
9293 getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()),
9394 getImportedAgentSessionSources: () => Effect.die("unused"),
......@@ -202,6 +203,7 @@ describe("CheckpointDiffQuery.layer", () => {
202203 getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }),
203204 getEventReplayStats: () => Effect.die("unused"),
204205 getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()),
206 getProjectShells: () => Effect.die("unused"),
205207 getProjectShellById: () => Effect.succeed(Option.none()),
206208 getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()),
207209 getImportedAgentSessionSources: () => Effect.die("unused"),
......@@ -291,6 +293,7 @@ describe("CheckpointDiffQuery.layer", () => {
291293 getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }),
292294 getEventReplayStats: () => Effect.die("unused"),
293295 getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()),
296 getProjectShells: () => Effect.die("unused"),
294297 getProjectShellById: () => Effect.succeed(Option.none()),
295298 getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()),
296299 getImportedAgentSessionSources: () => Effect.die("unused"),
......@@ -365,6 +368,7 @@ describe("CheckpointDiffQuery.layer", () => {
365368 getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }),
366369 getEventReplayStats: () => Effect.die("unused"),
367370 getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()),
371 getProjectShells: () => Effect.die("unused"),
368372 getProjectShellById: () => Effect.succeed(Option.none()),
369373 getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()),
370374 getImportedAgentSessionSources: () => Effect.die("unused"),
......@@ -424,6 +428,7 @@ describe("CheckpointDiffQuery.layer", () => {
424428 getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }),
425429 getEventReplayStats: () => Effect.die("unused"),
426430 getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()),
431 getProjectShells: () => Effect.die("unused"),
427432 getProjectShellById: () => Effect.succeed(Option.none()),
428433 getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()),
429434 getImportedAgentSessionSources: () => Effect.die("unused"),
apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts+1
......@@ -446,6 +446,7 @@ describe("OrchestrationEngine", () => {
446446 getEventReplayStats: () => Effect.die("unused"),
447447 getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()),
448448 getProjectShellById: () => Effect.succeed(Option.none()),
449 getProjectShells: () => Effect.succeed([]),
449450 getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()),
450451 getImportedAgentSessionSources: () => Effect.die("unused"),
451452 getThreadCheckpointContext: () => Effect.succeed(Option.none()),
apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts+52
......@@ -47,6 +47,58 @@ const encodeMessageContext = Schema.encodeEffect(
4747 Schema.fromJsonString(OrchestrationMessageContext),
4848);
4949
50it.effect("reads project shells without loading threads or resolving excluded projects", () => {
51 const resolved: string[] = [];
52 const layer = OrchestrationProjectionSnapshotQueryLive.pipe(
53 Layer.provide(ThreadBackgroundLiveness.layer),
54 Layer.provide(ThreadPlanProgress.layer),
55 Layer.provide(
56 Layer.succeed(RepositoryIdentityResolver.RepositoryIdentityResolver, {
57 resolve: (root) =>
58 Effect.sync(() => {
59 resolved.push(root);
60 return null;
61 }),
62 }),
63 ),
64 Layer.provideMerge(SqlitePersistenceMemory),
65 );
66 return Effect.gen(function* () {
67 const sql = yield* SqlClient.SqlClient;
68 const query = yield* ProjectionSnapshotQuery;
69 yield* sql`INSERT INTO projection_projects
70 (project_id, title, workspace_root, scripts_json, created_at, updated_at, deleted_at)
71 VALUES
72 ('p1', 'First', '/first', '[]', '2026-09-01T00:00:00Z', '2026-09-01T00:00:00Z', NULL),
73 ('p2', 'Second', '/second', '[]', '2026-09-02T00:00:00Z', '2026-09-02T00:00:00Z', NULL),
74 ('p3', 'Deleted', '/deleted', '[]', '2026-09-03T00:00:00Z', '2026-09-03T00:00:00Z', '2026-09-04T00:00:00Z')`;
75 const expected = (yield* query.getShellSnapshot()).projects;
76 resolved.length = 0;
77 yield* sql`INSERT INTO projection_threads
78 (thread_id, project_id, title, model_selection_json, runtime_mode, interaction_mode, created_at, updated_at)
79 VALUES ('t1', 'p1', 'Thread', 'invalid-json', 'full-access', 'default', '2026-09-01T00:00:00Z', '2026-09-01T00:00:00Z')`;
80
81 const counter = makeSqlStatementCounter();
82 const projects = yield* query.getProjectShells().pipe(Effect.withTracer(counter.tracer));
83 assert.deepStrictEqual(projects, expected);
84 assert.strictEqual(counter.count(), 1);
85 assert.deepStrictEqual(resolved.toSorted(), ["/first", "/second"]);
86 resolved.length = 0;
87 yield* sql`UPDATE projection_projects SET scripts_json = 'invalid-json' WHERE project_id IN ('p1', 'p3')`;
88 assert.deepStrictEqual(yield* query.getProjectShells([asProjectId("p2")]), [expected[1]!]);
89 assert.deepStrictEqual(resolved, ["/second"]);
90 resolved.length = 0;
91 const beforeEmpty = counter.count();
92 assert.deepStrictEqual(
93 yield* query.getProjectShells([]).pipe(Effect.withTracer(counter.tracer)),
94 [],
95 );
96 assert.strictEqual(counter.count(), beforeEmpty);
97 assert.deepStrictEqual(yield* query.getProjectShells([asProjectId("p3")]), []);
98 assert.deepStrictEqual(resolved, []);
99 }).pipe(Effect.provide(layer));
100});
101
50102const projectionSnapshotLayer = it.layer(
51103 OrchestrationProjectionSnapshotQueryLive.pipe(
52104 Layer.provide(ThreadBackgroundLiveness.layer),
apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts+29-2
......@@ -521,9 +521,14 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
521521 });
522522
523523 const listProjectRows = SqlSchema.findAll({
524 Request: Schema.Void,
524 Request: Schema.UndefinedOr(
525 Schema.Struct({
526 activeOnly: Schema.Boolean,
527 projectIds: Schema.optional(Schema.Array(ProjectId)),
528 }),
529 ),
525530 Result: ProjectionProjectDbRowSchema,
526 execute: () =>
531 execute: (filter) =>
527532 sql`
528533 SELECT
529534 project_id AS "projectId",
......@@ -539,6 +544,8 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
539544 updated_at AS "updatedAt",
540545 deleted_at AS "deletedAt"
541546 FROM projection_projects
547 WHERE ${filter?.activeOnly === true ? sql`deleted_at IS NULL` : sql`1 = 1`}
548 AND ${filter?.projectIds === undefined ? sql`1 = 1` : sql.in("project_id", filter.projectIds)}
542549 ORDER BY created_at ASC, project_id ASC
543550 `,
544551 });
......@@ -2947,6 +2954,25 @@ pending_approval_requests AS (
29472954 ),
29482955 );
29492956
2957 const getProjectShells: ProjectionSnapshotQueryShape["getProjectShells"] = (projectIds) => {
2958 if (projectIds?.length === 0) return Effect.succeed([]);
2959 return listProjectRows({ activeOnly: true, projectIds }).pipe(
2960 Effect.mapError(
2961 toPersistenceSqlOrDecodeError(
2962 "ProjectionSnapshotQuery.getProjectShells:query",
2963 "ProjectionSnapshotQuery.getProjectShells:decodeRows",
2964 ),
2965 ),
2966 Effect.flatMap((projects) =>
2967 resolveRepositoryIdentitiesForProjects(projects).pipe(
2968 Effect.map((identities) =>
2969 projects.map((row) => mapProjectShellRow(row, identities.get(row.projectId) ?? null)),
2970 ),
2971 ),
2972 ),
2973 );
2974 };
2975
29502976 const getProjectShellById: ProjectionSnapshotQueryShape["getProjectShellById"] = (projectId) =>
29512977 getActiveProjectRowById({ projectId }).pipe(
29522978 Effect.mapError(
......@@ -3659,6 +3685,7 @@ pending_approval_requests AS (
36593685 getEventReplayStats,
36603686 getActiveProjectByWorkspaceRoot,
36613687 getProjectShellById,
3688 getProjectShells,
36623689 getFirstActiveThreadIdByProjectId,
36633690 getImportedAgentSessionSources,
36643691 getThreadCheckpointContext,
apps/server/src/orchestration/Services/ProjectionSnapshotQuery.ts+4
......@@ -166,6 +166,10 @@ export interface ProjectionSnapshotQueryShape {
166166 projectId: ProjectId,
167167 ) => Effect.Effect<Option.Option<OrchestrationProjectShell>, ProjectionRepositoryError>;
168168
169 readonly getProjectShells: (
170 projectIds?: ReadonlyArray<ProjectId>,
171 ) => Effect.Effect<ReadonlyArray<OrchestrationProjectShell>, ProjectionRepositoryError>;
172
169173 /**
170174 * Read the earliest active thread for a project.
171175 */
apps/server/src/project/AgentSessionScanner.test.ts+1
......@@ -51,6 +51,7 @@ const makeProjectionSnapshotQueryLayer = (importedWorkspaceRoots: ReadonlyArray<
5151 getCounts: () => Effect.die("unused"),
5252 getEventReplayStats: () => Effect.die("unused"),
5353 getActiveProjectByWorkspaceRoot: () => Effect.die("unused"),
54 getProjectShells: () => Effect.die("unused"),
5455 getProjectShellById: () => Effect.die("unused"),
5556 getImportedAgentSessionSources: () => Effect.succeed([]),
5657 getFirstActiveThreadIdByProjectId: () => Effect.die("unused"),
apps/server/src/project/ProjectSetupScriptRunner.test.ts+1
......@@ -39,6 +39,7 @@ const makeProjectionSnapshotQueryLayer = (project: OrchestrationProject) =>
3939 Effect.succeed(
4040 workspaceRoot === project.workspaceRoot ? Option.some(project) : Option.none(),
4141 ),
42 getProjectShells: () => Effect.die("unused"),
4243 getProjectShellById: (projectId) =>
4344 Effect.succeed(projectId === project.id ? Option.some(project) : Option.none()),
4445 getFirstActiveThreadIdByProjectId: () => Effect.die("unused"),
apps/server/src/provider/Layers/ProviderService.test.ts+1
......@@ -4928,6 +4928,7 @@ describe("agent browser access", () => {
49284928 getCounts: () => Effect.die("unused"),
49294929 getEventReplayStats: () => Effect.die("unused"),
49304930 getActiveProjectByWorkspaceRoot: () => Effect.die("unused"),
4931 getProjectShells: () => Effect.die("unused"),
49314932 getProjectShellById: () => Effect.die("unused"),
49324933 getFirstActiveThreadIdByProjectId: () => Effect.die("unused"),
49334934 getThreadCheckpointContext: () => Effect.die("unused"),
apps/server/src/provider/Layers/ProviderSessionReaper.test.ts+1
......@@ -243,6 +243,7 @@ describe("ProviderSessionReaper", () => {
243243 getCounts: () => Effect.die("unused"),
244244 getEventReplayStats: () => Effect.die("unused"),
245245 getActiveProjectByWorkspaceRoot: () => Effect.die("unused"),
246 getProjectShells: () => Effect.die("unused"),
246247 getProjectShellById: () => Effect.die("unused"),
247248 getFirstActiveThreadIdByProjectId: () => Effect.die("unused"),
248249 getImportedAgentSessionSources: () => Effect.die("unused"),
apps/server/src/pullRequest/PullRequestService.test.ts+6-7
......@@ -193,13 +193,12 @@ function makeService(input: {
193193 input.resolveHandle ?? (() => Effect.die("Unexpected provider refinement")),
194194 }),
195195 Layer.mock(ProjectionSnapshotQuery.ProjectionSnapshotQuery)({
196 getShellSnapshot: () =>
197 Effect.succeed({
198 snapshotSequence: 1,
199 projects: input.projects,
200 threads: [],
201 updatedAt: "2026-07-01T00:00:00Z",
202 }),
196 getProjectShells: (projectIds) =>
197 Effect.succeed(
198 input.projects.filter((project) => projectIds?.includes(project.id) ?? true),
199 ),
200 getProjectShellById: (projectId) =>
201 Effect.succeed(Option.fromNullishOr(input.projects.find((p) => p.id === projectId))),
203202 }),
204203 SourceControlRateLimit.layer,
205204 Layer.effect(PullRequestReadCache.PullRequestReadCache, PullRequestReadCache.make).pipe(
apps/server/src/pullRequest/PullRequestService.ts+9-6
......@@ -603,7 +603,10 @@ export const make = Effect.gen(function* () {
603603 const listWorkspaceProjects = (
604604 filter: Pick<PullRequestListInput, "projectId" | "projectIds" | "host">,
605605 ): Effect.Effect<WorkspaceProjects, PullRequestError> =>
606 projections.getShellSnapshot().pipe(
606 (filter.projectId === undefined
607 ? projections.getProjectShells(filter.projectIds)
608 : projections.getProjectShellById(filter.projectId).pipe(Effect.map(Option.toArray))
609 ).pipe(
607610 Effect.mapError(
608611 (error) =>
609612 new PullRequestOperationError({
......@@ -612,12 +615,12 @@ export const make = Effect.gen(function* () {
612615 cause: error,
613616 }),
614617 ),
615 Effect.flatMap((snapshot) =>
616 refineUnknownProjectKinds(snapshot.projects, filter).pipe(
617 Effect.map((refinedKinds) => ({ refinedKinds, snapshot })),
618 Effect.flatMap((projects) =>
619 refineUnknownProjectKinds(projects, filter).pipe(
620 Effect.map((refinedKinds) => ({ refinedKinds, projects })),
618621 ),
619622 ),
620 Effect.map(({ refinedKinds, snapshot }) => {
623 Effect.map(({ refinedKinds, projects }) => {
621624 const supported: SupportedProject[] = [];
622625 const unimplemented = new Map<
623626 string,
......@@ -625,7 +628,7 @@ export const make = Effect.gen(function* () {
625628 >();
626629 const viewerRoots = new Map<string, string[]>();
627630 const seen = new Set<string>();
628 for (const project of snapshot.projects) {
631 for (const project of projects) {
629632 if (filter.projectId !== undefined && project.id !== filter.projectId) continue;
630633 if (filter.projectIds !== undefined && !filter.projectIds.includes(project.id)) continue;
631634 const identity = project.repositoryIdentity;
apps/server/src/serverRuntimeStartup.test.ts+4
......@@ -187,6 +187,7 @@ it.effect("resolveAutoBootstrapWelcomeTargets returns existing project and threa
187187 deletedAt: null,
188188 }),
189189 ),
190 getProjectShells: () => Effect.die("unused"),
190191 getProjectShellById: () => Effect.die("unused"),
191192 getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.some(bootstrapThreadId)),
192193 getImportedAgentSessionSources: () => Effect.die("unused"),
......@@ -314,6 +315,7 @@ it.effect.each([
314315 })
315316 : Option.none(),
316317 ),
318 getProjectShells: () => Effect.die("unused"),
317319 getProjectShellById: () => Effect.die("unused"),
318320 getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()),
319321 getImportedAgentSessionSources: () => Effect.die("unused"),
......@@ -384,6 +386,7 @@ it.effect(
384386 getCounts: () => Effect.die("unused"),
385387 getEventReplayStats: () => Effect.die("unused"),
386388 getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()),
389 getProjectShells: () => Effect.die("unused"),
387390 getProjectShellById: () => Effect.die("unused"),
388391 getFirstActiveThreadIdByProjectId: () => Effect.die("thread lookup failed"),
389392 getImportedAgentSessionSources: () => Effect.die("unused"),
......@@ -446,6 +449,7 @@ it.effect("resolveAutoBootstrapWelcomeTargets preserves typed UUID generation fa
446449 getCounts: () => Effect.die("unused"),
447450 getEventReplayStats: () => Effect.die("unused"),
448451 getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()),
452 getProjectShells: () => Effect.die("unused"),
449453 getProjectShellById: () => Effect.die("unused"),
450454 getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()),
451455 getImportedAgentSessionSources: () => Effect.die("unused"),