zeroclaw-labs/zeroclaw · error
unsupported delivery channel: {other}
Error message
unsupported delivery channel: {other} What it means
The channel-type prefix (text before the first dot, lowercased) matched no arm in deliver_announcement. The stateless arms cover telegram, discord, slack, signal, wechat, lark/feishu, webhook, wecom_ws, email, and whatsapp; everything else — including typos like "telegrm", types with no static send path (e.g. qq, matrix when no live instance is registered), or malformed refs that passed the dotted-ref check — falls into `other` and bails. Live-registered channels are matched earlier via CRON_CHANNEL_REGISTRY, so a normally-supported type can still land here when it is not connected.
Source
Thrown at crates/zeroclaw-channels/src/orchestrator/mod.rs:13054
let peer_resolver: Arc<dyn Fn() -> Vec<String> + Send + Sync> =
Arc::new(move || peers.clone());
let allowed_groups = wa.allowed_groups.clone();
let allowed_groups_resolver: Arc<dyn Fn() -> Vec<String> + Send + Sync> =
Arc::new(move || allowed_groups.clone());
let ch = WhatsAppWebChannel::new(
wa,
alias.to_string(),
peer_resolver,
allowed_groups_resolver,
)
.with_workspace_dir(config.channel_workspace_dir(&format!("whatsapp.{alias}")));
zeroclaw_api::channel::Channel::send(&ch, &make_msg(&safe_output)).await?;
}
#[cfg(not(feature = "whatsapp-web"))]
"whatsapp" | "whatsapp-web" | "whatsapp_web" => {
anyhow::bail!("WhatsApp channel requires the `whatsapp-web` feature");
}
other => anyhow::bail!("unsupported delivery channel: {other}"),
}
#[allow(unreachable_code)]
Ok(())
}
// ── Concurrent persist lock test ─────────────────────────
// Lives outside `mod tests` so it has direct access to private parent items.
#[cfg(test)]
#[test]
fn concurrent_persist_lock_serialization() {
use std::sync::Barrier;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
use zeroclaw_infra::session_backend::SessionBackend;
use zeroclaw_providers::ChatMessage;
use zeroclaw_runtime::approval::ApprovalManager;
use zeroclaw_runtime::observability::NoopObserver;View on GitHub (pinned to 88bb9c8533)
Solutions
- Check the type spelling against the supported list and use the exact dotted form <type>.<alias>
- If the type is live-only (e.g. matrix), ensure the channel is running and registered so the registry path handles it before the match
- If the config schema genuinely accepts a type deliver_announcement does not route, file/extend a match arm rather than working around it
Example fix
# before [cron.report] channel = "telegra.work" # typo -> bail: unsupported delivery channel: telegra # after [cron.report] channel = "telegram.work"
Defensive patterns
Strategy: type-guard
Validate before calling
const STATELESS_DELIVERY_TYPES: &[&str] = &[
"telegram", "discord", "slack", "signal", "wechat", "lark", "feishu",
"webhook", "wecom_ws", "wecom-ws", "email", "whatsapp", "whatsapp-web", "whatsapp_web",
];
fn delivery_type_supported(raw: &str) -> bool {
raw.split_once('.')
.map(|(kind, _)| STATELESS_DELIVERY_TYPES.contains(&kind.to_ascii_lowercase().as_str()))
.unwrap_or(false)
} Type guard
fn is_supported_delivery_ref(channel: &str) -> bool {
let Some((kind, alias)) = channel.split_once('.') else { return false };
!alias.is_empty()
&& STATELESS_DELIVERY_TYPES.contains(&kind.to_ascii_lowercase().as_str())
// live-registry types (e.g. matrix) are valid only while connected
|| matches!(kind.to_ascii_lowercase().as_str(), "matrix" if matrix_registered(channel))
}
assert!(is_supported_delivery_ref("telegram.work"));
assert!(!is_supported_delivery_ref("telegra.work")); Try / catch
match deliver_announcement(&cfg, channel, &target, thread, &out).await {
Err(e) if e.to_string().starts_with("unsupported delivery channel") => {
// Config typo or a live-only type that is offline: surface to the operator, never retry blindly.
}
rest => rest?,
} Prevention
- Validate every cron channel string against the supported type list when loading config
- Prefer fail-fast config validation over discovering typos at first cron fire
- When adding a new channel type to the schema, extend deliver_announcement's match in the same change (tests at orchestrator/mod.rs:33074 assert this invariant)
When it happens
Trigger: deliver_announcement("telegra.work", ...), deliver_announcement("qq.main", ...), or "matrix.home" when no live matrix instance is registered; any channel type outside the compiled arm list.
Common situations: Typos in cron channel refs; using a channel type that only supports live-registered delivery while it is offline; enum/schema drift where config accepts a type the delivery match was never extended for.
Related errors
- git channel `events` routing table ignores every event type;
- [channels.lark.{alias}] has use_feishu=false but cron channe
- Webhook channel requires the `channel-webhook` feature
- wecom_ws channel is not connected
- approval route '{route}' is not 'channel:recipient' (e.g. 'd
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/f0cec62f002a4ac1.
Report an issue: GitHub.