fix(orchestrator,agent): ask_agent survit au restart de la cible same-project et cross-project — #289 (RestartRendezvousCoordinator stable par RuntimeAgentKey (projectId, agentId) partagé entre le rendez-vous ask_structured et le scheduler AppAgentSelfRestarter: l'ask actif garde verrou de tour, ticket logique et conversation_id, abandonne le drain de l'ancienne session sans no-reply puis capture le Final de continuation de la session relancée (SELF_RESTART_CONTINUATION_PROMPT partagé, un seul tour de continuation); branche delegated du scheduler: pas de preempt/cancel_head/mark_idle locaux, ready + wait_for_handoff, échecs restart propagés explicitement et cancel en cascade sans relance de continuation; watchdog d'inactivité réarmé par l'activité de contrôle restart; corrélation cross-project (requesterProjectId, requesterAgentId, correlationId) conservée jusqu'au résultat post-restart puis supprimée; stop_agent d'une cible en restart annule le rendez-vous au lieu d'un NotFound; le scheduler standalone conserve son comportement historique sans ask actif; QA verte: orchestrator_service 87/87 dont 6 tests restart (final post-restart, nettoyage busy/workstate/registre, corrélation cross-project, failure explicite, cancel cross-project + stop same-project pendant restart), application lib 171/171 dont watchdog réarmé, backend 89+7 ignored, cargo check -p backend, fmt --check; réserves: pas d'E2E AppImage/vrai provider, 7 tests backend ignorés bind socket local; scripts/ hors périmètre; aucun push)
This commit is contained in:
@ -1037,9 +1037,6 @@ struct AppAgentResumer {
|
||||
contexts: ResumeContexts,
|
||||
}
|
||||
|
||||
const SELF_RESTART_PROMPT: &str =
|
||||
"IdeA vient de te redémarrer. Reprends immédiatement le travail en cours là où tu l'avais laissé.";
|
||||
|
||||
struct AppAgentSelfRestarter {
|
||||
launch_agent: Arc<LaunchAgent>,
|
||||
close_terminal: Arc<CloseTerminal>,
|
||||
@ -1049,6 +1046,7 @@ struct AppAgentSelfRestarter {
|
||||
events: Arc<dyn EventBus>,
|
||||
contexts: ResumeContexts,
|
||||
active: Arc<Mutex<HashSet<domain::RuntimeAgentKey>>>,
|
||||
rendezvous: Arc<application::RestartRendezvousCoordinator>,
|
||||
}
|
||||
|
||||
fn self_restart_launch_input(
|
||||
@ -1134,6 +1132,7 @@ impl AgentSelfRestartScheduler for AppAgentSelfRestarter {
|
||||
}
|
||||
|
||||
let restart_id = Uuid::new_v4().to_string();
|
||||
let delegated = self.rendezvous.begin(key, restart_id.clone());
|
||||
self.events.publish(DomainEvent::AgentRestartRequested {
|
||||
agent_id: agent,
|
||||
restart_id: restart_id.clone(),
|
||||
@ -1148,6 +1147,7 @@ impl AgentSelfRestartScheduler for AppAgentSelfRestarter {
|
||||
let events = Arc::clone(&self.events);
|
||||
let contexts = Arc::clone(&self.contexts);
|
||||
let active = Arc::clone(&self.active);
|
||||
let rendezvous = Arc::clone(&self.rendezvous);
|
||||
let restart_id_task = restart_id.clone();
|
||||
let project = project.clone();
|
||||
spawn_detached(async move {
|
||||
@ -1176,7 +1176,9 @@ impl AgentSelfRestartScheduler for AppAgentSelfRestarter {
|
||||
});
|
||||
|
||||
publish_phase(AgentRestartPhase::Stopping);
|
||||
input.preempt(key);
|
||||
if !delegated {
|
||||
input.preempt(key);
|
||||
}
|
||||
let stop_result = application::terminal::stop_live_agent_runtime_in_project(
|
||||
&project,
|
||||
agent,
|
||||
@ -1186,12 +1188,17 @@ impl AgentSelfRestartScheduler for AppAgentSelfRestarter {
|
||||
.await;
|
||||
|
||||
publish_phase(AgentRestartPhase::ReleasingInput);
|
||||
if let Some(ticket) = input.busy_state(key).ticket() {
|
||||
mailbox.cancel_head(key, ticket);
|
||||
if !delegated {
|
||||
if let Some(ticket) = input.busy_state(key).ticket() {
|
||||
mailbox.cancel_head(key, ticket);
|
||||
}
|
||||
input.mark_idle(key);
|
||||
}
|
||||
input.mark_idle(key);
|
||||
|
||||
let fail = |phase, error: AppError| {
|
||||
if delegated {
|
||||
rendezvous.fail(key, &restart_id_task, error.to_string());
|
||||
}
|
||||
events.publish(DomainEvent::AgentRestartFailed {
|
||||
agent_id: agent,
|
||||
restart_id: restart_id_task.clone(),
|
||||
@ -1199,6 +1206,11 @@ impl AgentSelfRestartScheduler for AppAgentSelfRestarter {
|
||||
message: error.to_string(),
|
||||
});
|
||||
};
|
||||
let finish_coordination = || {
|
||||
if delegated {
|
||||
rendezvous.finish(key, &restart_id_task);
|
||||
}
|
||||
};
|
||||
match stop_result {
|
||||
Ok(stopped) if !stopped.is_empty() => {}
|
||||
Ok(_) => {
|
||||
@ -1206,16 +1218,28 @@ impl AgentSelfRestartScheduler for AppAgentSelfRestarter {
|
||||
AgentRestartPhase::Stopping,
|
||||
AppError::NotFound(format!("running runtime for agent {agent}")),
|
||||
);
|
||||
finish_coordination();
|
||||
active.lock().ok().map(|mut set| set.remove(&key));
|
||||
return;
|
||||
}
|
||||
Err(error) => {
|
||||
fail(AgentRestartPhase::Stopping, error);
|
||||
finish_coordination();
|
||||
active.lock().ok().map(|mut set| set.remove(&key));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if delegated && rendezvous.is_cancelled(key, &restart_id_task) {
|
||||
fail(
|
||||
AgentRestartPhase::ReleasingInput,
|
||||
AppError::Process("restart cancelled with its active rendezvous".to_owned()),
|
||||
);
|
||||
finish_coordination();
|
||||
active.lock().ok().map(|mut set| set.remove(&key));
|
||||
return;
|
||||
}
|
||||
|
||||
publish_phase(AgentRestartPhase::Launching);
|
||||
let mcp_runtime = crate::mcp_endpoint::idea_exe_path().map(|exe| McpRuntime {
|
||||
exe,
|
||||
@ -1232,11 +1256,48 @@ impl AgentSelfRestartScheduler for AppAgentSelfRestarter {
|
||||
Ok(output) => output,
|
||||
Err(error) => {
|
||||
fail(AgentRestartPhase::Launching, error);
|
||||
finish_coordination();
|
||||
active.lock().ok().map(|mut set| set.remove(&key));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if delegated {
|
||||
if rendezvous.is_cancelled(key, &restart_id_task) {
|
||||
let _ = application::terminal::stop_live_agent_runtime_in_project(
|
||||
&project,
|
||||
agent,
|
||||
live.as_ref(),
|
||||
close_terminal.as_ref(),
|
||||
)
|
||||
.await;
|
||||
fail(
|
||||
AgentRestartPhase::Launching,
|
||||
AppError::Process(
|
||||
"restart cancelled before the continuation turn".to_owned(),
|
||||
),
|
||||
);
|
||||
finish_coordination();
|
||||
active.lock().ok().map(|mut set| set.remove(&key));
|
||||
return;
|
||||
}
|
||||
publish_phase(AgentRestartPhase::Resuming);
|
||||
rendezvous.ready(key, &restart_id_task);
|
||||
if let Err(message) = rendezvous.wait_for_handoff(key, &restart_id_task).await {
|
||||
fail(AgentRestartPhase::Resuming, AppError::Process(message));
|
||||
finish_coordination();
|
||||
active.lock().ok().map(|mut set| set.remove(&key));
|
||||
return;
|
||||
}
|
||||
events.publish(DomainEvent::AgentRestartCompleted {
|
||||
agent_id: agent,
|
||||
restart_id: restart_id_task.clone(),
|
||||
});
|
||||
finish_coordination();
|
||||
active.lock().ok().map(|mut set| set.remove(&key));
|
||||
return;
|
||||
}
|
||||
|
||||
publish_phase(AgentRestartPhase::Resuming);
|
||||
let resume_result = if let Some(session) = live
|
||||
.structured
|
||||
@ -1247,10 +1308,10 @@ impl AgentSelfRestartScheduler for AppAgentSelfRestarter {
|
||||
Arc::clone(&mailbox),
|
||||
project.id,
|
||||
agent,
|
||||
SELF_RESTART_PROMPT,
|
||||
application::SELF_RESTART_CONTINUATION_PROMPT,
|
||||
);
|
||||
let result = session
|
||||
.send(SELF_RESTART_PROMPT)
|
||||
.send(application::SELF_RESTART_CONTINUATION_PROMPT)
|
||||
.await
|
||||
.map(|stream| stream.for_each(drop))
|
||||
.map_err(|error| AppError::Process(error.to_string()));
|
||||
@ -1272,7 +1333,7 @@ impl AgentSelfRestartScheduler for AppAgentSelfRestarter {
|
||||
let ticket = domain::mailbox::Ticket::new(
|
||||
domain::mailbox::TicketId::new_random(),
|
||||
"IdeA",
|
||||
SELF_RESTART_PROMPT,
|
||||
application::SELF_RESTART_CONTINUATION_PROMPT,
|
||||
);
|
||||
let _pending = input.enqueue(key, ticket);
|
||||
Ok(())
|
||||
@ -3496,6 +3557,7 @@ impl BackendCore {
|
||||
Arc::clone(&events_port),
|
||||
resumer,
|
||||
));
|
||||
let restart_rendezvous = Arc::new(application::RestartRendezvousCoordinator::new());
|
||||
let self_restart = Arc::new(AppAgentSelfRestarter {
|
||||
launch_agent: Arc::clone(&launch_agent),
|
||||
close_terminal: Arc::clone(&close_terminal),
|
||||
@ -3505,6 +3567,7 @@ impl BackendCore {
|
||||
events: Arc::clone(&events_port),
|
||||
contexts: Arc::clone(&resume_contexts),
|
||||
active: Arc::new(Mutex::new(HashSet::new())),
|
||||
rendezvous: Arc::clone(&restart_rendezvous),
|
||||
}) as Arc<dyn AgentSelfRestartScheduler>;
|
||||
// Drain du scheduler (§21.5-b) : à chaque réveil tiré, `TokioScheduler` pousse une
|
||||
// `ScheduledTask::ResumeAgent` ; on l'exécute via le service (relance + AgentResumed).
|
||||
@ -3576,6 +3639,7 @@ impl BackendCore {
|
||||
.with_conversations(Arc::clone(&conversation_registry))
|
||||
.with_events(Arc::clone(&events_port))
|
||||
.with_self_restart(self_restart)
|
||||
.with_restart_rendezvous(restart_rendezvous)
|
||||
// Faits OS/runtime (exe $APPIMAGE/current_exe + endpoint loopback) pour
|
||||
// que les (re)lancements issus du chemin `ask` écrivent la déclaration MCP
|
||||
// réelle ⇒ le pont MCP de la cible se spawne et `idea_reply` débloque le round-trip.
|
||||
|
||||
Reference in New Issue
Block a user