Bug Description
Subagent completion announcements fail to deliver to Telegram with:
Error: Outbound not configured for channel: telegram
The announce flow retries 3 times (each with a different ephemeral WS connection) and gives up. Normal replies to Telegram work fine.
Root Cause
Plugin registry mismatch between pinned and active registries.
In src/channels/plugins/outbound/load.ts, createChannelRegistryLoader uses getActivePluginRegistry() to resolve channel outbound adapters:
const loadOutboundAdapterFromRegistry = createChannelRegistryLoader(
(entry) => entry.plugin.outbound
);
function createChannelRegistryLoader(resolveValue) {
// ...
return async (id) => {
const registry = getActivePluginRegistry(); // ← uses unpinned registry
// ...
};
}
However, resolveOutboundChannelPlugin (which runs just before in createChannelHandler) uses getChannelPlugin(), which resolves through requireActivePluginChannelRegistry() — the pinned registry.
The gateway pins the channel registry at startup via pinActivePluginChannelRegistry() so that config-schema reads, agent-specific plugin loads, and other setActivePluginRegistry() calls cannot evict channel plugins. This pinning is correct and intentional.
The problem: when the announce delivery path runs after agent completion, the active registry may have been swapped to an agent-specific or ephemeral registry that does not include the Telegram (or other channel) outbound adapter. The pinned registry still has it, but loadOutboundAdapterFromRegistry does not consult the pinned registry.
Normal replies work because they execute within the gateway's main request handling path where the active registry happens to be in sync with the pinned one.
Steps to Reproduce
- Configure Telegram (or any external channel)
- Send a message from Telegram that triggers a subagent spawn
- Wait for the subagent to complete
- Observe: announce delivery fails with
Outbound not configured for channel: telegram
- Observe: normal direct replies to Telegram still work fine
Log Evidence
[ws] ⇄ res ✗ agent errorCode=UNAVAILABLE errorMessage=Error: Outbound not configured for channel: telegram conn=aeea7ac2…90a4
[ws] ⇄ res ✗ agent cached=true conn=22f5fcd7…9a9b ← different connection
[ws] ⇄ res ✗ agent cached=true conn=8613cde9…6127 ← different connection
[warn] Subagent announce give up (retry-limit) retries=3
Each retry uses a different ephemeral WS connection, confirming the announce flow creates fresh connections that all fail to find the adapter.
Meanwhile, delivery-recovery on gateway restart successfully delivers to Telegram (because it runs within the gateway process where the pinned registry is available).
Proposed Fix
In createChannelRegistryLoader, use the pinned channel registry instead of the general active registry:
- const registry = getActivePluginRegistry();
+ const registry = requireActivePluginChannelRegistry();
Or alternatively, have loadChannelOutboundAdapter call through getChannelPlugin (which already uses the pinned registry) and extract the outbound adapter from there.
Environment
- OpenClaw version: latest (same build confirmed working on another instance with different usage patterns)
- Channel: Telegram
- Clean gateway restart does not fix the issue (confirmed)
- Affects: subagent announce delivery, cron job announce delivery (any
deliverOutboundPayloads path triggered from agent completion)
Labels
bug
Bug Description
Subagent completion announcements fail to deliver to Telegram with:
The announce flow retries 3 times (each with a different ephemeral WS connection) and gives up. Normal replies to Telegram work fine.
Root Cause
Plugin registry mismatch between pinned and active registries.
In
src/channels/plugins/outbound/load.ts,createChannelRegistryLoaderusesgetActivePluginRegistry()to resolve channel outbound adapters:However,
resolveOutboundChannelPlugin(which runs just before increateChannelHandler) usesgetChannelPlugin(), which resolves throughrequireActivePluginChannelRegistry()— the pinned registry.The gateway pins the channel registry at startup via
pinActivePluginChannelRegistry()so that config-schema reads, agent-specific plugin loads, and othersetActivePluginRegistry()calls cannot evict channel plugins. This pinning is correct and intentional.The problem: when the announce delivery path runs after agent completion, the active registry may have been swapped to an agent-specific or ephemeral registry that does not include the Telegram (or other channel) outbound adapter. The pinned registry still has it, but
loadOutboundAdapterFromRegistrydoes not consult the pinned registry.Normal replies work because they execute within the gateway's main request handling path where the active registry happens to be in sync with the pinned one.
Steps to Reproduce
Outbound not configured for channel: telegramLog Evidence
Each retry uses a different ephemeral WS connection, confirming the announce flow creates fresh connections that all fail to find the adapter.
Meanwhile, delivery-recovery on gateway restart successfully delivers to Telegram (because it runs within the gateway process where the pinned registry is available).
Proposed Fix
In
createChannelRegistryLoader, use the pinned channel registry instead of the general active registry:Or alternatively, have
loadChannelOutboundAdaptercall throughgetChannelPlugin(which already uses the pinned registry) and extract the outbound adapter from there.Environment
deliverOutboundPayloadspath triggered from agent completion)Labels
bug