fix(runtime): isolate agent state by project (#101)
This commit is contained in:
@ -724,7 +724,9 @@ impl ChangeAgentProfile {
|
||||
// Résolution **polymorphe** de la session vivante sur les deux registres
|
||||
// (§17.4) : structuré d'abord, puis PTY. Un agent ne vit que dans un seul des
|
||||
// deux à la fois (invariant « 1 session/agent »).
|
||||
let killed = self.kill_live_session(&input.agent_id).await?;
|
||||
let killed = self
|
||||
.kill_live_session(&input.project, &input.agent_id)
|
||||
.await?;
|
||||
let Some(node_id) = killed else {
|
||||
// Aucune session vivante (ni structurée, ni PTY) ⇒ rien à relancer.
|
||||
return Ok(None);
|
||||
@ -776,12 +778,15 @@ impl ChangeAgentProfile {
|
||||
/// node neuf côté relance via `LaunchAgentInput.node_id = None`.
|
||||
async fn kill_live_session(
|
||||
&self,
|
||||
project: &Project,
|
||||
agent_id: &AgentId,
|
||||
) -> Result<Option<Option<NodeId>>, AppError> {
|
||||
// 1. Session structurée vivante ? ⇒ shutdown polymorphe.
|
||||
if let Some(structured) = &self.structured {
|
||||
if let Some(session_id) = structured.session_id_for_agent(agent_id) {
|
||||
let node_id = structured.node_for_agent(agent_id);
|
||||
if let Some(session_id) =
|
||||
structured.session_id_for_agent_in_project(project.id, agent_id)
|
||||
{
|
||||
let node_id = structured.node_for_agent_in_project(project.id, agent_id);
|
||||
if let Some(session) = structured.remove(&session_id) {
|
||||
session
|
||||
.shutdown()
|
||||
@ -793,10 +798,15 @@ impl ChangeAgentProfile {
|
||||
}
|
||||
|
||||
// 2. Sinon, session PTY vivante ? ⇒ kill PTY (chemin historique).
|
||||
let Some(session_id) = self.sessions.session_for_agent(agent_id) else {
|
||||
let Some(session_id) = self
|
||||
.sessions
|
||||
.session_for_agent_in_project(project.id, agent_id)
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
let node_id = self.sessions.node_for_agent(agent_id);
|
||||
let node_id = self
|
||||
.sessions
|
||||
.node_for_agent_in_project(project.id, agent_id);
|
||||
if let Some(handle) = self.sessions.remove(&session_id) {
|
||||
self.pty.kill(&handle).await?;
|
||||
}
|
||||
@ -1506,9 +1516,13 @@ impl LaunchAgent {
|
||||
// (rend la session existante, pas de respawn) ;
|
||||
// - **second lancement neuf** : on vise un **autre** node, sans signal de
|
||||
// réattache ⇒ refus [`AppError::AgentAlreadyRunning`] (node hôte rapporté).
|
||||
let existing_pty = self.sessions.session_for_agent(&input.agent_id);
|
||||
let existing_pty = self
|
||||
.sessions
|
||||
.session_for_agent_in_project(input.project.id, &input.agent_id);
|
||||
if let Some(existing_id) = existing_pty {
|
||||
let host_node = self.sessions.node_for_agent(&input.agent_id);
|
||||
let host_node = self
|
||||
.sessions
|
||||
.node_for_agent_in_project(input.project.id, &input.agent_id);
|
||||
if input.allow_structured_alongside_pty && input.node_id.is_none() {
|
||||
crate::diag!(
|
||||
"[launch] existing PTY kept while structured launch proceeds: agent={} \
|
||||
@ -1519,9 +1533,11 @@ impl LaunchAgent {
|
||||
match reattach_decision(input.node_id, host_node, input.conversation_id.as_deref())
|
||||
{
|
||||
ReattachDecision::Rebind { node_id } => {
|
||||
if let Some(session) =
|
||||
self.sessions.rebind_agent_node(&input.agent_id, node_id)
|
||||
{
|
||||
if let Some(session) = self.sessions.rebind_agent_node_in_project(
|
||||
input.project.id,
|
||||
&input.agent_id,
|
||||
node_id,
|
||||
) {
|
||||
return Ok(LaunchAgentOutput {
|
||||
session,
|
||||
assigned_conversation_id: None,
|
||||
@ -1558,15 +1574,22 @@ impl LaunchAgent {
|
||||
// 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(&input.agent_id) {
|
||||
let host_node = structured.node_for_agent(&input.agent_id);
|
||||
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(&input.agent_id, 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.
|
||||
@ -1772,7 +1795,8 @@ impl LaunchAgent {
|
||||
// l'orchestrateur). C'est cet id — et **non** l'id de session moteur —
|
||||
// qui retrouve log + handoff au (re)lancement (P7) et survit au swap.
|
||||
let pair_conversation_id = input.conversation_id.clone().unwrap_or_else(|| {
|
||||
ConversationId::for_pair(
|
||||
ConversationId::for_project_pair(
|
||||
input.project.id,
|
||||
ConversationParty::User,
|
||||
ConversationParty::agent(agent.id),
|
||||
)
|
||||
@ -1788,6 +1812,7 @@ impl LaunchAgent {
|
||||
&run_dir,
|
||||
&session_plan,
|
||||
pair_conversation_id,
|
||||
input.project.id,
|
||||
&input.project.root,
|
||||
input.node_id,
|
||||
size,
|
||||
@ -1824,7 +1849,8 @@ impl LaunchAgent {
|
||||
size,
|
||||
);
|
||||
session.status = SessionStatus::Running;
|
||||
self.sessions.insert(handle, session.clone());
|
||||
self.sessions
|
||||
.insert_in_project(input.project.id, handle, session.clone());
|
||||
|
||||
self.events.publish(DomainEvent::AgentLaunched {
|
||||
agent_id: agent.id,
|
||||
@ -1863,6 +1889,7 @@ impl LaunchAgent {
|
||||
run_dir: &ProjectPath,
|
||||
session_plan: &SessionPlan,
|
||||
pair_conversation_id: String,
|
||||
project_id: domain::ProjectId,
|
||||
root: &ProjectPath,
|
||||
node_id: Option<NodeId>,
|
||||
size: PtySize,
|
||||
@ -1889,7 +1916,7 @@ impl LaunchAgent {
|
||||
|
||||
// Enregistre la session vivante (invariant « 1 session/agent » : déjà gardé en
|
||||
// amont sur les deux registres).
|
||||
structured.insert(Arc::clone(&session), agent.id, node_id);
|
||||
structured.insert_in_project(project_id, Arc::clone(&session), agent.id, node_id);
|
||||
|
||||
// ── SÉPARATION DES DEUX CLÉS (ARCHITECTURE §19.7, lot P8a) ──
|
||||
// - **id de paire** (`pair_conversation_id`) : clé **logique** persistée sur
|
||||
|
||||
Reference in New Issue
Block a user