Discord: implement messages.statusReactions lifecycle (done emoji persistence) — currently Telegram-only despite docs
Summary
The docs at docs/gateway/config-agents.md and docs/concepts/qa-e2e-automation.md state:
messages.statusReactions.enabled enables lifecycle status reactions on Slack, Discord, and Telegram.
Reading the bundled code, only Telegram actually wires the full lifecycle (queued → thinking → tool/coding/web → done/error → hold → cleanup). Discord uses only the legacy single-emoji ackReaction + cleanup-on-reply path. There is no Discord equivalent of the Telegram finalizeTelegramStatusReaction function. Setting messages.statusReactions = { enabled: true, emojis: { done: "✅" }, timing: { doneHoldMs: 3600000 } } has no observable effect on Discord guild channels — replies still cycle exactly like the bare ackReaction setup.
This is a follow-up to #78405 (silent text-reply drops) and represents the inverse problem: documented feature, missing implementation.
User-visible impact
For agents that take a while to respond (e.g. local Ollama models on big contexts: 30–120s), users have no way to distinguish "agent is still working" from "agent crashed silently". Telegram users get the full visual lifecycle (🤔 → 🔥/⚡ → ✅) and can leave a ✅ pinned long after the reply for "this turn was processed successfully" semantics.
Concrete sequence we wanted on Discord and could not get:
- User posts a message.
- Bot reacts with 🤔 immediately (works today via
ackReaction).
- While the agent runs tool calls, the reaction transitions to 🔥 (exec) / ⚡ (web search) / 👨💻 (coding) — doesn't happen on Discord.
- When the reply is sent, the reaction transitions to ✅ — doesn't happen on Discord.
- ✅ stays for
doneHoldMs then is cleaned up — doneHoldMs has no effect on Discord.
Currently on Discord we are forced to choose between:
removeAckAfterReply: false → 🤔 reappears immediately after the reply (the runtime calls restoreInitial() because there is no done transition path), so the user just sees 🤔 again, indistinguishable from "still thinking".
removeAckAfterReply: true → all reactions cleaned up post-reply, no visual signal that the turn completed successfully.
Neither produces "you can see at a glance which messages were processed".
Repro
OpenClaw 2026.5.5.
openclaw.json:
{
"messages": {
"ackReaction": "🤔",
"ackReactionScope": "all",
"removeAckAfterReply": false,
"statusReactions": {
"enabled": true,
"emojis": { "done": "✅" },
"timing": { "doneHoldMs": 3600000 }
}
}
}
Discord guild channel with requireMention: false, agent on local Ollama (slow inference, 30–60s reply time).
Observed: 🤔 appears immediately, no transitions during tool calls, 🤔 reappears right after the text reply is posted (no ✅ phase), doneHoldMs ignored.
Expected (per docs and Telegram parity): 🤔 → optional intermediate emojis → ✅ held for doneHoldMs → cleanup.
Code pointers (where to wire it)
The Telegram implementation is the reference. From dist/bot-D-7bCSXH.js:~4513:
const finalizeTelegramStatusReaction = async (params) => {
if (!statusReactionController) return;
if (params.outcome === "done") {
await statusReactionController.setDone();
if (removeAckAfterReply) {
await sleepWithAbort(statusReactionTiming.doneHoldMs);
await clearTelegramStatusReaction();
} else {
await statusReactionController.restoreInitial();
}
return;
}
// error path...
};
The statusReactionController factory is in dist/channel-feedback-B3QykyPj.js (finishWithEmoji, setDone, setError, etc.) — already provider-agnostic, just needs a Discord adapter (addReaction(channel, msg, emoji) / removeReaction(...)).
The Discord plugin already does single-emoji reactions (the ackReaction path), so the REST surface (PUT /channels/.../messages/.../reactions/.../@me) is wired. Plugging the existing controller into the Discord finalize callback should be small.
grep -rn "statusReactionController" dist/ returns matches only in bot-D-7bCSXH.js (Telegram). No Discord file references it, which confirms the gap.
There's also a deeper UX win: showing an in-progress emoji during long Ollama tool loops would give users a heartbeat. The same setThinking() / setTool() / setCompacting() events are already emitted by the runtime — the Discord plugin just doesn't subscribe.
Suggested implementation
- In
extensions/discord/src/monitor/message-handler.process.ts (or wherever the Discord deliver finalize hook lives), instantiate the same createStatusReactionController factory used by Telegram, with a Discord-specific reaction adapter.
- Wire it into the same lifecycle event stream the Telegram path uses (
pipeline.runtime-* emits the events).
- Add
finalizeDiscordStatusReaction(params) mirroring the Telegram one — same outcome === "done" / error branches, same doneHoldMs / errorHoldMs semantics.
- Update
docs/channels/discord.md to confirm full feature support, or — if this is intentionally Telegram-first — soften the wording in docs/gateway/config-agents.md to "Slack and Telegram only; Discord uses simplified ackReaction + cleanup-on-reply".
Why this is worth shipping
- Doc/code mismatch is a trust-erosion issue: someone who reads the docs and configures the full lifecycle, then sees nothing, assumes their config is wrong (we lost ~1h chasing down which key was rejected by the schema).
- Local-model usability: agents on Ollama / LM Studio can take 30–120s. Lifecycle reactions are exactly the use case where they pay off most. Cloud-model setups (GPT, Claude) reply in 2–5s and don't need it as much, which is probably why this gap survived — a Telegram-cloud user wouldn't notice.
- Parity is a small change: the runtime emits the events, the controller factory is already abstract, the Discord REST API supports per-message reactions. Mostly wiring + an adapter.
Environment
- OpenClaw 2026.5.5 (also tested 2026.5.4 with same result)
- Discord: bot in a personal guild,
requireMention: false, multiple bound agents (one slow Ollama-backed, others fast Lobster pipelines)
- Workaround currently in use:
statusReactions.enabled: false, ackReaction: "🤔", ackReactionScope: "all", removeAckAfterReply: true. Visual: 🤔 during processing, removed when reply lands. Acceptable but not what the docs describe.
Happy to test patches on main. Thanks!
Discord: implement
messages.statusReactionslifecycle (done emoji persistence) — currently Telegram-only despite docsSummary
The docs at
docs/gateway/config-agents.mdanddocs/concepts/qa-e2e-automation.mdstate:Reading the bundled code, only Telegram actually wires the full lifecycle (queued → thinking → tool/coding/web → done/error → hold → cleanup). Discord uses only the legacy single-emoji
ackReaction+ cleanup-on-reply path. There is no Discord equivalent of the TelegramfinalizeTelegramStatusReactionfunction. Settingmessages.statusReactions = { enabled: true, emojis: { done: "✅" }, timing: { doneHoldMs: 3600000 } }has no observable effect on Discord guild channels — replies still cycle exactly like the bareackReactionsetup.This is a follow-up to #78405 (silent text-reply drops) and represents the inverse problem: documented feature, missing implementation.
User-visible impact
For agents that take a while to respond (e.g. local Ollama models on big contexts: 30–120s), users have no way to distinguish "agent is still working" from "agent crashed silently". Telegram users get the full visual lifecycle (🤔 → 🔥/⚡ → ✅) and can leave a ✅ pinned long after the reply for "this turn was processed successfully" semantics.
Concrete sequence we wanted on Discord and could not get:
ackReaction).doneHoldMsthen is cleaned up — doneHoldMs has no effect on Discord.Currently on Discord we are forced to choose between:
removeAckAfterReply: false→ 🤔 reappears immediately after the reply (the runtime callsrestoreInitial()because there is nodonetransition path), so the user just sees 🤔 again, indistinguishable from "still thinking".removeAckAfterReply: true→ all reactions cleaned up post-reply, no visual signal that the turn completed successfully.Neither produces "you can see at a glance which messages were processed".
Repro
OpenClaw 2026.5.5.
openclaw.json:{ "messages": { "ackReaction": "🤔", "ackReactionScope": "all", "removeAckAfterReply": false, "statusReactions": { "enabled": true, "emojis": { "done": "✅" }, "timing": { "doneHoldMs": 3600000 } } } }Discord guild channel with
requireMention: false, agent on local Ollama (slow inference, 30–60s reply time).Observed: 🤔 appears immediately, no transitions during tool calls, 🤔 reappears right after the text reply is posted (no ✅ phase),
doneHoldMsignored.Expected (per docs and Telegram parity): 🤔 → optional intermediate emojis → ✅ held for
doneHoldMs→ cleanup.Code pointers (where to wire it)
The Telegram implementation is the reference. From
dist/bot-D-7bCSXH.js:~4513:The
statusReactionControllerfactory is indist/channel-feedback-B3QykyPj.js(finishWithEmoji,setDone,setError, etc.) — already provider-agnostic, just needs a Discord adapter (addReaction(channel, msg, emoji)/removeReaction(...)).The Discord plugin already does single-emoji reactions (the
ackReactionpath), so the REST surface (PUT /channels/.../messages/.../reactions/.../@me) is wired. Plugging the existing controller into the Discord finalize callback should be small.grep -rn "statusReactionController" dist/returns matches only inbot-D-7bCSXH.js(Telegram). No Discord file references it, which confirms the gap.There's also a deeper UX win: showing an in-progress emoji during long Ollama tool loops would give users a heartbeat. The same
setThinking() / setTool() / setCompacting()events are already emitted by the runtime — the Discord plugin just doesn't subscribe.Suggested implementation
extensions/discord/src/monitor/message-handler.process.ts(or wherever the Discord deliver finalize hook lives), instantiate the samecreateStatusReactionControllerfactory used by Telegram, with a Discord-specific reaction adapter.pipeline.runtime-*emits the events).finalizeDiscordStatusReaction(params)mirroring the Telegram one — sameoutcome === "done"/errorbranches, samedoneHoldMs/errorHoldMssemantics.docs/channels/discord.mdto confirm full feature support, or — if this is intentionally Telegram-first — soften the wording indocs/gateway/config-agents.mdto "Slack and Telegram only; Discord uses simplifiedackReaction+ cleanup-on-reply".Why this is worth shipping
Environment
requireMention: false, multiple bound agents (one slow Ollama-backed, others fast Lobster pipelines)statusReactions.enabled: false,ackReaction: "🤔",ackReactionScope: "all",removeAckAfterReply: true. Visual: 🤔 during processing, removed when reply lands. Acceptable but not what the docs describe.Happy to test patches on
main. Thanks!