diff --git a/apps/server/src/orchestration-v2/PullRequestSyncReactor.test.ts b/apps/server/src/orchestration-v2/PullRequestSyncReactor.test.ts index 0161054781567..6910b3c41198d 100644 --- a/apps/server/src/orchestration-v2/PullRequestSyncReactor.test.ts +++ b/apps/server/src/orchestration-v2/PullRequestSyncReactor.test.ts @@ -1253,7 +1253,7 @@ describe("PullRequestSyncReactor", () => { ), ); - it.effect("leaves a settled thread's links unread until the thread is unsettled", () => + it.effect("reads a settled thread's links once when it settles, then until unsettled", () => Effect.scoped( Effect.gen(function* () { yield* TestClock.setTime(Date.parse(NOW)); @@ -1269,6 +1269,46 @@ describe("PullRequestSyncReactor", () => { yield* Effect.gen(function* () { const reactor = yield* startAndSweep(fixture); + assert.deepStrictEqual(yield* Ref.get(fixture.summaryCalls), []); + + const threadId = ThreadId.make("settled"); + const at = DateTime.makeUnsafe(NOW); + yield* Queue.offer(fixture.domainEvents, { + type: "thread.settled", + id: EventId.make("event:settled"), + threadId, + occurredAt: at, + payload: { + createdBy: "user", + creationSource: "web", + id: threadId, + projectId: PROJECT_ID, + title: "settled", + providerInstanceId: ProviderInstanceId.make("codex"), + modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5" }, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + activeProviderThreadId: null, + lineage: { parentThreadId: null, relationshipToParent: null, rootThreadId: threadId }, + forkedFrom: null, + createdAt: at, + updatedAt: at, + archivedAt: null, + settledOverride: "settled", + settledAt: at, + lastVisitedAt: null, + deletedAt: null, + }, + }); + yield* Queue.take(fixture.snapshotReads); + yield* Queue.take(fixture.snapshotReads); + yield* reactor.drain; + assert.deepStrictEqual( + (yield* Ref.getAndSet(fixture.summaryCalls, [])).map((call) => call.number).toSorted(), + [5, 6], + ); for (let index = 0; index < 15; index += 1) yield* sweepAgain(fixture, reactor); assert.deepStrictEqual(yield* Ref.get(fixture.summaryCalls), []); diff --git a/apps/server/src/orchestration-v2/PullRequestSyncReactor.ts b/apps/server/src/orchestration-v2/PullRequestSyncReactor.ts index 9dffe4babb424..3d28743c325af 100644 --- a/apps/server/src/orchestration-v2/PullRequestSyncReactor.ts +++ b/apps/server/src/orchestration-v2/PullRequestSyncReactor.ts @@ -714,6 +714,9 @@ export const make = Effect.gen(function* () { ended.add(item.id); return Effect.andThen(linked, refreshUnmergedLinks(event.threadId)); } + // Settled threads are not swept, so read their links once to settle with fresh status. + case "thread.settled": + return refreshUnmergedLinks(event.threadId); case "run.updated": return isTerminalRunStatus(event.payload.status) && closeCommands.delete(event.threadId) ? refreshUnmergedLinks(event.threadId) diff --git a/docs/user/source-control.md b/docs/user/source-control.md index 949bddd8afbf0..9421cc698c6d7 100644 --- a/docs/user/source-control.md +++ b/docs/user/source-control.md @@ -298,7 +298,7 @@ Linking and unlinking are available in the web and desktop clients. The **Linked pull requests** panel lists every review and groups stacks. Unlink a review from its row menu. An unlinked stack layer stays out of later syncs. Open linked reviews refresh on the server; closed reviews refresh periodically so reopening one on the host is detected. Merged reviews refresh -when requested. A settled thread's reviews stop refreshing until you unsettle it. With **Auto-settle merged threads** enabled, a thread can settle after every linked +when requested. Settling a thread refreshes its open and closed reviews once; after that they stop refreshing until you unsettle it. With **Auto-settle merged threads** enabled, a thread can settle after every linked review is terminal. An open or unsynced link keeps it active. Ask the agent to watch, monitor, or babysit a pull request and it calls `watch_pull_request`. While