fix(chatqna): strip leaked |<-MSG->| markers from streaming SSE
Problem
The streaming path forwarded LLM token chunks verbatim, so the internal |<-MSG->| history delimiters and USER:/ASSISTANT: role markers the LLM sometimes echoes back leaked through the SSE stream to the frontend.
The non-streaming path already strips these from the assembled llm_response, but the streaming branch (_stream_with_metadata) returns early and bypassed that stripping. El Salvador (streaming + streaming-translation) was affected — markers reached the backend/frontend unprocessed.
Streaming: LLM chunks → SSE → frontend (RAW, not stripped) → user sees |<-MSG->|
Non-stream: LLM response → stripping (line ~1918) → frontend → clean
Fix
Apply the same stripping inside _stream_with_metadata (the single choke point every streamed token passes through). Because a marker can be split across token chunks (e.g. |<-M + SG->|), content is buffered:
- only the settled head is re-emitted
- a tail that could be the start of a marker is held back until the marker completes (and is stripped) or enough non-marker text arrives
- the role-marker regex tolerates leading whitespace so a marker split from its preceding separator across chunks (separator →
\\nin one chunk,USER:arriving next) is still stripped - unparseable chunks are forwarded verbatim so an unexpected orchestrator format never breaks the stream
Verification
- 12 new
TestStreamWithMetadatacases: separator/role in single chunk + across chunks, role split from separator, normal-content preservation, lone-pipe preservation, partial-marker flush, verbatim passthrough, unicode, realistic full-history echo - Adversarial sim (34 pseudo-random 1-5 char chunks through a full history echo): 0 marker leakage, all real content preserved
- Full OPEA suite: 425 passed
- Ruff lint + format: clean