feat: configurable outbound text chunk limits

This commit is contained in:
Peter Steinberger
2026-01-03 01:07:23 +01:00
parent 2d28fa34f5
commit 5684e2d658
12 changed files with 154 additions and 27 deletions

View File

@@ -55,6 +55,7 @@
### Fixes ### Fixes
- Chat UI: keep the chat scrolled to the latest message after switching sessions. - Chat UI: keep the chat scrolled to the latest message after switching sessions.
- Auto-reply: stream completed reply blocks as soon as they finish (configurable default + break); skip empty tool-only blocks unless verbose. - Auto-reply: stream completed reply blocks as soon as they finish (configurable default + break); skip empty tool-only blocks unless verbose.
- Messages: make outbound text chunk limits configurable (defaults remain 4000/Discord 2000).
- CLI onboarding: persist gateway token in config so local CLI auth works; recommend auth Off unless you need multi-machine access. - CLI onboarding: persist gateway token in config so local CLI auth works; recommend auth Off unless you need multi-machine access.
- Control UI: accept a `?token=` URL param to auto-fill Gateway auth; onboarding now opens the dashboard with token auth when configured. - Control UI: accept a `?token=` URL param to auto-fill Gateway auth; onboarding now opens the dashboard with token auth when configured.
- Agent prompt: remove hardcoded user name in system prompt example. - Agent prompt: remove hardcoded user name in system prompt example.

View File

@@ -282,7 +282,17 @@ Controls inbound/outbound prefixes and timestamps.
messages: { messages: {
messagePrefix: "[clawdis]", messagePrefix: "[clawdis]",
responsePrefix: "🦞", responsePrefix: "🦞",
timestampPrefix: "Europe/London" timestampPrefix: "Europe/London",
// outbound chunk size (chars); defaults vary by surface (e.g. 4000, Discord 2000)
textChunkLimit: 4000,
// optional per-surface overrides
textChunkLimitBySurface: {
whatsapp: 4000,
telegram: 4000,
signal: 4000,
imessage: 4000,
discord: 2000
}
} }
} }
``` ```

View File

@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { chunkText } from "./chunk.js"; import { chunkText, resolveTextChunkLimit } from "./chunk.js";
describe("chunkText", () => { describe("chunkText", () => {
it("keeps multi-line text in one chunk when under limit", () => { it("keeps multi-line text in one chunk when under limit", () => {
@@ -45,3 +45,30 @@ describe("chunkText", () => {
expect(chunks).toEqual(["Supercalif", "ragilistic", "expialidoc", "ious"]); expect(chunks).toEqual(["Supercalif", "ragilistic", "expialidoc", "ious"]);
}); });
}); });
describe("resolveTextChunkLimit", () => {
it("uses per-surface defaults", () => {
expect(resolveTextChunkLimit(undefined, "whatsapp")).toBe(4000);
expect(resolveTextChunkLimit(undefined, "telegram")).toBe(4000);
expect(resolveTextChunkLimit(undefined, "signal")).toBe(4000);
expect(resolveTextChunkLimit(undefined, "imessage")).toBe(4000);
expect(resolveTextChunkLimit(undefined, "discord")).toBe(2000);
});
it("supports a global override", () => {
const cfg = { messages: { textChunkLimit: 1234 } };
expect(resolveTextChunkLimit(cfg, "whatsapp")).toBe(1234);
expect(resolveTextChunkLimit(cfg, "discord")).toBe(1234);
});
it("prefers per-surface overrides over global", () => {
const cfg = {
messages: {
textChunkLimit: 1234,
textChunkLimitBySurface: { discord: 111 },
},
};
expect(resolveTextChunkLimit(cfg, "discord")).toBe(111);
expect(resolveTextChunkLimit(cfg, "telegram")).toBe(1234);
});
});

View File

@@ -2,6 +2,43 @@
// unintentionally breaking on newlines. Using [\s\S] keeps newlines inside // unintentionally breaking on newlines. Using [\s\S] keeps newlines inside
// the chunk so messages are only split when they truly exceed the limit. // the chunk so messages are only split when they truly exceed the limit.
import type { ClawdisConfig } from "../config/config.js";
export type TextChunkSurface =
| "whatsapp"
| "telegram"
| "discord"
| "signal"
| "imessage"
| "webchat";
const DEFAULT_CHUNK_LIMIT_BY_SURFACE: Record<TextChunkSurface, number> = {
whatsapp: 4000,
telegram: 4000,
discord: 2000,
signal: 4000,
imessage: 4000,
webchat: 4000,
};
export function resolveTextChunkLimit(
cfg: Pick<ClawdisConfig, "messages"> | undefined,
surface?: TextChunkSurface,
): number {
const surfaceOverride = surface
? cfg?.messages?.textChunkLimitBySurface?.[surface]
: undefined;
if (typeof surfaceOverride === "number" && surfaceOverride > 0) {
return surfaceOverride;
}
const globalOverride = cfg?.messages?.textChunkLimit;
if (typeof globalOverride === "number" && globalOverride > 0) {
return globalOverride;
}
if (surface) return DEFAULT_CHUNK_LIMIT_BY_SURFACE[surface];
return 4000;
}
export function chunkText(text: string, limit: number): string[] { export function chunkText(text: string, limit: number): string[] {
if (!text) return []; if (!text) return [];
if (limit <= 0) return [text]; if (limit <= 0) return [text];

View File

@@ -17,7 +17,7 @@ import {
DEFAULT_AGENT_WORKSPACE_DIR, DEFAULT_AGENT_WORKSPACE_DIR,
ensureAgentWorkspace, ensureAgentWorkspace,
} from "../agents/workspace.js"; } from "../agents/workspace.js";
import { chunkText } from "../auto-reply/chunk.js"; import { chunkText, resolveTextChunkLimit } from "../auto-reply/chunk.js";
import type { MsgContext } from "../auto-reply/templating.js"; import type { MsgContext } from "../auto-reply/templating.js";
import { import {
normalizeThinkLevel, normalizeThinkLevel,
@@ -524,6 +524,15 @@ export async function agentCommand(
return; return;
} }
const deliveryTextLimit =
deliveryProvider === "whatsapp" ||
deliveryProvider === "telegram" ||
deliveryProvider === "discord" ||
deliveryProvider === "signal" ||
deliveryProvider === "imessage"
? resolveTextChunkLimit(cfg, deliveryProvider)
: resolveTextChunkLimit(cfg, "whatsapp");
for (const payload of payloads) { for (const payload of payloads) {
const mediaList = const mediaList =
payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []); payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
@@ -564,7 +573,7 @@ export async function agentCommand(
if (deliveryProvider === "telegram" && telegramTarget) { if (deliveryProvider === "telegram" && telegramTarget) {
try { try {
if (media.length === 0) { if (media.length === 0) {
for (const chunk of chunkText(text, 4000)) { for (const chunk of chunkText(text, deliveryTextLimit)) {
await deps.sendMessageTelegram(telegramTarget, chunk, { await deps.sendMessageTelegram(telegramTarget, chunk, {
verbose: false, verbose: false,
token: telegramToken || undefined, token: telegramToken || undefined,
@@ -645,7 +654,7 @@ export async function agentCommand(
if (deliveryProvider === "imessage" && imessageTarget) { if (deliveryProvider === "imessage" && imessageTarget) {
try { try {
if (media.length === 0) { if (media.length === 0) {
for (const chunk of chunkText(text, 4000)) { for (const chunk of chunkText(text, deliveryTextLimit)) {
await deps.sendMessageIMessage(imessageTarget, chunk, { await deps.sendMessageIMessage(imessageTarget, chunk, {
maxBytes: cfg.imessage?.mediaMaxMb maxBytes: cfg.imessage?.mediaMaxMb
? cfg.imessage.mediaMaxMb * 1024 * 1024 ? cfg.imessage.mediaMaxMb * 1024 * 1024

View File

@@ -314,6 +314,15 @@ export type MessagesConfig = {
messagePrefix?: string; // Prefix added to all inbound messages (default: "[clawdis]" if no allowFrom, else "") messagePrefix?: string; // Prefix added to all inbound messages (default: "[clawdis]" if no allowFrom, else "")
responsePrefix?: string; // Prefix auto-added to all outbound replies (e.g., "🦞") responsePrefix?: string; // Prefix auto-added to all outbound replies (e.g., "🦞")
timestampPrefix?: boolean | string; // true/false or IANA timezone string (default: true with UTC) timestampPrefix?: boolean | string; // true/false or IANA timezone string (default: true with UTC)
/** Outbound text chunk size (chars). Default varies by provider (e.g. 4000, Discord 2000). */
textChunkLimit?: number;
/** Optional per-surface chunk overrides. */
textChunkLimitBySurface?: Partial<
Record<
"whatsapp" | "telegram" | "discord" | "signal" | "imessage" | "webchat",
number
>
>;
}; };
export type BridgeBindMode = "auto" | "lan" | "tailnet" | "loopback"; export type BridgeBindMode = "auto" | "lan" | "tailnet" | "loopback";
@@ -708,6 +717,17 @@ const MessagesSchema = z
messagePrefix: z.string().optional(), messagePrefix: z.string().optional(),
responsePrefix: z.string().optional(), responsePrefix: z.string().optional(),
timestampPrefix: z.union([z.boolean(), z.string()]).optional(), timestampPrefix: z.union([z.boolean(), z.string()]).optional(),
textChunkLimit: z.number().int().positive().optional(),
textChunkLimitBySurface: z
.object({
whatsapp: z.number().int().positive().optional(),
telegram: z.number().int().positive().optional(),
discord: z.number().int().positive().optional(),
signal: z.number().int().positive().optional(),
imessage: z.number().int().positive().optional(),
webchat: z.number().int().positive().optional(),
})
.optional(),
}) })
.optional(); .optional();

View File

@@ -12,7 +12,7 @@ import {
DEFAULT_AGENT_WORKSPACE_DIR, DEFAULT_AGENT_WORKSPACE_DIR,
ensureAgentWorkspace, ensureAgentWorkspace,
} from "../agents/workspace.js"; } from "../agents/workspace.js";
import { chunkText } from "../auto-reply/chunk.js"; import { chunkText, resolveTextChunkLimit } from "../auto-reply/chunk.js";
import { normalizeThinkLevel } from "../auto-reply/thinking.js"; import { normalizeThinkLevel } from "../auto-reply/thinking.js";
import type { CliDeps } from "../cli/deps.js"; import type { CliDeps } from "../cli/deps.js";
import type { ClawdisConfig } from "../config/config.js"; import type { ClawdisConfig } from "../config/config.js";
@@ -357,12 +357,13 @@ export async function runCronIsolatedAgentTurn(params: {
}; };
} }
const chatId = resolvedDelivery.to; const chatId = resolvedDelivery.to;
const textLimit = resolveTextChunkLimit(params.cfg, "telegram");
try { try {
for (const payload of payloads) { for (const payload of payloads) {
const mediaList = const mediaList =
payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []); payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
if (mediaList.length === 0) { if (mediaList.length === 0) {
for (const chunk of chunkText(payload.text ?? "", 4000)) { for (const chunk of chunkText(payload.text ?? "", textLimit)) {
await params.deps.sendMessageTelegram(chatId, chunk, { await params.deps.sendMessageTelegram(chatId, chunk, {
verbose: false, verbose: false,
token: telegramToken || undefined, token: telegramToken || undefined,
@@ -444,12 +445,13 @@ export async function runCronIsolatedAgentTurn(params: {
}; };
} }
const to = resolvedDelivery.to; const to = resolvedDelivery.to;
const textLimit = resolveTextChunkLimit(params.cfg, "signal");
try { try {
for (const payload of payloads) { for (const payload of payloads) {
const mediaList = const mediaList =
payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []); payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
if (mediaList.length === 0) { if (mediaList.length === 0) {
for (const chunk of chunkText(payload.text ?? "", 4000)) { for (const chunk of chunkText(payload.text ?? "", textLimit)) {
await params.deps.sendMessageSignal(to, chunk); await params.deps.sendMessageSignal(to, chunk);
} }
} else { } else {
@@ -482,12 +484,13 @@ export async function runCronIsolatedAgentTurn(params: {
}; };
} }
const to = resolvedDelivery.to; const to = resolvedDelivery.to;
const textLimit = resolveTextChunkLimit(params.cfg, "imessage");
try { try {
for (const payload of payloads) { for (const payload of payloads) {
const mediaList = const mediaList =
payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []); payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
if (mediaList.length === 0) { if (mediaList.length === 0) {
for (const chunk of chunkText(payload.text ?? "", 4000)) { for (const chunk of chunkText(payload.text ?? "", textLimit)) {
await params.deps.sendMessageIMessage(to, chunk); await params.deps.sendMessageIMessage(to, chunk);
} }
} else { } else {

View File

@@ -1,4 +1,4 @@
import { chunkText } from "../auto-reply/chunk.js"; import { chunkText, resolveTextChunkLimit } from "../auto-reply/chunk.js";
import { formatAgentEnvelope } from "../auto-reply/envelope.js"; import { formatAgentEnvelope } from "../auto-reply/envelope.js";
import { getReplyFromConfig } from "../auto-reply/reply.js"; import { getReplyFromConfig } from "../auto-reply/reply.js";
import type { ReplyPayload } from "../auto-reply/types.js"; import type { ReplyPayload } from "../auto-reply/types.js";
@@ -111,15 +111,16 @@ async function deliverReplies(params: {
client: Awaited<ReturnType<typeof createIMessageRpcClient>>; client: Awaited<ReturnType<typeof createIMessageRpcClient>>;
runtime: RuntimeEnv; runtime: RuntimeEnv;
maxBytes: number; maxBytes: number;
textLimit: number;
}) { }) {
const { replies, target, client, runtime, maxBytes } = params; const { replies, target, client, runtime, maxBytes, textLimit } = params;
for (const payload of replies) { for (const payload of replies) {
const mediaList = const mediaList =
payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []); payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
const text = payload.text ?? ""; const text = payload.text ?? "";
if (!text && mediaList.length === 0) continue; if (!text && mediaList.length === 0) continue;
if (mediaList.length === 0) { if (mediaList.length === 0) {
for (const chunk of chunkText(text, 4000)) { for (const chunk of chunkText(text, textLimit)) {
await sendMessageIMessage(target, chunk, { maxBytes, client }); await sendMessageIMessage(target, chunk, { maxBytes, client });
} }
} else { } else {
@@ -143,6 +144,7 @@ export async function monitorIMessageProvider(
): Promise<void> { ): Promise<void> {
const runtime = resolveRuntime(opts); const runtime = resolveRuntime(opts);
const cfg = loadConfig(); const cfg = loadConfig();
const textLimit = resolveTextChunkLimit(cfg, "imessage");
const allowFrom = resolveAllowFrom(opts); const allowFrom = resolveAllowFrom(opts);
const mentionRegexes = resolveMentionRegexes(cfg); const mentionRegexes = resolveMentionRegexes(cfg);
const includeAttachments = const includeAttachments =
@@ -274,6 +276,7 @@ export async function monitorIMessageProvider(
client, client,
runtime, runtime,
maxBytes: mediaMaxBytes, maxBytes: mediaMaxBytes,
textLimit,
}); });
}) })
.catch((err) => { .catch((err) => {
@@ -302,6 +305,7 @@ export async function monitorIMessageProvider(
client, client,
runtime, runtime,
maxBytes: mediaMaxBytes, maxBytes: mediaMaxBytes,
textLimit,
}); });
}; };

View File

@@ -1,4 +1,4 @@
import { chunkText } from "../auto-reply/chunk.js"; import { chunkText, resolveTextChunkLimit } from "../auto-reply/chunk.js";
import { import {
HEARTBEAT_PROMPT, HEARTBEAT_PROMPT,
stripHeartbeatToken, stripHeartbeatToken,
@@ -292,6 +292,7 @@ async function deliverHeartbeatReply(params: {
to: string; to: string;
text: string; text: string;
mediaUrls: string[]; mediaUrls: string[];
textLimit: number;
deps: Required< deps: Required<
Pick< Pick<
HeartbeatDeps, HeartbeatDeps,
@@ -303,10 +304,10 @@ async function deliverHeartbeatReply(params: {
> >
>; >;
}) { }) {
const { channel, to, text, mediaUrls, deps } = params; const { channel, to, text, mediaUrls, deps, textLimit } = params;
if (channel === "whatsapp") { if (channel === "whatsapp") {
if (mediaUrls.length === 0) { if (mediaUrls.length === 0) {
for (const chunk of chunkText(text, 4000)) { for (const chunk of chunkText(text, textLimit)) {
await deps.sendWhatsApp(to, chunk, { verbose: false }); await deps.sendWhatsApp(to, chunk, { verbose: false });
} }
return; return;
@@ -322,7 +323,7 @@ async function deliverHeartbeatReply(params: {
if (channel === "signal") { if (channel === "signal") {
if (mediaUrls.length === 0) { if (mediaUrls.length === 0) {
for (const chunk of chunkText(text, 4000)) { for (const chunk of chunkText(text, textLimit)) {
await deps.sendSignal(to, chunk); await deps.sendSignal(to, chunk);
} }
return; return;
@@ -338,7 +339,7 @@ async function deliverHeartbeatReply(params: {
if (channel === "imessage") { if (channel === "imessage") {
if (mediaUrls.length === 0) { if (mediaUrls.length === 0) {
for (const chunk of chunkText(text, 4000)) { for (const chunk of chunkText(text, textLimit)) {
await deps.sendIMessage(to, chunk); await deps.sendIMessage(to, chunk);
} }
return; return;
@@ -354,7 +355,7 @@ async function deliverHeartbeatReply(params: {
if (channel === "telegram") { if (channel === "telegram") {
if (mediaUrls.length === 0) { if (mediaUrls.length === 0) {
for (const chunk of chunkText(text, 4000)) { for (const chunk of chunkText(text, textLimit)) {
await deps.sendTelegram(to, chunk, { verbose: false }); await deps.sendTelegram(to, chunk, { verbose: false });
} }
return; return;
@@ -500,11 +501,13 @@ export async function runHeartbeatOnce(opts: {
sendSignal: opts.deps?.sendSignal ?? sendMessageSignal, sendSignal: opts.deps?.sendSignal ?? sendMessageSignal,
sendIMessage: opts.deps?.sendIMessage ?? sendMessageIMessage, sendIMessage: opts.deps?.sendIMessage ?? sendMessageIMessage,
}; };
const textLimit = resolveTextChunkLimit(cfg, delivery.channel);
await deliverHeartbeatReply({ await deliverHeartbeatReply({
channel: delivery.channel, channel: delivery.channel,
to: delivery.to, to: delivery.to,
text: normalized.text, text: normalized.text,
mediaUrls, mediaUrls,
textLimit,
deps, deps,
}); });

View File

@@ -1,4 +1,4 @@
import { chunkText } from "../auto-reply/chunk.js"; import { chunkText, resolveTextChunkLimit } from "../auto-reply/chunk.js";
import { formatAgentEnvelope } from "../auto-reply/envelope.js"; import { formatAgentEnvelope } from "../auto-reply/envelope.js";
import { getReplyFromConfig } from "../auto-reply/reply.js"; import { getReplyFromConfig } from "../auto-reply/reply.js";
import type { ReplyPayload } from "../auto-reply/types.js"; import type { ReplyPayload } from "../auto-reply/types.js";
@@ -178,15 +178,17 @@ async function deliverReplies(params: {
account?: string; account?: string;
runtime: RuntimeEnv; runtime: RuntimeEnv;
maxBytes: number; maxBytes: number;
textLimit: number;
}) { }) {
const { replies, target, baseUrl, account, runtime, maxBytes } = params; const { replies, target, baseUrl, account, runtime, maxBytes, textLimit } =
params;
for (const payload of replies) { for (const payload of replies) {
const mediaList = const mediaList =
payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []); payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
const text = payload.text ?? ""; const text = payload.text ?? "";
if (!text && mediaList.length === 0) continue; if (!text && mediaList.length === 0) continue;
if (mediaList.length === 0) { if (mediaList.length === 0) {
for (const chunk of chunkText(text, 4000)) { for (const chunk of chunkText(text, textLimit)) {
await sendMessageSignal(target, chunk, { await sendMessageSignal(target, chunk, {
baseUrl, baseUrl,
account, account,
@@ -215,6 +217,7 @@ export async function monitorSignalProvider(
): Promise<void> { ): Promise<void> {
const runtime = resolveRuntime(opts); const runtime = resolveRuntime(opts);
const cfg = loadConfig(); const cfg = loadConfig();
const textLimit = resolveTextChunkLimit(cfg, "signal");
const baseUrl = resolveBaseUrl(opts); const baseUrl = resolveBaseUrl(opts);
const account = resolveAccount(opts); const account = resolveAccount(opts);
const allowFrom = resolveAllowFrom(opts); const allowFrom = resolveAllowFrom(opts);
@@ -391,6 +394,7 @@ export async function monitorSignalProvider(
account, account,
runtime, runtime,
maxBytes: mediaMaxBytes, maxBytes: mediaMaxBytes,
textLimit,
}); });
}) })
.catch((err) => { .catch((err) => {
@@ -420,6 +424,7 @@ export async function monitorSignalProvider(
account, account,
runtime, runtime,
maxBytes: mediaMaxBytes, maxBytes: mediaMaxBytes,
textLimit,
}); });
}; };

View File

@@ -5,7 +5,7 @@ import { apiThrottler } from "@grammyjs/transformer-throttler";
import type { ApiClientOptions, Message } from "grammy"; import type { ApiClientOptions, Message } from "grammy";
import { Bot, InputFile, webhookCallback } from "grammy"; import { Bot, InputFile, webhookCallback } from "grammy";
import { chunkText } from "../auto-reply/chunk.js"; import { chunkText, resolveTextChunkLimit } from "../auto-reply/chunk.js";
import { formatAgentEnvelope } from "../auto-reply/envelope.js"; import { formatAgentEnvelope } from "../auto-reply/envelope.js";
import { getReplyFromConfig } from "../auto-reply/reply.js"; import { getReplyFromConfig } from "../auto-reply/reply.js";
import type { ReplyPayload } from "../auto-reply/types.js"; import type { ReplyPayload } from "../auto-reply/types.js";
@@ -60,6 +60,7 @@ export function createTelegramBot(opts: TelegramBotOptions) {
bot.api.config.use(apiThrottler()); bot.api.config.use(apiThrottler());
const cfg = loadConfig(); const cfg = loadConfig();
const textLimit = resolveTextChunkLimit(cfg, "telegram");
const allowFrom = opts.allowFrom ?? cfg.telegram?.allowFrom; const allowFrom = opts.allowFrom ?? cfg.telegram?.allowFrom;
const replyToMode = opts.replyToMode ?? cfg.telegram?.replyToMode ?? "off"; const replyToMode = opts.replyToMode ?? cfg.telegram?.replyToMode ?? "off";
const mediaMaxBytes = const mediaMaxBytes =
@@ -245,6 +246,7 @@ export function createTelegramBot(opts: TelegramBotOptions) {
runtime, runtime,
bot, bot,
replyToMode, replyToMode,
textLimit,
}); });
} catch (err) { } catch (err) {
runtime.error?.(danger(`handler failed: ${String(err)}`)); runtime.error?.(danger(`handler failed: ${String(err)}`));
@@ -268,8 +270,9 @@ async function deliverReplies(params: {
runtime: RuntimeEnv; runtime: RuntimeEnv;
bot: Bot; bot: Bot;
replyToMode: ReplyToMode; replyToMode: ReplyToMode;
textLimit: number;
}) { }) {
const { replies, chatId, runtime, bot, replyToMode } = params; const { replies, chatId, runtime, bot, replyToMode, textLimit } = params;
let hasReplied = false; let hasReplied = false;
for (const reply of replies) { for (const reply of replies) {
if (!reply?.text && !reply?.mediaUrl && !(reply?.mediaUrls?.length ?? 0)) { if (!reply?.text && !reply?.mediaUrl && !(reply?.mediaUrls?.length ?? 0)) {
@@ -286,7 +289,7 @@ async function deliverReplies(params: {
? [reply.mediaUrl] ? [reply.mediaUrl]
: []; : [];
if (mediaList.length === 0) { if (mediaList.length === 0) {
for (const chunk of chunkText(reply.text || "", 4000)) { for (const chunk of chunkText(reply.text || "", textLimit)) {
await sendTelegramText(bot, chatId, chunk, runtime, { await sendTelegramText(bot, chatId, chunk, runtime, {
replyToMessageId: replyToMessageId:
replyToId && (replyToMode === "all" || !hasReplied) replyToId && (replyToMode === "all" || !hasReplied)

View File

@@ -1,4 +1,4 @@
import { chunkText } from "../auto-reply/chunk.js"; import { chunkText, resolveTextChunkLimit } from "../auto-reply/chunk.js";
import { formatAgentEnvelope } from "../auto-reply/envelope.js"; import { formatAgentEnvelope } from "../auto-reply/envelope.js";
import { import {
normalizeGroupActivation, normalizeGroupActivation,
@@ -41,7 +41,6 @@ import {
} from "./reconnect.js"; } from "./reconnect.js";
import { formatError, getWebAuthAgeMs, readWebSelfId } from "./session.js"; import { formatError, getWebAuthAgeMs, readWebSelfId } from "./session.js";
const WEB_TEXT_LIMIT = 4000;
const DEFAULT_GROUP_HISTORY_LIMIT = 50; const DEFAULT_GROUP_HISTORY_LIMIT = 50;
const whatsappLog = createSubsystemLogger("gateway/providers/whatsapp"); const whatsappLog = createSubsystemLogger("gateway/providers/whatsapp");
const whatsappInboundLog = whatsappLog.child("inbound"); const whatsappInboundLog = whatsappLog.child("inbound");
@@ -502,6 +501,7 @@ async function deliverWebReply(params: {
replyResult: ReplyPayload; replyResult: ReplyPayload;
msg: WebInboundMsg; msg: WebInboundMsg;
maxMediaBytes: number; maxMediaBytes: number;
textLimit: number;
replyLogger: ReturnType<typeof getChildLogger>; replyLogger: ReturnType<typeof getChildLogger>;
connectionId?: string; connectionId?: string;
skipLog?: boolean; skipLog?: boolean;
@@ -510,12 +510,13 @@ async function deliverWebReply(params: {
replyResult, replyResult,
msg, msg,
maxMediaBytes, maxMediaBytes,
textLimit,
replyLogger, replyLogger,
connectionId, connectionId,
skipLog, skipLog,
} = params; } = params;
const replyStarted = Date.now(); const replyStarted = Date.now();
const textChunks = chunkText(replyResult.text || "", WEB_TEXT_LIMIT); const textChunks = chunkText(replyResult.text || "", textLimit);
const mediaList = replyResult.mediaUrls?.length const mediaList = replyResult.mediaUrls?.length
? replyResult.mediaUrls ? replyResult.mediaUrls
: replyResult.mediaUrl : replyResult.mediaUrl
@@ -1050,6 +1051,7 @@ export async function monitorWebProvider(
} }
const responsePrefix = cfg.messages?.responsePrefix; const responsePrefix = cfg.messages?.responsePrefix;
const textLimit = resolveTextChunkLimit(cfg, "whatsapp");
let didLogHeartbeatStrip = false; let didLogHeartbeatStrip = false;
let didSendReply = false; let didSendReply = false;
let toolSendChain: Promise<void> = Promise.resolve(); let toolSendChain: Promise<void> = Promise.resolve();
@@ -1091,6 +1093,7 @@ export async function monitorWebProvider(
replyResult: toolPayload, replyResult: toolPayload,
msg, msg,
maxMediaBytes, maxMediaBytes,
textLimit,
replyLogger, replyLogger,
connectionId, connectionId,
skipLog: true, skipLog: true,
@@ -1134,6 +1137,7 @@ export async function monitorWebProvider(
replyResult: blockPayload, replyResult: blockPayload,
msg, msg,
maxMediaBytes, maxMediaBytes,
textLimit,
replyLogger, replyLogger,
connectionId, connectionId,
skipLog: true, skipLog: true,
@@ -1238,6 +1242,7 @@ export async function monitorWebProvider(
replyResult: replyPayload, replyResult: replyPayload,
msg, msg,
maxMediaBytes, maxMediaBytes,
textLimit,
replyLogger, replyLogger,
connectionId, connectionId,
}); });