From d536b0580d044967dc644498d8c5a6e96464f767 Mon Sep 17 00:00:00 2001 From: Guillermo Casanova <75276669+Gigioxx@users.noreply.github.com> Date: Fri, 4 Sep 2026 12:44:01 -0400 Subject: [PATCH] fix(server): settle inactive threads with open PRs (#9610) --- .../ThreadSettlementPolicy.test.ts | 4 ++-- .../orchestration/ThreadSettlementPolicy.ts | 1 - .../ThreadSettlementReactor.test.ts | 20 ++++++++++++++++--- docs/user/thread-sidebar.md | 8 ++++---- 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/apps/server/src/orchestration/ThreadSettlementPolicy.test.ts b/apps/server/src/orchestration/ThreadSettlementPolicy.test.ts index a70fb9f68176dba6434a35202b22d5e19467bfad..05fb16203e0b39b4316463f5d271a9b13592de36 100644 --- a/apps/server/src/orchestration/ThreadSettlementPolicy.test.ts +++ b/apps/server/src/orchestration/ThreadSettlementPolicy.test.ts @@ -95,8 +95,8 @@ describe("resolveAutoSettlementAt", () => { expect(decide(makeThread({ latestUserMessageAt: "2026-08-25T12:00:00.000Z" }))).toBe(false); }); - it("keeps open pull requests active", () => { - expect(decide(makeThread(), { state: "open", updatedAt: NOW })).toBe(false); + it("settles inactive threads with open pull requests", () => { + expect(decide(makeThread(), { state: "open", updatedAt: NOW })).toBe(true); }); it("settles closed requests and honors the merge setting", () => { diff --git a/apps/server/src/orchestration/ThreadSettlementPolicy.ts b/apps/server/src/orchestration/ThreadSettlementPolicy.ts index 5270c30eebe2f04408cfa79856f4fe35d5733ea0..eac5a960a48229d523c83d7555952419401085b6 100644 --- a/apps/server/src/orchestration/ThreadSettlementPolicy.ts +++ b/apps/server/src/orchestration/ThreadSettlementPolicy.ts @@ -81,7 +81,6 @@ export function resolveAutoSettlementAt(input: { if (pullRequestSettles(thread, pullRequest, input.autoSettleOnMerge)) { return activityAt ?? thread.createdAt; } - if (pullRequest.state === "open") return null; } if (input.autoSettleAfterDays === null || activityAt === null) return null; return Date.parse(activityAt) < Date.parse(input.now) - input.autoSettleAfterDays * DAY_MS diff --git a/apps/server/src/orchestration/ThreadSettlementReactor.test.ts b/apps/server/src/orchestration/ThreadSettlementReactor.test.ts index c7d79517526290a83c68d16cce020fd501916e6f..d3b24d5f77b10127d2aa75b8792f2178b319fa42 100644 --- a/apps/server/src/orchestration/ThreadSettlementReactor.test.ts +++ b/apps/server/src/orchestration/ThreadSettlementReactor.test.ts @@ -408,6 +408,7 @@ describe("ThreadSettlementReactor", () => { const fixture = yield* makeHarness({ snapshot: makeSnapshot([ makeThread("merged-in-app", { + latestUserMessageAt: "2026-08-27T00:00:00.000Z", linkedPullRequest: { projectId: PROJECT_ID, repository: "owner/repository", @@ -415,7 +416,10 @@ describe("ThreadSettlementReactor", () => { url: "https://example.test/owner/repository/pull/42", }, }), - makeThread("slow-periodic-lookup", { branch: "another-feature" }), + makeThread("slow-periodic-lookup", { + branch: "another-feature", + latestUserMessageAt: "2026-08-27T00:00:00.000Z", + }), ]), branchPullRequest: () => Ref.updateAndGet(branchLookupCount, (count) => count + 1).pipe( @@ -460,7 +464,12 @@ describe("ThreadSettlementReactor", () => { const state = yield* Ref.make<"open" | "merged">("open"); const mergedThreadSettled = yield* Deferred.make(); const fixture = yield* makeHarness({ - snapshot: makeSnapshot([makeThread("branch-thread", { branch: "saved-feature" })]), + snapshot: makeSnapshot([ + makeThread("branch-thread", { + branch: "saved-feature", + latestUserMessageAt: "2026-08-27T00:00:00.000Z", + }), + ]), branchPullRequest: () => Ref.get(state).pipe( Effect.map((pullRequestState) => ({ state: pullRequestState, updatedAt: NOW })), @@ -500,6 +509,7 @@ describe("ThreadSettlementReactor", () => { const fixture = yield* makeHarness({ snapshot: makeSnapshot([ makeThread("merged-in-app", { + latestUserMessageAt: "2026-08-27T00:00:00.000Z", linkedPullRequest: { projectId: PROJECT_ID, repository: "owner/repository", @@ -508,6 +518,7 @@ describe("ThreadSettlementReactor", () => { }, }), makeThread("unrelated-linked", { + latestUserMessageAt: "2026-08-27T00:00:00.000Z", linkedPullRequest: { projectId: PROJECT_ID, repository: "owner/repository", @@ -683,7 +694,10 @@ describe("ThreadSettlementReactor", () => { const fixture = yield* makeHarness({ snapshot: makeSnapshot( [ - makeThread("missing-own-project", { linkedPullRequest }), + makeThread("missing-own-project", { + latestUserMessageAt: "2026-08-27T00:00:00.000Z", + linkedPullRequest, + }), makeThread("missing-branch-project", { branch: "saved-feature" }), ], [makeProject(LINKED_PROJECT_ID, "/workspace/linked")], diff --git a/docs/user/thread-sidebar.md b/docs/user/thread-sidebar.md index 3d6311641d59aa7554a39c888181d970c683e2d2..1932e9ce2a453cec41dd9205c3a163312f2aa44c 100644 --- a/docs/user/thread-sidebar.md +++ b/docs/user/thread-sidebar.md @@ -13,10 +13,10 @@ request merges if **Auto-settle merged threads** is enabled. Each server stores its own copy of the automatic settlement settings and checks them even when no web, desktop, or mobile client is connected. By default, it settles threads after three days without activity and when their pull request merges. An eligible idle thread also settles when its pull -request closes. An open pull request blocks inactivity settlement. Active work, pending input, and -live background work keep the thread active. T3 Code settles from a closed or merged pull request -only when its timestamp is not older than the user's latest activity. If that timestamp is not -available, the inactivity rule still applies. A manual un-settle also keeps the thread active. +request closes. An open pull request does not block inactivity settlement. Active work, pending +input, and live background work keep the thread active. T3 Code settles from a closed or merged +pull request only when its timestamp is not older than the user's latest activity. If that timestamp +is not available, the inactivity rule still applies. A manual un-settle also keeps the thread active. **Settled** lists threads by when their work finished, newest first. A thread you settle yourself sorts by the moment you settled it. A thread that settled on its own sorts by its last message or -- 2.54.0