fix(orchestrator): la délégation simple vers UX renvoie son Final structuré (#207, QA verte)

Une demande idea_ask_agent vers UX échouait avec « finished without a structured Final response » alors qu'Architect répondait. Deux causes corrigées:

- session/process: un tour CLI terminé en code non nul SANS stdout (ex. erreur 401/auth) renvoyait une sortie vide au lieu du stderr réel; l'orchestrateur concluait alors à un « no Final ». On remonte désormais le stderr effectif (drain sync + async + helper process_exit_message + test dédié).

- orchestrator/service: la branche « ask waiting on background » masquait le premier Final structuré; la réponse synchrone redevient le premier Final (suppression de active_rendezvous_has_background_tasks / mark_rendezvous_task_waiting).

Nettoyage runtime corollaire (#204): arrêt du live runtime stale (PTY + structuré) d'un agent recréé afin que la délégation route vers l'agent courant (LiveSessions, stop_live_agent_runtime_in_project, wiring backend via DeleteAgent::with_runtime_cleanup, lifecycle, workstate cascade).
This commit is contained in:
2026-08-12 14:09:06 +02:00
parent ba176e3cf2
commit 646b9d9284
11 changed files with 673 additions and 203 deletions

View File

@ -33,6 +33,7 @@ pub use registry::{
TerminalSessions,
};
pub use usecases::{
CloseTerminal, CloseTerminalInput, CloseTerminalOutput, OpenTerminal, OpenTerminalInput,
OpenTerminalOutput, ResizeTerminal, ResizeTerminalInput, WriteToTerminal, WriteToTerminalInput,
stop_live_agent_runtime_in_project, CloseTerminal, CloseTerminalInput, CloseTerminalOutput,
OpenTerminal, OpenTerminalInput, OpenTerminalOutput, ResizeTerminal, ResizeTerminalInput,
StoppedAgentRuntime, WriteToTerminal, WriteToTerminalInput,
};

View File

@ -4,13 +4,13 @@ use std::sync::Arc;
use domain::ports::{EventBus, PtyPort, SpawnSpec};
use domain::{
DomainEvent, NodeId, ProjectPath, PtySize, SessionId, SessionKind, SessionStatus,
TerminalSession,
AgentId, DomainEvent, NodeId, Project, ProjectPath, PtySize, SessionId, SessionKind,
SessionStatus, TerminalSession,
};
use crate::error::AppError;
use super::registry::TerminalSessions;
use super::registry::{LiveSessionKind, LiveSessions, TerminalSessions};
/// Input for [`OpenTerminal::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
@ -230,6 +230,59 @@ impl CloseTerminal {
}
}
/// Runtime session stopped for one agent.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct StoppedAgentRuntime {
/// Agent whose live runtime was stopped.
pub agent_id: AgentId,
/// Live session id removed from its registry.
pub session_id: SessionId,
/// Runtime registry that owned the session.
pub kind: LiveSessionKind,
}
/// 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.
pub async fn stop_live_agent_runtime_in_project(
project: &Project,
agent_id: AgentId,
live: &LiveSessions,
close: &CloseTerminal,
) -> Result<Vec<StoppedAgentRuntime>, AppError> {
let mut stopped = Vec::new();
if let Some(session_id) = live
.structured
.session_id_for_agent_in_project(project.id, &agent_id)
{
if let Some(session) = live.structured.remove(&session_id) {
session
.shutdown()
.await
.map_err(|e| AppError::Process(e.to_string()))?;
}
stopped.push(StoppedAgentRuntime {
agent_id,
session_id,
kind: LiveSessionKind::Structured,
});
}
if let Some(session_id) = live.pty.session_for_agent_in_project(project.id, &agent_id) {
close.execute(CloseTerminalInput { session_id }).await?;
stopped.push(StoppedAgentRuntime {
agent_id,
session_id,
kind: LiveSessionKind::Pty,
});
}
Ok(stopped)
}
/// The platform default interactive shell.
///
/// Resolution policy lives in the application layer (a metier default), not the