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

matrix: partial draft state unavailable

Error message

matrix: partial draft state unavailable

What it means

Matrix draft bookkeeping keeps a per-room State enum with distinct variants for partial, single-message and multi-message draft modes. insert_partial may only run while the room is in State::Partial; the let-else destructure fails on any other variant and aborts. The error signals a mode mismatch: a partial draft was recorded after the room already moved to (or never entered) partial mode.

Source

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

    impl State {
        /// Create the draft store matching the immutable Matrix stream mode.
        pub(super) fn for_stream_mode(mode: MatrixStreamMode) -> Self {
            match mode {
                MatrixStreamMode::Off => Self::Off,
                MatrixStreamMode::Partial => Self::Partial(HashMap::new()),
                MatrixStreamMode::SingleMessage => Self::Single(HashMap::new()),
                MatrixStreamMode::MultiMessage => Self::Multi(HashMap::new()),
            }
        }
    }

    pub(super) fn insert_partial(
        state: &mut State,
        key: DraftKey,
        draft: PartialDraft,
    ) -> Result<()> {
        let State::Partial(drafts) = state else {
            bail!("matrix: partial draft state unavailable");
        };
        drafts.insert(key, draft);
        Ok(())
    }

    pub(super) fn partial_for_update<'a>(
        state: &'a mut State,
        key: &DraftKey,
    ) -> Option<&'a mut PartialDraft> {
        match state {
            State::Partial(drafts) => drafts.get_mut(key),
            _ => None,
        }
    }

    pub(super) fn take_partial(state: &mut State, key: &DraftKey) -> Option<PartialDraft> {
        match state {
            State::Partial(drafts) => drafts.remove(key),

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Check or drive the State variant before inserting: only call insert_partial once the room is in Partial mode.
  2. Audit the transition that moved the state out of Partial (finalize/cancel/upgrade) and make it drain pending partials or make the insert path variant-aware.
  3. In tests, construct the state in the Partial variant before calling insert_partial.

Example fix

// before
drafts::insert_partial(&mut state, key, draft)?;

// after
if !matches!(state, State::Partial(_)) {
    state = State::Partial(Default::default());
}
drafts::insert_partial(&mut state, key, draft)?;
Defensive patterns

Strategy: type-guard

Type guard

fn in_partial_state(state: &State) -> bool {
    matches!(state, State::Partial(_))
}

Prevention

When it happens

Trigger: Calling insert_partial(state, key, draft) when state is not State::Partial - e.g. the room already began a single-message or multi-message lifecycle, or its state was finalized/cancelled and reset, while a partial-draft path still tries to insert.

Common situations: Two draft lifecycles racing on one room (interleaved update/finalize); a state transition that moves the room out of Partial without draining pending partial inserts; tests driving insert_partial against a freshly constructed non-Partial state (the *_for_same_room_concurrency tests pin this).

Related errors


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