fix(runtime): a turn gets the phase's budget, not 100s more than one call

A turn is an agent LOOP, not one model call. Each call inside it is bounded
separately by the daemon — `claude_cli`'s `timeout_secs`, 600s on gw-04,
verified in the live config — so TURN_TIMEOUT has to cover however many calls
the loop makes, not one of them. It was 700s.

MEASURED: a healthy research turn is ~157s. A throttled one blew the budget with
one slow call plus a second, and the executor killed it at 11m43s with no error
from the daemon, because nothing had failed yet. The operator got
"turn executor failed: turn timed out" and the container holding the reason was
torn down minutes later.

An hour matches the phase's own budget. A genuinely stuck CALL is still caught
at 600s by the daemon and surfaces as a real error; this only stops us killing
turns that are working, slowly.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Omar Sobh
2026-08-10 10:51:08 -07:00
co-authored by Claude Opus 5
parent b2e2735583
commit 4c418f7d9b
+16 -9
View File
@@ -24,15 +24,22 @@ use tokio::sync::Mutex;
use tokio_tungstenite::connect_async;
use tokio_tungstenite::tungstenite::Message;
/// Overall wall-clock budget for draining one turn's event stream. Must
/// exceed the daemon's own claude_cli provider timeout (600s on gw-04
/// via ZEROCLAW_providers__models__claude_cli__default__timeout_ms) —
/// otherwise the executor kills the ws before the daemon can reply and
/// we see a phantom "turn timed out" while the daemon still logs a
/// successful llm response coming back. 700s gives 100s of headroom so
/// a daemon that just barely made it under its own limit doesn't lose
/// its answer here.
const TURN_TIMEOUT: Duration = Duration::from_secs(700);
/// Overall wall-clock budget for draining one turn's event stream.
///
/// A turn is an agent LOOP, not one model call. Each call inside it is bounded
/// separately by the daemon — `claude_cli`'s `timeout_secs`, 600s on gw-04 —
/// so this has to cover however many calls the loop makes, not one of them.
///
/// It was 700s, which is 100s more than a single call may take. MEASURED: a
/// healthy research turn is ~157s, but a throttled one blew the budget with one
/// slow call plus a second, and the executor killed it mid-flight after 11m43s
/// with no error from the daemon — because nothing had failed yet. All the
/// operator got was "turn timed out".
///
/// An hour matches the phase's own budget. A genuinely stuck CALL is still
/// caught at 600s by the daemon and surfaces as a real error; this only stops
/// us killing turns that are working, slowly.
const TURN_TIMEOUT: Duration = Duration::from_secs(3600);
/// Drives ZeroClaw role-agents (in one container) to execute topology turns.
pub struct ZeroClawDriveExecutor {