New request
#3344: cancelStream() should respect active stream ownership and close the SSE source
We'll provision a sandbox, run an agent against the issue, and open a draft PR. You can pull the branch and iterate from there.
Issue
bugsprint-candidatestreaming
Summary
The user-facing cancelStream() in static/boot.js clears the local
S.activeStreamId, calls setBusy(false), and resets the composer status
unconditionally after issuing POST /api/chat/cancel. It does not:
- Call
closeLiveStream(sid, streamId), so the SSE EventSource for the cancelled turn may keep receiving events and pushing them into a subsequent turn's UI. - Read the cancel response, so
cancelled:false(backend already finalised / stale writeback / session rotated) is invisible to the user and offers no degraded feedback. - Guard the local-state clear against the original
streamId. If a new turn has already started in the same session while the cancel request was in flight, clearing wipes the new turn's busy state and desyncs the UI from the worker.
Acceptance criteria
cancelStream()snapshots the active(sid, streamId)at entry.- After the cancel fetch resolves or rejects, the function calls
closeLiveStream(sid, streamId)for the snapshot pair — even if the cancel was rejected, so the old SSE cannot leak. - The function only clears
S.activeStreamId/setBusy(false)/ composer status whenS.activeStreamIdis still the snapshot value. This is the owner guard. - When the cancel response is
cancelled:false, the function surfaces a short, generic toast (e.g.Stream is no longer active) so the user has a degraded feedback path. The toast is best-effort: ifshowToastis missing, swallow silently. - A network error in the cancel fetch must not throw and must not clear the new turn's active state.
Non-goals (out of scope for this issue)
- Extending the
/api/chat/cancelresponse shape (e.g. addingstatus/reasonto distinguish stale writeback from already-finalised). The current contract only exposescancelled:bool, which is enough to drive the owner guard and the toast. - Changes to
api/streaming.pyor to the SSE event handler instatic/messages.js. The frontend defense is the right surgical fix; backend silent-skip semantics are a separate conversation. - Changes to the live-stream UX or tool-card visuals that are being
shipped on
fix-stream-item-shape/live-stream-tool-cards-worklog.
Reproduction (manual)
- Open the WebUI in two browser tabs against the same session.
- Tab A: send a long-running prompt; the busy spinner and composer status appear.
- Tab A: click Stop.
- Within ~200ms, Tab A: send a second prompt. The new turn's spinner/status must remain visible for the full turn.
- Before the fix: Tab A's second turn briefly shows "idle / empty composer" during the cancel-drain window, because the first cancel wiped the new turn's state.
- After the fix: Tab A's second turn keeps its busy state; the first turn's SSE source is closed and never leaks tokens into the second turn.
Linked RFC
docs/rfcs/webui-run-state-consistency-contract.md(Invariants #2 and #4 — Live stream / SSE and Live UI scene layers)
Linked PR
Assessmentadvisory
bug● easy90% confidence
Localized async state-management bug in stream cancellation logic with precise acceptance criteria confined to boot and toast utilities.
Likely files
- static/boot.js
- static/ui.js
Create the request