disclaw/src/router.ts
Nick Tabeling 661cf12549 feat(DIS-106): RunResult discriminated union replaces Promise<string>
Add RunResult type in src/agent/types.ts (success|timeout|cli-error|parse-error).
Runner resolves instead of rejects for all error cases.
Router handles all four status variants explicitly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 10:25:21 +02:00

110 lines
3.2 KiB
TypeScript

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 { splitForDiscord } from "./discord/split";
import { sanitizeForDiscord } from "./runtime/sanitize";
import { ChannelQueue } from "./runtime/channel-queue";
import type { RunResult } from "./agent/types";
const channelQueue = new ChannelQueue();
/**
* 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<boolean> {
const channelId = message.channelId;
if (channelId === managementChannelId) {
return false;
}
const workspace = db.getWorkspaceByChannelId(channelId);
if (!workspace) {
return false;
}
const channel = message.channel as TextChannel;
const sanitizeOpts = {
repoRoot: workspace.workspace_path,
home: os.homedir(),
};
await channelQueue.enqueue(channelId, async () => {
const typingInterval = setInterval(() => {
channel.sendTyping().catch(() => {});
}, 5000);
await channel.sendTyping().catch(() => {});
try {
const history = db.getConversationHistory(workspace.id, 30);
const result: RunResult = await runAgent({
workspacePath: workspace.workspace_path,
channelName: channel.name,
userMessage: message.content,
conversationHistory: history,
});
if (result.status === "success") {
db.addConversation(
workspace.id,
message.id,
"user",
message.content,
message.author.username
);
const sanitizedResponse = sanitizeForDiscord(result.text, sanitizeOpts);
const chunks = splitForDiscord(sanitizedResponse);
let firstMsgId: string | null = null;
for (const chunk of chunks) {
const sent = await channel.send(chunk);
if (!firstMsgId) {
firstMsgId = sent.id;
}
}
if (firstMsgId) {
db.addConversation(
workspace.id,
firstMsgId,
"assistant",
sanitizedResponse,
null
);
}
} else if (result.status === "timeout") {
await channel
.send(`⏱️ Agent-Timeout nach ${result.timeoutMs / 1000}s`)
.catch(() => {});
} else if (result.status === "cli-error") {
await channel
.send(sanitizeForDiscord(`❌ CLI-Fehler: ${result.message}`, sanitizeOpts))
.catch(() => {});
} else if (result.status === "parse-error") {
await channel
.send(`⚠️ Antwort konnte nicht gelesen werden`)
.catch(() => {});
}
} catch (error) {
const msg = error instanceof Error ? error.message : "Unbekannter Fehler";
await channel
.send(sanitizeForDiscord(`Fehler bei der Verarbeitung: ${msg}`, sanitizeOpts))
.catch(() => {});
} finally {
clearInterval(typingInterval);
}
});
return true;
}