1use crate::execution_attempt::{read_attempt_activity, read_attempt_log_tail};
9use crate::execution_control::collect_process_ids;
10use crate::execution_store::{ExecutionRecord, ExecutionStatus, ExecutionStore};
11use crate::exit_reason::{
12 describe_exit_code, resolve_exit_reason, resolve_memory_exhaustion, CGROUP_OOM_EXIT_REASON,
13};
14use crate::isolation::isolation_log::FATAL_MARKER_TAIL_BYTES;
15use crate::output_blocks::{escape_for_links_notation, format_value_for_links_notation};
16use crate::status_footer::{parse_footer_from_tail, read_footer_from_log};
17use crate::status_probe::{
18 apply_end_time, backend_exit_code, is_detached_docker_record, is_detached_session_alive,
19 read_docker_state, resolve_oom_observation,
20};
21use chrono::Utc;
22use serde_json::Value;
23
24pub fn read_exit_code_from_log(log_path: &str) -> Option<i32> {
30 read_footer_from_log(log_path).exit_code
31}
32
33pub fn enrich_detached_status(record: &ExecutionRecord) -> ExecutionRecord {
42 let mut enriched = resolve_detached_status(record);
43 if let Some(state) = crate::cpu_penalty_monitor::read_state(record) {
44 enriched.cpu_penalty = Some(state);
45 }
46 read_attempt_activity(&mut enriched);
47 enriched.attempt_history = enrich_attempt_history(record);
48 let tail = crate::exit_evidence::record_tail(
49 &enriched,
50 read_attempt_log_tail(&enriched, FATAL_MARKER_TAIL_BYTES),
51 );
52 attach_exit_diagnostics(enriched, tail.as_deref())
53}
54
55fn attach_exit_diagnostics(mut enriched: ExecutionRecord, tail: Option<&str>) -> ExecutionRecord {
56 if enriched.status != ExecutionStatus::Executed {
57 return enriched;
58 }
59 let oom_kills = enriched.cgroup_memory.and_then(|memory| memory.oom_kills);
60 if let Some(reason) =
63 resolve_exit_reason(enriched.exit_code, tail, enriched.oom_killed, oom_kills)
64 {
65 enriched.exit_reason = Some(reason);
66 } else if enriched.exit_reason.as_deref() == Some(CGROUP_OOM_EXIT_REASON) {
67 enriched.exit_reason = None;
68 }
69 if enriched
70 .memory_exhausted_reason
71 .as_deref()
72 .is_some_and(|s| {
73 s.starts_with("Docker reported")
74 || s.starts_with("cgroup memory.events")
75 || s.starts_with("Exit-time evidence")
76 })
77 {
78 enriched.memory_exhausted = None;
79 enriched.memory_exhausted_reason = None;
80 }
81 if enriched.memory_exhausted.is_none() {
82 if let Some(memory) =
83 resolve_memory_exhaustion(enriched.exit_code, tail, enriched.oom_killed, oom_kills)
84 {
85 enriched.memory_exhausted = Some(memory.memory_exhausted);
86 enriched.memory_exhausted_reason = Some(memory.memory_exhausted_reason);
87 }
88 }
89 enriched
90}
91
92fn enrich_attempt_history(record: &ExecutionRecord) -> Vec<Value> {
93 let mut base = record.clone();
94 base.attempt_history.clear();
95 record
96 .attempt_history
97 .iter()
98 .enumerate()
99 .map(|(index, attempt)| {
100 let mut historical = base.clone();
101 historical.status = if attempt.get("status").and_then(Value::as_str) == Some("executed")
102 {
103 ExecutionStatus::Executed
104 } else {
105 ExecutionStatus::Executing
106 };
107 historical.exit_code = attempt
108 .get("exitCode")
109 .and_then(Value::as_i64)
110 .and_then(|n| i32::try_from(n).ok());
111 historical.oom_killed = attempt.get("oomKilled").and_then(Value::as_bool);
112 historical.cgroup_memory = attempt
113 .get("cgroupMemory")
114 .and_then(|v| serde_json::from_value(v.clone()).ok());
115 historical.exit_reason = attempt
116 .get("exitReason")
117 .and_then(Value::as_str)
118 .map(str::to_string);
119 historical.memory_exhausted = attempt.get("memoryExhausted").and_then(Value::as_bool);
120 historical.memory_exhausted_reason = attempt
121 .get("memoryExhaustedReason")
122 .and_then(Value::as_str)
123 .map(str::to_string);
124 historical.options.remove("exitEvidence");
125 if let Some(evidence) = attempt.get("exitEvidence") {
126 historical
127 .options
128 .insert("exitEvidence".into(), evidence.clone());
129 }
130 let end = if let Some(next) = record.attempt_history.get(index + 1) {
131 next.get("logOffset").and_then(Value::as_u64)
132 } else {
133 record.attempt.as_ref().and_then(|a| a.log_offset)
134 };
135 let tail = attempt
136 .get("logOffset")
137 .and_then(Value::as_u64)
138 .zip(end)
139 .filter(|(start, end)| end >= start)
140 .and_then(|(start, end)| {
141 crate::isolation::isolation_log::read_log_tail_range(
142 &record.log_path,
143 FATAL_MARKER_TAIL_BYTES,
144 start,
145 end,
146 )
147 });
148 let tail = crate::exit_evidence::record_tail(&historical, tail);
149 let enriched = attach_exit_diagnostics(historical, tail.as_deref()).to_json();
150 let mut output = attempt.clone();
151 for key in ["exitReason", "memoryExhausted", "memoryExhaustedReason"] {
152 if let Some(object) = output.as_object_mut() {
153 object.remove(key);
154 }
155 if let Some(value) = enriched.get(key) {
156 output[key] = value.clone();
157 }
158 }
159 output
160 })
161 .collect()
162}
163
164fn resolve_detached_status(record: &ExecutionRecord) -> ExecutionRecord {
169 if crate::launch_owner::has_active_launch(record) {
170 return record.clone();
171 }
172 let tail = read_attempt_log_tail(record, FATAL_MARKER_TAIL_BYTES);
173 let footer = parse_footer_from_tail(tail.as_deref().unwrap_or(""));
174 let footer_exit = footer.exit_code;
175 let is_detached_docker = is_detached_docker_record(record);
176 let docker_state = if is_detached_docker {
177 read_docker_state(record)
178 } else {
179 None
180 };
181
182 let oom_killed = resolve_oom_observation(record, docker_state.as_ref());
184 let clone_record = || {
185 let mut enriched = record.clone();
186 if oom_killed.is_some() {
187 enriched.oom_killed = oom_killed;
188 }
189 enriched
190 };
191
192 let alive = if is_detached_docker {
193 docker_state.as_ref().map(|state| state.running)
194 } else {
195 is_detached_session_alive(record)
196 };
197
198 let alive = match alive {
199 Some(value) => value,
200 None => {
201 let is_detached =
209 record.options.get("isolationMode").and_then(|v| v.as_str()) == Some("detached");
210 if is_detached && record.status == ExecutionStatus::Executing {
211 if footer_exit.is_some() {
212 let mut enriched = clone_record();
213 enriched.status = ExecutionStatus::Executed;
214 enriched.exit_code = footer_exit;
215 apply_end_time(&mut enriched, docker_state.as_ref(), &footer);
216 return enriched;
217 }
218 if oom_killed == Some(true) {
219 let mut enriched = clone_record();
226 enriched.status = ExecutionStatus::Executed;
227 enriched.exit_code = Some(137);
228 apply_end_time(&mut enriched, docker_state.as_ref(), &footer);
229 return enriched;
230 }
231 }
232 return clone_record();
233 }
234 };
235
236 let mut enriched = clone_record();
237
238 if alive && enriched.status == ExecutionStatus::Executed {
239 if enriched.exit_code.is_none() && footer_exit.is_none() {
246 enriched.status = ExecutionStatus::Executing;
248 enriched.exit_code = None;
249 enriched.end_time = None;
250 enriched.end_time_source = None;
251 }
252 } else if !alive && enriched.status == ExecutionStatus::Executing {
254 enriched.status = ExecutionStatus::Executed;
262 if enriched.exit_code.is_none() {
263 enriched.exit_code = Some(
264 backend_exit_code(docker_state.as_ref())
265 .or(footer_exit)
266 .or(if oom_killed == Some(true) {
267 Some(137)
268 } else {
269 None
270 })
271 .unwrap_or(-1),
272 );
273 }
274 apply_end_time(&mut enriched, docker_state.as_ref(), &footer);
275 }
276
277 enriched
278}
279
280pub fn attach_current_time(record: &ExecutionRecord) -> Option<String> {
285 if record.status == ExecutionStatus::Executing {
286 Some(Utc::now().to_rfc3339())
287 } else {
288 None
289 }
290}
291
292pub fn format_record_as_links_notation(record: &ExecutionRecord) -> String {
304 format_record_as_links_notation_with_current_time(record, None)
305}
306
307pub fn format_record_as_links_notation_with_current_time(
310 record: &ExecutionRecord,
311 current_time: Option<&str>,
312) -> String {
313 format_record_as_links_notation_with_enrichments(record, current_time, None)
314}
315
316fn append_links_array(lines: &mut Vec<String>, values: &[Value], indent: usize) {
317 let prefix = " ".repeat(indent);
318 if values.is_empty() {
319 lines.push(format!("{}()", prefix));
320 return;
321 }
322
323 lines.push(format!("{}(", prefix));
324 for value in values {
325 match value {
326 Value::Array(nested) => append_links_array(lines, nested, indent + 2),
327 Value::Object(map) => {
328 for (child_key, child_value) in map {
329 if !child_value.is_null() {
330 append_links_value(lines, child_key, child_value, indent + 2);
331 }
332 }
333 }
334 _ => lines.push(format!(
335 "{}{}",
336 " ".repeat(indent + 2),
337 format_value_for_links_notation(value)
338 )),
339 }
340 }
341 lines.push(format!("{})", prefix));
342}
343
344fn append_links_value(lines: &mut Vec<String>, key: &str, value: &Value, indent: usize) {
345 let prefix = " ".repeat(indent);
346 match value {
347 Value::Object(map) => {
348 if map.is_empty() {
349 return;
350 }
351 lines.push(format!("{}{}", prefix, key));
352 for (child_key, child_value) in map {
353 if !child_value.is_null() {
354 append_links_value(lines, child_key, child_value, indent + 4);
355 }
356 }
357 }
358 Value::Array(values) => {
359 lines.push(format!("{}{}", prefix, key));
360 append_links_array(lines, values, indent + 2);
361 }
362 _ => lines.push(format!(
363 "{}{} {}",
364 prefix,
365 key,
366 format_value_for_links_notation(value)
367 )),
368 }
369}
370
371fn format_record_as_links_notation_with_enrichments(
372 record: &ExecutionRecord,
373 current_time: Option<&str>,
374 process_ids: Option<&Value>,
375) -> String {
376 let json = record.to_json();
377 let mut lines = vec![record.uuid.clone()];
378
379 if let Value::Object(map) = json {
380 for (key, value) in map {
381 if !value.is_null() {
382 if key == "options" {
383 if let Value::Object(opts) = &value {
385 if !opts.is_empty() {
386 lines.push(" options".to_string());
387 for (opt_key, opt_value) in opts {
388 if !opt_value.is_null() {
389 let formatted = format_value_for_links_notation(opt_value);
390 lines.push(format!(" {} {}", opt_key, formatted));
391 }
392 }
393 }
394 }
395 } else if value.is_object() {
396 append_links_value(&mut lines, &key, &value, 2);
399 } else {
400 let formatted_value = match &value {
401 Value::String(s) => escape_for_links_notation(s),
402 Value::Bool(b) => b.to_string(),
403 Value::Number(n) => n.to_string(),
404 Value::Null => "null".to_string(),
405 Value::Object(_) | Value::Array(_) => {
406 format_value_for_links_notation(&value)
408 }
409 };
410 lines.push(format!(" {} {}", key, formatted_value));
411 }
412 }
413
414 if key == "pid" {
417 if let Some(process_ids) = process_ids {
418 append_links_value(&mut lines, "processIds", process_ids, 2);
419 }
420 }
421
422 if key == "startTime" {
424 if let Some(ct) = current_time {
425 lines.push(format!(" currentTime {}", escape_for_links_notation(ct)));
426 }
427 }
428 }
429 }
430
431 lines.join("\n")
432}
433
434pub fn format_record_as_text(record: &ExecutionRecord) -> String {
436 format_record_as_text_with_current_time(record, None)
437}
438
439pub fn format_record_as_text_with_current_time(
442 record: &ExecutionRecord,
443 current_time: Option<&str>,
444) -> String {
445 format_record_as_text_with_enrichments(record, current_time, None)
446}
447
448fn append_text_process_ids(lines: &mut Vec<String>, process_ids: &Value) {
449 let Value::Object(map) = process_ids else {
450 return;
451 };
452 if map.is_empty() {
453 return;
454 }
455
456 lines.push("Process IDs:".to_string());
457 for (key, value) in map {
458 let value_str = match value {
459 Value::String(s) => s.clone(),
460 Value::Bool(b) => b.to_string(),
461 Value::Number(n) => n.to_string(),
462 Value::Null => "null".to_string(),
463 other => serde_json::to_string(other).unwrap_or_default(),
464 };
465 lines.push(format!(" {}: {}", key, value_str));
466 }
467}
468
469fn format_record_as_text_with_enrichments(
470 record: &ExecutionRecord,
471 current_time: Option<&str>,
472 process_ids: Option<&Value>,
473) -> String {
474 let exit_code_str = record
475 .exit_code
476 .map(|c| describe_exit_code(Some(c)).text)
480 .unwrap_or_else(|| "N/A".to_string());
481 let pid_str = record
482 .pid
483 .map(|p| p.to_string())
484 .unwrap_or_else(|| "N/A".to_string());
485 let end_time_str = record.end_time.as_deref().unwrap_or("N/A");
486
487 let mut lines = vec![
488 "Execution Status".to_string(),
489 "=".repeat(50),
490 format!("UUID: {}", record.uuid),
491 format!("Status: {}", record.status),
492 format!("Command: {}", record.command),
493 format!("Exit Code: {}", exit_code_str),
494 ];
495 if let Some(oom_killed) = record.oom_killed {
496 lines.push(format!("OOM Killed: {}", oom_killed));
497 }
498 if let Some(ref exit_reason) = record.exit_reason {
499 lines.push(format!("Exit Reason: {}", exit_reason));
500 }
501 if let Some(memory_exhausted) = record.memory_exhausted {
502 lines.push(format!("Memory Exhausted: {}", memory_exhausted));
503 }
504 if let Some(ref reason) = record.memory_exhausted_reason {
505 lines.push(format!("Memory Evidence: {}", reason));
506 }
507 if let Some(ref memory) = record.cgroup_memory {
509 lines.push(format!(
510 "Cgroup Memory: {}",
511 crate::cgroup_memory::format_cgroup_memory(memory)
512 ));
513 }
514 if let Some(ref state) = record.cpu_penalty {
515 lines.push(format!("CPU Penalty: {}", state));
516 }
517 lines.push(format!("PID: {}", pid_str));
518 if let Some(process_ids) = process_ids {
519 append_text_process_ids(&mut lines, process_ids);
520 }
521 lines.extend([
522 format!("Working Directory: {}", record.working_directory),
523 format!("Shell: {}", record.shell),
524 format!("Platform: {}", record.platform),
525 format!("Start Time: {}", record.start_time),
526 ]);
527 if let Some(ct) = current_time {
528 lines.push(format!("Current Time: {}", ct));
529 }
530 lines.push(format!("End Time: {}", end_time_str));
531 if let Some(ref source) = record.end_time_source {
532 lines.push(format!("End Time Source: {}", source));
535 }
536 if let Some(ref started) = record.container_started_at {
537 lines.push(format!("Container Started: {}", started));
538 }
539 if let Some(ref detected) = record.stale_detected_at {
540 lines.push(format!("Stale Detected At: {}", detected));
541 }
542 lines.push(format!("Log Path: {}", record.log_path));
543 if let Some(attempt) = &record.attempt {
544 lines.extend([
545 format!("Attempt: {}", attempt.number),
546 format!("Attempt Started: {}", attempt.started_at),
547 format!(
548 "Attempt Log Offset: {}",
549 attempt
550 .log_offset
551 .map(|offset| offset.to_string())
552 .unwrap_or_else(|| "N/A".into())
553 ),
554 format!(
555 "Launch Accepted: {}",
556 attempt.launch_accepted_at.as_deref().unwrap_or("N/A")
557 ),
558 format!(
559 "Watcher Attached: {}",
560 attempt.watcher_attached_at.as_deref().unwrap_or("N/A")
561 ),
562 format!(
563 "Last Output: {}",
564 attempt.last_output_at.as_deref().unwrap_or("N/A")
565 ),
566 ]);
567 }
568
569 if !record.options.is_empty() {
571 lines.push("Options:".to_string());
572 for (key, value) in &record.options {
573 let value_str = match value {
574 Value::String(s) => s.clone(),
575 Value::Bool(b) => b.to_string(),
576 Value::Number(n) => n.to_string(),
577 Value::Null => "null".to_string(),
578 other => serde_json::to_string(other).unwrap_or_default(),
579 };
580 lines.push(format!(" {}: {}", key, value_str));
581 }
582 }
583
584 lines.join("\n")
585}
586
587fn record_json_with_enrichments(
588 record: &ExecutionRecord,
589 current_time: Option<&str>,
590 process_ids: Option<&Value>,
591) -> Value {
592 let mut json = record.to_json();
593 if let Value::Object(map) = &mut json {
594 if let Some(process_ids) = process_ids {
595 map.insert("processIds".to_string(), process_ids.clone());
596 }
597 if let Some(ct) = current_time {
598 map.insert("currentTime".to_string(), Value::String(ct.to_string()));
599 }
600 }
601 json
602}
603
604pub fn format_record(record: &ExecutionRecord, format: &str) -> Result<String, String> {
606 format_record_with_current_time(record, format, None)
607}
608
609pub fn format_record_with_current_time(
613 record: &ExecutionRecord,
614 format: &str,
615 current_time: Option<&str>,
616) -> Result<String, String> {
617 format_record_with_enrichments(record, format, current_time, None)
618}
619
620fn format_record_with_enrichments(
621 record: &ExecutionRecord,
622 format: &str,
623 current_time: Option<&str>,
624 process_ids: Option<&Value>,
625) -> Result<String, String> {
626 match format {
627 "links-notation" => Ok(format_record_as_links_notation_with_enrichments(
628 record,
629 current_time,
630 process_ids,
631 )),
632 "json" => serde_json::to_string_pretty(&record_json_with_enrichments(
633 record,
634 current_time,
635 process_ids,
636 ))
637 .map_err(|e| format!("Failed to serialize to JSON: {}", e)),
638 "text" => Ok(format_record_as_text_with_enrichments(
639 record,
640 current_time,
641 process_ids,
642 )),
643 _ => Err(format!("Unknown output format: {}", format)),
644 }
645}
646
647fn sort_records_by_start_time_desc(records: &mut [ExecutionRecord]) {
648 records.sort_by(|a, b| b.start_time.cmp(&a.start_time));
649}
650
651fn indent_block(block: &str, spaces: usize) -> String {
652 let prefix = " ".repeat(spaces);
653 block
654 .lines()
655 .map(|line| format!("{}{}", prefix, line))
656 .collect::<Vec<_>>()
657 .join("\n")
658}
659
660pub fn format_record_list_as_links_notation(records: &[ExecutionRecord]) -> String {
662 let current_times: Vec<Option<String>> = records.iter().map(attach_current_time).collect();
663 let process_ids = vec![None; records.len()];
664 format_record_list_as_links_notation_with_current_times(records, ¤t_times, &process_ids)
665}
666
667fn format_record_list_as_links_notation_with_current_times(
668 records: &[ExecutionRecord],
669 current_times: &[Option<String>],
670 process_ids: &[Option<Value>],
671) -> String {
672 let mut lines = vec![
673 "executions".to_string(),
674 format!(" count {}", records.len()),
675 ];
676
677 if records.is_empty() {
678 lines.push(" records ()".to_string());
679 return lines.join("\n");
680 }
681
682 lines.push(" records".to_string());
683 for ((record, current_time), process_ids) in records
684 .iter()
685 .zip(current_times.iter())
686 .zip(process_ids.iter())
687 {
688 let block = format_record_as_links_notation_with_enrichments(
689 record,
690 current_time.as_deref(),
691 process_ids.as_ref(),
692 );
693 lines.push(indent_block(&block, 4));
694 }
695
696 lines.join("\n")
697}
698
699pub fn format_record_list_as_text(records: &[ExecutionRecord]) -> String {
701 let current_times: Vec<Option<String>> = records.iter().map(attach_current_time).collect();
702 let process_ids = vec![None; records.len()];
703 format_record_list_as_text_with_current_times(records, ¤t_times, &process_ids)
704}
705
706fn format_record_list_as_text_with_current_times(
707 records: &[ExecutionRecord],
708 current_times: &[Option<String>],
709 process_ids: &[Option<Value>],
710) -> String {
711 let mut lines = vec![
712 "Executions".to_string(),
713 "=".repeat(50),
714 format!("Count: {}", records.len()),
715 ];
716
717 for ((record, current_time), process_ids) in records
718 .iter()
719 .zip(current_times.iter())
720 .zip(process_ids.iter())
721 {
722 lines.push(String::new());
723 lines.push(format_record_as_text_with_enrichments(
724 record,
725 current_time.as_deref(),
726 process_ids.as_ref(),
727 ));
728 }
729
730 lines.join("\n")
731}
732
733fn record_list_json_with_current_times(
734 records: &[ExecutionRecord],
735 current_times: &[Option<String>],
736 process_ids: &[Option<Value>],
737) -> Value {
738 let executions: Vec<Value> = records
739 .iter()
740 .zip(current_times.iter())
741 .zip(process_ids.iter())
742 .map(|((record, current_time), process_ids)| {
743 record_json_with_enrichments(record, current_time.as_deref(), process_ids.as_ref())
744 })
745 .collect();
746
747 serde_json::json!({
748 "count": records.len(),
749 "executions": executions,
750 })
751}
752
753pub fn format_record_list(records: &[ExecutionRecord], format: &str) -> Result<String, String> {
755 let current_times: Vec<Option<String>> = records.iter().map(attach_current_time).collect();
756 let process_ids = vec![None; records.len()];
757 format_record_list_with_current_times(records, format, ¤t_times, &process_ids)
758}
759
760fn format_record_list_with_current_times(
761 records: &[ExecutionRecord],
762 format: &str,
763 current_times: &[Option<String>],
764 process_ids: &[Option<Value>],
765) -> Result<String, String> {
766 match format {
767 "links-notation" => Ok(format_record_list_as_links_notation_with_current_times(
768 records,
769 current_times,
770 process_ids,
771 )),
772 "json" => serde_json::to_string_pretty(&record_list_json_with_current_times(
773 records,
774 current_times,
775 process_ids,
776 ))
777 .map_err(|e| format!("Failed to serialize to JSON: {}", e)),
778 "text" => Ok(format_record_list_as_text_with_current_times(
779 records,
780 current_times,
781 process_ids,
782 )),
783 _ => Err(format!("Unknown output format: {}", format)),
784 }
785}
786
787pub struct StatusQueryResult {
789 pub success: bool,
790 pub output: Option<String>,
791 pub error: Option<String>,
792}
793
794pub fn list_executions(
796 store: Option<&ExecutionStore>,
797 output_format: Option<&str>,
798) -> StatusQueryResult {
799 list_executions_filtered(store, output_format, false)
800}
801
802pub fn list_executions_filtered(
808 store: Option<&ExecutionStore>,
809 output_format: Option<&str>,
810 running_only: bool,
811) -> StatusQueryResult {
812 let store = match store {
813 Some(s) => s,
814 None => {
815 return StatusQueryResult {
816 success: false,
817 output: None,
818 error: Some("Execution tracking is disabled.".to_string()),
819 }
820 }
821 };
822
823 let mut records: Vec<ExecutionRecord> =
824 store.get_all().iter().map(enrich_detached_status).collect();
825 if running_only {
826 records.retain(|record| record.status == ExecutionStatus::Executing);
827 }
828 sort_records_by_start_time_desc(&mut records);
829 let current_times: Vec<Option<String>> = records.iter().map(attach_current_time).collect();
830 let process_ids: Vec<Option<Value>> = records.iter().map(collect_process_ids).collect();
831 let format = output_format.unwrap_or("links-notation");
832
833 match format_record_list_with_current_times(&records, format, ¤t_times, &process_ids) {
834 Ok(output) => StatusQueryResult {
835 success: true,
836 output: Some(output),
837 error: None,
838 },
839 Err(e) => StatusQueryResult {
840 success: false,
841 output: None,
842 error: Some(e),
843 },
844 }
845}
846
847pub fn query_status(
849 store: Option<&ExecutionStore>,
850 identifier: &str,
851 output_format: Option<&str>,
852) -> StatusQueryResult {
853 let store = match store {
854 Some(s) => s,
855 None => {
856 return StatusQueryResult {
857 success: false,
858 output: None,
859 error: Some("Execution tracking is disabled.".to_string()),
860 }
861 }
862 };
863
864 let record = match store.get(identifier) {
865 Some(r) => r,
866 None => {
867 return StatusQueryResult {
868 success: false,
869 output: None,
870 error: Some(format!(
871 "No execution found with UUID or session name: {}",
872 identifier
873 )),
874 }
875 }
876 };
877
878 let enriched = enrich_detached_status(&record);
880 let current_time = attach_current_time(&enriched);
882 let process_ids = collect_process_ids(&enriched);
883
884 let format = output_format.unwrap_or("links-notation");
885 match format_record_with_enrichments(
886 &enriched,
887 format,
888 current_time.as_deref(),
889 process_ids.as_ref(),
890 ) {
891 Ok(output) => StatusQueryResult {
892 success: true,
893 output: Some(output),
894 error: None,
895 },
896 Err(e) => StatusQueryResult {
897 success: false,
898 output: None,
899 error: Some(e),
900 },
901 }
902}
903
904#[cfg(test)]
905mod tests {
906 use super::*;
907 use crate::execution_store::ExecutionRecordOptions;
908 use serde_json::json;
909
910 fn executing_record() -> ExecutionRecord {
911 ExecutionRecord::with_options(ExecutionRecordOptions {
912 command: "sleep 60".to_string(),
913 uuid: Some("issue-126-rust".to_string()),
914 pid: Some(667105),
915 status: Some(ExecutionStatus::Executing),
916 log_path: Some("/tmp/issue-126.log".to_string()),
917 start_time: Some("2026-04-23T10:00:00Z".to_string()),
918 working_directory: Some("/home/user".to_string()),
919 shell: Some("/bin/bash".to_string()),
920 platform: Some("linux".to_string()),
921 ..Default::default()
922 })
923 }
924
925 #[test]
926 fn links_notation_indents_nested_process_id_arrays() {
927 let process_ids = json!({
928 "wrapperPid": 667105,
929 "screenPid": 667120,
930 "commandPids": [667121, 667122],
931 });
932 let output = format_record_with_enrichments(
933 &executing_record(),
934 "links-notation",
935 Some("2026-04-23T10:10:13.042Z"),
936 Some(&process_ids),
937 )
938 .expect("links-notation should format");
939
940 assert!(
941 output.contains(
942 " commandPids\n (\n 667121\n 667122\n )"
943 ),
944 "processIds should be a nested indented block, output: {}",
945 output
946 );
947 assert!(
948 !output.contains("\n(\n"),
949 "opening parenthesis must not start at column 1: {}",
950 output
951 );
952 }
953}