fix(agents): enforcer l'invariant « 1 session vivante par agent » (singleton)
Un agent ne peut tourner que dans une seule cellule à la fois. La garde dans LaunchAgent refuse le spawn si l'agent est déjà vivant dans un autre node (AGENT_ALREADY_RUNNING) ; idempotent sur le même node ; le chemin resume (agent mort) reste inchangé. Le node_id est désormais plombé jusqu'au use case. Corrige le reset asymétrique d'une cellule au changement d'onglet : deux leaves partageant le même agent id rendaient session_for_agent/is_agent_live/stop_agent ambigus (cible arbitraire). Le churn reset/reattach déclenchait aussi les accents mélangés (FIFO intact, non touché). - snapshot agentWasRunning calculé par node (is_node_live) et non par agent - commande list_live_agents + live_agents()/node_for_agent()/is_node_live() - UI : dropdown grise les agents déjà placés ailleurs ; 2e cellule en doublon affiche « disponible » au lieu d'une relance fantôme Tests : cargo test (application + app-tauri) vert ; tsc + vitest vert. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -481,6 +481,39 @@ impl LaunchAgent {
|
||||
.to_agent()
|
||||
.map_err(|e| AppError::Invalid(e.to_string()))?;
|
||||
|
||||
// 1b. Enforce the "one live session per agent" invariant (decision: an
|
||||
// agent is a singleton that runs in a single cell at a time). This
|
||||
// runs AFTER the NotFound resolution above (so an unknown agent still
|
||||
// errors NotFound) but BEFORE any I/O (run dir, seed, spawn). If the
|
||||
// agent already owns a live session:
|
||||
// - on a *different* (or unspecified) node → refuse the launch with
|
||||
// `AgentAlreadyRunning`, pointing at the existing host cell;
|
||||
// - on the *same* node → idempotent (a concurrent double-launch of the
|
||||
// very same cell): return the existing session without respawning,
|
||||
// assigning no new conversation id.
|
||||
// The resume path (agent dead ⇒ no live session) is unaffected.
|
||||
if let Some(existing_id) = self.sessions.session_for_agent(&input.agent_id) {
|
||||
let host_node = self
|
||||
.sessions
|
||||
.node_for_agent(&input.agent_id)
|
||||
.expect("a live session always has a host node");
|
||||
let same_node = input.node_id.as_ref() == Some(&host_node);
|
||||
if !same_node {
|
||||
return Err(AppError::AgentAlreadyRunning {
|
||||
agent_id: input.agent_id,
|
||||
node_id: host_node,
|
||||
});
|
||||
}
|
||||
// Same node: idempotent — hand back the already-registered session,
|
||||
// no respawn, nothing new to persist.
|
||||
if let Some(session) = self.sessions.session(&existing_id) {
|
||||
return Ok(LaunchAgentOutput {
|
||||
session,
|
||||
assigned_conversation_id: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Read its context and resolve its profile.
|
||||
let content = self
|
||||
.contexts
|
||||
|
||||
Reference in New Issue
Block a user