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 (includingView on GitHub (pinned to 88bb9c8533)
Solutions
- Log/inspect the identity the pairing flow extracted before persisting — if it is blank, the pairing data extraction upstream is the real bug
- Guard the call site: skip persistence (and re-queue the pairing) when identity.trim().is_empty() instead of letting the bail propagate
- 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
- Never feed raw pairing payloads into persistence — extract and assert the identifier first
- Log (without the identifier itself) that extraction produced a value, before calling persist_external_peer
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
- Missing [channels.{channel_type}.{alias}] section in config.
- media aes_key must decode to 16 raw bytes or 32 hex chars, g
- inbound attachment exceeds {} MB limit
- WhatsApp interactive buttons require 1..=3 options (got {});
- WhatsApp interactive list requires at least one section
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/9b228edb656fc458.
Report an issue: GitHub.