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

  1. Check the type spelling against the supported list and use the exact dotted form <type>.<alias>
  2. 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
  3. 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

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


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/f0cec62f002a4ac1. Report an issue: GitHub.