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