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

matrix: not a room id or alias: {id_or_alias}

Error message

matrix: not a room id or alias: {id_or_alias}

What it means

resolve_room accepts exactly two recipient shapes: room ids starting with '!' (parsed locally) and room aliases starting with '#' (resolved via the homeserver behind a cache). Anything else - user ids starting with '@', bare room names, URLs - is rejected up front. Note normalize_recipient first strips a '||'-suffixed form with a warning, so the value echoed in the error is the bare recipient that failed the prefix check.

Source

Thrown at crates/zeroclaw-channels/src/matrix.rs:1955

        id_or_alias: &str,
    ) -> Result<OwnedRoomId> {
        let (id_or_alias, normalized) = normalize_recipient(id_or_alias);
        if normalized {
            ::zeroclaw_log::record!(
                WARN,
                ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
                    .with_outcome(::zeroclaw_log::EventOutcome::Unknown)
                    .with_attrs(::serde_json::json!({"id_or_alias": id_or_alias})),
                "matrix: recipient contains `||`; using as the room target. Update channels.matrix or cron `delivery.to` to a plain room id/alias to silence this warning."
            );
        }
        if id_or_alias.starts_with('!') {
            return id_or_alias
                .parse::<matrix_sdk::ruma::OwnedRoomId>()
                .with_context(|| format!("parse room id {id_or_alias}"));
        }
        if !id_or_alias.starts_with('#') {
            bail!("matrix: not a room id or alias: {id_or_alias}");
        }
        if let Some(id) = cache.read().await.get(id_or_alias) {
            return Ok(id.clone());
        }
        let alias: &RoomAliasId = id_or_alias
            .try_into()
            .with_context(|| format!("parse room alias {id_or_alias}"))?;
        let resp = client
            .resolve_room_alias(alias)
            .await
            .with_context(|| format!("resolve room alias {id_or_alias}"))?;
        cache
            .write()
            .await
            .insert(id_or_alias.to_string(), resp.room_id.clone());
        Ok(resp.room_id)
    }
}

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Use the full alias form #room:example.org or the opaque room id form !xxxx:example.org.
  2. Copy the canonical value from a Matrix client's room settings (room address or internal room id).
  3. Fix the config key that produced the bad recipient - it is echoed verbatim in the error.
  4. If you meant to DM a user, create or join a room with them first and use that room's id or alias.

Example fix

# before: cron delivery target is a user id
[cron.check-in]
delivery.to = "@alice:example.org"

# after: use a room alias (or room id)
delivery.to = "#zeroclaw-ops:example.org"
Defensive patterns

Strategy: validation

Validate before calling

fn is_matrix_room_ref(recipient: &str) -> bool {
    let s = recipient.trim();
    s.starts_with('!') || s.starts_with('#')
}

for peer in peer_list {
    assert!(is_matrix_room_ref(&peer), "recipient {peer} is not a room id (!...) or alias (#room:server)");
}

Type guard

fn as_room_id_or_alias(s: &str) -> Option<&str> {
    let t = s.trim();
    (t.starts_with('!') || t.starts_with('#')).then_some(t)
}

Prevention

When it happens

Trigger: Calling any send or resolve path with a recipient that starts with neither '!' nor '#': '@user:example.org' (a user id), 'ops-room' (bare name), or a pasted URL - sourced from channels.matrix config, cron delivery.to, or an upstream message's channel field.

Common situations: Confusing a Matrix user id with a room id; pasting a room's display name instead of its canonical alias; cron or delivery config holding '#ops' without the homeserver suffix; peer list entries that were never canonicalized.

Related errors


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