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