mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-07 20:21:23 +00:00
fix: flush block streaming on paragraph boundaries for chunkMode=newline (#7014)
* feat: Implement paragraph boundary flushing in block streaming - Added `flushOnParagraph` option to `BlockReplyChunking` for immediate flushing on paragraph breaks. - Updated `EmbeddedBlockChunker` to handle paragraph boundaries during chunking. - Enhanced `createBlockReplyCoalescer` to support flushing on enqueue. - Added tests to verify behavior of flushing with and without `flushOnEnqueue` set. - Updated relevant types and interfaces to include `flushOnParagraph` and `flushOnEnqueue` options. * fix: Improve streaming behavior and enhance block chunking logic - Resolved issue with stuck typing indicator after streamed BlueBubbles replies. - Refactored `EmbeddedBlockChunker` to streamline fence-split handling and ensure maxChars fallback for newline chunking. - Added tests to validate new chunking behavior, including handling of paragraph breaks and fence scenarios. - Updated changelog to reflect these changes. * test: Add test for clamping long paragraphs in EmbeddedBlockChunker - Introduced a new test case to verify that long paragraphs are correctly clamped to maxChars when flushOnParagraph is enabled. - Updated logic in EmbeddedBlockChunker to handle cases where the next paragraph break exceeds maxChars, ensuring proper chunking behavior. * refactor: streamline logging and improve error handling in message processing - Removed verbose logging statements from the `processMessage` function to reduce clutter. - Enhanced error handling by using `runtime.error` for typing restart failures. - Updated the `applySystemPromptOverrideToSession` function to accept a string directly instead of a function, simplifying the prompt application process. - Adjusted the `runEmbeddedAttempt` function to directly use the system prompt override without invoking it as a function.
This commit is contained in:
@@ -71,6 +71,81 @@ describe("block reply coalescer", () => {
|
||||
coalescer.stop();
|
||||
});
|
||||
|
||||
it("flushes each enqueued payload separately when flushOnEnqueue is set", async () => {
|
||||
const flushes: string[] = [];
|
||||
const coalescer = createBlockReplyCoalescer({
|
||||
config: { minChars: 1, maxChars: 200, idleMs: 100, joiner: "\n\n", flushOnEnqueue: true },
|
||||
shouldAbort: () => false,
|
||||
onFlush: (payload) => {
|
||||
flushes.push(payload.text ?? "");
|
||||
},
|
||||
});
|
||||
|
||||
coalescer.enqueue({ text: "First paragraph" });
|
||||
coalescer.enqueue({ text: "Second paragraph" });
|
||||
coalescer.enqueue({ text: "Third paragraph" });
|
||||
|
||||
await Promise.resolve();
|
||||
expect(flushes).toEqual(["First paragraph", "Second paragraph", "Third paragraph"]);
|
||||
coalescer.stop();
|
||||
});
|
||||
|
||||
it("still accumulates when flushOnEnqueue is not set (default)", async () => {
|
||||
vi.useFakeTimers();
|
||||
const flushes: string[] = [];
|
||||
const coalescer = createBlockReplyCoalescer({
|
||||
config: { minChars: 1, maxChars: 2000, idleMs: 100, joiner: "\n\n" },
|
||||
shouldAbort: () => false,
|
||||
onFlush: (payload) => {
|
||||
flushes.push(payload.text ?? "");
|
||||
},
|
||||
});
|
||||
|
||||
coalescer.enqueue({ text: "First paragraph" });
|
||||
coalescer.enqueue({ text: "Second paragraph" });
|
||||
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
expect(flushes).toEqual(["First paragraph\n\nSecond paragraph"]);
|
||||
coalescer.stop();
|
||||
});
|
||||
|
||||
it("flushes short payloads immediately when flushOnEnqueue is set", async () => {
|
||||
const flushes: string[] = [];
|
||||
const coalescer = createBlockReplyCoalescer({
|
||||
config: { minChars: 10, maxChars: 200, idleMs: 50, joiner: "\n\n", flushOnEnqueue: true },
|
||||
shouldAbort: () => false,
|
||||
onFlush: (payload) => {
|
||||
flushes.push(payload.text ?? "");
|
||||
},
|
||||
});
|
||||
|
||||
coalescer.enqueue({ text: "Hi" });
|
||||
await Promise.resolve();
|
||||
expect(flushes).toEqual(["Hi"]);
|
||||
coalescer.stop();
|
||||
});
|
||||
|
||||
it("resets char budget per paragraph with flushOnEnqueue", async () => {
|
||||
const flushes: string[] = [];
|
||||
const coalescer = createBlockReplyCoalescer({
|
||||
config: { minChars: 1, maxChars: 30, idleMs: 100, joiner: "\n\n", flushOnEnqueue: true },
|
||||
shouldAbort: () => false,
|
||||
onFlush: (payload) => {
|
||||
flushes.push(payload.text ?? "");
|
||||
},
|
||||
});
|
||||
|
||||
// Each 20-char payload fits within maxChars=30 individually
|
||||
coalescer.enqueue({ text: "12345678901234567890" });
|
||||
coalescer.enqueue({ text: "abcdefghijklmnopqrst" });
|
||||
|
||||
await Promise.resolve();
|
||||
// Without flushOnEnqueue, these would be joined to 40+ chars and trigger maxChars split.
|
||||
// With flushOnEnqueue, each is sent independently within budget.
|
||||
expect(flushes).toEqual(["12345678901234567890", "abcdefghijklmnopqrst"]);
|
||||
coalescer.stop();
|
||||
});
|
||||
|
||||
it("flushes buffered text before media payloads", () => {
|
||||
const flushes: Array<{ text?: string; mediaUrls?: string[] }> = [];
|
||||
const coalescer = createBlockReplyCoalescer({
|
||||
|
||||
Reference in New Issue
Block a user