fix(application,backend,app-tauri,frontend): réconcilie l'état idle et les Finals après Stop — #357 (QA verte)

Cause: après un cancel manuel de tour, le ticket médié busy/FIFO n'était pas
retiré (l'agent restait Busy, bouton Stop persistant) et un terminal tardif du
tour annulé pouvait bloquer le tour suivant (Final non affiché).

Backend: StructuredSessions lie chaque génération de tour à son ticket médié
(bind_turn_accounting) et request_turn_cancel restitue l'identité (agent,
ticket) exacte observée avant annulation; CancelAgentChat.with_turn_accounting
retire le ticket exact (cancel_head + mark_idle) seulement s'il est toujours
le ticket courant — jamais un successeur déjà remplacé;
StructuredHumanTurnGuard expose accounting_identity et agent_send (app-tauri)
lie la comptabilité au démarrage du tour humain.

Frontend: CustomAgentChatView réconcilie l'interaction active depuis le
work-state (agent idle sans délégation -> interaction terminée, fallback
« Tour terminé sans réponse finale. ») au lieu de laisser un agentBusyChanged
sans tour-id clore un tour plus récent; les terminaux tardifs (final/error) du
tour read-model-settled remplacent le fallback; tour suivant isolé des
callbacks tardifs de la génération précédente.

Tests: cancel_agent_chat (retrait du ticket exact avant le prompt suivant,
nettoyage tardif sans idle sur ticket successeur), CustomAgentChatView
(isolation tour suivant, réconciliation work-state + remplacement fallback par
Final tardif).

Réserve non bloquante: alignement fixture skills préexistant porté par le
commit test dédié 83659d9a.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-09-14 15:13:03 +02:00
parent 83659d9a09
commit 50e395b602
7 changed files with 414 additions and 36 deletions

View File

@ -1774,6 +1774,16 @@ impl StructuredHumanTurnGuard {
}
}
/// Exact mediated ticket owned by this logical human turn, if it acquired Busy.
#[must_use]
pub fn accounting_identity(
&self,
) -> Option<(domain::RuntimeAgentKey, domain::mailbox::TicketId)> {
self.armed
.load(std::sync::atomic::Ordering::Acquire)
.then_some((self.agent, self.ticket_id))
}
/// Resolves whether a concrete provider terminal event really ends the
/// logical human turn. During self-restart it waits for the replacement
/// session, then acknowledges the handoff so the scheduler can complete.
@ -2757,7 +2767,6 @@ impl BackendCore {
// `profile.structured_adapter`. Injectés dans LaunchAgent (routage §17.4) et
// ChangeAgentProfile (shutdown polymorphe au hot-swap).
let structured_sessions = Arc::new(StructuredSessions::new());
let cancel_agent_chat = Arc::new(CancelAgentChat::new(Arc::clone(&structured_sessions)));
let live_sessions = Arc::new(LiveSessions::new(
Arc::clone(&terminal_sessions),
Arc::clone(&structured_sessions),
@ -3739,6 +3748,10 @@ impl BackendCore {
});
}
let input_mediator = Arc::clone(&mediated_inbox) as Arc<dyn domain::input::InputMediator>;
let cancel_agent_chat = Arc::new(
CancelAgentChat::new(Arc::clone(&structured_sessions))
.with_turn_accounting(Arc::clone(&input_mediator), Arc::clone(&mailbox)),
);
let background_tasks = Arc::new(AppBackgroundTaskStore::new(Arc::clone(&store_port)));
let background_tasks_port = Arc::clone(&background_tasks) as Arc<dyn BackgroundTaskStore>;
let (background_ready_tx, mut background_ready_rx) =