use std::{
path::{Path, PathBuf},
sync::{Arc, Mutex},
time::{Duration, Instant},
};
use serde_json::{Value, json};
use crate::configuration::BundleMember;
use crate::envelope::PromptBatchSettings;
use crate::runtime::inscriptions::emit_inscription;
use crate::runtime::signals::shutdown_requested;
use crate::transports::contract::OutcomeFuture;
use crate::transports::{
Chooser, DeliveryEnvelope, OutputView, StartupContext, Transport, TransportError,
TransportStatus, WorkerReadinessState,
};
use super::{
ACP_ERROR_CODE_INITIALIZE_FAILED, AcpBootstrapError, AcpTransport, bootstrap_acp_worker_runtime,
};
const RESPAWN_BACKOFF_MAX_MS_ENVVAR: &str = "AGENTMUX_RELAY_ACP_RESPAWN_BACKOFF_MAX_MS";
const RESPAWN_SLEEP_POLL_MS: u64 = 50;
const RESPAWN_BACKOFF_INITIAL_MS: u64 = 1_000;
const RESPAWN_BACKOFF_CAP_DEFAULT_MS: u64 = 30_000;
const RESPAWN_INIT_FAILURE_THRESHOLD: u32 = 3;
const RESPAWN_MONITOR_POLL_MS: u64 = 100;
const RESPAWN_TRIGGER_REASON: &str = "worker_unavailable";
pub type MirrorStateFn = Arc<dyn Fn(WorkerReadinessState) + Send + Sync>;
pub type PublishOutputFn = Arc<dyn Fn(Option<Arc<dyn OutputView>>) + Send + Sync>;
pub type BroadcastUiFn = Arc<dyn Fn(&str, Value) + Send + Sync>;
pub type InvalidateChoicesFn = Arc<dyn Fn() + Send + Sync>;
#[derive(Clone)]
pub struct AcpDriverServices {
pub mirror_state: MirrorStateFn,
pub publish_output: PublishOutputFn,
pub broadcast_ui: BroadcastUiFn,
pub invalidate_choices: InvalidateChoicesFn,
pub chooser: Chooser,
}
impl std::fmt::Debug for AcpDriverServices {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AcpDriverServices").finish_non_exhaustive()
}
}
pub struct AcpWorkerDriver {
transport: Arc<Mutex<AcpTransport>>,
namespace: String,
runtime_directory: PathBuf,
target_member: BundleMember,
services: AcpDriverServices,
}
impl AcpWorkerDriver {
#[must_use]
pub fn new(
target_member: BundleMember,
runtime_directory: PathBuf,
namespace: String,
services: AcpDriverServices,
batch_settings: PromptBatchSettings,
) -> Self {
Self {
transport: Arc::new(Mutex::new(AcpTransport::new(
batch_settings,
Some(Arc::clone(&services.mirror_state)),
))),
namespace,
runtime_directory,
target_member,
services,
}
}
fn target_session(&self) -> &str {
self.target_member.id.as_str()
}
fn lock_transport(&self) -> std::sync::MutexGuard<'_, AcpTransport> {
self.transport.lock().expect("acp transport mutex poisoned")
}
pub async fn bootstrap(&mut self) {
(self.services.mirror_state)(WorkerReadinessState::Initializing);
let handle = self.lock_transport().give_output();
(self.services.publish_output)(handle);
self.lock_transport()
.prepare_for_startup(self.services.chooser.clone(), self.target_member.id.clone());
let runtime_directory = self.runtime_directory.clone();
let target_member = self.target_member.clone();
let bootstrap_result = tokio::task::spawn_blocking(move || {
bootstrap_acp_worker_runtime(&runtime_directory, &target_member)
})
.await
.expect("ACP worker bootstrap task panicked");
match bootstrap_result {
Ok(runtime) => {
self.lock_transport().install_runtime(runtime);
(self.services.mirror_state)(WorkerReadinessState::Available);
}
Err(error) => {
self.lock_transport().mark_runtime_unavailable();
(self.services.mirror_state)(WorkerReadinessState::Unavailable);
emit_inscription(
"relay.acp.worker.bootstrap_failed",
&json!({
"namespace": self.namespace,
"target_session": self.target_session(),
"error_code": error.code,
"reason": error.reason,
}),
);
self.lock_transport().signal_respawn();
}
}
self.spawn_respawn_monitor();
}
fn spawn_respawn_monitor(&self) {
let transport = Arc::clone(&self.transport);
let respawn_needed = self.lock_transport().respawn_needed_subscribe();
let services = self.services.clone();
let namespace = self.namespace.clone();
let runtime_directory = self.runtime_directory.clone();
let target_member = self.target_member.clone();
tokio::spawn(acp_respawn_monitor(
transport,
respawn_needed,
services,
AcpRespawnState::new(),
namespace,
runtime_directory,
target_member,
));
}
}
impl Transport for AcpWorkerDriver {
fn startup(&mut self, context: StartupContext) -> Result<TransportStatus, TransportError> {
self.lock_transport().startup(context)
}
fn mailw(&mut self, envelope: DeliveryEnvelope) -> OutcomeFuture {
self.lock_transport().mailw(envelope)
}
fn raww(&mut self, content: String, append_enter: bool) -> OutcomeFuture {
self.lock_transport().raww(content, append_enter)
}
fn is_ready(&self) -> bool {
self.lock_transport().is_ready()
}
fn shutdown(&mut self) {
self.lock_transport().shutdown();
}
fn give_output(&self) -> Option<Arc<dyn OutputView>> {
self.lock_transport().give_output()
}
}
async fn acp_respawn_monitor(
transport: Arc<Mutex<AcpTransport>>,
mut respawn_needed: tokio::sync::watch::Receiver<bool>,
services: AcpDriverServices,
mut respawn_state: AcpRespawnState,
namespace: String,
runtime_directory: PathBuf,
target_member: BundleMember,
) {
let poll = Duration::from_millis(RESPAWN_MONITOR_POLL_MS);
loop {
tokio::select! {
biased;
changed = respawn_needed.changed() => {
if changed.is_err() {
return;
}
}
_ = tokio::time::sleep(poll) => {}
}
if shutdown_requested() {
return;
}
if !*respawn_needed.borrow_and_update() {
continue;
}
run_acp_respawn(
&transport,
&services,
&mut respawn_state,
namespace.as_str(),
runtime_directory.as_path(),
&target_member,
)
.await;
transport
.lock()
.expect("acp transport mutex poisoned")
.clear_respawn_signal();
}
}
async fn run_acp_respawn(
transport: &Arc<Mutex<AcpTransport>>,
services: &AcpDriverServices,
respawn_state: &mut AcpRespawnState,
namespace: &str,
runtime_directory: &Path,
target_member: &BundleMember,
) {
let target_session = target_member.id.as_str();
transport
.lock()
.expect("acp transport mutex poisoned")
.release_runtime();
loop {
if shutdown_requested() {
return;
}
let backoff = respawn_state.advance();
(services.mirror_state)(WorkerReadinessState::Recovering);
emit_inscription(
"relay.acp.respawn.triggered",
&json!({
"namespace": namespace,
"target_session": target_session,
"attempt": respawn_state.attempt,
"trigger_reason": RESPAWN_TRIGGER_REASON,
"backoff_ms": backoff.as_millis() as u64,
}),
);
(services.broadcast_ui)(
"acp_worker_respawn_started",
json!({
"attempt": respawn_state.attempt,
"trigger_reason": RESPAWN_TRIGGER_REASON,
"backoff_ms": backoff.as_millis() as u64,
}),
);
if !sleep_with_shutdown_gate(backoff).await {
return;
}
(services.invalidate_choices)();
transport
.lock()
.expect("acp transport mutex poisoned")
.prepare_for_startup(services.chooser.clone(), target_member.id.clone());
let bootstrap_dir = runtime_directory.to_path_buf();
let bootstrap_member = target_member.clone();
let bootstrap_result = tokio::task::spawn_blocking(move || {
bootstrap_acp_worker_runtime(&bootstrap_dir, &bootstrap_member)
})
.await
.expect("ACP respawn bootstrap task panicked");
match bootstrap_result {
Ok(runtime) => {
transport
.lock()
.expect("acp transport mutex poisoned")
.install_runtime(runtime);
(services.mirror_state)(WorkerReadinessState::Available);
emit_inscription(
"relay.acp.respawn.succeeded",
&json!({
"namespace": namespace,
"target_session": target_session,
"attempt": respawn_state.attempt,
}),
);
(services.broadcast_ui)(
"acp_worker_respawn_completed",
json!({
"attempt": respawn_state.attempt,
"outcome": "succeeded",
}),
);
respawn_state.reset_on_success();
return;
}
Err(error) => {
respawn_state.record_failure(&error);
emit_inscription(
"relay.acp.respawn.attempt_failed",
&json!({
"namespace": namespace,
"target_session": target_session,
"attempt": respawn_state.attempt,
"error_code": error.code,
"reason": error.reason,
}),
);
if error.is_permanent() || respawn_state.should_give_up() {
transport
.lock()
.expect("acp transport mutex poisoned")
.mark_runtime_unavailable();
(services.mirror_state)(WorkerReadinessState::Unavailable);
emit_inscription(
"relay.acp.respawn.permanent_failure",
&json!({
"namespace": namespace,
"target_session": target_session,
"attempts": respawn_state.attempt,
"final_error_code": error.code,
"reason": error.reason,
}),
);
(services.broadcast_ui)(
"acp_worker_respawn_completed",
json!({
"attempts": respawn_state.attempt,
"outcome": "permanent_failure",
"final_error_code": error.code,
"reason": error.reason,
}),
);
return;
}
}
}
}
}
async fn sleep_with_shutdown_gate(duration: Duration) -> bool {
let deadline = Instant::now() + duration;
while Instant::now() < deadline {
if shutdown_requested() {
return false;
}
let remaining = deadline.saturating_duration_since(Instant::now());
let poll = remaining.min(Duration::from_millis(RESPAWN_SLEEP_POLL_MS));
if poll.is_zero() {
break;
}
tokio::time::sleep(poll).await;
}
!shutdown_requested()
}
fn respawn_backoff_cap_ms() -> u64 {
std::env::var(RESPAWN_BACKOFF_MAX_MS_ENVVAR)
.ok()
.and_then(|raw| raw.trim().parse::<u64>().ok())
.filter(|value| *value > 0)
.unwrap_or(RESPAWN_BACKOFF_CAP_DEFAULT_MS)
}
struct AcpRespawnState {
attempt: u32,
next_backoff_ms: u64,
last_initialize_failure_reason: Option<String>,
consecutive_initialize_failures: u32,
}
impl AcpRespawnState {
fn new() -> Self {
Self {
attempt: 0,
next_backoff_ms: 0,
last_initialize_failure_reason: None,
consecutive_initialize_failures: 0,
}
}
fn advance(&mut self) -> Duration {
let cap = respawn_backoff_cap_ms();
let backoff = if self.next_backoff_ms == 0 {
RESPAWN_BACKOFF_INITIAL_MS.min(cap)
} else {
self.next_backoff_ms.min(cap)
};
self.next_backoff_ms = backoff.saturating_mul(2).min(cap);
self.attempt = self.attempt.saturating_add(1);
Duration::from_millis(backoff)
}
fn record_failure(&mut self, error: &AcpBootstrapError) {
if error.code == ACP_ERROR_CODE_INITIALIZE_FAILED {
if self.last_initialize_failure_reason.as_deref() == Some(error.reason.as_str()) {
self.consecutive_initialize_failures =
self.consecutive_initialize_failures.saturating_add(1);
} else {
self.last_initialize_failure_reason = Some(error.reason.clone());
self.consecutive_initialize_failures = 1;
}
} else {
self.last_initialize_failure_reason = None;
self.consecutive_initialize_failures = 0;
}
}
fn should_give_up(&self) -> bool {
self.consecutive_initialize_failures >= RESPAWN_INIT_FAILURE_THRESHOLD
}
fn reset_on_success(&mut self) {
self.attempt = 0;
self.next_backoff_ms = 0;
self.last_initialize_failure_reason = None;
self.consecutive_initialize_failures = 0;
}
}