1use crate::{requests::StoreMemoryRequest, BriefUpdateRequest};
5use serde::{Deserialize, Serialize};
6
7pub const OUTBOX_SCHEMA: u32 = 1;
8
9#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
10pub struct OutboxEnvelope {
11 pub schema: u32,
12 pub created_at: String,
13 pub caller_id: String,
14 pub operation_id: String,
15 #[serde(default, skip_serializing_if = "Option::is_none")]
16 pub space: Option<String>,
17 #[serde(default, skip_serializing_if = "Option::is_none")]
18 pub agent_name: Option<String>,
19 #[serde(default)]
20 pub reconcile_summary_version: bool,
21 #[serde(flatten)]
22 pub payload: OutboxPayload,
23}
24
25#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
26#[serde(tag = "kind", content = "payload", rename_all = "snake_case")]
27pub enum OutboxPayload {
28 BriefUpdate(BriefUpdateRequest),
29 MemoryStore(StoreMemoryRequest),
30}
31
32impl OutboxPayload {
33 pub fn kind(&self) -> &'static str {
34 match self {
35 Self::BriefUpdate(_) => "brief_update",
36 Self::MemoryStore(_) => "memory_store",
37 }
38 }
39}
40
41impl OutboxEnvelope {
42 pub fn file_name(&self, unix_millis: u128) -> String {
44 format!(
45 "{unix_millis:013}-{}-{}.json",
46 self.operation_id,
47 self.payload.kind()
48 )
49 }
50}
51
52#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
53pub struct OutboxDrainReport {
54 pub applied: u32,
55 pub duplicate: u32,
56 pub failed: u32,
57 pub remaining: u32,
58 pub details: Vec<OutboxDrainDetail>,
59}
60
61#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
62pub struct OutboxDrainDetail {
63 pub file: String,
64 pub kind: String,
65 pub outcome: String,
66 #[serde(default, skip_serializing_if = "Option::is_none")]
67 pub error: Option<String>,
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73 use crate::{BriefSummaryUpdate, WriteSpaceTarget};
74
75 fn brief_envelope() -> OutboxEnvelope {
76 OutboxEnvelope {
77 schema: OUTBOX_SCHEMA,
78 created_at: "2026-08-16T12:00:00.000Z".into(),
79 caller_id: "wenlan-cli".into(),
80 operation_id: "brief-op".into(),
81 space: Some("demo".into()),
82 agent_name: Some("codex".into()),
83 reconcile_summary_version: true,
84 payload: OutboxPayload::BriefUpdate(BriefUpdateRequest {
85 space: "demo".into(),
86 caller_id: "wenlan-cli".into(),
87 operation_id: "brief-op".into(),
88 summary: Some(BriefSummaryUpdate {
89 text: "queued summary".into(),
90 expected_version: 99,
91 }),
92 mutations: vec![],
93 }),
94 }
95 }
96
97 #[test]
98 fn brief_envelope_roundtrips() {
99 let envelope = brief_envelope();
100 let json = serde_json::to_string(&envelope).unwrap();
101 let decoded: OutboxEnvelope = serde_json::from_str(&json).unwrap();
102 assert_eq!(decoded, envelope);
103 assert_eq!(decoded.payload.kind(), "brief_update");
104 }
105
106 #[test]
107 fn memory_envelope_roundtrips() {
108 let envelope = OutboxEnvelope {
109 schema: OUTBOX_SCHEMA,
110 created_at: "2026-08-16T12:00:00.000Z".into(),
111 caller_id: "wenlan-cli".into(),
112 operation_id: "memory-op".into(),
113 space: None,
114 agent_name: None,
115 reconcile_summary_version: false,
116 payload: OutboxPayload::MemoryStore(StoreMemoryRequest {
117 content: "a queued memory with enough content".into(),
118 memory_type: Some("fact".into()),
119 space: WriteSpaceTarget::Inherit,
120 source_agent: None,
121 title: None,
122 confidence: None,
123 supersedes: None,
124 entity: None,
125 entity_id: None,
126 structured_fields: None,
127 retrieval_cue: None,
128 }),
129 };
130 let json = serde_json::to_string(&envelope).unwrap();
131 let decoded: OutboxEnvelope = serde_json::from_str(&json).unwrap();
132 assert_eq!(decoded, envelope);
133 assert_eq!(decoded.payload.kind(), "memory_store");
134 }
135
136 #[test]
137 fn canonical_file_name_is_zero_padded_and_kind_suffixed() {
138 assert_eq!(
139 brief_envelope().file_name(42),
140 "0000000000042-brief-op-brief_update.json"
141 );
142 }
143}