| author | |
| committer | |
| log | 3201e00ad4f59cc287517e61a19b069b497c16d2 |
| tree | c31d6c3becc9b1028abf254c4996e5d6cd605c6a |
| parent | e9127658a630c51a0d78a51fe550b28807514eae |
| signature | Signed by PGP key B5690EEEBB952194 |
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", () => { | ... | @@ -477,6 +477,112 @@ describe("OrchestrationEngine", () => { |
| 477 | await system.dispose(); | 477 | await system.dispose(); |
| 478 | }); | 478 | }); |
| 479 | 479 | ||
| 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 | |||
| 480 | it("records command ack duration using the first committed event type", async () => { | 586 | it("records command ack duration using the first committed event type", async () => { |
| 481 | const system = await createOrchestrationSystem(); | 587 | const system = await createOrchestrationSystem(); |
| 482 | const { engine } = system; | 588 | const { engine } = system; |
apps/server/src/orchestration/decider.ts+8-2| ... | @@ -313,11 +313,17 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" | ... | @@ -313,11 +313,17 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" |
| 313 | } | 313 | } |
| 314 | 314 | ||
| 315 | case "thread.meta.update": { | 315 | case "thread.meta.update": { |
| 316 | yield* requireThread({ | 316 | const thread = yield* requireThread({ |
| 317 | readModel, | 317 | readModel, |
| 318 | command, | 318 | command, |
| 319 | threadId: command.threadId, | 319 | threadId: command.threadId, |
| 320 | }); | 320 | }); |
| 321 | const branch = | ||
| 322 | command.branch !== undefined && | ||
| 323 | command.expectedBranch !== undefined && | ||
| 324 | thread.branch !== command.expectedBranch | ||
| 325 | ? thread.branch | ||
| 326 | : command.branch; | ||
| 321 | const occurredAt = yield* nowIso; | 327 | const occurredAt = yield* nowIso; |
| 322 | return { | 328 | return { |
| 323 | ...(yield* withEventBase({ | 329 | ...(yield* withEventBase({ |
| ... | @@ -333,7 +339,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" | ... | @@ -333,7 +339,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" |
| 333 | ...(command.modelSelection !== undefined | 339 | ...(command.modelSelection !== undefined |
| 334 | ? { modelSelection: command.modelSelection } | 340 | ? { modelSelection: command.modelSelection } |
| 335 | : {}), | 341 | : {}), |
| 336 | ...(command.branch !== undefined ? { branch: command.branch } : {}), | 342 | ...(branch !== undefined ? { branch } : {}), |
| 337 | ...(command.worktreePath !== undefined ? { worktreePath: command.worktreePath } : {}), | 343 | ...(command.worktreePath !== undefined ? { worktreePath: command.worktreePath } : {}), |
| 338 | updatedAt: occurredAt, | 344 | updatedAt: occurredAt, |
| 339 | }, | 345 | }, |
apps/web/src/components/GitActionsControl.logic.test.ts+13| ... | @@ -9,6 +9,7 @@ import { | ... | @@ -9,6 +9,7 @@ import { |
| 9 | resolveLiveThreadBranchUpdate, | 9 | resolveLiveThreadBranchUpdate, |
| 10 | resolveQuickAction, | 10 | resolveQuickAction, |
| 11 | resolveThreadBranchUpdate, | 11 | resolveThreadBranchUpdate, |
| 12 | resolveThreadBranchMetadataPatch, | ||
| 12 | } from "./GitActionsControl.logic"; | 13 | } from "./GitActionsControl.logic"; |
| 13 | 14 | ||
| 14 | function status(overrides: Partial<VcsStatusResult> = {}): VcsStatusResult { | 15 | function status(overrides: Partial<VcsStatusResult> = {}): VcsStatusResult { |
| ... | @@ -1111,6 +1112,18 @@ describe("resolveLiveThreadBranchUpdate", () => { | ... | @@ -1111,6 +1112,18 @@ describe("resolveLiveThreadBranchUpdate", () => { |
| 1111 | }); | 1112 | }); |
| 1112 | }); | 1113 | }); |
| 1113 | 1114 | ||
| 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 | |||
| 1114 | describe("resolveAutoFeatureBranchName", () => { | 1127 | describe("resolveAutoFeatureBranchName", () => { |
| 1115 | it("uses semantic preferred ref names when available", () => { | 1128 | it("uses semantic preferred ref names when available", () => { |
| 1116 | const ref = resolveAutoFeatureBranchName(["main", "feature/other"], "fix toast copy"); | 1129 | const ref = resolveAutoFeatureBranchName(["main", "feature/other"], "fix toast copy"); |
apps/web/src/components/GitActionsControl.logic.ts+10| ... | @@ -373,6 +373,16 @@ export function resolveThreadBranchUpdate( | ... | @@ -373,6 +373,16 @@ export function resolveThreadBranchUpdate( |
| 373 | }; | 373 | }; |
| 374 | } | 374 | } |
| 375 | 375 | ||
| 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 | |||
| 376 | export function resolveLiveThreadBranchUpdate(input: { | 386 | export function resolveLiveThreadBranchUpdate(input: { |
| 377 | threadBranch: string | null; | 387 | threadBranch: string | null; |
| 378 | gitStatus: VcsStatusResult | null; | 388 | gitStatus: VcsStatusResult | null; |
apps/web/src/components/GitActionsControl.tsx+2-3| ... | @@ -45,6 +45,7 @@ import { | ... | @@ -45,6 +45,7 @@ import { |
| 45 | requiresDefaultBranchConfirmation, | 45 | requiresDefaultBranchConfirmation, |
| 46 | resolveDefaultBranchActionDialogCopy, | 46 | resolveDefaultBranchActionDialogCopy, |
| 47 | resolveLiveThreadBranchUpdate, | 47 | resolveLiveThreadBranchUpdate, |
| 48 | resolveThreadBranchMetadataPatch, | ||
| 48 | resolveQuickAction, | 49 | resolveQuickAction, |
| 49 | resolveThreadBranchUpdate, | 50 | resolveThreadBranchUpdate, |
| 50 | } from "./GitActionsControl.logic"; | 51 | } from "./GitActionsControl.logic"; |
| ... | @@ -1033,13 +1034,11 @@ export default function GitActionsControl({ | ... | @@ -1033,13 +1034,11 @@ export default function GitActionsControl({ |
| 1033 | return; | 1034 | return; |
| 1034 | } | 1035 | } |
| 1035 | 1036 | ||
| 1036 | const worktreePath = activeServerThread.worktreePath; | ||
| 1037 | void updateThreadMetadata({ | 1037 | void updateThreadMetadata({ |
| 1038 | environmentId: activeThreadRef.environmentId, | 1038 | environmentId: activeThreadRef.environmentId, |
| 1039 | input: { | 1039 | input: { |
| 1040 | threadId: activeThreadRef.threadId, | 1040 | threadId: activeThreadRef.threadId, |
| 1041 | branch, | 1041 | ...resolveThreadBranchMetadataPatch(branch, activeServerThread.branch), |
| 1042 | worktreePath, | ||
| 1043 | }, | 1042 | }, |
| 1044 | }); | 1043 | }); |
| 1045 | 1044 |
packages/contracts/src/orchestration.ts+1| ... | @@ -552,6 +552,7 @@ const ThreadMetaUpdateCommand = Schema.Struct({ | ... | @@ -552,6 +552,7 @@ const ThreadMetaUpdateCommand = Schema.Struct({ |
| 552 | title: Schema.optional(TrimmedNonEmptyString), | 552 | title: Schema.optional(TrimmedNonEmptyString), |
| 553 | modelSelection: Schema.optional(ModelSelection), | 553 | modelSelection: Schema.optional(ModelSelection), |
| 554 | branch: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)), | 554 | branch: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)), |
| 555 | expectedBranch: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)), | ||
| 555 | worktreePath: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)), | 556 | worktreePath: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)), |
| 556 | }); | 557 | }); |
| 557 | 558 |