Skip to content

[Bug]: Old auto-settled threads re-appeared as unsettled in sidebar after version upgrade #9600

Description

@Jardo-51

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/server

Summary

Since #8600 moved settlement server-side, inactivity auto-settle no longer applies to any thread whose branch resolves to an open pull request. Those threads stay in the sidebar forever regardless of the configured threshold. Before #8600 they settled normally, so on upgrade a batch of long-quiet threads reappears in the active list.

On this machine that is 29 threads, all inactive for more than 30 days, against a configured threshold of 7 days.

Steps to reproduce

  1. Set Auto settle inactive threads to any threshold (7 days here).
  2. Have several threads on a long-lived feature branch that has an open PR/MR — one that stays open for weeks (long review cycle, draft MR, stacked work).
  3. Let those threads go quiet for well past the threshold. On a build before 0.0.38-nightly.20260901.1246 they classify as settled and collapse into the settled shelf.
  4. Update to a build containing fix(server): settle threads server-side #8600.
  5. Open the sidebar.

Expected behavior

Threads inactive past the configured threshold settle, as they did before #8600.

Actual behavior

Every thread on a branch with an open PR stays in the active list indefinitely. The sidebar fills with months-old threads and the inactivity threshold has no effect on them.

The rule itself is intentional and is not new — resolveAutoSettlementAt returns null for an open PR at any age:

if (pullRequest !== null) {
if (pullRequestSettles(thread, pullRequest, input.autoSettleOnMerge)) {
return activityAt ?? thread.createdAt;
}
if (pullRequest.state === "open") return null;
}

What changed is whether the PR state was ever known for these threads. The old client-side effectiveSettled carried the identical guard, but it only fired when changeRequest was actually supplied, and that came from threadChangeRequestSnapshotsAtom — a session-lifetime map populated from live VCS status only by sidebar rows that mounted and rendered:

const threadKey = scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id));
const snapshot = changeRequestSnapshotByKey.get(threadKey);
const changeRequest =
snapshot != null &&
(thread.linkedPullRequest == null
? thread.worktreePath === null || snapshot.branch === thread.branch
: snapshot.linkedPullRequest?.projectId === thread.linkedPullRequest.projectId &&
snapshot.linkedPullRequest.repository === thread.linkedPullRequest.repository &&
snapshot.linkedPullRequest.number === thread.linkedPullRequest.number)
? snapshot.pr
: null;

Threads sitting in the collapsed settled shelf never published a snapshot, so changeRequest resolved to null, the open-PR guard never fired, and they aged out on the threshold — and once settled and collapsed they stayed that way. ThreadSettlementReactor now resolves the branch→PR authoritatively for every candidate on every sweep, so the guard fires for all of them.

So the old behavior was arguably wrong (deciding on data the client did not have), but the user-visible effect of correcting it is that the inactivity threshold silently stopped applying to an entire class of threads, with no UI indicating why.

Diagnosis

The settlement reactor is healthy — this is not a sweep failure. From server.trace.ndjson across all 11 rotations:

  • automatic thread settlement skipped — 0 occurrences
  • automatic thread settlement sweep failed — 0 occurrences
  • ThreadSettlementReactor.sweep — 5 spans, all Success
  • ThreadSettlementReactor.pullRequestFor — 35 spans, all Success

Thread state in projection_threads (non-archived, non-deleted):

settled_override count
settled 794
active 1
null 32

29 of those 32 have last activity more than 30 days old. All 29 have pending_approval_count = 0, pending_user_input_count = 0, no snooze, session stopped or ready, and no linked_pull_request_json — nothing blocks them in isAutoSettlementCandidate. They collapse onto just five branches:

Threads Branch Forge PR/MR state
12 branch A self-hosted GitLab open
7 branch B self-hosted GitLab open (draft)
4 branch C GitHub open
3 branch D GitHub open
3 branch E GitHub open

Every one verified open against its forge. The 5 lookup groups match the ~7 pullRequestFor spans per sweep. The sweep settled 794 threads correctly; these 29 are refused solely by the open-PR guard.

sidebarAutoSettleOnMerge is false here, but that is not a factor — a merged PR falls through to the inactivity check; only open hard-blocks.

Impact

Minor bug or occasional failure

Version or commit

0.0.39-nightly.20260903.1273 (03728361aa7beb9c13da320097450e6fe65aac3e). Regression introduced by #8600, first shipped in 0.0.38-nightly.20260901.1246.

Environment

Linux x64 (Ubuntu 20.04), Node v24.15.0, self-hosted server (npx t3) + web client. Repos on GitHub and self-hosted GitLab.

Logs or stack traces

# settings.json
"sidebarAutoSettleAfterDays": 7
"sidebarAutoSettleOnMerge": false

# server.trace.ndjson* — no settlement warnings at all
$ grep -c 'automatic thread settlement skipped' server.trace.ndjson*   -> 0 in every rotation
$ grep -c 'automatic thread settlement sweep failed' server.trace.ndjson* -> 0 in every rotation

# ThreadSettlementReactor spans, current rotation
ThreadSettlementReactor.sweep            5  exit=Success  (~2.8s each)
ThreadSettlementReactor.pullRequestFor  35  exit=Success

# projection_threads, non-archived non-deleted, settled_override is null
# and last activity older than 30 days
count = 29
all: pending_approval_count=0 pending_user_input_count=0 snoozed_until=NULL
     linked_pull_request_json=NULL  session in (stopped, ready)
distinct (branch, workspace_root) = 5, every one with an open PR/MR

Workaround

Settle the affected threads manually. That sticks, because settledOverride is now persisted and excludes them from later sweeps.

Suggested direction

Some way for inactivity to eventually win over an open PR. Options, roughly in order of how small they are:

  • Let inactivity settle past some multiple of the threshold even with an open PR — an untouched thread on a PR that has itself been idle for a month is not live work.
  • Anchor the open-PR guard to the PR's updatedAt rather than treating any open PR as unfinished business, so an abandoned PR stops pinning its threads.
  • Make the open-PR guard a separate setting, so people with long-lived branches can opt out.

Happy to send a PR if you have a preference on the shape.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions