Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
72 changes: 72 additions & 0 deletions apps/server/src/orchestration/Layers/CheckpointReactor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,8 @@ describe("CheckpointReactor", () => {
readonly threadWorktreePath?: string | null;
readonly threadBranch?: string | null;
readonly secondThreadSharingWorktree?: boolean;
// Spells the shared worktree differently for the second thread, e.g. through
// a symlink to the first thread's directory.
readonly secondThreadWorktreePath?: (cwd: string) => string;
readonly localStatusRefName?: string | null;
readonly providerSessionCwd?: string;
Expand Down Expand Up @@ -1322,6 +1324,76 @@ describe("CheckpointReactor", () => {
},
);

it("does not adopt a drifted checkout when another thread spells the worktree via a symlink", async () => {
const linkParent = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-checkpoint-link-"));
tempDirs.push(linkParent);
const harness = await createHarness({
seedFilesystemCheckpoints: false,
threadBranch: "t3code/original-branch",
localStatusRefName: "t3code/renamed-by-agent",
secondThreadSharingWorktree: true,
secondThreadWorktreePath: (cwd) => {
const link = NodePath.join(linkParent, "worktree");
// A junction needs no symlink privilege on Windows; POSIX ignores the type.
NodeFS.symlinkSync(cwd, link, "junction");
return link;
},
});

harness.provider.emit({
type: "turn.completed",
eventId: EventId.make("evt-turn-completed-branch-drift-symlinked"),
provider: ProviderDriverKind.make("codex"),
createdAt: "2026-01-01T00:00:00.000Z",
threadId: ThreadId.make("thread-1"),
turnId: asTurnId("turn-branch-drift-symlinked"),
payload: { state: "completed" },
});

await harness.drain();

const snapshot = await harness.readModel();
const thread = snapshot.threads.find((entry) => entry.id === ThreadId.make("thread-1"));
expect(thread?.branch).toBe("t3code/original-branch");
});

it.each([false, true])(
"compares the session cwd with a symlinked worktree when shared=%s",
async (shared) => {
const linkParent = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-checkpoint-link-"));
tempDirs.push(linkParent);
const worktreePath = NodePath.join(linkParent, "worktree");
const pullRequestRefreshCalls: string[] = [];
const harness = await createHarness({
seedFilesystemCheckpoints: false,
threadWorktreePath: worktreePath,
threadBranch: "t3code/original-branch",
localStatusRefName: "t3code/renamed-by-agent",
secondThreadSharingWorktree: shared,
pullRequestRefreshCalls,
});
NodeFS.symlinkSync(harness.cwd, worktreePath, "junction");

harness.provider.emit({
type: "turn.completed",
eventId: EventId.make("evt-turn-completed-symlinked-cwd"),
provider: ProviderDriverKind.make("codex"),
createdAt: "2026-01-01T00:00:00.000Z",
threadId: ThreadId.make("thread-1"),
turnId: asTurnId("turn-symlinked-cwd"),
payload: { state: "completed" },
});

await harness.drain();

const snapshot = await harness.readModel();
const thread = snapshot.threads.find((entry) => entry.id === ThreadId.make("thread-1"));
expect(thread?.branch).toBe(shared ? "t3code/original-branch" : "t3code/renamed-by-agent");
expect(thread?.worktreePath).toBe(worktreePath);
expect(pullRequestRefreshCalls).toEqual(shared ? [] : [harness.cwd]);
},
);

it("does not adopt a temporary placeholder checkout as the thread branch", async () => {
const harness = await createHarness({
seedFilesystemCheckpoints: false,
Expand Down
23 changes: 18 additions & 5 deletions apps/server/src/orchestration/Layers/CheckpointReactor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,13 @@ const make = Effect.gen(function* () {
const startedTurns = new Map<ThreadId, TurnId>();
const pending = new Set<ThreadId>();

// Worktree paths are compared through this so that two spellings of the same
// directory (macOS `/tmp/foo` is a symlink to `/private/tmp/foo`) match. A
// path that cannot be resolved — the directory is gone — keeps its raw form,
// so resolution failures can only narrow matches back to string equality.
const canonicalWorktreePath = (worktreePath: string) =>
fileSystem.realPath(worktreePath).pipe(Effect.orElseSucceed(() => worktreePath));

const appendRevertFailureActivity = (input: {
readonly threadId: ThreadId;
readonly turnCount: number;
Expand Down Expand Up @@ -595,17 +602,23 @@ const make = Effect.gen(function* () {
!thread ||
thread.branch === null ||
thread.branch === checkedOutBranch ||
thread.worktreePath === null ||
thread.worktreePath !== input.cwd
thread.worktreePath === null
) {
return;
}

const canonicalWorktree = yield* canonicalWorktreePath(thread.worktreePath);
const canonicalCwd = yield* canonicalWorktreePath(input.cwd);
if (canonicalWorktree !== canonicalCwd) {
return;
}

const shell = yield* projectionSnapshotQuery.getShellSnapshot();
const worktreeIsShared = shell.threads.some(
(other) => other.id !== thread.id && other.worktreePath === thread.worktreePath,
const otherWorktreePaths = shell.threads.flatMap((other) =>
other.id !== thread.id && other.worktreePath !== null ? [other.worktreePath] : [],
);
if (worktreeIsShared) {
const canonicalOthers = yield* Effect.forEach(otherWorktreePaths, canonicalWorktreePath);
if (canonicalOthers.includes(canonicalWorktree)) {
return;
}

Expand Down
Loading