feat(commands): publish the declared set, so a slash command exists #886

Merged
coilysiren merged 1 commit from aos/claude/hs68-prompt-commands into main 2026-08-17 04:23:13 +00:00
Member

Building #127's remaining half turned up something bigger than the half: the registration Discord needs was missing for every command, not just prompts.

The finding

A slash command has three parts. This repository had two.

  • discordCommands() renders the declared set. Its only caller was a test.
  • onInteraction is wired when SIRENS_ECHO_DISCORD_COMMANDS is on.
  • Nothing ever called ApplicationCommandBulkOverwrite, or any other registration API.

So /echo, /job-status, /job-cancel and /mcps were unreachable even with the switch on. Discord shows a member no command nobody published, so the handler sat waiting for interactions that could not arrive. Declared, validated, tested, handled, and invisible.

That reframes what #127 was blocked on. The issue reads as "prompt commands are not built"; the truth was that the command surface underneath them had never been published.

What this ships

Registration on discord.ready, the first point the application id exists, behind the same default-false switch.

Per guild, not globally. A global command appears in every guild the bot is in, including ones the access policy refuses. Those would be visible, invocable, and answered not permitted here - advertising a summon path this deployment does not offer, which is the property docs/access-policy.reference.yaml exists to keep honest. A deployment admitting no guild registers nowhere and says so rather than falling back to global.

A bulk overwrite, so the published set is exactly the declared set. A command removed from JobCommands() disappears rather than lingering as an invocable ghost whose handler is gone.

A guild that refuses does not stop the others. Logged as discord.commands.failed, loop continues, first error returned at the end. A partial surface beats none, and swallowing it would be the silent-inert shape this repo keeps finding.

Six tests: the whole declared set reaching every admitted guild by name rather than by count, the two refusals, partial failure, the deployment gate, and the admitted set coming from the access policy.

What this does not ship, and why it is a separate issue

Prompt promotion, filed as #884 with consult.

The pieces exist - CommandFromPrompt maps the shape, MCPProvider.Prompt fetches, seedFromPrompt folds it into a turn. Assembling them is not the hard part. The hard part is one nothing on #127 had named:

An interaction must be answered in three seconds, and a model turn takes minutes. respondToCommand answers immediately. Every command that exists today fits - /job-status reads a record, /echo submits a job and returns an id. A prompt command is the first whose whole purpose is a model reply, and on #577's own numbers Echo's round 0 alone ranges 133.5s to 255.4s.

Two shapes - defer the interaction for 15 minutes, or submit a job - and choosing between them is a product call about what a slash command should feel like. That is why #884 is consult rather than something I should have picked while writing a PR.

Also on #884: whether prompts/list_changed re-registers (a Discord write on a server's schedule), Discord's command-count limit, and what the allowlist should actually contain. Nothing has named a promotable prompt yet.

#127 stays open, since this does not close it. Filing the slice and closing that one rather than weakening the reference is the house rule.

just gate passes.

#127 stays open. This closes the registration slice, filed as #885, and the prompt half is #884.

closes #885

Building #127's remaining half turned up something bigger than the half: **the registration Discord needs was missing for every command, not just prompts.** ## The finding A slash command has three parts. This repository had two. * `discordCommands()` renders the declared set. **Its only caller was a test.** * `onInteraction` is wired when `SIRENS_ECHO_DISCORD_COMMANDS` is on. * Nothing ever called `ApplicationCommandBulkOverwrite`, or any other registration API. So `/echo`, `/job-status`, `/job-cancel` and `/mcps` were unreachable **even with the switch on**. Discord shows a member no command nobody published, so the handler sat waiting for interactions that could not arrive. Declared, validated, tested, handled, and invisible. That reframes what #127 was blocked on. The issue reads as "prompt commands are not built"; the truth was that the command surface underneath them had never been published. ## What this ships Registration on `discord.ready`, the first point the application id exists, behind the same default-false switch. **Per guild, not globally.** A global command appears in every guild the bot is in, including ones the access policy refuses. Those would be visible, invocable, and answered `not permitted here` - advertising a summon path this deployment does not offer, which is the property `docs/access-policy.reference.yaml` exists to keep honest. A deployment admitting no guild registers nowhere and says so rather than falling back to global. **A bulk overwrite**, so the published set is exactly the declared set. A command removed from `JobCommands()` disappears rather than lingering as an invocable ghost whose handler is gone. **A guild that refuses does not stop the others.** Logged as `discord.commands.failed`, loop continues, first error returned at the end. A partial surface beats none, and swallowing it would be the silent-inert shape this repo keeps finding. Six tests: the whole declared set reaching every admitted guild by name rather than by count, the two refusals, partial failure, the deployment gate, and the admitted set coming from the access policy. ## What this does not ship, and why it is a separate issue **Prompt promotion**, filed as #884 with `consult`. The pieces exist - `CommandFromPrompt` maps the shape, `MCPProvider.Prompt` fetches, `seedFromPrompt` folds it into a turn. Assembling them is not the hard part. The hard part is one nothing on #127 had named: **An interaction must be answered in three seconds, and a model turn takes minutes.** `respondToCommand` answers immediately. Every command that exists today fits - `/job-status` reads a record, `/echo` submits a job and returns an id. A prompt command is the first whose whole purpose is a model reply, and on #577's own numbers Echo's round 0 alone ranges 133.5s to 255.4s. Two shapes - defer the interaction for 15 minutes, or submit a job - and choosing between them is a product call about what a slash command should feel like. That is why #884 is `consult` rather than something I should have picked while writing a PR. Also on #884: whether `prompts/list_changed` re-registers (a Discord write on a server's schedule), Discord's command-count limit, and what the allowlist should actually contain. Nothing has named a promotable prompt yet. **#127 stays open**, since this does not close it. Filing the slice and closing that one rather than weakening the reference is the house rule. `just gate` passes. **#127 stays open.** This closes the registration slice, filed as #885, and the prompt half is #884. closes #885
feat(commands): publish the declared set, so a slash command exists
All checks were successful
ci / test (pull_request) Successful in 50s
ci / publish-echo-image (pull_request) Has been skipped
ci / publish-observed (pull_request) Has been skipped
ci / image-build (pull_request) Successful in 26s
604f6e95c4
A slash command needs a declaration, a handler, and a registration. This
repository had the first two. discordCommands() rendered the set and its only
caller was a test, so /echo, /job-status, /job-cancel and /mcps were unreachable
even with SIRENS_ECHO_DISCORD_COMMANDS on, and onInteraction waited for
interactions Discord had no way to send.

Registration runs on discord.ready, the first point the application id exists,
still behind the same default-false switch.

Per guild rather than globally. A global command appears in every guild the bot
is in, including ones the access policy refuses, where it would be visible,
invocable, and answered with "not permitted here" - a summon path advertised
that this deployment does not offer. A deployment admitting no guild registers
nowhere and says so rather than falling back.

A bulk overwrite, so a command removed from the declaration disappears instead
of lingering as an invocable ghost whose handler is gone.

A guild that refuses the write is logged and the loop continues, because a
partial surface beats none. The first error is returned once the loop ends, so
the failure is reported rather than swallowed.

Prompt promotion, which is what sirens-echo#127 asked for, is not here. The
mapping exists and the allowlist does not, and underneath both is a problem
nothing had named: an interaction must be answered in three seconds and a model
turn takes minutes. Filed as sirens-echo#884.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Kai Siren <coilysiren@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coilyco-gaming/sirens-echo!886
No description provided.