fix(ui): stop New Task from mirroring the previous session's live stream#1584
Conversation
#1570 migrated publish_agents.yml to OIDC trusted publishing and removed the PYPI_API_TOKEN secret, but test_publish_workflow_exists_and_uses_pypi_action still asserted "secrets.PYPI_API_TOKEN" in the workflow — so the test fails on main and on every open PR current with it. Assert "id-token: write" (the OIDC marker) instead, matching how the workflow actually authenticates now.
…finalized The agent-wheel publish path (#1179) is armed: a v* release tag would fire the publish job (behind a manual gate, against PyPI projects that don't exist yet) before the Agent Hub distribution design is settled. Disable it rather than ship a half-finished release path: remove the v* tag trigger and hard-skip the approve/publish jobs (both commented for one-line re-enable). PR build + twine-check still run so agent wheels stay well-formed. The OIDC test fix in this PR stands as-is — the asserted workflow strings (id-token: write, tag gate, skip-existing, matrix) are all still present.
Clicking New Task (or switching sessions) while an agent was still streaming surfaced the previous conversation in the fresh window and showed it as still running. The chat store keeps isStreaming, streamingContent, and agentSteps as single global values, and the old ChatView kept writing into them until its delayed (~220ms) unmount, so the new session read the in-flight stream's state. Reset the streaming globals at session-swap time and short-circuit the stream write paths (chunk/agent-event/done/error/flush) once the view's sessionId is no longer the current one, so a backgrounded stream can no longer leak into the active view.
Review: fix(ui): stop New Task from mirroring the previous session's live streamSummarySolid, well-scoped fix for #1580 — the root cause (global Issues🟢 Repeated guard expression could be a named helper ( const isStale = () => useChatStore.getState().currentSessionId !== sessionId;Then each guard becomes 🟢 Switch-away-then-back mid-stream is worth a manual pass Strengths
VerdictApprove. Clean root-cause fix, full guard coverage, tests updated alongside both the UI and CI changes. The two 🟢 notes are optional — the guard-helper is cosmetic and the switch-back case just deserves a glance during the manual test you've already queued. |
The currentSessionId !== sessionId check appeared at eight call sites (stream chunk/agent-event/done/error/flush plus the message refresh/delete/resend callbacks). Collapse them into one documented useCallback so the intent reads at each guard and the 220ms transition relationship is explained in a single place. No behavior change — isStale() evaluates the same live store value.
itomek
left a comment
There was a problem hiding this comment.
isStale() guard keys off the synchronously-flipping currentSessionId; resetStreaming placement is correct. LGTM.
Generated by Claude Code
…icator (amd#1580) (amd#1656) ## Why this matters Follow-up to amd#1584. Before: clicking **New Task** (or switching sessions) while an agent was still generating *killed* the run — the SSE was torn down on unmount, the backend cancelled the turn, and the in-flight answer was discarded with no sign it had ever been running. @sdevinenamd reported the visible half ("losing the status bar… hard to tell if the original agent is still running") while waiting on the email-triage agent. After: the run keeps going in the background, finishes and persists on its own, the sidebar shows a spinner on any session with a live run, and re-opening that session re-attaches to its live stream so progress resumes. A chat turn is now a first-class background run (`RunManager`) that outlives the SSE connection: the producer + persistence run in a detached task, and the SSE endpoint is a thin subscriber over a replay buffer with multi-subscriber fan-out. `GET /api/chat/active` is the backend-truth source for the sidebar indicator (survives refresh/revisit); `GET /api/chat/attach` reconnects to an in-flight run. Because a disconnect no longer cancels, **Stop** now calls the cancel endpoint explicitly, and deleting a session cancels its run so it can't persist to a dead session. ## Test plan - [x] `pytest tests/unit/chat/ui/` — 720 passed (1 pre-existing Windows cp1252 flake in an unrelated `agent.py`-reading test, deselected). Includes new `test_run_manager.py` (run survives subscriber disconnect & persists, late-subscriber replay, active list, overlap-409, cancel) and `test_chat_active_attach.py` (active/attach endpoints, send-409-while-running, delete-cancels-run). - [x] `tsc --noEmit` — clean on all app code; `vite build` — clean. - [x] **Live Agent UI** (real backend + Lemonade/Gemma-4-E4B): started a long run → clicked **New Task** mid-stream → new session opened clean, old session kept its "Agent running" spinner → revisited it → live stream resumed → run completed in the background, answer persisted, session auto-titled, spinner cleared. - [ ] `vitest run` (added `chatStore` running-sessions cases) — runs in CI; not executed locally (vitest absent from the local env). Co-authored-by: Ovtcharov <[email protected]>
Why this matters
Before: clicking New Task (or switching sessions) in the Agent UI while an agent was still streaming opened a window that mirrored the previous conversation and showed it as still running — the fresh session was not fresh. Reported by @sdevinenamd in #1580 while waiting on the email-triage agent. After: New Task always opens a clean window, and any in-flight stream is left to finish in its own (now background) session instead of leaking into the new one.
The session was always created correctly server-side; the bug was purely client-side shared state. The chat store keeps
isStreaming,streamingContent, andagentStepsas single global values, and the oldChatViewkept writing into them until its delayed (~220ms, the view-transition fade) unmount — so the incoming view read the previous stream's state.The fix has two halves: reset the streaming globals at session-swap time so the incoming view starts clean, and guard every stream write path (
onChunk/onAgentEvent/onDone/onError/flushStreamBuffer) bysessionIdso a backgrounded stream can no longer mutate the shared store the active view reads from. This also covers switching to an existing session mid-stream, not just New Task.Fixes #1580.
Test plan
npx tsc --noEmitinsrc/gaia/apps/webui— cleanvitest run— 42/42 pass, including newchatStore.test.tscoveringresetStreaming