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

@ -122,6 +122,7 @@ fn leaf_cell(id: u128, sess: Option<u128>) -> LeafCell {
session: sess.map(session),
agent: None,
conversation_id: None,
engine_session_id: None,
agent_was_running: false,
}
}

View File

@ -20,6 +20,7 @@ fn leaf(id: u128, sess: Option<u128>) -> LeafCell {
session: sess.map(session),
agent: None,
conversation_id: None,
engine_session_id: None,
agent_was_running: false,
}
}
@ -32,6 +33,7 @@ fn leaf_with_resume(id: u128, sess: Option<u128>) -> LeafCell {
session: sess.map(session),
agent: None,
conversation_id: Some("conv-1".to_string()),
engine_session_id: None,
agent_was_running: true,
}
}
@ -674,6 +676,7 @@ fn agent_leaves_collects_only_agent_bearing_leaves() {
session: None,
agent: Some(agent_id(100)),
conversation_id: None,
engine_session_id: None,
agent_was_running: false,
}),
weight: 1.0,
@ -688,6 +691,7 @@ fn agent_leaves_collects_only_agent_bearing_leaves() {
session: None,
agent: Some(agent_id(101)),
conversation_id: None,
engine_session_id: None,
agent_was_running: false,
}),
weight: 1.0,
@ -783,6 +787,7 @@ fn move_session_preserves_resume_fields_on_both_leaves() {
session: Some(session(100)),
agent: None,
conversation_id: Some("conv-from".to_string()),
engine_session_id: None,
agent_was_running: true,
}),
weight: 1.0,
@ -793,6 +798,7 @@ fn move_session_preserves_resume_fields_on_both_leaves() {
session: None,
agent: None,
conversation_id: Some("conv-to".to_string()),
engine_session_id: None,
agent_was_running: true,
}),
weight: 1.0,
@ -842,6 +848,7 @@ fn agent_leaf_full(
session: None,
agent: Some(agent_id(agent)),
conversation_id: conv.map(str::to_string),
engine_session_id: None,
agent_was_running: running,
}
}
@ -1047,6 +1054,7 @@ fn leaf_serde_all_four_combinations_roundtrip() {
session: None,
agent: None,
conversation_id: conv.clone(),
engine_session_id: None,
agent_was_running: running,
};
let json = serde_json::to_string(&cell).unwrap();
@ -1062,6 +1070,7 @@ fn leaf_serde_omits_defaults() {
session: None,
agent: None,
conversation_id: None,
engine_session_id: None,
agent_was_running: false,
};
let json = serde_json::to_string(&cell).unwrap();
@ -1082,6 +1091,7 @@ fn leaf_serde_field_names_are_camel_case_when_present() {
session: None,
agent: None,
conversation_id: Some("c".to_string()),
engine_session_id: None,
agent_was_running: true,
};
let json = serde_json::to_string(&cell).unwrap();
@ -1108,6 +1118,7 @@ fn leaf_can_carry_conversation_without_session_and_inversely() {
session: None,
agent: None,
conversation_id: Some("c".to_string()),
engine_session_id: None,
agent_was_running: false,
};
let a_back: LeafCell = serde_json::from_str(&serde_json::to_string(&a).unwrap()).unwrap();
@ -1119,6 +1130,7 @@ fn leaf_can_carry_conversation_without_session_and_inversely() {
session: Some(session(7)),
agent: None,
conversation_id: None,
engine_session_id: None,
agent_was_running: false,
};
let b_back: LeafCell = serde_json::from_str(&serde_json::to_string(&b).unwrap()).unwrap();

View File

@ -406,6 +406,7 @@ fn layout_roundtrip() {
session: Some(session(100)),
agent: None,
conversation_id: None,
engine_session_id: None,
agent_was_running: false,
}),
weight: 1.5,
@ -416,6 +417,7 @@ fn layout_roundtrip() {
session: None,
agent: None,
conversation_id: None,
engine_session_id: None,
agent_was_running: false,
}),
weight: 2.5,
@ -441,6 +443,7 @@ fn leaf_with_agent_roundtrip_and_omits_null() {
session: None,
agent: Some(AgentId::from_uuid(agent_uuid)),
conversation_id: None,
engine_session_id: None,
agent_was_running: false,
}));
let rt = roundtrip(&tree);
@ -460,6 +463,7 @@ fn leaf_with_agent_roundtrip_and_omits_null() {
session: None,
agent: None,
conversation_id: None,
engine_session_id: None,
agent_was_running: false,
}));
let json2 = serde_json::to_string(&tree_no_agent).unwrap();

View File

@ -20,6 +20,7 @@ fn leaf_tree() -> LayoutTree {
session: None,
agent: None,
conversation_id: None,
engine_session_id: None,
agent_was_running: false,
}))
}