fix(domain,application,infrastructure,backend,app-tauri,frontend): Stop demande agent préserve la conversation ouverte — arrêt sans fermeture/clôture du chat, états cohérents après cancel sur les 3 adapters de session, cellule et workstate synchronisés — #343 (QA verte)

This commit is contained in:
2026-09-13 10:03:36 +02:00
parent a1e8a6466f
commit c37638ff24
26 changed files with 1210 additions and 232 deletions

View File

@ -2,7 +2,7 @@
use std::sync::Arc;
use domain::ports::{EventBus, PtyPort, SpawnSpec};
use domain::ports::{CancelCurrentTurnOutcome, EventBus, PtyPort, SpawnSpec};
use domain::{
AgentId, DomainEvent, NodeId, Project, ProjectPath, PtySize, SessionId, SessionKind,
SessionStatus, TerminalSession,
@ -11,7 +11,54 @@ use domain::{
use crate::error::AppError;
use crate::managed_temp::{ReleaseManagedTemp, ReleaseManagedTempInput};
use super::registry::{LiveRuntimeState, LiveSessionKind, LiveSessions, TerminalSessions};
use super::registry::{
LiveRuntimeState, LiveSessionKind, LiveSessions, StructuredSessions, TerminalSessions,
};
/// Result of cancelling the current request while preserving its structured session.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CancelAgentChatOutput {
/// Stable session that remains registered and reusable.
pub session_id: SessionId,
/// Whether this call cancelled work or observed an already-terminal turn.
pub outcome: CancelCurrentTurnOutcome,
}
/// Cancels one structured chat turn without stopping its reusable runtime.
pub struct CancelAgentChat {
sessions: Arc<StructuredSessions>,
}
impl CancelAgentChat {
/// Builds the use case from the structured runtime registry.
#[must_use]
pub fn new(sessions: Arc<StructuredSessions>) -> Self {
Self { sessions }
}
/// Requests cancellation from the live session and deliberately leaves its
/// registry entry, conversation identity and runtime state untouched.
///
/// # Errors
/// Returns [`AppError::NotFound`] only when the session itself is absent, and
/// a process error when the adapter could not interrupt its current child.
pub async fn execute(&self, session_id: SessionId) -> Result<CancelAgentChatOutput, AppError> {
let (session, outcome) = self
.sessions
.request_turn_cancel(&session_id)
.ok_or_else(|| AppError::NotFound(format!("structured session {session_id}")))?;
if outcome == CancelCurrentTurnOutcome::Cancelled {
if let Err(error) = session.cancel_current_turn().await {
self.sessions.restore_turn_after_cancel_failure(&session_id);
return Err(AppError::from(error));
}
}
Ok(CancelAgentChatOutput {
session_id,
outcome,
})
}
}
/// Input for [`OpenTerminal::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
@ -281,8 +328,9 @@ pub struct StoppedAgentRuntime {
/// Stops one agent's live runtime across both registries.
///
/// PTY sessions are closed through [`CloseTerminal`]. Structured sessions are
/// removed from their registry before `shutdown()` so relaunch guards stop seeing
/// them as live even while shutdown runs.
/// removed from their registry, have their current turn cancelled, then receive
/// `shutdown()` so relaunch guards stop seeing them as live during teardown and
/// no per-turn child survives it.
pub async fn stop_live_agent_runtime_in_project(
project: &Project,
agent_id: AgentId,
@ -298,6 +346,9 @@ pub async fn stop_live_agent_runtime_in_project(
let removed = match snapshot.kind {
LiveSessionKind::Structured => {
if let Some(removed) = live.structured.remove_runtime(&snapshot.session_id) {
if let Err(error) = removed.session.cancel_current_turn().await {
first_error.get_or_insert_with(|| AppError::Process(error.to_string()));
}
if let Err(error) = removed.session.shutdown().await {
first_error.get_or_insert_with(|| AppError::Process(error.to_string()));
}