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