fix(backend,orchestrator,chat): l'objectif suspendu guide le premier tour post-restart — #290 regression partielle (register_suspended_turn capture la demande utilisateur exacte au point d'entrée chat agent_send/chat.send avec source diagnostique, RestartRendezvousCoordinator conserve tour/objectif/restart_attempt_id, take_continuation consommable exactement une fois et seulement par la tentative courante: suspended_restart_continuation_prompt place l'objectif utilisateur avant toute consigne generique et StructuredHumanTurnTransition::Resume porte le prompt host-provided injecte au runtime de remplacement; nettoyage du tour suspendu sur succes, echec, annulation et abandon/expiration; diagnostics projet/agent/turn_id/restart_attempt_id/source; rendez-vous inter-agents #289 inchanges; QA verte reconfirmee par Git: cargo check --workspace, backend self_restart_contract_tests 3/3, orchestrator_service restart 10/10, workstate_actions 9/9, app-tauri chat_bridge 31/31, web-server websocket_chat_send 3/3, fmt --check, diff --check; restants: rebuild AppImage + validation live reprise objectif apres restart; carnet #290 iteration regression + passage store qa inclus; scripts/ hors périmètre; aucun push)

This commit is contained in:
2026-09-09 09:17:36 +02:00
parent 93a1c23a29
commit 18c1b01afb
10 changed files with 426 additions and 41 deletions

View File

@ -1879,13 +1879,14 @@ async fn ws_send_chat(
})?;
let session_meta = state.app.structured_sessions.meta_for_session(&sid);
let turn_guard = session_meta.as_ref().map(|(project_id, agent_id, _, _)| {
backend::StructuredHumanTurnGuard::start(
backend::StructuredHumanTurnGuard::start_with_source(
Arc::clone(&state.app.turn_watch_input),
Arc::clone(&state.app.input_mailbox),
Arc::clone(&state.app.restart_rendezvous),
*project_id,
*agent_id,
&prompt,
"websocket_chat_send",
)
});
@ -1935,6 +1936,7 @@ async fn ws_send_chat(
'logical_turn: loop {
let mut concrete_terminal = false;
let mut resume = false;
let mut resume_prompt = None;
for event in stream {
let provider_terminal =
matches!(event, ReplyEvent::Final { .. } | ReplyEvent::Error { .. });
@ -1943,8 +1945,9 @@ async fn ws_send_chat(
if let Some(guard) = turn_guard.as_mut() {
match guard.transition_after_provider_terminal_async().await {
backend::StructuredHumanTurnTransition::Complete => {}
backend::StructuredHumanTurnTransition::Resume => {
backend::StructuredHumanTurnTransition::Resume { prompt } => {
resume = true;
resume_prompt = Some(prompt);
break;
}
backend::StructuredHumanTurnTransition::Failed(message) => {
@ -1995,7 +1998,10 @@ async fn ws_send_chat(
if let Some(guard) = turn_guard.as_mut() {
match guard.transition_after_provider_terminal_async().await {
backend::StructuredHumanTurnTransition::Complete => {}
backend::StructuredHumanTurnTransition::Resume => resume = true,
backend::StructuredHumanTurnTransition::Resume { prompt } => {
resume = true;
resume_prompt = Some(prompt);
}
backend::StructuredHumanTurnTransition::Failed(message) => {
let chunk = reply_event_chunk(ReplyEvent::Error { message });
tx.send(ServerFrame::chat_output(
@ -2028,8 +2034,10 @@ async fn ws_send_chat(
code: "CHAT_RESTART_FAILED".to_owned(),
message: "restart completed without a replacement structured session".to_owned(),
})?;
let resume_prompt = resume_prompt
.unwrap_or_else(|| application::SELF_RESTART_CONTINUATION_PROMPT.to_owned());
stream = replacement
.send(application::SELF_RESTART_CONTINUATION_PROMPT)
.send(&resume_prompt)
.await
.map_err(|err| ErrorDto {
code: "CHAT_RESTART_FAILED".to_owned(),