Propagate Apps tool refreshes to existing threads (#43900)

## Why

Refreshing installed Apps without a thread should update the tools available to existing threads on their next turn.

## What changed

- Publish live tool catalogs to clients with matching transport, auth, protocol, and listing settings within the same account and home directory. Ignore the thread attribution header when matching scopes.
- Adopt updated tools before catalog reads and new calls, preserving running calls and rejecting calls prepared against an outdated catalog revision.
- Keep the newest successful fetch per scope so older refreshes cannot overwrite newer tools, and exclude disk snapshots from live updates.

## Testing

Add regression coverage for refresh propagation to an existing thread without another tools listing, scope and account isolation, out-of-order refreshes, late client startup, and running versus stale prepared calls.

GitOrigin-RevId: 7a5ee34e23742ce374c6647dc8928b76ea622448
This commit is contained in:
Matthew Zeng
2026-09-08 19:56:25 +00:00
committed by copyberry
parent f31bd3adff
commit 6d377e96eb
11 changed files with 370 additions and 22 deletions
@@ -336,8 +336,28 @@ impl McpConnectionSet {
let shares_codex_apps_tools_cache = is_host_owned_codex_apps
&& should_share_codex_apps_tools_cache(&server_name, uses_env_bearer_token);
let codex_apps_tools_cache_context = shares_codex_apps_tools_cache.then(|| {
// Tools/list has no thread selection or UI capabilities. Only equivalent
// transport/auth and listing settings may share executable Apps tools.
let mut transport = configured_config.transport.clone();
if let McpServerTransportConfig::StreamableHttp {
http_headers: Some(headers),
..
} = &mut transport
{
// mcp_server_config_for_url in codex-rs/codex-mcp/src/mcp/mod.rs
// adds thread attribution that threadless discovery does not carry.
headers.retain(|name, _| !name.eq_ignore_ascii_case("originator"));
}
let mut scope = serde_json::json!([
transport,
&configured_config.auth,
protocol_mode.preferred_protocol_version().as_str(),
catalog_item_limit,
]);
scope.sort_all_objects();
codex_apps_tools_cache
.context(codex_home.clone(), codex_apps_tools_cache_key.clone())
.with_live_scope(scope.to_string())
});
// The reserved Codex Apps registration follows the shared
// AuthManager across refreshes. In the hosted-plugin path, this