Hmbown/CodeWhale · error

Agent Mail can be canceled only while queued

Error message

Agent Mail can be canceled only while queued (status: {:?})

What it means

Agent Mail can only be canceled while it is still Queued. Once delivery has started (Delivering), completed (Delivered/Read), or been canceled (Canceled), the cancel operation refuses with the current status in the message. This keeps the status machine monotonic — you cannot un-send mail that is already in flight or delivered.

Solutions

  1. Check the envelope status is Queued before requesting cancellation.
  2. If the status is Delivered/Read, accept that the mail cannot be recalled and inform the user.
  3. Handle retries: a canceled mail is idempotently returned as-is, so re-cancel is safe; only in-flight/delivered statuses error.
  4. Minimize the queue→cancel window by issuing the cancel promptly after send.

Example fix

// before
match mail.status {
    AgentMailStatus::Queued => runtime.cancel_agent_mail(thread_id, id).await?,
    _ => runtime.cancel_agent_mail(thread_id, id).await?, // may bail
}
// after
if mail.status == AgentMailStatus::Queued {
    runtime.cancel_agent_mail(thread_id, id).await?;
} else {
    eprintln!("mail already {:?}; cannot cancel", mail.status);
}
Defensive patterns

Strategy: validation

Validate before calling

let mail = store.load_agent_mail(message_id)?;
if mail.status != AgentMailStatus::Queued {
    anyhow::bail!("cannot cancel: status is {:?}", mail.status);
}

Type guard

fn is_cancelable(status: &AgentMailStatus) -> bool { matches!(status, AgentMailStatus::Queued) }

Try / catch

match runtime.cancel_agent_mail(thread_id, message_id).await {
    Ok(env) => info!("canceled while {:?}", env.status),
    Err(e) if e.to_string().contains("only while queued") => info!("mail already left the queue; cannot cancel"),
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Calling the cancel-agent-mail flow when the envelope's status is Delivering, Delivered, Read, or Canceled.

Common situations: A race where delivery completes between the UI showing 'pending' and the user clicking cancel; retrying a cancel after it already succeeded (status now Canceled — that path returns Ok silently); canceling mail the agent has already started consuming.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/f9835accf6def5a3. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/runtime_threads.rs:6504

        thread_id: &str,
        message_id: &AgentMailMessageId,
    ) -> Result<AgentMailEnvelope> {
        let thread = self.get_thread(thread_id).await?;
        let address = agent_mail_address(&self.store.owner_id, &thread)?;
        let envelope = {
            let _mail_mutation = self.store.mail_mutation.lock();
            let mut envelope = self.store.load_agent_mail(message_id)?;
            if envelope.destination != address {
                bail!("Agent Mail ownership denied: message does not belong to this destination");
            }
            match envelope.status {
                AgentMailStatus::Canceled => envelope,
                AgentMailStatus::Queued => {
                    envelope.status = AgentMailStatus::Canceled;
                    self.store.save_agent_mail(&envelope)?;
                    envelope
                }
                _ => bail!(
                    "Agent Mail can be canceled only while queued (status: {:?})",
                    envelope.status
                ),
            }
        };
        self.emit_agent_mail_event(AGENT_MAIL_EVENT_CANCELED, &envelope)
            .await?;
        Ok(envelope)
    }

    /// Claim and project one envelope into the existing destination turn
    /// queue. A busy thread keeps queued mail untouched; retryable failures are
    /// claimed again only below the bounded attempt ceiling.
    pub async fn deliver_agent_mail(
        &self,
        thread_id: &str,
        message_id: &AgentMailMessageId,
    ) -> Result<(AgentMailEnvelope, Option<TurnRecord>)> {

View on GitHub (pinned to 73e0f67d83)