fix(domain,application,infrastructure,app-tauri,frontend): conversation_id redevient un pivot logique IdeA/log/handoff, reprise provider réservée à la sélection explicite — lifecycle: LaunchAgentInput::conversation_id documenté comme sélection explicite (Some = reprise native si disponible sinon handoff portable, None = conversation provider neuve), plus de copie implicite du pivot logique de la leaf au reboot ; fallback silencieux: provider_resume_fallback résolu via resolve_handoff et câblé jusqu'aux adaptateurs Claude/Codex/OpenCode via AgentSessionFactory::create resume_fallback_preamble + with_resume_fallback_preamble — le préambule reste dormant si la session native répond et n'est injecté que si la session provider s'avère introuvable/périmée (fake stale-then-fresh en test, relance fraîche sans erreur utilisateur) ; app-tauri launch_agent: contrat documenté, l'activation ordinaire d'une cellule doit envoyer None même si la leaf conserve un pivot logique d'avant reboot ; frontend CustomAgentChatView: freshOnOpen réinitialise l'état conversation complet (scrollback, view state, conversationId publié, bannière reprise, pagination historique) quand le runtime structuré n'existe plus au remontage — pas de fausse « Conversation reprise » après reboot ; tests: structured_launch_d3 étendu, structured_session_d0, agent_lifecycle structured, CustomAgentChatView freshOnOpen — #372 (QA verte: cargo check --tests -p domain -p application -p infrastructure, cargo test -p application --test structured_launch_d3, cargo test -p domain --test structured_session_d0, cargo test -p application --test agent_lifecycle structured, npm test -- CustomAgentChatView, npm run typecheck, git diff --check ; fichiers .ideai/* et scripts/ hors périmètre ; aucun push)

This commit is contained in:
2026-09-17 19:43:14 +02:00
parent 0914a77d9f
commit 07488769d3
19 changed files with 483 additions and 57 deletions

View File

@ -22,7 +22,10 @@ use domain::ports::{
use domain::sandbox::{SandboxEnforcer, SandboxPlan};
use domain::SessionId;
use super::process::{run_turn_cancellable, SpawnLine, TurnCancel};
use super::process::{
is_missing_provider_session, resume_fallback_prompt, run_turn_cancellable, SpawnLine,
TurnCancel,
};
/// Résultat du parsing d'une ligne Codex : **zéro ou plusieurs** événements et/ou un
/// id de conversation Codex capté. Miroir de `claude::ParsedLine` (vecteur d'events
@ -401,6 +404,8 @@ pub struct CodexExecSession {
env: Vec<(String, String)>,
/// Id de conversation **du moteur** Codex, capté au premier tour, `None` avant.
conversation_id: Mutex<Option<String>>,
/// Portable handoff injected only if the seeded provider session is stale.
resume_fallback_preamble: Option<String>,
/// Plan de sandbox OS **par lancement** (lot LP4-4), porté dans chaque
/// [`SpawnLine`]. `None` ⇒ aucun sandboxing (drain async natif).
sandbox: Option<SandboxPlan>,
@ -503,12 +508,20 @@ impl CodexExecSession {
model_reasoning_effort,
env,
conversation_id: Mutex::new(seed_conversation_id),
resume_fallback_preamble: None,
sandbox,
sandbox_enforcer,
turn_cancel: TurnCancel::new(),
}
}
/// Adds the portable context used by the one-shot stale-resume fallback.
#[must_use]
pub fn with_resume_fallback_preamble(mut self, preamble: Option<&str>) -> Self {
self.resume_fallback_preamble = preamble.map(str::to_owned);
self
}
/// Compose la ligne de commande d'un tour.
///
/// Format RÉEL vérifié 2026-06-10 (codex 0.137.0) :
@ -641,6 +654,7 @@ impl CodexExecSession {
prompt: &str,
tap: Option<std::sync::mpsc::Sender<ReplyEvent>>,
) -> Result<ReplyStream, AgentSessionError> {
let had_native_resume = self.conversation_id().is_some();
let spec = self.build_spawn_line(prompt);
let has_tap = tap.is_some();
let (line_tap, parser_thread) = tap.map_or((None, None), |event_tap| {
@ -670,14 +684,27 @@ impl CodexExecSession {
});
(Some(line_tx), Some(parser))
});
let raw_result = run_turn_cancellable(
let mut raw_result = run_turn_cancellable(
&spec,
None,
self.sandbox_enforcer.as_ref(),
line_tap,
line_tap.clone(),
&self.turn_cancel,
)
.await;
if had_native_resume && raw_result.as_ref().is_err_and(is_missing_provider_session) {
*self.conversation_id.lock().expect("mutex sain") = None;
let fallback_prompt =
resume_fallback_prompt(self.resume_fallback_preamble.as_deref(), prompt);
raw_result = run_turn_cancellable(
&self.build_spawn_line(&fallback_prompt),
None,
self.sandbox_enforcer.as_ref(),
line_tap,
&self.turn_cancel,
)
.await;
}
if let Some(parser) = parser_thread {
let _ = parser.join();
}