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

interaction reply target unknown or expired (id {interaction

Error message

interaction reply target unknown or expired (id {interaction_id})

What it means

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]).

Source

Thrown at crates/zeroclaw-channels/src/discord/mod.rs:1696

        "discord"
    }

    fn self_handle(&self) -> Option<String> {
        Self::bot_user_id_from_token(&self.bot_token)
    }

    fn self_addressed_mention(&self) -> Option<String> {
        self.self_handle().map(|id| format!("<@{id}>"))
    }

    async fn send(&self, message: &SendMessage) -> anyhow::Result<()> {
        if let Some(interaction_id) = parse_discord_interaction_target(&message.recipient) {
            let pending = {
                let guard = self.pending_interactions.lock();
                guard.get(interaction_id).cloned()
            };
            let Some(pending) = pending else {
                anyhow::bail!("interaction reply target unknown or expired (id {interaction_id})");
            };
            if pending.created.elapsed() > INTERACTION_TOKEN_TTL {
                anyhow::bail!("interaction followup token expired (id {interaction_id}, >15min)");
            }
            let raw = crate::util::strip_tool_call_tags(&message.content);
            let (content, embeds, _embed_failures, _embeds_truncated) =
                prepare_outgoing_embeds(&raw, self.workspace_dir.as_deref());
            let (content, component_rows) = parse_component_markers(&content);
            let component_action_rows = if component_rows.is_empty() {
                Vec::new()
            } else {
                self.build_marker_components(&component_rows)
            };
            let client = self.http_client();
            return deliver_interaction_answer(
                &client,
                &pending.app_id,
                &pending.token,

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. After a restart there is no recovery for old interactions — ask the user to re-invoke the command so a fresh pending entry registers
  2. Keep interaction replies on the instance that received the interaction, or externalize pending_interactions to shared storage
  3. Route by the exact reply_target the channel produced (discord_interaction_reply_target format), never a hand-built id
  4. On this error, fall back to a normal channel send when a real channel id is known
Defensive patterns

Strategy: validation

Validate before calling

// Before sending, confirm this process actually received the interaction:
// keep your own set of live interaction ids and check membership,
// or skip sends whose recipient starts with the sentinel after a restart.

Type guard

fn is_interaction_target(recipient: &str) -> bool {
    recipient
        .strip_prefix("interaction:")
        .is_some_and(|id| !id.is_empty() && !id.contains(':'))
}

Try / catch

match channel.send(&msg).await {
    Err(e) if e.to_string().contains("unknown or expired") => {
        // registry state is gone — deliver via a normal channel id instead
    }
    r => r?,
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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