use std::path::Path;
use crate::artifacts::RunArtifacts;
use crate::mem_limit_config::{format_memory_gib, load_mem_limit_bytes, system_total_memory_bytes};
use crate::sandbox_oom::gate_iteration_oom_killed;
#[must_use]
pub fn format_current_state(
work_dir: &Path,
gate_iteration: Option<usize>,
artifacts: Option<&RunArtifacts>,
) -> String {
[
format!("User: {}", format_user_identity()),
format!("Date/time: {}", format_local_datetime()),
format_sandbox_memory_line(work_dir),
format_retry_line(gate_iteration, artifacts),
]
.join("\n")
}
#[must_use]
pub fn format_user_identity() -> String {
let login = std::env::var("USER")
.or_else(|_| std::env::var("USERNAME"))
.unwrap_or_else(|_| "unknown".to_string());
let uid = effective_user_id();
let full_name = uid.and_then(passwd_gecos_full_name);
assemble_user_identity(&login, uid, full_name.as_deref())
}
#[must_use]
pub fn assemble_user_identity(login: &str, uid: Option<u32>, full_name: Option<&str>) -> String {
let include_full_name = full_name.filter(|name| !name.is_empty() && *name != login);
match (uid, include_full_name) {
(Some(uid), Some(name)) => format!("{login} (uid {uid}, {name})"),
(Some(uid), None) => format!("{login} (uid {uid})"),
(None, _) => login.to_string(),
}
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
mod passwd_gecos {
#![allow(unsafe_code)]
#[must_use]
pub fn full_name(uid: u32) -> Option<String> {
let pw = unsafe { libc::getpwuid(uid) };
if pw.is_null() {
return None;
}
let gecos = unsafe { std::ffi::CStr::from_ptr((*pw).pw_gecos) };
let full_name = gecos
.to_str()
.ok()?
.split(',')
.next()
.map(str::trim)
.filter(|s| !s.is_empty())?;
Some(full_name.to_string())
}
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn passwd_gecos_full_name(uid: u32) -> Option<String> {
passwd_gecos::full_name(uid)
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
fn passwd_gecos_full_name(_uid: u32) -> Option<String> {
None
}
#[cfg(unix)]
fn effective_user_id() -> Option<u32> {
std::process::Command::new("id")
.arg("-u")
.output()
.ok()
.filter(|o| o.status.success())
.and_then(|o| String::from_utf8(o.stdout).ok())
.and_then(|s| s.trim().parse().ok())
}
#[cfg(not(unix))]
fn effective_user_id() -> Option<u32> {
None
}
#[must_use]
pub fn format_local_datetime() -> String {
chrono::Local::now()
.format("%Y-%m-%d %H:%M:%S %Z")
.to_string()
}
#[must_use]
pub fn format_sandbox_memory_line(work_dir: &Path) -> String {
let limit = load_mem_limit_bytes(work_dir);
let rss = current_sandbox_rss_bytes().unwrap_or(0);
let available = limit.saturating_sub(rss);
let mut parts = vec![
format!("limit {}", format_memory_gib(limit)),
format!("in use {}", format_memory_gib(rss)),
format!("available {}", format_memory_gib(available)),
];
if let Some(host) = system_total_memory_bytes() {
parts.push(format!("host total {}", format_memory_gib(host)));
}
format!("Sandbox memory: {}", parts.join(", "))
}
fn current_sandbox_rss_bytes() -> Option<u64> {
#[cfg(unix)]
{
let stack = crate::active_agent_heartbeat::active_agent_process_group_for_stats()?;
crate::malvin_sandbox::malvin_session_rss_bytes(Some(stack.pgid), &stack.spawn_baseline)
}
#[cfg(not(unix))]
{
None
}
}
#[must_use]
pub fn format_retry_line(
gate_iteration: Option<usize>,
artifacts: Option<&RunArtifacts>,
) -> String {
let Some(iter) = gate_iteration.filter(|&i| i > 0) else {
return "Retry: not a retry (first session in this malvin run).".to_string();
};
if iter == 1 {
return "Retry: not a retry (first outer gate-loop session).".to_string();
}
let retry_num = iter - 1;
let reasons = infer_gate_retry_reasons(artifacts, iter);
if reasons.is_empty() {
format!(
"Retry: yes — outer gate-loop session {iter} (retry #{retry_num}); reason not recorded."
)
} else {
format!(
"Retry: yes — outer gate-loop session {iter} (retry #{retry_num}); reason: {}.",
reasons.join("; ")
)
}
}
fn infer_gate_retry_reasons(artifacts: Option<&RunArtifacts>, iteration: usize) -> Vec<String> {
let Some(artifacts) = artifacts else {
return Vec::new();
};
let prev = iteration.saturating_sub(1);
if prev == 0 {
return Vec::new();
}
let mut reasons = Vec::new();
append_unsolved_reason(&mut reasons, artifacts, prev);
append_oom_reason(&mut reasons, artifacts, prev);
append_gates_reason(&mut reasons, artifacts, prev);
reasons
}
fn append_unsolved_reason(reasons: &mut Vec<String>, artifacts: &RunArtifacts, prev: usize) {
if prev_exp_log_ran(artifacts, prev) && reasons.is_empty() {
reasons.push("quality gates did not pass after previous router session".to_string());
}
}
fn prev_exp_log_ran(artifacts: &RunArtifacts, prev: usize) -> bool {
artifacts.gate_exp_log_path(prev).is_file()
}
fn append_oom_reason(reasons: &mut Vec<String>, artifacts: &RunArtifacts, prev: usize) {
if gate_iteration_oom_killed(artifacts, prev) {
reasons.push("previous agent killed: sandbox exceeded memory limit (OOM)".to_string());
}
}
const fn append_gates_reason(_reasons: &mut Vec<String>, _artifacts: &RunArtifacts, _prev: usize) {}
#[cfg(test)]
#[path = "current_state_tests.rs"]
mod current_state_tests;