Skip to content

[Bug]: Channel plugin registrations lost on runtime registry swap — message tool "Unknown channel" for configured channels #48790

@mekenthompson

Description

@mekenthompson

Description

getChannelPlugin() returns undefined for properly configured core channels (e.g. Telegram) after the active plugin registry is replaced at runtime. This causes the message tool to fail with Unknown channel: telegram even though the channel is in CHANNEL_IDS and was fully functional at startup.

Root Cause

loadOpenClawPlugins() is called at multiple points during runtime — config schema lookups (server-methods/config.ts), maybeBootstrapChannelPlugin() (channel-resolution.ts), provider resolution (plugins/providers.ts), etc. Each call with a different cache key creates a new PluginRegistry and sets it as active via setActivePluginRegistry().

The new registry may have empty channels (if the load was scoped or the channel plugins were not re-initialized). getChannelPlugin() in src/channels/plugins/registry.ts reads from requireActivePluginRegistry().channels, which is now empty. The channel-plugin lookup cache invalidates on version change, reads the empty array, and returns undefined.

Meanwhile, isKnownChannel() / isDeliverableMessageChannel() still returns true because it checks the hardcoded CHANNEL_IDS array in src/channels/registry.ts. This creates a contradictory state: the channel is "known" but has no plugin.

The error surfaces in two places:

  1. src/infra/outbound/message.ts:145resolveRequiredPlugin() throws Unknown channel: ${channel}
  2. src/infra/outbound/channel-selection.ts:131isKnownChannel() passes but resolveAvailableKnownChannel() fails

There is also a double-lookup race in sendMessage(): resolveRequiredChannel() resolves the channel (line 130), then resolveRequiredPlugin() does a second plugin lookup (line 142). The registry can swap between these two calls.

Reproduction

Most likely to reproduce with:

  • Multiple channels configured (e.g. Telegram + Discord)
  • Multiple agents or cron jobs triggering config/schema lookups
  • Heartbeat or isolated sessions that trigger loadOpenClawPlugins() with different workspace/config parameters

The bug is intermittent — it only manifests when a message send coincides with a registry swap.

Upstream Context

PR #47902 (a69f6190) correctly identified this class of bug for httpRoutes and introduced the pinned httpRouteRegistry pattern. The same bug exists for channels but was not addressed.

Proposed Fix

Mirror the httpRouteRegistry pinning pattern for channels:

  1. Add channelRegistry / channelRegistryPinned state to src/plugins/runtime.ts
  2. Add pinActivePluginChannelRegistry() / releasePinnedPluginChannelRegistry() / getActivePluginChannelRegistry() / requireActivePluginChannelRegistry()
  3. In setActivePluginRegistry(), only update channelRegistry when not pinned
  4. Switch resolveCachedChannelPlugins() in src/channels/plugins/registry.ts to use requireActivePluginChannelRegistry()
  5. Pin the channel registry alongside the HTTP route registry in gateway startup (server-runtime-state.ts)
  6. Harden the channel plugin cache to check both registry object identity AND version number

We have a working implementation: mekenthompson@fe7b8a65d

Related Issues

Environment

  • OpenClaw version: affects all versions since plugin registry was introduced
  • Channels: Telegram + Discord
  • Install: Home Assistant addon (Docker)
  • OS: Linux 6.12.67 (x64), Node v22.22.1

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions