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:
@ -237,6 +237,14 @@ pub struct TerminalSessionDto {
|
||||
/// resumes. `None` for a plain terminal, a resume, or a degraded launch.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub assigned_conversation_id: Option<String>,
|
||||
/// **Id de session moteur** (resumable du provider courant) exposé par ce
|
||||
/// lancement, **distinct** de l'id de paire `assignedConversationId` (ARCHITECTURE
|
||||
/// §19.7, lot P8a). Le front le range dans le **cache** `engineSessionId` de la
|
||||
/// cellule (jamais sur `conversationId`). Absent pour un terminal nu, une reprise,
|
||||
/// ou quand le moteur n'expose encore aucun id. La source de vérité du resumable
|
||||
/// est `providers.json` (lot P8b).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub engine_session_id: Option<String>,
|
||||
/// How the frontend should render the cell hosting this session (§17.6):
|
||||
/// [`CellKind::Chat`] for a structured AI session (an `AgentChatView` driven by
|
||||
/// `agent_send`/`reattach_agent_chat`), [`CellKind::Pty`] for a raw terminal
|
||||
@ -269,6 +277,7 @@ impl From<OpenTerminalOutput> for TerminalSessionDto {
|
||||
rows: s.pty_size.rows,
|
||||
cols: s.pty_size.cols,
|
||||
assigned_conversation_id: None,
|
||||
engine_session_id: None,
|
||||
cell_kind: CellKind::Pty,
|
||||
}
|
||||
}
|
||||
@ -1165,6 +1174,7 @@ pub struct LaunchAgentRequestDto {
|
||||
impl From<LaunchAgentOutput> for TerminalSessionDto {
|
||||
fn from(out: LaunchAgentOutput) -> Self {
|
||||
let assigned_conversation_id = out.assigned_conversation_id;
|
||||
let engine_session_id = out.engine_session_id;
|
||||
// §17.6: the cell kind is *derived* from the launch routing. A structured
|
||||
// descriptor (`structured: Some(..)`) means LaunchAgent routed to an
|
||||
// AgentSession ⇒ chat cell; its absence means the PTY/terminal path ⇒
|
||||
@ -1181,6 +1191,7 @@ impl From<LaunchAgentOutput> for TerminalSessionDto {
|
||||
rows: s.pty_size.rows,
|
||||
cols: s.pty_size.cols,
|
||||
assigned_conversation_id,
|
||||
engine_session_id,
|
||||
cell_kind,
|
||||
}
|
||||
}
|
||||
@ -1257,6 +1268,7 @@ impl From<TerminalSession> for TerminalSessionDto {
|
||||
rows: s.pty_size.rows,
|
||||
cols: s.pty_size.cols,
|
||||
assigned_conversation_id: None,
|
||||
engine_session_id: None,
|
||||
cell_kind: CellKind::Pty,
|
||||
}
|
||||
}
|
||||
|
||||
@ -308,6 +308,7 @@ fn layout_dto_serialises_camelcase_tagged_tree() {
|
||||
session: None,
|
||||
agent: None,
|
||||
conversation_id: None,
|
||||
engine_session_id: None,
|
||||
agent_was_running: false,
|
||||
});
|
||||
let dto = LayoutDto::from(LoadLayoutOutput {
|
||||
@ -409,6 +410,7 @@ fn layout_dto_round_trips_a_split_tree_shape() {
|
||||
session: None,
|
||||
agent: None,
|
||||
conversation_id: None,
|
||||
engine_session_id: None,
|
||||
agent_was_running: false,
|
||||
})
|
||||
.split(
|
||||
@ -419,6 +421,7 @@ fn layout_dto_round_trips_a_split_tree_shape() {
|
||||
session: None,
|
||||
agent: None,
|
||||
conversation_id: None,
|
||||
engine_session_id: None,
|
||||
agent_was_running: false,
|
||||
},
|
||||
nid(9),
|
||||
|
||||
@ -307,6 +307,7 @@ fn launch_agent_output_maps_to_terminal_session_dto() {
|
||||
let out = LaunchAgentOutput {
|
||||
session,
|
||||
assigned_conversation_id: None,
|
||||
engine_session_id: None,
|
||||
structured: None,
|
||||
};
|
||||
let dto = TerminalSessionDto::from(out);
|
||||
@ -351,6 +352,7 @@ fn launch_agent_output_propagates_assigned_conversation_id() {
|
||||
let out = LaunchAgentOutput {
|
||||
session,
|
||||
assigned_conversation_id: Some("conv-xyz".to_owned()),
|
||||
engine_session_id: None,
|
||||
structured: None,
|
||||
};
|
||||
let dto = TerminalSessionDto::from(out);
|
||||
|
||||
@ -149,6 +149,7 @@ fn launch_output_with_structured_descriptor_derives_chat_cell_kind() {
|
||||
let out = LaunchAgentOutput {
|
||||
session,
|
||||
assigned_conversation_id: None,
|
||||
engine_session_id: None,
|
||||
structured: Some(descriptor),
|
||||
};
|
||||
let dto = TerminalSessionDto::from(out);
|
||||
@ -164,6 +165,7 @@ fn launch_output_without_structured_descriptor_derives_pty_cell_kind() {
|
||||
let out = LaunchAgentOutput {
|
||||
session,
|
||||
assigned_conversation_id: None,
|
||||
engine_session_id: None,
|
||||
structured: None,
|
||||
};
|
||||
let dto = TerminalSessionDto::from(out);
|
||||
@ -197,6 +199,7 @@ fn pty_launch_output_serialises_cellkind_pty_without_breaking_existing_keys() {
|
||||
let out = LaunchAgentOutput {
|
||||
session,
|
||||
assigned_conversation_id: None,
|
||||
engine_session_id: None,
|
||||
structured: None,
|
||||
};
|
||||
let v = serde_json::to_value(TerminalSessionDto::from(out)).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user