authorgravatar for 75276669+Gigioxx@users.noreply.github.comGuillermo Casanova <75276669+Gigioxx@users.noreply.github.com> 2026-09-04 12:44:01-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-04 12:44:01-04:00
logd536b0580d044967dc644498d8c5a6e96464f767
treed601f869c0caa9f1785656747971f501a2f29244
parentc66f15f39e61fda0a9a100578833ec12e7526859
signature Signed by PGP key B5690EEEBB952194

fix(server): settle inactive threads with open PRs (#9610)


4 files changed, 23 insertions(+), 10 deletions(-)

apps/server/src/orchestration/ThreadSettlementPolicy.test.ts+2-2
...@@ -95,8 +95,8 @@ describe("resolveAutoSettlementAt", () => {...@@ -95,8 +95,8 @@ describe("resolveAutoSettlementAt", () => {
95 expect(decide(makeThread({ latestUserMessageAt: "2026-08-25T12:00:00.000Z" }))).toBe(false);95 expect(decide(makeThread({ latestUserMessageAt: "2026-08-25T12:00:00.000Z" }))).toBe(false);
96 });96 });
9797
98 it("keeps open pull requests active", () => {98 it("settles inactive threads with open pull requests", () => {
99 expect(decide(makeThread(), { state: "open", updatedAt: NOW })).toBe(false);99 expect(decide(makeThread(), { state: "open", updatedAt: NOW })).toBe(true);
100 });100 });
101101
102 it("settles closed requests and honors the merge setting", () => {102 it("settles closed requests and honors the merge setting", () => {
apps/server/src/orchestration/ThreadSettlementPolicy.ts-1
...@@ -81,7 +81,6 @@ export function resolveAutoSettlementAt(input: {...@@ -81,7 +81,6 @@ export function resolveAutoSettlementAt(input: {
81 if (pullRequestSettles(thread, pullRequest, input.autoSettleOnMerge)) {81 if (pullRequestSettles(thread, pullRequest, input.autoSettleOnMerge)) {
82 return activityAt ?? thread.createdAt;82 return activityAt ?? thread.createdAt;
83 }83 }
84 if (pullRequest.state === "open") return null;
85 }84 }
86 if (input.autoSettleAfterDays === null || activityAt === null) return null;85 if (input.autoSettleAfterDays === null || activityAt === null) return null;
87 return Date.parse(activityAt) < Date.parse(input.now) - input.autoSettleAfterDays * DAY_MS86 return Date.parse(activityAt) < Date.parse(input.now) - input.autoSettleAfterDays * DAY_MS
apps/server/src/orchestration/ThreadSettlementReactor.test.ts+17-3
...@@ -408,6 +408,7 @@ describe("ThreadSettlementReactor", () => {...@@ -408,6 +408,7 @@ describe("ThreadSettlementReactor", () => {
408 const fixture = yield* makeHarness({408 const fixture = yield* makeHarness({
409 snapshot: makeSnapshot([409 snapshot: makeSnapshot([
410 makeThread("merged-in-app", {410 makeThread("merged-in-app", {
411 latestUserMessageAt: "2026-08-27T00:00:00.000Z",
411 linkedPullRequest: {412 linkedPullRequest: {
412 projectId: PROJECT_ID,413 projectId: PROJECT_ID,
413 repository: "owner/repository",414 repository: "owner/repository",
...@@ -415,7 +416,10 @@ describe("ThreadSettlementReactor", () => {...@@ -415,7 +416,10 @@ describe("ThreadSettlementReactor", () => {
415 url: "https://example.test/owner/repository/pull/42",416 url: "https://example.test/owner/repository/pull/42",
416 },417 },
417 }),418 }),
418 makeThread("slow-periodic-lookup", { branch: "another-feature" }),419 makeThread("slow-periodic-lookup", {
420 branch: "another-feature",
421 latestUserMessageAt: "2026-08-27T00:00:00.000Z",
422 }),
419 ]),423 ]),
420 branchPullRequest: () =>424 branchPullRequest: () =>
421 Ref.updateAndGet(branchLookupCount, (count) => count + 1).pipe(425 Ref.updateAndGet(branchLookupCount, (count) => count + 1).pipe(
...@@ -460,7 +464,12 @@ describe("ThreadSettlementReactor", () => {...@@ -460,7 +464,12 @@ describe("ThreadSettlementReactor", () => {
460 const state = yield* Ref.make<"open" | "merged">("open");464 const state = yield* Ref.make<"open" | "merged">("open");
461 const mergedThreadSettled = yield* Deferred.make<void>();465 const mergedThreadSettled = yield* Deferred.make<void>();
462 const fixture = yield* makeHarness({466 const fixture = yield* makeHarness({
463 snapshot: makeSnapshot([makeThread("branch-thread", { branch: "saved-feature" })]),467 snapshot: makeSnapshot([
468 makeThread("branch-thread", {
469 branch: "saved-feature",
470 latestUserMessageAt: "2026-08-27T00:00:00.000Z",
471 }),
472 ]),
464 branchPullRequest: () =>473 branchPullRequest: () =>
465 Ref.get(state).pipe(474 Ref.get(state).pipe(
466 Effect.map((pullRequestState) => ({ state: pullRequestState, updatedAt: NOW })),475 Effect.map((pullRequestState) => ({ state: pullRequestState, updatedAt: NOW })),
...@@ -500,6 +509,7 @@ describe("ThreadSettlementReactor", () => {...@@ -500,6 +509,7 @@ describe("ThreadSettlementReactor", () => {
500 const fixture = yield* makeHarness({509 const fixture = yield* makeHarness({
501 snapshot: makeSnapshot([510 snapshot: makeSnapshot([
502 makeThread("merged-in-app", {511 makeThread("merged-in-app", {
512 latestUserMessageAt: "2026-08-27T00:00:00.000Z",
503 linkedPullRequest: {513 linkedPullRequest: {
504 projectId: PROJECT_ID,514 projectId: PROJECT_ID,
505 repository: "owner/repository",515 repository: "owner/repository",
...@@ -508,6 +518,7 @@ describe("ThreadSettlementReactor", () => {...@@ -508,6 +518,7 @@ describe("ThreadSettlementReactor", () => {
508 },518 },
509 }),519 }),
510 makeThread("unrelated-linked", {520 makeThread("unrelated-linked", {
521 latestUserMessageAt: "2026-08-27T00:00:00.000Z",
511 linkedPullRequest: {522 linkedPullRequest: {
512 projectId: PROJECT_ID,523 projectId: PROJECT_ID,
513 repository: "owner/repository",524 repository: "owner/repository",
...@@ -683,7 +694,10 @@ describe("ThreadSettlementReactor", () => {...@@ -683,7 +694,10 @@ describe("ThreadSettlementReactor", () => {
683 const fixture = yield* makeHarness({694 const fixture = yield* makeHarness({
684 snapshot: makeSnapshot(695 snapshot: makeSnapshot(
685 [696 [
686 makeThread("missing-own-project", { linkedPullRequest }),697 makeThread("missing-own-project", {
698 latestUserMessageAt: "2026-08-27T00:00:00.000Z",
699 linkedPullRequest,
700 }),
687 makeThread("missing-branch-project", { branch: "saved-feature" }),701 makeThread("missing-branch-project", { branch: "saved-feature" }),
688 ],702 ],
689 [makeProject(LINKED_PROJECT_ID, "/workspace/linked")],703 [makeProject(LINKED_PROJECT_ID, "/workspace/linked")],
docs/user/thread-sidebar.md+4-4
...@@ -13,10 +13,10 @@ request merges if **Auto-settle merged threads** is enabled....@@ -13,10 +13,10 @@ request merges if **Auto-settle merged threads** is enabled.
13Each server stores its own copy of the automatic settlement settings and checks them even when no13Each server stores its own copy of the automatic settlement settings and checks them even when no
14web, desktop, or mobile client is connected. By default, it settles threads after three days without14web, desktop, or mobile client is connected. By default, it settles threads after three days without
15activity and when their pull request merges. An eligible idle thread also settles when its pull15activity and when their pull request merges. An eligible idle thread also settles when its pull
16request closes. An open pull request blocks inactivity settlement. Active work, pending input, and16request closes. An open pull request does not block inactivity settlement. Active work, pending
17live background work keep the thread active. T3 Code settles from a closed or merged pull request17input, and live background work keep the thread active. T3 Code settles from a closed or merged
18only when its timestamp is not older than the user's latest activity. If that timestamp is not18pull request only when its timestamp is not older than the user's latest activity. If that timestamp
19available, the inactivity rule still applies. A manual un-settle also keeps the thread active.19is not available, the inactivity rule still applies. A manual un-settle also keeps the thread active.
2020
21**Settled** lists threads by when their work finished, newest first. A thread you settle yourself21**Settled** lists threads by when their work finished, newest first. A thread you settle yourself
22sorts by the moment you settled it. A thread that settled on its own sorts by its last message or22sorts by the moment you settled it. A thread that settled on its own sorts by its last message or