Backend (VoiceGatewayService):
- [P0 Bug-1] handleAssistantChunk/Final: text mode must never apply blockUpstreamAudio
gate to text events (blockUpstreamAudio is for audio frames only). Non-KB text
queries now correctly stream subtitle back to client.
- [P0 Bug-3] sendUpstreamChatTextQuery: when upstream not ready, send
assistant_pending:false before error so client loading spinner can clear.
- [P1 Bug-6] handleUserPartial/handleUserFinal: early-return if textMode, guard
against spurious ASR echoes from S2S.
Frontend (ChatPanel S2S effect):
- [P0 Bug-2] Connection success now clears error; added cancelled flag to all
async setState paths to prevent state reversal on unmount.
- [P1 Bug-4] onAssistantPending: (false) always clears isLoading; (true) only
sets isLoading if not already streaming (streamingId drives UI, pending is
advisory).
- [P1 Bug-5] S2S mode loads session history via getSessionHistory (was Coze-only).
- [P2 Bug-8] When s2sService ref is null, also remove the placeholder assistant
bubble to avoid stale empty bubble in chat.
- [P2 Bug-9] All callbacks guard on cancelled flag to prevent React setState
warnings after unmount (cleanup triggers svc.disconnect which emits
'disconnected' state).
Verification: mvn test VoiceGatewaySmokeTest 20/20 pass, no voice regression.