Skip to content

Commit 21bedfb

Browse files
Bil0000maria-rcks
andauthored
feat(storage): clean up settled thread worktrees (#15208)
Co-authored-by: maria-rcks <maria@kuuro.net>
1 parent c814e51 commit 21bedfb

28 files changed

Lines changed: 1594 additions & 473 deletions

‎apps/server/src/environment/ServerEnvironment.test.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,13 @@ it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => {
118118
expect(descriptor.capabilities.serverSelfUpdate).toBe(
119119
mode === "web" ? undefined : "desktop-managed",
120120
);
121+
expect(descriptor.capabilities).toMatchObject({
122+
settledWorktreeCleanup: true,
123+
storageCleanupRun: true,
124+
projectWorktreeCleanup: true,
125+
worktreesDirectory: true,
126+
threadAutoSettlementScope: true,
127+
});
121128
}
122129
}),
123130
);

‎apps/server/src/environment/ServerEnvironment.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ export const make = Effect.gen(function* () {
231231
storageCleanupRun: true,
232232
otlpEndpointCheck: true,
233233
projectWorktreeCleanup: true,
234+
settledWorktreeCleanup: true,
234235
worktreesDirectory: true,
235236
threadAutoSettlementScope: true,
236237
threadRestartContinuation: true,

‎apps/server/src/orchestration-v2/CheckpointRestoreSafety.test.ts‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ import * as Layer from "effect/Layer";
1616
import * as Option from "effect/Option";
1717
import * as Path from "effect/Path";
1818
import { symlinksSupported } from "@t3tools/shared/testing/symlinks";
19+
import * as GitWorkflowService from "../git/GitWorkflowService.ts";
1920
import * as CheckpointRollbackService from "./CheckpointRollbackService.ts";
21+
import { isCheckpointRestoreIsolated } from "./CheckpointRestoreSafety.ts";
2022
import * as ThreadCommandExecutor from "./ThreadCommandExecutor.ts";
2123
import { CheckpointServiceV2 } from "./CheckpointService.ts";
2224
import { EventSinkV2 } from "./EventSink.ts";
@@ -105,6 +107,7 @@ it.effect.each([
105107
NodeServices.layer,
106108
ThreadCommandExecutor.layer,
107109
IdAllocator.layer,
110+
Layer.mock(GitWorkflowService.GitWorkflowService)({}),
108111
Layer.mock(ProjectStore.ProjectStoreV2)({
109112
get: () => Effect.succeed(Option.some({ workspaceRoot: parent } as never)),
110113
}),
@@ -197,3 +200,25 @@ it.effect.each([
197200
}
198201
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
199202
);
203+
204+
it.effect("lets rollback recreate a worktree storage cleanup removed", () =>
205+
Effect.gen(function* () {
206+
const fileSystem = yield* FileSystem.FileSystem;
207+
const path = yield* Path.Path;
208+
const parent = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-v2-restore-removed-" });
209+
const worktreePath = path.join(parent, "worktree");
210+
const isolated = yield* isCheckpointRestoreIsolated(
211+
{ id: ThreadId.make("restore-removed"), worktreePath },
212+
{ cwd: worktreePath },
213+
{
214+
fileSystem,
215+
path,
216+
projections: {
217+
getShellSnapshot: () => Effect.succeed({ threads: [], archivedThreads: [] }),
218+
} as never,
219+
projects: {} as never,
220+
},
221+
);
222+
assert.isTrue(isolated);
223+
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
224+
);

‎apps/server/src/orchestration-v2/CheckpointRestoreSafety.ts‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,19 @@ export const isCheckpointRestoreIsolated = Effect.fn("orchestrationV2.isCheckpoi
3333
const worktreePath = thread.worktreePath;
3434
let shared = worktreePath == null;
3535
if (!shared && worktreePath !== null) {
36-
const cwd = yield* fileSystem.realPath(scope.cwd);
37-
const worktreeCwd = yield* fileSystem.realPath(worktreePath);
36+
// Storage cleanup may have removed the worktree; rollback recreates it.
37+
const resolve = (target: string) =>
38+
fileSystem
39+
.realPath(target)
40+
.pipe(
41+
Effect.catch((error) =>
42+
error.reason._tag === "NotFound"
43+
? Effect.succeed(path.resolve(target))
44+
: Effect.fail(error),
45+
),
46+
);
47+
const cwd = yield* resolve(scope.cwd);
48+
const worktreeCwd = yield* resolve(worktreePath);
3849
shared = cwd !== worktreeCwd;
3950
if (!shared) {
4051
const shell = yield* projections.getShellSnapshot();

0 commit comments

Comments
 (0)