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
@@ -446,7 +446,7 @@ impl McpConnectionSet {
.client()
.await
.context("failed to get client")?;
let (client_tools, tools, list_start) = managed_client
let (tools, list_start) = managed_client
.tool_catalog
.refresh(
|| async {
@@ -474,8 +474,8 @@ impl McpConnectionSet {
Ok((client_tools, (fetch_ticket, list_start)))
},
|client_tools, (fetch_ticket, list_start)| {
// Discovery may accept another client's newer fetch. The catalog
// retains the tools fetched through this exact connection.
// Discovery can accept another scope's winner; executable catalogs
// receive only the latest successful fetch from their own scope.
let tools = match (
managed_client.codex_apps_tools_cache_context.as_ref(),
fetch_ticket,
@@ -489,10 +489,14 @@ impl McpConnectionSet {
(None, None) => client_tools.to_vec(),
_ => unreachable!("Codex Apps fetch ticket requires cache context"),
};
(client_tools.to_vec(), tools, list_start)
(tools, list_start)
},
)
.await?;
let client_tools = managed_client
.tool_catalog
.read(|catalog| catalog.tools.clone())
.await;
emit_duration(
MCP_TOOLS_LIST_DURATION_METRIC,
list_start.elapsed(),