fix: paragraph-aware newline chunking (#1726)

Thanks @tyler6204

Co-authored-by: Tyler Yust <64381258+tyler6204@users.noreply.github.com>
This commit is contained in:
Peter Steinberger
2026-01-25 13:24:00 +00:00
parent c3f5b4c416
commit 0130ecd800
17 changed files with 39 additions and 24 deletions

View File

@@ -53,6 +53,7 @@ Docs: https://docs.clawd.bot
- Media understanding: skip image understanding when the primary model already supports vision. (#1747) Thanks @tyler6204. - Media understanding: skip image understanding when the primary model already supports vision. (#1747) Thanks @tyler6204.
- Models: default missing custom provider fields so minimal configs are accepted. - Models: default missing custom provider fields so minimal configs are accepted.
- Messaging: keep newline chunking safe for fenced markdown blocks across channels. - Messaging: keep newline chunking safe for fenced markdown blocks across channels.
- Messaging: treat newline chunking as paragraph-aware (blank-line splits) to keep lists and headings together. (#1726) Thanks @tyler6204.
- TUI: reload history after gateway reconnect to restore session state. (#1663) - TUI: reload history after gateway reconnect to restore session state. (#1663)
- Heartbeat: normalize target identifiers for consistent routing. - Heartbeat: normalize target identifiers for consistent routing.
- Exec: keep approvals for elevated ask unless full mode. (#1616) Thanks @ivancasco. - Exec: keep approvals for elevated ask unless full mode. (#1616) Thanks @ivancasco.

View File

@@ -196,7 +196,7 @@ Provider options:
- `channels.bluebubbles.sendReadReceipts`: Send read receipts (default: `true`). - `channels.bluebubbles.sendReadReceipts`: Send read receipts (default: `true`).
- `channels.bluebubbles.blockStreaming`: Enable block streaming (default: `true`). - `channels.bluebubbles.blockStreaming`: Enable block streaming (default: `true`).
- `channels.bluebubbles.textChunkLimit`: Outbound chunk size in chars (default: 4000). - `channels.bluebubbles.textChunkLimit`: Outbound chunk size in chars (default: 4000).
- `channels.bluebubbles.chunkMode`: `length` (default) splits only when exceeding `textChunkLimit`; `newline` splits on every newline and sends each line immediately during streaming. - `channels.bluebubbles.chunkMode`: `length` (default) splits only when exceeding `textChunkLimit`; `newline` splits on blank lines (paragraph boundaries) before length chunking.
- `channels.bluebubbles.mediaMaxMb`: Inbound media cap in MB (default: 8). - `channels.bluebubbles.mediaMaxMb`: Inbound media cap in MB (default: 8).
- `channels.bluebubbles.historyLimit`: Max group messages for context (0 disables). - `channels.bluebubbles.historyLimit`: Max group messages for context (0 disables).
- `channels.bluebubbles.dmHistoryLimit`: DM history limit. - `channels.bluebubbles.dmHistoryLimit`: DM history limit.

View File

@@ -205,7 +205,7 @@ Notes:
## Capabilities & limits ## Capabilities & limits
- DMs and guild text channels (threads are treated as separate channels; voice not supported). - DMs and guild text channels (threads are treated as separate channels; voice not supported).
- Typing indicators sent best-effort; message chunking uses `channels.discord.textChunkLimit` (default 2000) and splits tall replies by line count (`channels.discord.maxLinesPerMessage`, default 17). - Typing indicators sent best-effort; message chunking uses `channels.discord.textChunkLimit` (default 2000) and splits tall replies by line count (`channels.discord.maxLinesPerMessage`, default 17).
- Optional newline chunking: set `channels.discord.chunkMode="newline"` to split on each line before length chunking. - Optional newline chunking: set `channels.discord.chunkMode="newline"` to split on blank lines (paragraph boundaries) before length chunking.
- File uploads supported up to the configured `channels.discord.mediaMaxMb` (default 8 MB). - File uploads supported up to the configured `channels.discord.mediaMaxMb` (default 8 MB).
- Mention-gated guild replies by default to avoid noisy bots. - Mention-gated guild replies by default to avoid noisy bots.
- Reply context is injected when a message references another message (quoted content + ids). - Reply context is injected when a message references another message (quoted content + ids).
@@ -307,7 +307,7 @@ ack reaction after the bot replies.
- `guilds.<id>.requireMention`: per-guild mention requirement (overridable per channel). - `guilds.<id>.requireMention`: per-guild mention requirement (overridable per channel).
- `guilds.<id>.reactionNotifications`: reaction system event mode (`off`, `own`, `all`, `allowlist`). - `guilds.<id>.reactionNotifications`: reaction system event mode (`off`, `own`, `all`, `allowlist`).
- `textChunkLimit`: outbound text chunk size (chars). Default: 2000. - `textChunkLimit`: outbound text chunk size (chars). Default: 2000.
- `chunkMode`: `length` (default) splits only when exceeding `textChunkLimit`; `newline` splits on every newline before length chunking. - `chunkMode`: `length` (default) splits only when exceeding `textChunkLimit`; `newline` splits on blank lines (paragraph boundaries) before length chunking.
- `maxLinesPerMessage`: soft max line count per message. Default: 17. - `maxLinesPerMessage`: soft max line count per message. Default: 17.
- `mediaMaxMb`: clamp inbound media saved to disk. - `mediaMaxMb`: clamp inbound media saved to disk.
- `historyLimit`: number of recent guild messages to include as context when replying to a mention (default 20; falls back to `messages.groupChat.historyLimit`; `0` disables). - `historyLimit`: number of recent guild messages to include as context when replying to a mention (default 20; falls back to `messages.groupChat.historyLimit`; `0` disables).

View File

@@ -219,7 +219,7 @@ This is useful when you want an isolated personality/model for a specific thread
## Limits ## Limits
- Outbound text is chunked to `channels.imessage.textChunkLimit` (default 4000). - Outbound text is chunked to `channels.imessage.textChunkLimit` (default 4000).
- Optional newline chunking: set `channels.imessage.chunkMode="newline"` to split on each line before length chunking. - Optional newline chunking: set `channels.imessage.chunkMode="newline"` to split on blank lines (paragraph boundaries) before length chunking.
- Media uploads are capped by `channels.imessage.mediaMaxMb` (default 16). - Media uploads are capped by `channels.imessage.mediaMaxMb` (default 16).
## Addressing / delivery targets ## Addressing / delivery targets
@@ -254,7 +254,7 @@ Provider options:
- `channels.imessage.includeAttachments`: ingest attachments into context. - `channels.imessage.includeAttachments`: ingest attachments into context.
- `channels.imessage.mediaMaxMb`: inbound/outbound media cap (MB). - `channels.imessage.mediaMaxMb`: inbound/outbound media cap (MB).
- `channels.imessage.textChunkLimit`: outbound chunk size (chars). - `channels.imessage.textChunkLimit`: outbound chunk size (chars).
- `channels.imessage.chunkMode`: `length` (default) or `newline` to split on newlines before length chunking. - `channels.imessage.chunkMode`: `length` (default) or `newline` to split on blank lines (paragraph boundaries) before length chunking.
Related global options: Related global options:
- `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`). - `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`).

View File

@@ -215,7 +215,7 @@ Provider options:
- `channels.matrix.initialSyncLimit`: initial sync limit. - `channels.matrix.initialSyncLimit`: initial sync limit.
- `channels.matrix.threadReplies`: `off | inbound | always` (default: inbound). - `channels.matrix.threadReplies`: `off | inbound | always` (default: inbound).
- `channels.matrix.textChunkLimit`: outbound text chunk size (chars). - `channels.matrix.textChunkLimit`: outbound text chunk size (chars).
- `channels.matrix.chunkMode`: `length` (default) or `newline` to split on newlines before length chunking. - `channels.matrix.chunkMode`: `length` (default) or `newline` to split on blank lines (paragraph boundaries) before length chunking.
- `channels.matrix.dm.policy`: `pairing | allowlist | open | disabled` (default: pairing). - `channels.matrix.dm.policy`: `pairing | allowlist | open | disabled` (default: pairing).
- `channels.matrix.dm.allowFrom`: DM allowlist (user IDs or display names). `open` requires `"*"`. The wizard resolves names to IDs when possible. - `channels.matrix.dm.allowFrom`: DM allowlist (user IDs or display names). `open` requires `"*"`. The wizard resolves names to IDs when possible.
- `channels.matrix.groupPolicy`: `allowlist | open | disabled` (default: allowlist). - `channels.matrix.groupPolicy`: `allowlist | open | disabled` (default: allowlist).

View File

@@ -415,7 +415,7 @@ Key settings (see `/gateway/configuration` for shared channel patterns):
- `channels.msteams.dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing) - `channels.msteams.dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing)
- `channels.msteams.allowFrom`: allowlist for DMs (AAD object IDs, UPNs, or display names). The wizard resolves names to IDs during setup when Graph access is available. - `channels.msteams.allowFrom`: allowlist for DMs (AAD object IDs, UPNs, or display names). The wizard resolves names to IDs during setup when Graph access is available.
- `channels.msteams.textChunkLimit`: outbound text chunk size. - `channels.msteams.textChunkLimit`: outbound text chunk size.
- `channels.msteams.chunkMode`: `length` (default) or `newline` to split on newlines before length chunking. - `channels.msteams.chunkMode`: `length` (default) or `newline` to split on blank lines (paragraph boundaries) before length chunking.
- `channels.msteams.mediaAllowHosts`: allowlist for inbound attachment hosts (defaults to Microsoft/Teams domains). - `channels.msteams.mediaAllowHosts`: allowlist for inbound attachment hosts (defaults to Microsoft/Teams domains).
- `channels.msteams.requireMention`: require @mention in channels/groups (default true). - `channels.msteams.requireMention`: require @mention in channels/groups (default true).
- `channels.msteams.replyStyle`: `thread | top-level` (see [Reply Style](#reply-style-threads-vs-posts)). - `channels.msteams.replyStyle`: `thread | top-level` (see [Reply Style](#reply-style-threads-vs-posts)).

View File

@@ -114,7 +114,7 @@ Provider options:
- `channels.nextcloud-talk.dmHistoryLimit`: DM history limit (0 disables). - `channels.nextcloud-talk.dmHistoryLimit`: DM history limit (0 disables).
- `channels.nextcloud-talk.dms`: per-DM overrides (historyLimit). - `channels.nextcloud-talk.dms`: per-DM overrides (historyLimit).
- `channels.nextcloud-talk.textChunkLimit`: outbound text chunk size (chars). - `channels.nextcloud-talk.textChunkLimit`: outbound text chunk size (chars).
- `channels.nextcloud-talk.chunkMode`: `length` (default) or `newline` to split on newlines before length chunking. - `channels.nextcloud-talk.chunkMode`: `length` (default) or `newline` to split on blank lines (paragraph boundaries) before length chunking.
- `channels.nextcloud-talk.blockStreaming`: disable block streaming for this channel. - `channels.nextcloud-talk.blockStreaming`: disable block streaming for this channel.
- `channels.nextcloud-talk.blockStreamingCoalesce`: block streaming coalesce tuning. - `channels.nextcloud-talk.blockStreamingCoalesce`: block streaming coalesce tuning.
- `channels.nextcloud-talk.mediaMaxMb`: inbound media cap (MB). - `channels.nextcloud-talk.mediaMaxMb`: inbound media cap (MB).

View File

@@ -111,7 +111,7 @@ Groups:
## Media + limits ## Media + limits
- Outbound text is chunked to `channels.signal.textChunkLimit` (default 4000). - Outbound text is chunked to `channels.signal.textChunkLimit` (default 4000).
- Optional newline chunking: set `channels.signal.chunkMode="newline"` to split on each line before length chunking. - Optional newline chunking: set `channels.signal.chunkMode="newline"` to split on blank lines (paragraph boundaries) before length chunking.
- Attachments supported (base64 fetched from `signal-cli`). - Attachments supported (base64 fetched from `signal-cli`).
- Default media cap: `channels.signal.mediaMaxMb` (default 8). - Default media cap: `channels.signal.mediaMaxMb` (default 8).
- Use `channels.signal.ignoreAttachments` to skip downloading media. - Use `channels.signal.ignoreAttachments` to skip downloading media.
@@ -170,7 +170,7 @@ Provider options:
- `channels.signal.historyLimit`: max group messages to include as context (0 disables). - `channels.signal.historyLimit`: max group messages to include as context (0 disables).
- `channels.signal.dmHistoryLimit`: DM history limit in user turns. Per-user overrides: `channels.signal.dms["<phone_or_uuid>"].historyLimit`. - `channels.signal.dmHistoryLimit`: DM history limit in user turns. Per-user overrides: `channels.signal.dms["<phone_or_uuid>"].historyLimit`.
- `channels.signal.textChunkLimit`: outbound chunk size (chars). - `channels.signal.textChunkLimit`: outbound chunk size (chars).
- `channels.signal.chunkMode`: `length` (default) or `newline` to split on newlines before length chunking. - `channels.signal.chunkMode`: `length` (default) or `newline` to split on blank lines (paragraph boundaries) before length chunking.
- `channels.signal.mediaMaxMb`: inbound/outbound media cap (MB). - `channels.signal.mediaMaxMb`: inbound/outbound media cap (MB).
Related global options: Related global options:

View File

@@ -349,7 +349,7 @@ ack reaction after the bot replies.
## Limits ## Limits
- Outbound text is chunked to `channels.slack.textChunkLimit` (default 4000). - Outbound text is chunked to `channels.slack.textChunkLimit` (default 4000).
- Optional newline chunking: set `channels.slack.chunkMode="newline"` to split on each line before length chunking. - Optional newline chunking: set `channels.slack.chunkMode="newline"` to split on blank lines (paragraph boundaries) before length chunking.
- Media uploads are capped by `channels.slack.mediaMaxMb` (default 20). - Media uploads are capped by `channels.slack.mediaMaxMb` (default 20).
## Reply threading ## Reply threading

View File

@@ -135,7 +135,7 @@ Notes:
## Limits ## Limits
- Outbound text is chunked to `channels.telegram.textChunkLimit` (default 4000). - Outbound text is chunked to `channels.telegram.textChunkLimit` (default 4000).
- Optional newline chunking: set `channels.telegram.chunkMode="newline"` to split on each line before length chunking. - Optional newline chunking: set `channels.telegram.chunkMode="newline"` to split on blank lines (paragraph boundaries) before length chunking.
- Media downloads/uploads are capped by `channels.telegram.mediaMaxMb` (default 5). - Media downloads/uploads are capped by `channels.telegram.mediaMaxMb` (default 5).
- Telegram Bot API requests time out after `channels.telegram.timeoutSeconds` (default 500 via grammY). Set lower to avoid long hangs. - Telegram Bot API requests time out after `channels.telegram.timeoutSeconds` (default 500 via grammY). Set lower to avoid long hangs.
- Group history context uses `channels.telegram.historyLimit` (or `channels.telegram.accounts.*.historyLimit`), falling back to `messages.groupChat.historyLimit`. Set `0` to disable (default 50). - Group history context uses `channels.telegram.historyLimit` (or `channels.telegram.accounts.*.historyLimit`), falling back to `messages.groupChat.historyLimit`. Set `0` to disable (default 50).
@@ -524,7 +524,7 @@ Provider options:
- `channels.telegram.accounts.<account>.capabilities.inlineButtons`: per-account override. - `channels.telegram.accounts.<account>.capabilities.inlineButtons`: per-account override.
- `channels.telegram.replyToMode`: `off | first | all` (default: `first`). - `channels.telegram.replyToMode`: `off | first | all` (default: `first`).
- `channels.telegram.textChunkLimit`: outbound chunk size (chars). - `channels.telegram.textChunkLimit`: outbound chunk size (chars).
- `channels.telegram.chunkMode`: `length` (default) or `newline` to split on newlines before length chunking. - `channels.telegram.chunkMode`: `length` (default) or `newline` to split on blank lines (paragraph boundaries) before length chunking.
- `channels.telegram.linkPreview`: toggle link previews for outbound messages (default: true). - `channels.telegram.linkPreview`: toggle link previews for outbound messages (default: true).
- `channels.telegram.streamMode`: `off | partial | block` (draft streaming). - `channels.telegram.streamMode`: `off | partial | block` (draft streaming).
- `channels.telegram.mediaMaxMb`: inbound/outbound media cap (MB). - `channels.telegram.mediaMaxMb`: inbound/outbound media cap (MB).

View File

@@ -271,7 +271,7 @@ WhatsApp can automatically send emoji reactions to incoming messages immediately
## Limits ## Limits
- Outbound text is chunked to `channels.whatsapp.textChunkLimit` (default 4000). - Outbound text is chunked to `channels.whatsapp.textChunkLimit` (default 4000).
- Optional newline chunking: set `channels.whatsapp.chunkMode="newline"` to split on each line before length chunking. - Optional newline chunking: set `channels.whatsapp.chunkMode="newline"` to split on blank lines (paragraph boundaries) before length chunking.
- Inbound media saves are capped by `channels.whatsapp.mediaMaxMb` (default 50 MB). - Inbound media saves are capped by `channels.whatsapp.mediaMaxMb` (default 50 MB).
- Outbound media items are capped by `agents.defaults.mediaMaxMb` (default 5 MB). - Outbound media items are capped by `agents.defaults.mediaMaxMb` (default 5 MB).

View File

@@ -38,7 +38,7 @@ Legend:
- `agents.defaults.blockStreamingChunk`: `{ minChars, maxChars, breakPreference? }`. - `agents.defaults.blockStreamingChunk`: `{ minChars, maxChars, breakPreference? }`.
- `agents.defaults.blockStreamingCoalesce`: `{ minChars?, maxChars?, idleMs? }` (merge streamed blocks before send). - `agents.defaults.blockStreamingCoalesce`: `{ minChars?, maxChars?, idleMs? }` (merge streamed blocks before send).
- Channel hard cap: `*.textChunkLimit` (e.g., `channels.whatsapp.textChunkLimit`). - Channel hard cap: `*.textChunkLimit` (e.g., `channels.whatsapp.textChunkLimit`).
- Channel chunk mode: `*.chunkMode` (`length` default, `newline` splits on each line before length chunking). - Channel chunk mode: `*.chunkMode` (`length` default, `newline` splits on blank lines (paragraph boundaries) before length chunking).
- Discord soft cap: `channels.discord.maxLinesPerMessage` (default 17) splits tall replies to avoid UI clipping. - Discord soft cap: `channels.discord.maxLinesPerMessage` (default 17) splits tall replies to avoid UI clipping.
**Boundary semantics:** **Boundary semantics:**

View File

@@ -1131,7 +1131,7 @@ Reaction notification modes:
- `own`: reactions on the bot's own messages (default). - `own`: reactions on the bot's own messages (default).
- `all`: all reactions on all messages. - `all`: all reactions on all messages.
- `allowlist`: reactions from `guilds.<id>.users` on all messages (empty list disables). - `allowlist`: reactions from `guilds.<id>.users` on all messages (empty list disables).
Outbound text is chunked by `channels.discord.textChunkLimit` (default 2000). Set `channels.discord.chunkMode="newline"` to split on line boundaries before length chunking. Discord clients can clip very tall messages, so `channels.discord.maxLinesPerMessage` (default 17) splits long multi-line replies even when under 2000 chars. Outbound text is chunked by `channels.discord.textChunkLimit` (default 2000). Set `channels.discord.chunkMode="newline"` to split on blank lines (paragraph boundaries) before length chunking. Discord clients can clip very tall messages, so `channels.discord.maxLinesPerMessage` (default 17) splits long multi-line replies even when under 2000 chars.
Retry policy defaults and behavior are documented in [Retry policy](/concepts/retry). Retry policy defaults and behavior are documented in [Retry policy](/concepts/retry).
### `channels.googlechat` (Chat API webhook) ### `channels.googlechat` (Chat API webhook)

View File

@@ -344,6 +344,11 @@ describe("chunkMarkdownTextWithMode", () => {
expect(chunkMarkdownTextWithMode(text, 1000, "newline")).toEqual([text]); expect(chunkMarkdownTextWithMode(text, 1000, "newline")).toEqual([text]);
}); });
it("defers long markdown paragraphs to markdown chunking in newline mode", () => {
const text = `\`\`\`js\n${"const a = 1;\n".repeat(20)}\`\`\``;
expect(chunkMarkdownTextWithMode(text, 40, "newline")).toEqual(chunkMarkdownText(text, 40));
});
it("does not split on blank lines inside a fenced code block", () => { it("does not split on blank lines inside a fenced code block", () => {
const text = "```python\ndef my_function():\n x = 1\n\n y = 2\n return x + y\n```"; const text = "```python\ndef my_function():\n x = 1\n\n y = 2\n return x + y\n```";
expect(chunkMarkdownTextWithMode(text, 1000, "newline")).toEqual([text]); expect(chunkMarkdownTextWithMode(text, 1000, "newline")).toEqual([text]);

View File

@@ -173,10 +173,16 @@ export function chunkByNewline(
* - Only breaks at paragraph separators ("\n\n" or more, allowing whitespace on blank lines) * - Only breaks at paragraph separators ("\n\n" or more, allowing whitespace on blank lines)
* - Packs multiple paragraphs into a single chunk up to `limit` * - Packs multiple paragraphs into a single chunk up to `limit`
* - Falls back to length-based splitting when a single paragraph exceeds `limit` * - Falls back to length-based splitting when a single paragraph exceeds `limit`
* (unless `splitLongParagraphs` is disabled)
*/ */
export function chunkByParagraph(text: string, limit: number): string[] { export function chunkByParagraph(
text: string,
limit: number,
opts?: { splitLongParagraphs?: boolean },
): string[] {
if (!text) return []; if (!text) return [];
if (limit <= 0) return [text]; if (limit <= 0) return [text];
const splitLongParagraphs = opts?.splitLongParagraphs !== false;
// Normalize to \n so blank line detection is consistent. // Normalize to \n so blank line detection is consistent.
const normalized = text.replace(/\r\n?/g, "\n"); const normalized = text.replace(/\r\n?/g, "\n");
@@ -186,7 +192,9 @@ export function chunkByParagraph(text: string, limit: number): string[] {
// boundaries, not only exceeding a length limit.) // boundaries, not only exceeding a length limit.)
const paragraphRe = /\n[\t ]*\n+/; const paragraphRe = /\n[\t ]*\n+/;
if (!paragraphRe.test(normalized)) { if (!paragraphRe.test(normalized)) {
return normalized.length <= limit ? [normalized] : chunkText(normalized, limit); if (normalized.length <= limit) return [normalized];
if (!splitLongParagraphs) return [normalized];
return chunkText(normalized, limit);
} }
const spans = parseFenceSpans(normalized); const spans = parseFenceSpans(normalized);
@@ -213,6 +221,8 @@ export function chunkByParagraph(text: string, limit: number): string[] {
if (!paragraph.trim()) continue; if (!paragraph.trim()) continue;
if (paragraph.length <= limit) { if (paragraph.length <= limit) {
chunks.push(paragraph); chunks.push(paragraph);
} else if (!splitLongParagraphs) {
chunks.push(paragraph);
} else { } else {
chunks.push(...chunkText(paragraph, limit)); chunks.push(...chunkText(paragraph, limit));
} }
@@ -235,7 +245,7 @@ export function chunkMarkdownTextWithMode(text: string, limit: number, mode: Chu
if (mode === "newline") { if (mode === "newline") {
// Paragraph chunking is fence-safe because we never split at arbitrary indices. // Paragraph chunking is fence-safe because we never split at arbitrary indices.
// If a paragraph must be split by length, defer to the markdown-aware chunker. // If a paragraph must be split by length, defer to the markdown-aware chunker.
const paragraphChunks = chunkByParagraph(text, limit); const paragraphChunks = chunkByParagraph(text, limit, { splitLongParagraphs: false });
const out: string[] = []; const out: string[] = [];
for (const chunk of paragraphChunks) { for (const chunk of paragraphChunks) {
const nested = chunkMarkdownText(chunk, limit); const nested = chunkMarkdownText(chunk, limit);

View File

@@ -58,7 +58,7 @@ describe("chunkDiscordText", () => {
maxLines: 50, maxLines: 50,
chunkMode: "newline", chunkMode: "newline",
}); });
expect(chunks).toEqual(["```js\nconst a = 1;\nconst b = 2;\n```", "After"]); expect(chunks).toEqual([text]);
}); });
it("reserves space for closing fences when chunking", () => { it("reserves space for closing fences when chunking", () => {

View File

@@ -192,7 +192,7 @@ describe("deliverOutboundPayloads", () => {
expect(sendWhatsApp).toHaveBeenNthCalledWith( expect(sendWhatsApp).toHaveBeenNthCalledWith(
2, 2,
"+1555", "+1555",
"\nLine two", "Line two",
expect.objectContaining({ verbose: false }), expect.objectContaining({ verbose: false }),
); );
}); });
@@ -241,9 +241,8 @@ describe("deliverOutboundPayloads", () => {
payloads: [{ text }], payloads: [{ text }],
}); });
expect(chunker).toHaveBeenCalledTimes(2); expect(chunker).toHaveBeenCalledTimes(1);
expect(chunker).toHaveBeenNthCalledWith(1, "```js\nconst a = 1;\nconst b = 2;\n```", 4000); expect(chunker).toHaveBeenNthCalledWith(1, text, 4000);
expect(chunker).toHaveBeenNthCalledWith(2, "After", 4000);
}); });
it("uses iMessage media maxBytes from agent fallback", async () => { it("uses iMessage media maxBytes from agent fallback", async () => {