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:
@ -51,6 +51,36 @@ impl ConversationId {
|
||||
pub const fn as_uuid(&self) -> uuid::Uuid {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// **Dérivation déterministe et model-agnostic** de l'id de paire pour le couple
|
||||
/// `{a, b}` (ARCHITECTURE §19.7). **Pure** (aucune I/O, aucun registre) et
|
||||
/// **insensible à l'ordre** : `for_pair(a, b) == for_pair(b, a)`.
|
||||
///
|
||||
/// Sert de **repli stable** quand aucun [`ConversationRegistry`] (stateful) n'est
|
||||
/// disponible pour matérialiser le fil — typiquement le lancement **direct** d'une
|
||||
/// cellule structurée neuve par l'utilisateur (`LaunchAgent`, lot P8a) : la cellule
|
||||
/// doit porter la **même** clé que celle sous laquelle le handoff a été (ou sera)
|
||||
/// rangé. Pour la paire canonique `User↔Agent`, le résultat est **exactement**
|
||||
/// l'id du repli de `OrchestratorService::resolve_conversation(None, agent)`
|
||||
/// (`ConversationId::from_uuid(agent.as_uuid())`), garantissant la cohérence de
|
||||
/// clé end-to-end sans coupler `LaunchAgent` à l'orchestrateur ni au registre.
|
||||
///
|
||||
/// Pour une paire `Agent↔Agent`, l'id est dérivé des deux UUID agent de façon
|
||||
/// commutative (XOR), stable et déterministe.
|
||||
#[must_use]
|
||||
pub fn for_pair(a: ConversationParty, b: ConversationParty) -> Self {
|
||||
match (a.as_agent(), b.as_agent()) {
|
||||
// User↔Agent (un seul agent) : aligné sur le repli de `resolve_conversation`.
|
||||
(Some(agent), None) | (None, Some(agent)) => Self::from_uuid(agent.as_uuid()),
|
||||
// Agent↔Agent : combinaison commutative des deux ids (insensible à l'ordre).
|
||||
(Some(x), Some(y)) => {
|
||||
let xor = x.as_uuid().as_u128() ^ y.as_uuid().as_u128();
|
||||
Self::from_uuid(uuid::Uuid::from_u128(xor))
|
||||
}
|
||||
// User↔User n'existe pas (invariant `Conversation`) : repli sûr non-panic.
|
||||
(None, None) => Self::from_uuid(uuid::Uuid::nil()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ConversationId {
|
||||
|
||||
Reference in New Issue
Block a user