From e1d3c2b76e68d9e07f01112152ad2785591f0056 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Thu, 4 Dec 2025 23:39:17 +0100 Subject: [PATCH] fix(mom): handle Slack msg_too_long errors gracefully - Add try/catch to all Slack API calls in promise chain - Truncate main channel messages at 35K with elaboration note - Truncate thread messages at 20K - Prevents process crash on long messages --- packages/mom/CHANGELOG.md | 6 ++ packages/mom/src/agent.ts | 45 +++++++++- packages/mom/src/slack.ts | 169 +++++++++++++++++++++++--------------- 3 files changed, 152 insertions(+), 68 deletions(-) diff --git a/packages/mom/CHANGELOG.md b/packages/mom/CHANGELOG.md index c2a79f0b..e71f99aa 100644 --- a/packages/mom/CHANGELOG.md +++ b/packages/mom/CHANGELOG.md @@ -4,6 +4,12 @@ ### Fixed +- Slack API errors (msg_too_long) no longer crash the process + - Added try/catch error handling to all Slack API calls in the message queue + - Main channel messages truncated at 35K with note to ask for elaboration + - Thread messages truncated at 20K + - replaceMessage also truncated at 35K + - Private channel messages not being logged - Added `message.groups` to required bot events in README - Added `groups:history` and `groups:read` to required scopes in README diff --git a/packages/mom/src/agent.ts b/packages/mom/src/agent.ts index e6bbaa54..fa68c991 100644 --- a/packages/mom/src/agent.ts +++ b/packages/mom/src/agent.ts @@ -127,7 +127,11 @@ function getRecentMessages(channelDir: string, turnCount: number): string { for (const msg of turn) { const date = (msg.date || "").substring(0, 19); const user = msg.userName || msg.user || ""; - const text = msg.text || ""; + let text = msg.text || ""; + // Truncate bot messages (tool results can be huge) + if (msg.isBot) { + text = truncateForContext(text, 50000, 2000, msg.ts); + } const attachments = (msg.attachments || []).map((a) => a.local).join(","); formatted.push(`${date}\t${user}\t${text}\t${attachments}`); } @@ -136,6 +140,43 @@ function getRecentMessages(channelDir: string, turnCount: number): string { return formatted.join("\n"); } +/** + * Truncate text to maxChars or maxLines, whichever comes first. + * Adds a note with stats and instructions if truncation occurred. + */ +function truncateForContext(text: string, maxChars: number, maxLines: number, ts?: string): string { + const lines = text.split("\n"); + const originalLines = lines.length; + const originalChars = text.length; + let truncated = false; + let result = text; + + // Check line limit first + if (lines.length > maxLines) { + result = lines.slice(0, maxLines).join("\n"); + truncated = true; + } + + // Check char limit + if (result.length > maxChars) { + result = result.substring(0, maxChars); + truncated = true; + } + + if (truncated) { + const remainingLines = originalLines - result.split("\n").length; + const remainingChars = originalChars - result.length; + result += `\n[... truncated ${remainingLines} more lines, ${remainingChars} more chars. `; + if (ts) { + result += `To get full content: jq -r 'select(.ts=="${ts}") | .text' log.jsonl > /tmp/msg.txt, then read /tmp/msg.txt in segments]`; + } else { + result += `Search log.jsonl for full content]`; + } + } + + return result; +} + function getMemory(channelDir: string): string { const parts: string[] = []; @@ -545,7 +586,7 @@ export function createAgentRunner(sandboxConfig: SandboxConfig): AgentRunner { date: new Date().toISOString(), ts: toSlackTs(), user: "bot", - text: `[Tool Result] ${event.toolName}: ${event.isError ? "ERROR: " : ""}${truncate(resultStr, 1000)}`, + text: `[Tool Result] ${event.toolName}: ${event.isError ? "ERROR: " : ""}${resultStr}`, attachments: [], isBot: true, }); diff --git a/packages/mom/src/slack.ts b/packages/mom/src/slack.ts index f0cf0007..aa24e667 100644 --- a/packages/mom/src/slack.ts +++ b/packages/mom/src/slack.ts @@ -24,7 +24,7 @@ export interface SlackContext { /** All known users in the workspace */ users: UserInfo[]; /** Send/update the main message (accumulates text). Set log=false to skip logging. */ - respond(text: string, log?: boolean): Promise; + respond(text: string, shouldLog?: boolean): Promise; /** Replace the entire message text (not append) */ replaceMessage(text: string): Promise; /** Post a message in the thread under the main message (for verbose details) */ @@ -352,40 +352,52 @@ export class MomBot { store: this.store, channels: this.getChannels(), users: this.getUsers(), - respond: async (responseText: string, log = true) => { + respond: async (responseText: string, shouldLog = true) => { // Queue updates to avoid race conditions updatePromise = updatePromise.then(async () => { - if (isThinking) { - // First real response replaces "Thinking..." - accumulatedText = responseText; - isThinking = false; - } else { - // Subsequent responses get appended - accumulatedText += "\n" + responseText; - } + try { + if (isThinking) { + // First real response replaces "Thinking..." + accumulatedText = responseText; + isThinking = false; + } else { + // Subsequent responses get appended + accumulatedText += "\n" + responseText; + } - // Add working indicator if still working - const displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText; + // Truncate accumulated text if too long (Slack limit is 40K, we use 35K for safety) + const MAX_MAIN_LENGTH = 35000; + const truncationNote = "\n\n_(message truncated, ask me to elaborate on specific parts)_"; + if (accumulatedText.length > MAX_MAIN_LENGTH) { + accumulatedText = + accumulatedText.substring(0, MAX_MAIN_LENGTH - truncationNote.length) + truncationNote; + } - if (messageTs) { - // Update existing message - await this.webClient.chat.update({ - channel: event.channel, - ts: messageTs, - text: displayText, - }); - } else { - // Post initial message - const result = await this.webClient.chat.postMessage({ - channel: event.channel, - text: displayText, - }); - messageTs = result.ts as string; - } + // Add working indicator if still working + const displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText; - // Log the response if requested - if (log) { - await this.store.logBotResponse(event.channel, responseText, messageTs!); + if (messageTs) { + // Update existing message + await this.webClient.chat.update({ + channel: event.channel, + ts: messageTs, + text: displayText, + }); + } else { + // Post initial message + const result = await this.webClient.chat.postMessage({ + channel: event.channel, + text: displayText, + }); + messageTs = result.ts as string; + } + + // Log the response if requested + if (shouldLog) { + await this.store.logBotResponse(event.channel, responseText, messageTs!); + } + } catch (err) { + log.logWarning("Slack respond error", err instanceof Error ? err.message : String(err)); } }); @@ -394,18 +406,29 @@ export class MomBot { respondInThread: async (threadText: string) => { // Queue thread posts to maintain order updatePromise = updatePromise.then(async () => { - if (!messageTs) { - // No main message yet, just skip - return; + try { + if (!messageTs) { + // No main message yet, just skip + return; + } + // Obfuscate usernames to avoid pinging people in thread details + let obfuscatedText = this.obfuscateUsernames(threadText); + + // Truncate thread messages if too long (20K limit for safety) + const MAX_THREAD_LENGTH = 20000; + if (obfuscatedText.length > MAX_THREAD_LENGTH) { + obfuscatedText = obfuscatedText.substring(0, MAX_THREAD_LENGTH - 50) + "\n\n_(truncated)_"; + } + + // Post in thread under the main message + await this.webClient.chat.postMessage({ + channel: event.channel, + thread_ts: messageTs, + text: obfuscatedText, + }); + } catch (err) { + log.logWarning("Slack respondInThread error", err instanceof Error ? err.message : String(err)); } - // Obfuscate usernames to avoid pinging people in thread details - const obfuscatedText = this.obfuscateUsernames(threadText); - // Post in thread under the main message - await this.webClient.chat.postMessage({ - channel: event.channel, - thread_ts: messageTs, - text: obfuscatedText, - }); }); await updatePromise; }, @@ -434,40 +457,54 @@ export class MomBot { }, replaceMessage: async (text: string) => { updatePromise = updatePromise.then(async () => { - // Replace the accumulated text entirely - accumulatedText = text; + try { + // Replace the accumulated text entirely, with truncation + const MAX_MAIN_LENGTH = 35000; + const truncationNote = "\n\n_(message truncated, ask me to elaborate on specific parts)_"; + if (text.length > MAX_MAIN_LENGTH) { + accumulatedText = text.substring(0, MAX_MAIN_LENGTH - truncationNote.length) + truncationNote; + } else { + accumulatedText = text; + } - const displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText; + const displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText; - if (messageTs) { - await this.webClient.chat.update({ - channel: event.channel, - ts: messageTs, - text: displayText, - }); - } else { - // Post initial message - const result = await this.webClient.chat.postMessage({ - channel: event.channel, - text: displayText, - }); - messageTs = result.ts as string; + if (messageTs) { + await this.webClient.chat.update({ + channel: event.channel, + ts: messageTs, + text: displayText, + }); + } else { + // Post initial message + const result = await this.webClient.chat.postMessage({ + channel: event.channel, + text: displayText, + }); + messageTs = result.ts as string; + } + } catch (err) { + log.logWarning("Slack replaceMessage error", err instanceof Error ? err.message : String(err)); } }); await updatePromise; }, setWorking: async (working: boolean) => { updatePromise = updatePromise.then(async () => { - isWorking = working; + try { + isWorking = working; - // If we have a message, update it to add/remove indicator - if (messageTs) { - const displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText; - await this.webClient.chat.update({ - channel: event.channel, - ts: messageTs, - text: displayText, - }); + // If we have a message, update it to add/remove indicator + if (messageTs) { + const displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText; + await this.webClient.chat.update({ + channel: event.channel, + ts: messageTs, + text: displayText, + }); + } + } catch (err) { + log.logWarning("Slack setWorking error", err instanceof Error ? err.message : String(err)); } }); await updatePromise;