From 824154deba0c5f65c05089a7906eddf839f2c5c9 Mon Sep 17 00:00:00 2001 From: Tyler Yust Date: Sat, 24 Jan 2026 13:16:38 -0800 Subject: [PATCH] refactor: simplify chunk mode handling for BlueBubbles - Removed `chunkMode` configuration from various account schemas and types, centralizing chunk mode logic to BlueBubbles only. - Updated `processMessage` to default to "newline" for BlueBubbles chunking. - Adjusted tests to reflect changes in chunk mode handling for BlueBubbles, ensuring proper functionality. --- extensions/bluebubbles/src/monitor.ts | 6 +----- extensions/matrix/src/config-schema.ts | 1 - extensions/matrix/src/types.ts | 2 -- extensions/mattermost/src/config-schema.ts | 1 - extensions/mattermost/src/types.ts | 2 -- .../nextcloud-talk/src/config-schema.ts | 1 - extensions/nextcloud-talk/src/types.ts | 2 -- src/auto-reply/chunk.test.ts | 21 ++++++++++++------- src/auto-reply/chunk.ts | 2 ++ src/auto-reply/reply/block-streaming.ts | 4 ++-- src/config/types.discord.ts | 2 -- src/config/types.imessage.ts | 2 -- src/config/types.msteams.ts | 2 -- src/config/types.signal.ts | 2 -- src/config/types.slack.ts | 2 -- src/config/types.telegram.ts | 2 -- src/config/types.whatsapp.ts | 4 ---- src/config/zod-schema.providers-core.ts | 6 ------ src/config/zod-schema.providers-whatsapp.ts | 2 -- src/infra/outbound/deliver.ts | 13 ++---------- 20 files changed, 20 insertions(+), 59 deletions(-) diff --git a/extensions/bluebubbles/src/monitor.ts b/extensions/bluebubbles/src/monitor.ts index 09c8359be..1e85a2e5c 100644 --- a/extensions/bluebubbles/src/monitor.ts +++ b/extensions/bluebubbles/src/monitor.ts @@ -1851,11 +1851,7 @@ async function processMessage( account.config.textChunkLimit && account.config.textChunkLimit > 0 ? account.config.textChunkLimit : DEFAULT_TEXT_LIMIT; - const chunkMode = core.channel.text.resolveChunkMode( - config, - "bluebubbles", - account.accountId, - ); + const chunkMode = account.config.chunkMode ?? "newline"; const tableMode = core.channel.text.resolveMarkdownTableMode({ cfg: config, channel: "bluebubbles", diff --git a/extensions/matrix/src/config-schema.ts b/extensions/matrix/src/config-schema.ts index 62b327d40..b153ae40f 100644 --- a/extensions/matrix/src/config-schema.ts +++ b/extensions/matrix/src/config-schema.ts @@ -50,7 +50,6 @@ export const MatrixConfigSchema = z.object({ replyToMode: z.enum(["off", "first", "all"]).optional(), threadReplies: z.enum(["off", "inbound", "always"]).optional(), textChunkLimit: z.number().optional(), - chunkMode: z.enum(["length", "newline"]).optional(), mediaMaxMb: z.number().optional(), autoJoin: z.enum(["always", "allowlist", "off"]).optional(), autoJoinAllowlist: z.array(allowFromEntry).optional(), diff --git a/extensions/matrix/src/types.ts b/extensions/matrix/src/types.ts index 3848eb5ec..b7ff7facd 100644 --- a/extensions/matrix/src/types.ts +++ b/extensions/matrix/src/types.ts @@ -69,8 +69,6 @@ export type MatrixConfig = { threadReplies?: "off" | "inbound" | "always"; /** Outbound text chunk size (chars). Default: 4000. */ textChunkLimit?: number; - /** Chunking mode: "length" (default) splits by size, "newline" splits on every newline. */ - chunkMode?: "length" | "newline"; /** Max outbound media size in MB. */ mediaMaxMb?: number; /** Auto-join invites (always|allowlist|off). Default: always. */ diff --git a/extensions/mattermost/src/config-schema.ts b/extensions/mattermost/src/config-schema.ts index 2a1b76248..40ae8a31a 100644 --- a/extensions/mattermost/src/config-schema.ts +++ b/extensions/mattermost/src/config-schema.ts @@ -25,7 +25,6 @@ const MattermostAccountSchemaBase = z groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(), groupPolicy: GroupPolicySchema.optional().default("allowlist"), textChunkLimit: z.number().int().positive().optional(), - chunkMode: z.enum(["length", "newline"]).optional(), blockStreaming: z.boolean().optional(), blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(), }) diff --git a/extensions/mattermost/src/types.ts b/extensions/mattermost/src/types.ts index a51cb34a4..a80196142 100644 --- a/extensions/mattermost/src/types.ts +++ b/extensions/mattermost/src/types.ts @@ -36,8 +36,6 @@ export type MattermostAccountConfig = { groupPolicy?: GroupPolicy; /** Outbound text chunk size (chars). Default: 4000. */ textChunkLimit?: number; - /** Chunking mode: "length" (default) splits by size, "newline" splits on every newline. */ - chunkMode?: "length" | "newline"; /** Disable block streaming for this account. */ blockStreaming?: boolean; /** Merge streamed block replies before sending. */ diff --git a/extensions/nextcloud-talk/src/config-schema.ts b/extensions/nextcloud-talk/src/config-schema.ts index 8eb5fa27b..b047c7903 100644 --- a/extensions/nextcloud-talk/src/config-schema.ts +++ b/extensions/nextcloud-talk/src/config-schema.ts @@ -44,7 +44,6 @@ export const NextcloudTalkAccountSchemaBase = z dmHistoryLimit: z.number().int().min(0).optional(), dms: z.record(z.string(), DmConfigSchema.optional()).optional(), textChunkLimit: z.number().int().positive().optional(), - chunkMode: z.enum(["length", "newline"]).optional(), blockStreaming: z.boolean().optional(), blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(), mediaMaxMb: z.number().positive().optional(), diff --git a/extensions/nextcloud-talk/src/types.ts b/extensions/nextcloud-talk/src/types.ts index ea079bbd1..18525ccab 100644 --- a/extensions/nextcloud-talk/src/types.ts +++ b/extensions/nextcloud-talk/src/types.ts @@ -62,8 +62,6 @@ export type NextcloudTalkAccountConfig = { dms?: Record; /** Outbound text chunk size (chars). Default: 4000. */ textChunkLimit?: number; - /** Chunking mode: "length" (default) splits by size, "newline" splits on every newline. */ - chunkMode?: "length" | "newline"; /** Disable block streaming for this account. */ blockStreaming?: boolean; /** Merge streamed block replies before sending. */ diff --git a/src/auto-reply/chunk.test.ts b/src/auto-reply/chunk.test.ts index c9ec0df3a..0e0a25d6d 100644 --- a/src/auto-reply/chunk.test.ts +++ b/src/auto-reply/chunk.test.ts @@ -299,20 +299,20 @@ describe("resolveChunkMode", () => { }); it("returns length for internal channel", () => { - const cfg = { channels: { telegram: { chunkMode: "newline" as const } } }; + const cfg = { channels: { bluebubbles: { chunkMode: "newline" as const } } }; expect(resolveChunkMode(cfg, "__internal__")).toBe("length"); }); - it("supports provider-level overrides", () => { - const cfg = { channels: { telegram: { chunkMode: "newline" as const } } }; - expect(resolveChunkMode(cfg, "telegram")).toBe("newline"); + it("supports provider-level overrides for bluebubbles", () => { + const cfg = { channels: { bluebubbles: { chunkMode: "newline" as const } } }; + expect(resolveChunkMode(cfg, "bluebubbles")).toBe("newline"); expect(resolveChunkMode(cfg, "discord")).toBe("length"); }); - it("supports account-level overrides", () => { + it("supports account-level overrides for bluebubbles", () => { const cfg = { channels: { - telegram: { + bluebubbles: { chunkMode: "length" as const, accounts: { primary: { chunkMode: "newline" as const }, @@ -320,7 +320,12 @@ describe("resolveChunkMode", () => { }, }, }; - expect(resolveChunkMode(cfg, "telegram", "primary")).toBe("newline"); - expect(resolveChunkMode(cfg, "telegram", "other")).toBe("length"); + expect(resolveChunkMode(cfg, "bluebubbles", "primary")).toBe("newline"); + expect(resolveChunkMode(cfg, "bluebubbles", "other")).toBe("length"); + }); + + it("ignores chunkMode for non-bluebubbles providers", () => { + const cfg = { channels: { ["telegram" as string]: { chunkMode: "newline" as const } } }; + expect(resolveChunkMode(cfg, "telegram")).toBe("length"); }); }); diff --git a/src/auto-reply/chunk.ts b/src/auto-reply/chunk.ts index 054fa975b..281612e37 100644 --- a/src/auto-reply/chunk.ts +++ b/src/auto-reply/chunk.ts @@ -101,6 +101,8 @@ export function resolveChunkMode( accountId?: string | null, ): ChunkMode { if (!provider || provider === INTERNAL_MESSAGE_CHANNEL) return DEFAULT_CHUNK_MODE; + // Chunk mode is only supported for BlueBubbles. + if (provider !== "bluebubbles") return DEFAULT_CHUNK_MODE; const channelsConfig = cfg?.channels as Record | undefined; const providerConfig = (channelsConfig?.[provider] ?? (cfg as Record | undefined)?.[provider]) as ProviderChunkConfig | undefined; diff --git a/src/auto-reply/reply/block-streaming.ts b/src/auto-reply/reply/block-streaming.ts index 3a4abd564..fb462b107 100644 --- a/src/auto-reply/reply/block-streaming.ts +++ b/src/auto-reply/reply/block-streaming.ts @@ -69,7 +69,7 @@ export function resolveBlockStreamingChunking( }); const chunkCfg = cfg?.agents?.defaults?.blockStreamingChunk; - // Check if channel has chunkMode: "newline" - if so, use newline-based streaming + // BlueBubbles-only: if chunkMode is "newline", use newline-based streaming const channelChunkMode = resolveChunkMode(cfg, providerKey, accountId); if (channelChunkMode === "newline") { // For newline mode: use very low minChars to flush quickly on newlines @@ -103,7 +103,7 @@ export function resolveBlockStreamingCoalescing( ): BlockStreamingCoalescing | undefined { const providerKey = normalizeChunkProvider(provider); - // When chunkMode is "newline", disable coalescing entirely to send each line immediately + // BlueBubbles-only: when chunkMode is "newline", disable coalescing to send each line immediately const channelChunkMode = resolveChunkMode(cfg, providerKey, accountId); if (channelChunkMode === "newline") { return undefined; diff --git a/src/config/types.discord.ts b/src/config/types.discord.ts index 167b56197..ae434dd15 100644 --- a/src/config/types.discord.ts +++ b/src/config/types.discord.ts @@ -108,8 +108,6 @@ export type DiscordAccountConfig = { groupPolicy?: GroupPolicy; /** Outbound text chunk size (chars). Default: 2000. */ textChunkLimit?: number; - /** Chunking mode: "length" (default) splits by size, "newline" splits on every newline. */ - chunkMode?: "length" | "newline"; /** Disable block streaming for this account. */ blockStreaming?: boolean; /** Merge streamed block replies before sending. */ diff --git a/src/config/types.imessage.ts b/src/config/types.imessage.ts index 249d8baae..ca83c0fe0 100644 --- a/src/config/types.imessage.ts +++ b/src/config/types.imessage.ts @@ -54,8 +54,6 @@ export type IMessageAccountConfig = { mediaMaxMb?: number; /** Outbound text chunk size (chars). Default: 4000. */ textChunkLimit?: number; - /** Chunking mode: "length" (default) splits by size, "newline" splits on every newline. */ - chunkMode?: "length" | "newline"; blockStreaming?: boolean; /** Merge streamed block replies before sending. */ blockStreamingCoalesce?: BlockStreamingCoalesceConfig; diff --git a/src/config/types.msteams.ts b/src/config/types.msteams.ts index 956784b79..05e27527a 100644 --- a/src/config/types.msteams.ts +++ b/src/config/types.msteams.ts @@ -72,8 +72,6 @@ export type MSTeamsConfig = { groupPolicy?: GroupPolicy; /** Outbound text chunk size (chars). Default: 4000. */ textChunkLimit?: number; - /** Chunking mode: "length" (default) splits by size, "newline" splits on every newline. */ - chunkMode?: "length" | "newline"; /** Merge streamed block replies before sending. */ blockStreamingCoalesce?: BlockStreamingCoalesceConfig; /** diff --git a/src/config/types.signal.ts b/src/config/types.signal.ts index fcebf7273..94cb82f3d 100644 --- a/src/config/types.signal.ts +++ b/src/config/types.signal.ts @@ -56,8 +56,6 @@ export type SignalAccountConfig = { dms?: Record; /** Outbound text chunk size (chars). Default: 4000. */ textChunkLimit?: number; - /** Chunking mode: "length" (default) splits by size, "newline" splits on every newline. */ - chunkMode?: "length" | "newline"; blockStreaming?: boolean; /** Merge streamed block replies before sending. */ blockStreamingCoalesce?: BlockStreamingCoalesceConfig; diff --git a/src/config/types.slack.ts b/src/config/types.slack.ts index 557ad62f3..0662bf36f 100644 --- a/src/config/types.slack.ts +++ b/src/config/types.slack.ts @@ -116,8 +116,6 @@ export type SlackAccountConfig = { /** Per-DM config overrides keyed by user ID. */ dms?: Record; textChunkLimit?: number; - /** Chunking mode: "length" (default) splits by size, "newline" splits on every newline. */ - chunkMode?: "length" | "newline"; blockStreaming?: boolean; /** Merge streamed block replies before sending. */ blockStreamingCoalesce?: BlockStreamingCoalesceConfig; diff --git a/src/config/types.telegram.ts b/src/config/types.telegram.ts index 858eb5a56..1ef7e7387 100644 --- a/src/config/types.telegram.ts +++ b/src/config/types.telegram.ts @@ -80,8 +80,6 @@ export type TelegramAccountConfig = { dms?: Record; /** Outbound text chunk size (chars). Default: 4000. */ textChunkLimit?: number; - /** Chunking mode: "length" (default) splits by size, "newline" splits on every newline. */ - chunkMode?: "length" | "newline"; /** Disable block streaming for this account. */ blockStreaming?: boolean; /** Chunking config for draft streaming in `streamMode: "block"`. */ diff --git a/src/config/types.whatsapp.ts b/src/config/types.whatsapp.ts index b671ce86d..ce1851ea0 100644 --- a/src/config/types.whatsapp.ts +++ b/src/config/types.whatsapp.ts @@ -55,8 +55,6 @@ export type WhatsAppConfig = { dms?: Record; /** Outbound text chunk size (chars). Default: 4000. */ textChunkLimit?: number; - /** Chunking mode: "length" (default) splits by size, "newline" splits on every newline. */ - chunkMode?: "length" | "newline"; /** Maximum media file size in MB. Default: 50. */ mediaMaxMb?: number; /** Disable block streaming for this account. */ @@ -124,8 +122,6 @@ export type WhatsAppAccountConfig = { /** Per-DM config overrides keyed by user ID. */ dms?: Record; textChunkLimit?: number; - /** Chunking mode: "length" (default) splits by size, "newline" splits on every newline. */ - chunkMode?: "length" | "newline"; mediaMaxMb?: number; blockStreaming?: boolean; /** Merge streamed block replies before sending. */ diff --git a/src/config/zod-schema.providers-core.ts b/src/config/zod-schema.providers-core.ts index b6681e652..9d1eaa285 100644 --- a/src/config/zod-schema.providers-core.ts +++ b/src/config/zod-schema.providers-core.ts @@ -102,7 +102,6 @@ export const TelegramAccountSchemaBase = z dmHistoryLimit: z.number().int().min(0).optional(), dms: z.record(z.string(), DmConfigSchema.optional()).optional(), textChunkLimit: z.number().int().positive().optional(), - chunkMode: z.enum(["length", "newline"]).optional(), blockStreaming: z.boolean().optional(), draftChunk: BlockStreamingChunkSchema.optional(), blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(), @@ -213,7 +212,6 @@ export const DiscordAccountSchema = z dmHistoryLimit: z.number().int().min(0).optional(), dms: z.record(z.string(), DmConfigSchema.optional()).optional(), textChunkLimit: z.number().int().positive().optional(), - chunkMode: z.enum(["length", "newline"]).optional(), blockStreaming: z.boolean().optional(), blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(), maxLinesPerMessage: z.number().int().positive().optional(), @@ -403,7 +401,6 @@ export const SlackAccountSchema = z dmHistoryLimit: z.number().int().min(0).optional(), dms: z.record(z.string(), DmConfigSchema.optional()).optional(), textChunkLimit: z.number().int().positive().optional(), - chunkMode: z.enum(["length", "newline"]).optional(), blockStreaming: z.boolean().optional(), blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(), mediaMaxMb: z.number().positive().optional(), @@ -497,7 +494,6 @@ export const SignalAccountSchemaBase = z dmHistoryLimit: z.number().int().min(0).optional(), dms: z.record(z.string(), DmConfigSchema.optional()).optional(), textChunkLimit: z.number().int().positive().optional(), - chunkMode: z.enum(["length", "newline"]).optional(), blockStreaming: z.boolean().optional(), blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(), mediaMaxMb: z.number().int().positive().optional(), @@ -551,7 +547,6 @@ export const IMessageAccountSchemaBase = z includeAttachments: z.boolean().optional(), mediaMaxMb: z.number().int().positive().optional(), textChunkLimit: z.number().int().positive().optional(), - chunkMode: z.enum(["length", "newline"]).optional(), blockStreaming: z.boolean().optional(), blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(), groups: z @@ -710,7 +705,6 @@ export const MSTeamsConfigSchema = z groupAllowFrom: z.array(z.string()).optional(), groupPolicy: GroupPolicySchema.optional().default("allowlist"), textChunkLimit: z.number().int().positive().optional(), - chunkMode: z.enum(["length", "newline"]).optional(), blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(), mediaAllowHosts: z.array(z.string()).optional(), requireMention: z.boolean().optional(), diff --git a/src/config/zod-schema.providers-whatsapp.ts b/src/config/zod-schema.providers-whatsapp.ts index 7266f8bf6..5a0d62379 100644 --- a/src/config/zod-schema.providers-whatsapp.ts +++ b/src/config/zod-schema.providers-whatsapp.ts @@ -30,7 +30,6 @@ export const WhatsAppAccountSchema = z dmHistoryLimit: z.number().int().min(0).optional(), dms: z.record(z.string(), DmConfigSchema.optional()).optional(), textChunkLimit: z.number().int().positive().optional(), - chunkMode: z.enum(["length", "newline"]).optional(), mediaMaxMb: z.number().int().positive().optional(), blockStreaming: z.boolean().optional(), blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(), @@ -86,7 +85,6 @@ export const WhatsAppConfigSchema = z dmHistoryLimit: z.number().int().min(0).optional(), dms: z.record(z.string(), DmConfigSchema.optional()).optional(), textChunkLimit: z.number().int().positive().optional(), - chunkMode: z.enum(["length", "newline"]).optional(), mediaMaxMb: z.number().int().positive().optional().default(50), blockStreaming: z.boolean().optional(), blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(), diff --git a/src/infra/outbound/deliver.ts b/src/infra/outbound/deliver.ts index 015c38a33..2665e9957 100644 --- a/src/infra/outbound/deliver.ts +++ b/src/infra/outbound/deliver.ts @@ -1,8 +1,4 @@ -import { - chunkTextWithMode, - resolveChunkMode, - resolveTextChunkLimit, -} from "../../auto-reply/chunk.js"; +import { resolveTextChunkLimit } from "../../auto-reply/chunk.js"; import type { ReplyPayload } from "../../auto-reply/types.js"; import { resolveChannelMediaMaxBytes } from "../../channels/plugins/media-limits.js"; import { loadChannelOutboundAdapter } from "../../channels/plugins/outbound/load.js"; @@ -196,7 +192,6 @@ export async function deliverOutboundPayloads(params: { fallbackLimit: handler.textChunkLimit, }) : undefined; - const chunkMode = resolveChunkMode(cfg, channel, accountId); const isSignalChannel = channel === "signal"; const signalTableMode = isSignalChannel ? resolveMarkdownTableMode({ cfg, channel: "signal", accountId }) @@ -217,11 +212,7 @@ export async function deliverOutboundPayloads(params: { results.push(await handler.sendText(text)); return; } - // Use newline chunking if explicitly configured, otherwise use the adapter's chunker - const chunks = - chunkMode === "newline" - ? chunkTextWithMode(text, textLimit, chunkMode) - : handler.chunker(text, textLimit); + const chunks = handler.chunker(text, textLimit); for (const chunk of chunks) { throwIfAborted(abortSignal); results.push(await handler.sendText(chunk));