pub use atm_storage::schema::{AlertKind, AtmMessageId, InboxMessage, PendingAck, ThreadMode};
use serde_json::{Map, Value};
use crate::config::types::{ByteCount, DEFAULT_CLAUDE_JSONL_BODY_EXPORT_MAX_BYTES};
use crate::error::AtmError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct SharedAppendPolicy {
pub(crate) atm_authored_body_export_max_bytes: ByteCount,
}
impl Default for SharedAppendPolicy {
fn default() -> Self {
Self {
atm_authored_body_export_max_bytes: ByteCount::new(
DEFAULT_CLAUDE_JSONL_BODY_EXPORT_MAX_BYTES,
),
}
}
}
#[cfg(test)]
pub(crate) fn to_shared_inbox_value(message: &InboxMessage) -> Result<Value, AtmError> {
to_shared_inbox_value_with_policy(message, SharedAppendPolicy::default())
}
fn strip_metadata_atm_namespace(object: &mut Map<String, Value>) {
let Some(metadata) = object.get_mut("metadata").and_then(Value::as_object_mut) else {
return;
};
metadata.remove("atm");
if metadata.is_empty() {
object.remove("metadata");
}
}
fn strip_removed_compatibility_fields(object: &mut Map<String, Value>) {
for field in [
"source_team",
"pendingAckAt",
"acknowledgedAt",
"acknowledgesMessageId",
"expiresAt",
] {
object.remove(field);
}
}
pub(crate) fn to_shared_inbox_value_with_policy(
message: &InboxMessage,
policy: SharedAppendPolicy,
) -> Result<Value, AtmError> {
let mut value = serde_json::to_value(message).map_err(|error| {
AtmError::mailbox_write(format!(
"failed to serialize shared inbox envelope for {} at {:?}: {error}",
message.from, message.timestamp
))
.with_source(error)
})?;
let object = value
.as_object_mut()
.ok_or_else(|| {
AtmError::mailbox_write(format!(
"failed to serialize shared inbox envelope for {} at {:?}: envelope did not encode as a JSON object",
message.from, message.timestamp
))
.with_recovery(
"Preserve the ATM shared-inbox envelope shape so serialization produces one JSON object per message before retrying mailbox export.",
)
})?;
strip_metadata_atm_namespace(object);
strip_removed_compatibility_fields(object);
if should_export_retrieval_stub(message, policy)? {
let message_id = message.message_id.ok_or_else(|| {
AtmError::mailbox_write(
"retrieval stub export requires an ATM-authored message_id on the source envelope",
)
.with_recovery(
"Preserve ATM-authored message ids on envelopes that export retrieval stubs before retrying shared inbox projection.",
)
})?;
let retrieval_stub = retrieval_stub_text(message_id);
object.insert("text".to_string(), Value::String(retrieval_stub));
}
Ok(value)
}
fn should_export_retrieval_stub(
message: &InboxMessage,
policy: SharedAppendPolicy,
) -> Result<bool, AtmError> {
let export_cap = policy
.atm_authored_body_export_max_bytes
.as_usize()
.ok_or_else(|| {
AtmError::mailbox_write(format!(
"failed to compare ATM-authored export cap for {} at {:?}: configured byte cap does not fit into usize",
message.from, message.timestamp
))
.with_recovery(
"Lower [atm].claude_jsonl_body_export_max_bytes to a bounded value before retrying mailbox export.",
)
})?;
Ok(message.message_id.is_some()
&& (policy.atm_authored_body_export_max_bytes.is_zero() || message.text.len() > export_cap))
}
fn retrieval_stub_text(message_id: AtmMessageId) -> String {
format!("atm read --message-id {message_id}")
}
#[cfg(test)]
mod tests {
use chrono::TimeZone;
use serde_json::{Map, Value, json};
use chrono::Utc;
use super::{
AlertKind, AtmMessageId, InboxMessage, PendingAck, SharedAppendPolicy, ThreadMode,
to_shared_inbox_value, to_shared_inbox_value_with_policy,
};
use crate::config::types::ByteCount;
use crate::roles::ROLE_TEAM_LEAD;
use crate::test_support::{TEST_SENDER, TEST_TEAM};
use crate::types::IsoTimestamp;
#[test]
fn alert_kind_round_trips_known_and_unknown_wire_values() {
let known: AlertKind =
serde_json::from_str(r#""missing_team_config""#).expect("known alert kind");
assert_eq!(known, AlertKind::MissingTeamConfig);
assert_eq!(
serde_json::to_string(&known).expect("encode known"),
r#""missing_team_config""#
);
let unknown: AlertKind =
serde_json::from_str(r#""future_alert_kind""#).expect("unknown alert kind");
assert_eq!(unknown, AlertKind::Unknown("future_alert_kind".to_string()));
assert_eq!(
serde_json::to_string(&unknown).expect("encode unknown"),
r#""future_alert_kind""#
);
}
#[test]
fn message_envelope_round_trips_with_current_inbox_shape() {
let envelope = InboxMessage {
from: TEST_SENDER.parse().expect("agent"),
text: "hello".into(),
timestamp: IsoTimestamp::from_datetime(
Utc.with_ymd_and_hms(2026, 3, 30, 0, 0, 0)
.single()
.expect("timestamp"),
),
read: false,
source_team: Some(TEST_TEAM.parse().expect("team")),
summary: Some("hello".into()),
message_id: Some(AtmMessageId::new()),
pending_ack_at: Some(IsoTimestamp::from_datetime(
Utc.with_ymd_and_hms(2026, 3, 30, 0, 0, 1)
.single()
.expect("timestamp"),
)),
acknowledged_at: None,
acknowledges_message_id: None,
parent_message_id: None,
thread_mode: None,
expires_at: None,
task_id: Some("TASK-123".parse().expect("task id")),
extra: Map::new(),
};
let encoded = serde_json::to_string(&envelope).expect("encode");
let decoded: InboxMessage = serde_json::from_str(&encoded).expect("decode");
assert_eq!(decoded, envelope);
}
#[test]
fn unknown_fields_are_preserved() {
let json = json!({
"from": ROLE_TEAM_LEAD,
"text": "hello",
"timestamp": "2026-03-30T00:00:00Z",
"read": false,
"futureField": {"nested": true}
});
let decoded: InboxMessage = serde_json::from_value(json).expect("decode");
assert_eq!(decoded.extra["futureField"], json!({"nested": true}));
let reencoded = serde_json::to_value(&decoded).expect("encode");
assert_eq!(reencoded["futureField"], json!({"nested": true}));
}
#[test]
fn message_id_is_optional() {
let json = json!({
"from": ROLE_TEAM_LEAD,
"text": "hello",
"timestamp": "2026-03-30T00:00:00Z",
"read": false
});
let decoded: InboxMessage = serde_json::from_value(json).expect("decode");
assert!(decoded.message_id.is_none());
assert!(decoded.task_id.is_none());
}
#[test]
fn blank_task_id_is_rejected() {
let json = json!({
"from": ROLE_TEAM_LEAD,
"text": "hello",
"timestamp": "2026-03-30T00:00:00Z",
"read": false,
"taskId": " "
});
let error = serde_json::from_value::<InboxMessage>(json).expect_err("blank task id");
assert!(error.to_string().contains("task id must not be blank"));
}
#[test]
fn pending_ack_round_trips() {
let pending_ack = PendingAck {
message_id: AtmMessageId::new(),
from: ROLE_TEAM_LEAD.parse().expect("agent"),
acked: true,
acked_at: Some(IsoTimestamp::from_datetime(
Utc.with_ymd_and_hms(2026, 3, 30, 0, 0, 1)
.single()
.expect("timestamp"),
)),
};
let encoded = serde_json::to_string(&pending_ack).expect("encode");
let decoded: PendingAck = serde_json::from_str(&encoded).expect("decode");
assert_eq!(decoded, pending_ack);
}
#[test]
fn atm_message_id_timestamp_matches_derived_timestamp() {
let (message_id, timestamp) = AtmMessageId::new_with_timestamp();
assert_eq!(message_id.timestamp(), timestamp);
}
#[test]
fn atm_message_id_parses_from_uuid_wire_string() {
let parsed: AtmMessageId = "11111111-1111-4111-8111-111111111111"
.parse()
.expect("parse uuid wire id");
assert_eq!(
parsed.into_uuid_wire().to_string(),
"11111111-1111-4111-8111-111111111111"
);
}
#[test]
fn atm_message_id_parses_from_ulid_string() {
let (message_id, _) = AtmMessageId::new_with_timestamp();
let parsed: AtmMessageId = message_id.to_string().parse().expect("parse atm id");
assert_eq!(parsed, message_id);
}
#[test]
fn atm_message_id_uuid_wire_round_trip_preserves_identity() {
let message_id: AtmMessageId = "01KRFK5QTF2R6NRS3Q0F8Z9K0S".parse().expect("parse atm id");
let round_trip = AtmMessageId::from_uuid_wire(message_id.into_uuid_wire());
assert_eq!(round_trip, message_id);
}
#[test]
fn shared_inbox_write_keeps_only_approved_immutable_fields() {
let envelope = InboxMessage {
from: TEST_SENDER.parse().expect("agent"),
text: "hello".into(),
timestamp: IsoTimestamp::from_datetime(
Utc.with_ymd_and_hms(2026, 3, 30, 0, 0, 0)
.single()
.expect("timestamp"),
),
read: false,
source_team: Some(TEST_TEAM.parse().expect("team")),
summary: Some("hello".into()),
message_id: Some(AtmMessageId::new()),
pending_ack_at: Some(IsoTimestamp::from_datetime(
Utc.with_ymd_and_hms(2026, 3, 30, 0, 0, 1)
.single()
.expect("timestamp"),
)),
acknowledged_at: None,
acknowledges_message_id: None,
parent_message_id: None,
thread_mode: None,
expires_at: None,
task_id: Some("TASK-123".parse().expect("task id")),
extra: Map::new(),
};
let encoded = to_shared_inbox_value(&envelope).expect("encode");
let object = encoded.as_object().expect("object");
assert!(object.contains_key("message_id"));
assert!(!object.contains_key("source_team"));
assert!(!object.contains_key("pendingAckAt"));
assert!(object.contains_key("taskId"));
assert!(
object
.get("metadata")
.and_then(Value::as_object)
.and_then(|metadata| metadata.get("atm"))
.is_none()
);
}
#[test]
fn shared_inbox_write_keeps_parent_message_id_and_thread_mode_when_set() {
let envelope = InboxMessage {
from: TEST_SENDER.parse().expect("agent"),
text: "threaded message".into(),
timestamp: IsoTimestamp::from_datetime(
Utc.with_ymd_and_hms(2026, 3, 30, 0, 0, 0)
.single()
.expect("timestamp"),
),
read: false,
source_team: None,
summary: None,
message_id: Some(AtmMessageId::new()),
pending_ack_at: None,
acknowledged_at: None,
acknowledges_message_id: None,
parent_message_id: Some(AtmMessageId::new()),
thread_mode: Some(ThreadMode::AddDetails),
expires_at: None,
task_id: None,
extra: Map::new(),
};
let encoded = to_shared_inbox_value(&envelope).expect("encode");
let object = encoded.as_object().expect("object");
assert!(
object.contains_key("parentMessageId"),
"parentMessageId must be kept in shared inbox export"
);
assert!(
object.contains_key("threadMode"),
"threadMode must be kept in shared inbox export"
);
}
#[test]
fn shared_inbox_write_strips_expires_at() {
let envelope = InboxMessage {
from: TEST_SENDER.parse().expect("agent"),
text: "expiring message".into(),
timestamp: IsoTimestamp::from_datetime(
Utc.with_ymd_and_hms(2026, 3, 30, 0, 0, 0)
.single()
.expect("timestamp"),
),
read: false,
source_team: None,
summary: None,
message_id: Some(AtmMessageId::new()),
pending_ack_at: None,
acknowledged_at: None,
acknowledges_message_id: None,
parent_message_id: None,
thread_mode: None,
expires_at: Some(IsoTimestamp::from_datetime(
Utc.with_ymd_and_hms(2026, 3, 31, 0, 0, 0)
.single()
.expect("timestamp"),
)),
task_id: None,
extra: Map::new(),
};
let encoded = to_shared_inbox_value(&envelope).expect("encode");
let object = encoded.as_object().expect("object");
assert!(
!object.contains_key("expiresAt"),
"expiresAt must be stripped from shared inbox export"
);
}
#[test]
fn shared_inbox_write_strips_ack_workflow_fields_from_export() {
let acknowledged_at = IsoTimestamp::from_datetime(
Utc.with_ymd_and_hms(2026, 3, 30, 0, 0, 2)
.single()
.expect("timestamp"),
);
let envelope = InboxMessage {
from: TEST_SENDER.parse().expect("agent"),
text: "ack reply".into(),
timestamp: IsoTimestamp::from_datetime(
Utc.with_ymd_and_hms(2026, 3, 30, 0, 0, 0)
.single()
.expect("timestamp"),
),
read: false,
source_team: Some(TEST_TEAM.parse().expect("team")),
summary: Some("ack reply".into()),
message_id: Some(AtmMessageId::new()),
pending_ack_at: None,
acknowledged_at: Some(acknowledged_at),
acknowledges_message_id: Some(AtmMessageId::new()),
parent_message_id: None,
thread_mode: None,
expires_at: None,
task_id: None,
extra: Map::new(),
};
let encoded = to_shared_inbox_value(&envelope).expect("encode");
let object = encoded.as_object().expect("object");
assert!(!object.contains_key("acknowledgedAt"));
assert!(!object.contains_key("acknowledgesMessageId"));
}
#[test]
fn shared_inbox_write_stubs_oversized_atm_authored_messages() {
let message_id = AtmMessageId::new();
let envelope = InboxMessage {
from: TEST_SENDER.parse().expect("agent"),
text: "x".repeat(32),
timestamp: IsoTimestamp::from_datetime(
Utc.with_ymd_and_hms(2026, 3, 30, 0, 0, 0)
.single()
.expect("timestamp"),
),
read: false,
source_team: Some(TEST_TEAM.parse().expect("team")),
summary: Some("oversized".into()),
message_id: Some(message_id),
pending_ack_at: None,
acknowledged_at: None,
acknowledges_message_id: None,
parent_message_id: None,
thread_mode: None,
expires_at: None,
task_id: None,
extra: Map::new(),
};
let encoded = to_shared_inbox_value_with_policy(
&envelope,
SharedAppendPolicy {
atm_authored_body_export_max_bytes: ByteCount::new(8),
},
)
.expect("encode");
assert_eq!(
encoded["text"],
json!(format!("atm read --message-id {message_id}"))
);
assert_eq!(encoded["summary"], json!("oversized"));
assert_eq!(
encoded["message_id"],
json!(message_id.into_uuid_wire().to_string())
);
}
#[test]
fn shared_inbox_write_exports_full_body_at_exact_cap() {
let message_id = AtmMessageId::new();
let text = "x".repeat(32);
let envelope = InboxMessage {
from: TEST_SENDER.parse().expect("agent"),
text: text.clone(),
timestamp: IsoTimestamp::from_datetime(
Utc.with_ymd_and_hms(2026, 3, 30, 0, 0, 0)
.single()
.expect("timestamp"),
),
read: false,
source_team: Some(TEST_TEAM.parse().expect("team")),
summary: Some("exact-cap".into()),
message_id: Some(message_id),
pending_ack_at: None,
acknowledged_at: None,
acknowledges_message_id: None,
parent_message_id: None,
thread_mode: None,
expires_at: None,
task_id: None,
extra: Map::new(),
};
let encoded = to_shared_inbox_value_with_policy(
&envelope,
SharedAppendPolicy {
atm_authored_body_export_max_bytes: ByteCount::new(text.len() as u64),
},
)
.expect("encode");
assert_eq!(encoded["text"], json!(text));
}
#[test]
fn shared_inbox_write_exports_stub_above_cap() {
let message_id = AtmMessageId::new();
let text = "x".repeat(32);
let envelope = InboxMessage {
from: TEST_SENDER.parse().expect("agent"),
text,
timestamp: IsoTimestamp::from_datetime(
Utc.with_ymd_and_hms(2026, 3, 30, 0, 0, 0)
.single()
.expect("timestamp"),
),
read: false,
source_team: Some(TEST_TEAM.parse().expect("team")),
summary: Some("above-cap".into()),
message_id: Some(message_id),
pending_ack_at: None,
acknowledged_at: None,
acknowledges_message_id: None,
parent_message_id: None,
thread_mode: None,
expires_at: None,
task_id: None,
extra: Map::new(),
};
let encoded = to_shared_inbox_value_with_policy(
&envelope,
SharedAppendPolicy {
atm_authored_body_export_max_bytes: ByteCount::new(31),
},
)
.expect("encode");
assert_eq!(
encoded["text"],
json!(format!("atm read --message-id {message_id}"))
);
}
#[test]
fn shared_inbox_write_keeps_full_body_for_claude_native_messages() {
let envelope = InboxMessage {
from: TEST_SENDER.parse().expect("agent"),
text: "x".repeat(32),
timestamp: IsoTimestamp::from_datetime(
Utc.with_ymd_and_hms(2026, 3, 30, 0, 0, 0)
.single()
.expect("timestamp"),
),
read: false,
source_team: None,
summary: Some("native".into()),
message_id: None,
pending_ack_at: None,
acknowledged_at: None,
acknowledges_message_id: None,
parent_message_id: None,
thread_mode: None,
expires_at: None,
task_id: None,
extra: Map::new(),
};
let encoded = to_shared_inbox_value_with_policy(
&envelope,
SharedAppendPolicy {
atm_authored_body_export_max_bytes: ByteCount::new(8),
},
)
.expect("encode");
assert_eq!(encoded["text"], json!("x".repeat(32)));
}
#[test]
fn shared_inbox_write_strips_metadata_atm_namespace() {
let mut extra = Map::new();
extra.insert(
"metadata".to_string(),
json!({
"atm": { "messageId": "01JQYVB6W51Q2E7E6T3Y4Q9N2M" },
"foreign": { "keep": true }
}),
);
let envelope = InboxMessage {
from: TEST_SENDER.parse().expect("agent"),
text: "hello".into(),
timestamp: IsoTimestamp::from_datetime(
Utc.with_ymd_and_hms(2026, 3, 30, 0, 0, 0)
.single()
.expect("timestamp"),
),
read: false,
source_team: None,
summary: None,
message_id: Some(AtmMessageId::new()),
pending_ack_at: None,
acknowledged_at: None,
acknowledges_message_id: None,
parent_message_id: None,
thread_mode: None,
expires_at: None,
task_id: None,
extra,
};
let encoded = to_shared_inbox_value(&envelope).expect("encode");
assert!(
encoded["metadata"]["foreign"]["keep"]
.as_bool()
.unwrap_or(false)
);
assert!(encoded["metadata"]["atm"].is_null());
}
}