docs(live-state): clôture programme persistance/live-state (LS8)

Documentation de clôture du programme live-state/persistance (LS1→LS7, livré @ fd7adbb) :
- docs/LS8-live-state-persistence-closure.md : référence durable (4 stores, flux
  d'injection, chemin chaud/froid, règle de frontière, points ouverts).
- ARCHITECTURE.md : §19 marqué LIVRÉ, §14.1 items 7-8 (live-state + handoff),
  §18.5 catalogue MCP de 14 outils, nouvelle §21 (cartographie de clôture).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 17:53:02 +02:00
parent fd7adbb8a0
commit 36be0cb396
2 changed files with 158 additions and 5 deletions

View File

@ -0,0 +1,123 @@
# 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/<id>/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/<id>/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/<id>/`, §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)
<persona .md de l'agent>
# 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.