Skip to content

Commit 3201e00

Browse files
[codex] Preserve worktree metadata during branch sync (#3822)
Co-authored-by: codex <codex@users.noreply.github.com>
1 parent e912765 commit 3201e00

6 files changed

Lines changed: 140 additions & 5 deletions

File tree

apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,112 @@ describe("OrchestrationEngine", () => {
477477
await system.dispose();
478478
});
479479

480+
it("does not regress a generated branch to a stale temporary worktree branch", async () => {
481+
const system = await createOrchestrationSystem();
482+
const { engine } = system;
483+
const createdAt = now();
484+
485+
await system.run(
486+
engine.dispatch({
487+
type: "project.create",
488+
commandId: CommandId.make("cmd-branch-race-project-create"),
489+
projectId: asProjectId("project-branch-race"),
490+
title: "Branch Race Project",
491+
workspaceRoot: "/tmp/project-branch-race",
492+
defaultModelSelection: {
493+
instanceId: ProviderInstanceId.make("codex"),
494+
model: "gpt-5-codex",
495+
},
496+
createdAt,
497+
}),
498+
);
499+
await system.run(
500+
engine.dispatch({
501+
type: "thread.create",
502+
commandId: CommandId.make("cmd-branch-race-thread-create"),
503+
threadId: ThreadId.make("thread-branch-race"),
504+
projectId: asProjectId("project-branch-race"),
505+
title: "Branch Race Thread",
506+
modelSelection: {
507+
instanceId: ProviderInstanceId.make("codex"),
508+
model: "gpt-5-codex",
509+
},
510+
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
511+
runtimeMode: "approval-required",
512+
branch: "t3code/generated-branch-name",
513+
worktreePath: "/tmp/project-branch-race-worktree",
514+
createdAt,
515+
}),
516+
);
517+
518+
await system.run(
519+
engine.dispatch({
520+
type: "thread.meta.update",
521+
commandId: CommandId.make("cmd-stale-temporary-branch-sync"),
522+
threadId: ThreadId.make("thread-branch-race"),
523+
branch: "t3code/1234abcd",
524+
expectedBranch: "t3code/1234abcd",
525+
}),
526+
);
527+
528+
const snapshot = await system.readModel();
529+
expect(snapshot.threads[0]?.branch).toBe("t3code/generated-branch-name");
530+
await system.dispose();
531+
});
532+
533+
it("allows authoritative worktree bootstrap to assign a temporary branch", async () => {
534+
const system = await createOrchestrationSystem();
535+
const { engine } = system;
536+
const createdAt = now();
537+
538+
await system.run(
539+
engine.dispatch({
540+
type: "project.create",
541+
commandId: CommandId.make("cmd-worktree-bootstrap-project-create"),
542+
projectId: asProjectId("project-worktree-bootstrap"),
543+
title: "Worktree Bootstrap Project",
544+
workspaceRoot: "/tmp/project-worktree-bootstrap",
545+
defaultModelSelection: {
546+
instanceId: ProviderInstanceId.make("codex"),
547+
model: "gpt-5-codex",
548+
},
549+
createdAt,
550+
}),
551+
);
552+
await system.run(
553+
engine.dispatch({
554+
type: "thread.create",
555+
commandId: CommandId.make("cmd-worktree-bootstrap-thread-create"),
556+
threadId: ThreadId.make("thread-worktree-bootstrap"),
557+
projectId: asProjectId("project-worktree-bootstrap"),
558+
title: "Worktree Bootstrap Thread",
559+
modelSelection: {
560+
instanceId: ProviderInstanceId.make("codex"),
561+
model: "gpt-5-codex",
562+
},
563+
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
564+
runtimeMode: "approval-required",
565+
branch: "main",
566+
worktreePath: null,
567+
createdAt,
568+
}),
569+
);
570+
await system.run(
571+
engine.dispatch({
572+
type: "thread.meta.update",
573+
commandId: CommandId.make("cmd-authoritative-worktree-bootstrap"),
574+
threadId: ThreadId.make("thread-worktree-bootstrap"),
575+
branch: "t3code/1234abcd",
576+
worktreePath: "/tmp/project-worktree-bootstrap-worktree",
577+
}),
578+
);
579+
580+
const snapshot = await system.readModel();
581+
expect(snapshot.threads[0]?.branch).toBe("t3code/1234abcd");
582+
expect(snapshot.threads[0]?.worktreePath).toBe("/tmp/project-worktree-bootstrap-worktree");
583+
await system.dispose();
584+
});
585+
480586
it("records command ack duration using the first committed event type", async () => {
481587
const system = await createOrchestrationSystem();
482588
const { engine } = system;

apps/server/src/orchestration/decider.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -313,11 +313,17 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
313313
}
314314

315315
case "thread.meta.update": {
316-
yield* requireThread({
316+
const thread = yield* requireThread({
317317
readModel,
318318
command,
319319
threadId: command.threadId,
320320
});
321+
const branch =
322+
command.branch !== undefined &&
323+
command.expectedBranch !== undefined &&
324+
thread.branch !== command.expectedBranch
325+
? thread.branch
326+
: command.branch;
321327
const occurredAt = yield* nowIso;
322328
return {
323329
...(yield* withEventBase({
@@ -333,7 +339,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
333339
...(command.modelSelection !== undefined
334340
? { modelSelection: command.modelSelection }
335341
: {}),
336-
...(command.branch !== undefined ? { branch: command.branch } : {}),
342+
...(branch !== undefined ? { branch } : {}),
337343
...(command.worktreePath !== undefined ? { worktreePath: command.worktreePath } : {}),
338344
updatedAt: occurredAt,
339345
},

apps/web/src/components/GitActionsControl.logic.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
resolveLiveThreadBranchUpdate,
1010
resolveQuickAction,
1111
resolveThreadBranchUpdate,
12+
resolveThreadBranchMetadataPatch,
1213
} from "./GitActionsControl.logic";
1314

1415
function status(overrides: Partial<VcsStatusResult> = {}): VcsStatusResult {
@@ -1111,6 +1112,18 @@ describe("resolveLiveThreadBranchUpdate", () => {
11111112
});
11121113
});
11131114

1115+
describe("resolveThreadBranchMetadataPatch", () => {
1116+
it("does not overwrite worktree metadata while reconciling a branch", () => {
1117+
assert.deepEqual(
1118+
resolveThreadBranchMetadataPatch("feature/current-ref", "feature/previous-ref"),
1119+
{
1120+
branch: "feature/current-ref",
1121+
expectedBranch: "feature/previous-ref",
1122+
},
1123+
);
1124+
});
1125+
});
1126+
11141127
describe("resolveAutoFeatureBranchName", () => {
11151128
it("uses semantic preferred ref names when available", () => {
11161129
const ref = resolveAutoFeatureBranchName(["main", "feature/other"], "fix toast copy");

apps/web/src/components/GitActionsControl.logic.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,16 @@ export function resolveThreadBranchUpdate(
373373
};
374374
}
375375

376+
export function resolveThreadBranchMetadataPatch(
377+
branch: string | null,
378+
expectedBranch: string | null,
379+
): {
380+
branch: string | null;
381+
expectedBranch: string | null;
382+
} {
383+
return { branch, expectedBranch };
384+
}
385+
376386
export function resolveLiveThreadBranchUpdate(input: {
377387
threadBranch: string | null;
378388
gitStatus: VcsStatusResult | null;

apps/web/src/components/GitActionsControl.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import {
4545
requiresDefaultBranchConfirmation,
4646
resolveDefaultBranchActionDialogCopy,
4747
resolveLiveThreadBranchUpdate,
48+
resolveThreadBranchMetadataPatch,
4849
resolveQuickAction,
4950
resolveThreadBranchUpdate,
5051
} from "./GitActionsControl.logic";
@@ -1033,13 +1034,11 @@ export default function GitActionsControl({
10331034
return;
10341035
}
10351036

1036-
const worktreePath = activeServerThread.worktreePath;
10371037
void updateThreadMetadata({
10381038
environmentId: activeThreadRef.environmentId,
10391039
input: {
10401040
threadId: activeThreadRef.threadId,
1041-
branch,
1042-
worktreePath,
1041+
...resolveThreadBranchMetadataPatch(branch, activeServerThread.branch),
10431042
},
10441043
});
10451044

packages/contracts/src/orchestration.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,7 @@ const ThreadMetaUpdateCommand = Schema.Struct({
552552
title: Schema.optional(TrimmedNonEmptyString),
553553
modelSelection: Schema.optional(ModelSelection),
554554
branch: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)),
555+
expectedBranch: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)),
555556
worktreePath: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)),
556557
});
557558

0 commit comments

Comments
 (0)