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
This commit is contained in:
Mario Zechner 2025-12-04 23:39:17 +01:00
parent c7585e37c9
commit e1d3c2b76e
3 changed files with 152 additions and 68 deletions

View file

@ -4,6 +4,12 @@
### Fixed ### 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 - Private channel messages not being logged
- Added `message.groups` to required bot events in README - Added `message.groups` to required bot events in README
- Added `groups:history` and `groups:read` to required scopes in README - Added `groups:history` and `groups:read` to required scopes in README

View file

@ -127,7 +127,11 @@ function getRecentMessages(channelDir: string, turnCount: number): string {
for (const msg of turn) { for (const msg of turn) {
const date = (msg.date || "").substring(0, 19); const date = (msg.date || "").substring(0, 19);
const user = msg.userName || msg.user || ""; 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(","); const attachments = (msg.attachments || []).map((a) => a.local).join(",");
formatted.push(`${date}\t${user}\t${text}\t${attachments}`); formatted.push(`${date}\t${user}\t${text}\t${attachments}`);
} }
@ -136,6 +140,43 @@ function getRecentMessages(channelDir: string, turnCount: number): string {
return formatted.join("\n"); 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 { function getMemory(channelDir: string): string {
const parts: string[] = []; const parts: string[] = [];
@ -545,7 +586,7 @@ export function createAgentRunner(sandboxConfig: SandboxConfig): AgentRunner {
date: new Date().toISOString(), date: new Date().toISOString(),
ts: toSlackTs(), ts: toSlackTs(),
user: "bot", user: "bot",
text: `[Tool Result] ${event.toolName}: ${event.isError ? "ERROR: " : ""}${truncate(resultStr, 1000)}`, text: `[Tool Result] ${event.toolName}: ${event.isError ? "ERROR: " : ""}${resultStr}`,
attachments: [], attachments: [],
isBot: true, isBot: true,
}); });

View file

@ -24,7 +24,7 @@ export interface SlackContext {
/** All known users in the workspace */ /** All known users in the workspace */
users: UserInfo[]; users: UserInfo[];
/** Send/update the main message (accumulates text). Set log=false to skip logging. */ /** Send/update the main message (accumulates text). Set log=false to skip logging. */
respond(text: string, log?: boolean): Promise<void>; respond(text: string, shouldLog?: boolean): Promise<void>;
/** Replace the entire message text (not append) */ /** Replace the entire message text (not append) */
replaceMessage(text: string): Promise<void>; replaceMessage(text: string): Promise<void>;
/** Post a message in the thread under the main message (for verbose details) */ /** Post a message in the thread under the main message (for verbose details) */
@ -352,40 +352,52 @@ export class MomBot {
store: this.store, store: this.store,
channels: this.getChannels(), channels: this.getChannels(),
users: this.getUsers(), users: this.getUsers(),
respond: async (responseText: string, log = true) => { respond: async (responseText: string, shouldLog = true) => {
// Queue updates to avoid race conditions // Queue updates to avoid race conditions
updatePromise = updatePromise.then(async () => { updatePromise = updatePromise.then(async () => {
if (isThinking) { try {
// First real response replaces "Thinking..." if (isThinking) {
accumulatedText = responseText; // First real response replaces "Thinking..."
isThinking = false; accumulatedText = responseText;
} else { isThinking = false;
// Subsequent responses get appended } else {
accumulatedText += "\n" + responseText; // Subsequent responses get appended
} accumulatedText += "\n" + responseText;
}
// Add working indicator if still working // Truncate accumulated text if too long (Slack limit is 40K, we use 35K for safety)
const displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText; 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) { // Add working indicator if still working
// Update existing message const displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;
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 (messageTs) {
if (log) { // Update existing message
await this.store.logBotResponse(event.channel, responseText, 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;
}
// 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) => { respondInThread: async (threadText: string) => {
// Queue thread posts to maintain order // Queue thread posts to maintain order
updatePromise = updatePromise.then(async () => { updatePromise = updatePromise.then(async () => {
if (!messageTs) { try {
// No main message yet, just skip if (!messageTs) {
return; // 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; await updatePromise;
}, },
@ -434,40 +457,54 @@ export class MomBot {
}, },
replaceMessage: async (text: string) => { replaceMessage: async (text: string) => {
updatePromise = updatePromise.then(async () => { updatePromise = updatePromise.then(async () => {
// Replace the accumulated text entirely try {
accumulatedText = text; // 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) { if (messageTs) {
await this.webClient.chat.update({ await this.webClient.chat.update({
channel: event.channel, channel: event.channel,
ts: messageTs, ts: messageTs,
text: displayText, text: displayText,
}); });
} else { } else {
// Post initial message // Post initial message
const result = await this.webClient.chat.postMessage({ const result = await this.webClient.chat.postMessage({
channel: event.channel, channel: event.channel,
text: displayText, text: displayText,
}); });
messageTs = result.ts as string; messageTs = result.ts as string;
}
} catch (err) {
log.logWarning("Slack replaceMessage error", err instanceof Error ? err.message : String(err));
} }
}); });
await updatePromise; await updatePromise;
}, },
setWorking: async (working: boolean) => { setWorking: async (working: boolean) => {
updatePromise = updatePromise.then(async () => { updatePromise = updatePromise.then(async () => {
isWorking = working; try {
isWorking = working;
// If we have a message, update it to add/remove indicator // If we have a message, update it to add/remove indicator
if (messageTs) { if (messageTs) {
const displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText; const displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;
await this.webClient.chat.update({ await this.webClient.chat.update({
channel: event.channel, channel: event.channel,
ts: messageTs, ts: messageTs,
text: displayText, text: displayText,
}); });
}
} catch (err) {
log.logWarning("Slack setWorking error", err instanceof Error ? err.message : String(err));
} }
}); });
await updatePromise; await updatePromise;