use std::{
collections::HashMap,
path::{Path, PathBuf},
sync::{Mutex, OnceLock},
};
use tokio::sync::{broadcast, watch};
use super::async_worker::{AcpWorkerReadinessState, AsyncWorkerKey, build_worker_key};
const PERMISSION_QUEUE_BROADCAST_CAPACITY: usize = 64;
#[derive(Clone, Debug)]
pub enum PermissionQueueEvent {
Enqueued {
permission_request_id: String,
message_id: String,
target_session: String,
},
Resolved {
permission_request_id: String,
target_session: String,
},
Invalidated {
permission_request_id: String,
target_session: String,
reason_code: String,
},
}
static WORKER_STATE_PUBLISHERS: OnceLock<
Mutex<HashMap<AsyncWorkerKey, watch::Sender<Option<AcpWorkerReadinessState>>>>,
> = OnceLock::new();
static PERMISSION_QUEUE_PUBLISHERS: OnceLock<
Mutex<HashMap<PathBuf, broadcast::Sender<PermissionQueueEvent>>>,
> = OnceLock::new();
fn worker_state_publishers()
-> &'static Mutex<HashMap<AsyncWorkerKey, watch::Sender<Option<AcpWorkerReadinessState>>>> {
WORKER_STATE_PUBLISHERS.get_or_init(|| Mutex::new(HashMap::new()))
}
fn permission_queue_publishers()
-> &'static Mutex<HashMap<PathBuf, broadcast::Sender<PermissionQueueEvent>>> {
PERMISSION_QUEUE_PUBLISHERS.get_or_init(|| Mutex::new(HashMap::new()))
}
pub fn subscribe_acp_worker_state(
bundle_name: &str,
runtime_directory: &Path,
target_session: &str,
) -> watch::Receiver<Option<AcpWorkerReadinessState>> {
let key = build_worker_key(bundle_name, runtime_directory, target_session);
let mut publishers = worker_state_publishers()
.lock()
.expect("worker state publishers mutex poisoned");
publishers
.entry(key)
.or_insert_with(|| watch::channel(None).0)
.subscribe()
}
pub fn subscribe_permission_queue_events(
runtime_directory: &Path,
) -> broadcast::Receiver<PermissionQueueEvent> {
let mut publishers = permission_queue_publishers()
.lock()
.expect("permission queue publishers mutex poisoned");
publishers
.entry(runtime_directory.to_path_buf())
.or_insert_with(|| broadcast::channel(PERMISSION_QUEUE_BROADCAST_CAPACITY).0)
.subscribe()
}
pub(super) fn publish_acp_worker_state(key: &AsyncWorkerKey, state: AcpWorkerReadinessState) {
let mut publishers = worker_state_publishers()
.lock()
.expect("worker state publishers mutex poisoned");
let sender = publishers
.entry(key.clone())
.or_insert_with(|| watch::channel(None).0);
let _ = sender.send(Some(state));
}
pub(super) fn publish_permission_queue_event(
runtime_directory: &Path,
event: PermissionQueueEvent,
) {
let mut publishers = permission_queue_publishers()
.lock()
.expect("permission queue publishers mutex poisoned");
let sender = publishers
.entry(runtime_directory.to_path_buf())
.or_insert_with(|| broadcast::channel(PERMISSION_QUEUE_BROADCAST_CAPACITY).0);
let _ = sender.send(event);
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[tokio::test(flavor = "current_thread")]
async fn publishers_deliver_state_and_queue_events_to_subscribers() {
let runtime_directory = PathBuf::from("/tmp/agentmux-observability-test-fake-runtime");
let bundle_name = "obstest_bundle";
let target_session = "obstest_target";
let mut state_receiver =
subscribe_acp_worker_state(bundle_name, runtime_directory.as_path(), target_session);
assert_eq!(*state_receiver.borrow_and_update(), None);
let key = build_worker_key(bundle_name, runtime_directory.as_path(), target_session);
publish_acp_worker_state(&key, AcpWorkerReadinessState::Unavailable);
state_receiver
.changed()
.await
.expect("watch sender outlives subscriber");
assert_eq!(
*state_receiver.borrow(),
Some(AcpWorkerReadinessState::Unavailable)
);
let mut queue_receiver = subscribe_permission_queue_events(runtime_directory.as_path());
publish_permission_queue_event(
runtime_directory.as_path(),
PermissionQueueEvent::Enqueued {
permission_request_id: "perm-1".to_string(),
message_id: "msg-1".to_string(),
target_session: target_session.to_string(),
},
);
publish_permission_queue_event(
runtime_directory.as_path(),
PermissionQueueEvent::Invalidated {
permission_request_id: "perm-1".to_string(),
target_session: target_session.to_string(),
reason_code: "runtime_permission_request_invalidated_by_respawn".to_string(),
},
);
match queue_receiver.recv().await.expect("first event") {
PermissionQueueEvent::Enqueued {
permission_request_id,
message_id,
target_session: actual_target,
} => {
assert_eq!(permission_request_id, "perm-1");
assert_eq!(message_id, "msg-1");
assert_eq!(actual_target, target_session);
}
other => panic!("expected Enqueued, got {other:?}"),
}
match queue_receiver.recv().await.expect("second event") {
PermissionQueueEvent::Invalidated {
permission_request_id,
target_session: actual_target,
reason_code,
} => {
assert_eq!(permission_request_id, "perm-1");
assert_eq!(actual_target, target_session);
assert_eq!(
reason_code,
"runtime_permission_request_invalidated_by_respawn"
);
}
other => panic!("expected Invalidated, got {other:?}"),
}
}
}