From 72ed4b9eeb08076adabf13cb96188becaf5baba8 Mon Sep 17 00:00:00 2001 From: Codex CLI Date: Thu, 16 Jul 2026 14:45:43 +0800 Subject: [PATCH 1/2] fix(team): reclaim paused work when ACP runtime is gone --- crates/aionui-team/src/session.rs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/crates/aionui-team/src/session.rs b/crates/aionui-team/src/session.rs index 7a3e0452e..1dcfb5acc 100644 --- a/crates/aionui-team/src/session.rs +++ b/crates/aionui-team/src/session.rs @@ -1010,10 +1010,24 @@ impl TeamSession { if let Some(target) = outcome.cancel_target { if let Some(turn_id) = target.turn_id { let agent = self.scheduler.get_agent(slot_id).await?; - self.cancellation_port + // The runtime may already be gone (for example after an ACP + // process crash). Pausing must still terminalize the work + // batch in that case; otherwise the TeamRun retains a stale + // active turn forever and blocks every queued user message. + if let Err(error) = self + .cancellation_port .cancel_agent_turn(&self.user_id, &agent.conversation_id, &turn_id) .await - .map_err(|error| TeamError::InvalidRequest(error.to_string()))?; + { + warn!( + team_id = %self.team.id, + team_run_id, + slot_id, + turn_id, + error = %error, + "team slot pause could not cancel an already-unavailable runtime; reclaiming work batch" + ); + } self.team_event_emitter().broadcast_child_turn( TEAM_CHILD_TURN_CANCELLED_EVENT, TeamChildTurnPayload { From a1ee856bb9e405f5b59e6d26ceb4bfccb95d48c9 Mon Sep 17 00:00:00 2001 From: Codex CLI Date: Thu, 16 Jul 2026 19:05:04 +0800 Subject: [PATCH 2/2] fix(team): retain sessions for active board work --- crates/aionui-team/src/service.rs | 43 +++++++++++++++++-------------- crates/aionui-team/src/session.rs | 8 +++--- 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/crates/aionui-team/src/service.rs b/crates/aionui-team/src/service.rs index 858b38b83..4cd6fdc39 100644 --- a/crates/aionui-team/src/service.rs +++ b/crates/aionui-team/src/service.rs @@ -39,7 +39,7 @@ use crate::prompt_dump::TeamPromptDumpConfig; use crate::provisioning::{TeamAgentProvisioner, TeamConversationProvisioningPort}; use crate::session::{AgentMessageQueueResult, TeamSession, attach_member_runtime, spawn_attach_agent_process_bg}; use crate::team_run::TeamRunManager; -use crate::types::{Team, TeamAgent, TeammateRole}; +use crate::types::{TaskStatus, Team, TeamAgent, TeammateRole}; use crate::work_coordinator::RuntimeConstraint; use crate::work_source::WorkSource; use crate::workspace::validate_create_workspace_path; @@ -1751,6 +1751,29 @@ impl TeamSessionService { continue; } + // A team may have no currently-running turn while work is still + // assigned on the task board. Do not tear down its session in + // that gap: task-board work needs the live MCP/event-loop path to + // deliver a wake message to its owner. Fail closed if the board + // cannot be read. + let has_active_board_work = session + .task_board() + .list_tasks(&team_id) + .await + .map(|tasks| { + tasks + .iter() + .any(|task| matches!(task.status, TaskStatus::Pending | TaskStatus::InProgress)) + }) + .unwrap_or(true); + if has_active_board_work { + debug!( + team_id, + matched_idle_count, "team idle cleanup skipped because task-board work is active" + ); + continue; + } + if !agents.iter().all(|agent| { self.task_manager .get_task(&agent.conversation_id) @@ -1938,7 +1961,6 @@ impl TeamSessionService { to_slot_id: &str, content: &str, ) -> Result { - self.require_active_team_run_for_team_work(team_id).await?; let session = { let entry = self .sessions @@ -1968,23 +1990,6 @@ impl TeamSessionService { session.shutdown_agent(caller_slot_id, target_slot_id, reason).await } - /// Friendly pre-check used before invoking run-scoped team tools. This is - /// not a concurrency guarantee; any operation - /// that writes mailbox, projection, scheduler, spawn, shutdown, or wake state - /// must still acquire a TeamRun operation lease in TeamSession/TeamRunManager. - pub(crate) async fn require_active_team_run_for_team_work(&self, team_id: &str) -> Result<(), TeamError> { - let entry = self - .sessions - .get(team_id) - .ok_or_else(|| TeamError::SessionNotFound(team_id.into()))?; - if entry.session.team_run_manager().current_active_run_id().is_some() { - return Ok(()); - } - Err(TeamError::InvalidRequest( - "no active team run for run-scoped wake".into(), - )) - } - pub(crate) async fn wake_leader_after_recovery_message( &self, team_id: &str, diff --git a/crates/aionui-team/src/session.rs b/crates/aionui-team/src/session.rs index 1dcfb5acc..9215afef5 100644 --- a/crates/aionui-team/src/session.rs +++ b/crates/aionui-team/src/session.rs @@ -610,9 +610,11 @@ impl TeamSession { slot_id: to_slot_id.to_owned(), role: target_role_for(to_agent.role), source: WorkSource::McpSendMessage, - binding: CausalBinding::InheritRunningBatch { - caller_slot_id: from_slot_id.to_owned(), - }, + // A teammate message must wake its target even after the previous + // user-visible run has settled. Preserve run causality when one + // exists, otherwise enqueue background work instead of rejecting + // the message for lacking an active run. + binding: CausalBinding::ActiveRunOrBackground, })?; let mailbox_message = match self .mailbox