Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
94279ba
fix(server): restore the checkpoint before rolling back the provider
Adamulek123 Oct 7, 2026
6e615b2
fix(server): report restored files when provider rollback fails
Adamulek123 Oct 7, 2026
b607655
test(server): isolate checkpoint rollback runtime fixture
Adamulek123 Oct 7, 2026
2fbce96
fix(server): tag partial checkpoint rollback failures
Adamulek123 Oct 7, 2026
75081c2
fix(server): stop retrying partial checkpoint restores
Adamulek123 Oct 8, 2026
6752613
chore: merge main into checkpoint rollback fix
Adamulek123 Oct 8, 2026
4703da1
Merge remote-tracking branch 'origin/main' into fix/v2-checkpoint-res…
Adamulek123 Oct 8, 2026
3d5e88b
fix(server): stop checkpoint replay after index refresh defects
Adamulek123 Oct 8, 2026
e237a3d
chore(server): sync checkpoint restore fix with main
Adamulek123 Oct 8, 2026
23ca6ab
test(server): consolidate checkpoint rollback regression coverage
juliusmarminge Oct 19, 2026
eb429e0
fix(server): integrate checkpoint restore ordering with main
juliusmarminge Oct 19, 2026
2308230
fix(server): merge checkpoint restore ordering with main
Adamulek123 Oct 27, 2026
f8ffa08
fix(server): guard checkpoint restore against active turns
Adamulek123 Oct 27, 2026
7257394
fix(server): stop retrying incomplete checkpoint finalization
Adamulek123 Oct 27, 2026
b756b4c
fix(server): preserve provider checkpoint restore support
Adamulek123 Oct 27, 2026
a319205
chore(server): move rollback safety follow-ups into a stack
Adamulek123 Oct 27, 2026
9adb32b
Merge commit '07242abff3cf2004a703ae631eae1416574a5f30' into fix/chec…
Adamulek123 Oct 27, 2026
2d4eb60
merge: resolve checkpoint restore conflicts with main
Adamulek123 Nov 11, 2026
e8025c3
merge: preserve conversation-only rewind alongside restore ordering
Adamulek123 Nov 11, 2026
4db3214
merge: preserve checkpoint restore ordering with current main
Adamulek123 Nov 11, 2026
2daa507
fix(server): stop retrying checkpoint restores after partial mutation
Adamulek123 Nov 11, 2026
fc7a456
merge: keep partial checkpoint restore handling current with main
Adamulek123 Nov 11, 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
4 changes: 2 additions & 2 deletions apps/server/src/checkpointing/CheckpointStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";

import type { CheckpointStoreError } from "./Errors.ts";
import type { VcsCheckpointOps } from "../vcs/VcsDriver.ts";
import type { VcsCheckpointOps, VcsCheckpointPartialRestoreError } from "../vcs/VcsDriver.ts";
import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts";

export interface CaptureCheckpointInput {
Expand Down Expand Up @@ -57,7 +57,7 @@ export class CheckpointStore extends Context.Service<

readonly restoreCheckpoint: (
input: RestoreCheckpointInput,
) => Effect.Effect<boolean, CheckpointStoreError>;
) => Effect.Effect<boolean, CheckpointStoreError | VcsCheckpointPartialRestoreError>;

readonly diffCheckpoints: (
input: DiffCheckpointsInput,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { ProjectionStoreV2, type ProjectionCheckpointRollbackContext } from "./P
import * as ProjectStore from "./ProjectStore.ts";
import { ProviderSessionManagerV2 } from "./ProviderSessionManager.ts";
import { RuntimePolicyV2 } from "./RuntimePolicy.ts";
import * as WorkspaceEntries from "../workspace/WorkspaceEntries.ts";

it.effect.each([
"nested",
Expand Down Expand Up @@ -95,6 +96,7 @@ it.effect.each([
Layer.provide(
Layer.mergeAll(
NodeServices.layer,
Layer.mock(WorkspaceEntries.WorkspaceEntries)({ refresh: () => Effect.void }),
Layer.mock(ServerConfig.ServerConfig)({ worktreesDir: "/synthetic-worktrees" } as never),
ServerSettings.layerTest(),
ThreadCommandExecutor.layer,
Expand Down Expand Up @@ -171,12 +173,13 @@ it.effect.each([
const error = yield* service
.execute({ threadId, providerThreadId, checkpointId, scopeId, restoreFiles })
.pipe(Effect.flip);
assert(error._tag === "CheckpointRollbackExecutionError");
assert.equal(error.reason, "shared-workspace");
assert.deepEqual(calls, []);
assert.equal(yield* fs.readFileString(otherFile), "other thread's uncommitted work");
} else {
yield* service.execute({ threadId, providerThreadId, checkpointId, scopeId, restoreFiles });
assert.deepEqual(calls, restoreFiles ? ["provider", "files"] : ["provider"]);
assert.deepEqual(calls, restoreFiles ? ["files", "provider"] : ["provider"]);
assert.equal(yield* fs.exists(otherFile), !restoreFiles);
}
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
Expand Down
197 changes: 197 additions & 0 deletions apps/server/src/orchestration-v2/CheckpointRollbackService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
MessageId,
NodeId,
type OrchestrationV2DomainEvent,
type OrchestrationV2ThreadProjection,
ProviderDriverKind,
ProviderTurnId,
RunAttemptId,
Expand All @@ -29,6 +30,8 @@ import * as Option from "effect/Option";
import * as Layer from "effect/Layer";

import * as SqlitePersistence from "../persistence/Sqlite.ts";
import * as WorkspaceEntries from "../workspace/WorkspaceEntries.ts";
import * as WorkspacePaths from "../workspace/WorkspacePaths.ts";
import * as ServerConfig from "../config.ts";
import * as ServerSettings from "../serverSettings.ts";
import * as GitWorkflow from "../git/GitWorkflowService.ts";
Expand All @@ -53,6 +56,7 @@ const layerCheckpointRollbackService = CheckpointRollbackService.layer.pipe(
Layer.provide(
Layer.mergeAll(
NodeServices.layer,
Layer.mock(WorkspaceEntries.WorkspaceEntries)({ refresh: () => Effect.void }),
Layer.mock(ServerConfig.ServerConfig)({ worktreesDir: "/synthetic-worktrees" } as never),
ServerSettings.layerTest(),
Layer.mock(GitWorkflow.GitWorkflowService)({}),
Expand Down Expand Up @@ -157,6 +161,7 @@ it.effect.each(["initial", "waiting"])(
},
],
]);
assert(error._tag === "CheckpointRollbackExecutionError");
assert.equal(error.reason, "rollback-target-invalid");
assert.equal(
error.message,
Expand Down Expand Up @@ -224,6 +229,7 @@ it.effect.each([
Layer.mergeAll(
ThreadCommandExecutor.layer,
NodeServices.layer,
Layer.mock(WorkspaceEntries.WorkspaceEntries)({ refresh: () => Effect.void }),
Layer.mock(ServerConfig.ServerConfig)({ worktreesDir } as never),
ServerSettings.layerTest(),
Layer.mock(ProjectStore.ProjectStoreV2)({
Expand Down Expand Up @@ -731,3 +737,194 @@ it.layer(ProjectionStore.layer.pipe(Layer.provideMerge(SqlitePersistence.layerMe
);
},
);

it.effect.each(["success", "restore-fails"] as const)(
"restores files and refreshes the workspace index before the provider: %s",
(outcome) =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries;
const cwd = yield* fs.makeTempDirectoryScoped({ prefix: "t3-rollback-index-" });
const currentFile = path.join(cwd, "rollback-current.txt");
const restoredFile = path.join(cwd, "rollback-restored.txt");
yield* fs.writeFileString(currentFile, "current turn");
const search = () => workspaceEntries.search({ cwd, query: "rollback", limit: 10 });
assert.deepEqual((yield* search()).entries, [{ path: "rollback-current.txt", kind: "file" }]);

const threadId = ThreadId.make("rollback-restore-state");
const providerThreadId = ProviderThreadId.make("rollback-restore-provider");
const providerSessionId = ProviderSessionId.make("rollback-restore-session");
const instanceId = ProviderInstanceId.make("rollback-restore-instance");
const checkpointId = CheckpointId.make("rollback-restore-checkpoint");
const scopeId = CheckpointScopeId.make("rollback-restore-scope");
const laterCheckpointId = CheckpointId.make("rollback-later-checkpoint");
const providerThread = {
id: providerThreadId,
providerSessionId,
providerInstanceId: instanceId,
};
const projection = {
thread: {
worktreePath: cwd,
activeProviderThreadId: providerThreadId,
modelSelection: { instanceId, model: "test" },
},
providerThreads: [providerThread],
providerSessions: [],
providerTurns: [
{
id: "turn-1",
providerThreadId,
runAttemptId: "attempt-1",
ordinal: 1,
status: "completed",
},
],
nodes: [],
attempts: [{ id: "attempt-1", runId: "run-1" }],
checkpoints: [
{ id: checkpointId, scopeId, status: "ready", appRunOrdinal: null },
{ id: laterCheckpointId, scopeId, status: "ready", appRunOrdinal: 1 },
],
checkpointScopes: [{ id: scopeId, cwd }],
runs: [{ id: "run-1", ordinal: 1, status: "completed", rootNodeId: null }],
} as unknown as OrchestrationV2ThreadProjection;
const restoreFailure = new CheckpointService.CheckpointRestoreError({
scopeId,
checkpointId,
cause: new Error("git restore failed"),
});
const providerConversation = ["turn-1"];
let providerCalls = 0;
const deletedRefs: CheckpointId[] = [];
const persistedEvents: string[] = [];
const testLayer = CheckpointRollbackService.layer.pipe(
Layer.provide(
Layer.mergeAll(
IdAllocator.layer,
Layer.mock(GitWorkflow.GitWorkflowService)({}),
Layer.mock(ServerConfig.ServerConfig)({
worktreesDir: "/synthetic-worktrees",
} as never),
ServerSettings.layerTest(),
ThreadCommandExecutor.layer,
Layer.succeed(WorkspaceEntries.WorkspaceEntries, workspaceEntries),
Layer.mock(ProjectStore.ProjectStoreV2)({
get: () => Effect.succeed(unrelatedProject),
}),
Layer.mock(CheckpointService.CheckpointServiceV2)({
restore: () =>
outcome === "restore-fails"
? Effect.fail(restoreFailure)
: Effect.gen(function* () {
yield* fs.remove(currentFile);
yield* fs.writeFileString(restoredFile, "checkpoint turn");
}).pipe(Effect.orDie),
deleteStaleRefs: ({ checkpoints }) =>
Effect.sync(() => {
deletedRefs.push(...checkpoints.map((checkpoint) => checkpoint.id));
}),
}),
Layer.mock(EventSink.EventSinkV2)({
write: ({ events }) =>
Effect.sync(() => {
persistedEvents.push(...events.map((event) => event.type));
return [];
}),
}),
Layer.mock(ProjectionStore.ProjectionStoreV2)({
getThread: () => Effect.succeed(projection.thread),
getCheckpointRollbackContext: () =>
Effect.succeed({
thread: projection.thread,
providerThread,
providerSession: undefined,
checkpoint: projection.checkpoints[0],
scope: projection.checkpointScopes[0],
targetAttempt: undefined,
runsToRollback: projection.runs,
staleCheckpoints: [projection.checkpoints[1]],
rootNodes: [],
rolledBackAttemptIds: [],
providerTurns: projection.providerTurns,
} as never),
getShellSnapshot: () =>
Effect.succeed({
schemaVersion: 1,
snapshotSequence: 0,
threads: [],
archivedThreads: [],
}),
}),
Layer.mock(ProviderSessionManager.ProviderSessionManagerV2)({
open: () =>
Effect.succeed({
rollbackThread: (input: ProviderAdapter.ProviderAdapterV2RollbackThreadInput) =>
Effect.gen(function* () {
providerCalls += 1;
const count = yield* resolveCodexRollbackTurnCount(input);
assert.equal(count, 1);
assert.equal(yield* fs.exists(currentFile).pipe(Effect.orDie), false);
assert.equal(
yield* fs.readFileString(restoredFile).pipe(Effect.orDie),
"checkpoint turn",
);
assert.deepEqual((yield* search()).entries, [
{ path: "rollback-restored.txt", kind: "file" },
]);
providerConversation.splice(providerConversation.length - count, count);
return { providerThread };
}),
} as never),
}),
Layer.mock(RuntimePolicy.RuntimePolicyV2)({
resolve: () => Effect.succeed({} as never),
}),
),
),
);
const service = yield* CheckpointRollbackService.CheckpointRollbackServiceV2.pipe(
Effect.provide(testLayer),
);
const rollback = service.execute({
threadId,
providerThreadId,
checkpointId,
scopeId,
});
if (outcome === "restore-fails") {
const error = yield* Effect.flip(rollback);
assert.equal(error.cause, restoreFailure);
assert.equal(providerCalls, 0);
assert.deepEqual(providerConversation, ["turn-1"]);
assert.deepEqual(persistedEvents, []);
assert.deepEqual(deletedRefs, []);
assert.equal(yield* fs.readFileString(currentFile), "current turn");
assert.equal(yield* fs.exists(restoredFile), false);
assert.deepEqual((yield* search()).entries, [
{ path: "rollback-current.txt", kind: "file" },
]);
} else {
yield* rollback;
assert.equal(providerCalls, 1);
assert.deepEqual(providerConversation, []);
assert.include(persistedEvents, "run.updated");
assert.deepEqual(deletedRefs, [laterCheckpointId]);
assert.equal(yield* fs.exists(currentFile), false);
assert.equal(yield* fs.readFileString(restoredFile), "checkpoint turn");
assert.deepEqual((yield* search()).entries, [
{ path: "rollback-restored.txt", kind: "file" },
]);
}
}).pipe(
Effect.scoped,
Effect.provide(
WorkspaceEntries.layer.pipe(
Layer.provide(WorkspacePaths.layer),
Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-rollback-config-" })),
Layer.provideMerge(NodeServices.layer),
),
),
),
);
Loading
Loading