TrendingDiscordMarch 21, 20265 min read

OpenClaw's Discord Resume Button Was Dead the Moment It Appeared

Users clicked “Resume” and got “This component has expired” — on a button that was created seconds ago. The culprit wasn't Discord's timeout. It was OpenClaw's own module system fighting itself.

What users experienced
1
Type /codex_resume
Bot creates picker with session list
2
Click a session button
Within seconds of the picker appearing
3
“This component has expired”
Immediate failure, every time

I want you to imagine something. You build a resume feature for your Discord bot. You test it locally. It works. You ship it. And then every single user who tries it gets the same error: “This component has expired.” Not after 15 minutes. Not after some timeout window. The moment they click the button. A freshly minted UI component, expired on arrival.

That's what happened to OpenClaw's /codex_resume command in Discord. And the root cause is the same architectural pattern that broke the entire plugin runtime last week.

The Same Bug, Wearing a Different Hat

If you read our coverage of OpenClaw's infrastructure crisis, this will sound familiar. The Discord component registry — the system that maps button IDs to their click handlers — was stored in a module-local Map. When Node.js created duplicate import graphs (a documented behavior in plugin architectures with cache-busting imports), each graph got its own isolated copy of that Map.

Module A creates the picker button. Module A's Map stores the callback. Module B receives the click event. Module B checks its Map. The callback doesn't exist. Component expired.

The button was never expired. It was registered in a parallel universe that the click handler couldn't see.

Two bugs, one PR

The module isolation problem was the headline, but PR #51260 also fixed a second, subtler issue: the Discord fallback path was silently discarding callback payloads. When a button carried callbackData (like a Codex resume token), the fallback handler threw it away instead of forwarding it as agent input. Telegram got this right months ago. Discord just... didn't.

The Fix Is a Pattern We've Seen Before

Contributor huntharo applied the same resolveGlobalMap helper from the infrastructure fix: store the component registry on globalThis using Symbol.for() keys. Now every import graph — no matter how many Node.js creates — converges on the same backing Map. The existing TTL and consume-once semantics are preserved. The button you register is the button the click handler finds.

The callback fix is equally straightforward: the fallback path now checks consumed.callbackData?.trim() before synthesizing a default command string. Resume tokens like codexapp:<token> now propagate correctly instead of being swallowed.

The Part Nobody Wants to Talk About

Here's my question, and I don't think it's unfair: how many other module-local Maps are lurking in OpenClaw's codebase?

Last week it was the heartbeat event registry, the agent event registry, the system event queue, and the session-binding service. This week it's the Discord component registry. Each one was independently bitten by the exact same duplicate-import-graph problem. Each one required the same globalThis migration. And each one was discovered reactively — after users hit the bug in production.

“When you fix the same category of bug twice in one week, that's not two bugs. That's a codebase telling you it has a systematic problem with module-level mutable state. The question is whether anyone is listening.”

Security flags, again

The automated security analysis flagged two issues with this PR — and the author acknowledged both as pre-existing:

  • High: The plugin SDK now exposes resolveDiscordAccount with raw bot tokens accessible to untrusted plugins
  • Medium: Button callbackData used directly as agent input enables potential command injection

“Pre-existing” is doing a lot of work in that sentence. The fact that raw bot tokens were accessible to untrusted plugins before this PR doesn't make it less of a problem — it makes it a problem that's been ignored longer. And the command injection vector through callbackData is arguably worse now that the callback actually gets forwarded to the agent instead of being silently dropped.

The Real Story Here

The fix works. Discord Codex resume pickers are functional again. Huntharo verified the end-to-end flow and added regression tests. The engineering is competent — which is exactly the problem. Competent engineering on individual PRs is what you do when you don't have the appetite for the systemic audit your codebase is begging for.

But competent engineering on individual PRs doesn't address the architectural question: OpenClaw's plugin system was designed around the assumption that JavaScript modules are singletons. That assumption is wrong in production. The project is now patching modules one by one as each one breaks, rather than auditing the entire codebase for the same class of vulnerability.

That's a choice. It might even be the pragmatic choice for a fast-moving open-source project. But it means we'll be writing this article again the next time a module-local Map gets split across import graphs. And the next time after that.

The full PR is available at PR #51260 on GitHub. For DeployClaw users, this fix is already live.

Carlos Simpson covers OpenClaw for DeployClaw News. DeployClaw is a managed OpenClaw hosting platform. Coverage is editorially independent. Got a tip? news@deployclaw.com