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
impl ReactiveHandler
pub fn new() -> Self
Sourcepub fn add_needs_review_hook(&self, f: Arc<dyn Fn(&str) + Send + Sync>)
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.
fn after_delivery(&self, resp: &InjectionResponse)
pub fn set_input_sender( &self, sender: Arc<dyn Fn(&str, &[u8]) -> Result<(), String> + Send + Sync>, )
pub fn set_message_sender( &self, sender: Arc<dyn Fn(&str, &str) -> Result<SenderDelivery, String> + Send + Sync>, )
pub fn set_agent_identity_confirmer( &self, confirmer: Arc<dyn Fn(&str) -> Option<String> + Send + Sync>, )
Sourcepub fn set_stable_agent_identity_confirmer(
&self,
confirmer: Arc<dyn Fn(&str) -> Option<String> + Send + Sync>,
)
pub fn set_stable_agent_identity_confirmer( &self, confirmer: Arc<dyn Fn(&str) -> Option<String> + Send + Sync>, )
See the inner Handler::set_stable_agent_identity_confirmer.
Sourcepub fn set_uid_identity_confirmer(
&self,
confirmer: Arc<dyn Fn(&str) -> Option<String> + Send + Sync>,
)
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).
Sourcepub fn set_block_liveness(&self, probe: Arc<dyn Fn(&str) -> bool + Send + Sync>)
pub fn set_block_liveness(&self, probe: Arc<dyn Fn(&str) -> bool + Send + Sync>)
See the inner Handler::set_block_liveness (identity M2).
Sourcepub 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>
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).
Sourcepub 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>
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.
Sourcepub fn names_for_block(&self, block_id: &str) -> Vec<String>
pub fn names_for_block(&self, block_id: &str) -> Vec<String>
Every name block_id is bound under — see Handler::names_for_block.
Sourcefn try_lock_bounded(&self) -> Result<MutexGuard<'_, Handler>, String>
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.
pub fn set_include_source(&self, include: bool)
Sourcepub fn register_agent(
&self,
agent_id: &str,
block_id: &str,
tab_id: Option<&str>,
) -> Result<(), String>
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.
Sourcepub fn register_agent_with_nonce(
&self,
agent_id: &str,
block_id: &str,
tab_id: Option<&str>,
registration_nonce: u64,
alias: Option<&str>,
) -> Result<(), String>
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>
See the inner Handler::register_agent_with_nonce.
Sourcepub 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>
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.
Sourcepub fn unregister_agent(&self, agent_id: &str) -> UnregisterOutcome
pub fn unregister_agent(&self, agent_id: &str) -> UnregisterOutcome
See the inner Handler::unregister_agent — by name, refusing an
ambiguous one (identity M2).
Sourcepub fn unregister_block(&self, block_id: &str) -> Vec<String>
pub fn unregister_block(&self, block_id: &str) -> Vec<String>
See the inner Handler::unregister_block. Returns the names the
block was bound under.
Sourcepub fn unregister_block_if_nonce(
&self,
block_id: &str,
expected_nonce: u64,
) -> bool
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).
pub fn update_last_seen(&self, agent_id: &str)
pub fn get_agent(&self, agent_id: &str) -> Option<AgentRegistration>
Sourcepub fn has_live_name(&self, name: &str) -> bool
pub fn has_live_name(&self, name: &str) -> bool
See the inner Handler::has_live_name.
Sourcepub fn lookup_by_name(&self, name: &str) -> LookupOutcome
pub fn lookup_by_name(&self, name: &str) -> LookupOutcome
See the inner Handler::lookup_by_name (identity M2).
Sourcepub fn get_agent_by_block(&self, block_id: &str) -> Option<AgentRegistration>
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.
Sourcepub fn try_get_agent_by_block(
&self,
block_id: &str,
) -> Option<AgentRegistration>
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.
pub fn list_agents(&self) -> Vec<AgentRegistration>
pub fn inject_message(&self, req: InjectionRequest) -> InjectionResponse
Sourcepub fn inject_held(&self, req: InjectionRequest) -> InjectionResponse
pub fn inject_held(&self, req: InjectionRequest) -> InjectionResponse
See Handler::inject_held.
Sourcepub fn has_uid_registration(&self, uid: &str) -> bool
pub fn has_uid_registration(&self, uid: &str) -> bool
Sourcepub fn block_for_uid(&self, uid: &str) -> Option<String>
pub fn block_for_uid(&self, uid: &str) -> Option<String>
pub fn get_audit_log(&self, limit: usize) -> Vec<AuditLogEntry>
Sourcepub 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>,
)
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.
Sourcepub 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>
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>
See the inner Handler::record_supervisor_decision.
Trait Implementations§
Auto Trait Implementations§
impl !Freeze for ReactiveHandler
impl RefUnwindSafe for ReactiveHandler
impl Send for ReactiveHandler
impl Sync for ReactiveHandler
impl Unpin for ReactiveHandler
impl UnsafeUnpin for ReactiveHandler
impl UnwindSafe for ReactiveHandler
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
§impl<T> Downcast for Twhere
T: Any,
impl<T> Downcast for Twhere
T: Any,
§fn into_any(self: Box<T>) -> Box<dyn Any>
fn into_any(self: Box<T>) -> Box<dyn Any>
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>
fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
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)
fn as_any(&self) -> &(dyn Any + 'static)
&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)
fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
&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
impl<T> DowncastSync for T
impl<A, B, T> HttpServerConnExec<A, B> for Twhere
B: Body,
§impl<T> Instrument for T
impl<T> Instrument for T
§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
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 moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
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