fix(agent): timeout MCP OpenCode aligné sur le plafond du rendez-vous (#95)

Le timeout hardcoded 15000ms (15s) dans le bloc mcp.idea de l'opencode.json
généré par IdeA était far too short pour idea_ask_agent, qui bloque pendant
que l'agent cible travaille. OpenCode tuait la requête à 15s avec l'erreur
-32001 « Request timed out », même si la cible répondait correctement
(visible dans le workstate). Claude/Codex n'étaient pas affectés car leur
config MCP n'a pas de champ timeout.

Désormais le timeout est résolu par resolve_opencode_mcp_timeout_ms() :
- défaut 4h (14_400_000 ms), aligné sur DEFAULT_RENDEZVOUS_CEILING ;
- overridable via IDEA_OPENCODE_MCP_TIMEOUT_MS ;
- fallback sûr sur 0/parse-error (jamais d'expiration instantanée).

Les 4 occurrences (lifecycle.rs x2 + assistant/mod.rs x2) utilisent la même
fonction exportée depuis application::agent. Aucune modification des configs
MCP Claude/Codex.
This commit is contained in:
2026-07-24 15:00:53 +02:00
parent 7d3e74a114
commit 6e9a3ff657
3 changed files with 31 additions and 5 deletions

View File

@ -2645,6 +2645,31 @@ fn mcp_server_wiring(
///
/// The MCP server is named `idea`; OpenCode therefore exposes IdeA tools as
/// `idea_idea_*` (`<serverName>_<toolName>`), matching the observed contract.
/// Default MCP request timeout (milliseconds) injected into the OpenCode agent
/// config (`opencode.json` → `mcp.idea.timeout`). OpenCode applies this **per
/// `tools/call`** to its MCP servers; the original hardcoded `15_000` (15 s) was
/// far too short for `idea_ask_agent`, which blocks while the target agent works
/// (potentially minutes). Aligning on the rendezvous ceiling (4 h) ensures
/// OpenCode's client never kills an in-flight delegation before the server-side
/// watchdog resolves.
///
/// **Must stay ≥ `DEFAULT_RENDEZVOUS_CEILING`** so the MCP client timeout and the
/// server-side absolute ceiling expire together — never the client first.
/// Overridable via `IDEA_OPENCODE_MCP_TIMEOUT_MS`.
pub const DEFAULT_OPENCODE_MCP_TIMEOUT_MS: u64 = 4 * 60 * 60 * 1000;
/// Resolves the effective OpenCode MCP request timeout (ms) from the
/// `IDEA_OPENCODE_MCP_TIMEOUT_MS` env var, falling back to
/// [`DEFAULT_OPENCODE_MCP_TIMEOUT_MS`]. Parse failure or `0` ⇒ default, so a
/// misconfigured value never collapses the timeout to an instant kill.
pub fn resolve_opencode_mcp_timeout_ms() -> u64 {
std::env::var("IDEA_OPENCODE_MCP_TIMEOUT_MS")
.ok()
.and_then(|v| v.trim().parse::<u64>().ok())
.filter(|&ms| ms > 0)
.unwrap_or(DEFAULT_OPENCODE_MCP_TIMEOUT_MS)
}
fn opencode_config_json(
config: &domain::profile::OpenCodeConfig,
project_root: &str,
@ -2725,7 +2750,7 @@ fn opencode_config_json(
"command": command_array,
"cwd": project_root,
"enabled": true,
"timeout": 15000
"timeout": resolve_opencode_mcp_timeout_ms()
}
}),
);
@ -2808,7 +2833,7 @@ fn opencode_provider_config_json(
"command": command_array,
"cwd": project_root,
"enabled": true,
"timeout": 15000
"timeout": resolve_opencode_mcp_timeout_ms()
}
}),
);

View File

@ -40,7 +40,8 @@ pub use lifecycle::{
ListAgentsOutput, LiveStateLeanProvider, McpRuntime, PermissionProjectorRegistry,
ProviderSessionProvider, ReadAgentContext, ReadAgentContextInput, ReadAgentContextOutput,
StructuredRoutingMode, StructuredSessionDescriptor, UpdateAgentContext,
UpdateAgentContextInput, AGENT_MEMORY_RECALL_BUDGET, LIVE_STATE_INJECT_MAX,
UpdateAgentContextInput, DEFAULT_OPENCODE_MCP_TIMEOUT_MS, AGENT_MEMORY_RECALL_BUDGET,
LIVE_STATE_INJECT_MAX, resolve_opencode_mcp_timeout_ms,
};
pub use resume::{
ListResumableAgents, ListResumableAgentsInput, ListResumableAgentsOutput, ResumableAgent,

View File

@ -407,7 +407,7 @@ fn opencode_config_json(
"command": command_array,
"cwd": project_root,
"enabled": true,
"timeout": 15000
"timeout": application::agent::resolve_opencode_mcp_timeout_ms()
}
}),
);
@ -492,7 +492,7 @@ fn opencode_provider_config_json(
"command": command_array,
"cwd": project_root,
"enabled": true,
"timeout": 15000
"timeout": application::agent::resolve_opencode_mcp_timeout_ms()
}
}),
);