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
- Trace the caller and confirm the source Matrix event actually carries a non-empty message id before draft_key runs.
- Validate and reject empty ids at the ingestion boundary, before the draft path is entered.
- 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
- Never default an event or message id to an empty string; make the field required at the parse boundary.
- Skip draft bookkeeping entirely for events without ids instead of synthesizing keys.
- In tests, always seed draft ids - fixtures with blank ids produce this exact bail.
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
- matrix: partial draft state unavailable
- matrix: single-message draft state unavailable
- matrix: multi-message draft state unavailable
- matrix: not a room id or alias: {id_or_alias}
- matrix: `homeserver` is required
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/e957053f05a25922.
Report an issue: GitHub.