{"record":{"id":"3a2e87ece72550a4","repo":"Hmbown/CodeWhale","slug":"agent-mail-can-be-marked-read-only-after-delivery","errorCode":null,"errorMessage":"Agent Mail can be marked read only after delivery","messagePattern":"Agent Mail can be marked read only after delivery","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/runtime_threads.rs","lineNumber":6472,"sourceCode":"        message_id: &AgentMailMessageId,\n    ) -> Result<AgentMailEnvelope> {\n        let thread = self.get_thread(thread_id).await?;\n        let address = agent_mail_address(&self.store.owner_id, &thread)?;\n        let envelope = {\n            let _mail_mutation = self.store.mail_mutation.lock();\n            let mut envelope = self.store.load_agent_mail(message_id)?;\n            if envelope.destination != address {\n                bail!(\"Agent Mail ownership denied: message does not belong to this destination\");\n            }\n            match envelope.status {\n                AgentMailStatus::Read => envelope,\n                AgentMailStatus::Delivered => {\n                    envelope.status = AgentMailStatus::Read;\n                    envelope.read_at = Some(Utc::now());\n                    self.store.save_agent_mail(&envelope)?;\n                    envelope\n                }\n                _ => bail!(\"Agent Mail can be marked read only after delivery\"),\n            }\n        };\n        self.emit_agent_mail_event(AGENT_MAIL_EVENT_READ, &envelope)\n            .await?;\n        Ok(envelope)\n    }\n\n    /// Withdraw a queued envelope before it starts delivery (#6176). Only\n    /// `Queued` mail can be canceled; anything that reached delivery keeps\n    /// its receipt. Re-canceling an already-canceled envelope is an\n    /// idempotent no-op returning the stored envelope.\n    pub async fn cancel_agent_mail(\n        &self,\n        thread_id: &str,\n        message_id: &AgentMailMessageId,\n    ) -> Result<AgentMailEnvelope> {\n        let thread = self.get_thread(thread_id).await?;\n        let address = agent_mail_address(&self.store.owner_id, &thread)?;","sourceCodeStart":6454,"sourceCodeEnd":6490,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/runtime_threads.rs#L6454-L6490","documentation":"Agent Mail envelopes move through a lifecycle (Queued → Delivering → Delivered → Read, plus Canceled). The `mark read` operation only accepts envelopes already in the Delivered state; anything else (Queued, Delivering, Canceled) is rejected with this bail. The library throws it to prevent marking mail as read before the recipient could have seen it, keeping the status machine and `read_at` timestamp meaningful.","triggerScenarios":"Calling the mark-agent-mail-read flow (runtime_threads.rs mark_read handler) with a message_id whose envelope status is Queued, Delivering, or Canceled instead of Delivered.","commonSituations":"Tool/UI double-invocation where read is requested twice (second call sees status already Read? no — Read passes through `_` too), racing a delivery that has not completed, or attempting to mark canceled mail read after a cancel.","solutions":["Check envelope.status == Delivered before calling the mark-read operation.","Deliver the mail first (run the deliver flow) and then mark it read.","If the mail was canceled, treat it as terminal and do not attempt read transitions.","Guard against double-marking: a status of Read already means it was read; return the existing envelope instead."],"exampleFix":"// before\nmail.markRead(message_id)?; // assumes delivered\n// after\nlet mail = store.load_agent_mail(message_id)?;\nif mail.status != AgentMailStatus::Delivered {\n    anyhow::bail!(\"refusing to mark read: status is {:?}\", mail.status);\n}\nmail.markRead(message_id)?;","handlingStrategy":"validation","validationCode":"let mail = store.load_agent_mail(message_id)?;\nif mail.status != AgentMailStatus::Delivered {\n    anyhow::bail!(\"cannot mark read: status is {:?}\", mail.status);\n}","typeGuard":"fn is_markable_as_read(status: &AgentMailStatus) -> bool { matches!(status, AgentMailStatus::Delivered) }","tryCatchPattern":"match runtime.mark_agent_mail_read(thread_id, message_id).await {\n    Ok(env) => info!(\"marked read at {:?}\", env.read_at),\n    Err(e) if e.to_string().contains(\"only after delivery\") => warn!(\"mail not in Delivered state; skipping\"),\n    Err(e) => return Err(e),\n}","preventionTips":["Always deliver before offering a 'mark read' action in the UI.","Treat Read/Canceled as terminal states and disable further transitions.","Serialize mail mutations through the mail_mutation lock to avoid racing delivery."],"tags":["agent-mail","state-machine","invalid-state-transition"],"backgroundTag":"invalid-state-transition","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}