authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-07-09 00:01:23-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-09 09:01:23+02:00
log3201e00ad4f59cc287517e61a19b069b497c16d2
treec31d6c3becc9b1028abf254c4996e5d6cd605c6a
parente9127658a630c51a0d78a51fe550b28807514eae
signature Signed by PGP key B5690EEEBB952194

[codex] Preserve worktree metadata during branch sync (#3822)

Co-authored-by: codex <codex@users.noreply.github.com>

6 files changed, 140 insertions(+), 5 deletions(-)

apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts+106
......@@ -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+8-2
......@@ -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+13
......@@ -9,6 +9,7 @@ import {
99 resolveLiveThreadBranchUpdate,
1010 resolveQuickAction,
1111 resolveThreadBranchUpdate,
12 resolveThreadBranchMetadataPatch,
1213} from "./GitActionsControl.logic";
1314
1415function status(overrides: Partial<VcsStatusResult> = {}): VcsStatusResult {
......@@ -1111,6 +1112,18 @@ describe("resolveLiveThreadBranchUpdate", () => {
11111112 });
11121113});
11131114
1115describe("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
11141127describe("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+10
......@@ -373,6 +373,16 @@ export function resolveThreadBranchUpdate(
373373 };
374374}
375375
376export 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
376386export function resolveLiveThreadBranchUpdate(input: {
377387 threadBranch: string | null;
378388 gitStatus: VcsStatusResult | null;
apps/web/src/components/GitActionsControl.tsx+2-3
......@@ -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+1
......@@ -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