1use std::collections::VecDeque;
3
4use vtcode_exec_events::VersionedThreadEvent;
5
6pub fn live_event_channel(capacity: usize) -> (LiveEventSender, LiveEventReceiver) {
17 assert!(capacity > 0, "live event capacity must be positive");
18 let inner = std::sync::Arc::new(LiveEventQueueInner {
19 capacity,
20 queue: std::sync::Mutex::new(VecDeque::new()),
21 senders: std::sync::atomic::AtomicUsize::new(1),
22 receiver_alive: std::sync::atomic::AtomicBool::new(true),
23 dropped_ordered: std::sync::atomic::AtomicU64::new(0),
24 dropped_stale: std::sync::atomic::AtomicU64::new(0),
25 notify: tokio::sync::Notify::new(),
26 });
27 (LiveEventSender { inner: inner.clone() }, LiveEventReceiver { inner })
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32enum CoalesceOp {
33 Replace,
35 Merge,
37 MergeOutput,
39}
40
41struct LiveEventSlot {
44 key: Option<String>,
45 op: CoalesceOp,
46 event: VersionedThreadEvent,
47}
48
49struct LiveEventQueueInner {
50 capacity: usize,
51 queue: std::sync::Mutex<VecDeque<LiveEventSlot>>,
52 senders: std::sync::atomic::AtomicUsize,
53 receiver_alive: std::sync::atomic::AtomicBool,
54 dropped_ordered: std::sync::atomic::AtomicU64,
55 dropped_stale: std::sync::atomic::AtomicU64,
56 notify: tokio::sync::Notify,
57}
58
59impl LiveEventQueueInner {
60 fn classify(event: &VersionedThreadEvent) -> Option<(String, CoalesceOp)> {
61 match event.event() {
62 vtcode_exec_events::ThreadEvent::ItemUpdated(update) => {
63 let op = match &update.item.details {
64 vtcode_exec_events::ThreadItemDetails::ToolOutput(output) if output.is_delta == Some(true) => {
65 CoalesceOp::MergeOutput
66 }
67 _ => CoalesceOp::Replace,
68 };
69 Some((update.item.id.clone(), op))
70 }
71 vtcode_exec_events::ThreadEvent::PlanDelta(delta) => Some((delta.item_id.clone(), CoalesceOp::Merge)),
72 _ => None,
73 }
74 }
75
76 fn publish(&self, event: VersionedThreadEvent) -> Result<(), ()> {
79 if !self.receiver_alive.load(std::sync::atomic::Ordering::Acquire) {
80 return Err(());
81 }
82 let mut queue = self.queue.lock().map_err(|_poisoned| ())?;
83 let classified = Self::classify(&event);
84 let slot_key = classified.as_ref().map(|(key, _)| key.clone());
85 let slot_op = classified.as_ref().map_or(CoalesceOp::Replace, |(_, op)| *op);
86 if let Some((key, op)) = classified {
87 let pending_slot = queue
88 .iter()
89 .position(|slot| slot.key.as_deref() == Some(key.as_str()) && slot.op == op)
90 .and_then(|index| queue.remove(index).map(|removed| (index, removed)));
91 if let Some((index, pending)) = pending_slot {
92 let merged = match op {
95 CoalesceOp::Replace => event,
96 CoalesceOp::Merge => Self::merge_plan_deltas(pending.event, event),
97 CoalesceOp::MergeOutput => Self::merge_tool_output_deltas(pending.event, event),
98 };
99 queue.insert(index, LiveEventSlot { key: Some(key), op, event: merged });
100 drop(queue);
101 self.notify.notify_waiters();
102 return Ok(());
103 }
104 }
105 if queue.len() >= self.capacity {
106 let victim = queue.iter().position(|slot| slot.key.is_some());
110 match victim {
111 Some(index) => {
112 drop(queue.remove(index));
113 let _ = self.dropped_stale.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
114 }
115 None => {
116 drop(queue.pop_front());
117 let _ = self.dropped_ordered.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
118 }
119 }
120 }
121 queue.push_back(LiveEventSlot { key: slot_key, op: slot_op, event });
122 drop(queue);
123 self.notify.notify_waiters();
124 Ok(())
125 }
126
127 fn merge_tool_output_deltas(pending: VersionedThreadEvent, next: VersionedThreadEvent) -> VersionedThreadEvent {
130 let (pending, next) = (pending.into_event(), next.into_event());
131 match (pending, next) {
132 (
133 vtcode_exec_events::ThreadEvent::ItemUpdated(older),
134 vtcode_exec_events::ThreadEvent::ItemUpdated(newer),
135 ) => {
136 let (
137 vtcode_exec_events::ThreadItemDetails::ToolOutput(older_output),
138 vtcode_exec_events::ThreadItemDetails::ToolOutput(newer_output),
139 ) = (&older.item.details, &newer.item.details)
140 else {
141 return VersionedThreadEvent::new(vtcode_exec_events::ThreadEvent::ItemUpdated(newer));
142 };
143 let mut merged = newer.clone();
144 let mut output = older_output.output.clone();
145 output.push_str(&newer_output.output);
146 merged.item.details =
147 vtcode_exec_events::ThreadItemDetails::ToolOutput(Box::new(vtcode_exec_events::ToolOutputItem {
148 call_id: newer_output.call_id.clone(),
149 tool_call_id: newer_output.tool_call_id.clone().or_else(|| older_output.tool_call_id.clone()),
150 spool_path: newer_output.spool_path.clone().or_else(|| older_output.spool_path.clone()),
151 output,
152 is_delta: Some(true),
153 exit_code: newer_output.exit_code.or(older_output.exit_code),
154 status: newer_output.status.clone(),
155 app_previews: if newer_output.app_previews.is_empty() {
156 older_output.app_previews.clone()
157 } else {
158 newer_output.app_previews.clone()
159 },
160 }));
161 VersionedThreadEvent::new(vtcode_exec_events::ThreadEvent::ItemUpdated(merged))
162 }
163 (_, fallback) => VersionedThreadEvent::new(fallback),
164 }
165 }
166
167 fn merge_plan_deltas(pending: VersionedThreadEvent, next: VersionedThreadEvent) -> VersionedThreadEvent {
170 let (pending, next) = (pending.into_event(), next.into_event());
171 match (pending, next) {
172 (vtcode_exec_events::ThreadEvent::PlanDelta(older), vtcode_exec_events::ThreadEvent::PlanDelta(newer)) => {
173 let mut merged = newer.clone();
174 let mut text = older.delta;
175 text.push_str(&newer.delta);
176 merged.delta = text;
177 if merged.recovery.is_none() {
178 merged.recovery = older.recovery;
179 }
180 VersionedThreadEvent::new(vtcode_exec_events::ThreadEvent::PlanDelta(merged))
181 }
182 (_, fallback) => VersionedThreadEvent::new(fallback),
183 }
184 }
185}
186
187pub struct LiveEventSender {
189 inner: std::sync::Arc<LiveEventQueueInner>,
190}
191
192impl Clone for LiveEventSender {
193 fn clone(&self) -> Self {
194 let _ = self.inner.senders.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
195 Self { inner: std::sync::Arc::clone(&self.inner) }
196 }
197}
198
199impl LiveEventSender {
200 pub fn publish(&self, event: VersionedThreadEvent) -> Result<(), LiveEventSendError> {
203 self.inner.publish(event).map_err(|()| LiveEventSendError(()))
204 }
205}
206
207impl Drop for LiveEventSender {
208 fn drop(&mut self) {
209 let _ = self.inner.senders.fetch_sub(1, std::sync::atomic::Ordering::Release);
210 self.inner.notify.notify_waiters();
211 }
212}
213
214pub struct LiveEventReceiver {
216 inner: std::sync::Arc<LiveEventQueueInner>,
217}
218
219impl LiveEventReceiver {
220 pub async fn recv(&self) -> Option<VersionedThreadEvent> {
224 loop {
225 {
226 let mut queue = self.inner.queue.lock().ok()?;
227 if let Some(slot) = queue.pop_front() {
228 return Some(slot.event);
229 }
230 if self.inner.senders.load(std::sync::atomic::Ordering::Acquire) == 0 {
231 return None;
232 }
233 }
234 let notified = self.inner.notify.notified();
235 {
236 let queue = self.inner.queue.lock().ok()?;
237 if !queue.is_empty() {
238 continue;
239 }
240 if self.inner.senders.load(std::sync::atomic::Ordering::Acquire) == 0 {
241 return None;
242 }
243 }
244 notified.await;
245 }
246 }
247
248 pub fn take_dropped_ordered(&self) -> u64 {
250 self.inner.dropped_ordered.swap(0, std::sync::atomic::Ordering::Relaxed)
251 }
252
253 pub fn take_dropped_stale(&self) -> u64 {
256 self.inner.dropped_stale.swap(0, std::sync::atomic::Ordering::Relaxed)
257 }
258}
259
260impl Drop for LiveEventReceiver {
261 fn drop(&mut self) {
262 self.inner.receiver_alive.store(false, std::sync::atomic::Ordering::Release);
263 self.inner.notify.notify_waiters();
264 }
265}
266
267#[derive(Debug, Clone, Copy, PartialEq, Eq)]
269pub struct LiveEventSendError(());
270
271impl std::fmt::Display for LiveEventSendError {
272 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
273 f.write_str("live event consumer disconnected")
274 }
275}
276
277impl std::error::Error for LiveEventSendError {}