feat(orchestrator): backstop no-reply du rendez-vous inter-agents

Remédiation du wedge persistant après échec live du fix Finding A (77e62e5).
Détecte la fin de tour d'un agent sollicité qui n'a pas appelé idea_reply et
débloque l'agent demandeur au lieu de le laisser en attente indéfinie.

Ajoute le suivi de tour côté inspector Claude (claude_turn_watcher) et la
résolution des chemins de session (claude_paths), câblés dans le rendez-vous
idea_ask_agent ⇄ idea_reply.

Build workspace vert, suite complète verte, zéro warning.
Backstop NON prouvé levé en live : merge develop interdit tant que la levée
du wedge n'est pas validée en conditions réelles.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 15:51:59 +02:00
parent 2ef5628c72
commit 744de20f4b
20 changed files with 867 additions and 1093 deletions

View File

@ -41,17 +41,22 @@ const MCP_PROTOCOL_VERSION: &str = "2024-11-05";
/// layer). The application layer already bounds the rendezvous itself, but this
/// adapter adds its own **finite** outer bound so a wedged target can never park a
/// `tools/call` task forever: on expiry the call returns a clean JSON-RPC error
/// instead of hanging. Generous on purpose (delegated turns can be very long), but
/// never infinite. Every other tool (`idea_reply`, `idea_list_agents`, …) is left
/// untouched — they don't rendezvous.
const ASK_RENDEZVOUS_TIMEOUT: Duration = Duration::from_secs(24 * 60 * 60);
/// instead of hanging.
///
/// **Plancher universel FINI (backstop no-reply).** The real turn-end signal now exists
/// (the transcript `turn_duration` watcher), but only Claude emits it; this outer bound
/// stays the last resort for any silent target that never emits it (Codex & co) or that
/// wedges. It must therefore NEVER be (quasi-)infinite. 600 s by default, overridable per
/// project via `IDEA_ASK_RENDEZVOUS_TIMEOUT_MS`. Every other tool (`idea_reply`,
/// `idea_list_agents`, …) is left untouched — they don't rendezvous.
const ASK_RENDEZVOUS_TIMEOUT: Duration = Duration::from_secs(600);
/// Resolves the effective `idea_ask_agent` rendezvous bound from an optional
/// configured override in **milliseconds** (project/profile level), mirroring the
/// application's [`resolve_turn_timeout`](application) shape: `Some(ms)` ⇒ that bound,
/// `None`/absent ⇒ the generous [`ASK_RENDEZVOUS_TIMEOUT`] default (statu quo, zero
/// regression). `Some(0)` is treated as "no override" so a misconfigured zero can never
/// collapse the safety net to an instant expiry. Pure and unit-testable without wiring.
/// `None`/absent ⇒ the finite [`ASK_RENDEZVOUS_TIMEOUT`] default (600 s). `Some(0)` is
/// treated as "no override" so a misconfigured zero can never collapse the safety net to
/// an instant expiry. Pure and unit-testable without wiring.
#[must_use]
pub fn resolve_ask_rendezvous_timeout(override_ms: Option<u32>) -> Duration {
match override_ms {