mirror of
https://github.com/openai/codex.git
synced 2026-11-09 00:39:32 +00:00
Prewarm shell snapshots for eligible turns (#42137)
## Why Shell Snapshot V2 previously captured the login-shell environment lazily when a command started, adding that work to the command path. ## What changed - Start one asynchronous snapshot capture after turn hooks accept the turn, so eligible local `exec_command` calls can reuse it without blocking the model. - Use the turn's resolved tools, permissions, sandbox, environment, and shell configuration, and skip prewarming for unsupported or network-managed cases. - Cancel outstanding prewarming during shutdown. Keep failed speculative captures retryable by real commands without consuming their retry budget. ## Testing Added coverage for non-blocking capture and reuse, failure recovery, sandbox and permission isolation, shutdown cancellation, hook-stopped turns, eligibility gates, and concurrent capture retries. GitOrigin-RevId: ff24a4f0328c06653e09d03fd0a3db1eccd65030
This commit is contained in:
1 parent
c5b5fa8089
commit
612e6491d5
13 files changed
+783
-50
No files matched your search
@@ -10,6 +10,7 @@ use opentelemetry_sdk::metrics::data::MetricData;
|
||||
use pretty_assertions::assert_eq;
|
||||
use test_case::test_case;
|
||||
|
||||
use super::CapturePurpose;
|
||||
use super::MAX_SNAPSHOT_ATTEMPTS;
|
||||
use super::SNAPSHOT_RETRY_BACKOFF;
|
||||
use super::ShellSnapshotCache;
|
||||
@@ -22,14 +23,19 @@ use crate::protocol::ShellInfo;
|
||||
use crate::protocol::ShellSnapshotRequest;
|
||||
use crate::telemetry::ExecServerTelemetry;
|
||||
|
||||
#[test_case(1; "succeeds_on_first_attempt")]
|
||||
#[test_case(2; "recovers_on_second_attempt")]
|
||||
#[test_case(3; "recovers_on_last_attempt")]
|
||||
#[test_case(4; "stops_after_three_failures")]
|
||||
#[test_case(1, CapturePurpose::Execution; "succeeds_on_first_attempt")]
|
||||
#[test_case(2, CapturePurpose::Execution; "recovers_on_second_attempt")]
|
||||
#[test_case(3, CapturePurpose::Execution; "recovers_on_last_attempt")]
|
||||
#[test_case(4, CapturePurpose::Execution; "stops_after_three_failures")]
|
||||
#[test_case(2, CapturePurpose::Prewarm; "failed_prewarm_releases_waiting_command")]
|
||||
#[test_case(3, CapturePurpose::Prewarm; "failed_prewarm_preserves_last_attempt")]
|
||||
#[test_case(4, CapturePurpose::Prewarm; "failed_prewarm_preserves_retry_limit")]
|
||||
#[tokio::test]
|
||||
async fn snapshot_failure_retries_are_bounded_and_single_flight(
|
||||
recovery_attempt: usize,
|
||||
initial_purpose: CapturePurpose,
|
||||
) -> anyhow::Result<()> {
|
||||
let prewarm_fails_first = initial_purpose == CapturePurpose::Prewarm;
|
||||
let home = tempfile::TempDir::new()?;
|
||||
let profile = home.path().join(".bashrc");
|
||||
std::fs::write(&profile, "printf x >> \"$HOME/captures\"\nexit 7\n")?;
|
||||
@@ -101,12 +107,23 @@ async fn snapshot_failure_retries_are_bounded_and_single_flight(
|
||||
)
|
||||
.await
|
||||
.expect("prepare concurrent capture");
|
||||
let prewarming = prewarm_fails_first && attempt == 1;
|
||||
let purpose = if prewarming {
|
||||
CapturePurpose::Prewarm
|
||||
} else {
|
||||
CapturePurpose::Execution
|
||||
};
|
||||
let (first, second) = tokio::join!(
|
||||
cache.prepare(¶ms, &mut prepared, &telemetry),
|
||||
cache.prepare(¶ms, &mut concurrent, &telemetry),
|
||||
biased;
|
||||
cache.prepare(¶ms, &mut prepared, &telemetry, purpose),
|
||||
cache.prepare(¶ms, &mut concurrent, &telemetry, CapturePurpose::Execution),
|
||||
);
|
||||
first.expect("capture failure must preserve command fallback");
|
||||
second.expect("concurrent request must share the capture attempt");
|
||||
if prewarming {
|
||||
first.expect_err("prewarm must report failure without caching it");
|
||||
} else {
|
||||
first.expect("capture failure must preserve command fallback");
|
||||
}
|
||||
second.expect("waiting command must complete even when prewarm fails");
|
||||
assert_eq!(
|
||||
(&prepared.command, &prepared.env),
|
||||
(&concurrent.command, &concurrent.env)
|
||||
@@ -115,7 +132,12 @@ async fn snapshot_failure_retries_are_bounded_and_single_flight(
|
||||
tokio::time::pause();
|
||||
if attempt < recovery_attempt || recovery_attempt > MAX_SNAPSHOT_ATTEMPTS {
|
||||
cache
|
||||
.prepare(¶ms, &mut prepared, &telemetry)
|
||||
.prepare(
|
||||
¶ms,
|
||||
&mut prepared,
|
||||
&telemetry,
|
||||
CapturePurpose::Execution,
|
||||
)
|
||||
.await
|
||||
.expect("capture must stay cached during backoff");
|
||||
assert_eq!(
|
||||
@@ -127,7 +149,10 @@ async fn snapshot_failure_retries_are_bounded_and_single_flight(
|
||||
}
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(home.path().join("captures"))?,
|
||||
"x".repeat(attempt.min(recovery_attempt).min(MAX_SNAPSHOT_ATTEMPTS))
|
||||
"x".repeat(
|
||||
attempt.min(recovery_attempt).min(MAX_SNAPSHOT_ATTEMPTS)
|
||||
+ usize::from(prewarm_fails_first)
|
||||
)
|
||||
);
|
||||
tokio::time::advance(SNAPSHOT_RETRY_BACKOFF).await;
|
||||
tokio::time::resume();
|
||||
@@ -170,7 +195,8 @@ async fn snapshot_failure_retries_are_bounded_and_single_flight(
|
||||
}
|
||||
let mut expected_counters = BTreeMap::new();
|
||||
let mut expected_durations = BTreeMap::new();
|
||||
let failures = (recovery_attempt - 1).min(MAX_SNAPSHOT_ATTEMPTS) as u64;
|
||||
let failures =
|
||||
(recovery_attempt - 1).min(MAX_SNAPSHOT_ATTEMPTS) as u64 + u64::from(prewarm_fails_first);
|
||||
for (success, count) in [
|
||||
("false", failures),
|
||||
("true", u64::from(recovery_attempt <= MAX_SNAPSHOT_ATTEMPTS)),
|
||||
|
||||
Reference in new issue
Block a user