Bug type
Regression (worked in 2026.4.29, fails in 2026.5.2)
Beta release blocker
No
Summary
After upgrading to OpenClaw 2026.4.29 → 2026.5.2, the now-externalized @openclaw/discord@2026.5.2 plugin fails to start the Discord channel when channels.discord.token is configured as a SecretRef (e.g. { id: "DISCORD_BOT_TOKEN", provider: "default", source: "env" }). Telegram with the same SecretRef pattern is unaffected.
The error fires before gateway ready:
[discord] channel startup failed: channels.discord.token: unresolved SecretRef
"env:default:DISCORD_BOT_TOKEN". Resolve this command against an active gateway
runtime snapshot before reading it.
The same config worked on 2026.4.29 (bundled dist/extensions/discord).
Steps to reproduce
- Install
OpenClaw 2026.5.2 (which auto-installs @openclaw/discord@2026.5.2 from npm via the doctor repair step).
~/.openclaw/openclaw.json:
"channels": {
"discord": {
"enabled": true,
"token": {
"id": "DISCORD_BOT_TOKEN",
"provider": "default",
"source": "env"
}
}
},
"secrets": {
"providers": { "default": { "source": "env" } }
}
- Ensure
DISCORD_BOT_TOKEN is in process.env (verified via dotenv autoload from ~/.openclaw/.env and directly via the LaunchAgent env file — both fail the same way).
- Restart the gateway.
- Observe
[discord] channel startup failed: ... unresolved SecretRef "env:default:DISCORD_BOT_TOKEN" at startup. Channel does not come up. Across multiple restarts: same error every time, not transient.
Expected behavior
Discord channel starts using the env-resolved DISCORD_BOT_TOKEN, same as on 2026.4.29 and same as Telegram with an analogous SecretRef config on 2026.5.2.
Actual behavior
Channel startup throws synchronously inside resolveDiscordToken because normalizeDiscordToken invokes normalizeResolvedSecretInputString (strict mode), which calls assertSecretInputResolved and throws when cfg.channels.discord.token is still a SecretRef object at startup time.
Root cause analysis (from inspecting the installed plugin source)
@openclaw/discord/src/token.ts resolveDiscordToken → normalizeDiscordToken:
export function normalizeDiscordToken(raw: unknown, path: string): string | undefined {
const trimmed = normalizeResolvedSecretInputString({ value: raw, path });
...
}
normalizeResolvedSecretInputString is the strict variant — it throws createUnresolvedSecretInputError ("Resolve this command against an active gateway runtime snapshot before reading it.") on unresolved SecretRefs.
Compare to @openclaw/discord/src/setup-account-state.ts inspectConfiguredToken:
function inspectConfiguredToken(value: unknown): {...} | null {
const normalized = normalizeSecretInputString(value); // ← safe variant; returns undefined on SecretRef
if (normalized) { return { ..., tokenStatus: "available" }; }
if (hasConfiguredSecretInput(value)) {
return { ..., tokenStatus: "configured_unavailable" };
}
return null;
}
The inspect path correctly distinguishes "string token" / "SecretRef present but unresolved" / "absent". The startup path does not — it goes through resolveDiscordToken with the raw OpenClawConfig and crashes.
Both hits in the channel startup chain are reachable:
@openclaw/discord/src/accounts.ts:110 — resolveDiscordAccount(...) calls resolveDiscordToken(params.cfg, { accountId }).
@openclaw/discord/src/setup-account-state.ts:122 — falls through to resolveDiscordToken(params.cfg, { accountId }) only when inspectConfiguredToken returns null, but the throw can also originate elsewhere depending on call path.
The error message itself ("Resolve this command against an active gateway runtime snapshot before reading it.") is the explicit contract violation: the channel startup code is reading from raw config instead of from a runtime snapshot whose secrets have been pre-resolved.
This is the same family as #75433 ("source/raw channel config instead of a resolved active runtime snapshot, then calling Telegram/Discord channel discovery helpers that synchronously resolve channel credentials"), but on the channel STARTUP path rather than the embedded reply path. Sister-bug to #76369 (@openclaw/bluebubbles 2026.5.2 webhook auth crashes on SecretRef password) — same externalization-induced SecretRef regression family.
Suggested fixes (not exhaustive)
- In
@openclaw/discord/src/token.ts — make normalizeDiscordToken use the gentle normalizeSecretInputString, and have resolveDiscordToken follow the inspectConfiguredToken pattern (treat unresolved SecretRef as "fall through to env / return none" rather than throw). This matches Telegram's behavior.
- In the core gateway startup — resolve
channels.* SecretRef-backed tokens before invoking startChannel(plugin.id) so the runtime snapshot the plugin sees has only string tokens. This is the contract the error message implies.
(2) is the architecturally correct fix; (1) is a defensive backstop that would also prevent this class of crash in any other startup-path consumer of resolveDiscordToken.
Workarounds attempted (none effective)
- Adding
DISCORD_BOT_TOKEN to the LaunchAgent env file (~/.openclaw/service-env/ai.openclaw.gateway.env) so it is in process.env from the very first instruction — same crash. The bug is not env-availability; it is the strict SecretRef resolver throwing on the SecretRef object itself.
- Multiple gateway restarts (
launchctl kickstart -k) — same crash every time. Not a startup race, fully deterministic.
- Setting
plugins.allow explicitly to ["discord"] to silence the "non-bundled plugins may auto-load" warning — no effect on the channel startup error (different code path).
Effective workaround
Roll back to 2026.4.29 (which still ships dist/extensions/discord bundled). Discord channel starts cleanly.
Environment
- macOS 26.2 (arm64), Node 25.9.0
- OpenClaw
2026.5.2 (commit 8b2a6e5), installed via npm i -g openclaw
@openclaw/discord@2026.5.2 (auto-installed by openclaw doctor step of openclaw update)
- Gateway managed via
launchd (gui/502/ai.openclaw.gateway), local mode
~/.openclaw/.env populated via dotenv (DISCORD_BOT_TOKEN, TELEGRAM_BOT_TOKEN, etc.)
- Telegram channel with the same SecretRef pattern (
channels.telegram.botToken: { id: "TELEGRAM_BOT_TOKEN", provider: "default", source: "env" }) starts and runs without issue on the same gateway boot.
Related
Bug type
Regression (worked in 2026.4.29, fails in 2026.5.2)
Beta release blocker
No
Summary
After upgrading to OpenClaw
2026.4.29→2026.5.2, the now-externalized@openclaw/discord@2026.5.2plugin fails to start the Discord channel whenchannels.discord.tokenis configured as a SecretRef (e.g.{ id: "DISCORD_BOT_TOKEN", provider: "default", source: "env" }). Telegram with the same SecretRef pattern is unaffected.The error fires before
gateway ready:The same config worked on
2026.4.29(bundleddist/extensions/discord).Steps to reproduce
OpenClaw 2026.5.2(which auto-installs@openclaw/discord@2026.5.2from npm via the doctor repair step).~/.openclaw/openclaw.json:DISCORD_BOT_TOKENis inprocess.env(verified via dotenv autoload from~/.openclaw/.envand directly via the LaunchAgent env file — both fail the same way).[discord] channel startup failed: ... unresolved SecretRef "env:default:DISCORD_BOT_TOKEN"at startup. Channel does not come up. Across multiple restarts: same error every time, not transient.Expected behavior
Discord channel starts using the env-resolved
DISCORD_BOT_TOKEN, same as on2026.4.29and same as Telegram with an analogous SecretRef config on2026.5.2.Actual behavior
Channel startup throws synchronously inside
resolveDiscordTokenbecausenormalizeDiscordTokeninvokesnormalizeResolvedSecretInputString(strict mode), which callsassertSecretInputResolvedand throws whencfg.channels.discord.tokenis still a SecretRef object at startup time.Root cause analysis (from inspecting the installed plugin source)
@openclaw/discord/src/token.tsresolveDiscordToken→normalizeDiscordToken:normalizeResolvedSecretInputStringis the strict variant — it throwscreateUnresolvedSecretInputError("Resolve this command against an active gateway runtime snapshot before reading it.") on unresolved SecretRefs.Compare to
@openclaw/discord/src/setup-account-state.tsinspectConfiguredToken:The inspect path correctly distinguishes "string token" / "SecretRef present but unresolved" / "absent". The startup path does not — it goes through
resolveDiscordTokenwith the rawOpenClawConfigand crashes.Both hits in the channel startup chain are reachable:
@openclaw/discord/src/accounts.ts:110—resolveDiscordAccount(...)callsresolveDiscordToken(params.cfg, { accountId }).@openclaw/discord/src/setup-account-state.ts:122— falls through toresolveDiscordToken(params.cfg, { accountId })only wheninspectConfiguredTokenreturns null, but the throw can also originate elsewhere depending on call path.The error message itself (
"Resolve this command against an active gateway runtime snapshot before reading it.") is the explicit contract violation: the channel startup code is reading from raw config instead of from a runtime snapshot whose secrets have been pre-resolved.This is the same family as #75433 ("source/raw channel config instead of a resolved active runtime snapshot, then calling Telegram/Discord channel discovery helpers that synchronously resolve channel credentials"), but on the channel STARTUP path rather than the embedded reply path. Sister-bug to #76369 (
@openclaw/bluebubbles2026.5.2 webhook auth crashes on SecretRef password) — same externalization-induced SecretRef regression family.Suggested fixes (not exhaustive)
@openclaw/discord/src/token.ts— makenormalizeDiscordTokenuse the gentlenormalizeSecretInputString, and haveresolveDiscordTokenfollow theinspectConfiguredTokenpattern (treat unresolved SecretRef as "fall through to env / return none" rather than throw). This matches Telegram's behavior.channels.*SecretRef-backed tokens before invokingstartChannel(plugin.id)so the runtime snapshot the plugin sees has only string tokens. This is the contract the error message implies.(2) is the architecturally correct fix; (1) is a defensive backstop that would also prevent this class of crash in any other startup-path consumer of
resolveDiscordToken.Workarounds attempted (none effective)
DISCORD_BOT_TOKENto the LaunchAgent env file (~/.openclaw/service-env/ai.openclaw.gateway.env) so it is inprocess.envfrom the very first instruction — same crash. The bug is not env-availability; it is the strict SecretRef resolver throwing on the SecretRef object itself.launchctl kickstart -k) — same crash every time. Not a startup race, fully deterministic.plugins.allowexplicitly to["discord"]to silence the "non-bundled plugins may auto-load" warning — no effect on the channel startup error (different code path).Effective workaround
Roll back to
2026.4.29(which still shipsdist/extensions/discordbundled). Discord channel starts cleanly.Environment
2026.5.2(commit8b2a6e5), installed vianpm i -g openclaw@openclaw/discord@2026.5.2(auto-installed byopenclaw doctorstep ofopenclaw update)launchd(gui/502/ai.openclaw.gateway), local mode~/.openclaw/.envpopulated via dotenv (DISCORD_BOT_TOKEN,TELEGRAM_BOT_TOKEN, etc.)channels.telegram.botToken: { id: "TELEGRAM_BOT_TOKEN", provider: "default", source: "env" }) starts and runs without issue on the same gateway boot.Related
@openclaw/bluebubbles@2026.5.2webhook auth SecretRef crash, sister bug from the 2026.5.2 externalization wave.