Skip to content

fix(tui): preserve streamed text when final payload regresses (#15452)#15573

Merged
steipete merged 5 commits intoopenclaw:mainfrom
TsekaLuk:codex/fix-tui-finalize-stream-loss
Feb 13, 2026
Merged

fix(tui): preserve streamed text when final payload regresses (#15452)#15573
steipete merged 5 commits intoopenclaw:mainfrom
TsekaLuk:codex/fix-tui-finalize-stream-loss

Conversation

@TsekaLuk
Copy link
Contributor

@TsekaLuk TsekaLuk commented Feb 13, 2026

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 delta events, but replace it with a shorter text on final.

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.ts
    • Added mergeTextPreferRicher() to prefer richer cumulative text and reject subset regressions.
    • In finalize(), pass the pre-final streamed display as streamedText fallback.
  • src/tui/tui-stream-assembler.test.ts
    • Added regression test: keep full streamed text when final payload drops earlier blocks.
    • Added guard test: still accept richer final payload when final extends streamed content.

Tests

  • pnpm vitest run src/tui/tui-stream-assembler.test.ts src/tui/tui-event-handlers.test.ts src/tui/tui.test.ts
    • Passed (18/18).

Broader Validation Notes

I also ran repo-level checks in this environment:

  • pnpm lint and pnpm check fail on pre-existing unrelated files (src/infra/provider-usage.auth.normalizes-keys.test.ts, src/plugins/discovery.test.ts) due unused vi imports.
  • pnpm build passed.
  • pnpm test has 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 TuiStreamAssembler to avoid a regression where a shorter final assistant payload can overwrite the richer text accumulated during delta streaming. It introduces mergeTextPreferRicher() to retain previously accumulated thinking/content when the next update appears to be a subset, and it changes finalize() to pass the pre-final streamed display text as the fallback into resolveFinalAssistantText().

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

  • This PR is close, but the new substring-based merge can produce incorrect final text in some realistic cases.
  • The targeted regression fix makes sense and tests cover the reported scenario, but mergeTextPreferRicher() uses broad includes checks 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.
  • src/tui/tui-stream-assembler.ts

Last reviewed commit: ffcd96e

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines +14 to +30
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;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修复:收敛了 merge 逻辑,仅在“行块前后缀丢失”回归形态下保留 streamed;不再用通用 substring 覆盖非空 final。并新增 NOT OK -> OK 回归测试。

@TsekaLuk
Copy link
Contributor Author

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.

@TsekaLuk
Copy link
Contributor Author

I tracked down the CI failure and pushed a minimal follow-up fix in commit 5bd515194.

Root cause was shared lint drift (no-unused-vars) in two test files:

  • src/plugins/discovery.test.ts
  • src/infra/provider-usage.auth.normalizes-keys.test.ts

This commit only removes unused vi imports (no behavior change) and retriggers CI.

@steipete steipete self-assigned this Feb 13, 2026
@TsekaLuk
Copy link
Contributor Author

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.

@TsekaLuk
Copy link
Contributor Author

Correction with exact text:

  • potential external blocker is CI check
  • failing lint: eslint(no-control-regex)
  • file path: src/gateway/server/ws-connection.ts

Tracked/fixed in #15610.

@TsekaLuk
Copy link
Contributor Author

Applied the baseline lint fix directly to this branch.

New commit: b672cbd6f

  • fixes eslint(no-control-regex) in src/gateway/server/ws-connection.ts
  • keeps branch pnpm check green locally

This should unblock the check job from the shared base-branch failure mode.

@TsekaLuk TsekaLuk force-pushed the codex/fix-tui-finalize-stream-loss branch from b672cbd to ef873aa Compare February 13, 2026 17:34
@TsekaLuk
Copy link
Contributor Author

Rebased this branch onto latest main and force-pushed to clear merge conflicts.

Current head: ef873aafd
Status now shows mergeable=MERGEABLE; CI has restarted.

@openclaw-barnacle openclaw-barnacle bot added the gateway Gateway runtime label Feb 13, 2026
steipete added a commit to TsekaLuk/openclaw that referenced this pull request Feb 13, 2026
@steipete steipete force-pushed the codex/fix-tui-finalize-stream-loss branch from 801ccd7 to 902ea6b Compare February 13, 2026 17:54
steipete added a commit to TsekaLuk/openclaw that referenced this pull request Feb 13, 2026
steipete added a commit to TsekaLuk/openclaw that referenced this pull request Feb 13, 2026
steipete added a commit to TsekaLuk/openclaw that referenced this pull request Feb 13, 2026
@steipete steipete force-pushed the codex/fix-tui-finalize-stream-loss branch from 902ea6b to 96d1a68 Compare February 13, 2026 18:00
steipete added a commit to TsekaLuk/openclaw that referenced this pull request Feb 13, 2026
@openclaw-barnacle openclaw-barnacle bot added size: M and removed gateway Gateway runtime size: S labels Feb 13, 2026
@steipete steipete force-pushed the codex/fix-tui-finalize-stream-loss branch from fa77833 to e4a5e3c Compare February 13, 2026 18:12
@steipete steipete merged commit 5cd9e21 into openclaw:main Feb 13, 2026
14 checks passed
@steipete
Copy link
Contributor

Merged via squash.

Thanks @TsekaLuk!

Plorax pushed a commit to Plorax/openclaw that referenced this pull request Feb 13, 2026
…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
steipete added a commit to AI-Reviewer-QS/openclaw that referenced this pull request Feb 13, 2026
…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
zhangyang-crazy-one pushed a commit to zhangyang-crazy-one/openclaw that referenced this pull request Feb 13, 2026
…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
steipete added a commit to azade-c/openclaw that referenced this pull request Feb 14, 2026
…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
GwonHyeok pushed a commit to learners-superpumped/openclaw that referenced this pull request Feb 15, 2026
…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
MisterGuy420 pushed a commit to MisterGuy420/openclaw-dev that referenced this pull request Feb 19, 2026
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.
zooqueen pushed a commit to hanzoai/bot that referenced this pull request Mar 6, 2026
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: TUI: streamed text replaced with shorter text on finalize after tool calls

2 participants