fix: preserve blank lines for newline chunking (#1645) (thanks @tyler6204)

This commit is contained in:
Peter Steinberger 2026-01-25 00:46:56 +00:00
parent b9edeae961
commit 354d77b33f
4 changed files with 32 additions and 10 deletions

View File

@ -14,6 +14,7 @@ Docs: https://docs.clawd.bot
- Docs: add Bedrock EC2 instance role setup + IAM steps. (#1625) Thanks @sergical. https://docs.clawd.bot/bedrock
- Exec approvals: forward approval prompts to chat with `/approve` for all channels (including plugins). (#1621) Thanks @czekaj. https://docs.clawd.bot/tools/exec-approvals https://docs.clawd.bot/tools/slash-commands
- Gateway: expose config.patch in the gateway tool with safe partial updates + restart sentinel. (#1653) Thanks @Glucksberg.
- BlueBubbles: add newline chunking mode option for streaming. (#1645) Thanks @tyler6204.
### Fixes
- BlueBubbles: keep part-index GUIDs in reply tags when short IDs are missing.

View File

@ -38,7 +38,7 @@ export type BlueBubblesAccountConfig = {
dms?: Record<string, unknown>;
/** Outbound text chunk size (chars). Default: 4000. */
textChunkLimit?: number;
/** Chunking mode: "newline" (default) splits on every newline; "length" splits by size. */
/** Chunking mode: "length" (default) splits by size; "newline" splits on every newline. */
chunkMode?: "length" | "newline";
blockStreaming?: boolean;
/** Merge streamed block replies before sending. */

View File

@ -246,10 +246,10 @@ describe("chunkByNewline", () => {
expect(chunks).toEqual(["Line one", "Line two", "Line three"]);
});
it("filters empty lines", () => {
it("preserves blank lines by folding into the next chunk", () => {
const text = "Line one\n\n\nLine two\n\nLine three";
const chunks = chunkByNewline(text, 1000);
expect(chunks).toEqual(["Line one", "Line two", "Line three"]);
expect(chunks).toEqual(["Line one", "\n\nLine two", "\nLine three"]);
});
it("trims whitespace from lines", () => {
@ -258,6 +258,12 @@ describe("chunkByNewline", () => {
expect(chunks).toEqual(["Line one", "Line two"]);
});
it("preserves leading blank lines on the first chunk", () => {
const text = "\n\nLine one\nLine two";
const chunks = chunkByNewline(text, 1000);
expect(chunks).toEqual(["\n\nLine one", "Line two"]);
});
it("falls back to length-based for long lines", () => {
const text = "Short line\n" + "a".repeat(50) + "\nAnother short";
const chunks = chunkByNewline(text, 20);

View File

@ -116,19 +116,34 @@ export function resolveChunkMode(
*/
export function chunkByNewline(text: string, maxLineLength: number): string[] {
if (!text) return [];
if (maxLineLength <= 0) return text.trim() ? [text] : [];
const lines = text.split("\n");
const chunks: string[] = [];
let pendingBlankLines = 0;
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed) continue; // skip empty lines
if (!trimmed) {
pendingBlankLines += 1;
continue;
}
if (trimmed.length <= maxLineLength) {
chunks.push(trimmed);
} else {
// Long line: fall back to length-based chunking
const subChunks = chunkText(trimmed, maxLineLength);
chunks.push(...subChunks);
const maxPrefix = Math.max(0, maxLineLength - 1);
const cappedBlankLines = pendingBlankLines > 0 ? Math.min(pendingBlankLines, maxPrefix) : 0;
const prefix = cappedBlankLines > 0 ? "\n".repeat(cappedBlankLines) : "";
pendingBlankLines = 0;
if (trimmed.length + prefix.length <= maxLineLength) {
chunks.push(prefix + trimmed);
continue;
}
const firstLimit = Math.max(1, maxLineLength - prefix.length);
const first = trimmed.slice(0, firstLimit);
chunks.push(prefix + first);
const remaining = trimmed.slice(firstLimit);
if (remaining) {
chunks.push(...chunkText(remaining, maxLineLength));
}
}