From 755c847d9a2336355de11f04e80dac269c1531ae Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 17 Jan 2026 17:23:30 +0000 Subject: [PATCH] fix: soften discord interaction logging --- src/discord/monitor/native-command.ts | 231 ++++++++++++++++++-------- src/discord/monitor/provider.ts | 13 +- 2 files changed, 173 insertions(+), 71 deletions(-) diff --git a/src/discord/monitor/native-command.ts b/src/discord/monitor/native-command.ts index c78a116f7..3148c1dfb 100644 --- a/src/discord/monitor/native-command.ts +++ b/src/discord/monitor/native-command.ts @@ -163,6 +163,35 @@ function decodeDiscordCommandArgValue(value: string): string { } } +function isDiscordUnknownInteraction(error: unknown): boolean { + if (!error || typeof error !== "object") return false; + const err = error as { + discordCode?: number; + status?: number; + message?: string; + rawBody?: { code?: number; message?: string }; + }; + if (err.discordCode === 10062 || err.rawBody?.code === 10062) return true; + if (err.status === 404 && /Unknown interaction/i.test(err.message ?? "")) return true; + if (/Unknown interaction/i.test(err.rawBody?.message ?? "")) return true; + return false; +} + +async function safeDiscordInteractionCall( + label: string, + fn: () => Promise, +): Promise { + try { + return await fn(); + } catch (error) { + if (isDiscordUnknownInteraction(error)) { + console.warn(`discord: ${label} skipped (interaction expired)`); + return null; + } + throw error; + } +} + function buildDiscordCommandArgCustomId(params: { command: string; arg: string; @@ -196,6 +225,73 @@ function parseDiscordCommandArgData( }; } +type DiscordCommandArgContext = { + cfg: ReturnType; + discordConfig: DiscordConfig; + accountId: string; + sessionPrefix: string; +}; + +async function handleDiscordCommandArgInteraction( + interaction: ButtonInteraction, + data: ComponentData, + ctx: DiscordCommandArgContext, +) { + const parsed = parseDiscordCommandArgData(data); + if (!parsed) { + await safeDiscordInteractionCall("command arg update", () => + interaction.update({ + content: "Sorry, that selection is no longer available.", + components: [], + }), + ); + return; + } + if (interaction.user?.id && interaction.user.id !== parsed.userId) { + await safeDiscordInteractionCall("command arg ack", () => interaction.acknowledge()); + return; + } + const commandDefinition = + findCommandByNativeName(parsed.command) ?? + listChatCommands().find((entry) => entry.key === parsed.command); + if (!commandDefinition) { + await safeDiscordInteractionCall("command arg update", () => + interaction.update({ + content: "Sorry, that command is no longer available.", + components: [], + }), + ); + return; + } + const updated = await safeDiscordInteractionCall("command arg update", () => + interaction.update({ + content: `✅ Selected ${parsed.value}.`, + components: [], + }), + ); + if (!updated) return; + const commandArgs = createCommandArgsWithValue({ + argName: parsed.arg, + value: parsed.value, + }); + const commandArgsWithRaw: CommandArgs = { + ...commandArgs, + raw: serializeCommandArgs(commandDefinition, commandArgs), + }; + const prompt = buildCommandTextFromArgs(commandDefinition, commandArgsWithRaw); + await dispatchDiscordCommandInteraction({ + interaction, + prompt, + command: commandDefinition, + commandArgs: commandArgsWithRaw, + cfg: ctx.cfg, + discordConfig: ctx.discordConfig, + accountId: ctx.accountId, + sessionPrefix: ctx.sessionPrefix, + preferFollowUp: true, + }); +} + class DiscordCommandArgButton extends Button { label: string; customId: string; @@ -223,55 +319,34 @@ class DiscordCommandArgButton extends Button { } async run(interaction: ButtonInteraction, data: ComponentData) { - const parsed = parseDiscordCommandArgData(data); - if (!parsed) { - await interaction.update({ - content: "Sorry, that selection is no longer available.", - components: [], - }); - return; - } - if (interaction.user?.id && interaction.user.id !== parsed.userId) { - await interaction.acknowledge(); - return; - } - const commandDefinition = - findCommandByNativeName(parsed.command) ?? - listChatCommands().find((entry) => entry.key === parsed.command); - if (!commandDefinition) { - await interaction.update({ - content: "Sorry, that command is no longer available.", - components: [], - }); - return; - } - await interaction.update({ - content: `✅ Selected ${parsed.value}.`, - components: [], - }); - const commandArgs = createCommandArgsWithValue({ - argName: parsed.arg, - value: parsed.value, - }); - const commandArgsWithRaw: CommandArgs = { - ...commandArgs, - raw: serializeCommandArgs(commandDefinition, commandArgs), - }; - const prompt = buildCommandTextFromArgs(commandDefinition, commandArgsWithRaw); - await dispatchDiscordCommandInteraction({ - interaction, - prompt, - command: commandDefinition, - commandArgs: commandArgsWithRaw, + await handleDiscordCommandArgInteraction(interaction, data, { cfg: this.cfg, discordConfig: this.discordConfig, accountId: this.accountId, sessionPrefix: this.sessionPrefix, - preferFollowUp: true, }); } } +class DiscordCommandArgFallbackButton extends Button { + label = "cmdarg"; + customId = "cmdarg:seed=1"; + private ctx: DiscordCommandArgContext; + + constructor(ctx: DiscordCommandArgContext) { + super(); + this.ctx = ctx; + } + + async run(interaction: ButtonInteraction, data: ComponentData) { + await handleDiscordCommandArgInteraction(interaction, data, this.ctx); + } +} + +export function createDiscordCommandArgFallbackButton(params: DiscordCommandArgContext): Button { + return new DiscordCommandArgFallbackButton(params); +} + function buildDiscordCommandArgMenu(params: { command: ChatCommandDefinition; menu: { arg: CommandArgDefinition; choices: string[]; title?: string }; @@ -408,11 +483,13 @@ async function dispatchDiscordCommandInteraction(params: { content, ...(options?.ephemeral !== undefined ? { ephemeral: options.ephemeral } : {}), }; - if (preferFollowUp) { - await interaction.followUp(payload); - return; - } - await interaction.reply(payload); + await safeDiscordInteractionCall("interaction reply", async () => { + if (preferFollowUp) { + await interaction.followUp(payload); + return; + } + await interaction.reply(payload); + }); }; const useAccessGroups = cfg.commands?.useAccessGroups !== false; @@ -567,18 +644,22 @@ async function dispatchDiscordCommandInteraction(params: { sessionPrefix, }); if (preferFollowUp) { - await interaction.followUp({ + await safeDiscordInteractionCall("interaction follow-up", () => + interaction.followUp({ + content: menuPayload.content, + components: menuPayload.components, + ephemeral: true, + }), + ); + return; + } + await safeDiscordInteractionCall("interaction reply", () => + interaction.reply({ content: menuPayload.content, components: menuPayload.components, ephemeral: true, - }); - return; - } - await interaction.reply({ - content: menuPayload.content, - components: menuPayload.components, - ephemeral: true, - }); + }), + ); return; } @@ -646,15 +727,23 @@ async function dispatchDiscordCommandInteraction(params: { responsePrefix: resolveEffectiveMessagesConfig(cfg, route.agentId).responsePrefix, humanDelay: resolveHumanDelayConfig(cfg, route.agentId), deliver: async (payload) => { - await deliverDiscordInteractionReply({ - interaction, - payload, - textLimit: resolveTextChunkLimit(cfg, "discord", accountId, { - fallbackLimit: 2000, - }), - maxLinesPerMessage: discordConfig?.maxLinesPerMessage, - preferFollowUp: preferFollowUp || didReply, - }); + try { + await deliverDiscordInteractionReply({ + interaction, + payload, + textLimit: resolveTextChunkLimit(cfg, "discord", accountId, { + fallbackLimit: 2000, + }), + maxLinesPerMessage: discordConfig?.maxLinesPerMessage, + preferFollowUp: preferFollowUp || didReply, + }); + } catch (error) { + if (isDiscordUnknownInteraction(error)) { + console.warn("discord: interaction reply skipped (interaction expired)"); + return; + } + throw error; + } didReply = true; }, onError: (err, info) => { @@ -697,13 +786,15 @@ async function deliverDiscordInteractionReply(params: { }), } : { content }; - if (!preferFollowUp && !hasReplied) { - await interaction.reply(payload); + await safeDiscordInteractionCall("interaction send", async () => { + if (!preferFollowUp && !hasReplied) { + await interaction.reply(payload); + hasReplied = true; + return; + } + await interaction.followUp(payload); hasReplied = true; - return; - } - await interaction.followUp(payload); - hasReplied = true; + }); }; if (mediaList.length > 0) { diff --git a/src/discord/monitor/provider.ts b/src/discord/monitor/provider.ts index c4c329c15..b6ec2b2be 100644 --- a/src/discord/monitor/provider.ts +++ b/src/discord/monitor/provider.ts @@ -27,7 +27,10 @@ import { registerDiscordListener, } from "./listeners.js"; import { createDiscordMessageHandler } from "./message-handler.js"; -import { createDiscordNativeCommand } from "./native-command.js"; +import { + createDiscordCommandArgFallbackButton, + createDiscordNativeCommand, +} from "./native-command.js"; export type MonitorDiscordOpts = { token?: string; @@ -149,6 +152,14 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) { { commands, listeners: [], + components: [ + createDiscordCommandArgFallbackButton({ + cfg, + discordConfig: discordCfg, + accountId: account.accountId, + sessionPrefix, + }), + ], }, [ new GatewayPlugin({