CopilotKit / CopilotKit/outpost
QA chat SSE: no cross-chunk buffer silently drops the metadata frame (sources + confidence)
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 7
- Forks
- 3
- Avg merge
- 7d 16h
- Merged PRs (30d)
- 15
Description
Origin: deferred bucket (d) from the Tier-3 cr-loop review of PR #167 (merged as 55744ca). Flagged independently by four reviewers. Separate subject from that PR, which only swapped this file's fetch for apiFetch.
apps/web/src/hooks/use-qa-chat.ts consumes the /api/qa SSE stream. Four defects, in severity order.
1. No cross-chunk buffer — frames are silently dropped
:99 does chunk.split('\n') per read, with no carry buffer for a partial frame. A ReadableStream read boundary can fall anywhere, so any SSE event split across two reads is parsed as two invalid halves. Both halves land in the empty catch at :127 and vanish.
The event most likely to be split is the largest one — the metadata frame carrying sources and confidence. So the visible symptom is an answer that renders fine but loses its citations, intermittently, with nothing logged. That is the worst possible shape for a bug: it looks like the model just didn't cite anything.
Fix: keep a buffer across reads, split on \n\n, and retain the trailing partial for the next iteration.
2. [DONE] never matches under CRLF
:100-109 compares against [DONE] exactly. With CRLF line endings the token arrives as [DONE]\r, falls through to JSON.parse, and throws — again into the empty catch. Normalise line endings before comparing.
3. The empty catch is what makes 1 and 2 invisible
:127 swallows every parse failure with no logging. Both defects above would be obvious in a single session with any logging at all. Related: #197 — nothing in apps/web uses the structured logger that exists.
4. Concurrent sends corrupt shared state
:68 assigns a new AbortController over the in-flight one without aborting it, so a second send leaves the first stream running and both write the same state. There is also no abort on unmount. Separately, :61 reads conversationHistory from a stale closure, so rapid sends replay incomplete history to the model — and :152 strands the user's message on abort while the error placeholder gets replayed as assistant history.
Test gaps
No coverage for a frame split across chunk boundaries, no CRLF case, no concurrent-send case. A regression test for #1 needs a mocked stream that deliberately splits a frame mid-JSON — worth writing first, since it fails today.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in apps/web/src/hooks/use-qa-chat.ts, especially the stream-reading logic around lines 61, 68, 99-127, and 152. Add mocked-stream regression coverage for a frame split mid-JSON, CRLF [DONE], concurrent sends, and abort behavior. Done means metadata is retained, termination and parse failures are handled visibly, and concurrent or aborted sends do not corrupt conversation state.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- frontend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100