diff --git a/doc/DEVELOPING.md b/doc/DEVELOPING.md index 60891a5a54..85e38bd816 100644 --- a/doc/DEVELOPING.md +++ b/doc/DEVELOPING.md @@ -715,6 +715,8 @@ These bounds apply to application filename storage, not total process memory. Re Workspace preparation resolves an existing root symlink before reading the snapshot and uses that resolved directory for the rest of the operation. Overlay staging checks the captured root identity and each selected path's ancestors before and after copying. A replaced root or a symlink in an ancestor directory stops staging before upload. A missing source file can be skipped; other source inspection errors stop staging. Selected symlink entries remain symlinks. +Execution-workspace close-readiness and branch-reconciliation status checks allow up to 32 MiB of buffered output. This accommodates nested task worktrees while preserving exact untracked-file counts. Larger output fails the scan and blocks destructive cleanup. Other buffered scan bounds stay unchanged. + The cache intentionally trades up to a few seconds of changed-file freshness for stable server latency. The file browser retains an explicit refresh action, does not start its query while the panel or browser tab is hidden, and presents overloads as retryable failures rather than an empty workspace. A full queue returns `503` with code `workspace_git_scan_saturated`; a scan exceeding its wall-clock limit returns `504` with code `workspace_git_scan_timeout`. Both responses include `Retry-After: 1`. Sandbox Git sync treats only the selected repository root as a clone source. A selected subfolder uses directory sync within that folder, applies the enclosing repository's ignore rules, and does not transfer parent files or Git history. diff --git a/server/src/__tests__/execution-workspaces-service.test.ts b/server/src/__tests__/execution-workspaces-service.test.ts index 48431a003b..1d1a3c22f1 100644 --- a/server/src/__tests__/execution-workspaces-service.test.ts +++ b/server/src/__tests__/execution-workspaces-service.test.ts @@ -1059,6 +1059,33 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => { expect(workspace?.status).toBe("active"); }); + it("counts large untracked worktrees without allowing destructive cleanup", async () => { + const seeded = await seedTerminalWorkspace({ mergedPr: true }); + const directory = path.join(seeded.worktreePath, ".worktrees", "task-retry"); + await fs.mkdir(directory, { recursive: true }); + for (let offset = 0; offset < 5_000; offset += 100) { + await Promise.all(Array.from({ length: 100 }, (_, index) => + fs.writeFile(path.join(directory, `${offset + index}-${"source".repeat(32)}.ts`), "uncommitted\n"), + )); + } + + const status = await execFileAsync( + "git", + ["-C", seeded.worktreePath, "status", "--porcelain", "--untracked-files=all"], + { maxBuffer: 2 * 1024 * 1024 }, + ); + expect(Buffer.byteLength(status.stdout, "utf8")).toBeGreaterThan(1024 * 1024); + + const readiness = await svc.getCloseReadiness(seeded.executionWorkspaceId); + expect(readiness?.git).toMatchObject({ hasUntrackedFiles: true, untrackedEntryCount: 5_000 }); + expect(readiness?.warnings).toContain("The workspace has 5000 untracked files."); + expect(readiness?.blockingReasons).not.toContain( + "Paperclip could not verify the workspace git status. Retry before destructive cleanup.", + ); + expect(await svc.sweepTerminalWorkspaces()).toMatchObject({ archived: 0, skippedUndelivered: 1 }); + await expect(fs.access(seeded.worktreePath)).resolves.toBeUndefined(); + }, 20_000); + it("refuses cleanup when the worktree changes after delivery assessment", async () => { const seeded = await seedTerminalWorkspace({ mergedPr: true }); await db.update(executionWorkspaces).set({ diff --git a/server/src/services/execution-workspaces.ts b/server/src/services/execution-workspaces.ts index c1ec53ffd6..2382faef82 100644 --- a/server/src/services/execution-workspaces.ts +++ b/server/src/services/execution-workspaces.ts @@ -415,6 +415,9 @@ async function runExpensiveGitStatus(input: { operation: input.operation, fairnessKeys: input.fairnessKeys, cacheTtlMs: 0, + // Nested task worktrees can exceed the scheduler's 1 MiB default. + // Keep exact file counts for readiness and reconciliation checks. + maxStdoutBytes: 32 * 1024 * 1024, }); }