fix: narrow finalize boundary-drop guard (#27711) (thanks @scz2011)

This commit is contained in:
Peter Steinberger
2026-02-26 20:49:23 +01:00
parent d6cbaea434
commit b01273cfc6
3 changed files with 41 additions and 3 deletions

View File

@@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- 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)
- 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)
- 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)
- 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.

View File

@@ -152,6 +152,35 @@ describe("TuiStreamAssembler", () => {
expect(finalText).toBe("Draft line 1");
});
it("prefers final text when non-text blocks appear only in final payload", () => {
const assembler = new TuiStreamAssembler();
assembler.ingestDelta(
"run-5c",
{
role: "assistant",
content: [
{ type: "text", text: "Draft line 1" },
{ type: "text", text: "Draft line 2" },
],
},
false,
);
const finalText = assembler.finalize(
"run-5c",
{
role: "assistant",
content: [
{ type: "tool_use", name: "search" },
{ type: "text", text: "Draft line 2" },
],
},
false,
);
expect(finalText).toBe("Draft line 2");
});
it("accepts richer final payload when it extends streamed text", () => {
const assembler = new TuiStreamAssembler();
assembler.ingestDelta(

View File

@@ -97,7 +97,10 @@ export class TuiStreamAssembler {
state: RunStreamState,
message: unknown,
showThinking: boolean,
opts?: { protectBoundaryDrops?: boolean },
opts?: {
protectBoundaryDrops?: boolean;
useIncomingNonTextForBoundaryDrops?: boolean;
},
) {
const thinkingText = extractThinkingFromMessage(message);
const contentText = extractContentFromMessage(message);
@@ -108,9 +111,11 @@ export class TuiStreamAssembler {
}
if (contentText) {
const nextContentBlocks = textBlocks.length > 0 ? textBlocks : [contentText];
const useIncomingNonTextForBoundaryDrops = opts?.useIncomingNonTextForBoundaryDrops !== false;
const shouldPreserveBoundaryDroppedText =
opts?.protectBoundaryDrops === true &&
(state.sawNonTextContentBlocks || sawNonTextContentBlocks) &&
(state.sawNonTextContentBlocks ||
(useIncomingNonTextForBoundaryDrops && sawNonTextContentBlocks)) &&
isDroppedBoundaryTextBlockSubset({
streamedTextBlocks: state.contentBlocks,
finalTextBlocks: nextContentBlocks,
@@ -151,7 +156,10 @@ export class TuiStreamAssembler {
const streamedDisplayText = state.displayText;
const streamedTextBlocks = [...state.contentBlocks];
const streamedSawNonTextContentBlocks = state.sawNonTextContentBlocks;
this.updateRunState(state, message, showThinking, { protectBoundaryDrops: true });
this.updateRunState(state, message, showThinking, {
protectBoundaryDrops: true,
useIncomingNonTextForBoundaryDrops: false,
});
const finalComposed = state.displayText;
const shouldKeepStreamedText =
streamedSawNonTextContentBlocks &&