{"record":{"id":"3e4f843514190a13","repo":"zeroclaw-labs/zeroclaw","slug":"cannot-persist-empty-telegram-identity","errorCode":null,"errorMessage":"Cannot persist empty Telegram identity","messagePattern":"Cannot persist empty Telegram identity","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/telegram.rs","lineNumber":1106,"sourceCode":"        self\n    }\n\n    async fn persist_allowed_identity(&self, identity: &str) -> anyhow::Result<()> {\n        use zeroclaw_config::multi_agent::{PeerGroupConfig, PeerUsername};\n\n        let Some(config) = &self.persist else {\n            ::zeroclaw_log::record!(\n                WARN,\n                ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)\n                    .with_outcome(::zeroclaw_log::EventOutcome::Unknown)\n                    .with_attrs(::serde_json::json!({\"identity\": identity})),\n                \"paired identity not persisted (no persistence handle wired)\"\n            );\n            return Ok(());\n        };\n        let normalized = Self::normalize_identity(identity);\n        if normalized.is_empty() {\n            anyhow::bail!(\"Cannot persist empty Telegram identity\");\n        }\n        let group_name = format!(\"telegram_{}\", self.alias);\n        let channel_ref: zeroclaw_config::providers::ChannelRef =\n            format!(\"telegram.{}\", self.alias).into();\n        let snapshot = {\n            let mut cfg = config.write();\n            if !cfg.channels.telegram.contains_key(&self.alias) {\n                anyhow::bail!(\n                    \"Missing [channels.telegram.{}] section. Run `zeroclaw config set channels.telegram.<alias>.bot_token <token>` to configure.\",\n                    self.alias\n                );\n            }\n            let group = cfg\n                .peer_groups\n                .entry(group_name)\n                .or_insert_with(|| PeerGroupConfig {\n                    channel: channel_ref,\n                    ..PeerGroupConfig::default()","sourceCodeStart":1088,"sourceCodeEnd":1124,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/telegram.rs#L1088-L1124","documentation":"persist_identity() normalizes the paired Telegram identity (Self::normalize_identity, which trims and keeps only usable fields such as the numeric id) before writing it into the telegram_<alias> config group. If normalization yields an empty value, the write aborts instead of persisting a blank identity entry.","triggerScenarios":"A pairing update whose `message.from` is absent — channel posts, anonymous group admins, messages from linked channels — so the constructed identity has no id; a manually built or test Identity with empty/whitespace id and username reaching persist_identity.","commonSituations":"Pairing attempted from a channel post or anonymous-admin message instead of a private chat; test harnesses constructing Identity::default(); upstream parsing changes that stopped populating the id field.","solutions":["Log the raw identity before persisting and confirm the field normalize_identity keeps (the user id) is present and non-blank.","Reject the pairing earlier — in the /start handler — when update.message.from is None, instead of building an empty identity.","For anonymous admins/channel posts, derive identity from sender_chat or chat.id rather than a missing from.","Trim-check inputs: whitespace-only ids normalize to empty and trigger this bail."],"exampleFix":"// before\nlet identity = maybe_from.map(|f| Identity::from(f)).unwrap_or_default();\nchannel.persist_identity(identity).await?;\n\n// after\nlet Some(from) = maybe_from else {\n    anyhow::bail!(\"cannot pair: message has no sender (channel post or anonymous admin)\");\n};\nchannel.persist_identity(Identity::from(from)).await?;","handlingStrategy":"validation","validationCode":"let normalized = TelegramChannel::normalize_identity(&identity);\nif normalized.is_empty() {\n    anyhow::bail!(\"cannot pair: identity has no usable id/username fields\");\n}\nchannel.persist_identity(identity).await?;","typeGuard":"fn has_persistable_identity(identity: &Identity) -> bool {\n    identity.id.trim().is_empty() == false\n        || identity.username.as_deref().map_or(false, |u| !u.trim().is_empty())\n}","tryCatchPattern":"if let Err(e) = channel.persist_identity(identity).await {\n    if e.to_string().contains(\"empty Telegram identity\") {\n        tracing::warn!(\"skipping persist for identity without sender fields\");\n    } else {\n        return Err(e);\n    }\n}","preventionTips":["Reject pairing at the /start handler when update.message.from is None (channel posts, anonymous admins).","Never construct a default/empty Identity for the pairing flow; bail at construction instead.","Add unit tests that normalize_identity on an id-less identity is detected before persist."],"tags":["telegram","identity","pairing","config","validation"],"backgroundTag":"identity-validation-failed","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}