fix: add message chunking to Discord adapter and log delivery failures (#459)
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user