feat(graph): treat timeout: 0 as no wall-clock bound

A zero on any of the five timeout fields (settings.timeout, agent, script,
llm, rag) now disables the bound instead of firing an immediate
"timed out after 0s" or graph bail. Unset values keep their defaults
(agent 300, script 30, rag 120, llm/settings none); non-zero values are
unchanged. One resolution helper, wall_clock(secs) -> Option<Duration>,
feeds all five call sites; the C17 peer-retirement mark and every error
message stay where they were.

The one existing test that relied on timeout: 0 firing
(run_item_chain_marks_identity_finished_when_agent_step_times_out) moves
to timeout: 1 with its 5s hold unchanged.

Design §3.13 / C21.
This commit is contained in:
Dark-Alex-17 committed 2026-09-11 10:56:27 -06:00
1 parent b666b7e494
commit d630c7e30b
7 files changed
+181 -61

No files matched your search

+36 -1
View File
@@ -9,6 +9,7 @@ use super::state::StateManager;
use super::types::{EndNode, Graph, Node, NodeType};
use super::user_interaction::{ApprovalNodeExecutor, InputNodeExecutor};
use super::validator::{AgentValidationContext, GraphValidator};
use super::wall_clock;
use crate::config::{RenderMode, RequestContext};
use crate::supervisor::mailbox::{Inbox, PeerAssignment, PeerRegistry, graph_agent_id};
use crate::utils::AbortSignal;
@@ -105,7 +106,7 @@ impl GraphExecutor {
.unwrap_or_default();
let script_executor = ScriptExecutor::new(&base_dir).with_envs(agent_envs);
let max_iterations = graph.settings.max_loop_iterations;
let graph_timeout = graph.settings.timeout.map(Duration::from_secs);
let graph_timeout = graph.settings.timeout.and_then(wall_clock);
let max_concurrency = graph.settings.max_concurrency;
let graph = Arc::new(graph);
let start = Instant::now();
@@ -1221,6 +1222,40 @@ nodes:
assert!(err.contains("sleeper"), "error should name frontier: {err}");
}
#[tokio::test]
async fn graph_timeout_zero_is_unbounded() {
if !cmd_available("bash") {
eprintln!("skipping: bash not available");
return;
}
let ws = TestWorkspace::new();
ws.write_script("sleeper.sh", "#!/bin/bash\nsleep 1.5\necho '{}'\n");
let yaml = r#"
name: zero_timeout_test
start: sleeper
settings:
timeout: 0
nodes:
sleeper:
type: script
script: sleeper.sh
state_updates: {}
next: done
done:
type: end
output: "done"
"#;
let graph: Graph = serde_yaml::from_str(yaml).unwrap();
let mut ctx = make_ctx();
let abort = create_abort_signal();
let result = GraphExecutor::new(graph, &ws.dir)
.execute(&mut ctx, abort)
.await;
result.unwrap_or_else(|e| panic!("timeout: 0 should not bound the run: {e:#}"));
}
#[cfg(unix)]
#[tokio::test]
async fn background_job_survives_graph_node_execution() {