1use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
40use std::io::BufRead;
41use std::path::{Path, PathBuf};
42
43use serde::Serialize;
44
45use crate::mailbox::{Envelope, MailAddress, MailKind, Mailbox, ReplyVia, StoredEnvelope};
46use crate::HarnessHomes;
47
48pub const TYPED_ID_PREFIX: &str = "u-";
50
51pub const CHANNEL_ID_PREFIX: &str = "c-";
54
55#[derive(Debug, Clone, PartialEq, Eq)]
58pub struct ChannelLine {
59 pub header: String,
61 pub from: String,
63 pub sent_at_ms: u64,
65 pub delivered_at_ms: Option<u64>,
67 pub text: String,
69}
70
71pub fn channel_line_id(address: &MailAddress, header: &str) -> String {
73 let hash = blake3::hash(format!("{address}\n{header}").as_bytes()).to_hex();
74 format!("{CHANNEL_ID_PREFIX}{}", &hash[..24])
75}
76
77fn channel_lines(text: &str, fallback_ms: u64, delivered_at_ms: Option<u64>) -> Vec<ChannelLine> {
80 let starts: Vec<usize> = text
81 .match_indices("[channel: ")
82 .map(|(at, _)| at)
83 .filter(|at| *at == 0 || text[..*at].ends_with('\n'))
84 .collect();
85 starts
86 .iter()
87 .enumerate()
88 .filter_map(|(n, start)| {
89 let end = starts.get(n + 1).copied().unwrap_or(text.len());
90 let line = text[*start..end].trim_end();
91 let header = &line[..line.find(']')? + 1];
92 let field = |name: &str| {
93 header
94 .split(" · ")
95 .find_map(|part| part.strip_prefix(name))
96 .map(|value| value.trim_end_matches(']').trim().to_string())
97 };
98 Some(ChannelLine {
99 header: header.to_string(),
100 from: field("from: ").unwrap_or_else(|| "a channel".to_string()),
101 sent_at_ms: field("at: ")
102 .and_then(|at| supercode_interchange::sidecar::rfc3339_to_ms(&at))
103 .and_then(|at| u64::try_from(at).ok())
104 .unwrap_or(fallback_ms),
105 delivered_at_ms,
106 text: line.to_string(),
107 })
108 })
109 .collect()
110}
111
112pub const ANSWER_ID_PREFIX: &str = "a-";
114
115pub fn answer_id(address: &MailAddress, native_id: &str) -> String {
118 let hash = blake3::hash(format!("{address}\n{native_id}").as_bytes()).to_hex();
119 format!("{ANSWER_ID_PREFIX}{}", &hash[..24])
120}
121
122pub fn user_address(machine: &str) -> std::io::Result<MailAddress> {
125 MailAddress::new(machine.to_string(), "operator", "user")
126 .map_err(|error| std::io::Error::other(error.0))
127}
128
129pub fn typed_line_id(address: &MailAddress, sent_at_ms: u64, text: &str) -> String {
132 let hash = blake3::hash(format!("{address}\n{sent_at_ms}\n{text}").as_bytes()).to_hex();
133 format!("{TYPED_ID_PREFIX}{}", &hash[..24])
134}
135
136#[derive(Debug, Clone, PartialEq, Eq)]
138pub struct TypedLine {
139 pub sent_at_ms: u64,
141 pub delivered_at_ms: Option<u64>,
143 pub withdrawn: bool,
145 pub text: String,
147}
148
149#[derive(Debug, Clone, PartialEq, Eq)]
151pub struct Answer {
152 pub native_id: String,
154 pub at_ms: u64,
156 pub text: String,
158}
159
160#[derive(Debug, Clone, Default)]
162pub struct TranscriptMail {
163 pub typed: Vec<TypedLine>,
165 pub answers: Vec<Answer>,
167 pub channel: Vec<ChannelLine>,
169 pub delivered: HashMap<String, u64>,
171}
172
173#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
175#[serde(tag = "state", rename_all = "snake_case")]
176pub enum Delivery {
177 Delivered {
179 at_ms: u64,
181 },
182 Sent,
184 Withdrawn,
186 Unknown,
189 NotRead,
192}
193
194impl Delivery {
195 pub fn describe(&self) -> String {
197 match self {
198 Self::Delivered { at_ms } => {
199 supercode_interchange::sidecar::ms_to_rfc3339(*at_ms as i64)
200 }
201 Self::Sent => "not yet".to_string(),
202 Self::Withdrawn => "never (taken back)".to_string(),
203 Self::Unknown => "unknown".to_string(),
204 Self::NotRead => "not read here (supercode message show reads it)".to_string(),
205 }
206 }
207}
208
209pub fn transcript(homes: &HarnessHomes, address: &MailAddress) -> Option<PathBuf> {
212 if address.harness != "claude-code" {
213 return None;
214 }
215 let file = format!("{}.jsonl", address.session_id);
216 std::fs::read_dir(&homes.claude_code)
217 .ok()?
218 .flatten()
219 .map(|project| project.path().join(&file))
220 .find(|path| path.is_file())
221}
222
223fn at_ms(value: &serde_json::Value) -> u64 {
224 value
225 .as_str()
226 .and_then(supercode_interchange::sidecar::rfc3339_to_ms)
227 .and_then(|at| u64::try_from(at).ok())
228 .unwrap_or_default()
229}
230
231fn prompt_text(content: &serde_json::Value) -> Option<String> {
232 let text = match content {
233 serde_json::Value::String(text) => text.clone(),
234 serde_json::Value::Array(parts) => parts
235 .iter()
236 .filter(|part| part["type"] == "text")
237 .filter_map(|part| part["text"].as_str())
238 .collect::<Vec<_>>()
239 .join("\n"),
240 _ => return None,
241 };
242 (!text.trim().is_empty()).then_some(text)
243}
244
245fn rendered_mail_ids(line: &str) -> Vec<&str> {
249 let mut ids = Vec::new();
250 for mark in ["cross-session-message id=\\\"", "(message "] {
251 for (at, _) in line.match_indices(mark) {
252 let rest = &line[at + mark.len()..];
253 let end = rest
254 .find(|character: char| !(character.is_ascii_alphanumeric() || character == '-'))
255 .unwrap_or(rest.len());
256 let id = &rest[..end];
257 if id.starts_with("m-") {
258 ids.push(id);
259 }
260 }
261 }
262 ids
263}
264
265fn machine_made(text: &str) -> bool {
268 text.contains("<cross-session-message")
269 || text.starts_with("[Cross-session")
270 || text.starts_with("[channel: ")
271 || text.starts_with("<task-notification>")
272}
273
274pub fn read_claude(path: &Path) -> std::io::Result<TranscriptMail> {
276 let reader = std::io::BufReader::new(std::fs::File::open(path)?);
277 let mut mail = TranscriptMail::default();
278 let mut queued: VecDeque<(String, Option<usize>)> = VecDeque::new();
284 let mut dequeued = 0_usize;
287 let mut absorbed: VecDeque<(String, Option<usize>)> = VecDeque::new();
290 let take = |waiting: &mut VecDeque<(String, Option<usize>)>, text: &str| {
291 let at = waiting.iter().position(|(words, _)| words == text)?;
292 waiting.remove(at).map(|(_, index)| index)
293 };
294 let mut not_typed: HashSet<usize> = HashSet::new();
296 let mut taken_in: HashMap<String, VecDeque<u64>> = HashMap::new();
299 let mut early: Vec<(String, Vec<String>, Option<usize>, u64)> = Vec::new();
303 for line in reader.lines() {
304 let line = line?;
305 let prompt = line.contains("\"promptSource\"") || line.contains("\"type\":\"user\"");
306 let queue = line.contains("\"queue-operation\"");
307 let attachment = line.contains("\"queued_command\"");
308 let answer = line.contains("\"stop_reason\":\"end_turn\"")
309 || line.contains("\"stop_reason\":\"stop_sequence\"");
310 let rendered = line.contains("cross-session-message id=") || line.contains("(message m-");
311 if !(prompt || queue || attachment || rendered || answer) {
312 continue;
313 }
314 let Ok(record) = serde_json::from_str::<serde_json::Value>(&line) else {
315 continue;
316 };
317 if record["isSidechain"] == true {
318 continue;
319 }
320 let kind = record["type"].as_str().unwrap_or_default();
321 let mut at = at_ms(&record["timestamp"]);
322 if kind == "queue-operation" && record["operation"] == "remove" {
323 if let Some(text) = record["content"].as_str() {
324 if let Some(found) = early.iter().position(|(words, ..)| words == text) {
327 let (_, ids, index, _) = early.remove(found);
328 for id in ids {
329 mail.delivered.entry(id).or_insert(at);
330 }
331 if let Some(index) = index {
332 mail.typed[index].delivered_at_ms = Some(at);
333 }
334 continue;
335 }
336 taken_in.entry(text.to_string()).or_default().push_back(at);
337 }
338 }
339 let mut waits_for_remove = false;
340 if kind == "attachment" && record["attachment"]["type"] == "queued_command" {
341 match record["attachment"]["prompt"]
342 .as_str()
343 .and_then(|text| taken_in.get_mut(text))
344 .and_then(VecDeque::pop_front)
345 {
346 Some(taken) => at = taken,
347 None => waits_for_remove = true,
348 }
349 }
350 if waits_for_remove {
351 let attached = &record["attachment"];
352 let text = attached["prompt"].as_str().unwrap_or_default().to_string();
353 let human = attached["origin"]["kind"] == "human" || attached["humanTurn"] == true;
354 let ids = rendered_mail_ids(&line)
355 .into_iter()
356 .map(str::to_string)
357 .collect();
358 let index = match take(&mut queued, &text) {
360 Some(Some(index)) if !human => {
361 not_typed.insert(index);
362 None
363 }
364 Some(index) => index,
365 None if human && !text.trim().is_empty() => {
366 mail.typed.push(TypedLine {
367 sent_at_ms: at,
368 delivered_at_ms: None,
369 withdrawn: false,
370 text: text.clone(),
371 });
372 Some(mail.typed.len() - 1)
373 }
374 None => None,
375 };
376 early.push((text, ids, index, at));
377 continue;
378 }
379 if rendered && matches!(kind, "user" | "attachment") {
383 for id in rendered_mail_ids(&line) {
384 mail.delivered.entry(id.to_string()).or_insert(at);
385 }
386 }
387 match kind {
388 "assistant"
390 if answer
391 && matches!(
392 record["message"]["stop_reason"].as_str(),
393 Some("end_turn" | "stop_sequence")
394 ) =>
395 {
396 let (Some(native_id), Some(text)) = (
397 record["uuid"].as_str(),
398 prompt_text(&record["message"]["content"]),
399 ) else {
400 continue;
401 };
402 mail.answers.push(Answer {
403 native_id: native_id.to_string(),
404 at_ms: at,
405 text,
406 });
407 }
408 "queue-operation" if record["operation"] == "dequeue" => dequeued += 1,
410 "queue-operation" => {
411 let Some(text) = record["content"].as_str() else {
412 continue;
413 };
414 match record["operation"].as_str() {
415 Some("enqueue") => {
416 let index = (!machine_made(text) && !text.trim().is_empty()).then(|| {
417 mail.typed.push(TypedLine {
418 sent_at_ms: at,
419 delivered_at_ms: None,
420 withdrawn: false,
421 text: text.to_string(),
422 });
423 mail.typed.len() - 1
424 });
425 queued.push_back((text.to_string(), index));
426 }
427 Some("remove") => {
428 let Some(index) = take(&mut queued, text) else {
429 continue;
430 };
431 match record["reason"].as_str() {
432 Some("absorbed_mid_turn" | "delivered_to_agent") => {
434 if let Some(index) = index {
435 mail.typed[index].delivered_at_ms = Some(at);
436 }
437 absorbed.push_back((text.to_string(), index));
438 }
439 _ => {
441 if let Some(index) = index {
442 mail.typed[index].withdrawn = true;
443 }
444 }
445 }
446 }
447 _ => {}
448 }
449 }
450 "attachment" => {
453 let attached = &record["attachment"];
454 let Some(text) = attached["prompt"].as_str() else {
455 continue;
456 };
457 if attached["type"] != "queued_command" || text.trim().is_empty() {
458 continue;
459 }
460 let human = attached["origin"]["kind"] == "human" || attached["humanTurn"] == true;
461 match take(&mut absorbed, text) {
462 Some(Some(index)) if !human => {
463 not_typed.insert(index);
464 }
465 Some(_) => {}
466 None if human => mail.typed.push(TypedLine {
467 sent_at_ms: at_ms(&attached["timestamp"]).min(at),
468 delivered_at_ms: Some(at),
469 withdrawn: false,
470 text: text.to_string(),
471 }),
472 None => {}
473 }
474 }
475 "user" => {
476 let Some(text) = prompt_text(&record["message"]["content"]) else {
477 continue;
478 };
479 let source = record["promptSource"].as_str();
480 let human = matches!(source, Some("typed" | "queued")) && record["isMeta"] != true;
481 if !human {
482 mail.channel.extend(channel_lines(&text, at, Some(at)));
483 }
484 let mut landing: Vec<Option<usize>> = Vec::new();
491 let let_go = std::mem::take(&mut dequeued);
492 if let_go == 0 {
493 landing.extend(take(&mut queued, &text));
494 }
495 let wanted = let_go;
496 while landing.len() < wanted {
497 let Some(at) = queued
498 .iter()
499 .position(|(words, _)| text.contains(words.as_str()))
500 else {
501 break;
502 };
503 landing.extend(queued.remove(at).map(|(_, index)| index));
504 }
505 if landing.is_empty() && wanted > 0 {
506 for _ in 0..wanted {
507 landing.extend(queued.pop_front().map(|(_, index)| index));
508 }
509 }
510 if !landing.is_empty() {
511 for index in landing.into_iter().flatten() {
512 if human {
513 mail.typed[index].delivered_at_ms = Some(at);
514 } else {
515 not_typed.insert(index);
516 }
517 }
518 continue;
519 }
520 if human {
521 mail.typed.push(TypedLine {
522 sent_at_ms: at,
523 delivered_at_ms: Some(at),
524 withdrawn: false,
525 text,
526 });
527 }
528 }
529 _ => {}
530 }
531 }
532 for (_, ids, index, at) in early {
535 for id in ids {
536 mail.delivered.entry(id).or_insert(at);
537 }
538 if let Some(index) = index {
539 mail.typed[index].delivered_at_ms.get_or_insert(at);
540 }
541 }
542 for (text, _) in &queued {
544 if text.starts_with("[channel: ") {
545 mail.channel.extend(channel_lines(text, 0, None));
546 }
547 }
548 let mut index = 0;
549 mail.typed.retain(|_| {
550 index += 1;
551 !not_typed.contains(&(index - 1))
552 });
553 Ok(mail)
554}
555
556pub fn read(homes: &HarnessHomes, address: &MailAddress) -> Option<TranscriptMail> {
558 read_claude(&transcript(homes, address)?).ok()
559}
560
561fn newest_before(items: &[(u64, String)], at: u64, inclusive: bool) -> Option<String> {
564 items
565 .iter()
566 .rev()
567 .find(|(when, _)| if inclusive { *when <= at } else { *when < at })
568 .map(|(_, id)| id.clone())
569}
570
571fn answer_times(address: &MailAddress, mail: &TranscriptMail) -> Vec<(u64, String)> {
573 let mut times: Vec<(u64, String)> = mail
574 .answers
575 .iter()
576 .map(|answer| (answer.at_ms, answer_id(address, &answer.native_id)))
577 .collect();
578 times.sort();
579 times
580}
581
582fn file_derived(
585 mailbox: &Mailbox,
586 filed: &HashMap<&str, &StoredEnvelope>,
587 envelope: Envelope,
588) -> std::io::Result<bool> {
589 if let Some(stored) = filed.get(envelope.id.as_str()) {
590 if stored.envelope.in_reply_to == envelope.in_reply_to {
591 return Ok(false);
592 }
593 std::fs::remove_file(&stored.path).ok();
594 }
595 mailbox.file_read(&envelope)?;
596 Ok(true)
597}
598
599pub fn file_typed_lines(mailbox: &Mailbox, mail: &TranscriptMail) -> std::io::Result<usize> {
602 let address = mailbox.address();
603 let filed = mailbox.list()?;
604 let by_id: HashMap<&str, &StoredEnvelope> = filed
605 .iter()
606 .map(|stored| (stored.envelope.id.as_str(), stored))
607 .collect();
608 let answers = answer_times(address, mail);
611 let mut typed_by_supercode: HashMap<&str, usize> = HashMap::new();
614 for stored in &filed {
615 if stored.envelope.kind == MailKind::User
616 && !stored.envelope.id.starts_with(TYPED_ID_PREFIX)
617 {
618 *typed_by_supercode
619 .entry(stored.envelope.body.trim())
620 .or_default() += 1;
621 }
622 }
623 let user = user_address(&address.machine)?;
624 let current: HashSet<String> = mail
628 .typed
629 .iter()
630 .map(|line| typed_line_id(address, line.sent_at_ms, &line.text))
631 .collect();
632 for stored in &filed {
633 if stored.envelope.id.starts_with(TYPED_ID_PREFIX) && !current.contains(&stored.envelope.id)
634 {
635 std::fs::remove_file(&stored.path).ok();
636 }
637 }
638 let mut count = 0;
639 for line in &mail.typed {
640 let id = typed_line_id(address, line.sent_at_ms, &line.text);
641 if !by_id.contains_key(id.as_str()) {
642 if let Some(left) = typed_by_supercode.get_mut(line.text.trim()) {
643 if *left > 0 {
644 *left -= 1;
645 continue;
646 }
647 }
648 }
649 let in_reply_to = newest_before(&answers, line.sent_at_ms, false);
650 count += usize::from(file_derived(
651 mailbox,
652 &by_id,
653 Envelope {
654 id,
655 created_at_ms: line.sent_at_ms,
656 from: user.clone(),
657 from_name: format!("user@{}", address.machine),
658 kind: MailKind::User,
659 reply_via: ReplyVia::None,
660 in_reply_to_inferred: in_reply_to.is_some(),
661 in_reply_to,
662 thread: None,
663 native_from: None,
664 body: line.text.clone(),
665 },
666 )?);
667 }
668 for line in &mail.channel {
669 let id = channel_line_id(address, &line.header);
670 let in_reply_to = newest_before(&answers, line.sent_at_ms, false);
671 count += usize::from(file_derived(
672 mailbox,
673 &by_id,
674 Envelope {
675 id,
676 created_at_ms: line.sent_at_ms,
677 from: MailAddress::new(address.machine.clone(), "operator", "channel")
678 .map_err(|error| std::io::Error::other(error.0))?,
679 from_name: line.from.clone(),
680 kind: MailKind::Channel,
681 reply_via: ReplyVia::None,
682 in_reply_to_inferred: in_reply_to.is_some(),
683 in_reply_to,
684 thread: None,
685 native_from: None,
686 body: line.text.clone(),
687 },
688 )?);
689 }
690 Ok(count)
691}
692
693pub fn file_answers(
697 root: &Path,
698 address: &MailAddress,
699 name: &str,
700 mail: &TranscriptMail,
701) -> std::io::Result<usize> {
702 let mailbox = Mailbox::open(root, &user_address(&address.machine)?)?;
703 let listed = mailbox.list()?;
704 let mut filed: HashMap<&str, &StoredEnvelope> = HashMap::new();
705 for stored in &listed {
706 if stored.envelope.id.starts_with(ANSWER_ID_PREFIX)
708 && stored.envelope.kind != MailKind::Answer
709 {
710 std::fs::remove_file(&stored.path).ok();
711 continue;
712 }
713 filed.insert(stored.envelope.id.as_str(), stored);
714 }
715 let mut people: Vec<(u64, String)> = mail
718 .typed
719 .iter()
720 .map(|line| {
721 (
722 line.sent_at_ms,
723 typed_line_id(address, line.sent_at_ms, &line.text),
724 )
725 })
726 .chain(
727 mail.channel
728 .iter()
729 .map(|line| (line.sent_at_ms, channel_line_id(address, &line.header))),
730 )
731 .chain(
732 Mailbox::open(root, address)?
733 .list()?
734 .into_iter()
735 .filter(|stored| {
736 stored.envelope.kind == MailKind::User
737 && !stored.envelope.id.starts_with(TYPED_ID_PREFIX)
738 })
739 .map(|stored| (stored.envelope.created_at_ms, stored.envelope.id)),
740 )
741 .collect();
742 people.sort();
743 let mut count = 0;
744 for answer in &mail.answers {
745 let id = answer_id(address, &answer.native_id);
746 let in_reply_to = newest_before(&people, answer.at_ms, true);
747 count += usize::from(file_derived(
748 &mailbox,
749 &filed,
750 Envelope {
751 id,
752 created_at_ms: answer.at_ms,
753 from: address.clone(),
754 from_name: name.to_string(),
755 kind: MailKind::Answer,
756 reply_via: ReplyVia::None,
757 in_reply_to_inferred: in_reply_to.is_some(),
758 in_reply_to,
759 thread: None,
760 native_from: None,
761 body: answer.text.clone(),
762 },
763 )?);
764 }
765 Ok(count)
766}
767
768pub fn sent_by(root: &Path, address: &MailAddress) -> Vec<(MailAddress, StoredEnvelope)> {
772 let mut sent: Vec<(MailAddress, StoredEnvelope)> = crate::mailbox::all_mailboxes(root)
773 .into_iter()
774 .filter(|mailbox| mailbox.address() != address)
775 .flat_map(|mailbox| {
776 let to = mailbox.address().clone();
777 mailbox
778 .list()
779 .unwrap_or_default()
780 .into_iter()
781 .filter(|stored| &stored.envelope.from == address)
782 .map(move |stored| (to.clone(), stored))
783 })
784 .collect();
785 sent.sort_by_key(|(_, stored)| stored.envelope.created_at_ms);
786 sent
787}
788
789pub fn deliveries(
792 address: &MailAddress,
793 stored: &[StoredEnvelope],
794 mail: Option<&TranscriptMail>,
795) -> BTreeMap<String, Delivery> {
796 let Some(mail) = mail else {
797 return stored
798 .iter()
799 .map(|stored| (stored.envelope.id.clone(), Delivery::Unknown))
800 .collect();
801 };
802 let typed: HashMap<String, &TypedLine> = mail
803 .typed
804 .iter()
805 .map(|line| (typed_line_id(address, line.sent_at_ms, &line.text), line))
806 .collect();
807 let channel: HashMap<String, &ChannelLine> = mail
808 .channel
809 .iter()
810 .map(|line| (channel_line_id(address, &line.header), line))
811 .collect();
812 let mut used: HashSet<usize> = HashSet::new();
815 let mut result = BTreeMap::new();
816 for stored in stored {
817 let envelope = &stored.envelope;
818 if typed.get(&envelope.id).is_some_and(|line| line.withdrawn) {
819 result.insert(envelope.id.clone(), Delivery::Withdrawn);
820 continue;
821 }
822 let at = if let Some(line) = typed.get(&envelope.id) {
823 line.delivered_at_ms
824 } else if let Some(line) = channel.get(&envelope.id) {
825 line.delivered_at_ms
826 } else if let Some(at) = mail
827 .delivered
828 .get(&envelope.id)
829 .or_else(|| mail.delivered.get(crate::mailbox::short_id(&envelope.id)))
830 {
831 Some(*at)
832 } else if envelope.kind == MailKind::User {
833 mail.typed
834 .iter()
835 .enumerate()
836 .find(|(index, line)| {
837 !used.contains(index)
838 && line.sent_at_ms + 1_000 >= envelope.created_at_ms
839 && line.text.trim() == envelope.body.trim()
840 })
841 .and_then(|(index, line)| {
842 used.insert(index);
843 line.delivered_at_ms
844 })
845 } else {
846 None
847 };
848 result.insert(
849 envelope.id.clone(),
850 match at {
851 Some(at_ms) => Delivery::Delivered { at_ms },
852 None => Delivery::Sent,
853 },
854 );
855 }
856 result
857}
858
859pub const MIN_PREFIX_DIGITS: usize = 4;
861
862pub fn find_messages(root: &Path, id: &str) -> std::io::Result<Vec<(Mailbox, StoredEnvelope)>> {
867 let digits = id.split_once('-').map_or(0, |(_, digits)| digits.len());
868 if digits < MIN_PREFIX_DIGITS {
869 return Ok(Vec::new());
870 }
871 let mut found: Vec<(Mailbox, StoredEnvelope)> = Vec::new();
872 for mailbox in crate::mailbox::all_mailboxes(root) {
873 for stored in mailbox.find_prefix(id)? {
874 if !found
875 .iter()
876 .any(|(_, known)| known.envelope.id == stored.envelope.id)
877 {
878 found.push((mailbox.clone(), stored));
879 }
880 }
881 }
882 Ok(found)
883}