* fix(chat): preserve reasoning across tool calls + REST reload (#771) A reasoning-model turn streams its thinking BEFORE the tool starts. The reducer wiped the streaming state at tool.start (returned a fresh StreamingState()), so the reasoning bubble vanished mid-turn and the finalized answer had no reasoning card. Real gateway capture for `run echo hi` (reasoning model): message.start reasoning.delta xN (thinking before the tool) tool.generating/start/complete (terminal) message.delta xN (answer after the tool) reasoning.available (FULL trace in payload text) message.complete (text + reasoning field) Fixes: - onToolStart keeps the streaming message + reasoningText alive across the tool call (only buffers are cleared via clearStreamingBuffers); an interim-text orphan still seals, but reasoning survives for the post-tool answer - EventParser now reads reasoning.available text and message.complete reasoning (the authoritative full trace the gateway sends) - onReasoningAvailable fills the streaming message with the full trace - onMessageComplete prefers the payload reasoning over delta leftovers - mapServerMessages folds reasoning-only empty assistant rows into the next real answer bubble (no more standalone reasoning ghost bubble) and carries tool names over from the live WS list by position (no more generic 'tool' title after a REST reload) Regression tests replay the captured sequence end-to-end. * fix(chat): dedupe WS + REST tool bubbles by result content (#771) The REST transcript tool rows carry no tool name, and WS messages (UUID ids) + REST rows (rest- ids) are BOTH persisted to Room — the cache paint and reloads could render the same tool call twice, with the second bubble falling back to the generic 'tool' title. Real captured pair for 'echo meow': terminal {tool_id, name, args, result:{...}} (WS) Tool {output, exit_code, error} (REST, no name) The previous positional carry-over misaligned whenever the REST page and the live tool list didn't line up 1:1 (multiple tool calls in history → newest call got null name). Fixes: - canonicalToolResultKey(): content-based matching between the WS tool.complete payload and the REST result row — int/float agnostic (0 vs 0.0), prefers the payload's 'result' field - mapServerMessages(): when a REST tool row's result matches a live WS tool message, REUSE the live message wholesale (same id + toolName + rich payload) so Room upserts instead of accumulating a rest- copy - dedupeCachedMessages(): cache paint drops rest- copies whenever a WS copy of the same logical message exists (tools matched by result, other roles by content) 11 new unit tests with the real captured payloads. * fix(chat): merge transcript reload with live WS bubbles (#771) The gateway persists a tool row only when the tool COMPLETES server-side. A REST transcript reload landing mid-turn (app background/foreground, reconnect re-resume, pull-refresh) therefore returned a page WITHOUT the in-flight tool row — replacing the message list outright made the tool bubble vanish and left tool.complete with no RUNNING message to update. - mergeTranscriptWithLive(): reload now merges instead of replacing — any current message the REST page doesn't cover (by id or logical content) survives, chronological order preserved - loadSessionMessages applies the merge before publishing state 4 new unit tests: in-flight RUNNING tool survives a mid-turn reload, reused-id and canonical-match cases don't duplicate, order preserved. * test(chat): end-to-end tool-bubble survival through mid-turn reload Chains the real user scenario at the state level: WS turn (message.start → reasoning → tool.start → tool.complete) + a transcript reload landing while the tool is still running server-side (REST page lacks the tool row) + stream finish. Asserts the tool bubble survives to the final list with its name and status intact. * debug(chat): add Logcat tracing for tool-bubble vanish (#771) Temporary diagnostic logging to catch the exact moment and code path where the tool bubble disappears. Logs to tag 'ChatReducer771' and 'ChatVM771' at every state mutation that touches messages: - onMessageComplete: message count + roles before the assistant append - loadSessionMessages: rest vs current vs merged counts and roles - syncCurrentSession: trigger + current tool count - sync merge: before/after merge counts and roles To reproduce: install this debug APK, run 'echo meow', watch the tool bubble vanish, then capture logcat: adb logcat -d -s ChatReducer771:D ChatVM771:D Will remove the logging once the root cause is pinned. * fix(chat): drop debug logging + harden sync merge (#771) The debug commit added android.util.Log calls in the reducer's onMessageComplete path, which is a Stub! in JVM unit tests — every test touching that path died with RuntimeException, and the long log lines broke ktlint (max-line-length). Remove them. Sync merge fix: the 5s sync poll could drop the newest WS tool bubble when the REST page lagged one row behind. Route the merge through sameLogicalMessage (canonical result-key match) and keep any WS message that has no REST counterpart. Adds 2 regression tests for the sync-merge scenario. --------- Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
1 parent
ec72ed4733
commit
b4d0a7c9c3
7 files changed
+872
-35
No files matched your search
@@ -68,12 +68,14 @@ object EventParser {
|
||||
}
|
||||
|
||||
"reasoning.available" -> {
|
||||
WsEvent.ReasoningAvailable(sessionId)
|
||||
val text = payload?.get("text") as? String
|
||||
WsEvent.ReasoningAvailable(sessionId, text)
|
||||
}
|
||||
|
||||
"message.complete" -> {
|
||||
val text = payload?.get("text") as? String ?: ""
|
||||
WsEvent.MessageComplete(text, sessionId)
|
||||
val reasoning = payload?.get("reasoning") as? String
|
||||
WsEvent.MessageComplete(text, sessionId, reasoning)
|
||||
}
|
||||
|
||||
"message.done" -> {
|
||||
|
||||
@@ -37,11 +37,26 @@ sealed class WsEvent {
|
||||
|
||||
data class ReasoningAvailable(
|
||||
val sessionId: String?,
|
||||
/**
|
||||
* Full reasoning text, emitted once reasoning finishes streaming
|
||||
* (`reasoning.available` payload `text`). The gateway sends the
|
||||
* complete trace here even when per-token `reasoning.delta` events
|
||||
* were throttled, dropped, or wiped by a mid-turn tool.start — the
|
||||
* client must use it as the authoritative fill for the card.
|
||||
*/
|
||||
val text: String? = null,
|
||||
) : WsEvent()
|
||||
|
||||
data class MessageComplete(
|
||||
val text: String,
|
||||
val sessionId: String?,
|
||||
/**
|
||||
* Full reasoning trace for the completed message, carried in the
|
||||
* `message.complete` payload (`reasoning` key). Authoritative
|
||||
* fallback when per-token `reasoning.delta` events never arrived
|
||||
* (e.g. a tool call wiped the streaming buffer mid-turn).
|
||||
*/
|
||||
val reasoning: String? = null,
|
||||
) : WsEvent()
|
||||
|
||||
data class MessageDone(
|
||||
|
||||
Reference in new issue
Block a user