Skip to content

Fix assistant losing answers when the chat panel is closed or reconnects - #332

Open
zwanski-start wants to merge 2 commits into
AutoForgeAI:masterfrom
zwanski-start:swarm/issue-133-barbara
Open

zwanski-start wants to merge 2 commits into
AutoForgeAI:masterfrom
zwanski-start:swarm/issue-133-barbara

Conversation

@zwanski-start

Copy link
Copy Markdown

Closes #133

Problem

If you closed the assistant panel (or switched views, reloaded the page, or lost the connection) while an answer was still streaming, the answer disappeared. It was still missing after reopening the conversation from history. There were three causes:

  1. The panel unmounted the chat when it closed. AssistantPanel rendered <AssistantChat> only while isOpen, so closing it closed the WebSocket.
  2. The server dropped the reply when the socket died. The router's send_json raised partway through the stream. That abandoned the generator before _query_claude reached add_message, so the reply was never saved. On reconnect, create_session also closed the live session and started a new one.
  3. The UI showed stale history. useConversation caches for 30s, so reopening showed the old message list even when the database had newer messages.

Changes

Server (server/services/assistant_chat_session.py, server/routers/assistant_chat.py)

  • _forward_chunks(): if the client disconnects mid-reply, the server keeps reading Claude's stream so the full reply finishes and gets saved.
  • _query_claude saves the reply in a finally block, so a partial reply is kept if the stream is aborted. aclosing() passes an early close through the nested generators.
  • create_session reuses the live session when the client reconnects to the same conversation. Queries are serialized with an asyncio.Lock. The new resume() waits for any reply still in progress before sending response_done.

UI

  • AssistantPanel: after the chat is first opened for a project, it stays mounted (keyed by project). While closed, the panel is inert, so it can't take focus or capture keyboard shortcuts.
  • AssistantChat: auto-scroll now scrolls the message container directly instead of using scrollIntoView, which could scroll the page while the panel was hidden off-screen.
  • useAssistantChat: after an unexpected reconnect it sends start again for the current conversation. When a resume finishes, it reloads the conversation from the database and replaces the live messages, so nothing shows twice. It also no longer reconnects after unmount (before, the close in the cleanup function triggered onclose, which reconnected).

Verification

  • New test_assistant_chat_session.py (4 tests): the full reply is saved when the socket drops mid-stream, a partial reply is saved when the stream is aborted, the session is reused for the same conversation, and resume() waits for the in-flight reply.
  • ruff check . passes. python test_security.py passes. test_ws_security.py passes. npm run lint shows 0 errors (the warnings were already there). npm run build passes.
  • Manual test in a browser (Playwright). I ran the real server with a stub Claude client that streams for about 9s, in an isolated HOME:
    • Close the panel mid-answer, then reopen: the full answer is there.
    • Reload the page mid-answer, then open the assistant: the full answer loads from history, with no duplicates.
    • Send a follow-up on the resumed session: it works.
    • While the panel is closed it is inert, focus stays on <body>, and the page doesn't scroll.

Notes / assumptions

  • The Vite dev proxy doesn't forward WebSockets for /api/* (only /ws has ws: true), so the assistant can't connect under npm run dev. This is already the case on master. I tested against the built UI served by the backend and left vite.config.ts unchanged.
  • If a reply streamed while you were offline ended with an ask_user question, you won't see the question buttons after reconnecting. Only the text is stored, as before.
  • I saw a mismatch between live message timestamps and stored ones (they differ by the UTC offset). It's already on master and outside this fix.

🤖 Generated with Claude Code

zwanski-start and others added 2 commits October 22, 2026 21:42
…ForgeAI#133)

- Keep consuming Claude's stream after the client goes away so the full
  answer is stored in the conversation history
- Persist partial responses if the stream is aborted
- Reuse the live session when reconnecting to the same conversation and
  wait for any in-flight response before signalling the client

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ct (AutoForgeAI#133)

- Keep AssistantChat mounted after the panel is first opened (per project)
  so closing it no longer drops the WebSocket mid-response; mark the
  hidden panel inert and scroll the message list directly so it can't
  steal focus or scroll the page
- On resume/reconnect, wait for the server to finish any in-flight reply,
  then reload the stored history instead of showing stale cached messages
- Stop reconnecting after the chat unmounts

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@zwanski-start

Copy link
Copy Markdown
Author

🔍 QA report: ✅ Passed

Tester: Guido (cubefarm agent, in a fresh session) · Round: 1 · Author: Barbara

The PR fixes #133: an assistant answer that is streaming when the panel is closed, the page is reloaded or the socket drops now finishes on the server, is saved to history, and shows once (no duplicates) when the chat reconnects. Tested end-to-end with a stub Claude client (no API key) on both the WebSocket protocol and in a real browser, including reusing the session for a follow-up. ruff, the security tests, the 4 new tests, the UI build and the UI lint pass. mypy and pytest show the same failures as master (environment/SDK related, not this PR).

Check Details
✅ Close panel mid-answer, reopen Sent a message, waited for partial output, closed the panel, waited about 10s and reopened. The full answer through END-OF-ANSWER was there. The socket stayed connected while the panel was hidden.
✅ Reload page mid-answer, reopen assistant Reloaded during streaming and opened the assistant right away. It showed 'Thinking…' while the server finished, then reloaded history. The full answer appeared exactly once and the user message was not duplicated.
✅ Unexpected socket drop and auto-reconnect Wrapped window.WebSocket and force-closed the assistant socket mid-stream. The hook reconnected, re-sent start for the conversation and waited for the reply to finish. The answer ended up complete and shown once, and the status returned to Connected.
✅ Server: reply saved after disconnect, session reused (raw WebSocket probe) With the websockets client: started a conversation, sent a message, closed the socket after 3 chunks, then reconnected with the same conversation_id. response_done arrived after about 9.6s (the server waited for the in-flight reply). The REST conversation endpoint returned the full reply, and a follow-up message streamed normally.
✅ Panel closed: inert, no focus capture, no page scroll With the panel closed: aria-hidden=true, inert=true, focus on BODY, scrollY=0. The K and G keyboard shortcuts still switched views.
✅ Kanban, dependency graph, phone layout, console Kanban showed 2 seeded features (one blocked) and the graph rendered nodes and an edge. At 390x844 the assistant panel fits with no horizontal scroll. No console errors in normal flows on the PR build.
✅ Unchanged behaviour compared with master Built and served master for comparison. The extra assistant socket opened and closed when the panel first opens, and the sidebar 'Assistant' button not opening the panel, both happen on master too. Escape not closing the panel while the textarea has focus is the global key handler's existing input guard, which this PR does not touch.
✅ Security and backward compatibility No changes to security.py, the localhost middleware or the DB schema. The new log line only says 'finishing response in background' and contains no secrets. The server process exited cleanly on stop.
⏭️ UI e2e suite All 15 tests skip themselves ('No projects available') because they look for a .neo-dropdown-item selector that no longer exists in ui/src. The suite is out of date on master too.
🧪 Commands run
Command Result
ruff check . All checks passed
mypy . 25 errors in 10 files, the same count as origin/master (the one in assistant_chat_session.py is an old unused type:ignore at line 320)
python -m pytest -q 148 passed, 21 failed; all failures are in test_devserver_security.py and also fail on master (144 passed, 21 failed)
python -m pytest -q test_assistant_chat_session.py test_ws_security.py 14 passed
python test_security.py ALL TESTS PASSED
npm --prefix ui run build tsc -b + vite build succeeded
npm --prefix ui run lint 0 errors, 6 warnings (same as before)
npx playwright test (e2e against :5820, system chromium) 15 skipped (out-of-date selector, also on master)
stub-Claude uvicorn server.main:app on 127.0.0.1:5820 + REST probes /api/projects 200, features 200, conversations 200, invalid project name 400
python wsprobe.py (WebSocket disconnect/resume probe) Full reply saved; resume waited about 9.6s for it; follow-up worked

No browser screenshots were taken in this round.

Posted by cubefarm · merges automatically once GitHub's checks pass

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug report: Assistant bot looses conversation

1 participant