use std::sync::{Mutex, MutexGuard};
use chrono::{DateTime, Utc};
use tokio::sync::broadcast;
use uuid::Uuid;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VisibleAssistantMessage {
pub id: String,
pub content: String,
pub created_at: DateTime<Utc>,
round_count: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VisibleMessageSnapshot {
pub version: u64,
pub messages: Vec<VisibleAssistantMessage>,
pub terminal: Option<String>,
pub history_committed: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VisibleMessageEvent {
pub version: u64,
pub kind: VisibleMessageEventKind,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VisibleMessageEventKind {
Started {
message_id: String,
created_at: DateTime<Utc>,
},
Delta {
message_id: String,
offset: usize,
content: String,
created_at: DateTime<Utc>,
},
Discarded {
message_id: String,
},
Terminal {
reason: String,
},
HistoryCommitted,
}
#[derive(Debug, Default)]
struct VisibleMessageReplay {
version: u64,
round_count: u32,
active_message_id: Option<String>,
messages: Vec<VisibleAssistantMessage>,
terminal: Option<String>,
history_committed: bool,
}
#[derive(Debug)]
pub struct VisibleMessageStream {
state: Mutex<VisibleMessageReplay>,
sender: broadcast::Sender<VisibleMessageEvent>,
}
impl Default for VisibleMessageStream {
fn default() -> Self {
Self::new()
}
}
impl VisibleMessageStream {
const CHANNEL_CAPACITY: usize = 1000;
pub fn new() -> Self {
let (sender, _) = broadcast::channel(Self::CHANNEL_CAPACITY);
Self {
state: Mutex::new(VisibleMessageReplay::default()),
sender,
}
}
fn lock(&self) -> MutexGuard<'_, VisibleMessageReplay> {
self.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn send_locked(&self, event: VisibleMessageEvent) {
let _ = self.sender.send(event);
}
pub fn subscribe_with_snapshot(
&self,
) -> (
broadcast::Receiver<VisibleMessageEvent>,
VisibleMessageSnapshot,
) {
let receiver = self.sender.subscribe();
let state = self.lock();
let snapshot = VisibleMessageSnapshot {
version: state.version,
messages: state
.messages
.iter()
.filter(|message| !message.content.is_empty())
.cloned()
.collect(),
terminal: state.terminal.clone(),
history_committed: state.history_committed,
};
(receiver, snapshot)
}
pub fn begin_round(&self, round_count: u32) {
self.lock().round_count = round_count;
}
pub fn start(&self, message_id: String, created_at: DateTime<Utc>) {
let mut state = self.lock();
if !state
.messages
.iter()
.any(|message| message.id == message_id)
{
let round_count = state.round_count;
state.messages.push(VisibleAssistantMessage {
id: message_id.clone(),
content: String::new(),
created_at,
round_count,
});
}
state.active_message_id = Some(message_id.clone());
state.history_committed = false;
state.version = state.version.saturating_add(1);
self.send_locked(VisibleMessageEvent {
version: state.version,
kind: VisibleMessageEventKind::Started {
message_id,
created_at,
},
});
}
pub fn append(&self, content: String) {
if content.is_empty() {
return;
}
let mut state = self.lock();
let message_id = match state.active_message_id.clone() {
Some(message_id) => message_id,
None => {
let message_id = Uuid::new_v4().to_string();
let created_at = Utc::now();
let round_count = state.round_count;
state.messages.push(VisibleAssistantMessage {
id: message_id.clone(),
content: String::new(),
created_at,
round_count,
});
state.active_message_id = Some(message_id.clone());
state.version = state.version.saturating_add(1);
self.send_locked(VisibleMessageEvent {
version: state.version,
kind: VisibleMessageEventKind::Started {
message_id: message_id.clone(),
created_at,
},
});
message_id
}
};
let Some(index) = state
.messages
.iter()
.position(|message| message.id == message_id)
else {
return;
};
let offset = state.messages[index].content.len();
let created_at = state.messages[index].created_at;
state.messages[index].content.push_str(&content);
state.version = state.version.saturating_add(1);
self.send_locked(VisibleMessageEvent {
version: state.version,
kind: VisibleMessageEventKind::Delta {
message_id,
offset,
content,
created_at,
},
});
}
pub fn discard(&self, message_id: &str) {
let mut state = self.lock();
let previous_len = state.messages.len();
state.messages.retain(|message| message.id != message_id);
if state.messages.len() == previous_len {
return;
}
if state.active_message_id.as_deref() == Some(message_id) {
state.active_message_id = None;
}
state.version = state.version.saturating_add(1);
self.send_locked(VisibleMessageEvent {
version: state.version,
kind: VisibleMessageEventKind::Discarded {
message_id: message_id.to_string(),
},
});
}
pub fn mark_terminal(&self, reason: impl Into<String>) {
let mut state = self.lock();
let reason = reason.into();
if state.terminal.as_deref() == Some(reason.as_str()) {
return;
}
state.terminal = Some(reason.clone());
state.version = state.version.saturating_add(1);
self.send_locked(VisibleMessageEvent {
version: state.version,
kind: VisibleMessageEventKind::Terminal { reason },
});
}
pub fn history_committed(&self) {
let mut state = self.lock();
state.messages.clear();
state.active_message_id = None;
state.history_committed = true;
state.version = state.version.saturating_add(1);
self.send_locked(VisibleMessageEvent {
version: state.version,
kind: VisibleMessageEventKind::HistoryCommitted,
});
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn subscribe_snapshot_and_live_tail_have_no_overlap_after_version_filter() {
let stream = VisibleMessageStream::new();
stream.begin_round(0);
let created_at = Utc::now();
stream.start("message-1".into(), created_at);
stream.append("before".into());
let (mut receiver, snapshot) = stream.subscribe_with_snapshot();
assert_eq!(snapshot.messages[0].content, "before");
stream.append(" after".into());
let event = receiver.try_recv().expect("live delta");
assert!(event.version > snapshot.version);
assert!(matches!(
event.kind,
VisibleMessageEventKind::Delta { offset: 6, ref content, .. } if content == " after"
));
}
#[test]
fn explicit_retry_rollback_discards_previous_attempt() {
let stream = VisibleMessageStream::new();
stream.begin_round(2);
stream.start("attempt-1".into(), Utc::now());
stream.append("partial".into());
let (mut receiver, snapshot) = stream.subscribe_with_snapshot();
assert_eq!(snapshot.messages.len(), 1);
stream.discard("attempt-1");
stream.start("attempt-2".into(), Utc::now());
let discarded = receiver.try_recv().expect("discarded event");
let started = receiver.try_recv().expect("replacement start");
assert!(matches!(
discarded.kind,
VisibleMessageEventKind::Discarded { ref message_id } if message_id == "attempt-1"
));
assert!(matches!(
started.kind,
VisibleMessageEventKind::Started { ref message_id, .. } if message_id == "attempt-2"
));
}
#[test]
fn multi_round_messages_survive_until_history_commit() {
let stream = VisibleMessageStream::new();
stream.begin_round(1);
stream.start("round-1".into(), Utc::now());
stream.append("first".into());
stream.begin_round(2);
stream.start("round-2".into(), Utc::now());
stream.append("second".into());
let (_, snapshot) = stream.subscribe_with_snapshot();
assert_eq!(snapshot.messages.len(), 2);
assert_eq!(snapshot.messages[0].round_count, 1);
assert_eq!(snapshot.messages[0].content, "first");
assert_eq!(snapshot.messages[1].round_count, 2);
assert_eq!(snapshot.messages[1].content, "second");
}
#[test]
fn failed_and_cancelled_terminal_reasons_remain_in_late_snapshots() {
for reason in ["error", "cancelled"] {
let stream = VisibleMessageStream::new();
stream.start("partial".into(), Utc::now());
stream.append("visible prefix".into());
stream.mark_terminal(reason);
let (_, snapshot) = stream.subscribe_with_snapshot();
assert_eq!(snapshot.terminal.as_deref(), Some(reason));
assert_eq!(snapshot.messages[0].content, "visible prefix");
}
}
#[test]
fn history_commit_clears_text_only_after_control_is_emitted() {
let stream = VisibleMessageStream::new();
stream.start("message-1".into(), Utc::now());
stream.append("visible".into());
let (mut receiver, snapshot) = stream.subscribe_with_snapshot();
assert_eq!(snapshot.messages.len(), 1);
stream.history_committed();
let committed = receiver.try_recv().expect("history commit control");
assert!(matches!(
committed.kind,
VisibleMessageEventKind::HistoryCommitted
));
let (_, settled) = stream.subscribe_with_snapshot();
assert!(settled.messages.is_empty());
assert!(settled.history_committed);
}
}