{"record":{"id":"9b228edb656fc458","repo":"zeroclaw-labs/zeroclaw","slug":"cannot-persist-empty-channel-type-identity","errorCode":null,"errorMessage":"Cannot persist empty {channel_type} identity","messagePattern":"Cannot persist empty (.+?) identity","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/identity_persist.rs","lineNumber":58,"sourceCode":"///   `<channel_type>_<alias>` key with `channel = \"<channel_type>.<alias>\"`\n///   — the shape WeChat pairing established. If that key is already taken\n///   by a group whose `channel` points elsewhere, the merge is rejected:\n///   appending there would store the identity where the reader for this\n///   channel never looks (and another channel's reader would pick it up).\n///\n/// Existing group entries (agents, other peers) are preserved.\npub(crate) fn merge_external_peer(\n    cfg: &mut Config,\n    channel_type: &str,\n    alias: &str,\n    identity: &str,\n) -> anyhow::Result<bool> {\n    use zeroclaw_config::multi_agent::{PeerGroupConfig, PeerUsername};\n    use zeroclaw_config::providers::ChannelRef;\n\n    let normalized = identity.trim();\n    if normalized.is_empty() {\n        anyhow::bail!(\"Cannot persist empty {channel_type} identity\");\n    }\n    // Existence comes from the canonical channel registry\n    // (`Config::channels_by_alias()` walks every configured\n    // `[channels.<type>.<alias>]` block regardless of type), so this writer\n    // holds no channel-type list of its own and a future QR-pairing channel\n    // needs no edit here.\n    let configured = cfg\n        .channels_by_alias()\n        .iter()\n        .any(|info| info.channel_type == channel_type && info.alias == alias);\n    if !configured {\n        anyhow::bail!(\n            \"Missing [channels.{channel_type}.{alias}] section in config.toml — \\\n             configure the channel before pairing\"\n        );\n    }\n\n    // Already authorized through any group the reader matches (including","sourceCodeStart":40,"sourceCodeEnd":76,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/identity_persist.rs#L40-L76","documentation":"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.","triggerScenarios":"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.","commonSituations":"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().","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"],"exampleFix":"// before\npersist_external_peer(persist.as_deref(), \"wechat\", &alias, &identity).await?;\n\n// after\nlet trimmed = identity.trim();\nif trimmed.is_empty() {\n    anyhow::bail!(\"pairing produced an empty wechat identity; refusing to persist\");\n}\npersist_external_peer(persist.as_deref(), \"wechat\", &alias, trimmed).await?;","handlingStrategy":"validation","validationCode":"// Caller-side guard before persisting a paired identity:\nlet identity = identity.trim();\nif identity.is_empty() {\n    anyhow::bail!(\"refusing to persist empty {channel_type} identity from pairing\");\n}\nlet changed = merge_external_peer(&mut cfg, channel_type, alias, identity)?;","typeGuard":null,"tryCatchPattern":"if let Err(e) = persist_external_peer(persist.as_deref(), channel_type, alias, &identity).await {\n    if e.to_string().contains(\"Cannot persist empty\") {\n        // pairing data extraction failed upstream; re-run pairing, do not write config\n    } else { return Err(e); }\n}","preventionTips":["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"],"tags":["identity-persist","qr-pairing","wechat","whatsapp","validation"],"backgroundTag":"empty-required-field","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}