fix(application,domain,infrastructure,web-server,app-tauri,frontend): arrêt d'agent fiable avec cascade sur l'arbre de propriété et statut cohérent — #320 (QA verte)

Symptôme: stopper un agent (ex Main sur UnityPlugin) laissait des travaux
poursuivre, la cascade n'atteignait pas les agents lancés et la pastille
restait orange pendant que l'agent continuait à en appeler d'autres.

- barrière d'admission par génération d'exécution (ExecutionControl): un stop
  ferme l'admission de tout le sous-arbre propriété/attente AVANT toute
  annulation asynchrone — plus aucune nouvelle action inter-agents après stop
- arbre de propriété durable en-process (launched_by) + dépendances d'attente
  actives: la cascade couvre transitivement agents et travaux lancés
- commandes d'un agent arrêté rejetées (AppError::AgentStopped); stop
  lui-même admis pour rester idempotent; réactivation sur relance humaine
  (génération +1) et enregistrement du propriétaire au spawn
- admission exposée aux adaptateurs hors commandes cœur (tickets, templates,
  plugins) via ensure_agent_execution_active; MCP server/tools, web-server et
  commandes Tauri alignés
- cohérence live-state: statut Idle avec intent « Arrêté » ou
  « Arrêt incomplet — Certains travaux sont encore actifs » selon l'issue de
  la cascade; workstate et surfaces UI (AgentsPanel, LayoutGrid,
  ProjectWorkStatePanel) reflètent l'état réel, plus de pastille orange fantôme
- tests: orchestrator_service (cascade/générations/idempotence),
  workstate_actions, background_task_cancel, error_codes, agents, layout,
  workstate

QA verte sur fix/320-agent-stop-cascade. Aucun push.
This commit is contained in:
2026-09-11 16:42:29 +02:00
parent 1dc892d80b
commit 1316e87010
20 changed files with 1048 additions and 154 deletions

View File

@ -103,6 +103,14 @@ pub enum AppError {
agent_id: AgentId,
},
/// The agent execution generation was explicitly stopped. New work from or
/// toward that generation is rejected until an explicit launch reactivates it.
#[error("agent {agent_id} is stopped and cannot accept new work")]
AgentStopped {
/// Agent protected by the stop barrier.
agent_id: AgentId,
},
/// The delegation target finished its structured/headless turn without producing
/// a [`ReplyEvent::Final`](domain::ports::ReplyEvent::Final), so the rendezvous
/// could not capture an inline answer. Surfaced promptly instead of blocking the
@ -154,6 +162,7 @@ impl AppError {
Self::AgentNotBoundToCell { .. } => "AGENT_NOT_BOUND_TO_CELL",
Self::ConflictAgentVisibleElsewhere { .. } => "CONFLICT_AGENT_VISIBLE_ELSEWHERE",
Self::RestartStopTimeout { .. } => "STOP_TIMEOUT",
Self::AgentStopped { .. } => "AGENT_STOPPED",
Self::TargetReturnedNoReply(_) => "TARGET_RETURNED_NO_REPLY",
Self::TargetCeilingActive(_) => "RENDEZVOUS_CEILING_ACTIVE",
Self::Internal(_) => "INTERNAL",