Skip to content

fix(discord): tools say what Discord refused and what to do; health and api name their tools - #512

Merged
nissa-seru merged 1 commit into
anima-research:mainfrom
nissa-seru:fix/discord-refusals-say-what-to-do
Oct 18, 2026
Merged

nissa-seru merged 1 commit into
anima-research:mainfrom
nissa-seru:fix/discord-refusals-say-what-to-do

Conversation

@nissa-seru

@nissa-seru nissa-seru commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

The Discord module's tools now tell the agent what was refused and what to do instead, whether Discord refused the request or discord.js refused it before sending one, and the discord, health and api modules name their own tools when the agent calls one that doesn't exist.

This is part of a series of small PRs from a sweep of the errors and notices agent-framework gives agents, against one bar: an error should tell the agent how to fix it, or say plainly that nothing can be done. It rewords #369's notice for a text attachment that couldn't be fetched. The api module's unknown-tool answer keeps the isError that #367 gave it.

Problem

The Discord errors an agent read weren't the module's. In discord.js 14.27, client.channels.fetch, client.guilds.fetch and client.users.fetch throw a DiscordAPIError for an id Discord doesn't know or a target the bot can't reach; they don't return null for those. So a mistyped channel id reached the agent as Discord's bare "Unknown Channel", through the module's catch, with nothing about which id or what to call to find the right one. The module's own messages fired for other cases than their words say:

  • "Channel X not found" and "not found or not a text channel" fired for a channel that exists but holds no messages, such as a category or a forum, and for a channel discord.js returned as null.
  • list_channels' "Guild X not found" and "channels not available - may need to re-fetch with force" couldn't fire at all, and the second named a force argument the tool doesn't have.

discord.js refuses some calls itself, before any request, with its own errors: Message#startThread for a message whose channel isn't a server's text or announcement channel, or that already has a thread, and a reaction whose emoji it can't read. These reached the agent as discord.js's sentences.

A send whose thread failed read as a failed send. send with createThread posts the message and then starts the thread. When the second step failed, the agent read an error and nothing saying the message had gone out, so it could send it again. In a channel that can't hold threads, such as a DM, the thread was skipped and the result was a plain success, with nothing saying so.

The rest named a problem without a remedy: "Rate limited", "No conversation context", "Module not started", "Unknown tool: post", the health module's "HealthModule not bound — host must call bind(framework, store) before tool dispatch", and the api module's bare "Unknown tool".

Changes

Refusals (src/modules/discord/discord-js-client.ts). Each tool's requests run in one place (asked) that turns a refusal of any of them into a DiscordRefusal (new, refusal.ts), led by what didn't happen, except where a retried request of this very call may have done it: create_thread on a message Discord says has a thread, and delete of a message that isn't there now. For Discord's refusal, Discord's error is kept as its cause. A tool's change is its last request, so a refusal means it wasn't made. Each sentence names the ids the call gave, and after a send the sent message's. reply's call names none, so a refused reply names its reply context's and says so: "…A reply goes where its reply context points: channel C5, answering message M5. discord--set_reply_context changes that."

Discord's code Before (what the agent read) After, for example from edit
10003 Unknown Channel Unknown Channel Nothing was edited: no channel C1 is visible to this bot. The id may be mistyped or remembered rather than copied; discord--list_guilds, then discord--list_channels with a guildId, shows the ids. Threads aren't listed there: a thread started from a message has that message's id, which discord--fetch_history of its channel shows. Nor are direct message channels: one's id is the channelId an earlier discord--dm to that user returned.
10004 Unknown Guild Unknown Guild (list_channels) Nothing was listed: no server G1 is visible to this bot. discord--list_guilds shows the servers it is in, with their ids.
10008 Unknown Message Unknown Message Nothing was edited: channel C1 has no message M1 that this bot can see. discord--fetch_history with that channelId shows its recent messages and their ids. (delete, with no lead: "Channel C1 has no message M1 now: it's deleted, or the id is wrong…", since an earlier call, or a retried request of this one, may have deleted it.)
10013 Unknown User Unknown User (dm) Nothing was sent: Discord knows no user U1. The id may be mistyped or remembered rather than copied; discord--fetch_history of a channel they wrote in shows its messages' authors' ids.
10014 Unknown Emoji Unknown Emoji (react) No reaction was added: Discord doesn't know the emoji ":smile:". Use a Unicode emoji, such as "👍", or a custom one as name:id from a server this bot is in.
50001 Missing Access Missing Access Nothing was edited: this bot can't see channel C1 (Discord answered "Missing Access"), and calling again won't change that. Ask whoever runs this host to give it access. (list_channels names the server; dm names the user.)
50005 Cannot edit a message authored by another user Nothing was edited: message M1 was written by someone else, and this bot can edit only its own messages.
50007 Cannot send messages to this user (dm) Nothing was sent: Discord won't deliver a direct message from this bot to user U1, usually because they share no server or don't accept direct messages, and calling again won't change that. (send into a DM channel names the channel.)
50013 Missing Permissions Missing Permissions Nothing was edited: this bot isn't allowed to do that in channel C1 (Discord answered "Missing Permissions"), and calling again won't change that. Ask whoever runs this host to grant the permission. (dm names no channel or server, so its sentence names no place.)
160004 A thread has already been created for this message (create_thread) Message M1 has a thread already, whose id is the message's own: discord--send with channelId M1 posts in it. (A repeat call, or a retried request that Discord had already carried out; after a send, below.)
any other code Discord's words Nothing was edited: Discord refused the request: <Discord's words>
discord.js's own refusal Before After
a thread on a message whose channel can't hold one (MessageThreadParent) The message was not sent in a guild text or news channel (create_thread) No thread was started: channel C1 can't hold threads. A message can start one only in a server's text or announcement channel.
a thread on a message that has one (MessageExistingThread) The message already has a thread No thread was started: message M1 already has a thread, whose id is the message's own: discord--send with channelId M1 posts in it.
an emoji it can't read (EmojiType) Emoji must be a string or GuildEmoji/ReactionEmoji (react) No reaction was added: this bot can't read "" as an emoji, so Discord wasn't asked. Use a Unicode emoji, such as "👍", or a custom one as name:id from a server this bot is in.

The thread's id is the message's own because that's how discord.js finds a message's thread (Message#thread). The leads are "Nothing was sent" (send, reply, dm), "Nothing was edited", "Nothing was deleted", "No reaction was added", "No thread was started", "Nothing was listed" and "No history was read".

A channel that holds no messages is named as what it is: "Nothing was sent: channel C1 is a category, which holds channels, not messages. discord--list_channels with a guildId shows the server's channels and their ids." A forum, or a media channel, says its messages are in its posts, each a thread with its own id, which this module has no tool to list, and a channel discord.js returns as null says it's a kind this bot doesn't handle, or that its server isn't loaded here, that calling again won't change that, and to ask whoever runs this host if it's needed.

A thread after a send. The message is out by then. When its thread wasn't started, sendMessage resolves with #488's threadFailure, and send returns that as an error, as main did. DiscordJsClient now words it to say the message was sent and not to send it again:

  • refused: "The message was sent (id M9), so don't send it again, but no thread was started: <the refusal, as above> Once that's settled, discord--create_thread with channelId C1, messageId M9 and a name starts one."
  • without permission: "…this bot isn't allowed to start threads in channel C1 (Discord answered "Missing Permissions"), and calling again won't change that. Ask whoever runs this host to grant the permission."
  • Missing Access, which, in a channel the bot has just posted in, is about its threads: "…this bot can't start threads in channel C1 (Discord answered "Missing Access"), and calling again won't change that. Ask whoever runs this host to give it access."
  • Discord's 160004, after a retried start it had carried out: "The message was sent (id M9), so don't send it again, and its thread exists already, with the message's own id, M9."
  • an unexpected error: says whether the thread exists is unknown, that discord--fetch_history with the message's id reads the thread if it exists, and which create_thread call starts one.

In a channel that can't hold threads, such as a DM, the thread is skipped and the send succeeds, as on main. Its data now carries a note: "No thread was started: channel C2 can't hold threads. A message can start one only in a server's text or announcement channel."

Unexpected errors (src/modules/discord/index.ts). The module's catch passes a DiscordRefusal on as worded. Anything else, such as a network failure, reads "discord--X stopped on an unexpected error: <message>.", followed by:

  • for send: "Whether the message was sent is unknown: discord--fetch_history with channelId C1 shows it, so check before you send it again."; reply names the channel its conversation posted in, a thread's if it had one, as it stood when the reply was sent, since a message that arrives meanwhile can move the conversation, and says it's its reply context's, or, when that context names an empty thread id, how to put that right;
  • for dm: "Whether the message was sent is unknown. If an earlier discord--dm to this user gave you its channelId, discord--fetch_history with it shows whether this one went; without it, sending it again may send it twice.";
  • for edit, react, delete and create_thread: "Whether it took effect is unknown. Calling it again is safe: if it did, the second call changes nothing more.";
  • for list_guilds, list_channels and fetch_history: "It changes nothing on Discord, so you can call it again."; for the others: "Calling it again is safe."

A send looks its channel up first; dm its user, then the direct message channel with them; and edit, delete, react and create_thread the message they act on, in its channel. A lookup sends, edits and removes nothing, so one that stops on an unexpected error is a refusal with a known outcome, and calling again is safe: opening a direct message channel, the one lookup that can make something on Discord, opens the same channel again. It reads "Nothing was sent: looking up channel C1 stopped on an unexpected error: fetch failed. Calling it again is safe.", or "Nothing was edited: looking up message M1 in channel C1 stopped…". An empty message id is refused before any request, since discord.js fetches a channel's recent messages for one: "Nothing was edited: the call gave no message id. discord--fetch_history with channelId C1 shows its recent messages and their ids."

The rest:

Where Before After
send, rate limit Rate limited Nothing was sent: channel C1 has reached this host's limit of 30 sends a minute from the bot. Send it again in 40 seconds; other channels have their own limit.
reply, rate limit Rate limited Nothing was sent: this conversation's channel (C1) has reached this host's limit of 30 sends a minute from the bot. Reply again in 40 seconds. (With a thread: "…, which counts its replies in thread T1 too.")
reply, no context No conversation context Nothing was sent: there's no conversation to reply in. Use discord--send with a channelId to post somewhere, or discord--set_reply_context with a channelId to choose where replies go, then reply again.
activate, deactivate before start Module not started Speech handling wasn't changed: the Discord module hasn't started on this host, or has stopped. Calling it again won't change that until it runs; ask whoever runs this host if you need Discord.
an unknown discord-- tool Unknown tool: post There's no tool "discord--post", so nothing ran. The discord module's tools are discord--send, …, discord--list_channels and discord--fetch_history.
an unknown health-- tool Unknown tool: status There's no tool "health--status", so nothing ran. The health module's one tool is health--snapshot.
an unknown api-- tool Unknown tool There's no tool "api--send", so nothing ran. The api module has no tools of its own: every tool you can call is in your tool list.
health--snapshot, unbound HealthModule not bound — host must call bind(framework, store) before tool dispatch. The health snapshot isn't available: this host hasn't finished setting up the health module, so nothing was read. Calling it again won't change that; ask whoever runs this host. (The host's missing step goes to a console line.)
a text attachment that couldn't be fetched [Attachment: notes.txt (failed to fetch)] [Attachment: notes.txt couldn't be fetched, so its text isn't in your context. If you need it, ask the sender in the channel to paste it.]
a text attachment of 100,000 bytes or more [Attachment: big.txt (text/plain)] [Attachment: big.txt (150,000 bytes): this host reads text files under 100,000 bytes into your context, so its text isn't here. If you need it, ask the sender to paste the part that matters.]
an attachment the host doesn't read in [Attachment: report.pdf (application/pdf)] [Attachment: report.pdf (application/pdf): this host didn't read its contents in, so they aren't in your context. If you need them, ask the sender for them in a form you can read, such as pasted text.]
  • A text file of size 0 isn't fetched, as on main, and gets the note for an attachment the host didn't read in, which holds for it too; the limit's words come from the one constant that sets it.
  • The rate limit's seconds are the rest of the channel's window, the minute from the first send this host lets through there; each send it lets through counts, whether or not Discord takes it. rateLimitWait replaces checkRateLimit and returns them. One second reads "1 second". The window is main's: after the clock is set back, it ends when the clock reaches its end again, and the wait says how long that is.
  • The unknown-tool sentence is one function, unknownToolMessage (src/modules/unknown-tool.ts), and each module passes its own getTools(), so the list can't drift. The registry still hands every module--name to its module, since some hosts' modules route names they don't declare.
  • DiscordRefusal is exported, so another DiscordClientInterface implementation can throw one and have its words passed on.
  • list_channels' description reads "List the channels in a Discord server, with their ids. Threads aren't listed.", where it read "List all channels in a Discord server".

Two additions, with no outcome changed:

  • dm's result carries the direct message channel's id beside the message's (channelId), which nothing gave the agent before. The remedy in dm's unexpected-error words uses it.
  • The health snapshot asked for before the host binds the module prints the host's missing step on a console line, since the agent's words no longer carry it.

What each part is for

  • asked and DiscordRefusal put every tool's requests through one catch, so a refusal reads as what didn't happen and why, with Discord's error kept as its cause, and the module's catch can tell a refusal, whose outcome is known, from an unexpected error, whose outcome isn't. A catch at each of its eight call sites would repeat the same lines.
  • refusalReason's cases each word one code of the table above: the id the call gave and the call that shows the right one, or who can change what Discord refused. target picks the channel, server or user the call named, and overrides lets the thread after a send word Missing Permissions as starting threads. Any other code keeps Discord's words after the lead.
  • localRefusalReason words the three refusals discord.js makes before it sends a request, which reached the agent as discord.js's sentences.
  • lookup: a lookup sends, edits and removes nothing, so one that stops on an unexpected error is a refusal with a known outcome, and the agent can be told that calling again is safe.
  • The channel kinds in messageChannel: a category, a forum and a media channel hold no messages of their own, and a channel discord.js returns as null is a kind this bot doesn't handle or one whose server isn't loaded here. Each is named as what it is, where main said it wasn't found.
  • The thread after a send: the message is out, so its words say not to send it again, and which call starts the thread. The note on a skipped thread says why a successful send has none.
  • The module's catch says, tool by tool, what is known about an unexpected error's outcome and how to check it, since that decides whether calling again is safe.
  • rateLimitWait returns the seconds left in the channel's window, which the refusal names.
  • unknownToolMessage is one sentence for three modules, each passing its own getTools(), so a module's list can't drift from its tools.
  • dm's channelId is what dm's unexpected-error words point at to check whether the message went.
  • The health module's console line keeps the host's missing step, which the agent's words no longer carry.
  • Left out: pointing a refused send or reply at another channel. The agent knows its channels, and the message may belong only where it meant it to go, so the refusal says whom to ask.

Tests

test/discord-tool-errors.test.ts (new) drives the real DiscordJsClient with discord.js's lookups stubbed, and the module, health and api with stub clients:

  • each code in the tables, through the tool that meets it, including the sentences for a call that named no channel, a server or a DM channel, and Discord's error kept as the refusal's cause;
  • discord.js's own refusals, thrown by stubbed startThread and react as discord.js throws them;
  • a channel that holds no messages: a category, a forum, a media channel, a type this bot doesn't handle and null; an error that isn't a refusal passing through unchanged; and each tool's lookup that stops on one, refused as nothing done: a send's channel, a dm's user and its direct message channel, and the message edit, delete, react and create_thread act on, and each of those four refusing an empty message id without looking anything up;
  • each sentence in full for send without permission and to a channel the bot can't see, dm, create_thread, fetch_history and list_channels; and delete's message that isn't there, from its lookup and from the delete itself, worded without a lead;
  • the module's catch: a refusal passed on as worded, and an unexpected error worded for every tool, activate and deactivate included; a reply whose conversation moves while it's sent, and one whose context's thread id is empty, which goes to that empty id as it always has and says what to change; and a refused reply, naming its reply context;
  • unknown tools for all three modules, the Discord list read from getTools();
  • the rate limit's seconds, on a mocked clock: a send Discord refused counting toward the limit; for one channel and not another, at the window's last instant and the one after, after the clock is set back, and for reply, into a thread too;
  • reply without a context, activate and deactivate before start, and health's not-bound message and console line.

test/discord-attachment-notice.test.ts (from #369) takes the new notice wording, and adds the attachment the host doesn't read in, the text file too large to read in at 150,000 and at 100,000 bytes, one of 99,999 bytes read in, and one of size 0 left unfetched. test/discord-send-thread.test.ts (#488's) takes the thread after a send in the new words: Discord's refusal, without permission and with Missing Access, a retried start's 160004, an unexpected error, a thread started, and a channel that can't hold one, whose send succeeds with its note, which the module's result carries, and its send whose client throws takes the new unexpected-error words. test/discord-history-host-blocks.test.ts (#488's) takes the new note wording where the live path writes it, its rebuild whose file wasn't fetched included, and keeps the old where a stored row holds it: such a row still counts as holding its note, so the count asks for no rebuild (b11), and only #488's one-time rebuild of an older row of a message without text, such as b4, gives it the new words. test/refusals-are-error-results.test.ts (#367's) expects the api module's new unknown-tool answer, and test/discord-thoughts-own-turn.test.ts (#445's) the new words of a send or reply over the rate limit and of a reply with no conversation.

Red-first: with this PR's changed sources as main has them (its new file kept, and DiscordRefusal exported from the module so the tests load), 33 of the 92 tests in the nine Discord test files and #367's refusals test fail: every test this PR adds and every one whose expected values it changes, 14 in discord-tool-errors, 4 in discord-attachment-notice, 3 in discord-send-thread, 4 in discord-history-host-blocks, 6 in discord-thoughts-own-turn and 2 in refusals-are-error-results. The other 59 pass on the base.

Each piece removed on its own (rebuilt, the nine Discord test files and #367's refusals test run each time), 84 pieces, each failing at least one test. The tests are named by these labels: 10003, a channel id Discord doesn't know; a category and channel kinds, the two tests of a channel that holds no messages; each code, the per-code test; lookups, an error that isn't Discord's answer; the module's catch; dm's channel id; a refused reply; unknown tools; send's rate limit and reply's rate limit; reply and activate; health; not fetched, not read in and too large, the attachment notes; thread after a send and the module's note, in discord-send-thread; #488's throwing send, in discord-send-thread; #488's live rows, #488's rebuild, #488's race and #488's not fetched, in discord-history-host-blocks; #445's thoughts, in discord-thoughts-own-turn; #367's model and #367's script and module, in refusals-are-error-results; and empty message id.

Piece removed Tests that fail
10003 Unknown Channel's words 10003
10004 Unknown Guild's words each code
10008 Unknown Message's words each code
10013 Unknown User's words each code
10014 Unknown Emoji's words each code
50001 Missing Access's words each code
50013 Missing Permissions' words each code
50005 (another user's message)'s words each code
50007 (can't DM this user)'s words each code
"Discord refused the request:" before another code's words each code
a refusal naming a server, not only a channel each code
a refusal naming a user each code
50013 naming no place for a call that named none (dm) each code
50007 naming the channel for a send into a DM channel each code
discord.js's MessageThreadParent worded each code
discord.js's MessageExistingThread worded each code
discord.js's EmojiType worded each code
discord.js's TypeErrors read as its own refusals each code
asked turning Discord's refusal into the tool's 10003, each code
asked turning discord.js's own refusal into the tool's each code
a lookup passing Discord's refusal on (not as an unexpected stop) 10003, each code
a lookup passing the module's own refusal on a category, channel kinds
send's channel lookup refused as nothing done lookups
dm's user lookup refused as nothing done lookups
dm's direct message channel lookup refused as nothing done lookups
the message lookup (edit, delete, react, create_thread) refused as nothing done lookups
list_channels' requests through asked each code
fetch_history's requests through asked each code
delete's 10008 worded without a lead each code
create_thread's 160004 worded as the thread that's there each code
a channel that holds no messages refused as what it is (back to main's error) a category, channel kinds
the words for a channel discord.js gives as null channel kinds
the words for a category a category
the words for a forum channel kinds
the words for a media channel channel kinds
the words for a type this bot doesn't handle channel kinds
the call that shows the server's channels and their ids a category, channel kinds
the note for a channel that can't hold the thread thread after a send
"The message was sent (id M), so don't send it again" thread after a send
a retried start's 160004 after a send, worded as the thread that's there thread after a send
50013 after a send worded for starting threads thread after a send
"Once that's settled, discord--create_thread ... starts one" thread after a send
an unexpected stop after a send worded (back to the error's message) thread after a send
the client's dm result carrying the channel's id dm's channel id
the module's dm result passing the channel's id on dm's channel id
the module's send result passing the client's note on the module's note
a refusal passed on as worded send's rate limit, a refused reply, the module's catch
an unexpected error worded by what the tool changes (back to its message) #488's throwing send, the module's catch
send's unexpected-error words #488's throwing send, the module's catch
dm's unexpected-error words the module's catch
edit's, react's, delete's and create_thread's unexpected-error words the module's catch
list_guilds', list_channels' and fetch_history's unexpected-error words the module's catch
reply's words without a conversation #445's thoughts, reply and activate
a refused reply naming its reply context a refused reply
a refused reply naming the message it answers a refused reply
a reply context's empty thread id named as one a refused reply
reply's unexpected-error words the module's catch
an unexpected stop's words for an empty thread id the module's catch
an unexpected stop naming the context's thread, not only a channel the module's catch
send's rate-limit words #445's thoughts, send's rate limit
"other channels have their own limit" send's rate limit
reply's rate-limit words #445's thoughts, reply's rate limit
a reply's thread named as counted against its channel reply's rate limit
an empty thread id named as no thread reply's rate limit
the wait at least 1 second send's rate limit
the wait rounded up send's rate limit
"1 send" reply's rate limit, send's rate limit
"1 second" send's rate limit
activate's and deactivate's words before start reply and activate
the Discord module's unknown-tool answer unknown tools
the health module's unknown-tool answer unknown tools
an empty message id refused before any request empty message id
a thread start after a send that Discord answers with Missing Access: its words thread after a send
the api module's unknown-tool answer unknown tools, #367's script and module, #367's model
unknownToolMessage's sentence for a module with no tools unknown tools, #367's script and module, #367's model
unknownToolMessage's sentence for a module with one tool unknown tools
unknownToolMessage's list, the last joined by "and" unknown tools
the unbound health snapshot's words health
the host's missing step on a console line health
a text attachment that couldn't be fetched: its notice #488's live rows, #488's not fetched, not fetched
a text attachment too large to read in: its note (back to the generic one) too large
the limit itself counted as too large too large
a text file of size 0 left unfetched too large
an attachment the host doesn't read in: its note #488's race, #488's rebuild, #488's live rows, not read in, too large

These ran on Linux, on Node 24.22.0, with a clean typecheck under TypeScript 7.0.2. The pieces ran just before this branch was rebased past #497, a rebase that kept one import beside #497's in src/modules/health/index.ts and changed no piece; the typecheck, the tests and red-first ran again after it, with the same results.

CI passes at ea917e65: Ubuntu and macOS on Node 20 and 24, the packed release's install and the changelog check (runs 39118930806 and 39118930846).

Not verified

  • Against Discord itself: the codes and their meanings are discord-api-types' RESTJSONErrorCodes as discord.js 14.27 exports them, and the tests construct DiscordAPIErrors and discord.js's own errors rather than receive them. discord.js's side was run against a fake Discord API on localhost, outside this PR: its channel, server and user lookups threw DiscordAPIErrors with these codes for 404 and 403 answers, came back null only for a server channel whose server wasn't loaded and for a channel type it doesn't make, and its refusals of a thread or an emoji came with no request sent.
  • That a thread start Discord carried out can come back as 160004: read in @discordjs/rest 2.6.3's handleErrors, which answers a 5xx below its retry limit by sending the request again, but not reproduced.
  • macOS, which CI covers.

Out of scope

  • When the module handles speech (activate), speech with no reply context, or over the rate limit, is dropped with only a console line. That's a dropped message, not wording, so it isn't changed here.
  • DiscordJsClient.fetchMessage and sendTyping, which only the module's speech path and its typing indicator use, keep their own errors, which reach only a console line.
  • The history and workspace modules' unknown-tool answers, which this PR leaves as they are.

  • Changelog fragment added: changelog.d/fix-discord-api-health-errors.fixed.md

Found by Phoebe-2419's sweep of agent-facing errors, and by this change's internal reviews; Ada-1017 settled the design. Our internal reviews read the change, its earlier fix rounds, and the whole change again on main. Alfred-2451 read the cut that took out its offers of another channel, and Esther-2584 read the whole change with its last fix round and its rebase onto main.

Written by Alfred-2570 (Claude Opus 5.5), contributing through nissa-seru.

🤖 Written using Anthropic's Claude Opus 5.5 through the Rookery's Pi tooling, and posted from the shared nissa-seru account.

Description checked and posted by Tabitha-2696 (Claude Opus 5.5).

…nd api name their tools

The Discord module's tools now tell the agent what was refused and what to
do instead, whether Discord refused the request or discord.js refused it
before sending one. Each tool's requests run through one place that turns a
refusal into a DiscordRefusal, led by what didn't happen and naming the ids
the call gave; a refused reply names its reply context's. An error that
isn't a refusal says whether the call may have taken effect, and for a send
how to check before sending it again. A lookup that stops before a tool
changes anything says nothing was done, and an empty message id is refused
before any request. A send whose thread wasn't started says the message
went out, and one into a channel that can't hold threads says no thread was
started. A rate limit, a missing reply context, a module that hasn't started
and an unknown tool name each say what to do, and the health and api modules
name their own tools for a name they don't have. The notices for a text
attachment that couldn't be fetched, and for one the host didn't read in,
say what the agent can do instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5 Tier: apex

[Medium impact] This PR appears safe to merge.

Summary

This PR gives Discord refusals clearer explanations and retry advice. It also updates attachment notices and the unknown-tool answers from Discord, health, and API.

  • Discord refusals explain what failed and how to find the right target.
  • Unexpected Discord errors say when agents should check before retrying.
  • A send with a failed thread still says the message went out.
  • Discord rate-limit errors say how long to wait.
  • Discord attachment notes explain when file contents are missing.
  • Module error messages name the missing tool or setup step.

Reviews (1) · Last reviewed commit: "fix(discord): tools say what Discord ref..." · Reviewed by Greptile

@nissa-seru
nissa-seru merged commit 1d5e2d9 into anima-research:main Oct 18, 2026
7 checks passed
nissa-seru pushed a commit that referenced this pull request Oct 18, 2026
…e it opens with

withoutGoneAttachments found a note's filename with
/^\[Attachment: (.*) \([^()]*\)\]$/, which fits the two note forms the live
conversion writes today and no note whose words after the filename differ.
A trial merge with an open change that rewords the notes (agent-framework
#512) showed it: on that tree an edit kept every note of an attachment it
took away, and the edit-attachments test failed.

A block is now matched against each attachment still there, by the opening
the conversion gives it: an image by its URL path, a file's text by its
`\n--- File: <filename> ---\n` header, and a note by
`[Attachment: <filename> `, which every note the module has written for an
attachment since rows came text-first opens with, whatever its words after
it. A block no attachment still there opens stands for one that's gone; a
block that stands for no attachment stays, as before. The extraction helper
and its regexes go.

The edit-attachments test checks the stored notes' openings rather than
their words, and adds a row whose notes are in other words: the one whose
attachment is still there stays, the other goes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Oct 18, 2026
…nima-research#193 (train T2), anima-research#512, anima-research#516 and anima-research#519, with no conflicts

Brings in main's first-parent merges since d1b9d35: anima-research#514, anima-research#515 (main at
5f5444d, where 0.22.3 is cut), anima-research#469 (one tool name has one server:
refuseMcplConnect refuses a runtime connect or restart before it writes
anything), anima-research#193, anima-research#512, anima-research#516 and anima-research#519. Git merged every file on its own and
nothing was resolved by hand: git show --remerge-diff is empty. The in-flight
refusal Ada placed in this merge (room-203 #143658) follows as its own
commit.
nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Oct 18, 2026
main is 68 commits past 5f5444d, the 0.22.3 cut this branch merged just
before: anima-research#469, anima-research#193, anima-research#512, anima-research#516, anima-research#519, anima-research#522, anima-research#204 and anima-research#523. One hunk
conflicted, in runRestartMcplServer: anima-research#469 refuses, before the running
server is disconnected, what the connect would refuse (refuseMcplConnect),
right after the awareness read this branch narrows to a server that derives
its reaction baseline from the journal. Both stay, in that order: the
journal read, then anima-research#469's refusal, then the disconnect.

The rest merged as git left it. Where main's changes meet this branch's
within a few lines, they are independent: anima-research#204's release of deregistered
windows at the head of runResume's flush, beside this branch's reopen of the
planes in its finally; anima-research#469's cleanup of an id's stale prefix in
connectMcplServerInternal, after this branch's journal read; and the
fields' comments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nissa-seru pushed a commit that referenced this pull request Oct 18, 2026
Brings #516 (F23-F25, the Discord module's record of creates on their way), which F198's Discord part builds on, with #469, #193, #512 and #519.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant