Fix errored stream bleeding into the next reply's leading chars
ober
d8719d10dd2ea11768cd8996116abc40eec47187
--- a/src/jcode/ui/tui.ss +++ b/src/jcode/ui/tui.ss @@ -1521,21 +1521,29 @@ (def (tui-stream-token! state token) "Handle a streaming token from the LLM — called from agent thread." - (let ((buf (app-state-stream-buf state))) - (tui-log "tui-stream-token: buf-len=~a token-len=~a" (string-length buf) (string-length token)) - ;; buf="" marks the start of an assistant turn (initial round, or a round - ;; resumed after tool calls). Reuse the trailing assistant block only - ;; when it is still the empty placeholder; otherwise append a fresh one - ;; so we don't overwrite the previous round's reply. - (when (and (string-empty? buf) - (let ((msgs (app-state-messages state))) - (or (null? msgs) - (let ((last (car (reverse msgs)))) - (or (not (streamed-reply-role? (msg-block-role last))) - (not (string-empty? (msg-block-content last)))))))) - (tui-log "tui-stream-token: adding new assistant block") + ;; Reuse the trailing block ONLY when it is the live streaming target: a + ;; streamed-reply block (assistant/expert) whose content still equals + ;; stream-buf. Otherwise — after an error/system/tool block, or once the + ;; buffer was reset between rounds — start a FRESH assistant block and + ;; clear the buffer. The old logic only opened a new block when stream-buf + ;; was empty, so a reply that arrived after an aborted stream appended onto + ;; whatever stale content was left behind. That is the "error bleeds into + ;; the next output" bug: a partial byte from the aborted stream prefixed + ;; the next reply and corrupted its leading chars (e.g. "Based on" shown as + ;; "<U+FFFD>ased on"). Keying reuse on content==stream-buf makes streaming + ;; robust to any preceding block. + (let* ((msgs (app-state-messages state)) + (last (and (pair? msgs) (car (reverse msgs)))) + (reuse? (and last + (streamed-reply-role? (msg-block-role last)) + (equal? (msg-block-content last) + (app-state-stream-buf state))))) + (tui-log "tui-stream-token: reuse?=~a token-len=~a" reuse? (string-length token)) + (unless reuse? + (tui-log "tui-stream-token: opening fresh assistant block") + (app-state-stream-buf-set! state "") (add-message! state (msg-block-assistant ""))) - (let ((new-buf (string-append buf token))) + (let ((new-buf (string-append (app-state-stream-buf state) token))) (app-state-stream-buf-set! state new-buf) (update-last-assistant! state new-buf) (app-state-scroll-offset-set! state 0)