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:
@ -20,9 +20,9 @@ use domain::ports::{
|
||||
};
|
||||
use domain::{
|
||||
Agent, AgentId, AgentManifest, AgentOrigin, AgentProfile, ContextInjection, ConversationId,
|
||||
DomainEvent, Handoff, HandoffStore, ManifestEntry, MarkdownDoc, MemoryIndexEntry, MemoryType,
|
||||
NodeId, ProfileId, Project, ProjectPath, PtySize, SessionId, SessionKind, SessionStatus, Skill,
|
||||
TerminalSession,
|
||||
ConversationParty, DomainEvent, Handoff, HandoffStore, ManifestEntry, MarkdownDoc,
|
||||
MemoryIndexEntry, MemoryType, NodeId, ProfileId, Project, ProjectPath, PtySize, SessionId,
|
||||
SessionKind, SessionStatus, Skill, TerminalSession,
|
||||
};
|
||||
|
||||
use crate::error::AppError;
|
||||
@ -737,13 +737,22 @@ pub struct LaunchAgentOutput {
|
||||
/// structurée (cf. [`Self::structured`]), conformément à §17.4. Garde la sortie
|
||||
/// **non cassante** pour A/B et le câblage existant.
|
||||
pub session: TerminalSession,
|
||||
/// The conversation id **assigned** by this launch, when the profile supports
|
||||
/// session assignment and the cell had none yet. The caller persists it on the
|
||||
/// hosting leaf (via the layout flow, e.g. `set_cell_conversation`) so the next
|
||||
/// open resumes instead of re-assigning. `None` when nothing new was assigned
|
||||
/// (resume of an existing id, degraded mode, or a profile without a session
|
||||
/// block) — the caller has nothing to persist.
|
||||
/// L'**id de paire IdeA** (pivot **logique** de la conversation, ARCHITECTURE
|
||||
/// §19.7) assigné par ce lancement, quand la cellule n'en portait pas encore. Le
|
||||
/// caller le persiste sur la feuille hôte via `set_cell_conversation` : c'est la
|
||||
/// clé qui retrouve **log + handoff** au (re)lancement (P7) et survit au swap de
|
||||
/// profil. **Ce n'est plus l'id de session moteur** (qui part désormais dans
|
||||
/// [`Self::engine_session_id`]). `None` quand rien de neuf n'a été assigné (reprise
|
||||
/// d'un id existant, mode dégradé, ou profil sans session) — rien à persister.
|
||||
pub assigned_conversation_id: Option<String>,
|
||||
/// L'**id de session moteur** (resumable du provider : id Claude `--resume`, ou
|
||||
/// l'UUID minté pour `--session-id`) exposé/attribué par ce lancement, **distinct**
|
||||
/// de l'id de paire (ARCHITECTURE §19.7, lot P8a). Le caller le range dans le
|
||||
/// **cache** `LeafCell::engine_session_id` (via `set_cell_engine_session`) — jamais
|
||||
/// sur `conversation_id`. La **source de vérité** du resumable reste `providers.json`
|
||||
/// (lot P8b, hors P8a). `None` pour le chemin PTY/legacy ou quand le moteur n'expose
|
||||
/// aucun id : rien à cacher.
|
||||
pub engine_session_id: Option<String>,
|
||||
/// Descripteur de la session **structurée**, présent **uniquement** quand ce
|
||||
/// lancement a routé vers un agent IA structuré (`profile.structured_adapter =
|
||||
/// Some(_)`, ARCHITECTURE §17.4). `None` pour le chemin PTY/terminal brut
|
||||
@ -1013,6 +1022,7 @@ impl LaunchAgent {
|
||||
return Ok(LaunchAgentOutput {
|
||||
session,
|
||||
assigned_conversation_id: None,
|
||||
engine_session_id: None,
|
||||
structured: None,
|
||||
});
|
||||
}
|
||||
@ -1031,6 +1041,7 @@ impl LaunchAgent {
|
||||
return Ok(LaunchAgentOutput {
|
||||
session,
|
||||
assigned_conversation_id: None,
|
||||
engine_session_id: None,
|
||||
structured: None,
|
||||
});
|
||||
}
|
||||
@ -1062,6 +1073,8 @@ impl LaunchAgent {
|
||||
return Ok(LaunchAgentOutput {
|
||||
session: structured_snapshot(&existing, input.agent_id, node_id, size),
|
||||
assigned_conversation_id: None,
|
||||
// Rebind de vue : aucune (ré)assignation ⇒ rien de neuf à cacher.
|
||||
engine_session_id: None,
|
||||
structured: Some(StructuredSessionDescriptor {
|
||||
session_id: existing.id(),
|
||||
agent_id: input.agent_id,
|
||||
@ -1183,6 +1196,24 @@ impl LaunchAgent {
|
||||
self.structured.as_ref(),
|
||||
profile.structured_adapter.is_some(),
|
||||
) {
|
||||
// ── Clé **logique** de la cellule = id de paire IdeA (ARCHITECTURE §19.7,
|
||||
// lot P8a) ──
|
||||
// - cellule porteuse d'un `conversation_id` (resume, ou lancement délégué
|
||||
// qui a déjà injecté l'id de paire A↔B) ⇒ c'est **déjà** l'id de paire,
|
||||
// on le conserve tel quel ;
|
||||
// - cellule structurée **neuve** (lancement direct utilisateur, aucun
|
||||
// requester) ⇒ on **dérive** `pair(User, agent)` via la fonction domaine
|
||||
// pure partagée avec `resolve_conversation` (aucun couplage à
|
||||
// l'orchestrateur). C'est cet id — et **non** l'id de session moteur —
|
||||
// qui retrouve log + handoff au (re)lancement (P7) et survit au swap.
|
||||
let pair_conversation_id =
|
||||
input.conversation_id.clone().unwrap_or_else(|| {
|
||||
ConversationId::for_pair(
|
||||
ConversationParty::User,
|
||||
ConversationParty::agent(agent.id),
|
||||
)
|
||||
.to_string()
|
||||
});
|
||||
return self
|
||||
.launch_structured(
|
||||
factory.as_ref(),
|
||||
@ -1192,7 +1223,7 @@ impl LaunchAgent {
|
||||
&prepared,
|
||||
&run_dir,
|
||||
&session_plan,
|
||||
assigned_conversation_id,
|
||||
pair_conversation_id,
|
||||
input.node_id,
|
||||
size,
|
||||
)
|
||||
@ -1227,6 +1258,8 @@ impl LaunchAgent {
|
||||
Ok(LaunchAgentOutput {
|
||||
session,
|
||||
assigned_conversation_id,
|
||||
// Chemin PTY/terminal brut : pas de session moteur structurée à cacher.
|
||||
engine_session_id: None,
|
||||
structured: None,
|
||||
})
|
||||
}
|
||||
@ -1250,7 +1283,7 @@ impl LaunchAgent {
|
||||
prepared: &PreparedContext,
|
||||
run_dir: &ProjectPath,
|
||||
session_plan: &SessionPlan,
|
||||
assigned_conversation_id: Option<String>,
|
||||
pair_conversation_id: String,
|
||||
node_id: Option<NodeId>,
|
||||
size: PtySize,
|
||||
) -> Result<LaunchAgentOutput, AppError> {
|
||||
@ -1266,10 +1299,15 @@ impl LaunchAgent {
|
||||
// amont sur les deux registres).
|
||||
structured.insert(Arc::clone(&session), agent.id, node_id);
|
||||
|
||||
// L'id de conversation à persister : celui que le moteur expose s'il en a un,
|
||||
// sinon l'id pré-attribué par le plan de session (§15).
|
||||
let engine_conversation_id = session.conversation_id();
|
||||
let conversation_id = engine_conversation_id.clone().or(assigned_conversation_id);
|
||||
// ── SÉPARATION DES DEUX CLÉS (ARCHITECTURE §19.7, lot P8a) ──
|
||||
// - **id de paire** (`pair_conversation_id`) : clé **logique** persistée sur
|
||||
// `LeafCell.conversation_id`. C'est lui qui retrouve log + handoff (P7) et
|
||||
// survit au swap. Stable, indépendant du provider.
|
||||
// - **id de session moteur** (`session.conversation_id()`) : resumable du
|
||||
// provider (Claude/Codex). Rangé **séparément** dans le cache
|
||||
// `LeafCell.engine_session_id` (via `set_cell_engine_session`) — **jamais**
|
||||
// sur `conversation_id`. `None` si le moteur n'expose encore aucun id.
|
||||
let engine_session_id = session.conversation_id();
|
||||
|
||||
let snapshot = structured_snapshot(&session, agent.id, node_id, size);
|
||||
|
||||
@ -1280,13 +1318,15 @@ impl LaunchAgent {
|
||||
|
||||
Ok(LaunchAgentOutput {
|
||||
session: snapshot,
|
||||
// Le caller persiste cet id sur la cellule (pivot de reprise §15.2).
|
||||
assigned_conversation_id: conversation_id.clone(),
|
||||
// Cellule ⇐ id de **paire** (pivot logique de reprise §15.2 / §19.7).
|
||||
assigned_conversation_id: Some(pair_conversation_id),
|
||||
// Cache moteur ⇐ id resumable du provider (distinct de la paire).
|
||||
engine_session_id: engine_session_id.clone(),
|
||||
structured: Some(StructuredSessionDescriptor {
|
||||
session_id,
|
||||
agent_id: agent.id,
|
||||
node_id,
|
||||
conversation_id: engine_conversation_id,
|
||||
conversation_id: engine_session_id,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user