# LS8 — Clôture du programme *live-state / persistance conversationnelle* > **Statut : LIVRÉ.** Lots LS1→LS7 mergés dans `develop` @ `fd7adbb`. Ce document est la **référence durable** du programme : ce qui est acquis, la cartographie des **4 stores**, le flux d'injection au lancement, la séparation chemin chaud / chemin froid, la règle de frontière gravée, et ce qui **reste ouvert**. > > Propriété : Agent Architecture. Fait foi avec `ARCHITECTURE.md` §18 (modules livrés), §19 (cadrage persistance, désormais livré) et §21 (cartographie de clôture). ADR lié : [`docs/adr/LS5-handoff-summary-bound-and-llm-seam.md`](adr/LS5-handoff-summary-bound-and-llm-seam.md). --- ## 1. Objet du programme Donner à un agent IA, **sans dégrader ses performances**, le contexte minimal pour : 1. **se coordonner** avec les autres agents du projet (qui fait quoi *maintenant*) — *live-state* ; 2. **reprendre** un fil de travail au redémarrage ou après un swap de profil, indépendamment du `resumable_id` propre au provider — *handoff cross-profile incrémental* ; 3. **conserver** un transcript humain fidèle, sans croissance disque non bornée — *log canonique + rotation* ; 4. **relire** ce transcript dans l'UI, par paire d'agents — *viewer LS7*. Principe directeur (priorité produit n°1) : **efficacité agent d'abord**. Tout ce qui touche le contexte injecté est **borné/distillé** ; le résumé n'appelle **jamais** un LLM sur le chemin chaud ; la rétention ne ralentit **jamais** l'append. --- ## 2. Acquis lot par lot (LS1→LS7) | Lot | Livré | Emplacement | |---|---|---| | **LS1** | Modèle live-state pur : `LiveState`/`LiveEntry`/`WorkStatus`, port `LiveStateStore`. Keyed **last-writer-wins** (jamais d'append), champs free-text bornés (soft-trunc `FIELD_PREVIEW_MAX_CHARS=160`, hard-reject `FIELD_MAX_BYTES=2 KiB`), `prune(now, ttl, max_n)`. | `domain/src/live_state.rs`, port dans `domain/src/ports.rs` | | **LS2** | Use cases `UpdateLiveState` (estampille via `Clock`, applique les bornes domaine) et `GetLiveStateLean` (**prune-on-read** TTL+max, renvoie le DTO maigre `LeanLiveState`). Adapter `FsLiveStateStore` (`live-state.json`, écriture atomique tmp+rename, fichier absent ⇒ état vide). | `application/src/workstate/live.rs`, `infrastructure/src/store/live_state.rs` | | **LS3** | Auto-update **dérivé** du cycle de délégation (zéro token agent, zéro appel modèle) : `ask` accepté ⇒ cible `Working` ; `reply` rendu ⇒ cible `Done` (+ `last_delegation`). **Best-effort strict** : un échec n'échoue jamais la délégation. Provider par root `LiveStateProvider`. | `application/src/orchestrator/service.rs`, wiring `app-tauri/src/state.rs` | | **LS4** | (a) Injection bornée d'une section `# État du projet` dans le convention file au lancement — **entre** `# Mémoire projet` et `# Reprise de la conversation`, cap `LIVE_STATE_INJECT_MAX`, exclut l'agent lancé, ordre manifeste, vide ⇒ section omise. (b) Outils MCP `idea_workstate_read` (lecture lean enrichie du nom) et `idea_workstate_set` (écrit la ligne de **l'agent courant** via identité handshake, jamais celle d'un autre). **Catalogue MCP 12 → 14.** Clôt le cold-start de coordination. | `application/src/agent/lifecycle.rs`, `domain/src/orchestrator.rs`, `infrastructure/src/orchestrator/mcp/tools.rs`, `app-tauri/src/state.rs` | | **LS5** | Borne du `summary_md` du handoff (le gain de perf central) : fn pure domaine `bound_handoff_summary` + `HANDOFF_SUMMARY_MAX_CHARS=4096`, borne par tour `TURN_LINE_MAX_CHARS=240` dans le résumeur. Appliquée **à l'écriture** (`RecordTurn`, après `fold`) **et défensivement à l'injection** (`resolve_handoff`, protège les `handoff.md` legacy). Stratégie de dépassement = **troncature distillée** (objectif + tours les plus récents), jamais re-fold ni rejet, jamais bloquant. **Seam LLM durci mais NON activé** : défaut runtime = `HeuristicHandoffSummarizer`. | `domain/src/conversation_log.rs`, `infrastructure/src/conversation_log/summarizer.rs`, `application/src/conversation/record.rs`, `application/src/agent/lifecycle.rs`. ADR : `docs/adr/LS5-…` | | **LS6** | Rétention/rotation du `log.jsonl` : **archive segmentée** (`log.jsonl` actif + `log.N.jsonl` anciens), **hors chemin chaud** (use case déclenché à la reprise, best-effort, idempotent ; l'`append` ne déclenche jamais la rotation). Politique pure `rotation_plan`, seuils `ROTATE_AFTER_TURNS=500` / `ROTATE_AFTER_BYTES=1 MiB` / `MAX_ARCHIVE_SEGMENTS=20`. Port `ConversationArchive` (`stats`/`rotate`/`page`) + use cases `RotateConversationLog` / `ReadConversationPage` + commande Tauri `read_conversation_page`. | `domain/src/conversation_log.rs`, `infrastructure/src/conversation_log/mod.rs`, `application/src/conversation/{rotate,paginate}.rs`, `app-tauri` | | **LS7** | Viewer humain **fil-par-paire** (frontend pur, lecture seule) : gateway `ConversationGateway.readPage` + adapter Tauri + mock, drill-down depuis le **Work panel**, swap du main-area en **état local** (pas de nouveau `kind` backend). | `frontend/src/{ports,adapters,domain}`, `frontend/src/features/conversations/`, intégration `ProjectsView.tsx` | --- ## 3. Cartographie des 4 stores (frontière gravée) Quatre stockages **disjoints**, chacun avec une nature, une surface et un cycle de vie propres. **Aucun** ne se substitue à un autre ; un seul (le handoff distillé) franchit vers un contexte agent. | Store | Fichier(s) | Nature | Surface | Versionné ? | Injecté à un agent ? | |---|---|---|---|---|---| | **Mémoire projet** | `.ideai/memory/*.md` + `MEMORY.md` | Savoir projet **stable, low-noise**, curé | Agent **et** humain | **Oui** (savoir projet) | **Oui** — index/hooks (pointeurs), §14.5.4 | | **Handoff** | `.ideai/conversations//handoff.md` | Reprise par fil = **dérivé distillé borné** (≤ 4096 chars) | Agent (distillé) | Non (runtime) | **Oui** — section `# Reprise de la conversation`, **bornée** (LS5) | | **Transcript canonique** | `.ideai/conversations//log.jsonl` (+ `log.N.jsonl`) | Journal **append-only riche**, source de vérité, volumineux/bruité | **Humain uniquement** | Voir §7 (point ouvert) | **JAMAIS** — interdit d'injection | | **Live-state** | `.ideai/live-state.json` | Coordination **transitoire maigre** (« qui fait quoi maintenant »), keyed LWW, prune TTL+max | Agent (maigre) + humain (Work panel) | **Non** — gitignoré, reconstruit au runtime | **Oui** — section `# État du projet`, **bornée** (LS4) | `providers.json` (resumable_id par provider, §19.7) complète le dossier conversation mais n'est pas une « mémoire » : c'est l'optimisation de reprise non portable, rangée à côté du handoff portable. **Règle de frontière (réaffirmée)** : - **Surface AGENT** = *borné / distillé / pointeur*. Live-state lean, handoff borné, index mémoire (pointeurs) — jamais de corps brut, jamais de transcript. - **Surface HUMAINE** = *riche*. Transcript complet, viewer LS7 — jamais soumis aux bornes agent. - **Le transcript append-only ne franchit JAMAIS vers un contexte agent.** Seul un **dérivé distillé** (le handoff) passe la frontière, et il est borné des deux côtés (écriture + injection). --- ## 4. Flux d'injection au lancement (`compose_convention_file`) Le convention file généré dans le run dir isolé de l'agent (`.ideai/run//`, §14.1) est assemblé **dans cet ordre** (sections vides omises) : ``` # Project root # Orchestration IdeA (prose adaptée surface MCP vs fichier ; brief capacités) # Skills disponibles (MCP : affordances ; sinon dump en fin de fichier) # Contexte projet (.ideai/CONTEXT.md si présent) # Mémoire projet (index/hooks — pointeurs, §14.5.4) # État du projet (LIVE-STATE, LS4 — lean, cap LIVE_STATE_INJECT_MAX, self exclu) # Reprise de la conversation (HANDOFF, LS5 — summary_md borné ≤ HANDOFF_SUMMARY_MAX_CHARS) ``` Toutes les sections agent-facing sont **bornées et distillées**. Les deux dernières (live-state, handoff) sont les plus situationnelles : *où en sont les autres* puis *où en est ce fil*. --- ## 5. Chemin chaud vs chemin froid La priorité perf se matérialise par une séparation stricte : **Chemin chaud** (latence ressentie par la délégation — doit rester cheap) : - `ConversationLog::append` d'un tour (`RecordTurn`) : append + `fsync`, **O(1)**, jamais de rotation. - `HandoffSummarizer::fold` incrémental (seulement le tour neuf, jamais de relecture totale) + `bound_handoff_summary` (pure, déterministe) avant `save`. - Auto-update live-state (LS3) : un seul `upsert` keyed, **best-effort**, déclenché uniquement sur les transitions `ask`/`reply` (jamais par tour/outil ⇒ pas de write-storm). - **Aucun appel LLM** sur ce chemin (seam non activé, défaut heuristique). **Chemin froid** (action humaine ou maintenance — peut être plus coûteux) : - Rotation `RotateConversationLog` : déclenchée **à la reprise/ouverture**, best-effort, idempotente. - Lecture paginée `read_conversation_page` (viewer LS7) : action humaine, archive-aware. - `GetLiveStateLean` (prune-on-read) à l'injection / via `idea_workstate_read`. **Invariant de cohérence INV-LS6** : la rotation n'élague/ne déplace **jamais** un tour d'id ≥ `up_to` du handoff courant ; le tour `up_to` et tous les postérieurs restent dans le segment **actif**. D'où `ConversationLog::read(since=up_to)` (fold incrémental) et la reprise restent corrects après toute rotation. Sans handoff (ou `up_to` nil) ⇒ aucune rotation. --- ## 6. Outils MCP (catalogue = 14) `idea_list_agents`, `idea_ask_agent`, `idea_reply`, `idea_launch_agent`, `idea_stop_agent`, `idea_update_context`, `idea_create_skill` (7 base) · `idea_context_read`, `idea_context_propose`, `idea_memory_read`, `idea_memory_write` (4 FileGuard C7) · `idea_skill_read` (skill-awareness) · `idea_workstate_read`, `idea_workstate_set` (LS4). Tous mappés 1:1 vers un `OrchestratorCommand` et servis par le **même** `OrchestratorService::dispatch` (fichier · MCP · UI). `idea_workstate_set` n'écrit **que** la ligne de l'agent courant, via l'identité **handshake** (jamais un argument modèle). --- ## 7. Ce qui reste ouvert (hors périmètre du programme) Aucun de ces points ne bloque la clôture ; ils sont listés pour la suite. **Issus du programme (différés à dessein)** : 1. **Activation réelle du seam LLM** (`LlmHandoffSummarizer`) — *si un jour souhaité*. Contrat figé (ADR LS5) : déclenchement **froid/hors chemin** (reprise/rotation/débounce, jamais par tour), timeout + **fallback heuristique**, sélection par flag, **défaut heuristique**. Le défaut runtime reste l'heuristique borné ; rien à activer tant que le besoin n'est pas exprimé. 2. **Balayage périodique de rotation** — aujourd'hui la rotation est déclenchée à la reprise/ouverture (idempotente). Un *sweep* de fond optionnel n'est pas câblé ; `rotation_plan` étant idempotent, il se branchera sans changement de contrat le jour voulu. 3. **Discordance doc/réalité sur le gitignore des conversations** — D19-4 fige `.ideai/conversations/` comme **gitignoré** (état d'exécution), mais `.gitignore` ne l'exclut pas et des `log.jsonl`/`handoff.md` sont actuellement **suivis** dans ce dépôt de dogfood. À trancher par Git/Main : soit aligner `.gitignore` sur D19-4, soit acter que le dépôt IdeA versionne ses propres logs comme artefacts (et amender D19-4). **Décision produit, hors lot doc.** **Chantiers hors-programme** (toujours listés dans la mémoire `remaining-work-idea-agent-control-ide`) : 4. **Intégration MCP réelle end-to-end UX** : le transport natif M5 est vivant (§18.5), mais le parcours utilisateur complet (déclaration profil → pont → outils visibles → observabilité UI des délégations) reste à durcir/valider e2e. 5. **Registre de sessions / singleton agent** : l'invariant « 1 agent = 1 session vivante » est livré et gardé (§18.6) ; les évolutions multi-fenêtres / déplacement d'onglet (§L10) restent ouvertes. 6. **Auto-update mémoire/contexte pendant la vie de l'agent** : la mémoire est injectée **au lancement** (§14.5.4) ; un rafraîchissement *en cours de session* (re-recall, re-injection de contexte sans relance) n'est pas couvert et reste un chantier produit distinct. --- ## 8. Conformité hexagonale & SOLID (rappel) - **Domaine pur** : `live_state.rs` et `conversation_log.rs` (value objects, ports, `rotation_plan`, `bound_handoff_summary`, `WorkStatus::parse`) — zéro I/O, zéro `tokio`. - **Application** : use cases orchestrant les ports (`UpdateLiveState`, `GetLiveStateLean`, `RecordTurn`, `RotateConversationLog`, `ReadConversationPage`). - **Infrastructure** : adapters FS (`FsLiveStateStore`, `FsConversationLog`/`ConversationArchive`, `FsHandoffStore`) + résumeur heuristique. `HandoffSummarizer` = **OCP** (heuristique ↔ LLM substituables sans toucher l'application). - **Présentation** : outils MCP (driving adapter), commande Tauri `read_conversation_page`, viewer React via gateway/adapter/mock. Le programme **n'a ajouté aucun couplage** vers la présentation/infrastructure depuis le domaine : la règle de dépendance (Présentation → Application → Domaine ← Infrastructure) tient.