Hmbown/CodeWhale · error · anyhow::Error

Agent Mail source and destination threads must differ

Error message

Agent Mail source and destination threads must differ

What it means

queue_agent_mail requires source_thread_id != destination_thread_id. Mail is a cross-thread/cross-task handoff primitive; self-addressed mail would let a thread trigger its own turn (a mail loop) and is rejected before any persistence happens.

Source

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

        if agent_mail_looks_like_raw_transcript(&request.summary) {
            bail!("Agent Mail accepts a bounded handoff summary, not a raw transcript");
        }
        request.summary = sanitize_agent_mail_text(&request.summary, MAX_AGENT_MAIL_SUMMARY_BYTES);
        request.sender.display_label = sanitize_agent_mail_text(
            &request.sender.display_label,
            codewhale_protocol::agent_mail::MAX_AGENT_MAIL_DISPLAY_LABEL_BYTES,
        );
        for evidence in &mut request.evidence {
            if let Some(label) = evidence.label.as_mut() {
                *label = sanitize_agent_mail_text(
                    label,
                    codewhale_protocol::agent_mail::MAX_AGENT_MAIL_EVIDENCE_LABEL_BYTES,
                );
            }
        }
        request.validate().map_err(|error| anyhow!(error))?;
        if request.source_thread_id == request.destination_thread_id {
            bail!("Agent Mail source and destination threads must differ");
        }

        let source_thread = self.get_thread(&request.source_thread_id).await?;
        let destination_thread = self.get_thread(&request.destination_thread_id).await?;
        let source = agent_mail_address(&self.store.owner_id, &source_thread)?;
        let destination = agent_mail_address(&self.store.owner_id, &destination_thread)?;
        if source.owner_id != destination.owner_id
            || source.workspace_id != destination.workspace_id
        {
            bail!(
                "Agent Mail ownership denied: source and destination must belong to the same runtime owner and workspace"
            );
        }
        let expected_sender = agent_mail_sender_identity(&source_thread)?;
        if request.sender.identity != expected_sender {
            bail!(
                "Agent Mail ownership denied: sender identity does not own the source task/session"
            );

View on GitHub (pinned to 0c42157ee5)

Solutions

  1. Fix the caller: destination must be a different thread - check the two ids differ before sending
  2. If a self-reminder was intended, use the thread's normal input path instead of Agent Mail
  3. Log both ids at the call site to catch the copy-paste/variable-reuse origin
  4. Add a debug_assert!(source != destination) in your wrapper before queue_agent_mail

Example fix

// before
let request = AgentMailSendRequest {
    source_thread_id: thread_id.clone(),
    destination_thread_id: thread_id.clone(), // bug: same thread
    ..
};

// after
let request = AgentMailSendRequest {
    source_thread_id: thread_id.clone(),
    destination_thread_id: peer_thread_id.clone(),
    ..
};
Defensive patterns

Strategy: validation

Validate before calling

if request.source_thread_id == request.destination_thread_id {
    return Err(anyhow::anyhow!("agent mail requires distinct source and destination threads"));
}
manager.queue_agent_mail(request).await?;

Prevention

When it happens

Trigger: Constructing AgentMailSendRequest with the same thread id on both sides - typically a variable reuse bug (source copied into destination) or an agent mailing its own thread because it lost track of its identity. Checked at runtime_threads.rs:3610-3612 after validate().

Common situations: An orchestrator iterates threads and accidentally sets destination from the loop variable it used for source; an agent that does not know its own thread_id defaults both to the current thread.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20). Data as JSON: /api/errors/f317d9231fb6a95f. Report an issue: GitHub.