diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 9e250998ff..c00e2e4030 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -3160,6 +3160,7 @@ dependencies = [ "codex-tools", "codex-utils-template", "pretty_assertions", + "rand 0.9.3", "serde", "serde_json", "tempfile", diff --git a/codex-rs/ext/goal/Cargo.toml b/codex-rs/ext/goal/Cargo.toml index 4d75ba2f26..eb1374f341 100644 --- a/codex-rs/ext/goal/Cargo.toml +++ b/codex-rs/ext/goal/Cargo.toml @@ -22,6 +22,7 @@ codex-protocol = { workspace = true } codex-state = { workspace = true } codex-tools = { workspace = true } codex-utils-template = { workspace = true } +rand = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } tokio = { workspace = true, features = ["sync"] } diff --git a/codex-rs/ext/goal/src/extension.rs b/codex-rs/ext/goal/src/extension.rs index e0ebd0c43a..4c490c125f 100644 --- a/codex-rs/ext/goal/src/extension.rs +++ b/codex-rs/ext/goal/src/extension.rs @@ -32,6 +32,7 @@ use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SubAgentSource; use codex_protocol::protocol::ThreadGoalStatus; use codex_protocol::protocol::TokenUsageInfo; +use rand::Rng; use crate::accounting::BudgetLimitedGoalDisposition; use crate::accounting::GoalAccountingState; @@ -46,9 +47,10 @@ use crate::spec::UPDATE_GOAL_TOOL_NAME; use crate::steering::budget_limit_steering_item; use crate::tool::GoalToolExecutor; -// Capacity failures do not consume user tokens, but retrying immediately can -// create a tight loop. Keep the retry cadence deliberately low. -const DEFERRED_GOAL_RETRY_DELAY: Duration = Duration::from_secs(5 * 60); +// Capacity failures do not consume user tokens. Add jitter around a five-minute +// average so clients that hit capacity together do not retry in lockstep. +const DEFERRED_GOAL_RETRY_MIN_SECS: u64 = 4 * 60; +const DEFERRED_GOAL_RETRY_MAX_SECS: u64 = 6 * 60; #[derive(Clone, Debug)] pub struct GoalExtensionConfig { @@ -313,7 +315,9 @@ where .accounting_state() .turn_is_current_active_goal(input.turn_id) { - return Some(DEFERRED_GOAL_RETRY_DELAY); + return Some(Duration::from_secs(rand::rng().random_range( + DEFERRED_GOAL_RETRY_MIN_SECS..=DEFERRED_GOAL_RETRY_MAX_SECS, + ))); } let reason = match input.error { CodexErrorInfo::UsageLimitExceeded => ActiveGoalStopReason::UsageLimit,