fix: add message chunking to Discord adapter and log delivery failures (#459)

This commit is contained in:
Cameron
2026-03-02 14:53:08 -08:00
committed by GitHub
parent 5a58d759e0
commit c37638d7ce
2 changed files with 75 additions and 6 deletions

View File

@@ -350,8 +350,14 @@ Ask the bot owner to approve with:
throw new Error(`Discord channel not found or not text-based: ${msg.chatId}`);
}
const result = await (channel as { send: (content: string) => Promise<{ id: string }> }).send(msg.text);
return { messageId: result.id };
const sendable = channel as { send: (content: string) => Promise<{ id: string }> };
const chunks = splitMessageText(msg.text);
let lastMessageId = '';
for (const chunk of chunks) {
const result = await sendable.send(chunk);
lastMessageId = result.id;
}
return { messageId: lastMessageId };
}
async sendFile(file: OutboundFile): Promise<{ messageId: string }> {
@@ -384,7 +390,12 @@ Ask the bot owner to approve with:
log.warn('Cannot edit message not sent by bot');
return;
}
await message.edit(text);
// Discord edit limit is 2000 chars -- truncate if needed (edits can't split)
const truncated = text.length > DISCORD_MAX_LENGTH
? text.slice(0, DISCORD_MAX_LENGTH - 1) + '\u2026'
: text;
await message.edit(truncated);
}
async addReaction(chatId: string, messageId: string, emoji: string): Promise<void> {
@@ -559,6 +570,58 @@ function resolveDiscordEmoji(input: string): string {
return input;
}
// Discord message length limit
const DISCORD_MAX_LENGTH = 2000;
// Leave some headroom when choosing split points
const DISCORD_SPLIT_THRESHOLD = 1900;
/**
* Split text into chunks that fit within Discord's 2000-char limit.
* Splits at paragraph boundaries (double newlines), falling back to
* single newlines, then hard-splitting at the threshold.
*/
function splitMessageText(text: string): string[] {
if (text.length <= DISCORD_SPLIT_THRESHOLD) {
return [text];
}
const chunks: string[] = [];
let remaining = text;
while (remaining.length > DISCORD_SPLIT_THRESHOLD) {
let splitIdx = -1;
const searchRegion = remaining.slice(0, DISCORD_SPLIT_THRESHOLD);
// Try paragraph boundary (double newline)
const lastParagraph = searchRegion.lastIndexOf('\n\n');
if (lastParagraph > DISCORD_SPLIT_THRESHOLD * 0.3) {
splitIdx = lastParagraph;
}
// Fall back to single newline
if (splitIdx === -1) {
const lastNewline = searchRegion.lastIndexOf('\n');
if (lastNewline > DISCORD_SPLIT_THRESHOLD * 0.3) {
splitIdx = lastNewline;
}
}
// Hard split as last resort
if (splitIdx === -1) {
splitIdx = DISCORD_SPLIT_THRESHOLD;
}
chunks.push(remaining.slice(0, splitIdx).trimEnd());
remaining = remaining.slice(splitIdx).trimStart();
}
if (remaining.trim()) {
chunks.push(remaining.trim());
}
return chunks;
}
type DiscordAttachment = {
id?: string;
name?: string | null;

View File

@@ -1024,8 +1024,13 @@ export class LettaBot implements AgentSession {
await adapter.sendMessage({ chatId: msg.chatId, text: prefixed, threadId: msg.threadId });
}
sentAnyMessage = true;
} catch {
if (messageId) sentAnyMessage = true;
} catch (finalizeErr) {
if (messageId) {
// Edit failed but original message was already visible
sentAnyMessage = true;
} else {
log.warn('finalizeMessage send failed:', finalizeErr instanceof Error ? finalizeErr.message : finalizeErr);
}
}
}
response = '';
@@ -1442,8 +1447,9 @@ export class LettaBot implements AgentSession {
}
sentAnyMessage = true;
this.store.resetRecoveryAttempts();
} catch {
} catch (sendErr) {
// Edit failed -- send as new message so user isn't left with truncated text
log.warn('Final message delivery failed:', sendErr instanceof Error ? sendErr.message : sendErr);
try {
await adapter.sendMessage({ chatId: msg.chatId, text: prefixedFinal, threadId: msg.threadId });
sentAnyMessage = true;