Skip to main content

wenlan_types/
outbox.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Durable envelopes shared by the CLI outbox writer and daemon drainer.
3
4use 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    /// Return the canonical durable filename for this envelope.
43    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}