fix(backend): disponibilité session user + hand-off CLI↔TUI sans collision — #169 #170

This commit is contained in:
2026-08-06 20:00:36 +02:00
parent c50b6c5388
commit 292a81ee60
2 changed files with 114 additions and 46 deletions

View File

@ -1924,51 +1924,58 @@ impl LaunchAgent {
}
}
}
// Garde structurée (§17.4) : même sémantique côté registre IA. R0a appliqué de
// façon identique — rebind de la cellule-vue pour une réattache légitime,
// idempotence sans node/conversation, refus d'un second lancement neuf ailleurs.
if let Some(structured) = &self.structured {
if let Some(existing) =
structured.session_for_agent_in_project(input.project.id, &input.agent_id)
{
let host_node =
structured.node_for_agent_in_project(input.project.id, &input.agent_id);
let node_id = match reattach_decision(
input.node_id,
host_node,
input.conversation_id.as_deref(),
) {
ReattachDecision::Rebind { node_id } => {
let _ = structured.rebind_agent_node_in_project(
input.project.id,
&input.agent_id,
node_id,
);
node_id
}
// Idempotent — garder le node hôte courant, sinon un node neuf.
ReattachDecision::Idempotent => host_node.unwrap_or_else(NodeId::new_random),
ReattachDecision::Refuse { node_id } => {
return Err(AppError::AgentAlreadyRunning {
// Garde structurée (§17.4) : même sémantique côté registre IA, mais seulement
// pour un lancement qui vise lui-même la surface structurée. Un lancement
// humain PTY/TUI explicite doit pouvoir coexister avec une conversation
// inter-agent/headless déjà vivante pour le même agent : les canaux d'entrée
// et de sortie sont distincts, et la garde PTY ci-dessus reste la source de
// vérité de l'occupation TUI utilisateur.
if wants_structured {
if let Some(structured) = &self.structured {
if let Some(existing) =
structured.session_for_agent_in_project(input.project.id, &input.agent_id)
{
let host_node =
structured.node_for_agent_in_project(input.project.id, &input.agent_id);
let node_id = match reattach_decision(
input.node_id,
host_node,
input.conversation_id.as_deref(),
) {
ReattachDecision::Rebind { node_id } => {
let _ = structured.rebind_agent_node_in_project(
input.project.id,
&input.agent_id,
node_id,
);
node_id
}
// Idempotent — garder le node hôte courant, sinon un node neuf.
ReattachDecision::Idempotent => {
host_node.unwrap_or_else(NodeId::new_random)
}
ReattachDecision::Refuse { node_id } => {
return Err(AppError::AgentAlreadyRunning {
agent_id: input.agent_id,
node_id,
});
}
};
return Ok(LaunchAgentOutput {
session: structured_snapshot(&existing, input.agent_id, node_id, size),
assigned_conversation_id: None,
// Rebind de vue : aucune (ré)assignation ⇒ rien de neuf à cacher.
engine_session_id: None,
structured: Some(StructuredSessionDescriptor {
session_id: existing.id(),
agent_id: input.agent_id,
node_id,
});
}
};
return Ok(LaunchAgentOutput {
session: structured_snapshot(&existing, input.agent_id, node_id, size),
assigned_conversation_id: None,
// Rebind de vue : aucune (ré)assignation ⇒ rien de neuf à cacher.
engine_session_id: None,
structured: Some(StructuredSessionDescriptor {
session_id: existing.id(),
agent_id: input.agent_id,
node_id,
conversation_id: existing.conversation_id(),
}),
// Réattache structurée (rebind de vue) : aucun profil re-résolu.
profile: None,
});
conversation_id: existing.conversation_id(),
}),
// Réattache structurée (rebind de vue) : aucun profil re-résolu.
profile: None,
});
}
}
}