fix(tui): preserve streamed text when final payload regresses (#15452)#15573
Conversation
src/tui/tui-stream-assembler.ts
Outdated
| function mergeTextPreferRicher(currentText: string, nextText: string): string { | ||
| const current = currentText.trim(); | ||
| const next = nextText.trim(); | ||
| if (!next) { | ||
| return current; | ||
| } | ||
| if (!current || current === next) { | ||
| return next; | ||
| } | ||
| if (next.includes(current)) { | ||
| return next; | ||
| } | ||
| if (current.includes(next)) { | ||
| return current; | ||
| } | ||
| return next; | ||
| } |
There was a problem hiding this comment.
Final text can be wrong
mergeTextPreferRicher() decides “richer” via substring checks (includes). That means a non-empty but shorter final payload can be ignored if it happens to be a substring of the streamed text (e.g. streamed "NOT OK", final "OK"), even when the final payload is the canonical value. This is a behavior change vs resolveFinalAssistantText() (src/tui/tui-formatters.ts:8-15), which prefers any non-empty final text and only falls back to streamed when final is empty. Consider restricting the “prefer streamed” behavior to the specific regression case (final is an exact suffix/prefix caused by dropped blocks) rather than general substring inclusion.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/tui/tui-stream-assembler.ts
Line: 14:30
Comment:
**Final text can be wrong**
`mergeTextPreferRicher()` decides “richer” via substring checks (`includes`). That means a *non-empty but shorter* final payload can be ignored if it happens to be a substring of the streamed text (e.g. streamed `"NOT OK"`, final `"OK"`), even when the final payload is the canonical value. This is a behavior change vs `resolveFinalAssistantText()` (`src/tui/tui-formatters.ts:8-15`), which prefers any non-empty final text and only falls back to streamed when final is empty. Consider restricting the “prefer streamed” behavior to the specific regression case (final is an exact suffix/prefix caused by dropped blocks) rather than general substring inclusion.
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
已修复:收敛了 merge 逻辑,仅在“行块前后缀丢失”回归形态下保留 streamed;不再用通用 substring 覆盖非空 final。并新增 NOT OK -> OK 回归测试。
|
Quick note on the cancelled checks: they were from older head SHAs and got auto-cancelled by workflow concurrency (cancel-in-progress: true) after I pushed follow-up fixes. Current head SHA is 1694a71, and the latest check suite has been re-triggered for this commit. I will keep monitoring and address any new findings immediately. |
|
I tracked down the CI failure and pushed a minimal follow-up fix in commit Root cause was shared lint drift (
This commit only removes unused |
|
Heads-up: if the new run fails on (), that is a base-branch lint regression now tracked in #15610.\n\nThis PR already has its own branch-specific fixes applied. |
|
Correction with exact text:
Tracked/fixed in #15610. |
|
Applied the baseline lint fix directly to this branch. New commit:
This should unblock the |
b672cbd to
ef873aa
Compare
|
Rebased this branch onto latest Current head: |
801ccd7 to
902ea6b
Compare
902ea6b to
96d1a68
Compare
fa77833 to
e4a5e3c
Compare
…aw#15452) (openclaw#15573) Merged via /review-pr -> /prepare-pr -> /merge-pr. Prepared head SHA: e4a5e3c Co-authored-by: TsekaLuk <79151285+TsekaLuk@users.noreply.github.com> Co-authored-by: steipete <58493+steipete@users.noreply.github.com> Reviewed-by: @steipete
…aw#15452) (openclaw#15573) Merged via /review-pr -> /prepare-pr -> /merge-pr. Prepared head SHA: e4a5e3c Co-authored-by: TsekaLuk <79151285+TsekaLuk@users.noreply.github.com> Co-authored-by: steipete <58493+steipete@users.noreply.github.com> Reviewed-by: @steipete
…aw#15452) (openclaw#15573) Merged via /review-pr -> /prepare-pr -> /merge-pr. Prepared head SHA: e4a5e3c Co-authored-by: TsekaLuk <79151285+TsekaLuk@users.noreply.github.com> Co-authored-by: steipete <58493+steipete@users.noreply.github.com> Reviewed-by: @steipete
…aw#15452) (openclaw#15573) Merged via /review-pr -> /prepare-pr -> /merge-pr. Prepared head SHA: e4a5e3c Co-authored-by: TsekaLuk <79151285+TsekaLuk@users.noreply.github.com> Co-authored-by: steipete <58493+steipete@users.noreply.github.com> Reviewed-by: @steipete
…aw#15452) (openclaw#15573) Merged via /review-pr -> /prepare-pr -> /merge-pr. Prepared head SHA: e4a5e3c Co-authored-by: TsekaLuk <79151285+TsekaLuk@users.noreply.github.com> Co-authored-by: steipete <58493+steipete@users.noreply.github.com> Reviewed-by: @steipete
Fixes openclaw#20453 The previous fix (openclaw#15573) only protected against boundary text block drops when there were non-text content blocks (like tool calls). This variant occurs with pure text responses where the final payload drops boundary text blocks that were streamed. Now we preserve streamed text when the final text blocks are a prefix or suffix subset of the streamed text blocks, regardless of whether non-text content was present.
…aw#15452) (openclaw#15573) Merged via /review-pr -> /prepare-pr -> /merge-pr. Prepared head SHA: e4a5e3c Co-authored-by: TsekaLuk <79151285+TsekaLuk@users.noreply.github.com> Co-authored-by: steipete <58493+steipete@users.noreply.github.com> Reviewed-by: @steipete
Summary
Fixes a TUI regression where the finalized assistant message can overwrite a richer streamed message with a shorter payload.
Closes #15452.
Problem
When an assistant turn emits text, then tool calls, then more text, the TUI can show the correct full streamed content during
deltaevents, but replace it with a shorter text onfinal.Root Cause
TuiStreamAssembler.finalize()rebuilt display state from the final payload and accepted shorter subsets (e.g. post-tool blocks only), which regressed already-correct streamed content.Changes
src/tui/tui-stream-assembler.tsmergeTextPreferRicher()to prefer richer cumulative text and reject subset regressions.finalize(), pass the pre-final streamed display asstreamedTextfallback.src/tui/tui-stream-assembler.test.tsTests
pnpm vitest run src/tui/tui-stream-assembler.test.ts src/tui/tui-event-handlers.test.ts src/tui/tui.test.tsBroader Validation Notes
I also ran repo-level checks in this environment:
pnpm lintandpnpm checkfail on pre-existing unrelated files (src/infra/provider-usage.auth.normalizes-keys.test.ts,src/plugins/discovery.test.ts) due unusedviimports.pnpm buildpassed.pnpm testhas unrelated pre-existing failures/timeouts in this environment (e.g.src/security/audit.test.ts,src/gateway/tools-invoke-http.test.ts,src/browser/pw-session.get-page-for-targetid.extension-fallback.test.ts, lobster extension timeouts).No failures were observed in the touched TUI tests.
Sign-Off
lobster-biscuit
Greptile Overview
Greptile Summary
This PR updates
TuiStreamAssemblerto avoid a regression where a shorterfinalassistant payload can overwrite the richer text accumulated duringdeltastreaming. It introducesmergeTextPreferRicher()to retain previously accumulated thinking/content when the next update appears to be a subset, and it changesfinalize()to pass the pre-final streamed display text as the fallback intoresolveFinalAssistantText().Tests add two new cases covering the reported regression (final drops earlier text blocks) and ensuring richer finals still win when they extend streamed content.
Confidence Score: 3/5
mergeTextPreferRicher()uses broadincludeschecks that can cause the assembler to ignore a legitimate non-empty final payload whenever it is shorter and happens to be a substring of the streamed text, changing the intended “final wins unless empty” behavior.Last reviewed commit: ffcd96e