{"record":{"id":"6460824f8f85897d","repo":"zeroclaw-labs/zeroclaw","slug":"interaction-reply-target-unknown-or-expired-id-i","errorCode":null,"errorMessage":"interaction reply target unknown or expired (id {interaction_id})","messagePattern":"interaction reply target unknown or expired \\(id (.+?)\\)","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/discord/mod.rs","lineNumber":1696,"sourceCode":"        \"discord\"\n    }\n\n    fn self_handle(&self) -> Option<String> {\n        Self::bot_user_id_from_token(&self.bot_token)\n    }\n\n    fn self_addressed_mention(&self) -> Option<String> {\n        self.self_handle().map(|id| format!(\"<@{id}>\"))\n    }\n\n    async fn send(&self, message: &SendMessage) -> anyhow::Result<()> {\n        if let Some(interaction_id) = parse_discord_interaction_target(&message.recipient) {\n            let pending = {\n                let guard = self.pending_interactions.lock();\n                guard.get(interaction_id).cloned()\n            };\n            let Some(pending) = pending else {\n                anyhow::bail!(\"interaction reply target unknown or expired (id {interaction_id})\");\n            };\n            if pending.created.elapsed() > INTERACTION_TOKEN_TTL {\n                anyhow::bail!(\"interaction followup token expired (id {interaction_id}, >15min)\");\n            }\n            let raw = crate::util::strip_tool_call_tags(&message.content);\n            let (content, embeds, _embed_failures, _embeds_truncated) =\n                prepare_outgoing_embeds(&raw, self.workspace_dir.as_deref());\n            let (content, component_rows) = parse_component_markers(&content);\n            let component_action_rows = if component_rows.is_empty() {\n                Vec::new()\n            } else {\n                self.build_marker_components(&component_rows)\n            };\n            let client = self.http_client();\n            return deliver_interaction_answer(\n                &client,\n                &pending.app_id,\n                &pending.token,","sourceCodeStart":1678,"sourceCodeEnd":1714,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/discord/mod.rs#L1678-L1714","documentation":"When a SendMessage recipient carries the interaction: sentinel, send() looks the id up in the in-memory pending_interactions map (Mutex<HashMap>) populated when interactions were received. This bail fires when the id is absent — the registry never knew it, dropped it after an earlier reply, or lost it. The map lives only in process memory, so any restart, crash, or different worker instance produces this error for targets that look valid. The 15-minute Discord TTL is a separate, later check ([56]).","triggerScenarios":"Process restarted between receiving the slash/button interaction and replying (map wiped); reply routed to a different instance behind a load balancer; replying to an interaction id captured in a previous run or copied from logs; entry already consumed by an earlier reply; sentinel id typo'd or fabricated.","commonSituations":"Multi-replica deployments without sticky routing or shared interaction state; dev iterating with restarts mid-conversation; long agent turns spanning a redeploy; replaying recorded messages.","solutions":["After a restart there is no recovery for old interactions — ask the user to re-invoke the command so a fresh pending entry registers","Keep interaction replies on the instance that received the interaction, or externalize pending_interactions to shared storage","Route by the exact reply_target the channel produced (discord_interaction_reply_target format), never a hand-built id","On this error, fall back to a normal channel send when a real channel id is known"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Before sending, confirm this process actually received the interaction:\n// keep your own set of live interaction ids and check membership,\n// or skip sends whose recipient starts with the sentinel after a restart.","typeGuard":"fn is_interaction_target(recipient: &str) -> bool {\n    recipient\n        .strip_prefix(\"interaction:\")\n        .is_some_and(|id| !id.is_empty() && !id.contains(':'))\n}","tryCatchPattern":"match channel.send(&msg).await {\n    Err(e) if e.to_string().contains(\"unknown or expired\") => {\n        // registry state is gone — deliver via a normal channel id instead\n    }\n    r => r?,\n}","preventionTips":["Assume any restart invalidates all pending interactions","Do not load-balance interaction replies across instances without shared state","Route only with the reply_target the channel itself produced"],"tags":["discord","interactions","state","restart","routing"],"backgroundTag":"pending-interaction-state-lost","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}