Hmbown/CodeWhale · error · anyhow::Error
Agent Mail accepts a bounded handoff summary, not a raw tran
Error message
Agent Mail accepts a bounded handoff summary, not a raw transcript
What it means
queue_agent_mail rejected the request because agent_mail_looks_like_raw_transcript matched the summary: markers such as '<turn_meta>', '<assistant', '<tool_result', '"messages":', '"role":"assistant"', or lines starting with 'assistant:' / 'system:' / 'tool:' / 'tool_result:'. Agent Mail is a typed, bounded handoff mechanism - the summary must be a distilled handoff (max 2048 bytes), not a pasted conversation transcript.
Source
Thrown at crates/tui/src/runtime_threads.rs:3593
/// Remove the goal for a thread. Returns `true` if a goal existed.
pub async fn remove_goal(&self, thread_id: &str) -> Result<bool> {
let thread_id = thread_id.to_string();
let store = self.store.clone();
tokio::task::spawn_blocking(move || store.delete_goal(&thread_id))
.await
.context("goal delete task panicked")?
}
/// Persist one canonical Agent Mail envelope in the runtime store. The
/// caller-supplied id is an idempotency key: an exact replay returns the
/// existing lifecycle record, while conflicting intent fails closed.
pub async fn queue_agent_mail(
&self,
mut request: AgentMailSendRequest,
) -> Result<AgentMailSendResponse> {
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");
}View on GitHub (pinned to 0c42157ee5)
Solutions
- Summarize the handoff in prose: goal, state, next action - no transcript markers
- If the text genuinely must mention 'assistant:' etc., rephrase the line so it does not start with the role prefix (the check is line-prefix based)
- Move raw context into evidence references (evidence refs with bounded labels) instead of the summary body
- Keep the summary under MAX_AGENT_MAIL_SUMMARY_BYTES (2048) so sanitize/truncate does not distort it
Example fix
// before request.summary = serde_json::to_string(&messages)?; // contains "messages": / "role":"assistant" // after request.summary = "Task: fix login bug. Auth flow traced; suspect token refresh. Next: add regression test for expired refresh_token.".to_string();
Defensive patterns
Strategy: validation
Validate before calling
// Reject transcript-shaped summaries before calling the API.
fn looks_like_transcript(summary: &str) -> bool {
let lower = summary.to_ascii_lowercase();
["<turn_meta>", "<assistant", "<tool_result", "\"messages\":", "\"role\":\"assistant\"", "\"role\": \"assistant\""]
.iter().any(|m| lower.contains(m))
|| lower.lines().any(|l| {
let l = l.trim_start();
l.starts_with("assistant:") || l.starts_with("system:")
|| l.starts_with("tool:") || l.starts_with("tool_result:")
})
}
if looks_like_transcript(&request.summary) {
return Err(anyhow::anyhow!("summary must be a distilled handoff, not a transcript"));
} Try / catch
// Cheap catch with rephrase guidance.
match manager.queue_agent_mail(request).await {
Ok(resp) => Ok(resp),
Err(e) if e.to_string().contains("bounded handoff summary") => {
Err(anyhow::anyhow!("agent mail summary rejected: rewrite as a short prose handoff"))
}
Err(e) => Err(e),
} Prevention
- Summaries are for goal/state/next-action prose, <= 2048 bytes
- Put raw context in evidence references, not the summary body
- Avoid lines that begin with 'assistant:'/'system:'/'tool:' - the detector is line-prefix based
When it happens
Trigger: Calling queue_agent_mail with a summary built by serializing the message history (serde_json of a messages array) or concatenating transcript lines with role prefixes. Heuristic at runtime_threads.rs:406-428; check at :3592-3594, before sanitization and validate().
Common situations: An agent prompted to 'hand off context' pastes its transcript; a wrapper forwards raw provider payloads; a summary legitimately starting with a line like 'system: ...' trips the prefix detector.
Related errors
- host cannot be empty
- continual harness entry id cannot be empty
- Patch is empty.
- No patch file provided and stdin is empty.
- Agent Mail source and destination threads must differ
AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20).
Data as JSON: /api/errors/c90fa28e722860bb.
Report an issue: GitHub.