Context
Discovered while reviewing #378, which fixes the confirmed #99 path where rebuilding the action buttons renders a disabled Send button during an active turn.
This issue tracks a separate turn-lifecycle race that can produce a similar symptom. It is not currently established as a root cause of #99.
Related review thread: #378 (comment)
Problem
Each chat request is sent with a unique workDoneToken, and ProgressParamsAdapter parses the token returned on $/progress. However, CopilotLanguageClient.notifyProgress() forwards only ChatProgressValue, so the request identity is unavailable to ChatView.
ChatView currently associates top-level progress primarily by conversationId. Consecutive turns in one conversation share that ID, and any accepted top-level end event calls actionBar.markTurnFinished(). Asynchronous request errors also finish the action bar without checking whether the failed request is still active.
A possible sequence is:
- Turn A is running.
- The user cancels A; the UI immediately allows another send.
- Turn B starts in the same conversation.
- A delayed
end(A) or error callback from A arrives.
- B is incorrectly marked finished and its Cancel button becomes Send.
- Subsequent progress for B may be ignored because
onChatProgress() gates processing on actionBar.isTurnRunning().
A real log attached to #99 confirms that the language server can send a cancellation end after $/cancelRequest (about 15 ms later in that trace). That trace did not overlap the cancellation end with the next turn, so it demonstrates the event shape but not this race itself.
Expected behavior
A terminal progress event or asynchronous callback must only finish the top-level request to which it belongs. Events from an older request must not change the lifecycle or action-button state of a newer active turn.
Acceptance criteria
start A -> cancel A -> start B -> end A leaves B running with Cancel available.
- A delayed error callback from A cannot finish B.
- The matching
end B still finishes B normally.
- Request identity is preserved sufficiently to distinguish consecutive turns in the same conversation.
- The lifecycle behavior is covered by automated tests.
- Detailed handling of stale events, persistence, conversation metadata, and subagent progress is decided during implementation design rather than assumed by this issue.
Context
Discovered while reviewing #378, which fixes the confirmed #99 path where rebuilding the action buttons renders a disabled Send button during an active turn.
This issue tracks a separate turn-lifecycle race that can produce a similar symptom. It is not currently established as a root cause of #99.
Related review thread: #378 (comment)
Problem
Each chat request is sent with a unique
workDoneToken, andProgressParamsAdapterparses the token returned on$/progress. However,CopilotLanguageClient.notifyProgress()forwards onlyChatProgressValue, so the request identity is unavailable toChatView.ChatViewcurrently associates top-level progress primarily byconversationId. Consecutive turns in one conversation share that ID, and any accepted top-levelendevent callsactionBar.markTurnFinished(). Asynchronous request errors also finish the action bar without checking whether the failed request is still active.A possible sequence is:
end(A)or error callback from A arrives.onChatProgress()gates processing onactionBar.isTurnRunning().A real log attached to #99 confirms that the language server can send a cancellation
endafter$/cancelRequest(about 15 ms later in that trace). That trace did not overlap the cancellation end with the next turn, so it demonstrates the event shape but not this race itself.Expected behavior
A terminal progress event or asynchronous callback must only finish the top-level request to which it belongs. Events from an older request must not change the lifecycle or action-button state of a newer active turn.
Acceptance criteria
start A -> cancel A -> start B -> end Aleaves B running with Cancel available.end Bstill finishes B normally.