feat(agent): robustesse routage ask — fix registre session + concurrence (R0+A0) — §14.3/§15

Stabilise le routage de `ask` avant le bind transport MCP (v5). Invariant
« 1 agent = 1 employé » durci ; un agent traite un tour à la fois.

- R0a garde LaunchAgent : lève AgentAlreadyRunning pour un lancement neuf
  ciblant un agent déjà vivant sur un autre node (PTY + structuré) ; rebind
  seulement même-node ou réattache explicite (conversation_id). Idem spawn_agent.
- R0b list_live_agents agrège PTY + structuré (LiveSessions) + dédup.
- R0c réconciliation des layouts.json à doublons à l'ouverture (host déterministe,
  idempotent) — corrige « une cellule reset au retour d'onglet ».
- R0d UI : option agent désactivée si vivant ailleurs + « aller à la cellule »,
  mapping AGENT_ALREADY_RUNNING.
- A0 sérialisation FIFO des tours par agent_id dans ask_agent (verrou tokio par
  agent ; agents différents en parallèle ; timeout tour 300s, cap attente 600s).

Cadrage : .ideai/briefs/orchestration-v5-transport-bind-cadrage.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 17:34:57 +02:00
parent 37e72747d3
commit 6ca519b815
21 changed files with 2402 additions and 85 deletions

View File

@ -1349,11 +1349,19 @@ pub struct LiveAgentListDto(pub Vec<LiveAgentDto>);
impl LiveAgentListDto {
/// Builds the wire list from the registry's `(AgentId, NodeId, SessionId)` tuples.
///
/// De-duplicates by `agent_id`: the "one live session per agent" invariant
/// guarantees an agent is live in at most one registry (PTY **or**
/// structured), but the aggregated input could in theory carry the same agent
/// twice; the UI contract is a dup-free set (each agent appears once), so we
/// keep the first occurrence and drop any later one for the same agent.
#[must_use]
pub fn from_pairs(pairs: Vec<(AgentId, NodeId, domain::SessionId)>) -> Self {
let mut seen = std::collections::HashSet::new();
Self(
pairs
.into_iter()
.filter(|(agent_id, _, _)| seen.insert(*agent_id))
.map(|(agent_id, node_id, session_id)| LiveAgentDto {
agent_id: agent_id.to_string(),
node_id: node_id.to_string(),