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:
@ -944,6 +944,13 @@ pub struct LaunchAgentRequestDto {
|
||||
/// launch may assign a new id when the profile supports it).
|
||||
#[serde(default)]
|
||||
pub conversation_id: Option<String>,
|
||||
/// The layout leaf (node) hosting this launch. Enforces the "one live session
|
||||
/// per agent" invariant: a launch into a node different from where the agent
|
||||
/// is already running is refused (`AGENT_ALREADY_RUNNING`); the same node is
|
||||
/// idempotent. Absent ⇒ a fresh node is minted (and any already-live agent is
|
||||
/// refused).
|
||||
#[serde(default)]
|
||||
pub node_id: Option<String>,
|
||||
}
|
||||
|
||||
impl From<LaunchAgentOutput> for TerminalSessionDto {
|
||||
@ -997,6 +1004,40 @@ impl From<InspectConversationOutput> for ConversationDetailsDto {
|
||||
}
|
||||
}
|
||||
|
||||
/// One currently-live agent and the cell hosting it, for `list_live_agents`.
|
||||
///
|
||||
/// Lets the UI disable an agent already running in another cell (it cannot be
|
||||
/// launched a second time — the "one live session per agent" invariant).
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LiveAgentDto {
|
||||
/// The live agent's id (UUID string).
|
||||
pub agent_id: String,
|
||||
/// The hosting layout leaf's node id (UUID string).
|
||||
pub node_id: String,
|
||||
}
|
||||
|
||||
/// Response DTO for `list_live_agents` (transparent array on the wire).
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct LiveAgentListDto(pub Vec<LiveAgentDto>);
|
||||
|
||||
impl LiveAgentListDto {
|
||||
/// Builds the wire list from the registry's `(AgentId, NodeId)` pairs.
|
||||
#[must_use]
|
||||
pub fn from_pairs(pairs: Vec<(AgentId, NodeId)>) -> Self {
|
||||
Self(
|
||||
pairs
|
||||
.into_iter()
|
||||
.map(|(agent_id, node_id)| LiveAgentDto {
|
||||
agent_id: agent_id.to_string(),
|
||||
node_id: node_id.to_string(),
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses an agent-id string (UUID) coming from the frontend.
|
||||
///
|
||||
/// # Errors
|
||||
|
||||
Reference in New Issue
Block a user