From 3d3a12a3e278f6e5885e604457a485df3e515e82 Mon Sep 17 00:00:00 2001 From: Dotta Date: Tue, 18 Aug 2026 21:47:34 +0000 Subject: [PATCH] fix(workspaces): keep inventory reads DB-only Backport the existing master guard so this long-lived feature worktree does not fan out Git scans across its cloned workspace inventory. Co-Authored-By: Paperclip --- .../execution-workspaces-service.test.ts | 16 ++++++++++++++-- server/src/services/execution-workspaces.ts | 16 ++++++++++++---- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/server/src/__tests__/execution-workspaces-service.test.ts b/server/src/__tests__/execution-workspaces-service.test.ts index 9f3693e1b92..cb30a047374 100644 --- a/server/src/__tests__/execution-workspaces-service.test.ts +++ b/server/src/__tests__/execution-workspaces-service.test.ts @@ -3873,7 +3873,7 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => { expect(comments).toHaveLength(0); }, 20_000); - it("returns full details at the observed volume without multiplying unconfigured shared service history", async () => { + it("keeps a large collection DB-only while a concurrent health-style query remains responsive", async () => { const companyId = randomUUID(); const projectId = randomUUID(); const projectWorkspaceId = randomUUID(); @@ -3935,9 +3935,21 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => { })), ); - const workspaces = await svc.list(companyId); + const inspectGitCloseReadiness = vi.fn(async () => { + throw new Error("collection inventory must not inspect git worktrees"); + }); + const inventoryService = executionWorkspaceService(db, { inspectGitCloseReadiness }); + const inventoryPromise = inventoryService.list(companyId); + const healthResponsive = await Promise.race([ + db.execute(sql`select 1 as ok`).then(() => true), + new Promise((resolve) => setTimeout(() => resolve(false), 2_000)), + ]); + const workspaces = await inventoryPromise; + expect(healthResponsive).toBe(true); + expect(inspectGitCloseReadiness).not.toHaveBeenCalled(); expect(workspaces).toHaveLength(workspaceCount); + expect(workspaces.every((workspace) => workspace.deliveryState === "unknown")).toBe(true); expect(workspaces.reduce((count, workspace) => count + (workspace.runtimeServices?.length ?? 0), 0)).toBe(0); expect(JSON.stringify(workspaces).length).toBeLessThan(12_000_000); diff --git a/server/src/services/execution-workspaces.ts b/server/src/services/execution-workspaces.ts index 54b0a562f16..91e8d1e64f5 100644 --- a/server/src/services/execution-workspaces.ts +++ b/server/src/services/execution-workspaces.ts @@ -230,6 +230,10 @@ export type ExecutionWorkspaceServiceOptions = { // becomes terminal before it archives the workspace. A value of 0 disables // the cooldown. The default is 7 days. workspaceReaperCooldownDays?: number; + inspectGitCloseReadiness?: (workspace: ExecutionWorkspace) => Promise<{ + git: ExecutionWorkspaceCloseGitReadiness | null; + warnings: string[]; + }>; }; function parseGitHubRepository(repoUrl: string | null) { @@ -1467,7 +1471,7 @@ export function executionWorkspaceService(db: Db, opts: ExecutionWorkspaceServic async function hydrateWorkspace(row: ExecutionWorkspaceRow, runtimeServices: WorkspaceRuntimeService[] = []) { const workspace = toExecutionWorkspace(row, runtimeServices); - const { git } = await inspectGitCloseReadiness(workspace); + const { git } = await (opts.inspectGitCloseReadiness ?? inspectGitCloseReadiness)(workspace); const assessment = await assessDelivery(row, git); return toExecutionWorkspace(row, runtimeServices, assessment.deliveryState); } @@ -2060,12 +2064,16 @@ export function executionWorkspaceService(db: Db, opts: ExecutionWorkspaceServic .where(and(...conditions)) .orderBy(desc(executionWorkspaces.lastUsedAt), desc(executionWorkspaces.createdAt)); const runtimeServicesByWorkspaceId = await loadEffectiveRuntimeServicesByExecutionWorkspace(db, companyId, rows); - return Promise.all(rows.map((row) => - hydrateWorkspace( + // Collection reads are deliberately DB-only. Delivery-state hydration + // inspects git and may resolve pull requests, so doing it for every row + // lets a large inventory launch an unbounded number of child processes. + // Detail and close-readiness reads retain the live hydration path. + return rows.map((row) => + toExecutionWorkspace( row, (runtimeServicesByWorkspaceId.get(row.id) ?? []).map(toRuntimeService), ), - )); + ); }, listSummaries: async (companyId: string, filters?: {