authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-17 16:41:06-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-17 16:41:06-07:00
logd17f46d7631b178b6834596264f0f44f210d584b
treefca0e071cf1b37397b29feb5cdc5a9f05b811afd
parent67993623afb14c017bb0b959478dc892f9c427da
signature Signed by PGP key B5690EEEBB952194

fix(server): refresh file search outside checkpoint processing (#12308)


2 files changed, 123 insertions(+), 8 deletions(-)

apps/server/src/orchestration/Layers/CheckpointReactor.test.ts+98-4
......@@ -295,6 +295,7 @@ describe("CheckpointReactor", () => {
295295 readonly checkpointLookupFailure?: (
296296 cwd: string,
297297 ) => VcsProcessTimeoutError | VcsProcessSpawnError | undefined;
298 readonly workspaceRefresh?: (cwd: string) => Effect.Effect<void>;
298299 readonly hasSession?: boolean;
299300 readonly seedFilesystemCheckpoints?: boolean;
300301 readonly initializeGit?: boolean;
......@@ -390,10 +391,10 @@ describe("CheckpointReactor", () => {
390391 ).pipe(Layer.provide(VcsDriverRegistry.layer)),
391392 ),
392393 Layer.provideMerge(
393 WorkspaceEntries.layer.pipe(
394 Layer.provide(WorkspacePaths.layer),
395 Layer.provideMerge(VcsDriverRegistry.layer),
396 ),
394 (options?.workspaceRefresh
395 ? Layer.mock(WorkspaceEntries.WorkspaceEntries)({ refresh: options.workspaceRefresh })
396 : WorkspaceEntries.layer
397 ).pipe(Layer.provide(WorkspacePaths.layer), Layer.provideMerge(VcsDriverRegistry.layer)),
397398 ),
398399 Layer.provideMerge(WorkspacePaths.layer),
399400 Layer.provideMerge(VcsProcess.layer),
......@@ -664,6 +665,99 @@ describe("CheckpointReactor", () => {
664665 }),
665666 );
666667
668 effectIt.effect(
669 "finalizes checkpoints in both workspaces while entry refresh is blocked and coalesces later scans",
670 () =>
671 Effect.gen(function* () {
672 const entered = yield* Deferred.make<void>();
673 const release = yield* Deferred.make<void>();
674 const refreshCalls: string[] = [];
675 let secondCwd = "";
676 const harness = yield* Effect.promise(() =>
677 createHarness({
678 seedFilesystemCheckpoints: false,
679 secondThreadSharingWorktree: true,
680 secondThreadWorktreePath: () => {
681 secondCwd = createGitRepository();
682 tempDirs.push(secondCwd);
683 return secondCwd;
684 },
685 workspaceRefresh: (cwd) =>
686 Effect.gen(function* () {
687 refreshCalls.push(cwd);
688 if (refreshCalls.length === 1) {
689 yield* Deferred.succeed(entered, undefined);
690 yield* Deferred.await(release);
691 }
692 }),
693 }),
694 );
695 for (const [index, threadId] of [
696 "thread-1",
697 "thread-2",
698 "thread-1",
699 "thread-1",
700 "thread-1",
701 ].entries()) {
702 const turnId = asTurnId(`turn-refresh-${index}`);
703 const id = ThreadId.make(threadId);
704 harness.provider.emit({
705 type: "turn.started",
706 eventId: EventId.make(`evt-refresh-start-${index}`),
707 provider: ProviderDriverKind.make("codex"),
708 createdAt: "2026-01-01T00:00:00.000Z",
709 threadId: id,
710 turnId,
711 });
712 if (index < 2)
713 expect(yield* harness.nextReceipt).toMatchObject({
714 type: "checkpoint.baseline.captured",
715 threadId: id,
716 });
717 const cwd = threadId === "thread-1" ? harness.cwd : secondCwd;
718 NodeFS.writeFileSync(NodePath.join(cwd, "README.md"), `snapshot ${index}\n`);
719 harness.provider.emit({
720 type: "turn.completed",
721 eventId: EventId.make(`evt-refresh-complete-${index}`),
722 provider: ProviderDriverKind.make("codex"),
723 createdAt: "2026-01-01T00:00:01.000Z",
724 threadId: id,
725 turnId,
726 payload: { state: "completed" },
727 });
728 expect(yield* harness.nextReceipt).toMatchObject({
729 type: "checkpoint.diff.finalized",
730 threadId: id,
731 turnId,
732 });
733 expect(yield* harness.nextReceipt).toMatchObject({
734 type: "turn.processing.quiesced",
735 threadId: id,
736 turnId,
737 });
738 if (index === 0) yield* Deferred.await(entered);
739 }
740 expect(refreshCalls).toEqual([harness.cwd]);
741 expect(
742 gitShowFileAtRef(
743 harness.cwd,
744 checkpointRefForThreadTurn(ThreadId.make("thread-1"), 4),
745 "README.md",
746 ),
747 ).toBe("snapshot 4\n");
748 expect(
749 gitShowFileAtRef(
750 secondCwd,
751 checkpointRefForThreadTurn(ThreadId.make("thread-2"), 1),
752 "README.md",
753 ),
754 ).toBe("snapshot 1\n");
755 yield* Deferred.succeed(release, undefined);
756 yield* Effect.promise(harness.drain);
757 expect(refreshCalls).toEqual([harness.cwd, secondCwd, harness.cwd]);
758 }),
759 );
760
667761 effectIt.effect("captures baseline and large turn summaries before completion receipts", () =>
668762 Effect.gen(function* () {
669763 const harness = yield* Effect.promise(() =>
apps/server/src/orchestration/Layers/CheckpointReactor.ts+25-4
......@@ -93,6 +93,25 @@ const make = Effect.gen(function* () {
9393 const path = yield* Path.Path;
9494 const vcsStatusBroadcaster = yield* VcsStatusBroadcaster;
9595 const pullRequests = yield* PullRequestService.PullRequestService;
96 const queuedEntryRefreshes = new Set<string>();
97 const entryRefreshWorker = yield* makeDrainableWorker((cwd: string) =>
98 Effect.sync(() => queuedEntryRefreshes.delete(cwd)).pipe(
99 Effect.andThen(workspaceEntries.refresh(cwd)),
100 Effect.catchCause((cause) =>
101 Cause.hasInterruptsOnly(cause)
102 ? Effect.failCause(cause)
103 : Effect.logWarning("failed to refresh checkpoint workspace entries", {
104 cwd,
105 }),
106 ),
107 ),
108 );
109 const refreshWorkspaceEntries = Effect.fn("refreshWorkspaceEntries")(function* (cwd: string) {
110 if (queuedEntryRefreshes.has(cwd)) return;
111 queuedEntryRefreshes.add(cwd);
112 yield* entryRefreshWorker.enqueue(cwd);
113 });
114
96115 const startedTurns = new Map<ThreadId, TurnId>();
97116 const pending = new Set<ThreadId>();
98117
......@@ -270,7 +289,7 @@ const make = Effect.gen(function* () {
270289
271290 // Refresh the workspace entry index so the @-mention file picker
272291 // reflects files created or deleted during this turn.
273 yield* workspaceEntries.refresh(input.cwd);
292 yield* refreshWorkspaceEntries(input.cwd);
274293
275294 // Git may have been initialized during this turn, leaving no pre-turn
276295 // snapshot. Keep the completion checkpoint for future turns, but do not
......@@ -629,7 +648,6 @@ const make = Effect.gen(function* () {
629648 ? Effect.failCause(cause)
630649 : Effect.logWarning("failed to refresh git status after turn completion", {
631650 threadId: event.threadId,
632 cause: Cause.pretty(cause),
633651 }),
634652 ),
635653 ),
......@@ -850,7 +868,7 @@ const make = Effect.gen(function* () {
850868
851869 // Refresh the workspace entry index so the @-mention file picker
852870 // reflects the reverted filesystem state.
853 yield* workspaceEntries.refresh(checkpointCwd);
871 yield* refreshWorkspaceEntries(checkpointCwd);
854872 }
855873
856874 const rolledBackTurns = Math.max(0, currentTurnCount - event.payload.turnCount);
......@@ -1041,7 +1059,10 @@ const make = Effect.gen(function* () {
10411059
10421060 return {
10431061 start,
1044 drain: worker.drain.pipe(Effect.andThen(statusRefreshWorker.drain)),
1062 drain: worker.drain.pipe(
1063 Effect.andThen(statusRefreshWorker.drain),
1064 Effect.andThen(entryRefreshWorker.drain),
1065 ),
10451066 } satisfies CheckpointReactorShape;
10461067});
10471068