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>
25 KiB
Orchestration v5 — Bind transport S-MCP + fix registre session (cadrage)
Agent Architecture. Ce document tranche le dernier kilomètre de l'orchestration native : (1) le bind transport entre une CLI MCP réellement lancée et le serveur MCP par projet (verrou §S-MCP, resté ouvert depuis M3), et (2) le fix de robustesse du registre de session (mémoire
session-registry-agent-ambiguity). Aucun code de production ici : décisions + contrats + découpage en lots testables.État du terrain (lu, pas présumé) :
infrastructure/src/orchestrator/mcp/{server,transport,jsonrpc,tools}.rs:McpServer::serve(&mut transport)boucle ligne-à-ligne surTransport::{recv,send};StdioTransport<R,W>(JSON Lines générique),MemoryTransport(tests).serven'est jamais appelé en prod.app-tauri/src/state.rs:ensure_mcp_servercrée unMcpServerpar projet et le parke sur un signal d'arrêt (McpServerHandle::start⇒let _server = server; stop_rx.recv().await;). Aucun transport, aucun pair.application/src/agent/lifecycle.rs:apply_mcp_configmatérialise la conf MCP (ConfigFile/Flag/Env) dans le run dir isolé, aprèsapply_injection, avant spawn/factory.start.mcp_server_declarationécrit un placeholder{"command":"idea","args":["mcp-server"],"transport":"stdio|socket"}.application/src/terminal/registry.rs:TerminalSessions(PTY) +StructuredSessions(IA) + agrégateurLiveSessions. Invariant « 1 session vivante/agent ».application/src/error.rs:AppError::AgentAlreadyRunning { agent_id, node_id }+ codeAGENT_ALREADY_RUNNING— défini mais jamais levé (le garde deLaunchAgentrebind/idempotent au lieu d'échouer).app-tauri/src/commands.rs::list_live_agentslit seulementterminal_sessions.live_agents()— aveugle aux sessions structurées.
0. Synthèse exécutive (décisions tranchées)
-
Transport S-MCP =
stdio-spawn: la CLI spawn elle-même un process serveur MCP fourni par IdeA. Ce sous-process est un client de boucle de retour (loopback) vers le process Tauri, pas un secondOrchestratorService. Justification : c'est le seul modèle réellement supporté par Claude Code / Codex (déclaration.mcp.json{command,args}), cross-OS sans port réseau (compatible AppImage / Windows / SSH-remote), et il résout le point dur « comment le process serveur retrouve le bon projet » par injection d'identité à l'args/envauLaunchAgent(le projet est connu à ce moment-là). Le socket est rejeté comme défaut (ports/permissions/cross-OS) mais gardé en TODO derrière le même traitTransport. -
Le binaire
idea mcp-serverest un sous-commande du binaire app-tauri existant (pas un nouveau crate/binaire à distribuer séparément) :main.rsrouteargv[1] == "mcp-server"vers un mode headless loopback qui lit stdin/stdout (JSON Lines =StdioTransport) et relaye chaque message JSON-RPC au process IdeA principal via un canal de loopback local (Unix socket / Windows named pipe par projet, créé parensure_mcp_server, chemin passé en--endpoint). LeMcpServer(qui tient l'OrchestratorService) vit dans le process Tauri ; le sous-processmcp-servern'est qu'un pont stdio↔loopback ultraléger. Un seul binaire à livrer (AppImage/setup.exe). -
Contrat conf↔serveur de bout en bout rendu cohérent :
apply_mcp_config(M1) écrit une déclaration qui pointe exactement vers ce queensure_mcp_server(M3) a mis à l'écoute —command = <exe IdeA>,args = ["mcp-server", "--endpoint", <loopback du projet>, "--project", <id>]. Fin du placeholder. -
Fix registre session = lot PRIORITAIRE et INDÉPENDANT du bind (peut/doit partir en premier) : l'invariant correct est « 1 session vivante par agent » (décision produit verrouillée, mémoire
session-registry-agent-ambiguity: un agent est un singleton, pas N instances). Le fix n'invente pas d'identité par cellule ; il durcit l'invariant sur les deux registres et réconcilie leslayouts.jsonà doublons. Trois trous concrets à boucher (cf. §3). -
Robustesse
ask: cible morte ⇒ lancement structuré puis envoi ; cible PTY brut ⇒Invalidexplicite (déjà fait) ; cible occupée par un autre tour ⇒ sérialisation FIFO par agent (nouveau, §4) ; timeout 300 s ⇒ cible vivante, erreur typée (déjà fait). Deuxasksimultanés sur la même cible ⇒ file, jamais d'entrelacement de tours. -
Frontières : le pilotage de
servevit dans l'adapter infra (McpServer+ une boucle par connexion sur le loopback du projet), supervisé parMcpServerHandle(app-tauri) qui ne fait qu'accepter les connexions et spawn une tâcheservepar pair. Aucune logique applicative ne fuit : le sous-processmcp-serverne connaît que des octets JSON-RPC ;OrchestratorServiceignore tout du transport.
1. Décision 1 — Modèle de transport S-MCP
1.1 Le point dur, posé proprement
Une CLI MCP (Claude Code, Codex) attend une déclaration de serveur de forme { "command": "...", "args": [...] } : à l'initialize, elle spawn ce process et parle JSON-RPC sur son stdin/stdout. Donc le « serveur » qu'elle voit est un process enfant à elle, distinct du process Tauri. C'est le cœur du problème : ce process enfant n'a pas l'Arc<OrchestratorService> du projet (use cases, registres de sessions, event bus vivent dans Tauri).
Deux familles de solutions :
| stdio-spawn (RETENU) | socket (rejeté en défaut) | |
|---|---|---|
| Ce que la CLI spawn | un pont idea mcp-server (stdio↔loopback) |
rien — elle se connecte à un serveur déjà à l'écoute |
Où vit l'OrchestratorService |
process Tauri (le pont relaie) | process Tauri (écoute directe) |
| Cross-OS / AppImage | ✅ pipes stdio + loopback local (Unix socket / named pipe) | ⚠️ port TCP (firewall/permissions) ou socket — déclaration CLI variable |
| Retrouver le bon projet | --endpoint/--project à l'args, fixés au LaunchAgent (projet connu) |
l'adresse encode le projet, mais la CLI doit la connaître |
| Compat Claude/Codex | ✅ {command,args} natif |
❓ support socket inégal selon CLI/version |
| Cycle de vie | la CLI possède le pont (meurt avec elle) | serveur long-vécu, connexions multiplexées |
1.2 Pourquoi stdio-spawn, malgré le sous-process en plus
- Compatibilité réelle :
{command,args}est le dénominateur commun des CLIs MCP. Le socket n'est pas universellement déclarable. - Cross-OS sans port réseau : le loopback local entre le pont et Tauri est un Unix domain socket (Linux/macOS) ou un named pipe (Windows) — déjà la techno la plus portable pour de l'IPC local, sans firewall ni permission réseau, donc AppImage-safe et SSH-remote-safe (le pont tourne côté machine de l'agent).
- Résolution du point dur par injection d'identité : le projet est connu au moment du
LaunchAgent(c'est lui qui écrit la conf MCP). On encode--endpoint <chemin loopback du projet>(+--project <id>en garde-fou) dans lesargsde la déclaration. Le pont n'a rien à deviner : il se connecte à l'endpoint du bon projet. LeMcpServercôté Tauri, lui, est déjà attaché à cetOrchestratorService/Project(créé parensure_mcp_server).
1.3 Le pont idea mcp-server (sous-commande du binaire existant)
- Pas de nouveau binaire distribué :
main.rsdétecteargv[1] == "mcp-server"avant d'initialiser Tauri/WebKit, et bascule en mode headless pont. Un seul exécutable livré (AppImage / setup.exe). - Rôle du pont :
StdioTransport(stdin, stdout)côté CLI ; un client de loopback côté Tauri. Boucle : lire une ligne JSON-RPC de la CLI → l'écrire sur le loopback → lire la réponse du loopback → l'écrire sur stdout. Zéro logique métier : c'est un tube transparent. (Optionnellement, le pont peut directement porter leMcpServersi l'OrchestratorServiceétait accessible — il ne l'est pas inter-process — d'où le relais.) - Côté Tauri :
ensure_mcp_servercrée l'endpoint loopback du projet (socket/pipe), etMcpServerHandleaccepte les connexions ; chaque connexion (= un pont = un agent) ⇒ une tâcheMcpServer::serve(&mut conn_transport)oùconn_transportenveloppe le flux loopback.McpServerest déjà branché à l'OrchestratorServicedu projet.
1.4 Identité de l'appelant (lève le requester_id = "mcp" figé)
server.rs::publish_processed tague aujourd'hui requester_id: "mcp" (placeholder). Avec stdio-spawn, le pont connaît l'agent (le LaunchAgent peut injecter --requester <agent-id> dans les args de la déclaration, comme --project). Le pont passe cette identité dans la poignée de connexion (premier message de handshake loopback, hors JSON-RPC MCP), et McpServer::serve la porte dans son contexte de connexion ⇒ OrchestratorRequestProcessed.requester_id devient l'agent réel. Observabilité UI exacte (qui a délégué à qui).
1.5 Socket = TODO derrière le même trait
Le trait Transport (jsonrpc.rs) isole déjà le serveur du transport. Un SocketTransport (TCP/HTTP-stream) reste un ajout sans toucher McpServer si une CLI l'exige. Non requis pour Claude/Codex ⇒ hors périmètre v5, documenté.
2. Décision 2 — Contrat conf injectée (M1) ↔ serveur écouté (M3/v5)
Bout en bout, un seul chemin :
profil.mcp = Some(McpCapability{ config: ConfigFile{".mcp.json"} | Flag{..} | Env{..}, transport })
│ (LaunchAgent, run dir isolé .ideai/run/<agent>/ ; après apply_injection, avant spawn)
▼
apply_mcp_config écrit la DÉCLARATION RÉELLE (fin du placeholder) :
{
"mcpServers": {
"idea": {
"command": "<chemin absolu de l'exe IdeA>", ← std::env::current_exe()
"args": ["mcp-server",
"--endpoint", "<loopback du projet>", ← fourni par ensure_mcp_server
"--project", "<project id>",
"--requester","<agent id>"] ← identité (§1.4)
}
}
}
│ (la CLI lit .mcp.json à l'initialize)
▼
la CLI SPAWN <exe IdeA> mcp-server --endpoint … --project … --requester …
│ (pont stdio↔loopback)
▼
le pont se connecte au LOOPBACK DU PROJET (créé par ensure_mcp_server)
│ (handshake : project id + requester id)
▼
McpServerHandle ACCEPTE ⇒ tâche McpServer::serve(conn) [McpServer tient l'OrchestratorService du projet]
│ tools/call → map_tool_call → OrchestratorCommand → OrchestratorService::dispatch(&project, cmd)
▼
réponse inline (idea_ask_agent ⇒ outcome.reply) renvoyée verbatim à la CLI
Invariant de cohérence à tester : le chemin de l'endpoint et l'exe écrits par apply_mcp_config sont exactement ceux que ensure_mcp_server met à l'écoute pour ce projet. Source de vérité unique : une fonction (app-tauri) calcule l'endpoint d'un ProjectId ; M1 (qui écrit la conf) et M3/v5 (qui écoute) l'appellent tous deux. Pas de chaîne dupliquée.
Le transport du profil reste surfacé dans la déclaration pour la voie socket future, mais en stdio-spawn il est implicite (la CLI spawn = stdio). McpConfigStrategy inchangé : ConfigFile écrit le fichier, Flag/Env passent le chemin de la conf (run dir) — sémantique déjà en place, on ne fait que remplir la déclaration de vrai contenu.
3. Décision 3 — Fix du registre de session (lot PRIORITAIRE, indépendant)
3.1 Invariant correct (verrouillé)
1 session vivante par agent (un agent = singleton : un .md, une conversation, un run dir, une mémoire). On ne modélise pas une identité par cellule. La cellule est une vue rebindable (§17.6). session_for_agent est donc déterministe par construction — à condition que l'invariant soit réellement enforced et que les deux registres soient considérés. Aujourd'hui il y a trois fuites :
3.2 Trou A — le garde de LaunchAgent ne lève jamais AgentAlreadyRunning
LaunchAgent::execute (lifecycle.rs ~877-920) : si l'agent a déjà une session vivante (PTY ou structurée) et qu'un node_id est demandé, il rebind silencieusement ; sans node, il rend la session existante (idempotent). C'est juste pour réattacher une vue, mais cela masque un vrai second lancement (deux cellules distinctes voulant le lancer chacune). AppError::AgentAlreadyRunning est défini mais jamais levé.
Décision : distinguer réattache de vue (légitime, rebind) de second lancement (à refuser). Le signal de discrimination existe déjà dans le flux : un LaunchAgentInput issu d'une réattache (la cellule sait que l'agent tournait : agent_was_running/conversation_id présents) vs un lancement neuf. Le garde lève AgentAlreadyRunning { agent_id, node_id_hôte } quand un lancement neuf vise un agent déjà vivant sur un autre node, et rebind seulement quand le node_id demandé est le node hôte (ou réattache explicite). L'orchestrateur spawn_agent (Visible{node_id}) suit la même règle.
3.3 Trou B — list_live_agents est aveugle aux sessions structurées
commands.rs::list_live_agents ⇒ state.terminal_sessions.live_agents() seulement. Un agent chat (structuré) vivant n'apparaît pas ⇒ l'UI ne le désactive pas dans le dropdown ⇒ on peut tenter de le relancer ailleurs.
Décision : la commande lit l'agrégateur LiveSessions::live_agents() (PTY + structuré), déjà présent dans le registre. Un seul point de vérité de liveness pour l'UI.
3.4 Trou C — layouts.json à doublons (deux feuilles, même agent)
Les layouts persistés contiennent déjà des feuilles en double sur le même agent id (constaté). À l'ouverture, ne pas auto-lancer la 2ᵉ ; réconcilier : une seule feuille reste « hôte vivant », les autres sont des vues mortes (pas de relance, pas de bannière fraîche). C'est précisément ce qui causait le symptôme (« une cellule reset au retour d'onglet »).
Décision : étape de réconciliation à l'ouverture du projet (app-tauri, jumelle de SnapshotRunningAgents) : pour chaque agent apparaissant sur N feuilles, garder une hôte, dé-flagger agent_was_running/conversation_id sur les autres feuilles dupliquées. La reprise (B, §15.2) ne relance alors qu'une session/agent.
3.5 Pourquoi indépendant du bind
Aucun de ces trois trous ne touche MCP/transport : ils vivent dans LaunchAgent, list_live_agents, et l'ouverture de projet. Le fix stabilise le routage de ask (qui s'appuie sur session_for_agent) avant d'ouvrir la vanne MCP ⇒ on évite de débugger un ask mal routé et un transport neuf en même temps. ⇒ Lot R0, livré en premier.
4. Décision 4 — Robustesse ask (sérialisation FIFO par agent)
Sémantique cible (au-dessus de l'existant) :
| Situation | Comportement |
|---|---|
| Cible inconnue | AppError::NotFound (fait) |
| Cible morte | lancement structuré (background) puis envoi (fait) |
| Cible vivante en PTY brut | AppError::Invalid explicite, jamais d'ACK trompeur (fait) |
| Cible vivante structurée, libre | rendez-vous direct send_blocking (fait) |
Cible vivante structurée, déjà en tour (autre ask) |
file FIFO par agent : le 2ᵉ ask attend son tour, pas d'entrelacement (NOUVEAU) |
| Timeout 300 s | cible vivante, erreur typée Timeout, retry possible (fait) |
Le seul vrai manque = la concurrence. Deux ask simultanés sur la même cible appelleraient send_blocking en parallèle sur la même AgentSession ⇒ deux tours entrelacés sur un moteur qui pilote une conversation. Inacceptable (cf. bug accents = writes non sérialisés).
Décision : sérialiser les tours par agent dans OrchestratorService::ask_agent via un verrou par agent_id (un Mutex/sémaphore d'unité, registre HashMap<AgentId, Arc<Mutex<()>>> détenu par le service, ou porté par l'entrée de StructuredSessions). Un ask acquiert le verrou de l'agent avant send_blocking, le relâche après le Final/timeout. Les ask concurrents forment une file naturelle (ordre d'acquisition). Le timeout s'applique au tour (pas à l'attente du verrou) — ou un timeout global borné l'attente totale (décision : timeout par tour ; l'attente en file ne consomme pas le budget, mais un plafond d'attente évite l'inanition). Cohérent avec « 1 conversation déterministe/agent ».
Erreurs typées remontées aux deux portes : MCP ⇒ tool_result_text(.., isError=true) (déjà) ; fichier ⇒ *.response.json avec le champ d'erreur (déjà via OrchestratorOutcome/watcher). Le verrou n'ajoute pas de nouveau type d'erreur ; un timeout d'attente en file ⇒ Timeout (même type).
5. Décision 5 — Frontières hexagonales (où vit serve)
McpServer::serve= adapter infra, piloté par connexion :McpServerHandle(app-tauri) accepte sur l'endpoint loopback du projet et, par pair connecté (= un agent), spawn une tâcheserve(conn). Le serveur reste sans état de connexion au-delà de l'identité du pair (portée par le contexte de connexion, §1.4).McpServerHandleévolue : aujourd'hui il parke (let _server; stop_rx.recv()). Demain il ouvre l'endpoint + boucle d'accept; à l'arrêt, ferme l'endpoint (les ponts enfants meurent avec leur CLI). Toujours non-bloquant pour open/close projet (l'acceptest async, parqué sur l'absence de pair).- Le sous-process
mcp-servervit dansapp-tauri(routemain.rs), mais ne connaît que stdio + loopback + JSON brut : zéroOrchestratorService, zéro use case. Frontière nette. - Aucune fuite applicative dans l'infra :
OrchestratorService::dispatchest appelé à l'identique par les trois portes (fichier, MCP, UI). Le verrou par agent (§4) est une règle applicative ⇒ il vit dansOrchestratorService(ou le registreStructuredSessions), pas dans l'adapter MCP.
6. Découpage en LOTS testables (méthode §3)
Ordre global : R0 (fix registre) d'abord — indépendant, débloque la robustesse de
ask. Puis bind transport M5x. Front (M5-UI) optionnel en fin.
Bloc R — Fix registre session (PRIORITAIRE, indépendant du transport)
| Lot | Côté | Périmètre | Critères de test |
|---|---|---|---|
| R0a | back (application) | Garde LaunchAgent : lever AgentAlreadyRunning{agent_id,node_hôte} pour un lancement neuf ciblant un agent déjà vivant sur un autre node (PTY ou structuré) ; rebind seulement si node demandé = node hôte / réattache. Même règle dans OrchestratorService::spawn_agent. |
lancement neuf d'un agent vivant ailleurs ⇒ AgentAlreadyRunning (code AGENT_ALREADY_RUNNING) ; réattache même node ⇒ rebind sans respawn ; idempotence background inchangée ; ask d'une cible morte ⇒ lancement OK (pas de faux positif). |
| R0b | back (app-tauri) | list_live_agents lit LiveSessions::live_agents() (PTY + structuré). |
un agent chat vivant apparaît dans la liste ; un agent PTY aussi ; aucun doublon ; sans session ⇒ vide. |
| R0c | back (app-tauri) | Réconciliation à l'ouverture projet : pour un agent sur N feuilles, garder une hôte, dé-flagger agent_was_running/conversation_id sur les autres. |
layout à doublons ⇒ après ouverture, une seule feuille « était en cours » pour l'agent ; layout sans doublon inchangé ; persistance idempotente (2ᵉ ouverture = no-op). |
| R0d | front | Dropdown agent du leaf : désactiver/"déjà placé" via la liste R0b (PTY+chat) ; gérer le retour AGENT_ALREADY_RUNNING (aller-à / déplacer). |
Vitest : agent vivant ailleurs ⇒ option désactivée + action « aller à la cellule » ; erreur backend mappée à un message clair. |
Bloc M5 — Bind transport S-MCP (stdio-spawn)
| Lot | Côté | Périmètre | Critères de test |
|---|---|---|---|
| M5a | back (app-tauri) | Endpoint loopback par projet : fonction unique mcp_endpoint(project_id) (Unix socket / Windows named pipe) ; ensure_mcp_server l'ouvre, stop_orchestrator_watch le ferme. |
endpoint créé à l'open, supprimé au close ; idempotent (1/projet) ; chemin déterministe par ProjectId ; pas de collision inter-projets. |
| M5b | back (app-tauri) | Sous-commande mcp-server dans main.rs (avant init Tauri) : pont StdioTransport(stdin,stdout) ↔ loopback (--endpoint), handshake (--project,--requester). |
argv mcp-server ⇒ mode headless, ne lance pas la webview ; relaie une requête JSON-RPC ligne→loopback→réponse→stdout ; EOF stdin ⇒ sortie propre ; endpoint absent ⇒ erreur non-zéro, jamais de hang. |
| M5c | back (infra/app-tauri) | McpServerHandle accepte sur l'endpoint et spawn McpServer::serve(conn) par pair ; identité du pair (requester) portée au contexte de connexion ⇒ OrchestratorRequestProcessed.requester_id = agent réel (fin du "mcp" figé). |
une connexion ⇒ une tâche serve ; initialize/tools/list/tools/call OK de bout en bout via le loopback (test d'intégration local, hors réseau) ; requester_id = l'agent ; déconnexion d'un pair n'affecte pas les autres ; arrêt ferme l'endpoint + termine les serve. |
| M5d | back (application) | apply_mcp_config écrit la déclaration RÉELLE (fin placeholder) : command = current_exe, args = ["mcp-server","--endpoint",mcp_endpoint(project),"--project",id,"--requester",agent]. ConfigFile non-clobbering ; Flag/Env portent le chemin de conf. Source d'endpoint partagée avec M5a. |
mcp=None ⇒ aucune écriture (inchangé) ; ConfigFile ⇒ .mcp.json pointe l'exe + endpoint exacts du projet ; endpoint identique à ensure_mcp_server (test de cohérence M1↔M3) ; non-clobbering. |
| M5e | back (intégration) | Smoke end-to-end loopback (sans CLI réelle) : un faux pont écrit tools/call idea_list_agents/idea_ask_agent sur le loopback d'un projet et reçoit la réponse inline du dispatch réel. |
idea_list_agents ⇒ JSON des agents ; idea_ask_agent vers une cible structurée ⇒ reply inline ; cible PTY ⇒ erreur typée ; JSON-RPC malformé ⇒ erreur, jamais panic ; hors réseau. |
Bloc A — Robustesse ask (concurrence)
| Lot | Côté | Périmètre | Critères de test |
|---|---|---|---|
| A0 | back (application) | Sérialisation FIFO par agent dans ask_agent : verrou par agent_id autour de send_blocking ; timeout par tour ; plafond d'attente en file. |
deux ask concurrents sur la même cible ⇒ tours séquentiels (pas d'entrelacement), ordre FIFO ; un ask sur agent A et un sur agent B ⇒ parallèles ; timeout d'un tour laisse la cible vivante et libère la file ; plafond d'attente ⇒ Timeout typé. |
Optionnel
| Lot | Côté | Périmètre | Critères de test |
|---|---|---|---|
| M5-UI | front | Badge source (mcp/file) + requester réel sur une délégation (réutilise OrchestratorRequestProcessed). |
badge source correct ; requester = agent réel (plus « mcp ») ; pas de régression sans event. |
Ordre recommandé : R0a→R0b→R0c→R0d (stabilise le routage), puis A0 (concurrence ask, ne dépend pas du transport), puis M5a→M5b→M5c→M5d→M5e (bind), puis M5-UI. R0 et A0 sont livrables sans toucher MCP ; M5 ne doit partir qu'après R0 (sinon on débugge ask mal routé + transport neuf ensemble).
7. Conformité hexagonale & SOLID (rappel)
- Règle de dépendance : JSON-RPC, stdio, loopback socket/pipe, sous-process
mcp-server= infra/app-tauri exclusivement.domain/applicationignorent MCP et le transport. Le verrou FIFO par agent est une règle applicative (vit dansOrchestratorService/registre), pas dans l'adapter. - S :
McpServer= traduire JSON-RPC →dispatch; le pont = relayer des octets ;McpServerHandle= cycle de vie +accept; le verrou = sérialiser les tours. Aucune responsabilité fourre-tout. - O : un
SocketTransportfutur = un impl deTransporten plus, zéro modif deMcpServer. Une CLI MCP de plus = un profil avecmcp = Some(..), zéro code. - L : les trois portes (fichier, MCP, UI) sont substituables derrière
OrchestratorService::dispatch— un comportement applicatif. - I :
McpServerHandlene voit que « accepter + servir » ;OrchestratorServicene voit quedispatch+ registres ; l'UI ne voit queLiveSessions::live_agents.