use chrono::{DateTime, Duration, Utc};
use octl_core::{Kind, Node, Status};
pub const STALL_GRACE: Duration = Duration::minutes(12);
pub const DRIVER_NODE_ID: &str = "n-0001";
#[must_use]
pub fn is_stalled(
run_status: Status,
kind: Kind,
driver: Option<&Node>,
now: DateTime<Utc>,
) -> bool {
if run_status != Status::Pending {
return false;
}
if kind != Kind::Orchestrate {
return false;
}
let Some(node) = driver else {
return false;
};
if node.status != Status::Pending {
return false;
}
if !node.children.is_empty() {
return false;
}
now.signed_duration_since(node.updated_at) > STALL_GRACE
}
#[must_use]
pub fn is_stillborn(
run_status: Status,
supervisor_alive: bool,
node_count: u32,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
) -> bool {
run_status == Status::Pending
&& !supervisor_alive
&& node_count == 0
&& updated_at == created_at
}
pub const ORPHAN_GRACE: Duration = Duration::minutes(15);
#[must_use]
pub fn is_orphaned(
run_status: Status,
supervisor_alive: bool,
node_count: u32,
updated_at: DateTime<Utc>,
now: DateTime<Utc>,
) -> bool {
matches!(run_status, Status::Pending | Status::Running)
&& !supervisor_alive
&& node_count > 0
&& now.signed_duration_since(updated_at) > ORPHAN_GRACE
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StallKind {
Stillborn,
Orphaned,
}
#[must_use]
pub fn stall_kind(
run_status: Status,
supervisor_alive: bool,
node_count: u32,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
now: DateTime<Utc>,
) -> Option<StallKind> {
if is_stillborn(
run_status,
supervisor_alive,
node_count,
created_at,
updated_at,
) {
Some(StallKind::Stillborn)
} else if is_orphaned(run_status, supervisor_alive, node_count, updated_at, now) {
Some(StallKind::Orphaned)
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use octl_core::{NodeId, RunId};
fn node(status: Status, children: usize, updated_at: DateTime<Utc>) -> Node {
Node {
schema_version: 1,
node_id: NodeId::parse_str("n-0001").unwrap(),
run_id: RunId::parse_str("01arz3ndektsv4rrffq69g5fav").unwrap(),
parent_node_id: None,
kind: Kind::Orchestrate,
status,
task: None,
worktree_path: None,
branch: None,
base_sha: None,
tmux_window: None,
tmux_identity: None,
agent_pid: None,
agent_pid_start_time: None,
supervisor_pid: None,
children: (0..children)
.map(|i| octl_core::ChildRef {
run_id: RunId::parse_str("01arz3ndektsv4rrffq69g5fav").unwrap(),
node_id: NodeId::parse_str(&format!("n-{:04}", i + 2)).unwrap(),
})
.collect(),
started_at: None,
updated_at,
last_report: None,
last_processed_report_seq_by_child: serde_json::Map::new(),
retry_attempts: 0,
}
}
fn now() -> DateTime<Utc> {
"2026-08-06T12:00:00Z".parse().unwrap()
}
#[test]
fn undriven_driver_past_grace_is_stalled() {
let created = now() - STALL_GRACE - Duration::seconds(1);
let n = node(Status::Pending, 0, created);
assert!(is_stalled(
Status::Pending,
Kind::Orchestrate,
Some(&n),
now()
));
}
#[test]
fn driver_with_child_is_not_stalled() {
let created = now() - STALL_GRACE - Duration::hours(1);
let n = node(Status::Pending, 1, created);
assert!(!is_stalled(
Status::Pending,
Kind::Orchestrate,
Some(&n),
now()
));
}
#[test]
fn driver_with_recent_activity_is_not_stalled() {
let recent = now() - Duration::minutes(1);
let n = node(Status::Pending, 0, recent);
assert!(!is_stalled(
Status::Pending,
Kind::Orchestrate,
Some(&n),
now()
));
}
#[test]
fn within_grace_window_is_not_stalled() {
let created = now() - STALL_GRACE + Duration::seconds(1);
let n = node(Status::Pending, 0, created);
assert!(!is_stalled(
Status::Pending,
Kind::Orchestrate,
Some(&n),
now()
));
}
#[test]
fn exactly_at_grace_boundary_is_not_stalled() {
let created = now() - STALL_GRACE;
let n = node(Status::Pending, 0, created);
assert!(!is_stalled(
Status::Pending,
Kind::Orchestrate,
Some(&n),
now()
));
}
#[test]
fn non_pending_run_status_is_never_stalled() {
let created = now() - STALL_GRACE - Duration::hours(1);
let n = node(Status::Pending, 0, created);
for run_status in [
Status::Running,
Status::Blocked,
Status::Done,
Status::Failed,
Status::Cancelled,
] {
assert!(
!is_stalled(run_status, Kind::Orchestrate, Some(&n), now()),
"run status {run_status:?} must not stall"
);
}
}
#[test]
fn non_orchestrate_kind_is_never_stalled() {
let created = now() - STALL_GRACE - Duration::hours(1);
let n = node(Status::Pending, 0, created);
for k in [Kind::Spinoff, Kind::FanOut, Kind::Orchestrated, Kind::Code] {
assert!(
!is_stalled(Status::Pending, k, Some(&n), now()),
"kind {k:?} must not stall"
);
}
}
#[test]
fn non_pending_driver_is_not_stalled() {
let created = now() - STALL_GRACE - Duration::hours(1);
for s in [
Status::Running,
Status::Blocked,
Status::Done,
Status::Failed,
Status::Cancelled,
] {
let n = node(s, 0, created);
assert!(
!is_stalled(Status::Pending, Kind::Orchestrate, Some(&n), now()),
"driver status {s:?} must not stall"
);
}
}
#[test]
fn missing_driver_node_is_not_stalled() {
assert!(!is_stalled(Status::Pending, Kind::Orchestrate, None, now()));
}
fn created() -> DateTime<Utc> {
"2026-08-06T11:00:00Z".parse().unwrap()
}
#[test]
fn stillborn_signature_is_detected() {
assert!(is_stillborn(
Status::Pending,
false,
0,
created(),
created()
));
}
#[test]
fn alive_supervisor_is_not_stillborn() {
assert!(!is_stillborn(
Status::Pending,
true,
0,
created(),
created()
));
}
#[test]
fn nonzero_node_count_is_not_stillborn() {
assert!(!is_stillborn(
Status::Pending,
false,
1,
created(),
created()
));
}
#[test]
fn forward_progress_is_not_stillborn() {
let updated = created() + Duration::seconds(1);
assert!(!is_stillborn(Status::Pending, false, 0, created(), updated));
}
#[test]
fn non_pending_run_is_not_stillborn() {
for s in [
Status::Running,
Status::Blocked,
Status::Done,
Status::Failed,
Status::Cancelled,
] {
assert!(
!is_stillborn(s, false, 0, created(), created()),
"status {s:?} must not be stillborn"
);
}
}
#[test]
fn pending_dead_supervisor_past_grace_is_orphaned() {
let idle = now() - ORPHAN_GRACE - Duration::seconds(1);
assert!(is_orphaned(Status::Pending, false, 1, idle, now()));
}
#[test]
fn running_dead_supervisor_past_grace_is_orphaned() {
let idle = now() - ORPHAN_GRACE - Duration::minutes(30);
assert!(is_orphaned(Status::Running, false, 3, idle, now()));
}
#[test]
fn recently_active_dead_supervisor_is_not_orphaned() {
let recent = now() - Duration::minutes(1);
assert!(!is_orphaned(Status::Pending, false, 1, recent, now()));
}
#[test]
fn exactly_at_orphan_grace_boundary_is_not_orphaned() {
let boundary = now() - ORPHAN_GRACE;
assert!(!is_orphaned(Status::Pending, false, 1, boundary, now()));
}
#[test]
fn alive_supervisor_is_never_orphaned() {
let idle = now() - ORPHAN_GRACE - Duration::hours(2);
assert!(!is_orphaned(Status::Pending, true, 1, idle, now()));
}
#[test]
fn zero_nodes_is_not_orphaned() {
let idle = now() - ORPHAN_GRACE - Duration::hours(1);
assert!(!is_orphaned(Status::Pending, false, 0, idle, now()));
}
#[test]
fn terminal_or_blocked_run_is_not_orphaned() {
let idle = now() - ORPHAN_GRACE - Duration::hours(1);
for s in [
Status::Blocked,
Status::Done,
Status::Failed,
Status::Cancelled,
] {
assert!(
!is_orphaned(s, false, 1, idle, now()),
"status {s:?} must not be orphaned"
);
}
}
#[test]
fn stall_kind_classifies_stillborn() {
assert_eq!(
stall_kind(Status::Pending, false, 0, created(), created(), now()),
Some(StallKind::Stillborn)
);
}
#[test]
fn stall_kind_classifies_orphaned() {
let idle = now() - ORPHAN_GRACE - Duration::seconds(1);
assert_eq!(
stall_kind(Status::Pending, false, 1, created(), idle, now()),
Some(StallKind::Orphaned)
);
}
#[test]
fn stall_kind_healthy_is_none() {
assert_eq!(
stall_kind(Status::Running, true, 2, created(), now(), now()),
None
);
let recent = now() - Duration::minutes(1);
assert_eq!(
stall_kind(Status::Pending, false, 1, created(), recent, now()),
None
);
}
}