1use crate::error::AtmError;
2use crate::schema::{AtmMessageId, ThreadMode};
3use crate::types::{AgentName, IsoTimestamp, TaskId, TeamName};
4use atm_storage::contract::{Message, MessageKey};
5use serde::{Deserialize, Serialize};
6use std::fmt;
7
8use super::sealed;
9pub use atm_storage::contract::{MailMessageState, MessageFingerprint};
10
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
12#[serde(try_from = "String", into = "String")]
13pub struct ReplaySource(String);
14
15impl ReplaySource {
16 pub fn new(value: impl Into<String>) -> Result<Self, AtmError> {
17 let value = value.into();
18 if value.trim().is_empty() {
19 return Err(AtmError::validation(
20 "replay source must not be empty or whitespace-only",
21 ));
22 }
23 Ok(Self(value))
24 }
25
26 pub fn as_str(&self) -> &str {
27 &self.0
28 }
29}
30
31impl fmt::Display for ReplaySource {
32 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33 f.write_str(&self.0)
34 }
35}
36
37impl TryFrom<String> for ReplaySource {
38 type Error = AtmError;
39
40 fn try_from(value: String) -> Result<Self, Self::Error> {
41 Self::new(value)
42 }
43}
44
45impl From<ReplaySource> for String {
46 fn from(value: ReplaySource) -> Self {
47 value.0
48 }
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
52pub struct MailStoreIngestReplayState {
53 pub team: TeamName,
54 pub agent: AgentName,
55 pub source: ReplaySource,
59 #[serde(default, skip_serializing_if = "Option::is_none")]
60 pub last_fingerprint: Option<MessageFingerprint>,
61 #[serde(default, skip_serializing_if = "Option::is_none")]
62 pub last_ingested_at: Option<IsoTimestamp>,
63 #[serde(default)]
64 pub ingested_rows: u64,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
68pub struct MailStoreHealthSnapshot {
69 pub team: TeamName,
70 pub agent: AgentName,
71 pub total_messages: u64,
72 pub pending_ack_messages: u64,
73 pub read_message_count: u64,
74 #[serde(default, skip_serializing_if = "Option::is_none")]
75 pub latest_message_timestamp: Option<IsoTimestamp>,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
79pub struct MailStoreMailboxMetadataRow {
80 pub message_key: MessageKey,
81 #[serde(default, skip_serializing_if = "Option::is_none")]
82 pub message_id: Option<AtmMessageId>,
83 #[serde(default, skip_serializing_if = "Option::is_none")]
84 pub parent_message_id: Option<AtmMessageId>,
85 #[serde(default, skip_serializing_if = "Option::is_none")]
86 pub thread_mode: Option<ThreadMode>,
87 pub from_agent: AgentName,
88 #[serde(default, skip_serializing_if = "Option::is_none")]
89 pub summary: Option<String>,
90 pub message_at: IsoTimestamp,
91 pub read: bool,
92 pub requires_ack: bool,
93 pub pending_ack: bool,
94 #[serde(default, skip_serializing_if = "Option::is_none")]
95 pub acknowledged_at: Option<IsoTimestamp>,
96 #[serde(default, skip_serializing_if = "Option::is_none")]
97 pub expires_at: Option<IsoTimestamp>,
98 #[serde(default, skip_serializing_if = "Option::is_none")]
99 pub task_id: Option<TaskId>,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
103pub struct MailStoreMailboxMetadataCounts {
104 pub total_messages: u64,
105 pub unread_message_count: u64,
106 pub pending_ack_messages: u64,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
111pub struct UpsertMailMessageStateRequest {
112 pub team: TeamName,
113 pub agent: AgentName,
114 pub actor: AgentName,
115 pub state: MailMessageState,
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
120pub struct UpsertMailMessageStateResponse {
121 pub state: MailMessageState,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
126pub struct LoadMailMessageStateRequest {
127 pub team: TeamName,
128 pub agent: AgentName,
129 pub actor: AgentName,
130 pub message_key: MessageKey,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
135pub struct LoadMailMessageStateResponse {
136 #[serde(default)]
137 pub state: Option<MailMessageState>,
138}
139
140pub type DoctorFinding = crate::doctor::DoctorFinding;
141
142#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
143pub struct MailStoreDoctorReport {
144 pub findings: Vec<DoctorFinding>,
145}
146
147#[deprecated(note = "use canonical atm-storage types instead")]
149pub trait MailStore: sealed::Sealed {
150 fn upsert_message(&self, record: Message) -> Result<(), AtmError>;
155
156 fn load_message(
160 &self,
161 team: &TeamName,
162 agent: &AgentName,
163 message_key: &MessageKey,
164 ) -> Result<Option<Message>, AtmError>;
165
166 fn query_mailbox_metadata(
170 &self,
171 team: &TeamName,
172 agent: &AgentName,
173 limit: Option<usize>,
174 ) -> Result<Vec<MailStoreMailboxMetadataRow>, AtmError>;
175
176 fn query_mailbox_metadata_counts(
180 &self,
181 team: &TeamName,
182 agent: &AgentName,
183 ) -> Result<MailStoreMailboxMetadataCounts, AtmError>;
184
185 fn upsert_message_state(
189 &self,
190 request: UpsertMailMessageStateRequest,
191 ) -> Result<UpsertMailMessageStateResponse, AtmError>;
192
193 fn load_message_state(
197 &self,
198 request: LoadMailMessageStateRequest,
199 ) -> Result<LoadMailMessageStateResponse, AtmError>;
200
201 fn record_ingest_replay_state(
205 &self,
206 team: &TeamName,
207 agent: &AgentName,
208 source: &ReplaySource,
209 state: &MailStoreIngestReplayState,
210 ) -> Result<(), AtmError>;
211
212 fn load_ingest_replay_state(
216 &self,
217 team: &TeamName,
218 agent: &AgentName,
219 source: &ReplaySource,
220 ) -> Result<Option<MailStoreIngestReplayState>, AtmError>;
221
222 fn health_snapshot(
226 &self,
227 team: &TeamName,
228 agent: &AgentName,
229 ) -> Result<MailStoreHealthSnapshot, AtmError>;
230}
231
232pub trait MailStoreDoctor: sealed::Sealed + Send + Sync {
234 fn inspect_mail_store(&self) -> Result<MailStoreDoctorReport, AtmError>;
239}