agent-team-mail-core 1.3.1

Core library for local agent team mail workflows.
Documentation
use std::fs::OpenOptions;
use std::io::Write;
use std::path::Path;

use serde_json::Value;

use crate::error::{AtmError, AtmErrorKind};
use crate::persistence;
use crate::schema::InboxMessage;
use crate::schema::inbox_message::{SharedAppendPolicy, to_shared_inbox_value_with_policy};

/// Atomically replace one shared inbox file from fully serialized records.
///
/// ATM serializes every envelope into one JSON array document, fsyncs that temp
/// file, and then performs same-filesystem replacement through the shared
/// persistence helper. On Linux, a successful return means the file contents
/// and renamed directory entry were durably published after the
/// parent-directory fsync. On macOS, ATM performs the same parent-directory
/// sync call, but APFS durability semantics may still differ from Linux after
/// power loss. On Windows, the shared helper returns `Ok(())` after temp-file
/// fsync plus rename without an additional parent-directory sync because the
/// standard library does not expose a portable directory-sync operation there.
///
/// # Errors
///
/// Returns [`AtmError`] with
/// [`crate::error_codes::AtmErrorCode::MailboxWriteFailed`] when message
/// serialization fails or the mailbox temp-file write, fsync, rename, or
/// parent-directory durability step cannot be completed.
///
/// Repair/rebuild only — not reachable from normal runtime send or ack paths.
pub fn write_messages(
    path: &Path,
    messages: &[InboxMessage],
    export_policy: SharedAppendPolicy,
) -> Result<(), AtmError> {
    write_message_iter(path, messages.iter(), export_policy)
}

pub fn write_message_iter<'a, I>(
    path: &Path,
    messages: I,
    export_policy: SharedAppendPolicy,
) -> Result<(), AtmError>
where
    I: IntoIterator<Item = &'a InboxMessage>,
{
    let iterator = messages.into_iter();
    let mut encoded = Vec::<Value>::new();
    for message in iterator {
        encoded.push(to_shared_inbox_value_with_policy(message, export_policy)?);
    }
    let mut bytes = serde_json::to_vec(&encoded)?;
    bytes.push(b'\n');

    persistence::atomic_write_bytes(
        path,
        &bytes,
        AtmErrorKind::MailboxWrite,
        "mailbox file",
        "Check that the mailbox directory is writable, has available disk space, and resides on a healthy filesystem before retrying the ATM command.",
    )
}

#[cfg_attr(
    not(test),
    allow(
        dead_code,
        reason = "JSONL append helper remains test-only after compat retirement"
    )
)]
pub fn append_message(
    path: &Path,
    message: &InboxMessage,
    export_policy: SharedAppendPolicy,
) -> Result<(), AtmError> {
    let encoded = to_shared_inbox_value_with_policy(message, export_policy)?;
    append_jsonl_record(path, &encoded)
}

pub fn append_jsonl_record<T: serde::Serialize>(path: &Path, record: &T) -> Result<(), AtmError> {
    let mut bytes = serde_json::to_vec(record)?;
    bytes.push(b'\n');

    let mut file = OpenOptions::new()
        .create(true)
        .append(true)
        .open(path)
        .map_err(|error| {
            AtmError::new(
                AtmErrorKind::MailboxWrite,
                format!("failed to open mailbox file {} for append: {error}", path.display()),
            )
            .with_recovery(
                "Check that the mailbox directory is writable, has available disk space, and resides on a healthy filesystem before retrying the ATM command.",
            )
            .with_source(error)
        })?;
    file.write_all(&bytes).map_err(|error| {
        AtmError::new(
            AtmErrorKind::MailboxWrite,
            format!("failed to append mailbox record {}: {error}", path.display()),
        )
        .with_recovery(
            "Check that the mailbox directory is writable, has available disk space, and resides on a healthy filesystem before retrying the ATM command.",
        )
        .with_source(error)
    })?;
    file.sync_data().map_err(|error| {
        AtmError::new(
            AtmErrorKind::MailboxWrite,
            format!("failed to sync appended mailbox record {}: {error}", path.display()),
        )
        .with_recovery(
            "Check that the mailbox directory is writable, has available disk space, and resides on a healthy filesystem before retrying the ATM command.",
        )
        .with_source(error)
    })
}