mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 09:01:14 +00:00
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:
parent
c7585e37c9
commit
e1d3c2b76e
3 changed files with 152 additions and 68 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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,9 +352,10 @@ 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 () => {
|
||||||
|
try {
|
||||||
if (isThinking) {
|
if (isThinking) {
|
||||||
// First real response replaces "Thinking..."
|
// First real response replaces "Thinking..."
|
||||||
accumulatedText = responseText;
|
accumulatedText = responseText;
|
||||||
|
|
@ -364,6 +365,14 @@ export class MomBot {
|
||||||
accumulatedText += "\n" + responseText;
|
accumulatedText += "\n" + responseText;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
// Add working indicator if still working
|
// Add working indicator if still working
|
||||||
const displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;
|
const displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;
|
||||||
|
|
||||||
|
|
@ -384,9 +393,12 @@ export class MomBot {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log the response if requested
|
// Log the response if requested
|
||||||
if (log) {
|
if (shouldLog) {
|
||||||
await this.store.logBotResponse(event.channel, responseText, messageTs!);
|
await this.store.logBotResponse(event.channel, responseText, messageTs!);
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
log.logWarning("Slack respond error", err instanceof Error ? err.message : String(err));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await updatePromise;
|
await updatePromise;
|
||||||
|
|
@ -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 () => {
|
||||||
|
try {
|
||||||
if (!messageTs) {
|
if (!messageTs) {
|
||||||
// No main message yet, just skip
|
// No main message yet, just skip
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Obfuscate usernames to avoid pinging people in thread details
|
// Obfuscate usernames to avoid pinging people in thread details
|
||||||
const obfuscatedText = this.obfuscateUsernames(threadText);
|
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
|
// Post in thread under the main message
|
||||||
await this.webClient.chat.postMessage({
|
await this.webClient.chat.postMessage({
|
||||||
channel: event.channel,
|
channel: event.channel,
|
||||||
thread_ts: messageTs,
|
thread_ts: messageTs,
|
||||||
text: obfuscatedText,
|
text: obfuscatedText,
|
||||||
});
|
});
|
||||||
|
} catch (err) {
|
||||||
|
log.logWarning("Slack respondInThread error", err instanceof Error ? err.message : String(err));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
await updatePromise;
|
await updatePromise;
|
||||||
},
|
},
|
||||||
|
|
@ -434,8 +457,15 @@ 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 {
|
||||||
|
// 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;
|
accumulatedText = text;
|
||||||
|
}
|
||||||
|
|
||||||
const displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;
|
const displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;
|
||||||
|
|
||||||
|
|
@ -453,11 +483,15 @@ export class MomBot {
|
||||||
});
|
});
|
||||||
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 () => {
|
||||||
|
try {
|
||||||
isWorking = working;
|
isWorking = working;
|
||||||
|
|
||||||
// If we have a message, update it to add/remove indicator
|
// If we have a message, update it to add/remove indicator
|
||||||
|
|
@ -469,6 +503,9 @@ export class MomBot {
|
||||||
text: displayText,
|
text: displayText,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
log.logWarning("Slack setWorking error", err instanceof Error ? err.message : String(err));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
await updatePromise;
|
await updatePromise;
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue