fix(desktop): stop button sends interrupt to wrong session + stale events re-arm busy (#66485)

Co-authored-by: Brooklyn Nicholson <brooklyn.bb.nicholson@gmail.com>
This commit is contained in:
ethernet 2026-07-17 15:44:10 -04:00 committed by GitHub
parent bcea5371c8
commit 11d36232c0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 40 additions and 10 deletions

View file

@ -249,6 +249,15 @@ export function useGatewayEventHandler(deps: GatewayEventDeps) {
}
if (busy) {
// Don't re-arm busy from a stale session.info if the user
// just clicked Stop (interrupted=true). The backend's
// cooperative interrupt may not have propagated yet, so
// running is still true in the heartbeat. The turn's
// finally block will emit running=false to clear busy.
if (state.interrupted) {
return state
}
return {
...state,
busy,
@ -307,14 +316,27 @@ export function useGatewayEventHandler(deps: GatewayEventDeps) {
triggerHaptic('streamStart')
}
updateSessionState(sessionId, state => ({
...state,
busy: true,
awaitingResponse: true,
sawAssistantPayload: false,
interrupted: false,
turnStartedAt: Date.now()
}))
updateSessionState(sessionId, state => {
// If the user clicked Stop (cancelRun set interrupted=true), don't
// let a stale message.start from a chained turn (goal follow-up,
// completion drain) or an in-flight LLM response re-arm busy.
// The interrupt is user intent — the backend's cooperative cancel
// may not have propagated yet, so its events are stale. The turn's
// finally block will emit session.info with running=false to clear
// busy for real once the agent loop actually exits.
if (state.interrupted) {
return state
}
return {
...state,
busy: true,
awaitingResponse: true,
sawAssistantPayload: false,
interrupted: false,
turnStartedAt: Date.now()
}
})
if (isActiveEvent) {
setTurnStartedAt(Date.now())

View file

@ -514,7 +514,16 @@ export function usePromptActions({
)
const cancelRun = useCallback(async () => {
const sessionId = activeSessionId || activeSessionIdRef.current
// Read from the ref, not the closure-captured `activeSessionId`. The
// actions bag is a stable ref mutated in place (Object.assign on each
// ContribWiring render), and ChatRoutesSurface is memoized on that stable
// ref — so it does NOT re-render when activeSessionId changes, which means
// the ChatView element's onCancel prop holds a stale cancelRun closure.
// The closure's `activeSessionId` can be a previous session's id (or null
// from a new-chat draft), sending session.interrupt to the wrong session.
// The ref is updated via useEffect on every activeSessionId change, so it
// always reflects the current session — same pattern submitText uses.
const sessionId = activeSessionIdRef.current
const releaseBusy = () => {
setMutableRef(busyRef, false)
@ -589,7 +598,6 @@ export function usePromptActions({
notifyError(stopError, copy.stopFailed)
}
}, [
activeSessionId,
activeSessionIdRef,
busyRef,
copy.stopFailed,