authorgravatar for anshsinghapa@gmail.comANSH SINGH <anshsinghapa@gmail.com> 2026-10-07 22:55:05+05:30
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-07 10:25:05-07:00
loge0201d3ab51b4593eff3c41d03ab9c9b2dc07471
tree3490b9732cc0cd28595802eb78ff12f3864e54a0
parent93e864042cf066f4cdb3efe65dbefaaf0d168894
signature Signed by PGP key B5690EEEBB952194

fix(server): free worktrees for terminal thread statuses (#15150)


2 files changed, 48 insertions(+), 1 deletions(-)

apps/server/src/storageCleanup.test.ts+42
...@@ -3,6 +3,7 @@ import {...@@ -3,6 +3,7 @@ import {
3 ProjectId,3 ProjectId,
4 ProviderInstanceId,4 ProviderInstanceId,
5 RunId,5 RunId,
6 RuntimeRequestId,
6 ThreadId,7 ThreadId,
7 type OrchestrationV2ThreadShell,8 type OrchestrationV2ThreadShell,
8} from "@t3tools/contracts";9} from "@t3tools/contracts";
...@@ -84,6 +85,47 @@ describe("V2 storage cleanup eligibility", () => {...@@ -84,6 +85,47 @@ describe("V2 storage cleanup eligibility", () => {
84 },85 },
85 );86 );
8687
88 it.each(["idle", "completed", "interrupted", "failed", "cancelled", "rolled_back"] as const)(
89 "allows cleanup once its thread is %s",
90 (status) => {
91 expect(storageCleanupThreadIdle(candidateWithStatus(status), NOW_MS)).toBe(true);
92 },
93 );
94
95 it.each(["completed", "interrupted", "cancelled", "rolled_back"] as const)(
96 "retains %s while background work is pending",
97 (status) => {
98 expect(
99 storageCleanupThreadIdle(
100 {
101 ...candidateWithStatus(status),
102 pendingBackgroundTasks: [{ taskId: "task-1", kind: "command" }],
103 },
104 NOW_MS,
105 ),
106 ).toBe(false);
107 },
108 );
109
110 it.each(["completed", "interrupted", "cancelled", "rolled_back"] as const)(
111 "retains %s while a runtime request is pending",
112 (status) => {
113 expect(
114 storageCleanupThreadIdle(
115 {
116 ...candidateWithStatus(status),
117 pendingRuntimeRequest: {
118 id: RuntimeRequestId.make("request-1"),
119 kind: "command",
120 createdAt: at(0),
121 },
122 },
123 NOW_MS,
124 ),
125 ).toBe(false);
126 },
127 );
128
87 it("retains an active run even if the shell status is idle", () => {129 it("retains an active run even if the shell status is idle", () => {
88 expect(130 expect(
89 storageCleanupThreadIdle({ ...candidate(), activeRunId: RunId.make("run") }, NOW_MS),131 storageCleanupThreadIdle({ ...candidate(), activeRunId: RunId.make("run") }, NOW_MS),
apps/server/src/storageCleanup.ts+6-1
...@@ -86,7 +86,12 @@ export function storageCleanupThreadIdle(thread: OrchestrationV2ThreadShell, now...@@ -86,7 +86,12 @@ export function storageCleanupThreadIdle(thread: OrchestrationV2ThreadShell, now
86 thread.branch !== null &&86 thread.branch !== null &&
87 thread.worktreePath !== null &&87 thread.worktreePath !== null &&
88 thread.activeRunId === null &&88 thread.activeRunId === null &&
89 (thread.status === "idle" || thread.status === "failed") &&89 (thread.status === "idle" ||
90 thread.status === "completed" ||
91 thread.status === "interrupted" ||
92 thread.status === "failed" ||
93 thread.status === "cancelled" ||
94 thread.status === "rolled_back") &&
90 (thread.pendingBackgroundTasks?.length ?? 0) === 0 &&95 (thread.pendingBackgroundTasks?.length ?? 0) === 0 &&
91 thread.pendingRuntimeRequest === null &&96 thread.pendingRuntimeRequest === null &&
92 !threadHasQueuedTurnStart(thread, now)97 !threadHasQueuedTurnStart(thread, now)