mirror of
https://github.com/openai/codex.git
synced 2026-11-10 16:44:38 +00:00
Recover capability discovery after executor disconnects (#38420)
## Why Transient executor disconnects could leave capability discovery and skill catalogs stuck on a cached failure for the rest of a thread, even after the executor reconnected. ## What changed - Replay capability discovery after executor recovery and retry transient failures on later requests while continuing to cache permanent failures. - Avoid caching skill catalogs produced from failed discovery so a later step can load the recovered catalog. - Mark the MCP runtime dirty when recovered manifests change the projected MCP servers, and allow discovery to be cancelled with the turn. ## Testing - Cover same-request recovery after a disconnect and recovery on a later request. - Cover retry classification through connection-attempt errors and skill catalog caching after discovery recovers. GitOrigin-RevId: a57f90844351e73ea831931f72a9ddc4e4f3335c
This commit is contained in:
1 parent
9946da9af1
commit
588e18aae5
10 files changed
+285
-31
No files matched your search
@@ -103,10 +103,9 @@ impl SkillsThreadState {
|
||||
|
||||
/// Returns catalogs for stable selected roots.
|
||||
///
|
||||
/// The first catalog returned for a root remains cached until this thread state is dropped.
|
||||
/// Environment availability only controls whether the root is projected into the current
|
||||
/// step; it never invalidates the cache. There is intentionally no filesystem watcher or
|
||||
/// content-based invalidation because selected environment roots are treated as stable.
|
||||
/// Successful catalogs, including empty or warning-bearing catalogs, remain cached until
|
||||
/// this thread state is dropped. Catalogs backed by failed discovery are not cached, so
|
||||
/// later steps can recover. There is no filesystem watcher because selected roots are stable.
|
||||
#[tracing::instrument(
|
||||
name = "skills.executor.catalog_snapshot",
|
||||
level = "info",
|
||||
@@ -140,6 +139,10 @@ impl SkillsThreadState {
|
||||
providers: &SkillProviders,
|
||||
query: SkillListQuery,
|
||||
) -> SkillCatalog {
|
||||
let discovery_failed = query
|
||||
.executor_capability_discovery
|
||||
.as_ref()
|
||||
.is_some_and(|discovery| discovery.roots().iter().any(|root| root.result.is_err()));
|
||||
let sandbox_contexts = query
|
||||
.executor_capability_discovery
|
||||
.as_ref()
|
||||
@@ -158,6 +161,9 @@ impl SkillsThreadState {
|
||||
}
|
||||
let roots = query.executor_roots.clone();
|
||||
let discovered = providers.list_executor_for_turn(query).await;
|
||||
if discovery_failed {
|
||||
return discovered;
|
||||
}
|
||||
let mut cache = self
|
||||
.executor_discovery_cache
|
||||
.lock()
|
||||
|
||||
@@ -8,6 +8,8 @@ use codex_config::ConfigLayerEntry;
|
||||
use codex_config::ConfigLayerSource;
|
||||
use codex_config::ConfigLayerStack;
|
||||
use codex_config::ConfigRequirementsToml;
|
||||
use codex_exec_server::CapabilityRootDiscovery;
|
||||
use codex_exec_server::ExecutorCapabilityDiscoverySnapshot;
|
||||
use codex_exec_server::LOCAL_FS;
|
||||
use codex_extension_api::ConversationHistory;
|
||||
use codex_extension_api::ExtensionData;
|
||||
@@ -1125,6 +1127,45 @@ async fn selected_executor_catalog_follows_step_availability_and_reuses_its_cach
|
||||
assert!(restored_fragment.body().contains("lint-fix"));
|
||||
assert_eq!(1, list_calls.load(Ordering::Relaxed));
|
||||
|
||||
let failed_discovery = ExecutorCapabilityDiscoverySnapshot::new(
|
||||
&selected_roots,
|
||||
vec![Err("exec-server transport disconnected".to_string())],
|
||||
Default::default(),
|
||||
);
|
||||
let recovered_discovery = ExecutorCapabilityDiscoverySnapshot::new(
|
||||
&selected_roots,
|
||||
vec![Ok(Arc::new(CapabilityRootDiscovery {
|
||||
id: "lint-fix".to_string(),
|
||||
path: PathUri::parse("file:///skills/lint-fix")?,
|
||||
plugin: None,
|
||||
skills: Vec::new(),
|
||||
namespace_manifests: Vec::new(),
|
||||
warnings: Vec::new(),
|
||||
error: None,
|
||||
}))],
|
||||
Default::default(),
|
||||
);
|
||||
for (turn_id, discovery, expected_list_calls) in [
|
||||
("failed-discovery", &failed_discovery, 2),
|
||||
("recovered-discovery", &recovered_discovery, 3),
|
||||
("cached-discovery", &recovered_discovery, 3),
|
||||
] {
|
||||
registry.context_contributors()[0]
|
||||
.contribute_world_state(WorldStateContributionInput {
|
||||
thread_id: codex_protocol::ThreadId::new(),
|
||||
turn_id,
|
||||
environments: &[],
|
||||
ready_selected_capability_roots: &selected_roots,
|
||||
executor_capability_discovery: Some(discovery),
|
||||
extension_metrics: None,
|
||||
session_store: &session_store,
|
||||
thread_store: &thread_store,
|
||||
turn_store: &ExtensionData::new(turn_id),
|
||||
})
|
||||
.await;
|
||||
assert_eq!(expected_list_calls, list_calls.load(Ordering::Relaxed));
|
||||
}
|
||||
|
||||
let mut listing_disabled_config = config.clone();
|
||||
listing_disabled_config.include_instructions = false;
|
||||
registry.config_contributors()[0].on_config_changed(
|
||||
|
||||
Reference in new issue
Block a user