[Fix] Auto-confirm an unanswered Discord routing card#451
Conversation
A Discord routing card had no timeout. Ask someone where to run a task, get distracted, and 15 minutes later the pending route expired with its Redis TTL — no launch, no message, the request simply gone. Slack auto-confirms its suggestion after 30s and Telegram after 5s; only Discord dropped the request. Reuse Slack's mechanism rather than inventing a third: the shared constant is renamed SLACK_AUTO_CONFIRM_TIMEOUT_MS -> ROUTING_AUTO_CONFIRM_TIMEOUT_MS, which is what `block-kit.ts` already called it in prose, and Discord imports the same 30s. The timer is in-process like Slack's and Telegram's, so a restart loses it and the route falls back to expiring. Claiming the pending route is atomic, so a click and the timer cannot both launch. Only a suggestion is auto-confirmed. The card previously announced "The best match is X" using the first option even on a fallback, where options are merely alphabetical and the router had no opinion — so it named a best match it had not chosen. The suggested option is now located in the rendered list and stored, the card only claims a match when one exists, and a fallback card auto-confirms nothing. `launchDiscordTask`'s `replaceMessage` now describes a message rather than an interaction, since an auto-confirm has no interaction to answer through: a click is still answered via its token, while the timer edits the card by id. Both go through `replaceOrPostDiscordMessage`, the analog of Slack's `postOrReplaceSlackMessage`, which falls back to posting when the card is gone. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
No new code issues found. See task
Reviewed 88032d2 |
Two real bugs in the auto-confirm path. Editing the card addressed the thread's parent channel. A thread is itself a channel and `editMessage` takes no separate thread id, unlike `postMessage` — so the edit could only ever return Unknown Message, and the fallback would post a second acknowledgement beside a routing card still showing live buttons. The exact duplicate this flow just removed. The card id is only knowable after posting, so the route is written a second time. That write was unconditional, so a Nevermind landing while the card was still posting would have its claim overwritten, bringing the route back from the dead for the timer to launch. Telegram already solved this with `XX` and said so in a comment; Discord now matches. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Both real, both fixed in ca0f62c. Thanks — the second one would have been ugly in production. 1. Editing through the parent channel. Correct, and the failure mode is exactly the duplicate this flow just removed: the edit could only ever return Unknown Message, so the fallback would post a second acknowledgement beside a routing card still showing live buttons. The cause was copying 2. Resurrecting a claimed route. Also correct. The card id is only knowable after posting, so the route is written a second time, and that write was unconditional — a Nevermind landing in that window would have its Tests added for both: a card in a thread is edited through the thread id (and posts only when the card is genuinely gone), and the second write is asserted to be conditional while the first is not. |
Problem
A Discord routing card had no timeout. Ask someone where to run a task, get distracted, and 15 minutes later the pending route expired with its Redis TTL — no launch, no message, the request simply gone.
Slack auto-confirms its suggestion after 30s (
runSlackAutoConfirm), Telegram after 5s (scheduleTelegramRoutingAutoConfirm). Only Discord dropped the request on the floor. This was the last behavioral divergence from Slack in the routing flow.Reusing rather than re-deriving
The shared constant is renamed
SLACK_AUTO_CONFIRM_TIMEOUT_MS→ROUTING_AUTO_CONFIRM_TIMEOUT_MSin@roomote/cloud-agents— which is already whatblock-kit.ts:2123calls it in prose — and Discord imports the same 30s value. One constant, two providers, no third copy. (Telegram's local 5s is left alone; changing it is a product call, not this fix.)The timer is in-process like both existing implementations, so a restart loses it and the route falls back to expiring exactly as it does today. Claiming the pending route is atomic (
GETDEL), so a click and the timer cannot both launch.A bug found on the way
The card announced "The best match is X" using
options[0]— but on afallbackdecision the options are merely alphabetical and the router had no opinion, so the card named a "best match" it had never chosen. Auto-confirming that would launch an alphabetical accident.The suggested option is now located in the rendered list (never assumed to be first — a suggestion can vanish before the card is built) and stored as
suggestedIndex. The card only claims a best match when one exists, and a fallback card auto-confirms nothing.Replace-by-message, not replace-by-interaction
launchDiscordTask'sreplaceMessagedescribed an interaction, but an auto-confirm has no interaction to answer through — nobody clicked. It now describes a message: a click is still answered via its token (that path is live-tested and untouched), while the timer edits the card by id.Both go through
replaceOrPostDiscordMessage, the Discord analog of Slack'spostOrReplaceSlackMessage, which falls back to posting when the card is gone — so a started task never loses its cancel control.Verification
245 tests pass across the Slack, Discord and Telegram handlers. New coverage, driven with fake timers:
🤖 Generated with Claude Code