use std::path::Path;
mod render;
pub use render::render;
const INBOX_DIR: &str = "inbox";
const MESSAGE_EXT: &str = ".md";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Epitaph {
FinalResponse,
Stopped,
BudgetExhausted,
Died,
Unknown(String),
}
impl Epitaph {
fn parse(value: &str) -> Epitaph {
match value {
"final-response" => Epitaph::FinalResponse,
"stopped" => Epitaph::Stopped,
"budget-exhausted" => Epitaph::BudgetExhausted,
"died" => Epitaph::Died,
other => Epitaph::Unknown(other.to_string()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Deposit {
pub sender: Option<String>,
pub deposited_at: Option<String>,
pub epitaph: Option<Epitaph>,
pub terminal_ref: Option<String>,
pub body: String,
}
pub fn parse_deposit(bytes: &[u8]) -> Deposit {
let text = String::from_utf8_lossy(bytes);
match split_frontmatter(&text) {
Some((frontmatter, body)) => Deposit {
sender: field(frontmatter, "from"),
deposited_at: field(frontmatter, "deposited_at"),
epitaph: field(frontmatter, "epitaph").map(|v| Epitaph::parse(&v)),
terminal_ref: field(frontmatter, "terminal_ref"),
body: body.to_string(),
},
None => Deposit {
body: text.into_owned(),
..Deposit::default()
},
}
}
pub fn list_inbox(workspace: &Path, agent_id: &str) -> Vec<Deposit> {
let dir = workspace.join(INBOX_DIR).join(agent_id);
let Ok(entries) = std::fs::read_dir(&dir) else {
return Vec::new();
};
let mut files: Vec<std::path::PathBuf> = entries
.flatten()
.map(|e| e.path())
.filter(|p| {
p.file_name()
.and_then(|n| n.to_str())
.is_some_and(|n| n.ends_with(MESSAGE_EXT))
})
.collect();
files.sort();
files
.iter()
.filter_map(|p| std::fs::read(p).ok())
.map(|bytes| parse_deposit(&bytes))
.collect()
}
fn split_frontmatter(text: &str) -> Option<(&str, &str)> {
text.strip_prefix("---\n")?.split_once("\n---\n")
}
fn field(frontmatter: &str, key: &str) -> Option<String> {
frontmatter
.lines()
.filter_map(|line| line.split_once(": "))
.find(|(k, _)| *k == key)
.map(|(_, v)| v.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn parses_ordinary_deposit() {
let d =
parse_deposit(b"---\nfrom: user\ndeposited_at: 2026-07-17T12:00:00Z\n---\nhello there");
assert_eq!(d.sender.as_deref(), Some("user"));
assert_eq!(d.deposited_at.as_deref(), Some("2026-07-17T12:00:00Z"));
assert_eq!(d.epitaph, None);
assert_eq!(d.terminal_ref, None);
assert_eq!(d.body, "hello there");
}
#[test]
fn epitaph_values_map_to_typed_variants() {
let cases = [
("final-response", Epitaph::FinalResponse),
("stopped", Epitaph::Stopped),
("budget-exhausted", Epitaph::BudgetExhausted),
("died", Epitaph::Died),
("wat", Epitaph::Unknown("wat".to_string())),
];
for (raw, want) in cases {
let file = format!(
"---\nfrom: c\ndeposited_at: t\nepitaph: {raw}\nterminal_ref: sha\n---\nbody"
);
let d = parse_deposit(file.as_bytes());
assert_eq!(d.epitaph, Some(want));
assert_eq!(d.terminal_ref.as_deref(), Some("sha"));
}
}
#[test]
fn result_message_without_body_has_empty_body() {
let d = parse_deposit(
b"---\nfrom: c\ndeposited_at: t\nepitaph: died\nterminal_ref: sha\n---\n",
);
assert_eq!(d.epitaph, Some(Epitaph::Died));
assert_eq!(d.body, "");
}
#[test]
fn malformed_file_is_raw_body_with_absent_fields() {
let d = parse_deposit(b"no frontmatter at all");
assert_eq!(
d,
Deposit {
body: "no frontmatter at all".to_string(),
..Deposit::default()
}
);
}
#[test]
fn opener_without_closer_is_raw_body() {
let d = parse_deposit(b"---\nfrom: x but no closing fence");
assert_eq!(d.sender, None);
assert!(d.body.starts_with("---\n"));
}
#[test]
fn frontmatter_line_without_separator_is_skipped() {
let d = parse_deposit(b"---\nfrom: user\nbare line\n---\nbody");
assert_eq!(d.sender.as_deref(), Some("user"));
assert_eq!(d.body, "body");
}
#[test]
fn list_inbox_orders_by_filename_skipping_temp_nonmd_and_unreadable() {
let dir = tempdir().unwrap();
let inbox = dir.path().join("inbox").join("a-1");
std::fs::create_dir_all(&inbox).unwrap();
std::fs::write(
inbox.join("user-002.md"),
b"---\nfrom: user\ndeposited_at: t2\n---\nsecond",
)
.unwrap();
std::fs::write(
inbox.join("user-001.md"),
b"---\nfrom: user\ndeposited_at: t1\n---\nfirst",
)
.unwrap();
std::fs::write(inbox.join(".user-003.md.tmp"), b"partial").unwrap();
std::fs::write(inbox.join("notes.txt"), b"not a deposit").unwrap();
std::fs::create_dir_all(inbox.join("sub.md")).unwrap();
let deposits = list_inbox(dir.path(), "a-1");
let bodies: Vec<&str> = deposits.iter().map(|d| d.body.as_str()).collect();
assert_eq!(bodies, vec!["first", "second"]);
}
#[test]
fn list_inbox_missing_dir_is_empty() {
let dir = tempdir().unwrap();
assert!(list_inbox(dir.path(), "nobody").is_empty());
}
}