fix(runtime): isolate agent state by project (#101)

This commit is contained in:
2026-07-25 22:14:02 +02:00
parent 6a87c4635f
commit 6e98fd89f7
52 changed files with 1894 additions and 805 deletions

View File

@ -10,7 +10,7 @@
use std::sync::Arc;
use domain::input::InputMediator;
use domain::{AgentId, NodeId, Project, SessionId};
use domain::{AgentId, NodeId, Project, RuntimeAgentKey, SessionId};
use crate::error::AppError;
use crate::orchestrator::OrchestratorService;
@ -65,11 +65,11 @@ impl AttachLiveAgent {
/// [`AppError::NotFound`] when the agent has no live session in either registry.
pub fn execute(&self, input: AttachLiveAgentInput) -> Result<AttachLiveAgentOutput, AppError> {
// PTY first, then structured (one-live-session-per-agent ⇒ at most one match).
if let Some(session) = self
.live
.pty
.rebind_agent_node(&input.agent_id, input.node_id)
{
if let Some(session) = self.live.pty.rebind_agent_node_in_project(
input.project.id,
&input.agent_id,
input.node_id,
) {
return Ok(AttachLiveAgentOutput {
agent_id: input.agent_id,
node_id: session.node_id,
@ -77,11 +77,11 @@ impl AttachLiveAgent {
kind: LiveSessionKind::Pty,
});
}
if let Some(session) = self
.live
.structured
.rebind_agent_node(&input.agent_id, input.node_id)
{
if let Some(session) = self.live.structured.rebind_agent_node_in_project(
input.project.id,
&input.agent_id,
input.node_id,
) {
return Ok(AttachLiveAgentOutput {
agent_id: input.agent_id,
node_id: input.node_id,
@ -168,28 +168,37 @@ impl StopLiveAgent {
&self,
input: StopLiveAgentInput,
) -> Result<StopLiveAgentOutput, AppError> {
self.stop_dependencies(input.agent_id).await;
self.stop_dependencies(&input.project, input.agent_id).await;
if let Some(mediator) = &self.input {
mediator.preempt(input.agent_id);
mediator.preempt(RuntimeAgentKey::new(input.project.id, input.agent_id));
}
self.stop_one(input.agent_id).await
self.stop_one_in_project(&input.project, input.agent_id)
.await
}
async fn stop_dependencies(&self, agent_id: AgentId) {
async fn stop_dependencies(&self, project: &Project, agent_id: AgentId) {
let Some(waits) = &self.waits else {
return;
};
for dep in waits.active_wait_dependencies(agent_id) {
if let Some(mediator) = &self.input {
mediator.preempt(dep);
mediator.preempt(RuntimeAgentKey::new(project.id, dep));
}
let _ = self.stop_one(dep).await;
let _ = self.stop_one_in_project(project, dep).await;
}
}
async fn stop_one(&self, agent_id: AgentId) -> Result<StopLiveAgentOutput, AppError> {
async fn stop_one_in_project(
&self,
project: &Project,
agent_id: AgentId,
) -> Result<StopLiveAgentOutput, AppError> {
// PTY first: delegate to the existing close primitive (removes + kills).
if let Some(session_id) = self.live.pty.session_for_agent(&agent_id) {
if let Some(session_id) = self
.live
.pty
.session_for_agent_in_project(project.id, &agent_id)
{
self.close
.execute(CloseTerminalInput { session_id })
.await?;
@ -201,7 +210,11 @@ impl StopLiveAgent {
}
// Structured: remove from the registry first (so the uniqueness guard no
// longer sees a live session), then shut the session down out of the lock.
if let Some(session_id) = self.live.structured.session_id_for_agent(&agent_id) {
if let Some(session_id) = self
.live
.structured
.session_id_for_agent_in_project(project.id, &agent_id)
{
if let Some(session) = self.live.structured.remove(&session_id) {
session
.shutdown()