Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
3694acf
feat(storage): clean up settled thread worktrees
Bil0000 Oct 3, 2026
5b28630
fix(server): guard provider startup against worktree cleanup
Bil0000 Oct 4, 2026
fa441e3
fix(server): release startup leases before provider work
Bil0000 Oct 4, 2026
254eca7
fix(server): restore cleaned worktrees before checkpoint rollback
Bil0000 Oct 4, 2026
ae7a9e7
fix(server): repair worktree cleanup test types on main
Bil0000 Oct 4, 2026
77fc976
fix(server): use physical worktree paths for cleanup leases
Bil0000 Oct 4, 2026
5be57b7
fix(server): refresh workspace state after acquiring its lease
Bil0000 Oct 4, 2026
335659c
fix(server): warn when cleanup cannot resolve a session workspace
Bil0000 Oct 4, 2026
19b891e
perf(server): select workspace leases with thread-only reads
Bil0000 Oct 4, 2026
dfd163f
test(server): preserve worktrees when thread paths cannot resolve
Bil0000 Oct 4, 2026
a0c0406
fix(storage): update settled cleanup for current main
Bil0000 Oct 14, 2026
faaaab3
fix(web): allow manual settled-worktree cleanup
Bil0000 Oct 14, 2026
c81c14e
refactor(storage): simplify settled worktree cleanup
maria-rcks Oct 15, 2026
0e75b7e
fix(storage): let rollback reach removed worktrees and leave settle s…
maria-rcks Oct 15, 2026
dfc7587
fix(storage): give settle scripts a head start and stop detached sess…
maria-rcks Oct 15, 2026
9b1a598
fix(storage): time the settle grace from the settle and sweep again a…
maria-rcks Oct 15, 2026
27ef12b
fix(storage): hold every cleanup rule for the settle script and end s…
maria-rcks Oct 15, 2026
7798267
fix(storage): leave settled cleanup to regular sweeps and trust live …
maria-rcks Oct 15, 2026
6dd347f
fix(storage): check session liveness without renewing its idle timer
maria-rcks Oct 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/server/src/environment/ServerEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ export const make = Effect.gen(function* () {
threadAutoSettlement: true,
storageCleanup: true,
projectWorktreeCleanup: true,
settledWorktreeCleanup: true,
threadRestartContinuation: true,
projectSettingsOverrides: true,
threadSnooze: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Path from "effect/Path";
import { symlinksSupported } from "@t3tools/shared/testing/symlinks";
import { GitWorkflowService } from "../git/GitWorkflowService.ts";
import {
CheckpointRollbackServiceV2,
layer as rollbackLayer,
Expand Down Expand Up @@ -106,6 +107,7 @@ it.effect.each([
Layer.mergeAll(
NodeServices.layer,
idAllocatorLayer,
Layer.mock(GitWorkflowService)({}),
Layer.mock(ProjectStore.ProjectStoreV2)({
get: () => Effect.succeed(Option.some({ workspaceRoot: parent } as never)),
}),
Expand Down
180 changes: 170 additions & 10 deletions apps/server/src/orchestration-v2/CheckpointRollbackService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,24 @@ import {
CheckpointId,
CheckpointScopeId,
type OrchestrationV2ThreadProjection,
ProjectId,
ProviderInstanceId,
ProviderSessionId,
ProviderThreadId,
ThreadId,
} from "@t3tools/contracts";
import * as Effect from "effect/Effect";
import * as Deferred from "effect/Deferred";
import * as Fiber from "effect/Fiber";
import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";
import * as Option from "effect/Option";
import { symlinksSupported } from "@t3tools/shared/testing/symlinks";
import * as Layer from "effect/Layer";

import * as GitWorkflow from "../git/GitWorkflowService.ts";
import { withWorkspaceLease } from "../workspace/workspaceLease.ts";
import { ProviderWorkspaceMissingError } from "../provider/Errors.ts";
import { resolveCodexRollbackTurnCount } from "./Adapters/CodexAdapterV2.ts";
import { isCheckpointRestoreIsolated } from "./CheckpointRestoreSafety.ts";
import * as CheckpointService from "./CheckpointService.ts";
Expand All @@ -36,6 +42,7 @@ const checkpointRollbackServiceLayer = CheckpointRollbackService.layer.pipe(
Layer.provide(
Layer.mergeAll(
NodeServices.layer,
Layer.mock(GitWorkflow.GitWorkflowService)({}),
Layer.mock(ProjectStore.ProjectStoreV2)({ get: () => Effect.succeed(unrelatedProject) }),
),
),
Expand Down Expand Up @@ -263,7 +270,7 @@ it.effect("rejects a rollback when provider selection changed before execution",
}).pipe(Effect.provide(testLayer));
});

it.effect("reports a missing provider turn as a structured rollback failure", () => {
it.effect("rejects an unavailable rollback target before opening a session", () => {
const threadId = ThreadId.make("thread:rollback-provider-turn-unavailable");
const providerThreadId = ProviderThreadId.make(
"provider-thread:rollback-provider-turn-unavailable",
Expand All @@ -275,9 +282,11 @@ it.effect("reports a missing provider turn as a structured rollback failure", ()
const scopeId = CheckpointScopeId.make("checkpoint-scope:rollback-provider-turn-unavailable");
const providerInstanceId = ProviderInstanceId.make("provider_rollback_provider_turn_unavailable");
const restore = vi.fn(() => Effect.die("checkpoint restore must not run"));
const open = vi.fn(() => Effect.die("provider session open must not run"));
const projection = {
thread: {
worktreePath: process.cwd(),
worktreePath: "/tmp/missing-invalid-rollback-worktree",
branch: "feature/rollback",
activeProviderThreadId: providerThreadId,
modelSelection: { instanceId: providerInstanceId, model: "test-model" },
},
Expand Down Expand Up @@ -306,7 +315,7 @@ it.effect("reports a missing provider turn as a structured rollback failure", ()
}),
}),
Layer.mock(ProviderSessionManager.ProviderSessionManagerV2)({
open: () => Effect.succeed({} as never),
open,
}),
Layer.mock(RuntimePolicy.RuntimePolicyV2)({
resolve: () => Effect.succeed({} as never),
Expand All @@ -318,14 +327,9 @@ it.effect("reports a missing provider turn as a structured rollback failure", ()
return Effect.gen(function* () {
const service = yield* CheckpointRollbackService.CheckpointRollbackServiceV2;
const error = yield* service
.execute({
threadId,
providerThreadId,
checkpointId,
scopeId,
})
.execute({ threadId, providerThreadId, checkpointId, scopeId, restoreFiles: false })
.pipe(Effect.flip);

assert.equal(open.mock.calls.length, 0);
assert.equal(error.reason, "provider-turn-unavailable");
assert.equal(
error.message,
Expand All @@ -336,6 +340,162 @@ it.effect("reports a missing provider turn as a structured rollback failure", ()
}).pipe(Effect.provide(testLayer));
});

it.effect.each([
{ order: "normal", restoreFiles: true },
{ order: "cleanup-first", restoreFiles: true },
{ order: "cleanup-first", restoreFiles: false },
{ order: "rollback-first", restoreFiles: true },
])("locks rollback with %s", ({ order, restoreFiles }) =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const workspaceRoot = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-rollback-" });
const worktreePath = path.join(workspaceRoot, "worktree");
yield* fileSystem.makeDirectory(worktreePath);
const threadId = ThreadId.make("rollback-worktree");
const projectId = ProjectId.make("rollback-worktree-project");
const providerThreadId = ProviderThreadId.make("rollback-worktree-provider-thread");
const providerSessionId = ProviderSessionId.make("rollback-worktree-provider-session");
const providerInstanceId = ProviderInstanceId.make("rollback-worktree-provider-instance");
const checkpointId = CheckpointId.make("rollback-worktree-checkpoint");
const scopeId = CheckpointScopeId.make("rollback-worktree-scope");
const branch = "feature/rollback";
const release = Deferred.makeUnsafe<void>();
const openEntered = Deferred.makeUnsafe<void>();
const calls: string[] = [];
let cleanupEntered = false;
const providerThread = { id: providerThreadId, providerSessionId, providerInstanceId };
const projection = {
thread: {
id: threadId,
projectId,
worktreePath,
branch,
activeProviderThreadId: providerThreadId,
modelSelection: { instanceId: providerInstanceId, model: "test-model" },
},
providerThreads: [providerThread],
providerSessions: [],
checkpoints: [{ id: checkpointId, scopeId, status: "ready", appRunOrdinal: 0 }],
checkpointScopes: [{ id: scopeId, cwd: worktreePath }],
runs: [],
attempts: [],
nodes: [],
providerTurns: [],
} as unknown as OrchestrationV2ThreadProjection;
const testLayer = CheckpointRollbackService.layer.pipe(
Layer.provide(
Layer.mergeAll(
NodeServices.layer,
Layer.mock(ProjectStore.ProjectStoreV2)({
get: (id) => {
assert.equal(id, projectId);
return Effect.succeed(Option.some({ workspaceRoot } as never));
},
}),
Layer.mock(GitWorkflow.GitWorkflowService)({
pruneWorktrees: (input) =>
Effect.sync(() => {
assert.deepEqual(input, { cwd: workspaceRoot });
calls.push("prune");
}),
createWorktree: (input) =>
Effect.gen(function* () {
assert.deepEqual(input, {
cwd: workspaceRoot,
refName: branch,
path: worktreePath,
});
calls.push("create");
yield* fileSystem.makeDirectory(worktreePath).pipe(Effect.orDie);
return {} as never;
}),
}),
Layer.mock(CheckpointService.CheckpointServiceV2)({
restore: () =>
Effect.gen(function* () {
assert.isTrue(yield* fileSystem.exists(worktreePath).pipe(Effect.orDie));
calls.push("files");
}),
}),
Layer.mock(EventSink.EventSinkV2)({
write: () =>
Effect.sync(() => {
calls.push("projection");
return [];
}),
}),
IdAllocator.layer,
Layer.mock(ProjectionStore.ProjectionStoreV2)({
getThreadRecords: () => Effect.succeed(projection),
getShellSnapshot: () =>
Effect.succeed({
schemaVersion: 1,
snapshotSequence: 0,
threads: [],
archivedThreads: [],
}),
}),
Layer.mock(ProviderSessionManager.ProviderSessionManagerV2)({
open: () =>
Effect.gen(function* () {
if (!(yield* fileSystem.exists(worktreePath).pipe(Effect.orDie))) {
return yield* new ProviderWorkspaceMissingError({ threadId, cwd: worktreePath });
}
calls.push("session");
yield* Deferred.succeed(openEntered, undefined);
if (order === "rollback-first") yield* Deferred.await(release);
return {} as never;
}),
}),
Layer.mock(RuntimePolicy.RuntimePolicyV2)({ resolve: () => Effect.succeed({} as never) }),
),
),
);
yield* Effect.gen(function* () {
const service = yield* CheckpointRollbackService.CheckpointRollbackServiceV2;
const cleanupEffect = withWorkspaceLease(
worktreePath,
Effect.gen(function* () {
cleanupEntered = true;
if (order === "cleanup-first") yield* Deferred.await(release);
yield* fileSystem.remove(worktreePath, { recursive: true });
calls.push("cleanup");
}),
);
const cleanup =
order === "cleanup-first"
? yield* cleanupEffect.pipe(Effect.forkChild({ startImmediately: true }))
: undefined;
const rollback = yield* service
.execute({ threadId, providerThreadId, checkpointId, scopeId, restoreFiles })
.pipe(Effect.forkChild({ startImmediately: true }));
if (order === "cleanup-first") assert.deepEqual(calls, []);
if (order === "rollback-first") yield* Deferred.await(openEntered);
const laterCleanup =
order === "rollback-first"
? yield* cleanupEffect.pipe(Effect.forkChild({ startImmediately: true }))
: undefined;
if (order === "rollback-first") assert.isFalse(cleanupEntered);
yield* Deferred.succeed(release, undefined);
if (cleanup !== undefined) yield* Fiber.join(cleanup);
yield* Fiber.join(rollback);
if (laterCleanup !== undefined) yield* Fiber.join(laterCleanup);
const rollbackCalls = restoreFiles
? ["session", "files", "projection"]
: ["session", "projection"];
assert.deepEqual(
calls,
order === "cleanup-first"
? ["cleanup", "prune", "create", ...rollbackCalls]
: order === "rollback-first"
? [...rollbackCalls, "cleanup"]
: rollbackCalls,
);
}).pipe(Effect.provide(testLayer));
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
);

it.effect.each([
{ restoreFiles: true, shared: "none" },
{ restoreFiles: false, shared: "root" },
Expand Down
Loading
Loading