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
resolveDiscordAccountwith 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.