agent conversation fix

This commit is contained in:
2026-06-27 12:42:37 +02:00
parent c2d1a669c5
commit 1fc7869160
38 changed files with 1173 additions and 1549 deletions

View File

@ -9,9 +9,11 @@
use std::sync::Arc;
use domain::input::InputMediator;
use domain::{AgentId, NodeId, Project, SessionId};
use crate::error::AppError;
use crate::orchestrator::OrchestratorService;
use crate::terminal::{CloseTerminal, CloseTerminalInput, LiveSessionKind, LiveSessions};
/// Input for [`AttachLiveAgent::execute`].
@ -125,13 +127,36 @@ pub struct StopLiveAgentOutput {
pub struct StopLiveAgent {
live: Arc<LiveSessions>,
close: Arc<CloseTerminal>,
input: Option<Arc<dyn InputMediator>>,
waits: Option<Arc<OrchestratorService>>,
}
impl StopLiveAgent {
/// Builds the use case from the live-session registry and the close primitive.
#[must_use]
pub fn new(live: Arc<LiveSessions>, close: Arc<CloseTerminal>) -> Self {
Self { live, close }
Self {
live,
close,
input: None,
waits: None,
}
}
/// Wires cancellation helpers used for user-driven stop cascades.
///
/// Stopping A while A waits on B should stop B too. The orchestrator service is
/// only used as a read-only provider of active wait edges; the input mediator is
/// used to preempt the running turn before the session is torn down.
#[must_use]
pub fn with_cascade(
mut self,
input: Arc<dyn InputMediator>,
waits: Arc<OrchestratorService>,
) -> Self {
self.input = Some(input);
self.waits = Some(waits);
self
}
/// Tears down the agent's live session.
@ -143,20 +168,40 @@ impl StopLiveAgent {
&self,
input: StopLiveAgentInput,
) -> Result<StopLiveAgentOutput, AppError> {
self.stop_dependencies(input.agent_id).await;
if let Some(mediator) = &self.input {
mediator.preempt(input.agent_id);
}
self.stop_one(input.agent_id).await
}
async fn stop_dependencies(&self, 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);
}
let _ = self.stop_one(dep).await;
}
}
async fn stop_one(&self, 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(&input.agent_id) {
if let Some(session_id) = self.live.pty.session_for_agent(&agent_id) {
self.close
.execute(CloseTerminalInput { session_id })
.await?;
return Ok(StopLiveAgentOutput {
agent_id: input.agent_id,
agent_id,
session_id,
kind: LiveSessionKind::Pty,
});
}
// 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(&input.agent_id) {
if let Some(session_id) = self.live.structured.session_id_for_agent(&agent_id) {
if let Some(session) = self.live.structured.remove(&session_id) {
session
.shutdown()
@ -164,14 +209,13 @@ impl StopLiveAgent {
.map_err(|e| AppError::Process(e.to_string()))?;
}
return Ok(StopLiveAgentOutput {
agent_id: input.agent_id,
agent_id,
session_id,
kind: LiveSessionKind::Structured,
});
}
Err(AppError::NotFound(format!(
"running session for agent {}",
input.agent_id
"running session for agent {agent_id}"
)))
}
}

View File

@ -13,11 +13,11 @@ pub use actions::{
AttachLiveAgent, AttachLiveAgentInput, AttachLiveAgentOutput, StopLiveAgent,
StopLiveAgentInput, StopLiveAgentOutput,
};
pub use reconcile::{ReconcileLiveState, ReconcileLiveStateInput};
pub use live::{
GetLiveStateLean, LeanLiveEntry, LeanLiveState, UpdateLiveState, UpdateLiveStateInput,
LIVE_STATE_MAX_ENTRIES, LIVE_STATE_TTL_MS,
};
pub use reconcile::{ReconcileLiveState, ReconcileLiveStateInput};
use std::collections::{HashMap, HashSet};
use std::sync::Arc;

View File

@ -173,7 +173,11 @@ mod tests {
run(store.clone(), HashSet::new(), 999).await;
let after = store.load().await.unwrap();
assert_eq!(after.entries.len(), 1, "still one keyed row (upsert, not append)");
assert_eq!(
after.entries.len(),
1,
"still one keyed row (upsert, not append)"
);
let row = &after.entries[0];
assert_eq!(row.status, WorkStatus::Idle, "orphan downgraded to idle");
assert_eq!(row.progress.as_deref(), Some(STALE_AT_RESTART_MARKER));
@ -225,6 +229,9 @@ mod tests {
let row = &store.load().await.unwrap().entries[0];
assert_eq!(row.status, WorkStatus::Idle);
assert_eq!(row.updated_at_ms, 3, "idle row never reconciled, not restamped");
assert_eq!(
row.updated_at_ms, 3,
"idle row never reconciled, not restamped"
);
}
}