--- name: ticket101-cross-talk-multi-project-rootcause description: memory note ticket101-cross-talk-multi-project-rootcause metadata: type: project --- --- slug: ticket101-cross-talk-multi-project-rootcause title: "Ticket #101 — défaut d'isolation multi-projet (registres runtime globaux par AgentId)" type: reference description: Cause racine du cross-talk entre projets ouverts simultanément (branche hybride wear-os + notification de tâche backend perdue) : les stores disque IdeA sont scoppés par projet, mais plusieurs registres mémoire runtime critiques restent indexés par AgentId seul, sans re-mint des UUID à l'ouverture. Plan de correction figé en 6 lots (clé RuntimeAgentKey). --- # Ticket #101 — défaut d'isolation multi-projet (cause racine) ## Symptômes (2 manifestations, même défaut) - **A — contamination du contexte d'inférence** : un agent a créé une pseudo-branche `feature/ticket91-wear-os-watch-sync` (ticket #91 purement front) où le suffixe `wear-os-watch-sync` appartient à l'AUTRE projet de l'utilisateur. Le nom n'apparaît dans aucun fichier `.ideai/` du projet IdeA → contamination au niveau du contexte d'inférence runtime (conversation injectée à l'agent Git mélangeait les projets). Preuve topo préservée : `main` divergé de `develop` (da907b8 vs 6a87c46), `feature/ticket99-agent-model-configuration` créée depuis main au lieu de develop. - **B — notification de tâche backend perdue / agent qui s'arrête** (sujet original #101) : un agent OpenCode lance `idea_run_in_background` puis s'arrête, sans retour de notification garanti. ## Cause racine (Architect, 2026-07-25) Stores disque **correctement scoppés par projet** (mémoire, contexte, handoff, live-state lus depuis `input.project.root` ; agents persistés dans `.ideai/agents.json` par root projet). MAIS les agents **ne re-mint pas leurs UUID à l'ouverture** → deux projets (surtout si l'un est une copie) peuvent porter les **mêmes `AgentId`**. Plusieurs **registres mémoire runtime restent globaux, indexés par `AgentId` seul** : - ConversationRegistry global + `ConversationId` dérivé des UUID agents seuls — `crates/domain/src/conversation.rs:71`, `crates/infrastructure/src/conversation/mod.rs:41`, orchestrateur `crates/application/src/orchestrator/service.rs:2192`. - Sessions PTY/structured retrouvées par `agent_id` seul — `crates/application/src/terminal/registry.rs:182,437` ; réutilisation session `service.rs:2293,2368`. - Verrous ask, busy-state, liveness, délégations différées par `AgentId` seul — `service.rs:418`, `crates/infrastructure/src/input/mod.rs:47`. - Inbox/mailbox par `AgentId` seul — `crates/domain/src/inbox.rs:68,156`, `input/mod.rs:1052`, `crates/infrastructure/src/mailbox/mod.rs:44`. - Wake par `AgentId` seul — `crates/application/src/orchestrator/wake.rs:87`, provider `crates/backend/src/lib.rs:687`. → Un agent du projet courant peut se rattacher à la conversation/session/inbox d'un autre projet (collision d'UUID) : contamination d'inférence (A) ET complétion livrable/bloquée/réveillant la mauvaise session (B). ## Pour B : runtime vs modèle La chaîne sink→`project_id`→wake est correcte jusqu'au bridge (`crates/infrastructure/src/background_task/sink.rs:23,142` ; `crates/backend/src/lib.rs:2228` recharge le projet par `project_id` puis `wake_agent`). Le défaut réapparaît juste après (inbox/mailbox/busy/session par AgentId seul). **Runtime IdeA suspecté en premier.** Le modèle GLM 5.2 n'est l'hypothèse principale que si les logs prouvent (pour le bon `project_id`) : enqueue + wake + `BackgroundTaskCompletionDelivered` émis sans reprise utile. Traces : `lib.rs:2238`, `wake.rs:93`. ## Plan de correction (6 lots, figé) 1. Introduire `RuntimeAgentKey { project_id, agent_id }` ; interdire toute map app-wide indexée par `AgentId` seul. 2. Propager la clé dans `AgentInbox`, `InputMediator`, `AgentMailbox`, busy/liveness/deferred, wake, session lookup, ask-locks. **Correctif structurel commun à A et B.** 3. Qualifier les conversations par projet : `ConversationId` + `ConversationRegistry` intègrent `ProjectId`. 4. Requalifier `TerminalSessions`/`StructuredSessions` par `(project_id, agent_id)` ; `AppWakeSessionProvider` refuse toute session vivante d'un autre projet. 5. Audit des autres états mémoire similaires (`session_limit`, tables de reprise/verrous par agent) pour éviter une demi-correction. 6. Télémétrie `project_id` explicite sur les logs diag critiques de wake/routage. ## Invariants - Sources de contexte sur disque restent root-scopées (ne pas régresser). - Templates/profils globaux restent globaux produit — ne doivent pas devenir vecteurs de session/conversation partagée. - Un projet non-actif doit continuer à recevoir ses wakes et complétions. - Pas de régression OpenCode/Codex/Claude (correctif transverse runtime, pas moteur-spécifique). - Aucun nouveau DTO frontend pour la correction minimale. ## QA — critère de vérité Test d'intégration **non-cross-talk** : 2 projets ouverts simultanément contenant **volontairement les mêmes `AgentId`** : - délégation dans projet A ne réutilise jamais une session/conversation vivante de projet B ; - complétion `(project A, agent X)` n'entre ni dans l'inbox ni dans la session de `(project B, agent X)` ; - le wake d'un projet non-actif fonctionne quand même ; - une collision d'ids qui échouait avant devient verte sur PTY, structured et background wake. ## Hors périmètre - #91 (popup de notification) : purement front, indépendant du défaut runtime. - Remint systématique des AgentId à l'ouverture : piste complémentaire non retenue dans le fix minimal (la clé runtime scellée par projet rend la collision inoffensive même sans remint). ## Topologie - Branche de travail à créer pour le fix (Git décidera). Base `develop` (6a87c46). `main`/`feature/ticket99-agent-model-configuration` divergent sur da907b8 (preuves préservées, à nettoyer après enquête). ## Lié - #91 (relatesTo) : popup de notification — front pur. - Mémoire `background-tasks-first-class-design` + `b8-command-runner-pty-framing` : design du flux de complétion (le défaut est en aval du sink). - Mémoire `mcp-bridge-and-delegation-runtime-notes` : règle rebuild AppImage (le binaire qui tourne = AppImage, pas les sources).