feat(persistence): P8a — cohérence de clé conversation (id de paire stable)

Corrige l'incohérence P6/P7 : la cellule et la persistance partagent désormais
le MÊME id de paire IdeA, déterministe et stable au redémarrage — la clé sous
laquelle P6b sauve log/handoff == celle sous laquelle P7 les charge.

- domaine : ConversationId::for_pair(a,b) pur/déterministe (User↔Agent = uuid
  agent ; Agent↔Agent = XOR commutatif) ; LeafCell gagne engine_session_id
  (cache resumable moteur, additif serde default)
- infrastructure : InMemoryConversationRegistry::resolve utilise for_pair au
  lieu de new_random() → id recalculable sans état, identique après restart
- application : launch_structured persiste l'id de paire sur la cellule (et l'id
  moteur sur engine_session_id) ; cellule neuve ⇒ dérive for_pair(User,agent) ;
  resolve_conversation repli factorisé sur for_pair ; withers layout clone-and-
  mutate (préservent les champs additifs)
- app-tauri : engine_session_id propagé dans les DTO

Suites domain/infra/application/app-tauri vertes (assertion structured_launch_d3
recodée sur le contrat). Test déterminisme/restart dédié à ajouter au retour de
quota agent (binôme Test interrompu par limite de session).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 18:10:50 +02:00
parent 19ba77824f
commit e583b2b49f
22 changed files with 284 additions and 106 deletions

View File

@ -90,11 +90,14 @@ impl ConversationRegistry for InMemoryConversationRegistry {
.cloned()
.expect("by_pair id always present in by_id");
}
// Lazy create: mint a fresh Dormant conversation for the pair. The pair is
// valid by construction at call sites (resolve is only asked for real pairs);
// `try_new` still guards the invariants, and on the (impossible) error we fall
// back to a normalised pair to never panic in production.
let id = ConversationId::new_random();
// Lazy create: mint a Dormant conversation for the pair. The id is **derived
// deterministically** from the pair (`ConversationId::for_pair`, ARCHITECTURE
// §19.7) so the **same pair always yields the same id across registry instances**
// (i.e. it survives an IDE restart with an empty `by_pair`/`by_id`). This keeps
// the persistence key stable and aligned with `LaunchAgent` (P8a) / the
// `resolve_conversation` fallback. The pair is valid by construction at call
// sites; `try_new` still guards the invariants.
let id = ConversationId::for_pair(key.0, key.1);
let conv = Conversation::try_new(id, key.0, key.1)
.expect("pair_key yields a valid distinct/≤1-user pair");
inner.by_pair.insert(key, id);