zeroclaw-labs/zeroclaw · error · anyhow::Error

Cannot persist empty {channel_type} identity

Error message

Cannot persist empty {channel_type} identity

What it means

merge_external_peer is the single writer that persists a QR-paired identity (WeChat, WhatsApp Web) into config.toml peer groups. It first trims the identity and refuses to persist an empty one — an empty external peer would create a peer_groups entry that authorizes nothing but pollutes the canonical config. The bail happens before any channel-registry or group lookup.

Source

Thrown at crates/zeroclaw-channels/src/identity_persist.rs:58

///   `<channel_type>_<alias>` key with `channel = "<channel_type>.<alias>"`
///   — the shape WeChat pairing established. If that key is already taken
///   by a group whose `channel` points elsewhere, the merge is rejected:
///   appending there would store the identity where the reader for this
///   channel never looks (and another channel's reader would pick it up).
///
/// Existing group entries (agents, other peers) are preserved.
pub(crate) fn merge_external_peer(
    cfg: &mut Config,
    channel_type: &str,
    alias: &str,
    identity: &str,
) -> anyhow::Result<bool> {
    use zeroclaw_config::multi_agent::{PeerGroupConfig, PeerUsername};
    use zeroclaw_config::providers::ChannelRef;

    let normalized = identity.trim();
    if normalized.is_empty() {
        anyhow::bail!("Cannot persist empty {channel_type} identity");
    }
    // Existence comes from the canonical channel registry
    // (`Config::channels_by_alias()` walks every configured
    // `[channels.<type>.<alias>]` block regardless of type), so this writer
    // holds no channel-type list of its own and a future QR-pairing channel
    // needs no edit here.
    let configured = cfg
        .channels_by_alias()
        .iter()
        .any(|info| info.channel_type == channel_type && info.alias == alias);
    if !configured {
        anyhow::bail!(
            "Missing [channels.{channel_type}.{alias}] section in config.toml — \
             configure the channel before pairing"
        );
    }

    // Already authorized through any group the reader matches (including

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Log/inspect the identity the pairing flow extracted before persisting — if it is blank, the pairing data extraction upstream is the real bug
  2. Guard the call site: skip persistence (and re-queue the pairing) when identity.trim().is_empty() instead of letting the bail propagate
  3. If this reproduces with a real QR scan, file it against the channel (WeChat/WhatsApp Web) — the scanner should never hand an empty identity to persistence

Example fix

// before
persist_external_peer(persist.as_deref(), "wechat", &alias, &identity).await?;

// after
let trimmed = identity.trim();
if trimmed.is_empty() {
    anyhow::bail!("pairing produced an empty wechat identity; refusing to persist");
}
persist_external_peer(persist.as_deref(), "wechat", &alias, trimmed).await?;
Defensive patterns

Strategy: validation

Validate before calling

// Caller-side guard before persisting a paired identity:
let identity = identity.trim();
if identity.is_empty() {
    anyhow::bail!("refusing to persist empty {channel_type} identity from pairing");
}
let changed = merge_external_peer(&mut cfg, channel_type, alias, identity)?;

Try / catch

if let Err(e) = persist_external_peer(persist.as_deref(), channel_type, alias, &identity).await {
    if e.to_string().contains("Cannot persist empty") {
        // pairing data extraction failed upstream; re-run pairing, do not write config
    } else { return Err(e); }
}

Prevention

When it happens

Trigger: persist_external_peer / merge_external_peer is called with an identity that is "" or only whitespace — e.g. a completed QR pairing whose extracted phone number / wxid came back empty, or a test calling merge with a blank string. The tests merge_creates_group_in_the_wechat_shape and merge_rejects_conventional_key_with_mismatched_channel_ref exercise the neighbouring paths of this same function.

Common situations: A pairing handshake that succeeded at the transport level but yielded no usable account identifier; a channel implementation change that renamed the field feeding identity; hand-written test or CLI code passing String::new().

Related errors


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