{"record":{"id":"b69f13707f1bb3dc","repo":"zeroclaw-labs/zeroclaw","slug":"matrix-room-recipient-is-not-in-joined-state","errorCode":null,"errorMessage":"matrix: room {recipient} is not in joined state","messagePattern":"matrix: room (.+?) is not in joined state","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/matrix.rs","lineNumber":3760,"sourceCode":"\n    pub(super) async fn resolve_joined_room(\n        client: &Client,\n        cache: &Arc<TokioRwLock<HashMap<String, OwnedRoomId>>>,\n        recipient: &str,\n    ) -> Result<Room> {\n        let id = client::resolve_room(client, cache, recipient).await?;\n        let room = client.get_room(&id).ok_or_else(|| {\n            ::zeroclaw_log::record!(\n                ERROR,\n                ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Fail)\n                    .with_outcome(::zeroclaw_log::EventOutcome::Failure)\n                    .with_attrs(::serde_json::json!({\"recipient\": recipient})),\n                \"matrix: bot is not in room\"\n            );\n            anyhow::Error::msg(format!(\"matrix: bot is not in room {recipient}\"))\n        })?;\n        if room.state() != RoomState::Joined {\n            bail!(\"matrix: room {recipient} is not in joined state\");\n        }\n        Ok(room)\n    }\n\n    #[derive(Clone, Copy, Debug, PartialEq, Eq)]\n    pub(super) enum AttachmentKind {\n        Auto,\n        Image,\n        Audio,\n        Video,\n        File,\n        Voice,\n    }\n\n    async fn upload_attachment(\n        room: &Room,\n        att: &MediaAttachment,\n        kind: AttachmentKind,","sourceCodeStart":3742,"sourceCodeEnd":3778,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/matrix.rs#L3742-L3778","documentation":"resolve_joined_room is the pre-send gate: it resolves the recipient to a room id, looks the Room up in the SDK cache (a missing room fails earlier as 'matrix: bot is not in room'), then requires room.state() == RoomState::Joined. A room in Invited, Left, Knocked or Banned state aborts here, because Matrix only lets a client send events to rooms it has joined.","triggerScenarios":"Sending or reacting when the bot's cached state for the room is anything but Joined: an invite not yet accepted (Invited), the bot left or was kicked (Left/Banned), or the state cache has not hydrated from the initial sync yet at startup.","commonSituations":"Freshly invited bot whose auto-join has not run or is disabled; bot kicked mid-conversation while queued messages were pending; sends issued immediately after startup before the first sync completes; stale room cache after re-login.","solutions":["Ensure invites are accepted before dispatch: join invited rooms when the invite state appears, or have an admin re-invite the bot and let it join.","If the bot was kicked or left, get it re-invited, re-join, then retry the send.","After startup, wait for the first sync to complete before dispatching, or retry the send once after a short delay so room state hydrates.","Confirm the recipient resolves to the room the bot actually occupies - parallel rooms can share a similar alias."],"exampleFix":"// before: send immediately after startup\nlet room = resolve_joined_room(&client, &cache, recipient).await?;\n\n// after: accept pending invites and let sync settle first\nfor invited in client.invited_rooms() {\n    invited.join().await.ok();\n}\ntokio::time::sleep(std::time::Duration::from_secs(2)).await; // allow state sync\nlet room = resolve_joined_room(&client, &cache, recipient).await?;","handlingStrategy":"validation","validationCode":"async fn ensure_joined(client: &Client, room_id: &str) -> anyhow::Result<()> {\n    let id: OwnedRoomId = room_id.parse()?;\n    let room = client\n        .get_room(&id)\n        .ok_or_else(|| anyhow::anyhow!(\"room {room_id} not in cache\"))?;\n    if room.state() != RoomState::Joined {\n        room.join().await?; // accepts a pending invite or re-joins a left room\n        anyhow::ensure!(room.state() == RoomState::Joined, \"still not joined: {room_id}\");\n    }\n    Ok(())\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Handle invite state changes in the sync loop: auto-join rooms from allowlisted peers.","Gate outbound queues on a completed initial sync.","Monitor membership kicks via sync logs so operators can re-invite before sends fail.","Retry once on this error after a short delay - cache hydration races are the benign case."],"tags":["matrix","room-state","membership","join","send"],"backgroundTag":"room-not-joined","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}