1use std::collections::{BTreeMap, BTreeSet};
2use std::path::{Path, PathBuf};
3
4pub const MAX_ALERT_LINE_CHARS: usize = 240;
7pub const ALERT_ELLIPSIS: &str = "…";
8pub const MAX_RENDERED_ALERT_LINES: usize = 3;
9
10pub const EXCLUDED_FINALIZATION_COMMANDS: &[&str] = &[
13 "configure",
14 "ping",
15 "version",
16 "status",
17 "bash_abort_inflight",
18 "bash_status",
19 "bash_write",
20 "bash_promote",
21 "bash_wait_detach",
22 "bash_regex_match",
23 "bash_drain_completions",
24 "bash_notify",
25 "bash_unnotify",
26 "bash_ack_completions",
27];
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum AlertSeverity {
31 Error,
32 Warning,
33 Information,
34 Hint,
35}
36
37impl AlertSeverity {
38 fn canonical_name(self) -> &'static str {
39 match self {
40 Self::Error => "error",
41 Self::Warning => "warning",
42 Self::Information => "information",
43 Self::Hint => "hint",
44 }
45 }
46}
47
48#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct AlertDiagnostic {
51 pub file: String,
53 pub line: u32,
54 pub column: u32,
55 pub end_line: u32,
56 pub end_column: u32,
57 pub severity: AlertSeverity,
58 pub source: Option<String>,
59 pub code: Option<String>,
60 pub message: String,
61}
62
63impl AlertDiagnostic {
64 #[must_use]
65 pub fn error(file: impl Into<String>, line: u32, message: impl Into<String>) -> Self {
66 Self {
67 file: file.into(),
68 line,
69 column: 0,
70 end_line: line,
71 end_column: 0,
72 severity: AlertSeverity::Error,
73 source: None,
74 code: None,
75 message: message.into(),
76 }
77 }
78
79 fn identity(&self) -> AlertIdentity {
80 AlertIdentity {
81 file: self.file.clone(),
82 line: self.line,
83 column: self.column,
84 end_line: self.end_line,
85 end_column: self.end_column,
86 severity: self.severity.canonical_name().to_string(),
87 source: self.source.clone().unwrap_or_default(),
88 code: self.code.clone().unwrap_or_default(),
89 message: normalize_alert_message(&self.message),
90 }
91 }
92}
93
94#[derive(Debug, Clone, PartialEq, Eq)]
96pub struct AlertObservation {
97 pub producer_key: String,
98 pub diagnostics: Vec<AlertDiagnostic>,
99}
100
101impl AlertObservation {
102 #[must_use]
103 pub fn new(producer_key: impl Into<String>, diagnostics: Vec<AlertDiagnostic>) -> Self {
104 Self {
105 producer_key: producer_key.into(),
106 diagnostics,
107 }
108 }
109}
110
111#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
113pub struct AlertIdentity {
114 pub file: String,
115 pub line: u32,
116 pub column: u32,
117 pub end_line: u32,
118 pub end_column: u32,
119 pub severity: String,
120 pub source: String,
121 pub code: String,
122 pub message: String,
123}
124
125#[derive(Debug, Clone, PartialEq, Eq)]
126struct AlertCandidate {
127 identity: AlertIdentity,
128 entered_observation_ordinal: u64,
129}
130
131#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
132struct PartitionKey {
133 dispatch_root: PathBuf,
134 producer_key: String,
135}
136
137#[derive(Debug, Default)]
138struct ProducerPartition {
139 baseline_established: bool,
140 live: BTreeMap<AlertIdentity, AlertCandidate>,
141 rendered: BTreeSet<AlertIdentity>,
142}
143
144#[derive(Debug, Default)]
145struct SessionAlertState {
146 next_observation_ordinal: u64,
147 agent_visible_response_ordinal: u64,
148 partitions: BTreeMap<PartitionKey, ProducerPartition>,
149}
150
151#[derive(Debug, Default)]
157pub struct AlertEngine {
158 sessions: BTreeMap<String, SessionAlertState>,
159}
160
161#[derive(Debug, Clone, PartialEq, Eq)]
163pub struct RenderedAlert {
164 pub text: String,
165 pub shown: Vec<AlertIdentity>,
166 pub counted_only: Vec<AlertIdentity>,
167 pub agent_visible_response_ordinal: u64,
168}
169
170impl RenderedAlert {
171 #[must_use]
172 pub fn represented_identities(&self) -> impl Iterator<Item = &AlertIdentity> {
173 self.shown.iter().chain(&self.counted_only)
174 }
175}
176
177impl AlertEngine {
178 pub fn observe_authoritative_batch(
181 &mut self,
182 session_id: &str,
183 dispatch_root: &Path,
184 observations: impl IntoIterator<Item = AlertObservation>,
185 ) {
186 let root = canonical_dispatch_root(dispatch_root);
187 let state = self.sessions.entry(session_id.to_string()).or_default();
188 state.next_observation_ordinal = state.next_observation_ordinal.saturating_add(1);
189 let observation_ordinal = state.next_observation_ordinal;
190
191 for observation in observations {
192 let key = PartitionKey {
193 dispatch_root: root.clone(),
194 producer_key: observation.producer_key,
195 };
196 let current = observation
197 .diagnostics
198 .into_iter()
199 .filter(|diagnostic| diagnostic.severity == AlertSeverity::Error)
200 .map(|diagnostic| diagnostic.identity())
201 .collect::<BTreeSet<_>>();
202 let partition = state.partitions.entry(key).or_default();
203
204 if !partition.baseline_established {
205 partition.baseline_established = true;
208 partition.live = current
209 .into_iter()
210 .map(|identity| {
211 let candidate = AlertCandidate {
212 identity: identity.clone(),
213 entered_observation_ordinal: observation_ordinal,
214 };
215 (identity, candidate)
216 })
217 .collect();
218 partition.rendered = partition.live.keys().cloned().collect();
219 continue;
220 }
221
222 partition
225 .rendered
226 .retain(|identity| current.contains(identity));
227 let previous_live = std::mem::take(&mut partition.live);
228 partition.live = current
229 .into_iter()
230 .map(|identity| {
231 let candidate =
232 previous_live
233 .get(&identity)
234 .cloned()
235 .unwrap_or_else(|| AlertCandidate {
236 identity: identity.clone(),
237 entered_observation_ordinal: observation_ordinal,
238 });
239 (identity, candidate)
240 })
241 .collect();
242 }
243 }
244
245 pub fn observe_authoritative(
247 &mut self,
248 session_id: &str,
249 dispatch_root: &Path,
250 producer_key: impl Into<String>,
251 diagnostics: Vec<AlertDiagnostic>,
252 ) {
253 self.observe_authoritative_batch(
254 session_id,
255 dispatch_root,
256 [AlertObservation::new(producer_key, diagnostics)],
257 );
258 }
259
260 pub fn finalize(
263 &mut self,
264 session_id: &str,
265 dispatch_root: &Path,
266 command: &str,
267 ) -> Option<RenderedAlert> {
268 if is_excluded_finalization_command(command) {
269 return None;
270 }
271
272 let root = canonical_dispatch_root(dispatch_root);
273 let state = self.sessions.entry(session_id.to_string()).or_default();
274 state.agent_visible_response_ordinal =
275 state.agent_visible_response_ordinal.saturating_add(1);
276 let response_ordinal = state.agent_visible_response_ordinal;
277
278 let mut deliverable = Vec::new();
279 for (partition_key, partition) in &state.partitions {
280 if partition_key.dispatch_root != root {
281 continue;
282 }
283 for (identity, candidate) in &partition.live {
284 if !partition.rendered.contains(identity) {
285 deliverable.push((partition_key.clone(), candidate.clone()));
286 }
287 }
288 }
289 if deliverable.is_empty() {
290 return None;
291 }
292
293 deliverable.sort_by(|(_, left), (_, right)| {
296 right
297 .entered_observation_ordinal
298 .cmp(&left.entered_observation_ordinal)
299 .then_with(|| left.identity.cmp(&right.identity))
300 });
301
302 let shown_count = if deliverable.len() > MAX_RENDERED_ALERT_LINES - 1 {
303 MAX_RENDERED_ALERT_LINES - 1
304 } else {
305 deliverable.len()
306 };
307 let shown = deliverable
308 .iter()
309 .take(shown_count)
310 .map(|(_, candidate)| candidate.identity.clone())
311 .collect::<Vec<_>>();
312 let counted_only = deliverable
313 .iter()
314 .skip(shown_count)
315 .map(|(_, candidate)| candidate.identity.clone())
316 .collect::<Vec<_>>();
317
318 let mut lines = shown.iter().map(render_alert_line).collect::<Vec<_>>();
319 if !counted_only.is_empty() {
320 lines.push(format!("(+{} more)", counted_only.len()));
321 }
322 let text = format!(
323 "<system-reminder>\n{}\n</system-reminder>",
324 lines.join("\n")
325 );
326
327 for (partition_key, candidate) in deliverable {
330 if let Some(partition) = state.partitions.get_mut(&partition_key) {
331 partition.rendered.insert(candidate.identity);
332 }
333 }
334
335 Some(RenderedAlert {
336 text,
337 shown,
338 counted_only,
339 agent_visible_response_ordinal: response_ordinal,
340 })
341 }
342
343 #[must_use]
344 pub fn agent_visible_response_ordinal(&self, session_id: &str) -> u64 {
345 self.sessions
346 .get(session_id)
347 .map_or(0, |state| state.agent_visible_response_ordinal)
348 }
349
350 #[must_use]
351 pub fn partition_is_baselined(
352 &self,
353 session_id: &str,
354 dispatch_root: &Path,
355 producer_key: &str,
356 ) -> bool {
357 let key = PartitionKey {
358 dispatch_root: canonical_dispatch_root(dispatch_root),
359 producer_key: producer_key.to_string(),
360 };
361 self.sessions
362 .get(session_id)
363 .and_then(|state| state.partitions.get(&key))
364 .is_some_and(|partition| partition.baseline_established)
365 }
366
367 #[must_use]
368 pub fn partition_rendered_identities(
369 &self,
370 session_id: &str,
371 dispatch_root: &Path,
372 producer_key: &str,
373 ) -> BTreeSet<AlertIdentity> {
374 let key = PartitionKey {
375 dispatch_root: canonical_dispatch_root(dispatch_root),
376 producer_key: producer_key.to_string(),
377 };
378 self.sessions
379 .get(session_id)
380 .and_then(|state| state.partitions.get(&key))
381 .map_or_else(BTreeSet::new, |partition| partition.rendered.clone())
382 }
383}
384
385#[must_use]
386pub fn is_excluded_finalization_command(command: &str) -> bool {
387 EXCLUDED_FINALIZATION_COMMANDS.contains(&command)
388}
389
390#[must_use]
393pub fn normalize_alert_message(message: &str) -> String {
394 let first_line = message.lines().next().unwrap_or_default().trim();
395 let collapsed = first_line.split_whitespace().collect::<Vec<_>>().join(" ");
396 if collapsed.is_ascii() {
397 collapsed
398 } else {
399 compose_common_nfc(&collapsed)
400 }
401}
402
403fn canonical_dispatch_root(root: &Path) -> PathBuf {
404 std::fs::canonicalize(root).unwrap_or_else(|_| root.to_path_buf())
405}
406
407fn render_alert_line(identity: &AlertIdentity) -> String {
408 let line = format!(
409 "New error in {}:{}: {}",
410 identity.file, identity.line, identity.message
411 );
412 truncate_alert_line(&line)
413}
414
415fn truncate_alert_line(line: &str) -> String {
416 if line.chars().count() <= MAX_ALERT_LINE_CHARS {
417 return line.to_string();
418 }
419
420 let prefix_len = MAX_ALERT_LINE_CHARS.saturating_sub(ALERT_ELLIPSIS.chars().count());
421 let mut truncated = line.chars().take(prefix_len).collect::<String>();
422 truncated.push_str(ALERT_ELLIPSIS);
423 truncated
424}
425
426fn compose_common_nfc(input: &str) -> String {
430 let mut normalized = String::with_capacity(input.len());
431 let mut chars = input.chars().peekable();
432 while let Some(base) = chars.next() {
433 let Some(&mark) = chars.peek() else {
434 normalized.push(base);
435 continue;
436 };
437 if let Some(composed) = compose_pair(base, mark) {
438 normalized.push(composed);
439 chars.next();
440 } else {
441 normalized.push(base);
442 }
443 }
444 normalized
445}
446
447fn compose_pair(base: char, mark: char) -> Option<char> {
448 let composed = match (base, mark) {
449 ('A', '\u{0300}') => 'À',
450 ('A', '\u{0301}') => 'Á',
451 ('A', '\u{0302}') => 'Â',
452 ('A', '\u{0303}') => 'Ã',
453 ('A', '\u{0308}') => 'Ä',
454 ('A', '\u{030A}') => 'Å',
455 ('C', '\u{0327}') => 'Ç',
456 ('E', '\u{0300}') => 'È',
457 ('E', '\u{0301}') => 'É',
458 ('E', '\u{0302}') => 'Ê',
459 ('E', '\u{0308}') => 'Ë',
460 ('I', '\u{0300}') => 'Ì',
461 ('I', '\u{0301}') => 'Í',
462 ('I', '\u{0302}') => 'Î',
463 ('I', '\u{0308}') => 'Ï',
464 ('N', '\u{0303}') => 'Ñ',
465 ('O', '\u{0300}') => 'Ò',
466 ('O', '\u{0301}') => 'Ó',
467 ('O', '\u{0302}') => 'Ô',
468 ('O', '\u{0303}') => 'Õ',
469 ('O', '\u{0308}') => 'Ö',
470 ('U', '\u{0300}') => 'Ù',
471 ('U', '\u{0301}') => 'Ú',
472 ('U', '\u{0302}') => 'Û',
473 ('U', '\u{0308}') => 'Ü',
474 ('Y', '\u{0301}') => 'Ý',
475 ('a', '\u{0300}') => 'à',
476 ('a', '\u{0301}') => 'á',
477 ('a', '\u{0302}') => 'â',
478 ('a', '\u{0303}') => 'ã',
479 ('a', '\u{0308}') => 'ä',
480 ('a', '\u{030A}') => 'å',
481 ('c', '\u{0327}') => 'ç',
482 ('e', '\u{0300}') => 'è',
483 ('e', '\u{0301}') => 'é',
484 ('e', '\u{0302}') => 'ê',
485 ('e', '\u{0308}') => 'ë',
486 ('i', '\u{0300}') => 'ì',
487 ('i', '\u{0301}') => 'í',
488 ('i', '\u{0302}') => 'î',
489 ('i', '\u{0308}') => 'ï',
490 ('n', '\u{0303}') => 'ñ',
491 ('o', '\u{0300}') => 'ò',
492 ('o', '\u{0301}') => 'ó',
493 ('o', '\u{0302}') => 'ô',
494 ('o', '\u{0303}') => 'õ',
495 ('o', '\u{0308}') => 'ö',
496 ('u', '\u{0300}') => 'ù',
497 ('u', '\u{0301}') => 'ú',
498 ('u', '\u{0302}') => 'û',
499 ('u', '\u{0308}') => 'ü',
500 ('y', '\u{0301}') => 'ý',
501 ('y', '\u{0308}') => 'ÿ',
502 _ => return None,
503 };
504 Some(composed)
505}
506
507#[cfg(test)]
508mod tests {
509 use super::{
510 compose_common_nfc, normalize_alert_message, AlertDiagnostic, AlertEngine, AlertSeverity,
511 EXCLUDED_FINALIZATION_COMMANDS, MAX_ALERT_LINE_CHARS,
512 };
513 use std::path::Path;
514
515 fn error(file: &str, line: u32, message: &str) -> AlertDiagnostic {
516 AlertDiagnostic::error(file, line, message)
517 }
518
519 #[test]
520 fn normalizer_uses_only_the_contractual_transformations() {
521 assert_eq!(
522 normalize_alert_message(" Cafe\u{301} failed\nsecond line"),
523 "Café failed"
524 );
525 assert_eq!(
526 normalize_alert_message("src/a.rs:42 \"quoted\""),
527 "src/a.rs:42 \"quoted\""
528 );
529 }
530
531 fn normalize_alert_message_reference(message: &str) -> String {
532 let first_line = message.lines().next().unwrap_or_default().trim();
533 let collapsed = first_line.split_whitespace().collect::<Vec<_>>().join(" ");
534 compose_common_nfc(&collapsed)
535 }
536
537 #[test]
538 fn ascii_fast_path_preserves_normalized_bytes() {
539 let mut messages = vec![
540 String::new(),
541 " mismatched types\tnear `request` \nignored".to_string(),
542 "Cafe\u{301} failed".to_string(),
543 "déjà vu".to_string(),
544 "a\u{0327}\u{0301} unfamiliar decomposition".to_string(),
545 ];
546 for character in '\0'..='\u{7f}' {
547 messages.push(format!(
548 " prefix{character}{character}suffix\t detail \nignored"
549 ));
550 }
551
552 for message in messages {
553 assert_eq!(
554 normalize_alert_message(&message).as_bytes(),
555 normalize_alert_message_reference(&message).as_bytes(),
556 "normalization changed for {message:?}",
557 );
558 }
559 }
560
561 #[test]
562 fn first_observation_is_the_named_default_silent_baseline() {
563 let root = Path::new("/dispatch-root");
564 let mut engine = AlertEngine::default();
565 engine.observe_authoritative("session", root, "server-a", vec![error("a.rs", 3, "old")]);
566
567 assert!(engine.partition_is_baselined("session", root, "server-a"));
568 assert!(engine.finalize("session", root, "read").is_none());
569 assert_eq!(engine.agent_visible_response_ordinal("session"), 1);
570
571 engine.observe_authoritative(
572 "session",
573 root,
574 "server-a",
575 vec![error("a.rs", 3, "old"), error("a.rs", 7, "new")],
576 );
577 let alert = engine.finalize("session", root, "read").expect("new alert");
578 assert!(alert.text.contains("a.rs:7: new"));
579 assert!(!alert.text.contains("your edit"));
580 assert_eq!(alert.agent_visible_response_ordinal, 2);
581 }
582
583 #[test]
584 fn cold_root_silence_still_advances_the_session_response_ordinal() {
585 let mut engine = AlertEngine::default();
586 assert!(engine
587 .finalize("session", Path::new("/cold-root"), "read")
588 .is_none());
589 assert_eq!(engine.agent_visible_response_ordinal("session"), 1);
590 }
591
592 #[test]
593 fn dispatch_root_isolation_never_borrows_or_consumes_another_root() {
594 let dispatch_root = Path::new("/dispatch-root");
595 let other_root = Path::new("/other-root");
596 let mut engine = AlertEngine::default();
597 for root in [dispatch_root, other_root] {
598 engine.observe_authoritative("session", root, "server", Vec::new());
599 engine.observe_authoritative(
600 "session",
601 root,
602 "server",
603 vec![error("src/lib.rs", 4, root.to_string_lossy().as_ref())],
604 );
605 }
606
607 let alert = engine
608 .finalize("session", dispatch_root, "inspect")
609 .expect("dispatch-root alert");
610 assert!(alert.text.contains("/dispatch-root"));
611 assert!(!alert.text.contains("/other-root"));
612 assert!(engine
613 .finalize("session", other_root, "read")
614 .expect("other root remains pending")
615 .text
616 .contains("/other-root"));
617 }
618
619 #[test]
620 fn coalescing_orders_ties_and_marks_counted_identities_rendered() {
621 let root = Path::new("/root");
622 let mut engine = AlertEngine::default();
623 engine.observe_authoritative("session", root, "server", Vec::new());
624 engine.observe_authoritative(
625 "session",
626 root,
627 "server",
628 vec![
629 error("z.rs", 3, "z"),
630 error("a.rs", 2, "a"),
631 error("m.rs", 1, "m"),
632 ],
633 );
634
635 let alert = engine
636 .finalize("session", root, "read")
637 .expect("coalesced alert");
638 assert_eq!(alert.text.lines().count(), 5);
639 assert!(alert.text.find("a.rs:2").unwrap() < alert.text.find("m.rs:1").unwrap());
640 assert!(alert.text.contains("(+1 more)"));
641 assert_eq!(alert.represented_identities().count(), 3);
642 assert_eq!(
643 engine
644 .partition_rendered_identities("session", root, "server")
645 .len(),
646 3
647 );
648 assert!(engine.finalize("session", root, "read").is_none());
649 }
650
651 #[test]
652 fn excluded_commands_preserve_pending_alerts_and_do_not_advance_visible_ordinal() {
653 let root = Path::new("/root");
654 let mut engine = AlertEngine::default();
655 engine.observe_authoritative("session", root, "server", Vec::new());
656 engine.observe_authoritative("session", root, "server", vec![error("a.rs", 1, "boom")]);
657
658 for command in EXCLUDED_FINALIZATION_COMMANDS {
659 assert!(engine.finalize("session", root, command).is_none());
660 }
661 assert_eq!(engine.agent_visible_response_ordinal("session"), 0);
662 assert!(engine.finalize("session", root, "inspect").is_some());
663 assert_eq!(engine.agent_visible_response_ordinal("session"), 1);
664 }
665
666 #[test]
667 fn warning_information_and_hints_never_enter_the_alert_channel() {
668 let root = Path::new("/root");
669 let mut engine = AlertEngine::default();
670 for severity in [
671 AlertSeverity::Warning,
672 AlertSeverity::Information,
673 AlertSeverity::Hint,
674 ] {
675 let mut diagnostic = error("a.rs", 1, "not an error");
676 diagnostic.severity = severity;
677 engine.observe_authoritative(
678 "session",
679 root,
680 severity.canonical_name(),
681 vec![diagnostic],
682 );
683 }
684 assert!(engine.finalize("session", root, "read").is_none());
685 }
686
687 #[test]
688 fn truncation_never_wraps_the_rendered_line() {
689 let root = Path::new("/root");
690 let mut engine = AlertEngine::default();
691 engine.observe_authoritative("session", root, "server", Vec::new());
692 engine.observe_authoritative(
693 "session",
694 root,
695 "server",
696 vec![error("a.rs", 1, &"x".repeat(MAX_ALERT_LINE_CHARS * 2))],
697 );
698 let alert = engine
699 .finalize("session", root, "read")
700 .expect("long alert");
701 let line = alert.text.lines().nth(1).expect("alert line");
702 assert!(line.chars().count() <= MAX_ALERT_LINE_CHARS);
703 assert!(!line.contains('\n'));
704 }
705
706 #[test]
707 #[ignore = "manual performance probe"]
708 fn ascii_alert_normalization_perf_probe() {
709 const PASSES: usize = 100_000;
710 const SAMPLE_COUNT: usize = 11;
711 const MESSAGES: [&str; 8] = [
712 "error[E0308]: mismatched types in crates/aft/src/commands/inspect.rs: expected `Result`, found `Option`",
713 "Cannot find name 'resolvedProjectRoot'. Did you mean 'resolveProjectRoot'?",
714 "borrow of moved value: `request` value borrowed here after move",
715 "the trait bound `PathBuf: Copy` is not satisfied required by this call",
716 "Module not found: Can't resolve '../../transport/runtime' in '/workspace/src'",
717 "Property 'agent_visible_response_ordinal' does not exist on type 'AlertState'",
718 "unused import: `std::collections::HashMap` `#[warn(unused_imports)]` on by default",
719 "lifetime may not live long enough returning this value requires that `'1` must outlive `'static'",
720 ];
721
722 for message in MESSAGES {
723 std::hint::black_box(normalize_alert_message(message));
724 }
725
726 let mut samples_ms = Vec::with_capacity(SAMPLE_COUNT);
727 let mut checksum = 0usize;
728 for _ in 0..SAMPLE_COUNT {
729 let started = std::time::Instant::now();
730 let mut sample_checksum = 0usize;
731 for _ in 0..PASSES {
732 for message in MESSAGES {
733 sample_checksum = sample_checksum.wrapping_add(
734 std::hint::black_box(normalize_alert_message(std::hint::black_box(
735 message,
736 )))
737 .len(),
738 );
739 }
740 }
741 samples_ms.push(started.elapsed().as_secs_f64() * 1_000.0);
742 checksum = checksum.wrapping_add(std::hint::black_box(sample_checksum));
743 }
744 samples_ms.sort_by(f64::total_cmp);
745
746 println!(
747 "ascii-alert-normalization: calls_per_sample={} samples_ms={samples_ms:?} min_ms={:.3} median_ms={:.3} max_ms={:.3} checksum={checksum}",
748 PASSES * MESSAGES.len(),
749 samples_ms[0],
750 samples_ms[SAMPLE_COUNT / 2],
751 samples_ms[SAMPLE_COUNT - 1],
752 );
753 }
754}