| ... | ... | @@ -91,12 +91,29 @@ export function canSettle( |
| 91 | 91 | } |
| 92 | 92 | |
| 93 | 93 | /** |
| 94 | | * Settled resolution over the server-backed settled lifecycle. The explicit |
| 95 | | * user override (thread.settle / thread.unsettle commands, projected into |
| 96 | | * settledOverride + settledAt) wins in both directions; without one, a |
| 97 | | * thread auto-settles on a merged/closed PR or inactivity past the window. |
| 98 | | * The server un-settles on real activity (user message, session start, |
| 99 | | * approval/user-input request), so an override never goes stale silently. |
| 94 | * A merged/closed change request settles its thread only once the thread has |
| 95 | * been idle this long. Without the idle guard the merge signal is permanent: |
| 96 | * sending a message to a merged-PR thread would un-settle the row only until |
| 97 | * its turn completed, then the still-merged PR would snap it straight back |
| 98 | * into the settled tail. An hour keeps the follow-up conversation visible |
| 99 | * while it is warm; once the burst goes stale the merge signal settles it |
| 100 | * again. Activity timestamps can originate on another device while `now` is |
| 101 | * this caller's clock: skew shortens or stretches the window by its size, |
| 102 | * the same exposure the inactivity auto-settle already accepts — worst case |
| 103 | * is a row changing lists early or late, never lost work. |
| 104 | */ |
| 105 | export const CHANGE_REQUEST_SETTLE_IDLE_MS = 60 * 60 * 1_000; |
| 106 | |
| 107 | /** |
| 108 | * Settled resolution over the server-backed settled lifecycle. Activity |
| 109 | * blockers (pending approval/user-input, a live session, an unadjudicated |
| 110 | * queued turn) are checked first and hold a thread active regardless of any |
| 111 | * override. Past the blockers, the explicit user override (thread.settle / |
| 112 | * thread.unsettle commands, projected into settledOverride + settledAt) |
| 113 | * wins in both directions; without one, a thread auto-settles on a |
| 114 | * merged/closed PR (once idle) or inactivity past the window. The server |
| 115 | * un-settles on real activity (user message, session start, approval/ |
| 116 | * user-input request), so an override never goes stale silently. |
| 100 | 117 | */ |
| 101 | 118 | export function effectiveSettled( |
| 102 | 119 | shell: OrchestrationThreadShell, |
| ... | ... | @@ -130,7 +147,16 @@ export function effectiveSettled( |
| 130 | 147 | // until real activity clears it server-side. |
| 131 | 148 | if (shell.settledOverride === "active") return false; |
| 132 | 149 | if (options.changeRequestState === "merged" || options.changeRequestState === "closed") { |
| 133 | | return true; |
| 150 | // Only an idle thread settles on the merge signal: the signal itself |
| 151 | // never clears, so without this guard fresh activity (a message sent in |
| 152 | // a settled thread) would re-settle the moment its turn completed. |
| 153 | const lastActivityAt = threadLastActivityAt(shell); |
| 154 | if ( |
| 155 | lastActivityAt === null || |
| 156 | Date.parse(lastActivityAt) < Date.parse(options.now) - CHANGE_REQUEST_SETTLE_IDLE_MS |
| 157 | ) { |
| 158 | return true; |
| 159 | } |
| 134 | 160 | } |
| 135 | 161 | if (options.autoSettleAfterDays === null) return false; |
| 136 | 162 | |