authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-09-06 02:56:31-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-06 02:56:31-07:00
loge0adcc8a24db604c522282edea2823e3ef7a2bc6
treedbbed1073de602e70d0097f7e904a4d88d8f794f
parent62f568b88b58e1e7cf422c21ee2e08f10977b714
signature Signed by PGP key B5690EEEBB952194

fix(server): capture checkpoints before refreshing PR status (#10347)

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

2 files changed, 64 insertions(+), 3 deletions(-)

apps/server/src/orchestration/Layers/CheckpointReactor.test.ts+45-1
...@@ -302,6 +302,7 @@ describe("CheckpointReactor", () => {...@@ -302,6 +302,7 @@ describe("CheckpointReactor", () => {
302 readonly providerName?: ProviderDriverKind;302 readonly providerName?: ProviderDriverKind;
303 readonly gitStatusRefreshCalls?: Array<string>;303 readonly gitStatusRefreshCalls?: Array<string>;
304 readonly pullRequestRefreshCalls?: Array<string>;304 readonly pullRequestRefreshCalls?: Array<string>;
305 readonly pullRequestRefresh?: Effect.Effect<void>;
305 }) {306 }) {
306 const cwd = createGitRepository();307 const cwd = createGitRepository();
307 if (options?.initializeGit === false) {308 if (options?.initializeGit === false) {
...@@ -357,7 +358,7 @@ describe("CheckpointReactor", () => {...@@ -357,7 +358,7 @@ describe("CheckpointReactor", () => {
357 refreshPullRequestStatus: (cwd: string) =>358 refreshPullRequestStatus: (cwd: string) =>
358 Effect.sync(() => {359 Effect.sync(() => {
359 options?.pullRequestRefreshCalls?.push(cwd);360 options?.pullRequestRefreshCalls?.push(cwd);
360 }).pipe(Effect.as(null)),361 }).pipe(Effect.andThen(options?.pullRequestRefresh ?? Effect.void), Effect.as(null)),
361 streamStatus: () => Stream.empty,362 streamStatus: () => Stream.empty,
362 });363 });
363364
...@@ -879,6 +880,49 @@ describe("CheckpointReactor", () => {...@@ -879,6 +880,49 @@ describe("CheckpointReactor", () => {
879 expect(pullRequestRefreshCalls).toEqual([harness.cwd]);880 expect(pullRequestRefreshCalls).toEqual([harness.cwd]);
880 });881 });
881882
883 effectIt.effect("captures files while the pull request lookup is still pending", () =>
884 Effect.gen(function* () {
885 const lookupStarted = yield* Deferred.make<void>();
886 const finishLookup = yield* Deferred.make<void>();
887 const harness = yield* Effect.promise(() =>
888 createHarness({
889 seedFilesystemCheckpoints: false,
890 threadBranch: "t3code/feature",
891 localStatusRefName: "t3code/feature",
892 pullRequestRefresh: Deferred.succeed(lookupStarted, undefined).pipe(
893 Effect.andThen(Deferred.await(finishLookup)),
894 ),
895 }),
896 );
897 NodeFS.writeFileSync(NodePath.join(harness.cwd, "README.md"), "completed turn\n");
898 harness.provider.emit({
899 type: "turn.completed",
900 eventId: EventId.make("evt-turn-completed-slow-pr"),
901 provider: ProviderDriverKind.make("codex"),
902 createdAt: "2026-01-01T00:00:00.000Z",
903 threadId: ThreadId.make("thread-1"),
904 turnId: asTurnId("turn-slow-pr"),
905 payload: { state: "completed" },
906 });
907
908 yield* Deferred.await(lookupStarted);
909 yield* Effect.gen(function* () {
910 expect(yield* harness.nextReceipt).toMatchObject({
911 type: "checkpoint.diff.finalized",
912 turnId: "turn-slow-pr",
913 });
914 expect(
915 gitShowFileAtRef(
916 harness.cwd,
917 checkpointRefForThreadTurn(ThreadId.make("thread-1"), 1),
918 "README.md",
919 ),
920 ).toBe("completed turn\n");
921 }).pipe(Effect.ensuring(Deferred.succeed(finishLookup, undefined)));
922 yield* Effect.promise(harness.drain);
923 }),
924 );
925
882 it("re-asks for the pull request after adopting a drifted checkout", async () => {926 it("re-asks for the pull request after adopting a drifted checkout", async () => {
883 const pullRequestRefreshCalls: string[] = [];927 const pullRequestRefreshCalls: string[] = [];
884 const harness = await createHarness({928 const harness = await createHarness({
apps/server/src/orchestration/Layers/CheckpointReactor.ts+19-2
...@@ -605,6 +605,23 @@ const make = Effect.gen(function* () {...@@ -605,6 +605,23 @@ const make = Effect.gen(function* () {
605 );605 );
606 });606 });
607607
608 // Refreshing git status ends in a remote PR lookup under the vcs status
609 // write lock. Run it on its own worker so file capture for this turn (and
610 // checkpoints for other threads) never wait behind that network call.
611 const statusRefreshWorker = yield* makeDrainableWorker(
612 (event: Extract<ProviderRuntimeEvent, { type: "turn.completed" }>) =>
613 refreshLocalGitStatusFromTurnCompletion(event).pipe(
614 Effect.catchCause((cause) =>
615 Cause.hasInterruptsOnly(cause)
616 ? Effect.failCause(cause)
617 : Effect.logWarning("failed to refresh git status after turn completion", {
618 threadId: event.threadId,
619 cause: Cause.pretty(cause),
620 }),
621 ),
622 ),
623 );
624
608 const ensurePreTurnBaselineFromDomainTurnStart = Effect.fn(625 const ensurePreTurnBaselineFromDomainTurnStart = Effect.fn(
609 "ensurePreTurnBaselineFromDomainTurnStart",626 "ensurePreTurnBaselineFromDomainTurnStart",
610 )(function* (627 )(function* (
...@@ -853,7 +870,7 @@ const make = Effect.gen(function* () {...@@ -853,7 +870,7 @@ const make = Effect.gen(function* () {
853 const isTrackedTurn = sameId(startedTurnId, turnId);870 const isTrackedTurn = sameId(startedTurnId, turnId);
854 if (isTrackedTurn) startedTurns.delete(event.threadId);871 if (isTrackedTurn) startedTurns.delete(event.threadId);
855 if (event.type === "turn.completed") {872 if (event.type === "turn.completed") {
856 yield* refreshLocalGitStatusFromTurnCompletion(event);873 yield* statusRefreshWorker.enqueue(event);
857 }874 }
858 if (875 if (
859 turnId !== null &&876 turnId !== null &&
...@@ -944,7 +961,7 @@ const make = Effect.gen(function* () {...@@ -944,7 +961,7 @@ const make = Effect.gen(function* () {
944961
945 return {962 return {
946 start,963 start,
947 drain: worker.drain,964 drain: worker.drain.pipe(Effect.andThen(statusRefreshWorker.drain)),
948 } satisfies CheckpointReactorShape;965 } satisfies CheckpointReactorShape;
949});966});
950967