# Conversation par paire — cadrage d'architecture (multi-agent solide par construction)

> **Agent Architecture.** Ce document tranche le modèle qui rend le multi-agent
> **solide par construction** : l'utilisateur n'a plus à « faire les choses dans le
> bon ordre ». Aucun code de production ici — décisions, contrats (ports/entités),
> découpage en lots testables, frontière backend/frontend.
>
> **Décisions produit arbitrées (NON négociables, rappel) :** (A) conversation par
> paire = un fil entre deux parties, session propre, matérialisation paresseuse ;
> (B) entrée médiée par IdeA (le terminal xterm reste la vue de sortie brute
> INCHANGÉE, seule l'entrée change de chemin), Envoyer=enqueue / Interrompre=préempte ;
> (C) FileGuard borné aux `.md` de contexte + la mémoire, via outils MCP, verrou
> lecteurs/écrivain ; (D) zéro git, hexagonal+SOLID stricts, corrélation par ticket,
> MCP Claude-only, fix `bind_endpoint`, abandon du band-aid `\n`→`\r`.
>
> **État du terrain (lu, pas présumé).** L'essentiel des briques existe déjà :
> `domain/src/mailbox.rs` (`AgentMailbox`, `Ticket`, `TicketId`, `PendingReply`,
> `MailboxError`) ; `infrastructure/src/mailbox/mod.rs` (`InMemoryMailbox`, FIFO par
> agent + `oneshot`) ; `application/src/orchestrator/service.rs` (`ask_agent`,
> `reply`, `ensure_live_pty`, verrou de tour `ask_locks`) ; surface MCP complète
> (`mcp/tools.rs`, `mcp/server.rs`) ; transport bindé (`app-tauri/src/mcp_endpoint.rs`,
> `state.rs::bind_endpoint`/`ensure_mcp_server`/`serve_peer`). **Ce cadrage
> formalise et complète ; il ne réécrit pas.**

---

## 0. Synthèse exécutive (décisions tranchées)

1. **La conversation devient une entité de premier plan** (`Conversation` + `ConversationId`),
   absente aujourd'hui. Le couplage actuel « 1 session vivante / agent »
   (`session-registry-agent-ambiguity`) est **remplacé** par « 1 session vivante /
   **conversation** ». Un agent peut donc avoir **N sessions** simultanées (une par
   fil), mais **une seule tâche traitée à la fois** (l'entrée reste sérialisée, §B).
   C'est ce qui supprime la fuite de contexte : la délégation A→B n'emprunte plus la
   conversation User↔B.

2. **L'entrée passe par un `InputMediator`** (nouveau port application) : toutes les
   entrées (humaine **et** inter-agents) convergent vers **une file FIFO unique par
   agent**, `enqueue`/`preempt` distincts. Le terminal xterm n'écrit **plus jamais
   en direct dans le PTY** ; il devient une **vue de sortie pure**. La file existante
   (`AgentMailbox` + `ask_locks`) est **absorbée** par le `InputMediator` : la
   messagerie inter-agents n'est qu'une **source d'entrée parmi deux**.

3. **`FileGuard` (nouveau port domaine)** : un verrou lecteurs/écrivain **borné** aux
   fichiers qu'IdeA possède (`.md` de contexte d'agent + mémoire). Les agents perdent
   l'accès fs brut à ces chemins et passent par de **nouveaux outils MCP**
   `idea_context_read/propose` et `idea_memory_read/write`. Le contexte **global
   projet** est **mono-écrivain (l'orchestrateur)** ; les autres *proposent*.

4. **Détection occupé/libre = double signal avec fallback sûr** : (a) **retour-de-prompt**
   détecté par motif déclaré dans le profil CLI, (b) **signal explicite** de l'agent
   (un `idea_reply`, ou fin de tour MCP). **En cas de doute → forwarder** (on
   enqueue ; jamais piéger un message). L'occupé/libre remonte au front via un
   `DomainEvent` (Channel Tauri), pas via parsing front.

5. **Fixes durables embarqués** : `bind_endpoint` unlink déjà le socket cadavre
   (`reclaim_name(true)`, état OK — on **verrouille ce comportement par un test de
   non-régression**) ; le band-aid `\n`→`\r` et l'« injection PTV » de
   `service.rs:459` **disparaissent** (l'entrée passe désormais par le `InputMediator`,
   pas par une écriture PTY préfixée d'un orchestrateur).

6. **Garde-fous d'orchestration** : timeout par tour (déjà), **plafond d'attente en
   file** (déjà, `ASK_QUEUE_WAIT_CAP`), **détection de cycle** sur un graphe wait-for
   (nouveau, dans le domaine — pur, testable) pour refuser une délégation
   ré-entrante (A→B→A) avant deadlock.

---

## 1. Modèle de domaine

### 1.1 Nouvelles entités / VO

#### `ConversationId` (VO)
- `newtype(uuid::Uuid)`, calqué sur `TicketId`/`AgentId`. Immuable, non vide.
- **Implémenté** : `crates/domain/src/conversation.rs` (nouveau module, à exporter
  dans `lib.rs` à côté de `mailbox`).

#### `ConversationParty` (VO, enum)
```text
ConversationParty =
  | User                       // l'humain (une seule instance logique côté IdeA)
  | Agent(AgentId)             // un agent du projet
```
- Invariant : une `Conversation` relie **deux parties distinctes** (jamais
  `Agent(x)↔Agent(x)`, jamais `User↔User`).

#### `Conversation` (entité)
```text
Conversation {
  id: ConversationId,
  left: ConversationParty,
  right: ConversationParty,
  session: ConversationSession,   // état d'I/O (voir 1.2)
  resumable_id: Option<String>,   // session-id reprenable de la CLI (suspend = stocke)
}
```
- **Invariants** : `left != right` ; au plus **une** des deux parties est `User` ;
  identité d'une conversation = la **paire non ordonnée** `{left, right}` pour un
  agent donné (deux paires identiques ⇒ même conversation — clé de la matérialisation
  paresseuse). Pur, I/O-free.
- **Matérialisation paresseuse** : une `Conversation` `Agent↔Agent` n'existe en
  registre que s'il y a **au moins une tâche** ; suspendue, elle ne garde que
  `resumable_id` (pas de session vivante). C'est une **règle du `ConversationRegistry`**
  (application), pas un champ persistant lourd.

#### `ConversationSession` (VO, enum — l'état d'I/O du fil)
```text
ConversationSession =
  | Dormant                          // jamais lancée, ou suspendue (resumable_id seul)
  | Live { handle_ref: SessionRef }  // un flux d'I/O vivant (PTY ou structuré)
```
- `SessionRef` = abstraction d'un handle de session (référence vers une `TerminalSession`
  existante, cf. `domain/src/terminal.rs`). Le domaine ne tient **pas** le PTY (infra).

#### `Task` / `Ticket` (extension de l'existant)
- `Ticket` (`domain/src/mailbox.rs`) est **étendu** pour porter **l'origine** et la
  **conversation cible** :
```text
Ticket {
  id: TicketId,                       // existant
  source: InputSource,                // NOUVEAU : Human | Agent(AgentId)
  conversation: ConversationId,       // NOUVEAU : le fil dans lequel la tâche entre
  requester: String,                  // existant (label d'affichage du préfixe)
  task: String,                       // existant
}
```
- `InputSource` (VO, enum) : `Human | Agent(AgentId)`. Remplace l'actuel
  `requester: String` libre comme **source de vérité** (le `String` reste un label
  d'affichage dérivé). Permet de **propager l'identité du demandeur** (D) et
  d'alimenter le graphe wait-for (détection de cycle).
- **Compat** : `Ticket::new` garde sa signature ; on ajoute `Ticket::from_human(...)`
  et `Ticket::from_agent(source, conversation, ...)` (Open/Closed, pas de breaking).

#### File FIFO + état occupé/libre (VO)
- `AgentInbox` (concept porté par le port `InputMediator`, pas une entité persistée) :
  **une file FIFO par `AgentId`**, **une tâche en cours à la fois**.
- `AgentBusyState` (VO, enum) : `Idle | Busy { ticket: TicketId, since_ms: u64 }`.
  Dérivé, publié au front. Invariant : un agent passe à `Busy` **à l'enqueue qui
  démarre un tour** ; revient `Idle` sur **retour-de-prompt** OU **signal explicite**
  (cf. §6) ; **en cas de doute, reste `Busy`** mais la file **continue d'accepter**
  (forward, jamais bloquer l'émetteur).

#### `WaitForGraph` (VO pur — détection de cycle)
- `domain/src/conversation.rs` : structure pure `wait_edges: Vec<(AgentId, AgentId)>`
  (« A attend B »). Fonction pure `would_cycle(graph, from, to) -> bool`.
- Invariant : une `AskAgent` de `A` vers `B` est **refusée** (`MailboxError`/`AppError`
  typé) si elle crée un cycle dans le graphe d'attente (A→B alors que B→…→A).
  100 % testable sans I/O.

### 1.2 Invariants transverses

- **1 session vivante / conversation** (remplace « 1 / agent »). `session_for(conversation)`
  est déterministe ; `sessions_for_agent(agent)` peut renvoyer N (une par fil actif).
- **1 tâche traitée à la fois / agent** : l'`InputMediator` sérialise l'entrée. Deux
  fils d'un même agent partagent **la même file d'entrée** (le process CLI sous-jacent
  est unique — « 1 agent = 1 employé »). *Conséquence assumée : un agent occupé par
  son fil User retarde une délégation entrante — c'est voulu (un employé, une tâche).*
- **Séparation stricte des contextes** : écrire dans la conversation `A↔B` ne touche
  jamais `User↔B`. Garanti par le fait que la session reprise (`resumable_id`) est
  **par conversation**, pas par agent.

---

## 2. Ports (traits domaine)

> Signatures **conceptuelles**. « Consommé par » = application ; « Implémenté par » = infra/app-tauri.

### `ConversationRegistry` (NOUVEAU — domaine, `conversation.rs`)
- **Rôle** : résoudre/ouvrir paresseusement une conversation pour une paire, tenir son
  `session`/`resumable_id`, suspendre/reprendre.
```rust
trait ConversationRegistry: Send + Sync {
    /// Get-or-create paresseux : retourne le fil de la paire {a,b}, en l'ouvrant
    /// (Dormant) s'il n'existait pas. Pur registre — n'ouvre AUCUNE session.
    fn resolve(&self, a: ConversationParty, b: ConversationParty) -> Conversation;
    /// Marque une conversation Live avec la session donnée.
    fn bind_session(&self, id: ConversationId, session: SessionRef);
    /// Suspend : passe Dormant, conserve le resumable_id rendu par la CLI.
    fn suspend(&self, id: ConversationId, resumable_id: Option<String>);
    fn get(&self, id: ConversationId) -> Option<Conversation>;
}
```
- **Consommé par** : `OrchestratorService` (au lieu de `session_for_agent` brut),
  `LaunchAgent`, la reprise au redémarrage.
- **Implémenté par** : `InMemoryConversationRegistry` (infra) — `HashMap` + mutex sync,
  jamais tenu en travers d'un `.await` (cf. `ask_locks` existant).

### `InputMediator` (NOUVEAU — domaine ou application ; **décision : domaine**, `input.rs`)
- **Rôle** : le point de convergence de **toutes** les entrées d'un agent (FIFO unique),
  avec `enqueue` (Envoyer) et `preempt` (Interrompre) **distincts**, plus l'état busy.
```rust
trait InputMediator: Send + Sync {
    /// Envoyer = enqueue : ajoute la tâche en queue FIFO de l'agent, retourne le
    /// PendingReply à attendre (réutilise le type mailbox existant).
    fn enqueue(&self, agent: AgentId, ticket: Ticket) -> PendingReply;
    /// Interrompre = préempte : signale au tour en cours de s'arrêter (Échap/stop).
    /// N'est PAS un enqueue ; ne corrèle aucun ticket.
    fn preempt(&self, agent: AgentId);
    /// Marque l'agent libre (retour-de-prompt ou signal explicite) ⇒ avance la file.
    fn mark_idle(&self, agent: AgentId);
    fn busy_state(&self, agent: AgentId) -> AgentBusyState;
}
```
- **Décision frontière** : `InputMediator` **absorbe** `AgentMailbox`. Le mailbox
  existant devient le **moteur de corrélation par ticket** *interne* à
  l'implémentation du `InputMediator` (l'`InMemoryMailbox` est réutilisé tel quel, sa
  FIFO + `oneshot` sont exactement ce qu'il faut). On **n'a donc pas** deux files
  concurrentes : `ask_locks` (verrou de tour) + `InMemoryMailbox` (slots de réponse)
  sont unifiés derrière ce port. *(Voir §5 pour le chemin de migration.)*
- **Consommé par** : `OrchestratorService::ask_agent` (source = `Agent`), et le
  **nouveau** use case `SubmitHumanInput` (source = `Human`).
- **Implémenté par** : `MediatedInbox` (infra) composant `InMemoryMailbox` + le
  registre de verrous de tour + l'état busy.

### `FileGuard` (NOUVEAU — domaine, `fileguard.rs`)
- **Rôle** : verrou **lecteurs/écrivain par fichier** sur le périmètre **borné**
  (contexte `.md` + mémoire). N lecteurs OU 1 écrivain ; mono-écrivain pour le
  contexte global (l'orchestrateur).
```rust
enum GuardedResource {                  // VO — le périmètre borné, fermé
    AgentContext(AgentId),
    ProjectContext,                     // mono-écrivain : orchestrateur uniquement
    Memory(MemorySlug),
}
trait FileGuard: Send + Sync {
    async fn acquire_read(&self, who: ConversationParty, res: GuardedResource)
        -> Result<ReadLease, GuardError>;
    async fn acquire_write(&self, who: ConversationParty, res: GuardedResource)
        -> Result<WriteLease, GuardError>;
}
```
- `ReadLease`/`WriteLease` = gardes RAII (libèrent à la fin de portée). `GuardError`
  typé : `Busy` (attendre), `Forbidden` (un agent ≠ orchestrateur veut écrire
  `ProjectContext` ⇒ refus, doit *proposer*).
- **Invariant clé** : toute lecture/écriture des ressources gardées **transite par ce
  port** ; l'accès fs brut à ces chemins est retiré aux agents (cf. §3 outils MCP).
- **Consommé par** : `UpdateAgentContext`, `MemoryStore`-consumers, les nouveaux
  use cases `ReadContext`/`ProposeContext`/`ReadMemory`/`WriteMemory`.
- **Implémenté par** : `RwFileGuard` (infra) — `HashMap<GuardedResource, RwLock-like>`
  (tokio `RwLock` ou sémaphore), + la règle mono-écrivain pour `ProjectContext`.

### `AgentMailbox` (existant — **conservé**, statut révisé)
- Reste le **contrat de rendez-vous par ticket** (corrélation **par `TicketId`**, voir
  §3.3 — on **abandonne** la corrélation purement positionnelle « tête de file » dès
  qu'un agent peut avoir plusieurs fils). Devient un **détail d'implémentation** du
  `InputMediator` ; n'est plus injecté seul dans `OrchestratorService`.

### Ports inchangés réutilisés
- `PtyPort` (écriture du tour dans le PTY = désormais le **seul** chemin d'écriture,
  piloté par le `InputMediator`, plus par `ask_agent` directement).
- `ProfileStore` (porte le **motif de retour-de-prompt** par profil, §6).
- `EventBus` (publie `AgentBusyChanged`, `AgentReplied`).

---

## 3. Adapters (infra) + outils MCP

### 3.1 Adapters
| Port | Adapter | Notes |
|---|---|---|
| `ConversationRegistry` | `InMemoryConversationRegistry` | `HashMap<ConversationId, Conversation>` + index paire→id ; mutex sync. |
| `InputMediator` | `MediatedInbox` | compose `InMemoryMailbox` (existant) + verrous de tour + état busy ; publie `AgentBusyChanged`. |
| `FileGuard` | `RwFileGuard` | `RwLock` par `GuardedResource` ; règle mono-écrivain `ProjectContext`. |
| `AgentMailbox` | `InMemoryMailbox` | **inchangé** (réutilisé sous `MediatedInbox`). |

### 3.2 Nouveaux outils MCP (`infrastructure/src/orchestrator/mcp/tools.rs`)
Ajouts **purement additifs** au `catalogue()` (Open/Closed — le dispatch reste intact) :

- **`idea_context_read { target? }`** → action wire `context.read` →
  `OrchestratorCommand::ReadContext { target }`. `target` absent = le contexte **global
  projet** ; sinon le `.md` d'un agent. Passe par `FileGuard::acquire_read`.
- **`idea_context_propose { target?, content }`** → `context.propose` →
  `OrchestratorCommand::ProposeContext`. Pour un agent : écriture directe sous verrou
  écrivain. Pour le **global** : ce n'est **pas** une écriture, c'est une **proposition**
  (déposée pour validation par l'orchestrateur/UI ; `FileGuard` refuse l'écriture
  directe avec `Forbidden`).
- **`idea_memory_read { slug? }`** → `memory.read` → `ReadMemory` (sous `FileGuard`).
- **`idea_memory_write { slug, content }`** → `memory.write` → `WriteMemory` (verrou
  écrivain ; mémoire = partagée projet, cf. `shared-project-memory`).

Chaque outil suit le **patron existant** : `map_tool_call` construit un
`OrchestratorRequest`, `validate()` reste l'**unique autorité** de validation, le
`requester` du handshake porte l'identité (`ConversationParty::Agent`).

### 3.3 Corrélation `idea_reply` **par ticket** (D)
- **Changement** : aujourd'hui `idea_reply` corrèle **positionnellement** (tête de la
  file de l'émetteur — `mailbox.resolve(from, result)`). Dès qu'un agent peut avoir
  **plusieurs fils**, la tête de « sa » file est ambiguë.
- **Décision** : le préfixe injecté dans le PTY (`[IdeA · tâche de A · ticket <id>]`)
  porte **déjà** le `ticket_id`. On expose un champ **optionnel** `ticket` au schéma de
  `idea_reply` (`{ result, ticket? }`) ; quand présent, `resolve` corrèle **par
  `TicketId`** (déterministe, multi-fil) ; absent, on **retombe** sur la tête de file
  (compat agents simples, mono-fil). Le préfixe doit donc **demander à l'agent de
  renvoyer le `ticket`** (mise à jour de la description outil + protocole §B-5
  existant). `AgentMailbox::resolve` gagne une variante `resolve_ticket(agent,
  ticket_id, result)`.

---

## 4. Frontière front : vue de sortie (xterm inchangé) / entrée médiée

### 4.1 État actuel à modifier
`frontend/src/features/terminals/TerminalView.tsx` câble aujourd'hui **directement**
les frappes au PTY :
```ts
const onKey = term.onData((data) => {
  if (handle) void handle.write(encoder.encode(data));   // ← chemin à couper
});
```
C'est **exactement** le couplage que le Modèle B retire.

### 4.2 Décision frontend
1. **xterm reste la vue de sortie brute, INCHANGÉE** : `onData (PTY) → term.write`
   conservé tel quel. **Interdiction** de ressusciter `AgentChatView` (déjà supprimé
   dans le diff courant — ne pas le réintroduire).
2. **`term.onData` (frappes) n'écrit plus dans le PTY** pour une cellule **agent**.
   Deux modes :
   - **Cellule terminal simple (non-agent)** : comportement actuel conservé (écriture
     directe — pas de médiation, c'est un shell brut).
   - **Cellule agent** : les frappes vont dans un **champ de saisie géré par IdeA**
     (composant `MediatedInput`, rendu **sous** le terminal), pas dans le PTY. xterm
     passe en lecture seule pour l'entrée (sortie toujours live).
3. **Nouveau port UI `InputGateway`** (`frontend/src/ports/index.ts`) :
   ```ts
   interface InputGateway {
     submit(projectId: string, agentId: string, text: string): Promise<void>; // Envoyer = enqueue
     interrupt(projectId: string, agentId: string): Promise<void>;            // Interrompre = preempt
   }
   ```
   Adapter Tauri : `invoke("submit_agent_input", …)` / `invoke("interrupt_agent", …)`
   (nouvelles commands app-tauri → `SubmitHumanInput` / `preempt`). Mock pour tests.
4. **Occupé/libre remonte par event** : un `DomainEvent::AgentBusyChanged { agent_id,
   busy }` relayé en event Tauri (pas un Channel haute-fréquence — événement discret).
   Le `MediatedInput` désactive « Envoyer » pendant `Busy` mais **autorise toujours
   l'enqueue** (le bouton enfile derrière ; jamais bloqué — fallback « forward »), et
   active « Interrompre ». Le front **ne parse jamais** la sortie pour deviner l'état.

### 4.3 Composants/state touchés
- `features/terminals/TerminalView.tsx` : brancher le mode agent (entrée détournée).
- `features/terminals/MediatedInput.tsx` (**nouveau**) : champ + boutons Envoyer/Interrompre.
- `features/layout/LayoutGrid.tsx` : déjà route vers `TerminalView` ; ajoute le
  `MediatedInput` sous le terminal quand `agent != null`.
- `ports/index.ts` + `adapters/agent.ts` (ou nouvel `adapters/input.ts`) + mock.
- state : un store léger `agentBusy: Record<agentId, boolean>` alimenté par l'event.

---

## 5. Impact sur le code existant

### 5.1 Supprimé / retiré
- **L'écriture PTY préfixée par `ask_agent`** (`service.rs` ~459 :
  `pty.write(&handle, "[IdeA · tâche …]\n")`) **n'est plus le chemin d'entrée**. La
  tâche déléguée entre désormais par `InputMediator::enqueue` (qui, dans son impl,
  écrira la ligne dans le PTY — mais **sérialisée derrière l'entrée humaine** du même
  agent, ce qui n'était pas le cas avant). → la logique d'écriture **déménage** de
  `ask_agent` vers l'impl `MediatedInbox`.
- **Band-aid `\n`→`\r`** : abandonné (le « mode injection PTV » disparaît). Plus de
  réécriture de fin de ligne ad hoc.
- **`AgentChatView`** (front) : déjà supprimé dans le diff courant — **rester** supprimé.

### 5.2 Modifié
- **`OrchestratorService`** : ne reçoit plus `with_mailbox(mailbox, pty)` séparément
  mais `with_input_mediator(Arc<dyn InputMediator>)` + `with_conversations(Arc<dyn
  ConversationRegistry>)`. `ask_agent` devient : résoudre la **conversation A↔B**
  (paresseux), vérifier le **graphe wait-for** (refus si cycle), `enqueue` la tâche
  (source = `Agent`), `await PendingReply` borné. `reply` corrèle **par ticket** (§3.3).
  `ensure_live_pty` reste, mais branché sur `session_for(conversation)` au lieu de
  `session_for_agent`.
- **`session_for_agent`** (registre `terminal/registry.rs`) : devient
  `session_for(conversation_id)` ; `sessions_for_agent` (pluriel) ajouté. Lève
  l'ambiguïté `session-registry-agent-ambiguity` **par construction** (la clé est la
  conversation, pas l'agent).
- **`bind_endpoint`** (`state.rs`) : **déjà** `reclaim_name(true)` ⇒ unlink du cadavre.
  **Action = verrouiller par un test** (ouvrir/fermer/SIGKILL simulé/rebind sans
  `EADDRINUSE`). Pas de changement de code attendu, sauf si le test révèle un trou.
- **`idea_reply`** (tools.rs / orchestrator.rs / server.rs) : champ `ticket?` ajouté,
  `Reply { from, ticket: Option<TicketId>, result }`, `map_tool_call` le propage.
- **`Ticket`** (`mailbox.rs`) : champs `source: InputSource`, `conversation:
  ConversationId` ajoutés (constructeurs additifs).

### 5.3 Ajouté
- Domaine : `conversation.rs` (`ConversationId`, `Conversation`, `ConversationParty`,
  `ConversationSession`, `WaitForGraph`), `input.rs` (`InputMediator`, `InputSource`,
  `AgentBusyState`), `fileguard.rs` (`FileGuard`, `GuardedResource`, leases).
- Application : use cases `SubmitHumanInput`, `ReadContext`/`ProposeContext`,
  `ReadMemory`/`WriteMemory` ; détection de cycle câblée dans `ask_agent`.
- Infra : `InMemoryConversationRegistry`, `MediatedInbox`, `RwFileGuard` ; outils MCP
  `idea_context_*` / `idea_memory_*`.
- app-tauri : commands `submit_agent_input`, `interrupt_agent` ; relais event
  `AgentBusyChanged` ; câblage des nouveaux ports au composition root (`state.rs`).
- Front : `MediatedInput`, `InputGateway` + adapter + mock + store busy.

---

## 6. Détection occupé/libre

**Mécanisme retenu = double signal, OR, avec fallback sûr.**

| Signal | Source | Fiabilité |
|---|---|---|
| **Retour-de-prompt** | motif (regex/literal) déclaré dans le **profil CLI** (`AgentProfile`, nouveau champ `prompt_ready_pattern: Option<String>`), détecté sur le flux PTY par l'impl `MediatedInbox` | bon pour un shell/CLI au prompt stable ; faillible (motif dans la sortie) |
| **Signal explicite** | l'agent appelle `idea_reply` (fin d'une délégation) **ou** un signal de fin-de-tour MCP | déterministe quand l'agent coopère |

- Transition `Busy → Idle` = **premier** des deux signaux qui arrive.
- **Fallback « en cas de doute → forwarder »** : si **aucun** signal n'est sûr (motif
  absent du profil, agent muet), l'agent **reste marqué `Busy`** mais la file
  **continue d'accepter** les `enqueue` ; un message entrant **n'est jamais rejeté**,
  il patiente dans la FIFO. On ne « piège » donc jamais un message ; au pire il attend.
- **Garde-fou anti-blocage** : le timeout par tour (`ASK_AGENT_TIMEOUT`, existant)
  retire le ticket de tête et **relâche** le tour même si aucun signal n'est venu ⇒
  la file avance. L'agent reste vivant.
- Le motif vit **dans le profil** (donnée, pas code) ⇒ ajouter une CLI = éditer un
  profil (Open/Closed, cohérent §9 CLAUDE.md).

---

## 7. Découpage en lots livrables (ordonnés par dépendance)

> Chaque lot = binôme dev/test. **B = DevBackend (Rust)**, **F = DevFrontend (TS/React)**.
> Chemin critique : C1 → C2 → C3 → C4. FileGuard (C6) et front (F1/F2) parallélisables.

### Bloc Conversation (cœur — backend)
| Lot | Côté | Périmètre | Tests |
|---|---|---|---|
| **C1** | B (domaine) | `conversation.rs` : `ConversationId`, `ConversationParty`, `Conversation`, `ConversationSession`, `WaitForGraph::would_cycle`. `input.rs` : `InputSource`, `AgentBusyState`. Extension `Ticket` (source+conversation, ctors additifs). | invariants paire (left≠right, ≤1 User) ; identité = paire non ordonnée ; `would_cycle` (A→B→A refusé, A→B→C ok) ; ticket porte source+conversation. Pur, sans I/O. |
| **C2** | B (domaine+infra) | Ports `ConversationRegistry` + `InputMediator` (domaine) ; adapters `InMemoryConversationRegistry` + `MediatedInbox` (compose `InMemoryMailbox` existant). | resolve paresseux (même paire ⇒ même id) ; enqueue→PendingReply ; preempt distinct d'enqueue ; busy_state transitions ; 2 enqueue même agent sérialisés ; agents ≠ parallèles. |
| **C3** | B (application) | `OrchestratorService` : `with_input_mediator`+`with_conversations` ; `ask_agent` réécrit (résout conversation A↔B, garde wait-for, enqueue source=Agent, await) ; `reply` par ticket. `session_for(conversation)`. Retrait écriture PTY directe + band-aid `\r`. | ask A→B route dans la bonne conversation (pas User↔B) ; cycle A→B→A ⇒ erreur typée avant deadlock ; reply corrèle par ticket (multi-fil) ; reply sans ticket = fallback tête ; timeout libère file, cible vivante. |
| **C4** | B (application+app-tauri) | Use case `SubmitHumanInput` (source=Human) + commands `submit_agent_input`/`interrupt_agent` ; event `AgentBusyChanged` relayé. Câblage composition root (`state.rs`). | submit humain enfile dans la **même** FIFO que les délégations ; interrupt = preempt (pas enqueue) ; busy event émis aux bons moments ; câblage : un ask et un submit concurrents sur A sérialisent. |

### Bloc détection occupé/libre (backend)
| Lot | Côté | Périmètre | Tests |
|---|---|---|---|
| **C5** | B (domaine+infra) | Champ profil `prompt_ready_pattern` ; détection retour-de-prompt dans `MediatedInbox` ; OR avec signal explicite ; fallback « reste Busy mais accepte ». | motif détecté ⇒ Idle ; idea_reply ⇒ Idle ; ni l'un ni l'autre ⇒ Busy mais enqueue accepté ; timeout ⇒ file avance. |

### Bloc FileGuard (backend — parallélisable après C1)
| Lot | Côté | Périmètre | Tests |
|---|---|---|---|
| **C6** | B (domaine+infra) | `fileguard.rs` (port + `GuardedResource` + leases) ; `RwFileGuard` ; règle mono-écrivain `ProjectContext`. | N lecteurs concurrents OK ; 1 écrivain exclusif ; agent≠orchestrateur écrit ProjectContext ⇒ `Forbidden` ; lease RAII libère. |
| **C7** | B (application+infra MCP) | Use cases `ReadContext`/`ProposeContext`/`ReadMemory`/`WriteMemory` sous FileGuard ; outils MCP `idea_context_*`/`idea_memory_*` ; retrait accès fs brut de ces chemins. | map_tool_call → command ; validate exige `content` ; propose global ≠ write direct ; lecture concurrente non bloquante ; écriture sérialisée. |

### Bloc frontend
| Lot | Côté | Périmètre | Tests (Vitest/RTL, gateways mock) |
|---|---|---|---|
| **F1** | F | `InputGateway` (port+adapter+mock) ; `MediatedInput` (Envoyer=submit / Interrompre=interrupt) ; store busy alimenté par event. | submit appelle gateway.submit ; interrupt appelle interrupt ; busy event désactive Envoyer (mais enqueue possible), active Interrompre. |
| **F2** | F | `TerminalView` mode agent : frappes → `MediatedInput` (plus le PTY) ; xterm reste sortie live INCHANGÉE pour le non-agent. `LayoutGrid` monte `MediatedInput` sous le terminal si `agent != null`. | cellule agent ⇒ onData ne write pas le PTY ; cellule simple ⇒ comportement actuel ; sortie PTY toujours peinte ; jamais d'AgentChatView. |

### Bloc durcissement
| Lot | Côté | Périmètre | Tests |
|---|---|---|---|
| **D1** | B (app-tauri) | Test de non-régression `bind_endpoint` : bind → drop (SIGKILL simulé : laisser le fichier socket) → rebind **sans** `EADDRINUSE`. Verrouille `reclaim_name(true)`. | rebind après cadavre OK ; idempotent ; pas de fuite de fichier après close. |

**Ordre recommandé** : **C1 → C2 → C3 → C4** (cœur), **C5** après C2, **C6 → C7**
en parallèle (après C1), **F1 → F2** dès que les commands C4 existent (mock avant),
**D1** isolé n'importe quand.

---

## 8. Stratégie de tests par couche

| Couche | Type | Comment |
|---|---|---|
| **domaine** (`conversation`, `input`, `fileguard`, `mailbox` étendu) | unitaires **purs**, sans I/O ni async là où possible | invariants de paire, `would_cycle`, transitions `AgentBusyState`, ctors `Ticket`. Déterministe. C'est là que vit la garantie « solide par construction ». |
| **application** (`OrchestratorService`, `SubmitHumanInput`, use cases FileGuard) | unitaires avec **ports mockés** (fakes manuels, façon `service.rs` actuel) | ask route la bonne conversation ; cycle refusé ; reply par ticket ; submit+ask sérialisés ; FileGuard mono-écrivain. **Aucun vrai PTY/fs/MCP.** |
| **infra** (`MediatedInbox`, `RwFileGuard`, `InMemoryConversationRegistry`, outils MCP) | intégration **ciblée** | FIFO réelle + `oneshot` ; RwLock concurrence ; `map_tool_call` round-trip ; `bind_endpoint` (D1). Réutilise les tests `InMemoryMailbox` existants. |
| **app-tauri** | commands ↔ use cases | `submit_agent_input`/`interrupt_agent` mappent bien ; event `AgentBusyChanged` émis ; câblage composition root cohérent (endpoint partagé). |
| **frontend** (`MediatedInput`, `TerminalView`) | Vitest + RTL, **gateways mock** | entrée détournée hors PTY ; busy désactive Envoyer sans bloquer enqueue ; xterm sortie inchangée ; **sans backend**. |

---

## 9. Risques / points ouverts

1. **Fiabilité de la détection retour-de-prompt** (C5) — le plus dur. Un motif dans la
   sortie d'un agent peut **faussement** signaler Idle (libère trop tôt) ou ne jamais
   matcher (reste Busy). *Mitigation* : OR avec le signal explicite `idea_reply` +
   fallback « reste Busy mais accepte » + timeout par tour. *Reste ouvert* : faut-il un
   « heartbeat » MCP de fin-de-tour côté CLI ? (hors périmètre immédiat, Claude-only).

2. **Suspension/reprise de session par conversation** (`resumable_id`) — un agent à N
   fils doit reprendre **le bon** session-id par fil au redémarrage. Dépend du
   `session{assignFlag,resumeFlag}` du profil (cf. `conversation-resume-architecture`).
   *Ouvert* : capacité réelle des CLI à tenir N conversations resumables simultanées
   pour un même process « 1 agent = 1 employé » — possible conflit entre « N fils » et
   « 1 process ». **Décision de cadrage** : **1 process/agent**, les fils **partagent
   la file d'entrée** (sérialisés) ; le `resumable_id` par conversation sert surtout à
   la **reprise au redémarrage**, pas à du vrai parallélisme intra-process.

3. **Deadlock & détection de cycle** (`WaitForGraph`) — couvre A→B→A directs et
   transitifs, mais le graphe doit être **alimenté en temps réel** (arête posée à
   l'enqueue, retirée au reply/timeout). *Risque* : arête fantôme si un reply se perd
   ⇒ faux positif de cycle. *Mitigation* : retrait d'arête garanti par le RAII du tour
   (comme `_turn` aujourd'hui) + timeout.

4. **Corrélation par ticket vs agents « simples »** — un agent qui ne renvoie pas le
   `ticket` dans `idea_reply` retombe sur la corrélation positionnelle (tête de file),
   ambiguë en multi-fil. *Mitigation* : protocole §B-5 (description outil) **insiste**
   sur le renvoi du ticket ; mono-fil reste correct sans. *Ouvert* : forcer le ticket
   requis casserait des agents simples — on garde optionnel.

5. **Périmètre FileGuard contournable** — tant que l'agent garde un shell brut (PTY),
   il peut écrire les `.md`/mémoire **par le filesystem** malgré le verrou MCP. Le
   verrou n'est étanche que si l'accès fs à ces chemins est **réellement** retiré
   (sandbox, cf. `agent-permissions-architecture` / Landlock). *Ouvert* : sans sandbox
   OS, le `FileGuard` est **coopératif** (protège des collisions IdeA↔IdeA, pas d'un
   agent qui contourne). À acter : FileGuard = correction des collisions **dans le
   chemin IdeA** d'abord ; étanchéité réelle = lot sandbox ultérieur.

6. **Migration `AgentMailbox` → `InputMediator`** — risque de double-file transitoire.
   *Mitigation* : `MediatedInbox` **enveloppe** `InMemoryMailbox` (pas de réécriture),
   `OrchestratorService` bascule d'un `with_mailbox` vers `with_input_mediator` en un
   lot (C2→C3), tests existants `InMemoryMailbox` conservés verts.

---

*Document maintenu par l'Agent Architecture — cadrage « conversation par paire »,
base des lots C1→C7 / F1→F2 / D1 avant tout code.*
