1use crate::model::{Harness, HostStats, ProcKind, ProcNode};
10use std::collections::HashMap;
11use std::path::PathBuf;
12use std::time::{SystemTime, UNIX_EPOCH};
13use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, System, UpdateKind};
14
15#[derive(Debug, Clone)]
16pub struct RawProc {
17 pub pid: u32,
18 pub ppid: Option<u32>,
19 pub name: String,
20 pub exe: Option<PathBuf>,
21 pub cmd: Vec<String>,
22 pub cwd: Option<PathBuf>,
23 pub cpu_percent: f32,
24 pub rss_bytes: u64,
25 pub start_time: u64,
27 pub run_time: u64,
28}
29
30impl RawProc {
31 pub fn cmdline(&self) -> String {
32 if self.cmd.is_empty() { self.name.clone() } else { self.cmd.join(" ") }
33 }
34
35 fn program(&self) -> String {
37 let from_cmd = self.cmd.first().map(|c| basename(c));
38 let from_exe = self.exe.as_ref().and_then(|e| e.file_name()).map(|f| f.to_string_lossy().into_owned());
39 from_cmd.or(from_exe).unwrap_or_else(|| self.name.clone())
40 }
41
42 fn is_codex_launcher(&self) -> bool {
45 matches!(self.program().to_ascii_lowercase().as_str(), "node" | "node.exe" | "nodejs" | "bun" | "bun.exe")
46 && self.cmd.get(1).is_some_and(|s| s.replace('\\', "/").ends_with("/@openai/codex/bin/codex.js"))
47 }
48
49 fn codex_args(&self) -> Option<&[String]> {
50 if matches!(self.program().to_ascii_lowercase().as_str(), "codex" | "codex.exe") {
51 Some(self.cmd.get(1..).unwrap_or_default())
52 } else if self.is_codex_launcher() {
53 self.cmd.get(2..)
54 } else {
55 None
56 }
57 }
58
59 pub(crate) fn is_codex_runtime_of(&self, parent: &RawProc) -> bool {
62 self.ppid == Some(parent.pid)
63 && parent.is_codex_launcher()
64 && !self.is_codex_launcher()
65 && classify_agent(self) == Some(Harness::Codex)
66 && self.codex_args() == parent.codex_args()
67 }
68}
69
70fn basename(s: &str) -> String {
71 s.rsplit('/').next().unwrap_or(s).to_string()
72}
73
74pub struct ProcessScanner {
75 sys: System,
76 self_pid: Option<u32>,
77}
78
79impl Default for ProcessScanner {
80 fn default() -> Self {
81 Self::new()
82 }
83}
84
85impl ProcessScanner {
86 pub fn new() -> Self {
87 let mut sys = System::new();
88 sys.refresh_memory();
89 sys.refresh_cpu_usage();
90 sys.refresh_processes_specifics(ProcessesToUpdate::All, true, Self::refresh_kind());
91 let self_pid = sysinfo::get_current_pid().ok().map(|p| p.as_u32());
92 ProcessScanner { sys, self_pid }
93 }
94
95 pub fn refresh(&mut self) {
96 self.sys.refresh_memory();
97 self.sys.refresh_cpu_usage();
98 self.sys.refresh_processes_specifics(ProcessesToUpdate::All, true, Self::refresh_kind());
99 }
100
101 fn refresh_kind() -> ProcessRefreshKind {
112 ProcessRefreshKind::nothing()
113 .without_tasks()
114 .with_memory()
115 .with_cpu()
116 .with_exe(UpdateKind::OnlyIfNotSet)
117 .with_cmd(UpdateKind::OnlyIfNotSet)
118 .with_cwd(UpdateKind::OnlyIfNotSet)
119 }
120
121 pub fn host(&self) -> HostStats {
122 HostStats {
123 hostname: System::host_name(),
124 cpu_percent: self.sys.global_cpu_usage(),
125 cpu_count: self.sys.cpus().len(),
126 mem_used_bytes: self.sys.used_memory(),
127 mem_total_bytes: self.sys.total_memory(),
128 }
129 }
130
131 pub fn processes(&self) -> Vec<RawProc> {
132 self.sys
133 .processes()
134 .iter()
135 .filter(|(pid, _)| Some(pid.as_u32()) != self.self_pid)
136 .map(|(pid, p)| RawProc {
137 pid: pid.as_u32(),
138 ppid: p.parent().map(|x| x.as_u32()),
139 name: p.name().to_string_lossy().into_owned(),
140 exe: p.exe().map(|e| e.to_path_buf()),
141 cmd: p.cmd().iter().map(|c| c.to_string_lossy().into_owned()).collect(),
142 cwd: p.cwd().map(|c| c.to_path_buf()),
143 cpu_percent: p.cpu_usage(),
144 rss_bytes: p.memory(),
145 start_time: p.start_time(),
146 run_time: p.run_time(),
147 })
148 .collect()
149 }
150}
151
152pub fn classify_agent(p: &RawProc) -> Option<Harness> {
154 let prog = p.program();
155 let prog = prog.strip_suffix(".exe").unwrap_or(&prog).to_ascii_lowercase();
156 let joined = p.cmd.join(" ");
157
158 let script = p.cmd.get(1).map(|s| s.to_ascii_lowercase()).unwrap_or_default();
160
161 if prog == "claude" || script.contains("@anthropic-ai/claude-code") || script.ends_with("/claude") {
162 return Some(Harness::Claude);
163 }
164 if let Some(args) = p.codex_args() {
165 return (!codex_helper(args)).then_some(Harness::Codex);
166 }
167 if prog == "gemini" || script.contains("@google/gemini-cli") {
168 return Some(Harness::Gemini);
169 }
170 if prog == "opencode" {
171 return Some(Harness::OpenCode);
172 }
173 if prog == "kodelet" {
174 let args = kodelet_command(p.cmd.get(1..).unwrap_or_default());
175 return (args.first().map(String::as_str) == Some("serve")
176 || (args.first().map(String::as_str) == Some("runner") && args.get(1).map(String::as_str) == Some("start")))
177 .then_some(Harness::Kodelet);
178 }
179 if prog == "aider" || joined.contains("aider/main.py") {
180 return Some(Harness::Aider);
181 }
182 if prog == "copilot" || script.contains("@github/copilot") {
183 return Some(Harness::Copilot);
184 }
185 if prog == "cursor-agent" {
186 return Some(Harness::Cursor);
187 }
188 None
189}
190
191pub fn classify_child(p: &RawProc) -> ProcKind {
193 let prog = p.program().to_ascii_lowercase();
194 let joined = p.cmdline().to_ascii_lowercase();
195 if p.codex_args().is_some_and(codex_helper) {
197 return ProcKind::Tool;
198 }
199 if prog == "kodelet" || prog.starts_with("kodelet-extension-") || prog == "kodelet-subagent" {
202 return ProcKind::Tool;
203 }
204 if matches!(prog.as_str(), "zsh" | "bash" | "sh" | "fish" | "dash" | "pwsh" | "cmd") {
205 return ProcKind::Shell;
206 }
207 if looks_like_mcp(&prog, &joined) {
208 return ProcKind::Mcp;
209 }
210 ProcKind::Tool
211}
212
213fn codex_helper(args: &[String]) -> bool {
218 matches!(
219 args.first().map(String::as_str),
220 Some(
221 "--codex-run-as-apply-patch"
222 | "--codex-run-as-arg0-exec-helper"
223 | "--codex-run-as-fs-helper"
224 | "--run-as-windows-sandbox"
225 | "--__codex-windows-mxc"
226 | "mcp"
227 | "sandbox"
228 | "exec-server"
229 | "stdio-to-uds"
230 | "responses-api-proxy"
231 )
232 ) || (args.first().map(String::as_str) == Some("app-server") && args.get(1).map(String::as_str) == Some("daemon"))
233}
234
235pub(crate) fn kodelet_command(mut args: &[String]) -> &[String] {
239 while let Some(arg) = args.first() {
240 let flag = arg.split('=').next().unwrap_or(arg);
241 let takes_value = match flag {
242 "--provider"
243 | "--model"
244 | "--max-tokens"
245 | "--thinking-budget-tokens"
246 | "--weak-model"
247 | "--weak-model-max-tokens"
248 | "--reasoning-effort"
249 | "--log-level"
250 | "--log-format"
251 | "--allowed-commands"
252 | "--allowed-domains-file"
253 | "--sysprompt"
254 | "--sysprompt-arg"
255 | "--allowed-tools"
256 | "--tool-mode"
257 | "--anthropic-api-access"
258 | "--profile"
259 | "--context-patterns"
260 | "--compact-ratio" => true,
261 "--enable-openai-search" | "--no-skills" | "--enable-fs-search-tools" => false,
262 _ => break,
263 };
264 let count = if takes_value && !arg.contains('=') { 2 } else { 1 };
265 args = args.get(count..).unwrap_or_default();
266 }
267 args
268}
269
270pub fn looks_like_mcp(prog: &str, joined: &str) -> bool {
274 prog.contains("mcp")
275 || joined.contains("modelcontextprotocol")
276 || joined.contains("mcp-server")
277 || joined.contains("mcp_server")
278 || joined.contains("-mcp ")
279 || joined.ends_with("-mcp")
280 || joined.contains("mcp-")
281 || joined.contains("/mcp/")
282 || joined.contains(" mcp ")
283}
284
285fn now_secs() -> u64 {
286 SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0)
287}
288
289pub fn build_forest(procs: &[RawProc]) -> (Vec<ProcNode>, Vec<ProcNode>) {
295 let by_pid: HashMap<u32, &RawProc> = procs.iter().map(|p| (p.pid, p)).collect();
296 let mut children: HashMap<u32, Vec<u32>> = HashMap::new();
297 for p in procs {
298 if let Some(pp) = p.ppid {
299 children.entry(pp).or_default().push(p.pid);
300 }
301 }
302 let harness_of: HashMap<u32, Harness> = procs.iter().filter_map(|p| classify_agent(p).map(|h| (p.pid, h))).collect();
303
304 let has_agent_ancestor = |mut pid: u32| -> bool {
305 let mut hops = 0;
306 while let Some(p) = by_pid.get(&pid) {
307 match p.ppid {
308 Some(pp) if pp != pid && hops < 64 => {
309 if harness_of.contains_key(&pp) {
310 return true;
311 }
312 pid = pp;
313 hops += 1;
314 }
315 _ => return false,
316 }
317 }
318 false
319 };
320
321 let now = now_secs();
322 fn build(
323 pid: u32,
324 kind: ProcKind,
325 by_pid: &HashMap<u32, &RawProc>,
326 children: &HashMap<u32, Vec<u32>>,
327 harness_of: &HashMap<u32, Harness>,
328 now: u64,
329 depth: usize,
330 ) -> ProcNode {
331 let p = by_pid[&pid];
332 let mut kids = Vec::new();
333 if depth < 32
334 && let Some(cs) = children.get(&pid)
335 {
336 let mut cs = cs.clone();
337 cs.sort_unstable();
338 for c in cs {
339 if c == pid {
340 continue;
341 }
342 let k = if harness_of.contains_key(&c) { ProcKind::Agent } else { classify_child(by_pid[&c]) };
343 kids.push(build(c, k, by_pid, children, harness_of, now, depth + 1));
344 }
345 }
346 ProcNode {
347 pid,
348 ppid: p.ppid,
349 name: p.program(),
350 cmdline: p.cmdline(),
351 kind,
352 harness: harness_of.get(&pid).copied(),
353 cpu_percent: p.cpu_percent,
354 rss_bytes: p.rss_bytes,
355 age_secs: if p.run_time > 0 { p.run_time } else { now.saturating_sub(p.start_time) },
356 cwd: p.cwd.clone(),
357 children: kids,
358 }
359 }
360
361 let mut roots: Vec<ProcNode> = harness_of
362 .keys()
363 .filter(|pid| !has_agent_ancestor(**pid))
364 .map(|pid| build(*pid, ProcKind::Agent, &by_pid, &children, &harness_of, now, 0))
365 .collect();
366 roots.sort_by_key(|r| r.pid);
367
368 let mut orphans: Vec<ProcNode> = procs
370 .iter()
371 .filter(|p| !harness_of.contains_key(&p.pid))
372 .filter(|p| classify_child(p) == ProcKind::Mcp)
373 .filter(|p| !has_agent_ancestor(p.pid))
374 .map(|p| ProcNode {
375 pid: p.pid,
376 ppid: p.ppid,
377 name: p.program(),
378 cmdline: p.cmdline(),
379 kind: ProcKind::Mcp,
380 harness: None,
381 cpu_percent: p.cpu_percent,
382 rss_bytes: p.rss_bytes,
383 age_secs: if p.run_time > 0 { p.run_time } else { now.saturating_sub(p.start_time) },
384 cwd: p.cwd.clone(),
385 children: Vec::new(),
386 })
387 .collect();
388 let orphan_pids: std::collections::HashSet<u32> = orphans.iter().map(|o| o.pid).collect();
390 orphans.retain(|o| {
391 let mut pid = o.pid;
392 let mut hops = 0;
393 while let Some(p) = by_pid.get(&pid) {
394 match p.ppid {
395 Some(pp) if pp != pid && hops < 64 => {
396 if orphan_pids.contains(&pp) {
397 return false;
398 }
399 pid = pp;
400 hops += 1;
401 }
402 _ => break,
403 }
404 }
405 true
406 });
407 orphans.sort_by_key(|o| std::cmp::Reverse(o.age_secs));
408 (roots, orphans)
409}
410
411pub fn session_id_from_args(cmd: &[String]) -> Option<String> {
413 let mut it = cmd.iter();
414 while let Some(a) = it.next() {
415 if a == "--resume" || a == "-r" || a == "resume" {
416 if let Some(v) = it.next()
417 && looks_like_uuid(v)
418 {
419 return Some(v.clone());
420 }
421 } else if let Some(v) = a.strip_prefix("--resume=")
422 && looks_like_uuid(v)
423 {
424 return Some(v.to_string());
425 }
426 }
427 None
428}
429
430fn looks_like_uuid(s: &str) -> bool {
431 s.len() == 36 && s.chars().all(|c| c.is_ascii_hexdigit() || c == '-')
432}
433
434#[cfg(test)]
435mod tests {
436 use super::*;
437
438 fn proc(pid: u32, ppid: Option<u32>, cmd: &[&str]) -> RawProc {
439 RawProc {
440 pid,
441 ppid,
442 name: basename(cmd[0]),
443 exe: None,
444 cmd: cmd.iter().map(|s| s.to_string()).collect(),
445 cwd: None,
446 cpu_percent: 0.0,
447 rss_bytes: 0,
448 start_time: 0,
449 run_time: 1,
450 }
451 }
452
453 #[test]
454 fn kodelet_execution_hosts_are_agents_but_clients_and_helpers_are_not() {
455 for cmd in [
456 vec!["kodelet", "serve", "--managed"],
457 vec!["/usr/local/bin/kodelet", "serve", "--embedded-runner=false"],
458 vec!["kodelet", "runner", "start"],
459 vec!["kodelet", "--log-level", "debug", "--profile=coding", "serve"],
460 vec!["kodelet", "--no-skills", "runner", "start"],
461 ] {
462 assert_eq!(classify_agent(&proc(1, None, &cmd)), Some(Harness::Kodelet), "{cmd:?}");
463 }
464 for cmd in [
465 vec!["kodelet"],
466 vec!["kodelet", "run", "serve mcp"],
467 vec!["kodelet", "chat", "--resume", "20260919T090000-abcdef"],
468 vec!["kodelet", "acp"],
469 vec!["kodelet", "runner", "list"],
470 vec!["kodelet", "server", "logs"],
471 vec!["kodelet", "conversation", "show", "abc"],
472 vec!["kodelet", "--model", "serve"],
473 vec!["kodelet", "--", "serve"],
474 vec!["kodelet-extension-code-search"],
475 vec!["kodelet-extension-mcp"],
476 vec!["kodelet-subagent"],
477 vec!["cat", "/usr/local/bin/kodelet"],
478 ] {
479 let p = proc(1, None, &cmd);
480 assert_eq!(classify_agent(&p), None, "{cmd:?}");
481 assert_eq!(classify_child(&p), ProcKind::Tool, "{cmd:?}");
482 }
483 }
484
485 #[test]
486 fn refreshes_processes_without_tasks() {
487 let kind = ProcessScanner::refresh_kind();
488 assert!(!kind.tasks(), "Linux threads share their process's RSS and must not become tree nodes");
489 assert!(kind.memory());
490 assert!(kind.cpu());
491 }
492
493 #[cfg(target_os = "linux")]
494 #[test]
495 fn scanner_excludes_live_worker_threads() {
496 use std::sync::mpsc;
497
498 std::thread::scope(|scope| {
499 let (ready, tid) = mpsc::channel();
500 let (_release, wait) = mpsc::channel::<()>();
501 scope.spawn(move || {
502 let path = std::fs::read_link("/proc/thread-self").unwrap();
503 let tid: u32 = path.file_name().unwrap().to_str().unwrap().parse().unwrap();
504 ready.send(tid).unwrap();
505 let _ = wait.recv();
508 });
509 let tid = tid.recv().unwrap();
510 let pid = std::process::id();
511 assert_ne!(tid, pid);
512
513 let mut scanner = ProcessScanner::new();
514 scanner.self_pid = None;
517 for _ in 0..2 {
518 let procs = scanner.processes();
519 assert!(procs.iter().any(|p| p.pid == pid), "the process itself must remain visible");
520 assert!(!procs.iter().any(|p| p.pid == tid), "a worker thread is not a child process");
521 scanner.refresh();
522 }
523 });
524 }
525
526 #[test]
527 fn nested_harnesses_are_agents_with_each_process_counted_once() {
528 let mut procs = vec![
529 proc(10, None, &["node", "/usr/lib/node_modules/@openai/codex/bin/codex.js", "--yolo"]),
530 proc(11, Some(10), &["/opt/codex/bin/codex", "--yolo"]),
531 proc(12, Some(11), &["bash", "-c", "codex --yolo"]),
532 proc(13, Some(12), &["codex", "--yolo"]),
533 proc(14, Some(10), &["codex", "exec", "review"]),
534 proc(15, Some(11), &["codex", "--yolo"]),
535 ];
536 for p in &mut procs {
537 p.rss_bytes = 100;
538 p.cpu_percent = 1.0;
539 }
540 let (roots, orphans) = build_forest(&procs);
541 assert!(orphans.is_empty());
542 assert_eq!(roots.len(), 1);
543 let root = &roots[0];
544 assert_eq!(root.pid, 10);
545 let runtime = &root.children[0];
546 assert_eq!(runtime.pid, 11);
547 assert_eq!(runtime.kind, ProcKind::Agent);
548 assert_eq!(runtime.children[0].kind, ProcKind::Shell);
549 assert_eq!(runtime.children[0].children[0].kind, ProcKind::Agent, "tool-launched harnesses are still agents");
550 assert_eq!(runtime.children[1].kind, ProcKind::Agent);
551 assert_eq!(root.children[1].kind, ProcKind::Agent);
552 assert_eq!(root.totals(), (6.0, 600, 6, 0), "each real PID contributes resources exactly once");
553 }
554
555 #[test]
556 fn codex_runtime_matches_only_its_launcher_and_forwarded_arguments() {
557 let launcher = proc(10, None, &["node", "/usr/lib/node_modules/@openai/codex/bin/codex.js", "exec", "review the diff"]);
558 let runtime = proc(11, Some(10), &["/opt/codex/bin/codex", "exec", "review the diff"]);
559 assert!(runtime.is_codex_runtime_of(&launcher));
560
561 for child in [
562 proc(12, Some(10), &["codex", "exec", "different task"]),
563 proc(12, Some(10), &["codex", "exec", "review", "the", "diff"]),
564 proc(12, Some(99), &["codex", "exec", "review the diff"]),
565 proc(12, Some(10), &["node", "/usr/lib/node_modules/@openai/codex/bin/codex.js", "exec", "review the diff"]),
566 ] {
567 assert!(!child.is_codex_runtime_of(&launcher), "not the forwarded runtime: {child:?}");
568 }
569 let nested = proc(12, Some(11), &["codex", "exec", "review the diff"]);
570 assert!(!nested.is_codex_runtime_of(&runtime), "a native agent is not a launcher");
571
572 let launcher = proc(10, None, &["node", "/usr/lib/node_modules/@openai/codex/bin/codex.js", "mcp", "list"]);
573 let helper = proc(11, Some(10), &["codex", "mcp", "list"]);
574 assert!(!helper.is_codex_runtime_of(&launcher), "a management helper does not own agent rollouts");
575 }
576
577 #[test]
578 fn legacy_process_subagent_kind_deserializes_as_agent() {
579 let kind: ProcKind = serde_json::from_str("\"subagent\"").unwrap();
580 assert_eq!(kind, ProcKind::Agent);
581 assert_eq!(kind.label(), "agent");
582 assert_eq!(serde_json::to_value(kind).unwrap(), "agent");
583 }
584
585 #[test]
586 fn codex_helpers_are_tools_not_agents_or_mcp_servers() {
587 let helpers: &[&[&str]] = &[
588 &["codex", "--codex-run-as-apply-patch", "patch"],
589 &["codex", "--codex-run-as-arg0-exec-helper"],
590 &["codex", "--codex-run-as-fs-helper"],
591 &["codex.exe", "--run-as-windows-sandbox"],
592 &["codex.exe", "--__codex-windows-mxc"],
593 &["codex", "mcp", "list"],
594 &["codex", "sandbox", "--", "sh"],
595 &["codex", "exec-server"],
596 &["codex", "stdio-to-uds", "/tmp/socket"],
597 &["codex", "responses-api-proxy"],
598 &["codex", "app-server", "daemon", "start"],
599 &["node", "/usr/lib/node_modules/@openai/codex/bin/codex.js", "mcp", "list"],
600 &["codex-linux-sandbox", "--", "sh"],
601 &["apply_patch", "patch"],
602 &["applypatch", "patch"],
603 ];
604 let mut procs = vec![proc(1, None, &["codex", "--yolo"])];
605 for (i, cmd) in helpers.iter().enumerate() {
606 let mut p = proc(i as u32 + 2, Some(1), cmd);
607 p.exe = Some(PathBuf::from("/opt/codex/bin/codex"));
609 assert_eq!(classify_agent(&p), None, "{cmd:?}");
610 assert_eq!(classify_child(&p), ProcKind::Tool, "{cmd:?}");
611 procs.push(p);
612 }
613 let (roots, orphans) = build_forest(&procs);
614 assert_eq!(roots.len(), 1);
615 assert!(roots[0].children.iter().all(|p| p.kind == ProcKind::Tool));
616 assert!(orphans.is_empty());
617
618 for cmd in [
619 vec!["codex"],
620 vec!["codex", "app-server", "--listen", "stdio://"],
621 vec!["codex", "exec", "mcp"],
622 vec!["codex", "--", "sandbox"],
623 vec!["codex", "review"],
624 vec!["codex", "resume", "--last"],
625 vec!["codex", "tcp-tunnel"],
628 ] {
629 assert_eq!(classify_agent(&proc(20, None, &cmd)), Some(Harness::Codex), "{cmd:?}");
630 }
631 assert_eq!(
632 classify_agent(&proc(20, None, &["cat", "/usr/lib/node_modules/@openai/codex/bin/codex.js"])),
633 None,
634 "mentioning the launcher path is not running it"
635 );
636 }
637
638 #[test]
639 fn classifies_roots_and_children() {
640 let procs = vec![
641 proc(1, None, &["/sbin/launchd"]),
642 proc(10, Some(1), &["claude", "--resume", "a29e19c3-2856-4510-87a0-80ce170ad830"]),
643 proc(11, Some(10), &["/bin/zsh", "-c", "cargo test"]),
644 proc(12, Some(10), &["npx", "-y", "@modelcontextprotocol/server-filesystem", "/tmp"]),
645 proc(13, Some(10), &["claude", "-p", "summarise"]),
646 proc(20, Some(1), &["uvx", "mcp-server-git"]),
647 proc(
648 30,
649 Some(1),
650 &["/Applications/ChatGPT.app/Contents/Frameworks/Codex Framework.framework/Helpers/browser_crashpad_handler"],
651 ),
652 ];
653 let (roots, orphans) = build_forest(&procs);
654 assert_eq!(roots.len(), 1);
655 let root = &roots[0];
656 assert_eq!(root.harness, Some(Harness::Claude));
657 let kinds: Vec<ProcKind> = root.children.iter().map(|c| c.kind).collect();
658 assert_eq!(kinds, vec![ProcKind::Shell, ProcKind::Mcp, ProcKind::Agent]);
659 assert_eq!(orphans.len(), 1);
660 assert_eq!(orphans[0].pid, 20);
661 assert_eq!(session_id_from_args(&procs[1].cmd).as_deref(), Some("a29e19c3-2856-4510-87a0-80ce170ad830"));
662 }
663}