mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-08 17:10:50 -10:00
fix(runtime-api): report MCP servers' last connect error; scope Fleet member-start replay to its session
`GET /v1/apps/mcp/servers` now carries `last_error` for a configured server
that is in connection backoff, truncated to 2,048 characters, so clients can
say why a server is not connected instead of only showing it as disconnected.
`McpPool::last_connect_error` returns the error only while the same
configuration is the one in backoff (runtime-added flag, reviewed plugin
source and runtime config hash unchanged); runtime-added servers report none.
docs/RUNTIME_API.md documents the field.
Fleet member starts are keyed by (state namespace, operation key), so a
replayed start in one session can no longer resolve to a member that another
session started under the same operation key.
Recovered from stranded drafts (the readiness MCP last-error patch, the MCP
pool accessor and release-engine 9cb914b617) and rewritten against the current
has_connection_backoff.
Verified: the five runtime_api MCP management tests pass (5 passed, 0 failed:
crud, redacts_credentials, project_ownership_blocks_global_shadow_mutations,
blocks_credential_retargeting, revision_precondition_prevents_stale_mutation),
run on the shared tree with other sessions' uncommitted edits present;
clippy on codewhale-engine and codewhale-app-server libs was clean earlier
today; the patch applies to HEAD cd7f5b32b1 and every item it references
exists there. No new tests.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
cd7f5b32b1
commit
4c5426f9bb
5 files changed
+52
-4
No files matched your search
@@ -70,6 +70,10 @@ until the matching tag, packages, checksums, and release assets exist.
|
||||
|
||||
### Added
|
||||
|
||||
- **MCP servers say why they are not connected:** the Runtime API's
|
||||
`GET /v1/apps/mcp/servers` now includes a configured server's last
|
||||
connection error while it is retrying, so the desktop and other clients can
|
||||
show the reason instead of only "disconnected".
|
||||
- **Inspect and repair plugin snapshots:** `codew plugins status [name]
|
||||
[--json]` reports bundle state and diagnostics without changing plugin
|
||||
stores. Runtime loading restores a missing snapshot only from bytes matching
|
||||
@@ -285,6 +289,9 @@ until the matching tag, packages, checksums, and release assets exist.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Fleet members:** a replayed member start now matches only within its own
|
||||
session, so two sessions that reuse an operation key no longer receive each
|
||||
other's agent.
|
||||
- **Plugin stages created during a registry load are checked:** a plugin whose staged copy appears while the registry is loading now goes through the same restage lock, receipt and policy checks as any other stage instead of being left unstaged; invalid bytes are still refused and stay in the failure cache.
|
||||
- **Host-provided model clients:** keep configured session isolation and
|
||||
approval deadlines when optional credential preflight fails.
|
||||
|
||||
@@ -1640,6 +1640,8 @@ struct McpServerEntry {
|
||||
url: Option<String>,
|
||||
connected: bool,
|
||||
has_connection_backoff: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
last_error: Option<String>,
|
||||
enabled_tools: Vec<String>,
|
||||
disabled_tools: Vec<String>,
|
||||
always_allow_tools: Vec<String>,
|
||||
@@ -7380,6 +7382,10 @@ async fn list_mcp_servers(
|
||||
has_connection_backoff: pool
|
||||
.as_ref()
|
||||
.and_then(|pool| pool.has_connection_backoff(&name, config.timeouts, &server_cfg)),
|
||||
last_error: pool
|
||||
.as_ref()
|
||||
.and_then(|pool| pool.last_connect_error(&name, config.timeouts, &server_cfg))
|
||||
.map(|error| truncate_text(error, 2048)),
|
||||
enabled_tools: server_cfg.enabled_tools.clone(),
|
||||
disabled_tools: server_cfg.disabled_tools.clone(),
|
||||
always_allow_tools: server_cfg.always_allow_tools.clone(),
|
||||
@@ -7405,6 +7411,7 @@ async fn list_mcp_servers(
|
||||
.as_ref()
|
||||
.is_some_and(|pool| pool.connected_servers().contains(&name.as_str())),
|
||||
has_connection_backoff: None,
|
||||
last_error: None,
|
||||
enabled_tools: server_cfg.enabled_tools.clone(),
|
||||
disabled_tools: server_cfg.disabled_tools.clone(),
|
||||
always_allow_tools: server_cfg.always_allow_tools.clone(),
|
||||
|
||||
@@ -4547,6 +4547,31 @@ impl McpPool {
|
||||
Some(self.connect_backoff.contains_key(name))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn last_connect_error(
|
||||
&self,
|
||||
name: &str,
|
||||
timeouts: McpTimeouts,
|
||||
server: &McpServerConfig,
|
||||
) -> Option<&str> {
|
||||
if self.has_connection_backoff(name, timeouts, server) != Some(true) {
|
||||
return None;
|
||||
}
|
||||
let current = self.config.servers.get(name)?;
|
||||
if current.runtime_added != server.runtime_added
|
||||
|| (current.reviewed_plugin.is_some() && current.reviewed_plugin_source().is_none())
|
||||
|| (server.reviewed_plugin.is_some() && server.reviewed_plugin_source().is_none())
|
||||
|| server
|
||||
.reviewed_plugin_source()
|
||||
.is_some_and(|source| source.validate_before_use(name, "inspect").is_err())
|
||||
|| runtime_servers_hash(&self.dynamic_servers.read()) != self.runtime_config_hash
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let error = self.connect_backoff.get(name)?.last_error.as_str();
|
||||
(!error.is_empty()).then_some(error)
|
||||
}
|
||||
|
||||
/// The needs-auth server that owns a model tool name (`mcp_<server>_…`),
|
||||
/// if any: the server the model is trying to reach with a real tool name
|
||||
/// from a catalog built before its login lapsed. Longest configured name
|
||||
|
||||
@@ -4196,7 +4196,7 @@ pub struct SubAgentManager {
|
||||
/// agent id, so the task's failure text blames the route that failed.
|
||||
/// Taken when the task settles.
|
||||
rerouted_requests: HashMap<String, ReroutedRequest>,
|
||||
member_starts: HashMap<String, (String, String)>,
|
||||
member_starts: HashMap<(String, String), (String, String)>,
|
||||
}
|
||||
|
||||
/// One child request waiting on a person (approvals C2). The store is the
|
||||
@@ -23669,7 +23669,11 @@ pub async fn start_fleet_member(
|
||||
"prompt": &start.prompt,
|
||||
}))
|
||||
.map_err(|error| error.to_string())?;
|
||||
if let Some((accepted, agent_id)) = manager.read().await.member_starts.get(&start.operation_key)
|
||||
let operation_identity = (
|
||||
runtime.context.state_namespace.clone(),
|
||||
start.operation_key.clone(),
|
||||
);
|
||||
if let Some((accepted, agent_id)) = manager.read().await.member_starts.get(&operation_identity)
|
||||
{
|
||||
return if *accepted == request {
|
||||
Ok(agent_id.clone())
|
||||
@@ -23703,6 +23707,6 @@ pub async fn start_fleet_member(
|
||||
.write()
|
||||
.await
|
||||
.member_starts
|
||||
.insert(start.operation_key, (request, result.agent_id.clone()));
|
||||
.insert(operation_identity, (request, result.agent_id.clone()));
|
||||
Ok(result.agent_id)
|
||||
}
|
||||
+6
-1
@@ -2139,7 +2139,12 @@ This persisted project decision is separate from a thread’s `trust_mode` / Ful
|
||||
`capabilities.skill_detail` on `GET /v1/runtime/info`, which is the source a
|
||||
client should use rather than probing this path: a `404` here means "no such
|
||||
skill" and is indistinguishable from "no such route".
|
||||
- `GET /v1/apps/mcp/servers`
|
||||
- `GET /v1/apps/mcp/servers` — each row reports `connected` and
|
||||
`has_connection_backoff`; while a server is cooling down after a failed
|
||||
connect it also carries `last_error`, that attempt's diagnosis (at most 2048
|
||||
characters). `last_error` is omitted when the server is ready or connecting,
|
||||
when its configuration changed since the failure, and when its reviewed
|
||||
plugin authority no longer holds.
|
||||
- `GET /v1/apps/mcp/tools?server=<optional>`
|
||||
|
||||
Skill activation toggles are persisted under a cross-process transaction lock.
|
||||
|
||||
Reference in new issue
Block a user