mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 10:00:42 +00:00
fix(gateway): preserve repeated characters in chat stream merge (#72400)
* fix(gateway): preserve repeated characters in chat stream merge * fix(gateway): cap live chat stream buffers
This commit is contained in:
@@ -60,6 +60,7 @@ Docs: https://docs.openclaw.ai
|
||||
- WebChat/Control UI: support non-video file attachments in chat uploads while preserving the existing image attachment path and MIME-sniff fallback for generic image uploads. (#70947) Thanks @IAMSamuelRodda.
|
||||
- Skills/memory: restore Chokidar v5 hot reloads by watching concrete skill and memory roots with filters, including SKILL.md removals and deleted skill folders without broad workspace recursion. Fixes #27404, #33585, and #41606. Thanks @shelvenzhou, @08820048, and @rocke2020.
|
||||
- Gateway/chat: keep duplicate attachment-backed `chat.send` retries with the same idempotency key on the documented in-flight path so aborts still target the real active run. Fixes #70139. Thanks @Feelw00.
|
||||
- Gateway/chat: preserve repeated boundary characters while merging assistant chat stream deltas, including repeated digits, CJK characters, and markdown/table tokens. Fixes #63769; carries forward #63994 and #65457. Thanks @yon950905 and @mohuaxiao.
|
||||
- Plugins: share package entrypoint resolution between install and discovery, reject mismatched `runtimeExtensions`, and cache bundled runtime-dependency manifest reads during scans. Thanks @vincentkoc.
|
||||
- WhatsApp/Web: keep quiet but healthy linked-device sessions connected by basing the watchdog on WhatsApp Web transport activity, while retaining a longer app-silence cap so frame activity cannot mask a stuck session forever. Fixes #70678; carries forward the focused #71466 approach and keeps #63939 as related configurable-timeout follow-up. Thanks @vincentkoc and @oromeis.
|
||||
- Discord/gateway: count failed health-monitor restart attempts toward cooldown and hourly caps, and evict stale account lifecycle state during channel reloads so repeated Discord gateway recovery cannot loop on old status. Fixes #38596. (#40413) Thanks @jellyAI-dev and @vashquez.
|
||||
|
||||
@@ -13,23 +13,13 @@ import {
|
||||
|
||||
export { resolveAssistantEventPhase } from "../shared/chat-message-content.js";
|
||||
|
||||
function appendUniqueSuffix(base: string, suffix: string): string {
|
||||
if (!suffix) {
|
||||
return base;
|
||||
export const MAX_LIVE_CHAT_BUFFER_CHARS = 500_000;
|
||||
|
||||
function capLiveAssistantBuffer(text: string): string {
|
||||
if (text.length <= MAX_LIVE_CHAT_BUFFER_CHARS) {
|
||||
return text;
|
||||
}
|
||||
if (!base) {
|
||||
return suffix;
|
||||
}
|
||||
if (base.endsWith(suffix)) {
|
||||
return base;
|
||||
}
|
||||
const maxOverlap = Math.min(base.length, suffix.length);
|
||||
for (let overlap = maxOverlap; overlap > 0; overlap -= 1) {
|
||||
if (base.slice(-overlap) === suffix.slice(0, overlap)) {
|
||||
return base + suffix.slice(overlap);
|
||||
}
|
||||
}
|
||||
return base + suffix;
|
||||
return text.slice(-MAX_LIVE_CHAT_BUFFER_CHARS);
|
||||
}
|
||||
|
||||
export function resolveMergedAssistantText(params: {
|
||||
@@ -39,20 +29,20 @@ export function resolveMergedAssistantText(params: {
|
||||
}): string {
|
||||
const { previousText, nextText, nextDelta } = params;
|
||||
if (nextText && previousText) {
|
||||
if (nextText.startsWith(previousText)) {
|
||||
return nextText;
|
||||
if (nextText.startsWith(previousText) && nextText.length > previousText.length) {
|
||||
return capLiveAssistantBuffer(nextText);
|
||||
}
|
||||
if (previousText.startsWith(nextText) && !nextDelta) {
|
||||
return previousText;
|
||||
return capLiveAssistantBuffer(previousText);
|
||||
}
|
||||
}
|
||||
if (nextDelta) {
|
||||
return appendUniqueSuffix(previousText, nextDelta);
|
||||
return capLiveAssistantBuffer(previousText + nextDelta);
|
||||
}
|
||||
if (nextText) {
|
||||
return nextText;
|
||||
return capLiveAssistantBuffer(nextText);
|
||||
}
|
||||
return previousText;
|
||||
return capLiveAssistantBuffer(previousText);
|
||||
}
|
||||
|
||||
export function normalizeLiveAssistantEventText(params: { text: string; delta?: unknown }): {
|
||||
|
||||
73
src/gateway/server-chat.stream-text-merge.test.ts
Normal file
73
src/gateway/server-chat.stream-text-merge.test.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
MAX_LIVE_CHAT_BUFFER_CHARS,
|
||||
resolveMergedAssistantText,
|
||||
} from "./live-chat-projector.js";
|
||||
|
||||
describe("server chat stream text merge", () => {
|
||||
it.each([
|
||||
{
|
||||
name: "repeated digits",
|
||||
chunks: ["1", "1", "1"],
|
||||
expected: "111",
|
||||
},
|
||||
{
|
||||
name: "repeated CJK punctuation",
|
||||
chunks: ["。", "。", "。"],
|
||||
expected: "。。。",
|
||||
},
|
||||
{
|
||||
name: "repeated markdown emphasis tokens",
|
||||
chunks: ["**", "**"],
|
||||
expected: "****",
|
||||
},
|
||||
{
|
||||
name: "repeated markdown table separators",
|
||||
chunks: ["|", "|", "|"],
|
||||
expected: "|||",
|
||||
},
|
||||
])("appends incremental deltas without collapsing $name", ({ chunks, expected }) => {
|
||||
const merged = chunks.reduce(
|
||||
(previousText, nextDelta) =>
|
||||
resolveMergedAssistantText({
|
||||
previousText,
|
||||
nextText: nextDelta,
|
||||
nextDelta,
|
||||
}),
|
||||
"",
|
||||
);
|
||||
|
||||
expect(merged).toBe(expected);
|
||||
});
|
||||
|
||||
it("keeps cumulative snapshots from duplicating already-buffered text", () => {
|
||||
expect(
|
||||
resolveMergedAssistantText({
|
||||
previousText: "Hello",
|
||||
nextText: "Hello world",
|
||||
nextDelta: " world",
|
||||
}),
|
||||
).toBe("Hello world");
|
||||
});
|
||||
|
||||
it("keeps non-prefix incremental segments after tool calls", () => {
|
||||
expect(
|
||||
resolveMergedAssistantText({
|
||||
previousText: "Before tool call",
|
||||
nextText: "After tool call",
|
||||
nextDelta: "\nAfter tool call",
|
||||
}),
|
||||
).toBe("Before tool call\nAfter tool call");
|
||||
});
|
||||
|
||||
it("caps merged live text while preserving the newest assistant output", () => {
|
||||
const result = resolveMergedAssistantText({
|
||||
previousText: "a".repeat(MAX_LIVE_CHAT_BUFFER_CHARS - 2),
|
||||
nextText: "",
|
||||
nextDelta: "bbbb",
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(MAX_LIVE_CHAT_BUFFER_CHARS);
|
||||
expect(result.endsWith("bbbb")).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user