fix(mom): handle Slack message length limits and API errors gracefully

- Truncate messages exceeding Slack's 40,000 char limit
- Catch Slack API errors in queue and post to thread instead of crashing
- Add error context to queue operations for better debugging
- Wrap replaceMessage in try/catch
This commit is contained in:
Mario Zechner 2025-11-27 19:22:53 +01:00
parent 0e95592eb7
commit 4f845cdd1b

View file

@ -377,16 +377,32 @@ export function createAgentRunner(sandboxConfig: SandboxConfig): AgentRunner {
// Track stop reason // Track stop reason
let stopReason = "stop"; let stopReason = "stop";
// Slack message limit is 40,000 characters
const SLACK_MAX_LENGTH = 40000;
const truncateForSlack = (text: string): string => {
if (text.length <= SLACK_MAX_LENGTH) return text;
return text.substring(0, SLACK_MAX_LENGTH - 100) + "\n\n_(truncated - message too long)_";
};
// Promise queue to ensure ctx.respond/respondInThread calls execute in order // Promise queue to ensure ctx.respond/respondInThread calls execute in order
// Handles errors gracefully by posting to thread instead of crashing
const queue = { const queue = {
chain: Promise.resolve(), chain: Promise.resolve(),
enqueue<T>(fn: () => Promise<T>): Promise<T> { enqueue(fn: () => Promise<void>, errorContext: string): void {
const result = this.chain.then(fn); this.chain = this.chain.then(async () => {
this.chain = result.then( try {
() => {}, await fn();
() => {}, } catch (err) {
); // swallow errors for chain const errMsg = err instanceof Error ? err.message : String(err);
return result; log.logWarning(`Slack API error (${errorContext})`, errMsg);
// Try to post error to thread, but don't crash if that fails too
try {
await ctx.respondInThread(`_Error: ${errMsg}_`);
} catch {
// Ignore - we tried our best
}
}
});
}, },
flush(): Promise<void> { flush(): Promise<void> {
return this.chain; return this.chain;
@ -421,7 +437,7 @@ export function createAgentRunner(sandboxConfig: SandboxConfig): AgentRunner {
}); });
// Show label in main message only // Show label in main message only
queue.enqueue(() => ctx.respond(`_→ ${label}_`)); queue.enqueue(() => ctx.respond(`_→ ${label}_`), "tool label");
break; break;
} }
@ -469,11 +485,11 @@ export function createAgentRunner(sandboxConfig: SandboxConfig): AgentRunner {
threadMessage += "*Result:*\n```\n" + threadResult + "\n```"; threadMessage += "*Result:*\n```\n" + threadResult + "\n```";
queue.enqueue(() => ctx.respondInThread(threadMessage)); queue.enqueue(() => ctx.respondInThread(truncateForSlack(threadMessage)), "tool result thread");
// Show brief error in main message if failed // Show brief error in main message if failed
if (event.isError) { if (event.isError) {
queue.enqueue(() => ctx.respond(`_Error: ${truncate(resultStr, 200)}_`)); queue.enqueue(() => ctx.respond(`_Error: ${truncate(resultStr, 200)}_`), "tool error");
} }
break; break;
} }
@ -528,15 +544,15 @@ export function createAgentRunner(sandboxConfig: SandboxConfig): AgentRunner {
// Post thinking to main message and thread // Post thinking to main message and thread
for (const thinking of thinkingParts) { for (const thinking of thinkingParts) {
log.logThinking(logCtx, thinking); log.logThinking(logCtx, thinking);
queue.enqueue(() => ctx.respond(`_${thinking}_`)); queue.enqueue(() => ctx.respond(truncateForSlack(`_${thinking}_`)), "thinking main");
queue.enqueue(() => ctx.respondInThread(`_${thinking}_`)); queue.enqueue(() => ctx.respondInThread(truncateForSlack(`_${thinking}_`)), "thinking thread");
} }
// Post text to main message and thread // Post text to main message and thread
if (text.trim()) { if (text.trim()) {
log.logResponse(logCtx, text); log.logResponse(logCtx, text);
queue.enqueue(() => ctx.respond(text)); queue.enqueue(() => ctx.respond(truncateForSlack(text)), "response main");
queue.enqueue(() => ctx.respondInThread(text)); queue.enqueue(() => ctx.respondInThread(truncateForSlack(text)), "response thread");
} }
} }
break; break;
@ -566,13 +582,18 @@ export function createAgentRunner(sandboxConfig: SandboxConfig): AgentRunner {
.map((c) => c.text) .map((c) => c.text)
.join("\n") || ""; .join("\n") || "";
if (finalText.trim()) { if (finalText.trim()) {
await ctx.replaceMessage(finalText); try {
await ctx.replaceMessage(truncateForSlack(finalText));
} catch (err) {
const errMsg = err instanceof Error ? err.message : String(err);
log.logWarning("Failed to replace message with final text", errMsg);
}
} }
// Log usage summary if there was any usage // Log usage summary if there was any usage
if (totalUsage.cost.total > 0) { if (totalUsage.cost.total > 0) {
const summary = log.logUsageSummary(logCtx, totalUsage); const summary = log.logUsageSummary(logCtx, totalUsage);
queue.enqueue(() => ctx.respondInThread(summary)); queue.enqueue(() => ctx.respondInThread(summary), "usage summary");
await queue.flush(); await queue.flush();
} }