Skip to content

Commit b01273c

Browse files
committed
fix: narrow finalize boundary-drop guard (#27711) (thanks @scz2011)
1 parent d6cbaea commit b01273c

File tree

3 files changed

+41
-3
lines changed

3 files changed

+41
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai
1717

1818
### Fixes
1919

20+
- TUI/stream assembly: preserve streamed text across real tool-boundary drops without keeping stale streamed text when non-text blocks appear only in the final payload. Landed from contributor PR #27711 by @scz2011. (#27674)
2021
- Models/MiniMax auth header defaults: set `authHeader: true` for both onboarding-generated MiniMax API providers and implicit built-in MiniMax (`minimax`, `minimax-portal`) provider templates so first requests no longer fail with MiniMax `401 authentication_error` due to missing `Authorization` header. Landed from contributor PRs #27622 by @riccoyuanft and #27631 by @kevinWangSheng. (#27600, #15303)
2122
- Pi image-token usage: stop re-injecting history image blocks each turn, process image references from the current prompt only, and prune already-answered user-image blocks in stored history to prevent runaway token growth. (#27602)
2223
- BlueBubbles/SSRF: auto-allowlist the configured `serverUrl` hostname for attachment fetches so localhost/private-IP BlueBubbles setups are no longer false-blocked by default SSRF checks. Landed from contributor PR #27648 by @lailoo. (#27599) Thanks @taylorhou for reporting.

src/tui/tui-stream-assembler.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,35 @@ describe("TuiStreamAssembler", () => {
152152
expect(finalText).toBe("Draft line 1");
153153
});
154154

155+
it("prefers final text when non-text blocks appear only in final payload", () => {
156+
const assembler = new TuiStreamAssembler();
157+
assembler.ingestDelta(
158+
"run-5c",
159+
{
160+
role: "assistant",
161+
content: [
162+
{ type: "text", text: "Draft line 1" },
163+
{ type: "text", text: "Draft line 2" },
164+
],
165+
},
166+
false,
167+
);
168+
169+
const finalText = assembler.finalize(
170+
"run-5c",
171+
{
172+
role: "assistant",
173+
content: [
174+
{ type: "tool_use", name: "search" },
175+
{ type: "text", text: "Draft line 2" },
176+
],
177+
},
178+
false,
179+
);
180+
181+
expect(finalText).toBe("Draft line 2");
182+
});
183+
155184
it("accepts richer final payload when it extends streamed text", () => {
156185
const assembler = new TuiStreamAssembler();
157186
assembler.ingestDelta(

src/tui/tui-stream-assembler.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,10 @@ export class TuiStreamAssembler {
9797
state: RunStreamState,
9898
message: unknown,
9999
showThinking: boolean,
100-
opts?: { protectBoundaryDrops?: boolean },
100+
opts?: {
101+
protectBoundaryDrops?: boolean;
102+
useIncomingNonTextForBoundaryDrops?: boolean;
103+
},
101104
) {
102105
const thinkingText = extractThinkingFromMessage(message);
103106
const contentText = extractContentFromMessage(message);
@@ -108,9 +111,11 @@ export class TuiStreamAssembler {
108111
}
109112
if (contentText) {
110113
const nextContentBlocks = textBlocks.length > 0 ? textBlocks : [contentText];
114+
const useIncomingNonTextForBoundaryDrops = opts?.useIncomingNonTextForBoundaryDrops !== false;
111115
const shouldPreserveBoundaryDroppedText =
112116
opts?.protectBoundaryDrops === true &&
113-
(state.sawNonTextContentBlocks || sawNonTextContentBlocks) &&
117+
(state.sawNonTextContentBlocks ||
118+
(useIncomingNonTextForBoundaryDrops && sawNonTextContentBlocks)) &&
114119
isDroppedBoundaryTextBlockSubset({
115120
streamedTextBlocks: state.contentBlocks,
116121
finalTextBlocks: nextContentBlocks,
@@ -151,7 +156,10 @@ export class TuiStreamAssembler {
151156
const streamedDisplayText = state.displayText;
152157
const streamedTextBlocks = [...state.contentBlocks];
153158
const streamedSawNonTextContentBlocks = state.sawNonTextContentBlocks;
154-
this.updateRunState(state, message, showThinking, { protectBoundaryDrops: true });
159+
this.updateRunState(state, message, showThinking, {
160+
protectBoundaryDrops: true,
161+
useIncomingNonTextForBoundaryDrops: false,
162+
});
155163
const finalComposed = state.displayText;
156164
const shouldKeepStreamedText =
157165
streamedSawNonTextContentBlocks &&

0 commit comments

Comments
 (0)