From b80507d4bda04d7194976520dc50c5a577585e9a Mon Sep 17 00:00:00 2001 From: adityagarud Date: Sun, 6 Sep 2026 05:34:30 +0000 Subject: [PATCH 1/2] fix(server): skip disabled settlement lookups --- .../ThreadSettlementReactor.test.ts | 58 ++++++++++++++++++- .../orchestration/ThreadSettlementReactor.ts | 4 ++ 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/apps/server/src/orchestration/ThreadSettlementReactor.test.ts b/apps/server/src/orchestration/ThreadSettlementReactor.test.ts index eefc18f7b461..988e12505363 100644 --- a/apps/server/src/orchestration/ThreadSettlementReactor.test.ts +++ b/apps/server/src/orchestration/ThreadSettlementReactor.test.ts @@ -281,6 +281,58 @@ const startHarness = Effect.fn("startThreadSettlementHarness")(function* ( }); describe("ThreadSettlementReactor", () => { + it.effect("skips PR work on startup, timer and merge sweeps when settlement is disabled", () => + Effect.scoped( + Effect.gen(function* () { + yield* TestClock.setTime(Date.parse(NOW)); + const fixture = yield* makeHarness({ + snapshot: makeSnapshot([ + makeThread("branch-thread", { branch: "feature" }), + makeThread("linked-thread", { + linkedPullRequest: { + projectId: PROJECT_ID, + repository: "owner/repository", + number: 42, + url: "https://example.test/owner/repository/pull/42", + }, + }), + ]), + settings: { + ...DEFAULT_SERVER_SETTINGS, + sidebarAutoSettleAfterDays: null, + sidebarAutoSettleOnMerge: false, + }, + }); + + yield* Effect.gen(function* () { + const reactor = yield* ThreadSettlementReactor.ThreadSettlementReactor; + yield* startHarness(reactor, fixture.activation, fixture.snapshotReads); + yield* TestClock.adjust("1 minute"); + yield* Queue.take(fixture.snapshotReads); + yield* reactor.drain; + yield* fixture.publishMerge; + yield* Queue.take(fixture.snapshotReads); + yield* reactor.drain; + + assert.deepStrictEqual(yield* Ref.get(fixture.branchCalls), []); + assert.deepStrictEqual(yield* Ref.get(fixture.summaryCalls), []); + assert.deepStrictEqual(yield* Ref.get(fixture.invalidatedCwds), []); + assert.deepStrictEqual(yield* Ref.get(fixture.commands), []); + + yield* fixture.updateSettings({ sidebarAutoSettleAfterDays: 1 }); + yield* Queue.take(fixture.snapshotReads); + yield* reactor.drain; + assert.strictEqual((yield* Ref.get(fixture.branchCalls)).length, 1); + assert.strictEqual((yield* Ref.get(fixture.summaryCalls)).length, 1); + assert.deepStrictEqual( + (yield* Ref.get(fixture.commands)).map((command) => command.threadId).toSorted(), + [ThreadId.make("branch-thread"), ThreadId.make("linked-thread")], + ); + }).pipe(Effect.provide(fixture.layer)); + }), + ), + ); + it.effect("starts without clients and skips protected threads before pull request lookup", () => Effect.scoped( Effect.gen(function* () { @@ -600,14 +652,14 @@ describe("ThreadSettlementReactor", () => { Effect.tap((count) => count === 1 ? Deferred.succeed(firstLookupStarted, undefined) - : count === 3 + : count === 2 ? Deferred.succeed(laterLookupStarted, undefined) : Effect.void, ), Effect.tap((count) => count === 1 ? Deferred.await(releaseFirstLookup) - : count === 3 + : count === 2 ? Deferred.await(releaseLaterLookup) : Effect.void, ), @@ -643,7 +695,7 @@ describe("ThreadSettlementReactor", () => { yield* reactor.drain; assert.strictEqual(yield* Ref.get(fixture.snapshotReadCount), 3); - assert.strictEqual(yield* Ref.get(lookupCount), 3); + assert.strictEqual(yield* Ref.get(lookupCount), 2); assert.deepStrictEqual( (yield* Ref.get(fixture.commands)).map((command) => command.threadId), [ThreadId.make("settings-thread")], diff --git a/apps/server/src/orchestration/ThreadSettlementReactor.ts b/apps/server/src/orchestration/ThreadSettlementReactor.ts index 70de3c41d7e9..465bf0685786 100644 --- a/apps/server/src/orchestration/ThreadSettlementReactor.ts +++ b/apps/server/src/orchestration/ThreadSettlementReactor.ts @@ -44,6 +44,10 @@ export const make = Effect.gen(function* () { mergedPullRequest: PullRequestService.PullRequestMergeEvent | null, ) { const snapshot = yield* snapshots.getShellSnapshot(); + const settings = yield* settingsService.getSettings; + if (!settings.sidebarAutoSettleOnMerge && settings.sidebarAutoSettleAfterDays === null) { + return; + } const now = DateTime.formatIso(yield* DateTime.now); const projects = new Map(snapshot.projects.map((project) => [project.id, project])); // A merge event re-sweeps every candidate, not just the threads linked to From 68a4096cad7fc1ca2a1b42bce311c2a7935cc34d Mon Sep 17 00:00:00 2001 From: adityagarud Date: Sun, 6 Sep 2026 05:39:26 +0000 Subject: [PATCH 2/2] fix(server): skip disabled settlement snapshots --- .../ThreadSettlementReactor.test.ts | 24 +++++++++++++------ .../orchestration/ThreadSettlementReactor.ts | 2 +- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/apps/server/src/orchestration/ThreadSettlementReactor.test.ts b/apps/server/src/orchestration/ThreadSettlementReactor.test.ts index 988e12505363..fcba210cf12b 100644 --- a/apps/server/src/orchestration/ThreadSettlementReactor.test.ts +++ b/apps/server/src/orchestration/ThreadSettlementReactor.test.ts @@ -149,6 +149,7 @@ const makeHarness = Effect.fn("makeThreadSettlementHarness")(function* (options: const snapshotReadCount = yield* Ref.make(0); const snapshotReads = yield* Queue.unbounded(); const settings = yield* Ref.make(options.settings ?? DEFAULT_SERVER_SETTINGS); + const settingsReads = yield* Queue.unbounded(); const settingsChanges = yield* PubSub.unbounded(); const mergedPullRequests = yield* PubSub.unbounded(); const commands = yield* Ref.make>([]); @@ -208,7 +209,7 @@ const makeHarness = Effect.fn("makeThreadSettlementHarness")(function* (options: const serverSettings = ServerSettingsService.of({ start: Effect.void, ready: Effect.void, - getSettings: Ref.get(settings), + getSettings: Ref.get(settings).pipe(Effect.tap((value) => Queue.offer(settingsReads, value))), updateSettings, streamChanges: Stream.fromPubSub(settingsChanges), subscribeChanges: PubSub.subscribe(settingsChanges).pipe( @@ -253,6 +254,7 @@ const makeHarness = Effect.fn("makeThreadSettlementHarness")(function* (options: snapshots, snapshotReadCount, snapshotReads, + settingsReads, commands, branchCalls, summaryCalls, @@ -306,18 +308,23 @@ describe("ThreadSettlementReactor", () => { yield* Effect.gen(function* () { const reactor = yield* ThreadSettlementReactor.ThreadSettlementReactor; - yield* startHarness(reactor, fixture.activation, fixture.snapshotReads); + yield* reactor.start(); + yield* Queue.take(fixture.settingsReads); + yield* Deferred.succeed(fixture.activation, undefined); + yield* Queue.take(fixture.settingsReads); + yield* reactor.drain; yield* TestClock.adjust("1 minute"); - yield* Queue.take(fixture.snapshotReads); + yield* Queue.take(fixture.settingsReads); yield* reactor.drain; yield* fixture.publishMerge; - yield* Queue.take(fixture.snapshotReads); + yield* Queue.take(fixture.settingsReads); yield* reactor.drain; assert.deepStrictEqual(yield* Ref.get(fixture.branchCalls), []); assert.deepStrictEqual(yield* Ref.get(fixture.summaryCalls), []); assert.deepStrictEqual(yield* Ref.get(fixture.invalidatedCwds), []); assert.deepStrictEqual(yield* Ref.get(fixture.commands), []); + assert.strictEqual(yield* Ref.get(fixture.snapshotReadCount), 0); yield* fixture.updateSettings({ sidebarAutoSettleAfterDays: 1 }); yield* Queue.take(fixture.snapshotReads); @@ -679,13 +686,16 @@ describe("ThreadSettlementReactor", () => { yield* Deferred.succeed(fixture.activation, undefined); yield* Queue.take(fixture.snapshotReads); yield* Deferred.await(firstLookupStarted); + yield* Queue.clear(fixture.settingsReads); yield* fixture.updateSettings({ sidebarAutoSettleOnMerge: false }); yield* Deferred.succeed(releaseFirstLookup, undefined); - yield* Queue.take(fixture.snapshotReads); + // The in-flight decision and the newly queued sweep both read the disabled settings. + yield* Queue.take(fixture.settingsReads); + yield* Queue.take(fixture.settingsReads); yield* reactor.drain; assert.deepStrictEqual(yield* Ref.get(fixture.commands), []); - assert.strictEqual(yield* Ref.get(fixture.snapshotReadCount), 2); + assert.strictEqual(yield* Ref.get(fixture.snapshotReadCount), 1); yield* Ref.set(state, "closed"); yield* fixture.updateSettings({ enableAgentBrowserAccess: false }); @@ -694,7 +704,7 @@ describe("ThreadSettlementReactor", () => { yield* Deferred.succeed(releaseLaterLookup, undefined); yield* reactor.drain; - assert.strictEqual(yield* Ref.get(fixture.snapshotReadCount), 3); + assert.strictEqual(yield* Ref.get(fixture.snapshotReadCount), 2); assert.strictEqual(yield* Ref.get(lookupCount), 2); assert.deepStrictEqual( (yield* Ref.get(fixture.commands)).map((command) => command.threadId), diff --git a/apps/server/src/orchestration/ThreadSettlementReactor.ts b/apps/server/src/orchestration/ThreadSettlementReactor.ts index 465bf0685786..48c72a6a43c1 100644 --- a/apps/server/src/orchestration/ThreadSettlementReactor.ts +++ b/apps/server/src/orchestration/ThreadSettlementReactor.ts @@ -43,11 +43,11 @@ export const make = Effect.gen(function* () { const sweep = Effect.fn("ThreadSettlementReactor.sweep")(function* ( mergedPullRequest: PullRequestService.PullRequestMergeEvent | null, ) { - const snapshot = yield* snapshots.getShellSnapshot(); const settings = yield* settingsService.getSettings; if (!settings.sidebarAutoSettleOnMerge && settings.sidebarAutoSettleAfterDays === null) { return; } + const snapshot = yield* snapshots.getShellSnapshot(); const now = DateTime.formatIso(yield* DateTime.now); const projects = new Map(snapshot.projects.map((project) => [project.id, project])); // A merge event re-sweeps every candidate, not just the threads linked to