| ... | @@ -91,12 +91,29 @@ export function canSettle( | ... | @@ -91,12 +91,29 @@ export function canSettle( |
| 91 | } | 91 | } |
| 92 | | 92 | |
| 93 | /** | 93 | /** |
| 94 | * Settled resolution over the server-backed settled lifecycle. The explicit | 94 | * A merged/closed change request settles its thread only once the thread has |
| 95 | * user override (thread.settle / thread.unsettle commands, projected into | 95 | * been idle this long. Without the idle guard the merge signal is permanent: |
| 96 | * settledOverride + settledAt) wins in both directions; without one, a | 96 | * sending a message to a merged-PR thread would un-settle the row only until |
| 97 | * thread auto-settles on a merged/closed PR or inactivity past the window. | 97 | * its turn completed, then the still-merged PR would snap it straight back |
| 98 | * The server un-settles on real activity (user message, session start, | 98 | * into the settled tail. An hour keeps the follow-up conversation visible |
| 99 | * approval/user-input request), so an override never goes stale silently. | 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 | export function effectiveSettled( | 118 | export function effectiveSettled( |
| 102 | shell: OrchestrationThreadShell, | 119 | shell: OrchestrationThreadShell, |
| ... | @@ -130,7 +147,16 @@ export function effectiveSettled( | ... | @@ -130,7 +147,16 @@ export function effectiveSettled( |
| 130 | // until real activity clears it server-side. | 147 | // until real activity clears it server-side. |
| 131 | if (shell.settledOverride === "active") return false; | 148 | if (shell.settledOverride === "active") return false; |
| 132 | if (options.changeRequestState === "merged" || options.changeRequestState === "closed") { | 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 | if (options.autoSettleAfterDays === null) return false; | 161 | if (options.autoSettleAfterDays === null) return false; |
| 136 | | 162 | |