1use std::ops::Deref;
6use std::sync::{Arc, Mutex};
7
8use crate::compaction::is_compaction_summary;
9use crate::event::EventEnvelope;
10use crate::message::Message;
11
12#[derive(Clone)]
13pub struct MessageWindow {
14 messages: Arc<Vec<Message>>,
15 start: usize,
16}
17
18impl MessageWindow {
19 pub fn to_vec(&self) -> Vec<Message> {
20 self.as_slice().to_vec()
21 }
22
23 fn as_slice(&self) -> &[Message] {
24 &self.messages[self.start..]
25 }
26}
27
28impl Deref for MessageWindow {
29 type Target = [Message];
30
31 fn deref(&self) -> &Self::Target {
32 self.as_slice()
33 }
34}
35
36struct Acc {
37 compacted: Vec<(u64, Message)>,
38 full_raw: Vec<(u64, Message)>,
39 replayed: usize,
40 full_cache: Arc<Vec<Message>>,
41 window_cache: MessageWindow,
42}
43
44pub struct MessageStream {
45 events: Arc<Mutex<Vec<EventEnvelope>>>,
46 initial_compacted: Vec<(u64, Message)>,
47 initial_raw: Vec<(u64, Message)>,
48 acc: Mutex<Acc>,
49}
50
51impl MessageStream {
52 pub fn new(events: Arc<Mutex<Vec<EventEnvelope>>>) -> Self {
53 let empty = Arc::new(Vec::new());
54 Self {
55 events,
56 initial_compacted: Vec::new(),
57 initial_raw: Vec::new(),
58 acc: Mutex::new(Acc {
59 compacted: Vec::new(),
60 full_raw: Vec::new(),
61 replayed: 0,
62 full_cache: Arc::clone(&empty),
63 window_cache: MessageWindow {
64 messages: empty,
65 start: 0,
66 },
67 }),
68 }
69 }
70
71 pub fn with_initial(
72 events: Arc<Mutex<Vec<EventEnvelope>>>,
73 compacted: Vec<(u64, Message)>,
74 raw: Vec<(u64, Message)>,
75 ) -> Self {
76 let full: Arc<Vec<Message>> = Arc::new(raw.iter().map(|(_, msg)| msg.clone()).collect());
77 let start = full.iter().rposition(is_compaction_summary).unwrap_or(0);
78 let window = MessageWindow {
79 messages: Arc::clone(&full),
80 start,
81 };
82 Self {
83 events,
84 initial_compacted: compacted,
85 initial_raw: raw,
86 acc: Mutex::new(Acc {
87 compacted: Vec::new(),
88 full_raw: Vec::new(),
89 replayed: 0,
90 full_cache: full,
91 window_cache: window,
92 }),
93 }
94 }
95
96 pub fn full_messages(&self) -> Arc<Vec<Message>> {
97 let events = self.events.lock().expect("events poisoned");
98 let mut acc = self.acc.lock().expect("acc poisoned");
99 self.ensure_fresh_locked(&events, &mut acc);
100 Arc::clone(&acc.full_cache)
101 }
102
103 pub fn window(&self) -> MessageWindow {
104 let events = self.events.lock().expect("events poisoned");
105 let mut acc = self.acc.lock().expect("acc poisoned");
106 self.ensure_fresh_locked(&events, &mut acc);
107 acc.window_cache.clone()
108 }
109
110 fn ensure_fresh_locked(&self, events: &[EventEnvelope], acc: &mut Acc) {
111 if acc.compacted.is_empty() {
112 acc.compacted = self.initial_compacted.clone();
113 acc.full_raw = self.initial_raw.clone();
114 }
115 if acc.replayed >= events.len() {
116 return;
117 }
118 for ev in &events[acc.replayed..] {
119 crate::projection::message_window::apply_envelope_to_messages(ev, &mut acc.compacted);
120 match &ev.event {
121 crate::event::Event::UserMsg {
122 message,
123 flow_run_id: None,
124 ..
125 }
126 | crate::event::Event::AssistantMsg {
127 message,
128 flow_run_id: None,
129 ..
130 }
131 | crate::event::Event::ToolResultMsg {
132 message,
133 flow_run_id: None,
134 ..
135 }
136 | crate::event::Event::SystemMsg { message, .. } => {
137 acc.full_raw.push((ev.seq, message.clone()));
138 }
139 _ => {}
140 }
141 }
142 acc.replayed = events.len();
143
144 let compacted: Vec<Message> = acc.compacted.iter().map(|(_, msg)| msg).cloned().collect();
145 let start = compacted
146 .iter()
147 .rposition(is_compaction_summary)
148 .unwrap_or(0);
149 let compacted_arc = Arc::new(compacted);
150 acc.window_cache = MessageWindow {
151 messages: Arc::clone(&compacted_arc),
152 start,
153 };
154
155 let raw: Vec<Message> = acc.full_raw.iter().map(|(_, msg)| msg).cloned().collect();
156 acc.full_cache = Arc::new(raw);
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163 use crate::event::TurnId;
164 use crate::event::{Event, EventEnvelope};
165 use crate::message::{MessagePart, MessageRole};
166
167 fn user(text: &str) -> Message {
168 Message {
169 role: MessageRole::User,
170 parts: vec![MessagePart::Text {
171 text: text.to_string(),
172 }],
173 turn_id: TurnId::now(),
174 }
175 }
176
177 fn assistant(text: &str) -> Message {
178 Message {
179 role: MessageRole::Assistant,
180 parts: vec![MessagePart::Text {
181 text: text.to_string(),
182 }],
183 turn_id: TurnId::now(),
184 }
185 }
186
187 fn compact_summary(text: &str) -> Message {
188 Message::system_compact_summary(TurnId::now(), text, 0, 1, 2)
189 }
190
191 fn make_msg_event(ty: &str, msg: &Message, _seq: u64) -> Event {
192 match ty {
193 "user_msg" => Event::UserMsg {
194 turn_id: msg.turn_id.clone(),
195 flow_run_id: None,
196 message: msg.clone(),
197 },
198 "assistant_msg" => Event::AssistantMsg {
199 turn_id: msg.turn_id.clone(),
200 flow_run_id: None,
201 message: msg.clone(),
202 },
203 "system_msg" => Event::SystemMsg {
204 turn_id: msg.turn_id.clone(),
205 message: msg.clone(),
206 },
207 _ => unreachable!(),
208 }
209 }
210
211 fn make_context_compact(
212 range_start: u64,
213 range_end: u64,
214 before_tokens: u64,
215 after_tokens: u64,
216 summary_text: &str,
217 replacement_msg_seq: u64,
218 ) -> Event {
219 Event::ContextCompact {
220 session_id: "test".into(),
221 before_tokens,
222 after_tokens,
223 compacted_range_start: range_start,
224 compacted_range_end: range_end,
225 summary_text: Some(summary_text.into()),
226 replacement_msg_seq: Some(replacement_msg_seq),
227 }
228 }
229
230 fn event_envelopes(events: Vec<Event>) -> Arc<Mutex<Vec<EventEnvelope>>> {
231 Arc::new(Mutex::new(
232 events
233 .into_iter()
234 .enumerate()
235 .map(|(i, event)| EventEnvelope::new((i + 1) as u64, event))
236 .collect(),
237 ))
238 }
239
240 #[test]
241 fn full_messages_filters_only_message_events() {
242 let u1 = user("hello");
243 let a1 = assistant("hi there");
244 let events = event_envelopes(vec![
245 make_msg_event("user_msg", &u1, 1),
246 Event::TurnStart {
247 turn_id: TurnId::now(),
248 },
249 make_msg_event("assistant_msg", &a1, 2),
250 Event::LlmCall {
251 model: "m".into(),
252 provider: "p".into(),
253 usage: crate::provider::TokenUsage::default(),
254 wallclock_ms: 0,
255 ttft_ms: None,
256 tokens_per_second: None,
257 status: crate::event::LlmCallStatus::Ok,
258 run_id: None,
259 node_id: None,
260 },
261 ]);
262 let ms = MessageStream::new(events);
263 let msgs = ms.full_messages();
264 assert_eq!(msgs.len(), 2);
265 assert_eq!(msgs[0].text_concat(), "hello");
266 assert_eq!(msgs[1].text_concat(), "hi there");
267 }
268
269 #[test]
270 fn window_no_summary_returns_all() {
271 let events = vec![
272 make_msg_event("user_msg", &user("a"), 1),
273 make_msg_event("assistant_msg", &assistant("b"), 2),
274 make_msg_event("user_msg", &user("c"), 3),
275 ];
276 let ms = MessageStream::new(event_envelopes(events));
277 assert_eq!(ms.window().len(), 3);
278 }
279
280 #[test]
281 fn window_single_summary_starts_from_it() {
282 let s1 = compact_summary("summary 1");
283 let events = vec![
284 make_msg_event("user_msg", &user("old"), 1),
285 make_msg_event("assistant_msg", &assistant("old"), 2),
286 make_msg_event("system_msg", &s1, 3),
287 make_msg_event("user_msg", &user("new"), 4),
288 make_msg_event("assistant_msg", &assistant("new"), 5),
289 ];
290 let ms = MessageStream::new(event_envelopes(events));
291 let w = ms.window();
292 assert_eq!(w.len(), 3);
293 assert!(matches!(w[0].parts[0], MessagePart::CompactSummary { .. }));
294 }
295
296 #[test]
297 fn window_multiple_summaries_uses_last() {
298 let s1 = compact_summary("summary 1");
299 let s2 = compact_summary("summary 2");
300 let events = vec![
301 make_msg_event("system_msg", &s1, 1),
302 make_msg_event("user_msg", &user("m1"), 2),
303 make_msg_event("system_msg", &s2, 3),
304 make_msg_event("user_msg", &user("m2"), 4),
305 ];
306 let ms = MessageStream::new(event_envelopes(events));
307 let w = ms.window();
308 assert_eq!(w.len(), 2);
309 assert!(matches!(w[0].parts[0], MessagePart::CompactSummary { .. }));
310 if let MessagePart::CompactSummary { summary, .. } = &w[0].parts[0] {
311 assert_eq!(summary, "summary 2");
312 }
313 }
314
315 #[test]
316 fn window_no_prefix_before_summary() {
317 let s1 = compact_summary("summary");
318 let events = vec![
319 make_msg_event("user_msg", &user("very old"), 1),
320 make_msg_event("assistant_msg", &assistant("very old"), 2),
321 make_msg_event("system_msg", &s1, 3),
322 make_msg_event("user_msg", &user("new"), 4),
323 ];
324 let ms = MessageStream::new(event_envelopes(events));
325 let w = ms.window();
326 assert_eq!(w.len(), 2);
327 assert!(matches!(w[0].parts[0], MessagePart::CompactSummary { .. }));
328 assert_eq!(w[1].text_concat(), "new");
329 }
330
331 #[test]
332 fn window_empty_stream_returns_empty() {
333 let ms = MessageStream::new(event_envelopes(Vec::new()));
334 assert!(ms.window().is_empty());
335 }
336
337 #[test]
338 fn context_compact_replaces_range_with_summary() {
339 let events = vec![
340 make_msg_event("user_msg", &user("old u1"), 1),
341 make_msg_event("assistant_msg", &assistant("old a1"), 2),
342 make_msg_event("user_msg", &user("old u2"), 3),
343 make_msg_event("system_msg", &compact_summary("summary"), 4),
344 make_context_compact(0, 2, 100, 50, "compaction summary text", 4),
345 make_msg_event("user_msg", &user("after compact"), 5),
346 ];
347 let ms = MessageStream::new(event_envelopes(events));
348 let w = ms.window();
349 assert_eq!(w.len(), 2);
350 assert!(matches!(w[0].parts[0], MessagePart::CompactSummary { .. }));
351 }
352
353 #[test]
354 fn multiple_compactions_applied_in_order() {
355 let events = vec![
356 make_msg_event("user_msg", &user("a"), 1),
357 make_msg_event("assistant_msg", &assistant("b"), 2),
358 make_msg_event("system_msg", &compact_summary("s1"), 3),
359 make_context_compact(0, 1, 200, 100, "first summary", 3),
360 make_msg_event("user_msg", &user("c"), 4),
361 make_msg_event("assistant_msg", &assistant("d"), 5),
362 make_msg_event("system_msg", &compact_summary("s2"), 6),
363 make_context_compact(1, 2, 150, 80, "second summary", 6),
364 make_msg_event("user_msg", &user("e"), 7),
365 ];
366 let ms = MessageStream::new(event_envelopes(events));
367 let w = ms.window();
368 assert_eq!(w.len(), 2);
369 assert!(matches!(w[0].parts[0], MessagePart::CompactSummary { .. }));
370 if let MessagePart::CompactSummary { summary, .. } = &w[0].parts[0] {
371 assert_eq!(summary, "second summary");
372 }
373 }
374
375 #[test]
376 fn compact_then_user_message_produces_summary_plus_user() {
377 let events = vec![
378 make_msg_event("user_msg", &user("old u1"), 1),
379 make_msg_event("assistant_msg", &assistant("old a1"), 2),
380 make_msg_event("user_msg", &user("old u2"), 3),
381 make_msg_event("system_msg", &compact_summary("compact summary"), 4),
382 make_context_compact(0, 2, 200, 100, "compact summary", 4),
383 make_msg_event("user_msg", &user("new message after compact"), 5),
384 ];
385 let ms = MessageStream::new(event_envelopes(events));
386 let w = ms.window();
387 assert_eq!(w.len(), 2);
388 assert!(matches!(w[0].parts[0], MessagePart::CompactSummary { .. }));
389 assert_eq!(w[1].text_concat(), "new message after compact");
390 }
391
392 #[test]
393 fn no_compaction_window_equals_full_messages() {
394 let events = vec![
395 make_msg_event("user_msg", &user("first"), 1),
396 make_msg_event("assistant_msg", &assistant("second"), 2),
397 make_msg_event("user_msg", &user("third"), 3),
398 ];
399 let ms = MessageStream::new(event_envelopes(events));
400 assert_eq!(ms.full_messages().len(), 3);
401 assert_eq!(ms.window().len(), 3);
402 }
403
404 #[test]
405 fn full_messages_retains_compacted_history() {
406 let events = vec![
407 make_msg_event("user_msg", &user("old u1"), 1),
408 make_msg_event("assistant_msg", &assistant("old a1"), 2),
409 make_msg_event("user_msg", &user("old u2"), 3),
410 make_msg_event("system_msg", &compact_summary("summary"), 4),
411 make_context_compact(0, 2, 200, 100, "summary", 4),
412 make_msg_event("user_msg", &user("after compact"), 5),
413 ];
414 let ms = MessageStream::new(event_envelopes(events));
415 let w = ms.window();
417 assert_eq!(w.len(), 2);
418 let f = ms.full_messages();
420 assert_eq!(f.len(), 5, "full must retain compacted messages");
421 assert_eq!(f[0].text_concat(), "old u1");
422 assert_eq!(f[1].text_concat(), "old a1");
423 assert_eq!(f[2].text_concat(), "old u2");
424 }
425
426 #[test]
427 fn third_compaction_replaces_second_summary() {
428 let events = vec![
429 make_msg_event("user_msg", &user("a"), 1),
430 make_msg_event("assistant_msg", &assistant("b"), 2),
431 make_msg_event("system_msg", &compact_summary("s1"), 3),
432 make_context_compact(0, 1, 100, 50, "s1 text", 3),
433 make_msg_event("user_msg", &user("c"), 4),
434 make_msg_event("system_msg", &compact_summary("s2"), 5),
435 make_context_compact(0, 1, 80, 40, "s2 text", 6),
436 make_msg_event("user_msg", &user("d"), 6),
437 make_msg_event("system_msg", &compact_summary("s3"), 7),
438 make_context_compact(0, 1, 70, 30, "s3 text", 9),
439 make_msg_event("user_msg", &user("final"), 8),
440 ];
441 let ms = MessageStream::new(event_envelopes(events));
442 let w = ms.window();
443 assert_eq!(w.len(), 2);
444 if let MessagePart::CompactSummary { summary, .. } = &w[0].parts[0] {
445 assert_eq!(summary, "s3 text");
446 }
447 assert_eq!(w[1].text_concat(), "final");
448 }
449
450 #[test]
451 fn reopened_session_keeps_initial_messages_after_new_events() {
452 let initial_compacted = vec![
453 (1, compact_summary("compaction summary")),
454 (2, assistant("tail assistant")),
455 ];
456 let initial_raw = vec![
457 (1, compact_summary("compaction summary")),
458 (2, assistant("tail assistant")),
459 ];
460 let events = Arc::new(Mutex::new(Vec::new()));
461 let ms = MessageStream::with_initial(events.clone(), initial_compacted, initial_raw);
462
463 events.lock().unwrap().push(EventEnvelope::new(
464 1,
465 Event::TurnStart {
466 turn_id: TurnId::now(),
467 },
468 ));
469 events.lock().unwrap().push(EventEnvelope::new(
470 2,
471 Event::UserMsg {
472 turn_id: TurnId::now(),
473 flow_run_id: None,
474 message: user("latest user"),
475 },
476 ));
477
478 let w = ms.window();
479 assert_eq!(w.len(), 3);
480 assert!(matches!(w[0].parts[0], MessagePart::CompactSummary { .. }));
481 assert_eq!(w[1].text_concat(), "tail assistant");
482 assert_eq!(w[2].text_concat(), "latest user");
483 }
484
485 #[test]
488 fn full_messages_retains_pre_compact_history_after_runtime_compact() {
489 let initial_compacted = vec![
490 (1, compact_summary("prior summary")),
491 (2, user("old user")),
492 (3, assistant("old assistant")),
493 ];
494 let initial_raw = vec![
495 (1, compact_summary("prior summary")),
496 (2, user("old user")),
497 (3, assistant("old assistant")),
498 ];
499 let events = Arc::new(Mutex::new(Vec::new()));
500 let ms = MessageStream::with_initial(events.clone(), initial_compacted, initial_raw);
501
502 events.lock().unwrap().push(EventEnvelope::new(
503 10,
504 Event::UserMsg {
505 turn_id: TurnId::now(),
506 flow_run_id: None,
507 message: user("new user before compact"),
508 },
509 ));
510 events.lock().unwrap().push(EventEnvelope::new(
511 11,
512 Event::AssistantMsg {
513 turn_id: TurnId::now(),
514 flow_run_id: None,
515 message: assistant("new assistant before compact"),
516 },
517 ));
518
519 let before = ms.full_messages();
520 assert_eq!(before.len(), 5, "pre-compact full should have all 5 msgs");
521
522 events.lock().unwrap().push(EventEnvelope::new(
523 12,
524 Event::SystemMsg {
525 turn_id: TurnId::now(),
526 message: compact_summary("runtime summary"),
527 },
528 ));
529 events.lock().unwrap().push(EventEnvelope::new(
530 13,
531 Event::ContextCompact {
532 session_id: "test".into(),
533 before_tokens: 1000,
534 after_tokens: 100,
535 compacted_range_start: 1,
536 compacted_range_end: 2,
537 summary_text: Some("runtime summary".into()),
538 replacement_msg_seq: Some(12),
539 },
540 ));
541
542 events.lock().unwrap().push(EventEnvelope::new(
543 14,
544 Event::UserMsg {
545 turn_id: TurnId::now(),
546 flow_run_id: None,
547 message: user("after compact user"),
548 },
549 ));
550
551 let w = ms.window();
552 assert_eq!(w.len(), 4, "window after compact");
553 assert!(matches!(w[0].parts[0], MessagePart::CompactSummary { .. }));
554
555 let full = ms.full_messages();
556 assert!(
557 full.len() >= 6,
558 "full must retain pre-compact history, got {} msgs: {:?}",
559 full.len(),
560 full.iter().map(|m| m.text_concat()).collect::<Vec<_>>()
561 );
562 let texts: Vec<String> = full.iter().map(|m| m.text_concat()).collect();
563 assert!(
564 texts.iter().any(|t| t.contains("old user")),
565 "full must contain pre-compact 'old user', got: {:?}",
566 texts
567 );
568 assert!(
569 texts.iter().any(|t| t.contains("old assistant")),
570 "full must contain pre-compact 'old assistant', got: {:?}",
571 texts
572 );
573 }
574}