Skip to main content

ReactiveHandler

Struct ReactiveHandler 

Source
pub struct ReactiveHandler {
    inner: Mutex<Handler>,
    needs_review_hooks: RwLock<Vec<Arc<dyn Fn(&str) + Send + Sync>>>,
}
Expand description

Thread-safe wrapper around Handler.

Fields§

§inner: Mutex<Handler>§needs_review_hooks: RwLock<Vec<Arc<dyn Fn(&str) + Send + Sync>>>

Called with the target block_id after a successful delivery whose marker says ESCALATE=required — the notification Router turns it into a “needs your review” OS notification (fixed text, no content). A list, not a slot: this handler is process-global, and each Router (one per broker/AppState) registers its own hook and ignores blocks that aren’t in its own store — so a second AppState can never steal another’s notifications (ReAgent P1 on #3654).

Implementations§

Source§

impl ReactiveHandler

Source

pub fn new() -> Self

Source

pub fn add_needs_review_hook(&self, f: Arc<dyn Fn(&str) + Send + Sync>)

Register a needs-review hook. Must not block or call back into this handler; hooks run after the inner lock is released.

Source

fn after_delivery(&self, resp: &InjectionResponse)

Source

pub fn set_input_sender( &self, sender: Arc<dyn Fn(&str, &[u8]) -> Result<(), String> + Send + Sync>, )

Source

pub fn set_message_sender( &self, sender: Arc<dyn Fn(&str, &str) -> Result<SenderDelivery, String> + Send + Sync>, )

Source

pub fn set_agent_identity_confirmer( &self, confirmer: Arc<dyn Fn(&str) -> Option<String> + Send + Sync>, )

Source

pub fn set_stable_agent_identity_confirmer( &self, confirmer: Arc<dyn Fn(&str) -> Option<String> + Send + Sync>, )

Source

pub fn set_uid_identity_confirmer( &self, confirmer: Arc<dyn Fn(&str) -> Option<String> + Send + Sync>, )

See the inner Handler::set_uid_identity_confirmer (identity M2).

Source

pub fn set_block_liveness(&self, probe: Arc<dyn Fn(&str) -> bool + Send + Sync>)

See the inner Handler::set_block_liveness (identity M2).

Source

pub fn register_agent_full( &self, display: &str, block_id: &str, tab_id: Option<&str>, registration_nonce: u64, stable: Option<&str>, uid: Option<&str>, no_uid_counter: &'static str, ) -> Result<(), String>

See the inner Handler::register_agent_full (identity M2).

Source

pub fn try_register_agent_full( &self, display: &str, block_id: &str, tab_id: Option<&str>, registration_nonce: u64, stable: Option<&str>, uid: Option<&str>, no_uid_counter: &'static str, ) -> Result<(), String>

[try_register_agent_with_nonce]’s full-identity form — same non-blocking, bounded-retry lock discipline, for the spawn path.

Source

pub fn names_for_block(&self, block_id: &str) -> Vec<String>

Every name block_id is bound under — see Handler::names_for_block.

Source

fn try_lock_bounded(&self) -> Result<MutexGuard<'_, Handler>, String>

Bounded try_lock, shared by the spawn-path entry points — see [try_register_agent_with_nonce]’s doc comment for the reasoning.

Source

pub fn set_include_source(&self, include: bool)

Source

pub fn register_agent( &self, agent_id: &str, block_id: &str, tab_id: Option<&str>, ) -> Result<(), String>

Test-facing name-only form — see Handler::register_agent.

Source

pub fn register_agent_with_nonce( &self, agent_id: &str, block_id: &str, tab_id: Option<&str>, registration_nonce: u64, alias: Option<&str>, ) -> Result<(), String>

Source

pub fn try_register_agent_with_nonce( &self, agent_id: &str, block_id: &str, tab_id: Option<&str>, registration_nonce: u64, alias: Option<&str>, ) -> Result<(), String>

Like [register_agent_with_nonce], but never blocks indefinitely: if the lock is still held after a short bounded retry (try_lock_bounded), this returns an error instead of waiting for it. Test-only; the spawn path uses the full-identity form, Self::try_register_agent_full, which shares this reasoning.

The case that matters is this exact call running on the same OS thread as an in-flight inject_message — the reactive-delivery spawn fallback (bootstrap/delivery.rs’s install_agent_turn_delivery) runs a respawn synchronously on the injecting thread via block_in_place, and that respawn’s own auto-registration (PersistentSubprocessController::spawn_process) used to re-lock this same non-reentrant Mutex<Handler> two modules away from the call site TurnRegistration::Skip actually guards — permanently deadlocking the reactive handler process-wide (every persistent agent’s first message after an idle period, or after any srv restart). See docs/incident/INCIDENT_2026_09_07_BACKEND_UPTIME_TIMER_FROZEN.md.

Skipping in that case loses nothing: inject_message only reaches the spawn fallback after resolving target_agent through resolve_target (the uid_to_block / name_to_blocks registry), so this exact agent/block pair is registered BY CONSTRUCTION before this call is ever reached on that path — the same “redundant, not just deadlock-prone” reasoning documented on TurnRegistration::Skip for the sibling re-lock it guards.

Genuine cross-thread contention is a real, separate risk this method does not get to treat as harmless: respawn_once_for_ leftover_queue and other internal callers reach spawn_process directly, with no run_agent_turn Register-tail behind them to repair a skip — a losing race there can leave a live respawned agent unregistered and unreachable until a human intervenes via the UI (#3084). The retry loop in try_lock_bounded exists for exactly that case: a same-thread reentrant call can never succeed no matter how many times it retries (nothing on that call stack can release the lock), but ordinary cross-thread contention — a few HashMap reads under the same lock elsewhere, typically microseconds — very likely clears within the retry budget. See the comment in this method’s body for the full reasoning.

A skip’s caller (spawn_process) is also responsible for not trusting registration_nonce as this spawn’s exit-time cleanup key when the call returns Err here — the registration this skip left in place (if any) still belongs to whichever nonce is actually on record, not to this spawn’s own unwritten one. See spawn_process’s handling of try_register_agent_full’s Err arm.

Source

pub fn unregister_agent(&self, agent_id: &str) -> UnregisterOutcome

See the inner Handler::unregister_agent — by name, refusing an ambiguous one (identity M2).

Source

pub fn unregister_block(&self, block_id: &str) -> Vec<String>

See the inner Handler::unregister_block. Returns the names the block was bound under.

Source

pub fn unregister_block_if_nonce( &self, block_id: &str, expected_nonce: u64, ) -> bool

See the inner Handler::unregister_block_if_nonce — atomic compare-and-remove under the handler lock (issue #2363).

Source

pub fn update_last_seen(&self, agent_id: &str)

Source

pub fn get_agent(&self, agent_id: &str) -> Option<AgentRegistration>

Source

pub fn has_live_name(&self, name: &str) -> bool

See the inner Handler::has_live_name.

Source

pub fn lookup_by_name(&self, name: &str) -> LookupOutcome

See the inner Handler::lookup_by_name (identity M2).

Source

pub fn get_agent_by_block(&self, block_id: &str) -> Option<AgentRegistration>

Used by server/app_api/fleet.rs’s ordinary (non-reentrant) request handlers — no longer dead code, but NOT safe to call from spawn_process’s skip-arm; see [try_get_agent_by_block] for why.

Source

pub fn try_get_agent_by_block( &self, block_id: &str, ) -> Option<AgentRegistration>

Non-blocking counterpart to [get_agent_by_block], for the one caller that can be running on a thread already holding this lock: spawn_process’s handling of a skipped registration (try_register_agent_with_nonce returning Err).

reagent P0 on PR #3084 (caught twice — the fix for the P1 nonce-leak finding introduced this exact regression on its first attempt): that fix originally called the plain, blocking [get_agent_by_block] from inside the skip arm. In the routine reentrant case — the same thread already holds self.inner via an in-flight inject_message — that blocking .lock() reproduces INCIDENT_2026_09_07’s exact permanent self-deadlock, on the very thread try_register_agent_ with_nonce’s own retry-then-fail was designed to protect. Same bounded-retry reasoning as that method: a same-thread reentrant call can never succeed (nothing on this call stack releases the lock) and correctly returns None after exhausting the budget — the caller’s nonce-leak mitigation simply doesn’t apply in that sub-case, which is a strict improvement on the pre-this-PR baseline (no cleanup, but no deadlock either), not a regression. Ordinary cross-thread contention gets a real chance to succeed, same as the sibling method.

Source

pub fn list_agents(&self) -> Vec<AgentRegistration>

Source

pub fn inject_message(&self, req: InjectionRequest) -> InjectionResponse

Source

pub fn deliver_system_note( &self, target_agent: &str, text: &str, ) -> Result<(), String>

Source

pub fn inject_held(&self, req: InjectionRequest) -> InjectionResponse

Source

pub fn has_uid_registration(&self, uid: &str) -> bool

Source

pub fn block_for_uid(&self, uid: &str) -> Option<String>

Source

pub fn get_audit_log(&self, limit: usize) -> Vec<AuditLogEntry>

Source

pub fn log_fleet_action_audit( &self, source_agent: Option<&str>, target_agent: &str, block_id: &str, action: &str, success: bool, error_message: Option<&str>, request_id: &str, reason: Option<&str>, )

Records an action that isn’t a jekt injection (fleet bulk-stop, pane-lifecycle close/etc. — crates/srv/src/server/app_api/{fleet, pane}.rs) into the SAME audit ring buffer, so it shows up in Warden’s Audit tab exactly like an ordinary injection (SPEC_MULTI_AGENT_FLEET_CONTROL_2026_08_20.md §6 — fleet actions get visibility there without Warden owning any new code). action fills the slot log_audit normally uses for the injected message text (e.g. "fleet.bulk-stop", "pane.close"); target_agent is the resolved agent name for block_id when known, else block_id itself (an unregistered/already-stopped block has no agent to name).

reason: an optional caller-supplied free-text note (e.g. ClosePane’s reason field, SPEC_AGENT_PANE_LIFECYCLE_CONTROL_2026_09_10.md §5.2). None for every pre-existing caller (FleetBulkStop has no such field today) — purely additive, no behavior change for them.

Source

pub fn record_supervisor_decision( &self, target_agent: &str, action: SupervisorAction, reason: &str, request_id: &str, source_agent: Option<&str>, source_uid: &str, ) -> Result<InjectionResponse, String>

Trait Implementations§

Source§

impl Default for ReactiveHandler

Source§

fn default() -> Self

Returns the “default value” for a type. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
§

impl<T> Downcast for T
where T: Any,

§

fn into_any(self: Box<T>) -> Box<dyn Any>

Convert Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>. Box<dyn Any> can then be further downcast into Box<ConcreteType> where ConcreteType implements Trait.
§

fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>

Convert Rc<Trait> (where Trait: Downcast) to Rc<Any>. Rc<Any> can then be further downcast into Rc<ConcreteType> where ConcreteType implements Trait.
§

fn as_any(&self) -> &(dyn Any + 'static)

Convert &Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot generate &Any’s vtable from &Trait’s.
§

fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)

Convert &mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot generate &mut Any’s vtable from &mut Trait’s.
§

impl<T> DowncastSync for T
where T: Any + Send + Sync,

§

fn into_any_arc(self: Arc<T>) -> Arc<dyn Any + Send + Sync> ⓘ

Convert Arc<Trait> (where Trait: Downcast) to Arc<Any>. Arc<Any> can then be further downcast into Arc<ConcreteType> where ConcreteType implements Trait.
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

§

impl<A, B, T> HttpServerConnExec<A, B> for T
where B: Body,

§

impl<T> Instrument for T

§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided [Span], returning an Instrumented wrapper. Read more
§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
§

impl<T> NoneValue for T
where T: Default,

§

type NoneType = T

§

fn null_value() -> T

The none-equivalent value.
§

impl<T> NoneValue for T
where T: Default,

§

type NoneType = T

§

fn null_value() -> T

The none-equivalent value.
§

impl<T> PolicyExt for T
where T: ?Sized,

§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns [Action::Follow] only if self and other return Action::Follow. Read more
§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns [Action::Follow] if either self or other returns Action::Follow. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

§

fn vzip(self) -> V

§

impl<T> WithSubscriber for T

§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a [WithDispatch] wrapper. Read more
§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a [WithDispatch] wrapper. Read more