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

matrix: draft message id is empty

Error message

matrix: draft message id is empty

What it means

draft_key builds the (room_id, draft_id) key Matrix draft bookkeeping uses to correlate in-flight drafts with their source events across update, finalize and cancel. It trims the incoming draft id and rejects empty ids, because an empty key would make every draft in a room collide on lookup. Hitting it means a caller passed an id that is empty or whitespace-only - a caller contract violation, not a network or server problem.

Source

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

    use matrix_sdk::ruma::{OwnedEventId, OwnedRoomId};
    use zeroclaw_runtime::agent::loop_::{
        DRAFT_PLACEHOLDER, REASONING_FULL_PREFIX, is_thinking_status_text, thinking_status_round,
    };

    use super::{MatrixStreamMode, markers};

    const MULTI_MESSAGE_SYNTHETIC_PREFIX: &str = "multi_message_synthetic:";

    #[derive(Debug, Clone, PartialEq, Eq, Hash)]
    pub(super) struct DraftKey {
        room_id: OwnedRoomId,
        draft_id: String,
    }

    pub(super) fn draft_key(room_id: OwnedRoomId, draft_id: &str) -> Result<DraftKey> {
        let draft_id = draft_id.trim();
        if draft_id.is_empty() {
            bail!("matrix: draft message id is empty");
        }
        Ok(DraftKey {
            room_id,
            draft_id: draft_id.to_string(),
        })
    }

    pub(super) fn new_multi_message_draft_id() -> String {
        format!(
            "{MULTI_MESSAGE_SYNTHETIC_PREFIX}{}",
            uuid::Uuid::new_v4().simple()
        )
    }

    #[derive(Debug, Clone)]
    pub(super) struct PartialDraft {
        pub event_id: OwnedEventId,
        pub thread_anchor: Option<OwnedEventId>,

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Trace the caller and confirm the source Matrix event actually carries a non-empty message id before draft_key runs.
  2. Validate and reject empty ids at the ingestion boundary, before the draft path is entered.
  3. If the id comes from user or config input, validate it at parse time rather than deep in draft bookkeeping.

Example fix

// before
let key = draft_key(room_id, maybe_id.as_deref().unwrap_or(""))?;

// after
let raw = maybe_id.as_deref().map(str::trim).unwrap_or("");
if raw.is_empty() {
    anyhow::bail!("upstream event for room {room_id} has no message id; skipping draft");
}
let key = draft_key(room_id, raw)?;
Defensive patterns

Strategy: validation

Validate before calling

fn usable_draft_id(id: &str) -> bool {
    !id.trim().is_empty()
}

// gate before building the key
if !usable_draft_id(&event_id) {
    return Ok(()); // skip draft bookkeeping for id-less events
}

Type guard

fn non_empty_draft_id(id: &str) -> Option<&str> {
    let trimmed = id.trim();
    (!trimmed.is_empty()).then_some(trimmed)
}

Prevention

When it happens

Trigger: Calling draft_key(room_id, draft_id) with a draft id that is empty or only whitespace: the message-id field of an upstream Matrix event was blank, or adapter/test code constructed the DraftKey from unsanitized input.

Common situations: Forwarding event ids from malformed or synthetic Matrix events (dev bridges, fixtures with missing ids); refactors that pass a placeholder empty string; reading the wrong field of an event and defaulting it to empty.

Related errors


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