Skip to content

fix(frontend): preserve streamed message prefix on reconnect#3318

Open
LittleChenLiya wants to merge 2 commits into
bytedance:mainfrom
LittleChenLiya:fix/issue-3279-stream-history
Open

fix(frontend): preserve streamed message prefix on reconnect#3318
LittleChenLiya wants to merge 2 commits into
bytedance:mainfrom
LittleChenLiya:fix/issue-3279-stream-history

Conversation

@LittleChenLiya
Copy link
Copy Markdown
Collaborator

@LittleChenLiya LittleChenLiya commented May 29, 2026

问题原因

流式回答过程中切换到其他对话再返回时,前端会同时合并已持久化的历史消息和重连后的 live stream 消息。对于同一个 AI 消息 ID,现有合并逻辑总是让 live stream 消息覆盖历史消息;如果重连后的流只包含后续增量文本,已显示并持久化的前缀就会在流式过程中从页面上消失。

修改内容

  • 在流式状态下,仅对带有 stream metadata 的同 ID AI 消息合并历史前缀和 live 增量。
  • 保留 live 消息的其他字段,避免影响元数据、工具调用等运行时信息。
  • 补充消息合并单元测试,覆盖重连增量、中文无重叠增量、重复尾部去重、完整 live 内容以及无 stream metadata 的替换场景。

关联 issue

Closes #3279

Problem Cause

When users leave a chat during streaming and then return, the frontend merges persisted history messages with the reconnected live stream messages. For the same AI message ID, the previous merge logic always let the live stream message replace the history message. If the reconnected stream only contains later delta text, the already rendered and persisted prefix disappears from the page while streaming continues.

Changes

  • During active streaming, merge the persisted history prefix with the live delta only for same-ID AI messages that carry stream metadata.
  • Preserve the live message object fields so metadata, tool calls, and other runtime information continue to come from the live stream message.
  • Added message merge unit coverage for reconnect deltas, Chinese text without overlap, repeated suffix dedupe, full live content, and replacement updates without stream metadata.

Related Issue

Closes #3279

@LittleChenLiya LittleChenLiya requested a review from Copilot June 4, 2026 04:03
@github-actions github-actions Bot added needs-validation Touches front/back contract surface; needs real-path validation risk:medium Medium risk: regular code changes size/M PR changes 100-300 lines area:frontend Next.js frontend under frontend/ and removed size/M PR changes 100-300 lines risk:medium Medium risk: regular code changes needs-validation Touches front/back contract surface; needs real-path validation labels Jun 4, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR improves mergeMessages behavior to preserve already-persisted AI text when a streaming connection reconnects and resumes with only new deltas, avoiding duplicated tails and retaining live metadata.

Changes:

  • Added multiple unit tests covering AI-prefix preservation, overlap deduplication, and non-stitching behaviors.
  • Extended mergeMessages with optional partial-overlap stitching for AI messages (including array-based text content).
  • Updated useThreadStream to enable the new stitching mode only for messages with streamMetadata while loading.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
frontend/tests/unit/core/threads/message-merge.test.ts Adds tests for reconnect/stream delta merge behavior and overlap deduplication.
frontend/src/core/threads/hooks.ts Implements AI text overlap detection + stitching and wires it into mergeMessages/useThreadStream.

Comment on lines +150 to +172
function shouldJoinPartialAiText(
historyText: string,
threadText: string,
allowDisjointText: boolean,
) {
if (!historyText || !threadText) {
return false;
}
if (threadText.startsWith(historyText)) {
return false;
}
if (historyText.endsWith(threadText)) {
return true;
}
const overlap = longestTextOverlap(historyText, threadText);
if (overlap > 0) {
return true;
}
if (allowDisjointText) {
return true;
}
return /\s$/.test(historyText) || /^\s/.test(threadText);
}
Comment on lines 282 to +286
export function mergeMessages(
historyMessages: Message[],
threadMessages: Message[],
optimisticMessages: Message[],
preservePartialAiOverlap: boolean | PartialAiOverlapPredicate = false,
Comment on lines +140 to +148
function longestTextOverlap(left: string, right: string): number {
const maxLength = Math.min(left.length, right.length);
for (let length = maxLength; length > 0; length--) {
if (left.endsWith(right.slice(0, length))) {
return length;
}
}
return 0;
}
Comment on lines +161 to +177
test("mergeMessages does not stitch unrelated live AI replacement text while streaming", () => {
const persistedAi = {
id: "ai-1",
type: "ai",
content: "old answer",
} as Message;
const liveReplacementAi = {
id: "ai-1",
type: "ai",
content: "new answer",
response_metadata: { model: "test-model" },
} as Message;

expect(mergeMessages([persistedAi], [liveReplacementAi], [], true)).toEqual([
liveReplacementAi,
]);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:frontend Next.js frontend under frontend/

Projects

None yet

Development

Successfully merging this pull request may close these issues.

流式输出过程中切换页面再返回,已输出的内容丢失,只显示后续新内容

2 participants