fix(chat): clear stale reasoning when a new message starts (#755) (#760)

A new assistant message was seeded with the previous message's
reasoningText (and isReasoning flag) via onMessageStart, and
resetStreaming() never cleared the shared streaming state. If the
next reply produced no reasoning, the stale trace stayed glued to
the new bubble and got persisted to Room.

- onMessageStart: new message + StreamingState start with empty
  reasoning instead of inheriting the previous turn's
- resetStreaming: clears isReasoning + reasoningText on the shared
  state (finalized messages keep their own copy for the card)
- regression tests: MessageStart seeding, no-reasoning persistence,
  reset clearing, finalized-message preservation

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
M57andM57 authored and GitHub committed 2026-08-01 20:14:35 +03:00
1 parent e014d2a0e7
commit 3a96edd4c1
4 files changed
+145 -3

No files matched your search

@@ -49,6 +49,10 @@ class ChatStreamingController(
lastFlushMs = 0L
lastThinkingFlushMs = 0L
lastReasoningFlushMs = 0L
// Issue #755: the shared streaming state must not carry the previous
// message's reasoning into the next one. The finalized message keeps
// its own copy (persisted to Room), so clearing here is safe.
streamingState.update { it.copy(isReasoning = false, reasoningText = "") }
}
/** Resets buffers and starts a fresh streaming message (called on MessageStart). */
@@ -146,7 +146,10 @@ object ChatWsEventReducer {
ChatMessage(
role = MessageRole.ASSISTANT,
content = "",
reasoningText = streamingState.reasoningText,
// Issue #755: never seed a new message with the previous
// message's reasoning — the card must start blank and only
// fill once real reasoning deltas arrive.
reasoningText = "",
isStreaming = true,
)
val effects = mutableListOf<ReducerEffect>()
@@ -178,8 +181,10 @@ object ChatWsEventReducer {
val newStreamingState =
StreamingState(
streamingMessage = msg,
isReasoning = streamingState.isReasoning,
reasoningText = streamingState.reasoningText,
// Issue #755: the new message starts with a clean reasoning
// slate — no isReasoning flag, no inherited reasoning text.
isReasoning = false,
reasoningText = "",
)
val newState = preState
val sid = newState.currentSessionId