Refresh live thread tools through app/installed (#43039)

## Why

Calling `app/installed` with `threadId` and `forceRefresh: true` refreshed a separate runtime snapshot without updating the thread's tools for subsequent turns.

## What changed

Use the thread's current configuration and refresh its live app tools when a thread is specified. Account for the refreshed snapshot's model-visible tools when reporting `callable`. Requests without `threadId` continue to use a separate runtime for refreshes.

## Testing

Add integration coverage verifying that refreshed tools replace previous tools in subsequent model requests and can be called, and that a failed refresh preserves the last working tools. Extend thread configuration coverage to include forced refreshes.

GitOrigin-RevId: fc13e28a59da00af470cf25c0cde356504e1575b
This commit is contained in:
Alex Daley
2026-09-05 15:30:18 +00:00
committed by copyberry
parent 32351a7b1a
commit 2cfee7de25
3 changed files with 208 additions and 28 deletions
@@ -47,25 +47,40 @@ impl AppsRequestProcessor {
let mut snapshot_age = None;
let mut snapshot_tool_count = 0;
let result = async {
let config = self
.load_apps_config(params.thread_id.as_deref())
.await?;
let (config, thread) = match params.thread_id.as_deref() {
Some(thread_id) => {
let (_, thread) = self.load_thread(thread_id).await?;
let config = thread.config().await;
(config.as_ref().clone(), Some(thread))
}
None => (
self.load_latest_config(/*fallback_cwd*/ None).await?,
None,
),
};
let auth = self.auth_manager.auth().await;
let runtime_enabled = config
.features
.apps_enabled_for_auth(auth.as_ref().is_some_and(CodexAuth::uses_codex_backend));
let mcp_manager = self.thread_manager.mcp_manager();
let mcp_config = mcp_manager.runtime_config(&config).await;
let mut mcp_servers = effective_mcp_servers(&mcp_config, auth.as_ref());
mcp_servers.retain(|name, _| name == CODEX_APPS_MCP_SERVER_NAME);
let mcp_config = Arc::new(mcp_config.for_threadless_operations(&mcp_servers));
let cache_key = connector_runtime_context_key(auth.as_ref());
let previous_snapshot = mcp_manager
.codex_apps_tools_cache()
.current_snapshot(config.codex_home.to_path_buf(), cache_key.clone());
let snapshot = if force_refresh && runtime_enabled {
let mut model_visible_tool_names = None;
let tools = if force_refresh && runtime_enabled {
let refresh_result = async {
if let Some(thread) = thread {
let snapshot = thread.refresh_codex_apps_tools().await?;
model_visible_tool_names = Some(snapshot.model_visible_tool_names);
snapshot_age = Some(Duration::ZERO);
return Ok(snapshot.tools);
}
let mcp_config = mcp_manager.runtime_config(&config).await;
let mut mcp_servers = effective_mcp_servers(&mcp_config, auth.as_ref());
mcp_servers.retain(|name, _| name == CODEX_APPS_MCP_SERVER_NAME);
let mcp_config = Arc::new(mcp_config.for_threadless_operations(&mcp_servers));
anyhow::ensure!(
!mcp_servers.is_empty(),
"host-owned MCP server '{CODEX_APPS_MCP_SERVER_NAME}' is not enabled"
@@ -125,14 +140,17 @@ impl AppsRequestProcessor {
};
cancellation_token.cancel();
runtime.shutdown().await;
result
result.map(|snapshot| {
snapshot_age = Some(snapshot.age());
snapshot.tools().to_vec()
})
}
.await;
match refresh_result {
Ok(snapshot) => {
Ok(tools) => {
refresh_disposition = "success";
Some(snapshot)
tools
}
Err(err) => {
refresh_disposition = "error";
@@ -147,17 +165,22 @@ impl AppsRequestProcessor {
refresh_disposition = "skipped_apps_disabled";
retained_previous_snapshot = previous_snapshot.is_some();
}
previous_snapshot
};
let Some(snapshot) = snapshot else {
return Ok(AppsInstalledResponse { apps: Vec::new() });
previous_snapshot.map_or_else(Vec::new, |snapshot| {
snapshot_age = Some(snapshot.age());
snapshot.tools().to_vec()
})
};
snapshot_age = Some(snapshot.age());
snapshot_tool_count = snapshot.tools().len();
snapshot_tool_count = tools.len();
let apps = installed_connector_runtime(
&config.config_layer_stack,
snapshot.tools().iter().map(connector_runtime_tool),
tools.iter().map(|tool| {
let mut runtime_tool = connector_runtime_tool(tool);
if let Some(names) = &model_visible_tool_names {
runtime_tool.model_visible &= names.contains(tool.tool.name.as_ref());
}
runtime_tool
}),
)
.into_iter()
.map(|app| InstalledApp {