import * as os from "os"; import { Message, TextChannel } from "discord.js"; import { DisclawDatabase } from "./db/database"; import { DisclawConfig } from "./config/loader"; import { runAgent } from "./agent/runner"; import { sanitizeForDiscord } from "./runtime/sanitize"; const DISCORD_MAX_LENGTH = 2000; /** * Splits a message at line breaks so each chunk stays under the Discord * character limit. If a single line exceeds the limit, it is split at * the character boundary as a last resort. */ function splitMessage(text: string): string[] { if (text.length <= DISCORD_MAX_LENGTH) { return [text]; } const chunks: string[] = []; const lines = text.split("\n"); let current = ""; for (const line of lines) { // If adding this line would exceed the limit, flush current chunk if (current.length + line.length + 1 > DISCORD_MAX_LENGTH) { if (current.length > 0) { chunks.push(current); current = ""; } // Handle lines that are themselves too long if (line.length > DISCORD_MAX_LENGTH) { let remaining = line; while (remaining.length > DISCORD_MAX_LENGTH) { chunks.push(remaining.slice(0, DISCORD_MAX_LENGTH)); remaining = remaining.slice(DISCORD_MAX_LENGTH); } if (remaining.length > 0) { current = remaining; } continue; } } current += (current.length > 0 ? "\n" : "") + line; } if (current.length > 0) { chunks.push(current); } return chunks; } /** * Routes a Discord message to the correct agent based on channel_id. * Returns true if the message was handled, false if the channel is not * mapped to any agent. */ export async function routeMessage( message: Message, db: DisclawDatabase, config: DisclawConfig, managementChannelId: string | null ): Promise { const channelId = message.channelId; // Ignore messages in the management channel (commands only) if (channelId === managementChannelId) { return false; } // Look up workspace for this channel const workspace = db.getWorkspaceByChannelId(channelId); if (!workspace) { return false; } const channel = message.channel as TextChannel; // Show typing indicator while the agent works const typingInterval = setInterval(() => { channel.sendTyping().catch(() => {}); }, 5000); // Send immediately as well await channel.sendTyping().catch(() => {}); try { // Load recent conversation history for context const history = db.getConversationHistory(workspace.id, 30); // Run the agent const response = await runAgent({ workspacePath: workspace.workspace_path, channelName: channel.name, userMessage: message.content, conversationHistory: history, }); // Store user message db.addConversation( workspace.id, message.id, "user", message.content, message.author.username ); // Split and send response const chunks = splitMessage(response); let firstMsgId: string | null = null; for (const chunk of chunks) { const sent = await channel.send(sanitizeForDiscord(chunk, { home: os.homedir() })); if (!firstMsgId) { firstMsgId = sent.id; } } // Store assistant response (use first message id for dedup) if (firstMsgId) { db.addConversation( workspace.id, firstMsgId, "assistant", response, null ); } } catch (error) { const msg = error instanceof Error ? error.message : "Unbekannter Fehler"; const safeMsg = sanitizeForDiscord(`Fehler bei der Verarbeitung: ${msg}`, { home: os.homedir() }); await channel.send(safeMsg).catch(() => {}); } finally { clearInterval(typingInterval); } return true; }