Skip to main content

net/adapter/net/
mesh_rpc.rs

1//! `Mesh::serve_rpc` / `Mesh::call` glue — the wire-up between
2//! `MeshNode`'s pub/sub + per-channel-hash dispatch hook and the
3//! `cortex::rpc` server / client folds.
4//!
5//! See `docs/misc/NRPC_DESIGN.md` for the full architectural framing.
6//! In short:
7//!
8//! - `serve_rpc(service, handler)` registers an inbound dispatcher
9//!   for `<service>.requests`'s channel hash. The dispatcher pushes
10//!   inbound REQUEST/CANCEL events through the
11//!   [`crate::adapter::net::cortex::RpcServerFold`], which spawns
12//!   the user handler. The fold's emit closure publishes RESPONSE
13//!   events on `<service>.replies.<caller_origin>` via
14//!   [`MeshNode::publish`].
15//!
16//! - `call(target, service, payload, opts)` allocates a `call_id`,
17//!   registers a oneshot in the per-Mesh `RpcClientPending`,
18//!   subscribes to its own reply channel from `target` (lazy,
19//!   cached), publishes the REQUEST envelope on `<service>.requests`,
20//!   awaits the oneshot. Drop sends a CANCEL.
21//!
22//! Phase 1 surface — direct entity-to-entity addressing
23//! (`call(target_node_id, ...)`), no service discovery layer yet.
24//! Phase 2 will add `call_service(name, ...)` over the existing
25//! capability-announcement registry.
26
27use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU8, Ordering};
28use std::sync::Arc;
29use std::time::Instant;
30
31use bytes::Bytes;
32use parking_lot::Mutex;
33use tokio::sync::mpsc;
34use tokio::task::JoinHandle;
35
36use super::channel::{ChannelHash, ChannelId, ChannelName, ChannelPublisher, PublishConfig};
37use super::cortex::{
38    build_trace_headers, encode_request_grant, encode_stream_grant, EventMeta,
39    RpcAsyncResponseEmitter, RpcCancellationToken, RpcClientFold, RpcClientStreamingHandler,
40    RpcContext, RpcDuplexFold, RpcDuplexHandler, RpcHandler, RpcHandlerError, RpcInboundDispatcher,
41    RpcInboundEvent, RpcRequestChunkPayload, RpcRequestGrantEmitter, RpcRequestPayload,
42    RpcResponseEmitter, RpcResponsePayload, RpcServerFold, RpcServerStreamingFold, RpcStatus,
43    RpcStreamingHandler, RpcStreamingRequestFold, StreamItem, TraceContext, DISPATCH_RPC_CANCEL,
44    DISPATCH_RPC_REQUEST, DISPATCH_RPC_REQUEST_CHUNK, DISPATCH_RPC_REQUEST_GRANT,
45    DISPATCH_RPC_STREAM_GRANT, EVENT_META_SIZE, FLAG_RPC_CLIENT_STREAMING_REQUEST,
46    FLAG_RPC_PROPAGATE_TRACE, FLAG_RPC_REQUEST_END, FLAG_RPC_STREAMING_RESPONSE,
47    HEADER_NRPC_REQUEST_WINDOW_INITIAL, HEADER_NRPC_STREAM_WINDOW_INITIAL,
48};
49use super::mesh_rpc_metrics::{CallMetricsGuard, CallOutcome};
50use crate::error::AdapterError;
51
52use super::mesh::MeshNode;
53use super::redex::{RedexEntry, RedexEvent, RedexFold};
54
55// ============================================================================
56// Public types.
57// ============================================================================
58
59/// How `Mesh::call_service` picks a target from the set of nodes
60/// advertising the requested service.
61#[derive(Debug, Clone, Default, PartialEq, Eq)]
62pub enum RoutingPolicy {
63    /// Naive round-robin via the per-Mesh `call_id` counter.
64    /// Distributes calls evenly across candidates regardless of
65    /// load. The default.
66    #[default]
67    RoundRobin,
68    /// Pick a candidate at random per call. Stateless, cheap, and
69    /// gives even distribution under independent calls.
70    Random,
71    /// Consistent-hash to a target by `key`. Same `key` always
72    /// hits the same target as long as the candidate set is
73    /// stable. Useful for session affinity (route a given
74    /// conversation / shard / user to the same backend).
75    Sticky {
76        /// Caller-supplied identifier — hash maps this to the
77        /// target. Use a session id, shard key, or conversation
78        /// id depending on the application.
79        key: u64,
80    },
81    /// Pick the candidate with the smallest measured `latency_us`
82    /// per the local `ProximityGraph`. Candidates the proximity
83    /// graph hasn't observed yet (no entity ↔ node_id mapping or
84    /// no pingwave received) sort to the bottom — better to pick
85    /// a known-fast node than gamble on an unknown one.
86    ///
87    /// Falls back deterministically to the first sorted candidate
88    /// when no candidates have proximity data, so a freshly-
89    /// discovered service still routes consistently.
90    LowestLatency,
91}
92
93/// Options for [`MeshNode::call`] and [`MeshNode::call_service`].
94#[derive(Debug, Clone)]
95pub struct CallOptions {
96    /// Hard deadline for the call. The future returned by `call`
97    /// races a `tokio::time::sleep_until`; whichever fires first
98    /// wins. On timeout the caller emits a CANCEL event for
99    /// `call_id` so the server can drop the in-flight handler.
100    /// `None` means no deadline; the caller waits indefinitely
101    /// (or until the future is dropped).
102    pub deadline: Option<Instant>,
103    /// How `call_service` picks a target. Ignored by `call`
104    /// (which takes an explicit `target_node_id`). Default:
105    /// `RoundRobin`.
106    pub routing_policy: RoutingPolicy,
107    /// Skip candidates whose `ProximityGraph` entry reports
108    /// `!is_available()` (i.e. `Unhealthy` or `Unknown`).
109    /// Default `true`. Candidates with no proximity entry at all
110    /// are KEPT — absence of evidence is not evidence of
111    /// unhealth, and a freshly-announced service shouldn't be
112    /// filtered just because pingwaves haven't propagated yet.
113    pub filter_unhealthy: bool,
114    /// W3C Trace Context to propagate to the server. When `Some`,
115    /// the call sets `FLAG_RPC_PROPAGATE_TRACE` on the request and
116    /// emits `traceparent` / `tracestate` headers; the server's
117    /// `RpcContext::trace_context` will be populated with the same
118    /// values. nRPC is transport-only — application code on both
119    /// sides reads / writes this via whatever tracing backend it
120    /// has wired up (tracing-opentelemetry, Datadog, etc.).
121    pub trace_context: Option<TraceContext>,
122    /// Per-call concurrency cap. Future Phase 2 work; v1 ignores
123    /// this and the per-Mesh `RpcClientPending` doesn't bound
124    /// in-flight count.
125    pub max_in_flight_per_target: u32,
126    /// **Streaming responses only.** Initial credit window for
127    /// per-streaming-response flow control. When `Some(n)`, the
128    /// caller emits `nrpc-stream-window-initial: n` on the
129    /// REQUEST and the server's pump task awaits one credit per
130    /// emitted chunk. The returned [`RpcStream`] auto-grants 1
131    /// credit per consumed chunk so the in-flight credit holds
132    /// near `n` (or use [`RpcStream::grant`] for batched / custom
133    /// cadence). `None` (the default) → unbounded: server pumps
134    /// chunks as fast as the publish path can take them
135    /// (back-compat / pre-flow-control behavior). Ignored by
136    /// non-streaming `call` / `call_service`.
137    pub stream_window_initial: Option<u32>,
138    /// **Client-streaming / duplex only.** Initial credit window
139    /// for per-call request-direction flow control. Mirror of
140    /// [`Self::stream_window_initial`] for the upload direction. When
141    /// `Some(n)`, the caller emits `nrpc-request-window-initial: n`
142    /// on the REQUEST and its `send().await` sink awaits one
143    /// credit per pushed chunk; the server refills via
144    /// [`DISPATCH_RPC_REQUEST_GRANT`] events. `None` → unbounded:
145    /// caller's send sink doesn't block (legacy / fast-path).
146    /// Ignored by unary `call` / `call_streaming`.
147    ///
148    /// Bidi streaming plan (Phase C).
149    pub request_window_initial: Option<u32>,
150    /// Caller-supplied request headers. Appended to the wire
151    /// `RpcRequestPayload::headers` after any auto-generated
152    /// headers (trace context, stream-window). Useful for
153    /// application-level metadata the server needs at
154    /// dispatch-time — e.g., the `net-where` predicate
155    /// header (Phase 9b of `CAPABILITY_SYSTEM_SDK_PLAN.md`) that
156    /// services consult for predicate-pushdown filtering.
157    ///
158    /// Each entry is `(name, value_bytes)`. Names use the lowercase
159    /// `cyberdeck-*` / `nrpc-*` convention; the substrate doesn't
160    /// validate names beyond the `MAX_RPC_HEADER_NAME_LEN` cap
161    /// enforced at encode time.
162    ///
163    /// Default: empty.
164    pub request_headers: Vec<(String, Vec<u8>)>,
165    /// Caller-side cancel token. Mint via
166    /// [`MeshNode::reserve_cancel_token`]; pair with
167    /// [`MeshNode::cancel`] from any thread to abort the in-flight
168    /// call. `None` (or `Some(0)` — the "no token" sentinel) → no
169    /// cancel slot is reserved and the call has no external abort
170    /// path beyond Drop-on-future-cancellation.
171    ///
172    /// Honored uniformly by every call shape: `call`, `call_service`,
173    /// `call_streaming`, `call_client_stream`, `call_duplex`. The
174    /// substrate registers the token in a per-mesh cancel registry
175    /// at call construction and removes it on resolution (success,
176    /// error, or Drop). A cancel that fires mid-flight surfaces to
177    /// the caller as [`RpcError::Cancelled`] and emits CANCEL on
178    /// the wire via the existing per-call-shape guards (UnaryCallGuard,
179    /// ClientStreamCallRaw::Drop, DuplexCallRaw::Drop).
180    ///
181    /// Cancel-before-register is race-safe: a cancel that arrives
182    /// in the gap between `reserve_cancel_token` and the call's
183    /// internal register step latches a pre-cancel flag on the
184    /// registry's orphan entry; the subsequent register observes
185    /// it and the call short-circuits to [`RpcError::Cancelled`]
186    /// without ever publishing the REQUEST.
187    pub cancel_token: Option<u64>,
188}
189
190impl Default for CallOptions {
191    fn default() -> Self {
192        Self {
193            deadline: None,
194            routing_policy: RoutingPolicy::default(),
195            filter_unhealthy: true,
196            trace_context: None,
197            max_in_flight_per_target: 64,
198            stream_window_initial: None,
199            request_window_initial: None,
200            request_headers: Vec::new(),
201            cancel_token: None,
202        }
203    }
204}
205
206/// What [`MeshNode::call`] returns on success.
207#[derive(Debug, Clone)]
208pub struct RpcReply {
209    /// Response payload from the server's handler. Caller decodes
210    /// according to its application protocol.
211    pub body: Bytes,
212    /// Headers attached by the server's response.
213    pub headers: Vec<(String, Vec<u8>)>,
214    /// Wall-clock latency from `call(...)` to RESPONSE arrival.
215    pub latency_ns: u64,
216}
217
218/// What [`MeshNode::call`] returns on failure.
219#[derive(Debug, thiserror::Error)]
220pub enum RpcError {
221    /// No subscription / no route to the target. Either
222    /// `target_node_id` is unknown to the local mesh, or the
223    /// caller's reply-channel subscription couldn't be set up.
224    #[error("no route to target {target:#x}: {reason}")]
225    NoRoute {
226        /// Target node id the call was directed at.
227        target: u64,
228        /// Diagnostic — typically the underlying transport error.
229        reason: String,
230    },
231    /// Caller's deadline elapsed before a RESPONSE arrived. The
232    /// caller emits a CANCEL on timeout so the server can drop
233    /// the in-flight handler; this variant is returned to the
234    /// awaiting caller.
235    #[error("timeout after {elapsed_ms}ms")]
236    Timeout {
237        /// Wall-clock milliseconds elapsed before timeout fired.
238        elapsed_ms: u64,
239    },
240    /// Server returned a non-`Ok` status. Body carries the
241    /// server's diagnostic (UTF-8) when available.
242    #[error("server returned status {status:#06x}: {message}")]
243    ServerError {
244        /// Wire-level `RpcStatus` value the server returned.
245        status: u16,
246        /// UTF-8 diagnostic from the response body, when the body
247        /// decodes as valid UTF-8; otherwise hex-truncated.
248        message: String,
249        /// Reply headers from the error response — the wire has always
250        /// carried them (same frame field as success replies); the
251        /// caller used to discard them here. Empty when the server
252        /// attached none. The message stays the human diagnostic;
253        /// headers are the structured sidecar channel (e.g. a
254        /// `net-failure-schematic` verdict).
255        headers: Vec<(String, Vec<u8>)>,
256    },
257    /// Underlying transport error (publish failure, encryption,
258    /// etc.).
259    #[error("transport: {0}")]
260    Transport(#[from] AdapterError),
261    /// Client-local serialization or deserialization failure.
262    /// `direction = Encode` means the typed wrapper failed to
263    /// encode the request before it ever hit the wire;
264    /// `direction = Decode` means the response landed but the
265    /// typed wrapper failed to decode it. Either way this is a
266    /// caller-fixable bug (wrong codec, schema drift, malformed
267    /// `Serialize` impl) — NOT a transient infra failure — so
268    /// retry / circuit-breaker predicates skip it by default.
269    #[error("codec ({direction:?}): {message}")]
270    Codec {
271        /// Which side of the call the codec failure happened on.
272        direction: CodecDirection,
273        /// Decode/encode diagnostic from the underlying serde impl.
274        message: String,
275    },
276    /// v0.4 capability-auth gate denied the call. Either the
277    /// target's latest `CapabilityAnnouncement` does not list
278    /// the requested `nrpc:<service>` tag, or it lists the tag
279    /// with allow-lists the caller does not match. See
280    /// `docs/plans/CAPABILITY_AUTH_PLAN.md` §3 for the model.
281    ///
282    /// Raised by the caller-side gate inside
283    /// [`MeshNode::call_service`] BEFORE the request hits the
284    /// wire, and surfaced by the caller on receipt of a
285    /// `RpcStatus::CapabilityDenied` response (the callee-side
286    /// defense-in-depth path).
287    #[error("capability denied: target {target:#x} does not authorize nrpc:{capability}")]
288    CapabilityDenied {
289        /// Target node id the gate denied.
290        target: u64,
291        /// Service / capability tag (without the `nrpc:` prefix)
292        /// the gate denied.
293        capability: String,
294    },
295    /// Caller-side cancellation fired via
296    /// [`MeshNode::cancel`] with the call's `cancel_token`.
297    /// Triggers a Drop-on-cancel CANCEL frame on the wire so the
298    /// server's in-flight handler observes the cancel; the
299    /// awaiting caller returns this variant. NOT retried by the
300    /// default retry policy — cancellation is caller-driven and
301    /// re-issuing the call defeats the point.
302    #[error("call cancelled by caller")]
303    Cancelled,
304}
305
306/// Which side of the call surfaced a [`RpcError::Codec`] failure.
307#[derive(Debug, Clone, Copy, PartialEq, Eq)]
308pub enum CodecDirection {
309    /// Encoding the outbound request failed before the call was issued.
310    Encode,
311    /// Decoding the inbound response failed after the call returned Ok.
312    Decode,
313}
314
315/// RAII handle returned by [`MeshNode::serve_rpc`]. Dropping it
316/// unregisters the inbound dispatcher and removes the service
317/// from the local-services registry (so subsequent
318/// `announce_capabilities` calls stop emitting the
319/// `nrpc:<service>` tag).
320///
321/// **Bridge task lifecycle.** The bridge task that drains the
322/// inbound mpsc into the fold is NOT aborted on Drop. The
323/// `register_rpc_inbound` dispatcher closure owns the only
324/// `mpsc::Sender` clone, so `unregister_rpc_inbound` (which drops
325/// the dispatcher) closes the channel; the bridge's `rx.recv()`
326/// then yields `None` and the task exits cleanly after draining
327/// any queued events. Aborting would race events that are
328/// mid-`fold.lock().apply()` — those events would be killed
329/// without their RESPONSE being emitted, so the corresponding
330/// callers would just time out.
331///
332/// Outstanding handler executions (already-spawned tokio tasks)
333/// continue to completion regardless.
334pub struct ServeHandle {
335    /// Channel hash to unregister on Drop.
336    channel_hash: ChannelHash,
337    /// Service name to remove from `rpc_local_services` on Drop.
338    service: String,
339    /// The bridge task. Held only so callers can introspect /
340    /// detach it; Drop does NOT abort it (see struct doc-comment).
341    /// Detaches naturally when the handle is dropped — the bridge
342    /// exits on its own once the dispatcher's `mpsc::Sender` is
343    /// dropped via `unregister_rpc_inbound`.
344    _bridge: JoinHandle<()>,
345    /// The per-service response drainer task (unary `serve_rpc` only;
346    /// `None` for the streaming/duplex variants, which still spawn per
347    /// emit). Like `_bridge`, held only to detach — it exits on its own
348    /// once the emit closure (the sole `Sender` owner, dropped when the
349    /// bridge task ends and the fold drops) is gone. See §8a.
350    _response_drain: Option<JoinHandle<()>>,
351    /// Hold an Arc back to the mesh so we can unregister on Drop
352    /// without the mesh having to track us.
353    mesh: Arc<MeshNode>,
354}
355
356impl Drop for ServeHandle {
357    fn drop(&mut self) {
358        // Order matters: unregister the dispatcher FIRST so no new
359        // events can land in the bridge's mpsc, THEN drop the
360        // service-tag entry. The bridge task drains any in-flight
361        // events naturally and exits when its `rx.recv()` yields
362        // `None` (which happens as soon as the dispatcher closure
363        // — the sole `tx` owner — is dropped above).
364        self.mesh.unregister_rpc_inbound(self.channel_hash);
365        self.mesh.rpc_local_services_arc().remove(&self.service);
366    }
367}
368
369/// A response ready to publish, handed from a (synchronous) `serve_rpc`
370/// emit closure to the per-service response drainer task. Replaces the
371/// pre-§8a `tokio::spawn`-per-response: the emit closure builds the wire
372/// payload (cheap, sync) and `try_send`s this job; one drain task does the
373/// `.await` publish. The reply `ChannelName` is `Arc<str>` and `payload` is
374/// `Bytes`, so the hand-off is a couple of moves — no copy, no per-response
375/// task allocation/scheduling.
376struct RpcResponseJob {
377    caller_origin: u64,
378    call_id: u64,
379    target_hint: Option<u64>,
380    reply_channel: ChannelName,
381    /// PERF_AUDIT §3.10 — cached
382    /// `ChannelId::new(reply_channel).hash()`, populated by the
383    /// emit closure's `reply_channel_cache` lookup. Pre-fix the
384    /// drainer re-ran xxh3 over the channel name per response;
385    /// the same `OriginKeyedLru` now caches the triple so a
386    /// cache hit is one Arc bump + two `u64` copies.
387    reply_channel_hash: ChannelHash,
388    /// PERF_AUDIT §3.10 — cached
389    /// `MeshNode::publish_stream_id(&reply_channel_id)`.
390    reply_stream_id: u64,
391    payload: Bytes,
392}
393
394/// Cached triple `(ChannelName, ChannelHash, stream_id)` for the
395/// per-caller reply channel. Stored in the per-`serve_rpc`
396/// `OriginKeyedLru` so each subsequent response to the same
397/// caller is one Arc bump on the name + two `u64` copies — no
398/// xxh3, no `publish_stream_id`.
399///
400/// Per PERF_AUDIT §3.10.
401#[derive(Clone)]
402struct CachedReplyChannel {
403    name: ChannelName,
404    hash: ChannelHash,
405    stream_id: u64,
406}
407
408// ============================================================================
409// Streaming caller-side: RpcStream.
410// ============================================================================
411
412/// An open streaming RPC call. Implements `Stream<Item =
413/// Result<Bytes, RpcError>>` — yields chunks as the server emits
414/// them, terminates on a clean stream-end frame OR a non-`Ok`
415/// status (which is yielded as the last `Err` item before the
416/// stream closes).
417///
418/// Dropping the stream emits a CANCEL to the server (best-effort)
419/// and discards the pending entry — any chunks the server emits
420/// after the drop are silently discarded by the client fold.
421pub struct RpcStream {
422    mesh: Arc<MeshNode>,
423    target_node_id: u64,
424    request_channel: ChannelName,
425    /// Cached `ChannelId::new(request_channel).hash()`. Pre-fix
426    /// `spawn_grant_publish` re-ran xxh3 over the channel name on
427    /// every auto/explicit grant — per PERF_AUDIT §3.10 the value
428    /// is invariant for the stream's lifetime, so we cache it once
429    /// at construction.
430    request_channel_hash: ChannelHash,
431    /// Cached `MeshNode::publish_stream_id(&request_channel_id)`.
432    /// Same reasoning as `request_channel_hash`.
433    request_stream_id: u64,
434    self_origin: u64,
435    call_id: u64,
436    inner: tokio::sync::mpsc::UnboundedReceiver<StreamItem>,
437    /// Set true once we've yielded the terminal item (or an
438    /// error). Subsequent polls return `None`.
439    done: bool,
440    /// `Some(_)` if this stream uses flow control (caller set
441    /// `CallOptions::stream_window_initial`). Auto-grant
442    /// accumulates 1 credit per delivered chunk and fires one
443    /// batched `spawn_grant_publish` once the accumulator reaches
444    /// `window / 2` (or 1 for tiny windows). Keeps the server's
445    /// pump fed at roughly the configured rate without the per-
446    /// chunk spawn-storm + AEAD-storm the pre-fix path produced.
447    /// `None` → no flow control; `poll_next` does not emit grants.
448    /// Per PERF_AUDIT_2026_06_10_FULL_CRATE.md §3.3.
449    stream_window: Option<u32>,
450    /// Auto-grant accumulator: chunks delivered since the last
451    /// emitted grant. Flushed at the `window / 2` threshold (see
452    /// the doc on [`Self::stream_window`]).
453    grant_pending: u32,
454    /// Observer-fire bookkeeping. Latched on terminal observation
455    /// in `poll_next`; fired once from `Drop` so the Deck NRPC
456    /// tab + every other `RpcObserver` consumer sees one event
457    /// per streaming-response call.
458    observer: StreamingObserverState,
459    /// v3 cancel-watcher keep-alive (C-S1). Dropping this field
460    /// (on stream Drop) resolves the matching watcher task's
461    /// oneshot receiver with `Err`, telling the watcher to exit
462    /// cleanly + release the registry entry. When the call was
463    /// opened without `cancel_token`, this is a placeholder sender
464    /// with no watcher behind it — drop has no observable effect.
465    _cancel_keep_alive: StreamCancelKeepAlive,
466}
467
468impl RpcStream {
469    /// Server-assigned `call_id`. Useful for trace correlation /
470    /// custom logging at the call site.
471    pub fn call_id(&self) -> u64 {
472        self.call_id
473    }
474
475    /// Whether this stream is flow-controlled (caller set
476    /// `CallOptions::stream_window_initial`). Useful for tests +
477    /// diagnostics; user code typically doesn't need to inspect
478    /// this.
479    pub fn flow_controlled(&self) -> bool {
480        self.stream_window.is_some()
481    }
482
483    /// Explicitly grant `amount` more credits to the server's
484    /// pump. Spawns a fire-and-forget publish; doesn't await
485    /// acknowledgement. **No-op when flow control was not enabled
486    /// for this stream** — the server would silently drop the
487    /// grant anyway, and emitting wire traffic with no purpose
488    /// would just burn bandwidth.
489    ///
490    /// Auto-grant (1 credit per delivered chunk) covers the
491    /// common case; use this for batched cadence (e.g. grant
492    /// `window/2` after every `window/2` chunks consumed) when
493    /// `auto_grant`-style amortization isn't enough.
494    pub fn grant(&self, amount: u32) {
495        if !self.flow_controlled() || amount == 0 {
496            return;
497        }
498        spawn_grant_publish(
499            Arc::clone(&self.mesh),
500            self.target_node_id,
501            self.request_channel_hash,
502            self.request_stream_id,
503            self.self_origin,
504            self.call_id,
505            amount,
506        );
507    }
508}
509
510/// PERF_AUDIT §3.3 — auto-grant coalescing decision for
511/// [`RpcStream::poll_next`]. Accumulates one credit (the chunk
512/// that was just delivered to the consumer) into `pending` and
513/// returns `Some(amount)` when the accumulator reaches the flush
514/// threshold of `window / 2` (clamped to ≥ 1 so a window of 1
515/// degenerates to the pre-fix per-chunk cadence).
516///
517/// Liveness invariant (why no flush-on-drop / timer backstop is
518/// needed): the credits left pending never exceed
519/// `threshold - 1 < window`. The server starts with `window`
520/// credits and `credits = window - (sent - delivered) - pending`,
521/// so whenever the consumer has polled everything that was sent
522/// (the only state in which it could block waiting on the server),
523/// `credits = window - pending >= window - threshold + 1 >= 1` —
524/// the server can always make progress. A consumer that stops
525/// polling stalls the pump by design (that's flow control), and
526/// the chunks already buffered in the stream's mpsc are enough to
527/// carry `pending` across the threshold as soon as it resumes.
528fn accumulate_auto_grant(pending: &mut u32, window: u32) -> Option<u32> {
529    *pending = pending.saturating_add(1);
530    let threshold = (window / 2).max(1);
531    if *pending >= threshold {
532        let amount = *pending;
533        *pending = 0;
534        Some(amount)
535    } else {
536        None
537    }
538}
539
540/// Shared fire-and-forget GRANT-publish helper. Used by
541/// [`RpcStream::grant`] (explicit) and the auto-grant in
542/// [`RpcStream::poll_next`]. Same direct-unicast publish path as
543/// [`spawn_cancel_publish`], just with a different dispatch byte
544/// + a 4-byte u32 payload.
545///
546/// PERF_AUDIT §3.10 — takes `request_channel_hash` and
547/// `request_stream_id` as pre-computed inputs (cached on
548/// `RpcStream`) so the per-chunk grant path doesn't re-run
549/// `ChannelId::new` + xxh3 on every call.
550fn spawn_grant_publish(
551    mesh: Arc<MeshNode>,
552    target: u64,
553    request_channel_hash: ChannelHash,
554    request_stream_id: u64,
555    self_origin: u64,
556    call_id: u64,
557    amount: u32,
558) {
559    tokio::spawn(async move {
560        let meta = EventMeta::new(DISPATCH_RPC_STREAM_GRANT, 0, self_origin, call_id, 0);
561        let mut buf = Vec::with_capacity(EVENT_META_SIZE + 4);
562        buf.extend_from_slice(&meta.to_bytes());
563        buf.extend_from_slice(&encode_stream_grant(amount));
564        let payload = Bytes::from(buf);
565        let _ = mesh
566            .publish_to_peer(
567                target,
568                request_channel_hash,
569                request_stream_id,
570                /* reliable */ true,
571                std::slice::from_ref(&payload),
572            )
573            .await;
574    });
575}
576
577impl futures::Stream for RpcStream {
578    type Item = Result<Bytes, RpcError>;
579
580    fn poll_next(
581        mut self: std::pin::Pin<&mut Self>,
582        cx: &mut std::task::Context<'_>,
583    ) -> std::task::Poll<Option<Self::Item>> {
584        if self.done {
585            return std::task::Poll::Ready(None);
586        }
587        match self.inner.poll_recv(cx) {
588            std::task::Poll::Ready(Some(StreamItem::Chunk(body))) => {
589                // Auto-grant: accumulate 1 credit per delivered
590                // chunk and fire a batched `spawn_grant_publish`
591                // only when the accumulator reaches `window / 2`
592                // (or 1 for tiny windows). Per PERF_AUDIT §3.3 —
593                // pre-fix this spawned one task + one reliable
594                // AEAD packet per chunk, a spawn-storm + AEAD-
595                // storm under bursting; the server side already
596                // fixed the identical shape via
597                // `build_request_grant_emitter` (§3.3 audit text).
598                // Callers needing finer cadence still have
599                // `RpcStream::grant` for explicit batches.
600                if let Some(window) = self.stream_window {
601                    let mut pending = self.grant_pending;
602                    if let Some(amount) = accumulate_auto_grant(&mut pending, window) {
603                        spawn_grant_publish(
604                            Arc::clone(&self.mesh),
605                            self.target_node_id,
606                            self.request_channel_hash,
607                            self.request_stream_id,
608                            self.self_origin,
609                            self.call_id,
610                            amount,
611                        );
612                    }
613                    self.grant_pending = pending;
614                }
615                self.observer.add_response_bytes(body.len() as u32);
616                std::task::Poll::Ready(Some(Ok(body)))
617            }
618            std::task::Poll::Ready(Some(StreamItem::End)) => {
619                self.done = true;
620                self.observer.latch_ok();
621                std::task::Poll::Ready(None)
622            }
623            std::task::Poll::Ready(Some(StreamItem::Error(resp))) => {
624                self.done = true;
625                let status = resp.status.to_wire();
626                let message = String::from_utf8(resp.body.to_vec()).unwrap_or_else(|e| {
627                    format!("<{} bytes of non-utf8 body>", e.into_bytes().len())
628                });
629                self.observer
630                    .latch_error(format!("server returned status {status:#06x}: {message}"));
631                std::task::Poll::Ready(Some(Err(RpcError::ServerError {
632                    status,
633                    message,
634                    headers: resp.headers,
635                })))
636            }
637            std::task::Poll::Ready(None) => {
638                self.done = true;
639                std::task::Poll::Ready(None)
640            }
641            std::task::Poll::Pending => std::task::Poll::Pending,
642        }
643    }
644}
645
646impl Drop for RpcStream {
647    fn drop(&mut self) {
648        // Best-effort CANCEL to the server. Spawn a task because
649        // Drop can't be async; the publish happens off-thread.
650        // Also clear our pending entry so any in-flight chunks
651        // are dropped on arrival.
652        self.mesh.rpc_client_pending_arc().cancel(self.call_id);
653        spawn_cancel_publish(
654            Arc::clone(&self.mesh),
655            self.target_node_id,
656            self.request_channel.clone(),
657            self.self_origin,
658            self.call_id,
659        );
660        // Fire the observer with the latched status (Ok / Error /
661        // Canceled). Idempotent — only the first fire emits.
662        self.observer.fire();
663    }
664}
665
666// ============================================================================
667// Phase C — caller-side client-streaming / duplex primitive.
668// ============================================================================
669
670/// Shared REQUEST_CHUNK-publish helper. Builds the wire frame and
671/// fires through `publish_to_peer` direct-unicast (same routing
672/// pattern as the initial REQUEST — caller knows the target).
673/// PERF_AUDIT §3.10 — accepts pre-computed
674/// `request_channel_hash` and `request_stream_id` (cached on
675/// `ClientStreamCallRaw`) so the per-chunk client-stream send path
676/// doesn't re-run `ChannelId::new` + xxh3 on every chunk.
677async fn publish_request_chunk(
678    mesh: &Arc<MeshNode>,
679    target: u64,
680    request_channel_hash: ChannelHash,
681    request_stream_id: u64,
682    self_origin: u64,
683    chunk: &RpcRequestChunkPayload,
684) -> Result<(), RpcError> {
685    let meta = EventMeta::new(DISPATCH_RPC_REQUEST_CHUNK, 0, self_origin, chunk.call_id, 0);
686    let mut buf = Vec::with_capacity(EVENT_META_SIZE + chunk.encoded_len());
687    buf.extend_from_slice(&meta.to_bytes());
688    chunk.encode_into(&mut buf);
689    let payload = Bytes::from(buf);
690    mesh.publish_to_peer(
691        target,
692        request_channel_hash,
693        request_stream_id,
694        /* reliable */ true,
695        std::slice::from_ref(&payload),
696    )
697    .await
698    .map_err(RpcError::Transport)
699}
700
701/// Internal state of a [`ClientStreamCallRaw`]. The state machine
702/// is small: open the call (initial REQUEST not yet sent), then
703/// send N items (the first becomes the initial REQUEST, subsequent
704/// become REQUEST_CHUNKs), then finish (terminal REQUEST_END
705/// frame). After finish, no further sends are accepted.
706#[derive(Debug, Clone, Copy, PartialEq, Eq)]
707enum ClientStreamState {
708    /// Pending entry registered, reply subscription ensured, but
709    /// the initial REQUEST has NOT been published to the wire yet.
710    /// First `send` flips this to `Sending`.
711    JustOpened,
712    /// Initial REQUEST has been published; subsequent sends ride
713    /// as REQUEST_CHUNKs.
714    Sending,
715    /// `finish` has been called; the terminal REQUEST_END frame
716    /// (or the initial REQUEST with FLAG_END for the degenerate
717    /// zero-send path) has been published. The terminal RESPONSE
718    /// has not necessarily arrived yet — that's awaited on the
719    /// caller's terminal_rx.
720    Finishing,
721    /// Terminal RESPONSE has been delivered. Drop is a no-op.
722    Done,
723}
724
725/// Caller-side handle for a client-streaming (or duplex Phase D)
726/// RPC. Push N items via [`ClientStreamCallRaw::send`], then
727/// [`ClientStreamCallRaw::finish`] to await the terminal RESPONSE.
728///
729/// **Lazy initial REQUEST.** The initial REQUEST is published on
730/// the FIRST `send()` (or on `finish()` if the caller sends nothing
731/// — that's the "zero-item upload" degenerate path that opens and
732/// closes the call in one frame). Constructing the handle does
733/// NOT yet emit any wire traffic beyond the reply-channel
734/// subscription setup.
735///
736/// **Flow control.** When the caller set
737/// [`CallOptions::request_window_initial`] to `Some(n)`, the
738/// handle holds an `n`-permit `Semaphore` that gates `send`. The
739/// server's [`DISPATCH_RPC_REQUEST_GRANT`] events refill the
740/// semaphore. When `None`, `send` doesn't block (caller is on the
741/// unbounded-credit fast path).
742///
743/// **Cancellation.** Dropping the handle BEFORE `finish` returns
744/// `Ok` fires a best-effort CANCEL to the server and clears the
745/// pending entry. Dropping after a successful `finish` is a no-op
746/// (terminal RESPONSE already delivered + entry removed).
747///
748/// Bidi streaming plan (Phase C).
749pub struct ClientStreamCallRaw {
750    mesh: Arc<MeshNode>,
751    target_node_id: u64,
752    request_channel: ChannelName,
753    /// PERF_AUDIT §3.10 — cached `ChannelId::new(request_channel).hash()`
754    /// so per-chunk REQUEST_CHUNK publishes don't re-run xxh3.
755    request_channel_hash: ChannelHash,
756    /// PERF_AUDIT §3.10 — cached
757    /// `MeshNode::publish_stream_id(&request_channel_id)`.
758    request_stream_id: u64,
759    self_origin: u64,
760    call_id: u64,
761    service: String,
762    /// Header set queued for the initial REQUEST. Drained on the
763    /// first publish (either `send` or `finish`).
764    initial_headers: Vec<(String, Vec<u8>)>,
765    /// Flag bits queued for the initial REQUEST. Always carries
766    /// `FLAG_RPC_CLIENT_STREAMING_REQUEST`; may also carry
767    /// `FLAG_RPC_PROPAGATE_TRACE` when the caller supplied a
768    /// trace context.
769    initial_flags: u16,
770    /// `deadline_ns` from `CallOptions::deadline`. Embedded in the
771    /// initial REQUEST.
772    deadline_ns: u64,
773    /// Per-call semaphore for upload credits. `None` when the
774    /// caller didn't opt into flow control (`request_window_initial`
775    /// was `None` on the `CallOptions`).
776    credit_sem: Option<Arc<tokio::sync::Semaphore>>,
777    /// Background task that drains REQUEST_GRANT credits from the
778    /// pending entry's grant mpsc into `credit_sem`. Aborted on
779    /// Drop. `None` when flow control is off.
780    grant_pump: Option<JoinHandle<()>>,
781    /// Single-shot terminal-RESPONSE receiver. Taken by `finish`;
782    /// after that `Drop` doesn't attempt to await again.
783    terminal_rx: Option<tokio::sync::oneshot::Receiver<RpcResponsePayload>>,
784    /// State machine. See [`ClientStreamState`].
785    state: ClientStreamState,
786    /// Wall-clock start (for `RpcReply::latency_ns` reporting).
787    started: Instant,
788    /// Observer-fire bookkeeping. Latched on terminal observation
789    /// in `finish`; fired once from `Drop` so the Deck NRPC tab +
790    /// every `RpcObserver` consumer sees one event per
791    /// client-streaming call.
792    observer: StreamingObserverState,
793    /// v3 cancel-watcher keep-alive (C-S1). Dropping this field
794    /// (on call Drop) tells the watcher task to exit cleanly and
795    /// release the registry entry. See
796    /// [`spawn_stream_cancel_watcher`] for the lifecycle.
797    _cancel_keep_alive: StreamCancelKeepAlive,
798}
799
800impl ClientStreamCallRaw {
801    /// Server-assigned `call_id`. Useful for trace correlation /
802    /// custom logging.
803    pub fn call_id(&self) -> u64 {
804        self.call_id
805    }
806
807    /// Whether this call is flow-controlled (caller set
808    /// `CallOptions::request_window_initial`).
809    pub fn flow_controlled(&self) -> bool {
810        self.credit_sem.is_some()
811    }
812
813    /// Push one body chunk to the server. Encodes as the initial
814    /// REQUEST (first call) or as a REQUEST_CHUNK (subsequent
815    /// calls). When flow control is opted into, awaits one credit
816    /// before publishing.
817    ///
818    /// Returns `Err(RpcError::Codec)` if called after [`Self::finish`].
819    pub async fn send(&mut self, body: Bytes) -> Result<(), RpcError> {
820        match self.state {
821            ClientStreamState::Finishing | ClientStreamState::Done => {
822                return Err(RpcError::Codec {
823                    direction: CodecDirection::Encode,
824                    message: "send() called after finish()".to_string(),
825                });
826            }
827            _ => {}
828        }
829        // Gate on credit when flow control is opted into.
830        if let Some(sem) = self.credit_sem.as_ref() {
831            let permit = sem.clone().acquire_owned().await.map_err(|_| {
832                RpcError::Transport(AdapterError::Connection("credit semaphore closed".into()))
833            })?;
834            permit.forget();
835        }
836        self.observer.add_request_bytes(body.len() as u32);
837        match self.state {
838            ClientStreamState::JustOpened => {
839                // First send → initial REQUEST.
840                let req = RpcRequestPayload {
841                    service: self.service.clone(),
842                    deadline_ns: self.deadline_ns,
843                    flags: self.initial_flags,
844                    headers: std::mem::take(&mut self.initial_headers),
845                    body: body.clone(),
846                };
847                self.publish_initial_request(&req).await?;
848                self.state = ClientStreamState::Sending;
849            }
850            ClientStreamState::Sending => {
851                let chunk = RpcRequestChunkPayload {
852                    call_id: self.call_id,
853                    flags: 0,
854                    headers: vec![],
855                    body: body.clone(),
856                };
857                publish_request_chunk(
858                    &self.mesh,
859                    self.target_node_id,
860                    self.request_channel_hash,
861                    self.request_stream_id,
862                    self.self_origin,
863                    &chunk,
864                )
865                .await?;
866            }
867            ClientStreamState::Finishing | ClientStreamState::Done => unreachable!(),
868        }
869        Ok(())
870    }
871
872    /// Close the upload direction and await the server's terminal
873    /// RESPONSE. Emits a REQUEST_CHUNK with `FLAG_RPC_REQUEST_END`
874    /// (empty body) if the call has already published its initial
875    /// REQUEST, or an initial REQUEST with both
876    /// `FLAG_RPC_CLIENT_STREAMING_REQUEST` and
877    /// `FLAG_RPC_REQUEST_END` set (the degenerate "zero-item
878    /// upload" path) if nothing was sent.
879    ///
880    /// Consumes the handle — Drop after `finish` is a no-op.
881    pub async fn finish(mut self) -> Result<RpcReply, RpcError> {
882        match self.state {
883            ClientStreamState::JustOpened => {
884                let req = RpcRequestPayload {
885                    service: self.service.clone(),
886                    deadline_ns: self.deadline_ns,
887                    flags: self.initial_flags | FLAG_RPC_REQUEST_END,
888                    headers: std::mem::take(&mut self.initial_headers),
889                    body: Bytes::new(),
890                };
891                self.publish_initial_request(&req).await?;
892            }
893            ClientStreamState::Sending => {
894                let chunk = RpcRequestChunkPayload {
895                    call_id: self.call_id,
896                    flags: FLAG_RPC_REQUEST_END,
897                    headers: vec![],
898                    body: Bytes::new(),
899                };
900                publish_request_chunk(
901                    &self.mesh,
902                    self.target_node_id,
903                    self.request_channel_hash,
904                    self.request_stream_id,
905                    self.self_origin,
906                    &chunk,
907                )
908                .await?;
909            }
910            ClientStreamState::Finishing | ClientStreamState::Done => {
911                return Err(RpcError::Codec {
912                    direction: CodecDirection::Encode,
913                    message: "finish() called twice".to_string(),
914                });
915            }
916        }
917        self.state = ClientStreamState::Finishing;
918        let terminal_rx = self.terminal_rx.take().ok_or_else(|| {
919            RpcError::Transport(AdapterError::Connection(
920                "terminal receiver already consumed".into(),
921            ))
922        })?;
923        // Honor the deadline if the caller set one.
924        let resp = if self.deadline_ns > 0 {
925            let now = std::time::SystemTime::now()
926                .duration_since(std::time::UNIX_EPOCH)
927                .map(|d| d.as_nanos() as u64)
928                .unwrap_or(0);
929            let remaining = self.deadline_ns.saturating_sub(now);
930            match tokio::time::timeout(std::time::Duration::from_nanos(remaining), terminal_rx)
931                .await
932            {
933                Ok(Ok(r)) => r,
934                Ok(Err(_)) => {
935                    let msg = "terminal sender dropped before response arrived";
936                    self.observer.latch_error(msg);
937                    return Err(RpcError::Transport(AdapterError::Connection(msg.into())));
938                }
939                Err(_elapsed) => {
940                    let elapsed_ms = self.started.elapsed().as_millis() as u64;
941                    self.observer.latch_timeout();
942                    return Err(RpcError::Timeout { elapsed_ms });
943                }
944            }
945        } else {
946            match terminal_rx.await {
947                Ok(r) => r,
948                Err(_) => {
949                    let msg = "terminal sender dropped before response arrived";
950                    self.observer.latch_error(msg);
951                    return Err(RpcError::Transport(AdapterError::Connection(msg.into())));
952                }
953            }
954        };
955        self.state = ClientStreamState::Done;
956        self.observer.add_response_bytes(resp.body.len() as u32);
957        if !resp.status.is_ok() {
958            // String::from_utf8 takes `Vec<u8>`. `Bytes::to_vec()`
959            // matches the prior `resp.body.clone()` semantics (full
960            // copy of the body for the error-formatting path);
961            // bulk-throughput improvement lives on the decode side,
962            // not here.
963            let message = String::from_utf8(resp.body.to_vec())
964                .unwrap_or_else(|e| format!("<{} bytes of non-utf8 body>", e.into_bytes().len()));
965            self.observer.latch_error(format!(
966                "server returned status {:#06x}: {message}",
967                resp.status.to_wire()
968            ));
969            return Err(RpcError::ServerError {
970                status: resp.status.to_wire(),
971                message,
972                headers: resp.headers,
973            });
974        }
975        self.observer.latch_ok();
976        let latency_ns = self.started.elapsed().as_nanos() as u64;
977        Ok(RpcReply {
978            body: resp.body,
979            headers: resp.headers,
980            latency_ns,
981        })
982    }
983
984    async fn publish_initial_request(&self, req: &RpcRequestPayload) -> Result<(), RpcError> {
985        let meta = EventMeta::new(DISPATCH_RPC_REQUEST, 0, self.self_origin, self.call_id, 0);
986        let mut buf = Vec::with_capacity(EVENT_META_SIZE + req.encoded_len());
987        buf.extend_from_slice(&meta.to_bytes());
988        req.encode_into(&mut buf);
989        let payload = Bytes::from(buf);
990        // PERF_AUDIT §3.10 — use the cached hash + stream_id from
991        // construction; no per-publish `ChannelId::new` + xxh3.
992        self.mesh
993            .publish_to_peer(
994                self.target_node_id,
995                self.request_channel_hash,
996                self.request_stream_id,
997                /* reliable */ true,
998                std::slice::from_ref(&payload),
999            )
1000            .await
1001            .map_err(RpcError::Transport)
1002    }
1003}
1004
1005impl Drop for ClientStreamCallRaw {
1006    fn drop(&mut self) {
1007        if let Some(task) = self.grant_pump.take() {
1008            task.abort();
1009        }
1010        // Fire the observer with whatever status was latched
1011        // (Ok / Error / Timeout / Canceled). Idempotent — only
1012        // the first call emits.
1013        self.observer.fire();
1014        if matches!(self.state, ClientStreamState::Done) {
1015            // Successful completion — pending entry already gone,
1016            // no CANCEL needed.
1017            return;
1018        }
1019        self.mesh.rpc_client_pending_arc().cancel(self.call_id);
1020        // Only fire CANCEL on the wire if the server has actually
1021        // seen the initial REQUEST. A `JustOpened` Drop means we
1022        // never published anything; no need to CANCEL a call the
1023        // server doesn't know about.
1024        if !matches!(self.state, ClientStreamState::JustOpened) {
1025            spawn_cancel_publish(
1026                Arc::clone(&self.mesh),
1027                self.target_node_id,
1028                self.request_channel.clone(),
1029                self.self_origin,
1030                self.call_id,
1031            );
1032        }
1033    }
1034}
1035
1036// ============================================================================
1037// Phase D — caller-side duplex primitive.
1038// ============================================================================
1039
1040/// Shared state between a `DuplexSink` and its sibling
1041/// `DuplexStream`. Both halves hold an `Arc<DuplexInner>`; when
1042/// the refcount hits zero (i.e. both halves dropped) the Drop
1043/// fires CANCEL to the server unless the call was cleanly closed
1044/// (`clean_close = true`).
1045struct DuplexInner {
1046    mesh: Arc<MeshNode>,
1047    target_node_id: u64,
1048    request_channel: ChannelName,
1049    /// PERF_AUDIT §3.10 — cached channel-id hash + stream id so
1050    /// per-chunk publishes from the upload side don't re-run
1051    /// `ChannelId::new` + xxh3.
1052    request_channel_hash: ChannelHash,
1053    request_stream_id: u64,
1054    self_origin: u64,
1055    call_id: u64,
1056    /// Whether the initial REQUEST was successfully published.
1057    /// `false` means we never reached the wire — no CANCEL needed
1058    /// (server doesn't know about the call).
1059    initial_sent: std::sync::atomic::AtomicBool,
1060    /// Set true when the call closes cleanly — terminal RESPONSE
1061    /// (or terminal Error) was observed on the response stream.
1062    /// Suppresses CANCEL-on-drop.
1063    clean_close: std::sync::atomic::AtomicBool,
1064    /// Observer-fire bookkeeping. Latched from the various
1065    /// terminal-observation sites (DuplexCall::next /
1066    /// DuplexStream::poll_next yielding End or Error); fired
1067    /// once on Drop. The DuplexCall / DuplexSink / DuplexStream
1068    /// each share access via the surrounding Arc<DuplexInner>.
1069    observer: StreamingObserverState,
1070    /// v3 cancel-watcher keep-alive (C-S1). Lives on
1071    /// `Arc<DuplexInner>` so it survives `into_split` — both
1072    /// halves of the duplex hold the same Arc, so the watcher
1073    /// task exits only when BOTH halves drop (matching the
1074    /// Drop-fires-CANCEL semantics above). Wrapped in `Option`
1075    /// for `mem::take`-style construction patterns; populated
1076    /// once at `call_duplex` time and never cleared.
1077    _cancel_keep_alive: Option<StreamCancelKeepAlive>,
1078}
1079
1080impl Drop for DuplexInner {
1081    fn drop(&mut self) {
1082        self.mesh.rpc_client_pending_arc().cancel(self.call_id);
1083        // Fire the observer with the latched status (Ok / Error /
1084        // Canceled). Idempotent — only the first call emits.
1085        self.observer.fire();
1086        if self.clean_close.load(Ordering::SeqCst) {
1087            return;
1088        }
1089        if !self.initial_sent.load(Ordering::SeqCst) {
1090            return;
1091        }
1092        spawn_cancel_publish(
1093            Arc::clone(&self.mesh),
1094            self.target_node_id,
1095            self.request_channel.clone(),
1096            self.self_origin,
1097            self.call_id,
1098        );
1099    }
1100}
1101
1102/// Send half of a duplex call. Push items via `send`; emit the
1103/// terminal REQUEST_END frame via `finish_sending`. After
1104/// `finish_sending` the upload side is closed but the sibling
1105/// `DuplexStream` continues yielding response chunks until the
1106/// server's terminal frame arrives.
1107///
1108/// Bidi streaming plan (Phase D).
1109pub struct DuplexSink {
1110    inner: Arc<DuplexInner>,
1111    service: String,
1112    initial_headers: Vec<(String, Vec<u8>)>,
1113    initial_flags: u16,
1114    deadline_ns: u64,
1115    credit_sem: Option<Arc<tokio::sync::Semaphore>>,
1116    grant_pump: Option<JoinHandle<()>>,
1117    state: ClientStreamState,
1118}
1119
1120impl DuplexSink {
1121    /// Push one body chunk to the server. Same semantics as
1122    /// [`ClientStreamCallRaw::send`].
1123    pub async fn send(&mut self, body: Bytes) -> Result<(), RpcError> {
1124        match self.state {
1125            ClientStreamState::Finishing | ClientStreamState::Done => {
1126                return Err(RpcError::Codec {
1127                    direction: CodecDirection::Encode,
1128                    message: "send() called after finish_sending()".to_string(),
1129                });
1130            }
1131            _ => {}
1132        }
1133        if let Some(sem) = self.credit_sem.as_ref() {
1134            let permit = sem.clone().acquire_owned().await.map_err(|_| {
1135                RpcError::Transport(AdapterError::Connection("credit semaphore closed".into()))
1136            })?;
1137            permit.forget();
1138        }
1139        self.inner.observer.add_request_bytes(body.len() as u32);
1140        match self.state {
1141            ClientStreamState::JustOpened => {
1142                let req = RpcRequestPayload {
1143                    service: self.service.clone(),
1144                    deadline_ns: self.deadline_ns,
1145                    flags: self.initial_flags,
1146                    headers: std::mem::take(&mut self.initial_headers),
1147                    body: body.clone(),
1148                };
1149                self.publish_initial_request(&req).await?;
1150                self.inner.initial_sent.store(true, Ordering::SeqCst);
1151                self.state = ClientStreamState::Sending;
1152            }
1153            ClientStreamState::Sending => {
1154                let chunk = RpcRequestChunkPayload {
1155                    call_id: self.inner.call_id,
1156                    flags: 0,
1157                    headers: vec![],
1158                    body: body.clone(),
1159                };
1160                publish_request_chunk(
1161                    &self.inner.mesh,
1162                    self.inner.target_node_id,
1163                    self.inner.request_channel_hash,
1164                    self.inner.request_stream_id,
1165                    self.inner.self_origin,
1166                    &chunk,
1167                )
1168                .await?;
1169            }
1170            ClientStreamState::Finishing | ClientStreamState::Done => unreachable!(),
1171        }
1172        Ok(())
1173    }
1174
1175    /// Close the upload direction. Emits the terminal REQUEST_END
1176    /// frame. The response stream continues until the server's
1177    /// terminal RESPONSE arrives (use the sibling `DuplexStream`).
1178    pub async fn finish_sending(mut self) -> Result<(), RpcError> {
1179        match self.state {
1180            ClientStreamState::JustOpened => {
1181                let req = RpcRequestPayload {
1182                    service: self.service.clone(),
1183                    deadline_ns: self.deadline_ns,
1184                    flags: self.initial_flags | FLAG_RPC_REQUEST_END,
1185                    headers: std::mem::take(&mut self.initial_headers),
1186                    body: Bytes::new(),
1187                };
1188                self.publish_initial_request(&req).await?;
1189                self.inner.initial_sent.store(true, Ordering::SeqCst);
1190            }
1191            ClientStreamState::Sending => {
1192                let chunk = RpcRequestChunkPayload {
1193                    call_id: self.inner.call_id,
1194                    flags: FLAG_RPC_REQUEST_END,
1195                    headers: vec![],
1196                    body: Bytes::new(),
1197                };
1198                publish_request_chunk(
1199                    &self.inner.mesh,
1200                    self.inner.target_node_id,
1201                    self.inner.request_channel_hash,
1202                    self.inner.request_stream_id,
1203                    self.inner.self_origin,
1204                    &chunk,
1205                )
1206                .await?;
1207            }
1208            ClientStreamState::Finishing | ClientStreamState::Done => {
1209                return Err(RpcError::Codec {
1210                    direction: CodecDirection::Encode,
1211                    message: "finish_sending() called twice".to_string(),
1212                });
1213            }
1214        }
1215        self.state = ClientStreamState::Finishing;
1216        Ok(())
1217    }
1218
1219    /// Server-assigned `call_id`. Same value on the sibling
1220    /// `DuplexStream`.
1221    pub fn call_id(&self) -> u64 {
1222        self.inner.call_id
1223    }
1224
1225    /// Whether this call is flow-controlled on the upload side.
1226    pub fn flow_controlled(&self) -> bool {
1227        self.credit_sem.is_some()
1228    }
1229
1230    async fn publish_initial_request(&self, req: &RpcRequestPayload) -> Result<(), RpcError> {
1231        let meta = EventMeta::new(
1232            DISPATCH_RPC_REQUEST,
1233            0,
1234            self.inner.self_origin,
1235            self.inner.call_id,
1236            0,
1237        );
1238        let mut buf = Vec::with_capacity(EVENT_META_SIZE + req.encoded_len());
1239        buf.extend_from_slice(&meta.to_bytes());
1240        req.encode_into(&mut buf);
1241        let payload = Bytes::from(buf);
1242        // PERF_AUDIT §3.10 — cached hash + stream_id from the
1243        // inner `ClientStreamCallRaw`.
1244        self.inner
1245            .mesh
1246            .publish_to_peer(
1247                self.inner.target_node_id,
1248                self.inner.request_channel_hash,
1249                self.inner.request_stream_id,
1250                /* reliable */ true,
1251                std::slice::from_ref(&payload),
1252            )
1253            .await
1254            .map_err(RpcError::Transport)
1255    }
1256}
1257
1258impl Drop for DuplexSink {
1259    fn drop(&mut self) {
1260        if let Some(task) = self.grant_pump.take() {
1261            task.abort();
1262        }
1263        // The shared DuplexInner's Drop (when refcount hits 0)
1264        // does the CANCEL — nothing to do here beyond aborting
1265        // the grant pump.
1266    }
1267}
1268
1269/// Receive half of a duplex call. Implements `futures::Stream`
1270/// yielding `Result<Bytes, RpcError>` per inbound RESPONSE chunk.
1271/// EOF on terminal Ok; one final `Err(RpcError::ServerError)` on
1272/// terminal non-Ok.
1273///
1274/// Bidi streaming plan (Phase D).
1275pub struct DuplexStream {
1276    inner: Arc<DuplexInner>,
1277    chunks_rx: tokio::sync::mpsc::UnboundedReceiver<StreamItem>,
1278    done: bool,
1279}
1280
1281impl DuplexStream {
1282    /// Server-assigned `call_id`. Same value on the sibling
1283    /// `DuplexSink`.
1284    pub fn call_id(&self) -> u64 {
1285        self.inner.call_id
1286    }
1287}
1288
1289impl futures::Stream for DuplexStream {
1290    type Item = Result<Bytes, RpcError>;
1291
1292    fn poll_next(
1293        mut self: std::pin::Pin<&mut Self>,
1294        cx: &mut std::task::Context<'_>,
1295    ) -> std::task::Poll<Option<Self::Item>> {
1296        if self.done {
1297            return std::task::Poll::Ready(None);
1298        }
1299        match self.chunks_rx.poll_recv(cx) {
1300            std::task::Poll::Ready(Some(StreamItem::Chunk(body))) => {
1301                self.inner.observer.add_response_bytes(body.len() as u32);
1302                std::task::Poll::Ready(Some(Ok(body)))
1303            }
1304            std::task::Poll::Ready(Some(StreamItem::End)) => {
1305                self.done = true;
1306                self.inner.clean_close.store(true, Ordering::SeqCst);
1307                self.inner.observer.latch_ok();
1308                std::task::Poll::Ready(None)
1309            }
1310            std::task::Poll::Ready(Some(StreamItem::Error(resp))) => {
1311                self.done = true;
1312                self.inner.clean_close.store(true, Ordering::SeqCst);
1313                let status = resp.status.to_wire();
1314                let message = String::from_utf8(resp.body.to_vec()).unwrap_or_else(|e| {
1315                    format!("<{} bytes of non-utf8 body>", e.into_bytes().len())
1316                });
1317                self.inner
1318                    .observer
1319                    .latch_error(format!("server returned status {status:#06x}: {message}"));
1320                std::task::Poll::Ready(Some(Err(RpcError::ServerError {
1321                    status,
1322                    message,
1323                    headers: resp.headers,
1324                })))
1325            }
1326            std::task::Poll::Ready(None) => {
1327                self.done = true;
1328                std::task::Poll::Ready(None)
1329            }
1330            std::task::Poll::Pending => std::task::Poll::Pending,
1331        }
1332    }
1333}
1334
1335/// Caller-side handle for a duplex RPC. Combines a `DuplexSink`
1336/// (upload) and `DuplexStream` (download). For application code
1337/// that wants to encode requests in one task and decode responses
1338/// in another, use [`Self::into_split`] to peel off the two halves.
1339///
1340/// Bidi streaming plan (Phase D).
1341pub struct DuplexCallRaw {
1342    sink: DuplexSink,
1343    stream: DuplexStream,
1344}
1345
1346impl DuplexCallRaw {
1347    /// Server-assigned `call_id`.
1348    pub fn call_id(&self) -> u64 {
1349        self.sink.call_id()
1350    }
1351
1352    /// Whether the upload side is flow-controlled.
1353    pub fn flow_controlled(&self) -> bool {
1354        self.sink.flow_controlled()
1355    }
1356
1357    /// Push one body chunk to the server. Delegates to the inner
1358    /// `DuplexSink::send`.
1359    pub async fn send(&mut self, body: Bytes) -> Result<(), RpcError> {
1360        self.sink.send(body).await
1361    }
1362
1363    /// Close the upload direction. Delegates to the inner
1364    /// `DuplexSink::finish_sending` but keeps the receive side
1365    /// alive so the caller can keep polling response chunks.
1366    ///
1367    /// NOTE: consumes the sink half but not the stream half.
1368    /// Internally, we replace `self.sink` with a no-op
1369    /// placeholder so subsequent send() / finish_sending()
1370    /// surface a clear error (`send() after finish_sending()`).
1371    pub async fn finish_sending(&mut self) -> Result<(), RpcError> {
1372        // Take the sink out by swapping in a placeholder whose
1373        // state is `Done` so subsequent sends error cleanly.
1374        let placeholder = DuplexSink {
1375            inner: Arc::clone(&self.sink.inner),
1376            service: String::new(),
1377            initial_headers: Vec::new(),
1378            initial_flags: 0,
1379            deadline_ns: 0,
1380            credit_sem: None,
1381            grant_pump: None,
1382            state: ClientStreamState::Done,
1383        };
1384        let sink = std::mem::replace(&mut self.sink, placeholder);
1385        sink.finish_sending().await
1386    }
1387
1388    /// Pull the next response chunk. `None` on terminal Ok;
1389    /// `Some(Err)` then `None` on terminal non-Ok. Same shape as
1390    /// `futures::StreamExt::next`.
1391    pub async fn next(&mut self) -> Option<Result<Bytes, RpcError>> {
1392        use futures::StreamExt;
1393        self.stream.next().await
1394    }
1395
1396    /// Split into independent send / receive halves. Both halves
1397    /// hold an `Arc<DuplexInner>`; CANCEL fires only when BOTH
1398    /// halves drop without a clean close.
1399    pub fn into_split(self) -> (DuplexSink, DuplexStream) {
1400        (self.sink, self.stream)
1401    }
1402}
1403
1404impl futures::Stream for DuplexCallRaw {
1405    type Item = Result<Bytes, RpcError>;
1406
1407    fn poll_next(
1408        mut self: std::pin::Pin<&mut Self>,
1409        cx: &mut std::task::Context<'_>,
1410    ) -> std::task::Poll<Option<Self::Item>> {
1411        std::pin::Pin::new(&mut self.stream).poll_next(cx)
1412    }
1413}
1414
1415// ============================================================================
1416// Unary call: CANCEL-on-drop guard.
1417// ============================================================================
1418
1419/// RAII guard that fires CANCEL to the server if the unary call
1420/// future is dropped before a response arrives. Without this, a
1421/// `select!`-loser future (e.g. hedge runner-up) would leave the
1422/// server-side handler running to completion — wasting CPU on a
1423/// reply nobody will read.
1424///
1425/// The guard is built *after* the REQUEST has been successfully
1426/// published — if the publish fails, no guard is constructed and
1427/// no CANCEL is sent. On the success path the call function flips
1428/// `completed = true` so Drop becomes a no-op (the server already
1429/// finished and removed its in-flight entry).
1430struct UnaryCallGuard {
1431    pending: Arc<super::cortex::RpcClientPending>,
1432    mesh: Arc<MeshNode>,
1433    target_node_id: u64,
1434    request_channel: ChannelName,
1435    self_origin: u64,
1436    call_id: u64,
1437    /// True after the call resolved Ok or got a definitive
1438    /// non-cancellable Err. Drop checks this — `false` fires
1439    /// CANCEL, `true` is a no-op (still removes the pending
1440    /// entry).
1441    completed: bool,
1442}
1443
1444impl Drop for UnaryCallGuard {
1445    fn drop(&mut self) {
1446        self.pending.cancel(self.call_id);
1447        if !self.completed {
1448            spawn_cancel_publish(
1449                Arc::clone(&self.mesh),
1450                self.target_node_id,
1451                self.request_channel.clone(),
1452                self.self_origin,
1453                self.call_id,
1454            );
1455        }
1456    }
1457}
1458
1459// ============================================================================
1460// Streaming/duplex observer-fire bookkeeping.
1461//
1462// The unary `MeshNode::call` fires `RpcObserver::on_call` at each
1463// terminal return path (see line ~2306). The streaming /
1464// client-streaming / duplex paths have multiple terminal points
1465// (poll_next sees End / Error; finish() returns; Drop without
1466// terminal observation). To avoid sprinkling `fire_rpc_observer_outbound`
1467// at every terminal site, each handle holds a
1468// `StreamingObserverState` that latches the terminal status on
1469// observation and fires exactly once on Drop. The Deck NRPC tab
1470// + every consumer of `RpcObserver` get one event per streaming
1471// / duplex call, same as for unary today.
1472// ============================================================================
1473
1474/// Per-call observer-fire bookkeeping shared between the
1475/// streaming + client-streaming + duplex caller-side handles.
1476/// Latches terminal status on observation; `fire()` (called from
1477/// the handle's Drop) emits one `RpcCallEvent` with the latched
1478/// status (or `Canceled` if nothing latched — i.e. the handle
1479/// was dropped before observing its terminator).
1480///
1481/// Status discriminator:
1482///   0 = none latched (Drop → Canceled)
1483///   1 = Ok
1484///   2 = Error (message in `observer_msg`)
1485///   3 = Timeout
1486pub(crate) struct StreamingObserverState {
1487    mesh: Arc<MeshNode>,
1488    target_node_id: u64,
1489    service: String,
1490    started: Instant,
1491    request_bytes: AtomicU32,
1492    response_bytes: AtomicU32,
1493    observer_status: AtomicU8,
1494    observer_msg: parking_lot::Mutex<Option<String>>,
1495    fired: AtomicBool,
1496}
1497
1498impl StreamingObserverState {
1499    pub(crate) fn new(
1500        mesh: Arc<MeshNode>,
1501        target_node_id: u64,
1502        service: impl Into<String>,
1503        request_bytes: u32,
1504    ) -> Self {
1505        Self {
1506            mesh,
1507            target_node_id,
1508            service: service.into(),
1509            started: Instant::now(),
1510            request_bytes: AtomicU32::new(request_bytes),
1511            response_bytes: AtomicU32::new(0),
1512            observer_status: AtomicU8::new(0),
1513            observer_msg: parking_lot::Mutex::new(None),
1514            fired: AtomicBool::new(false),
1515        }
1516    }
1517
1518    pub(crate) fn add_request_bytes(&self, n: u32) {
1519        self.request_bytes.fetch_add(n, Ordering::Relaxed);
1520    }
1521
1522    pub(crate) fn add_response_bytes(&self, n: u32) {
1523        self.response_bytes.fetch_add(n, Ordering::Relaxed);
1524    }
1525
1526    pub(crate) fn latch_ok(&self) {
1527        self.observer_status.store(1, Ordering::Relaxed);
1528    }
1529
1530    pub(crate) fn latch_error(&self, msg: impl Into<String>) {
1531        *self.observer_msg.lock() = Some(msg.into());
1532        self.observer_status.store(2, Ordering::Relaxed);
1533    }
1534
1535    pub(crate) fn latch_timeout(&self) {
1536        self.observer_status.store(3, Ordering::Relaxed);
1537    }
1538
1539    /// Fire the observer event. Idempotent — only the first call
1540    /// actually emits; subsequent are no-ops. Called from each
1541    /// streaming handle's Drop.
1542    pub(crate) fn fire(&self) {
1543        if self.fired.swap(true, Ordering::SeqCst) {
1544            return;
1545        }
1546        let status_code = self.observer_status.load(Ordering::Relaxed);
1547        let status = match status_code {
1548            1 => crate::adapter::net::cortex::rpc_observer::RpcCallStatus::Ok,
1549            2 => {
1550                let msg = self.observer_msg.lock().clone().unwrap_or_default();
1551                crate::adapter::net::cortex::rpc_observer::RpcCallStatus::Error(msg)
1552            }
1553            3 => crate::adapter::net::cortex::rpc_observer::RpcCallStatus::Timeout,
1554            _ => crate::adapter::net::cortex::rpc_observer::RpcCallStatus::Canceled,
1555        };
1556        self.mesh.fire_rpc_observer_outbound(
1557            self.target_node_id,
1558            &self.service,
1559            self.started.elapsed().as_millis() as u32,
1560            status,
1561            self.request_bytes.load(Ordering::Relaxed),
1562            self.response_bytes.load(Ordering::Relaxed),
1563        );
1564    }
1565}
1566
1567/// Per-call cap on in-flight request-direction credits. Tokio's
1568/// `Semaphore::MAX_PERMITS` is `usize::MAX >> 3`; we cap the
1569/// caller-side accumulator at this value so a misbehaving server
1570/// can't make the caller hold an unbounded outstanding window.
1571/// 1M credits is already orders of magnitude beyond any sane
1572/// request burst — a caller sitting on 1M unconsumed credits is
1573/// either misconfigured or under attack.
1574const REQUEST_GRANT_PER_CALL_CAP: usize = 1_000_000;
1575
1576/// Add `credits` to a caller-side request-direction credit
1577/// semaphore, capped so the accumulator never exceeds
1578/// [`REQUEST_GRANT_PER_CALL_CAP`]. Per-frame cap of `usize::MAX >> 4`
1579/// remains as a second line of defense against pathological frame
1580/// values.
1581fn add_request_grant_credits(sem: &tokio::sync::Semaphore, credits: u32) {
1582    if credits == 0 {
1583        return;
1584    }
1585    let current = sem.available_permits();
1586    let remaining = REQUEST_GRANT_PER_CALL_CAP.saturating_sub(current);
1587    let safe = (credits as usize).min(usize::MAX >> 4).min(remaining);
1588    if safe > 0 {
1589        sem.add_permits(safe);
1590    }
1591}
1592
1593/// Build a coalescing REQUEST_GRANT emitter.
1594///
1595/// Naive emitters `tokio::spawn` one publish task per consumed
1596/// chunk, which becomes a spawn-storm + AEAD-storm under bursting.
1597/// This helper hands back an emitter that pushes `(caller_origin,
1598/// call_id, credits)` into an unbounded mpsc; a single dedicated
1599/// drainer task `try_recv`s the queue to drain whatever is
1600/// immediately available, coalesces credits per call_id, and
1601/// publishes ONE batched REQUEST_GRANT per call per drain cycle.
1602///
1603/// Lifecycle: the drainer task lives as long as any clone of the
1604/// returned emitter (mpsc sender count > 0). When the fold and all
1605/// in-flight handlers release the emitter, `rx.recv` returns `None`
1606/// and the drainer exits naturally.
1607fn build_request_grant_emitter(
1608    mesh: Arc<MeshNode>,
1609    service: String,
1610    server_origin: u64,
1611    diag_tag: &'static str,
1612) -> RpcRequestGrantEmitter {
1613    let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<(u64, u64, u32)>();
1614    tokio::spawn(async move {
1615        while let Some(first) = rx.recv().await {
1616            let mut summed: std::collections::HashMap<(u64, u64), u32> =
1617                std::collections::HashMap::new();
1618            let (caller, call_id, credits) = first;
1619            summed.insert((caller, call_id), credits);
1620            // Coalesce anything immediately queued behind the first
1621            // wake. Bounded by what the substrate has produced so
1622            // far; doesn't add latency since `try_recv` returns
1623            // immediately when the queue is empty.
1624            while let Ok((caller, call_id, credits)) = rx.try_recv() {
1625                let entry = summed.entry((caller, call_id)).or_insert(0);
1626                *entry = entry.saturating_add(credits);
1627            }
1628            for ((caller, call_id), credits) in summed {
1629                let reply_channel_name = format!("{service}.replies.{caller:016x}");
1630                let reply_channel = match ChannelName::new(&reply_channel_name) {
1631                    Ok(c) => c,
1632                    Err(e) => {
1633                        tracing::warn!(
1634                            error = %e,
1635                            channel = %reply_channel_name,
1636                            tag = diag_tag,
1637                            "rpc grant drainer: invalid reply channel name");
1638                        continue;
1639                    }
1640                };
1641                let meta = EventMeta::new(DISPATCH_RPC_REQUEST_GRANT, 0, server_origin, call_id, 0);
1642                let mut buf = Vec::with_capacity(EVENT_META_SIZE + 12);
1643                buf.extend_from_slice(&meta.to_bytes());
1644                buf.extend_from_slice(&encode_request_grant(call_id, credits));
1645                let publisher = ChannelPublisher::new(reply_channel, PublishConfig::default());
1646                if let Err(e) = mesh.publish(&publisher, Bytes::from(buf)).await {
1647                    tracing::warn!(
1648                        error = %e,
1649                        caller_origin = format!("{:#x}", caller),
1650                        call_id,
1651                        tag = diag_tag,
1652                        "rpc grant drainer: REQUEST_GRANT publish failed");
1653                }
1654            }
1655        }
1656    });
1657    Arc::new(move |caller_origin, call_id, credits| {
1658        // Send failure means the drainer has exited (all sender
1659        // clones dropped, then we somehow cloned a stale one).
1660        // Treat as a no-op — the call is tearing down anyway.
1661        let _ = tx.send((caller_origin, call_id, credits));
1662    })
1663}
1664
1665/// Per-service map from a caller's `origin_hash` (wire field) to the
1666/// AEAD-verified `from_node` of the session that delivered their
1667/// inbound REQUEST. Populated by the serve_rpc bridge tasks at
1668/// REQUEST-receipt time; consulted by [`publish_response_to_caller`]
1669/// to skip the roster fan-out on the response leg.
1670///
1671/// Lives per `serve_rpc*` registration rather than mesh-wide because
1672/// the source-of-truth `MeshNode::origin_hash_to_node` is only safe
1673/// to populate from *signed* capability announcements — populating
1674/// it from unsigned wire `origin_hash` fields would let any session
1675/// peer pre-claim arbitrary origins. This map is bridge-local and
1676/// only used by the matching service's response emit, so a malicious
1677/// peer can at most misdirect responses for THEIR own request — they
1678/// already could.
1679///
1680/// **Bounded** ([`OriginKeyedLru`]): the key is the wire-claimed
1681/// `origin_hash` and the bridge inserts it *before* the capability gate,
1682/// so an unbounded map would let one authed peer spray distinct origins and
1683/// amplify server memory. The LRU caps the footprint; eviction costs only a
1684/// response-path cache miss (roster fallback), never correctness.
1685type RpcOriginNodeCache = Arc<OriginKeyedLru<u64>>;
1686
1687/// Capacity bound for the per-`serve_rpc` caller-keyed caches
1688/// ([`RpcOriginNodeCache`] and the §8b reply-channel cache). Sized for the
1689/// legitimate active-caller working set of a single service; well past it the
1690/// LRU evicts cold origins rather than growing without limit under a
1691/// crafted-origin flood. Each entry is tiny (a `u64` and, for the reply
1692/// cache, an `Arc<str>` channel name), so the whole bound is a few hundred KB
1693/// per service.
1694const RPC_CALLER_CACHE_CAP: usize = 4096;
1695
1696/// Non-zero form of [`RPC_CALLER_CACHE_CAP`], validated at compile time so
1697/// `OriginKeyedLru::new` carries no runtime `unwrap`/`expect`. A zero cap
1698/// would fail the build here rather than panic at startup.
1699const RPC_CALLER_CACHE_CAP_NZ: std::num::NonZeroUsize =
1700    match std::num::NonZeroUsize::new(RPC_CALLER_CACHE_CAP) {
1701        Some(n) => n,
1702        None => panic!("RPC_CALLER_CACHE_CAP must be non-zero"),
1703    };
1704
1705/// Thread-safe, bounded LRU keyed by the wire-claimed caller `origin_hash`.
1706///
1707/// Backs both [`RpcOriginNodeCache`] and the §8b reply-channel cache. Wraps
1708/// `lru::LruCache` (which needs `&mut` even to read, to bump the entry to
1709/// most-recently-used) in a `parking_lot::Mutex`. The per-response lock is
1710/// uncontended in the common case — one fold drives a given service — and is
1711/// far cheaper than the `format!` + `ChannelName` allocation / roster fan-out
1712/// the caches exist to avoid. Eviction is always safe: a miss just recomputes
1713/// the value (channel name) or falls back to the roster lookup.
1714struct OriginKeyedLru<V>(Mutex<lru::LruCache<u64, V>>);
1715
1716impl<V: Clone> OriginKeyedLru<V> {
1717    fn new() -> Self {
1718        Self(Mutex::new(lru::LruCache::new(RPC_CALLER_CACHE_CAP_NZ)))
1719    }
1720
1721    /// Look up `origin`, promoting it to most-recently-used on a hit.
1722    fn get(&self, origin: u64) -> Option<V> {
1723        self.0.lock().get(&origin).cloned()
1724    }
1725
1726    /// Insert / refresh `origin`, evicting the least-recently-used entry
1727    /// when at capacity.
1728    fn insert(&self, origin: u64, value: V) {
1729        self.0.lock().put(origin, value);
1730    }
1731}
1732
1733/// Direct-send a built RESPONSE (or streaming chunk) packet to the
1734/// caller's reply channel, bypassing the roster fan-out path
1735/// [`MeshNode::publish`] uses.
1736///
1737/// **Fast path:** when the bridge has cached the caller's
1738/// `from_node` (i.e. the server processed an inbound REQUEST from
1739/// this caller via an AEAD-authenticated session), or when the
1740/// caller's capability announcement has reached us, the response
1741/// rides `publish_to_peer` — one DashMap lookup instead of roster
1742/// lookup + ACL check + subnet filter + per-recipient `Vec<Bytes>`
1743/// allocation.
1744///
1745/// **Fallback:** when neither lookup resolves — pathological cases
1746/// like a test harness where the caller never announces and the
1747/// bridge cache is empty — fall back to [`MeshNode::publish`] via
1748/// the roster, matching the pre-T1.2 behavior verbatim.
1749/// PERF_AUDIT §3.10 — accepts pre-computed
1750/// `reply_channel_hash` and `reply_stream_id` so the per-response
1751/// path doesn't re-run `ChannelId::new` + xxh3 + `publish_stream_id`
1752/// on every send. The emit closure's `OriginKeyedLru<CachedReplyChannel>`
1753/// caches the triple per caller_origin.
1754async fn publish_response_to_caller(
1755    mesh: &MeshNode,
1756    caller_origin: u64,
1757    target_hint: Option<u64>,
1758    reply_channel: &ChannelName,
1759    reply_channel_hash: ChannelHash,
1760    reply_stream_id: u64,
1761    payload: Bytes,
1762) -> Result<(), AdapterError> {
1763    let resolved = target_hint.or_else(|| mesh.get_node_by_origin_hash(caller_origin));
1764    if let Some(target_node_id) = resolved {
1765        return mesh
1766            .publish_to_peer(
1767                target_node_id,
1768                reply_channel_hash,
1769                reply_stream_id,
1770                /* reliable */ true,
1771                std::slice::from_ref(&payload),
1772            )
1773            .await;
1774    }
1775    // Fallback: roster fan-out. Reached when the caller's origin is
1776    // unknown to both the bridge cache AND the global reverse index.
1777    let publisher = ChannelPublisher::new(reply_channel.clone(), PublishConfig::default());
1778    mesh.publish(&publisher, payload).await.map(|_| ())
1779}
1780
1781/// Shared CANCEL-publish helper: spawn a task that fires a
1782/// CANCEL event for `call_id` to `target` on the request channel.
1783/// Both [`RpcStream::Drop`] and [`UnaryCallGuard::Drop`] use it.
1784fn spawn_cancel_publish(
1785    mesh: Arc<MeshNode>,
1786    target: u64,
1787    request_channel: ChannelName,
1788    self_origin: u64,
1789    call_id: u64,
1790) {
1791    tokio::spawn(async move {
1792        let meta = EventMeta::new(DISPATCH_RPC_CANCEL, 0, self_origin, call_id, 0);
1793        let request_channel_id = ChannelId::new(request_channel);
1794        let request_channel_hash = request_channel_id.hash();
1795        let stream_id = MeshNode::publish_stream_id(&request_channel_id);
1796        let payload = Bytes::from(meta.to_bytes().to_vec());
1797        let _ = mesh
1798            .publish_to_peer(
1799                target,
1800                request_channel_hash,
1801                stream_id,
1802                /* reliable */ true,
1803                std::slice::from_ref(&payload),
1804            )
1805            .await;
1806    });
1807}
1808
1809/// Type alias for the keep-alive sender that streaming-call handles
1810/// store. Its purpose is *only* to signal "stream done" when the
1811/// handle drops: the cancel-watcher task `select!`s on the matching
1812/// receiver, and dropping the sender (which happens on handle Drop)
1813/// resolves the receiver with an `Err` so the watcher exits cleanly.
1814///
1815/// `()` payload because the signal IS the resolution; no data is
1816/// transmitted.
1817type StreamCancelKeepAlive = tokio::sync::oneshot::Sender<()>;
1818
1819/// Spawn a cancel-watcher task for a streaming call (call_streaming,
1820/// call_client_stream, call_duplex). The watcher races
1821/// `cancel_notify.notified()` against the keep-alive oneshot — first
1822/// to fire wins. On cancel, the watcher drops the pending-streaming
1823/// entry (which closes the receiver's mpsc, letting the stream's
1824/// poll_next observe EOF), then releases the registry entry. On
1825/// handle Drop, the keep-alive sender drops, the oneshot resolves
1826/// `Err`, and the watcher exits via the done arm with a registry
1827/// release.
1828///
1829/// When `cancel_token == 0` (the "no token" sentinel), this is a
1830/// no-op: the returned sender is a placeholder whose drop has no
1831/// observable effect, and no task is spawned. Lets the streaming
1832/// call shapes always store a keep-alive on the returned handle
1833/// without branching on whether a token was set.
1834fn spawn_stream_cancel_watcher(
1835    cancel_notify: Arc<tokio::sync::Notify>,
1836    cancel_token: u64,
1837    cancel_registry: Arc<crate::adapter::net::cancel_registry::CancelRegistry>,
1838    pending: Arc<crate::adapter::net::cortex::RpcClientPending>,
1839    call_id: u64,
1840) -> StreamCancelKeepAlive {
1841    let (done_tx, done_rx) = tokio::sync::oneshot::channel();
1842    if cancel_token == 0 {
1843        // No-op fast path. The returned sender is held by the
1844        // handle but never paired with a watcher, so its eventual
1845        // drop has no effect. Avoids spawning a task per
1846        // cancel-less stream.
1847        return done_tx;
1848    }
1849    tokio::spawn(async move {
1850        tokio::select! {
1851            biased;
1852            _ = cancel_notify.notified() => {
1853                // Cancel fired. Drop the pending-stream entry so
1854                // the receiver's mpsc closes (causing the stream's
1855                // poll_next to observe EOF via Ready(None)). The
1856                // handle's Drop will then fire CANCEL on the wire
1857                // via its existing per-shape Drop impl.
1858                pending.cancel(call_id);
1859                cancel_registry.release(cancel_token);
1860            }
1861            _ = done_rx => {
1862                // Stream completed normally — sender dropped on
1863                // handle Drop, recv returns Err. Just release the
1864                // registry entry; no CANCEL emission needed (the
1865                // handle's Drop handles that path itself if it
1866                // wasn't a clean close).
1867                cancel_registry.release(cancel_token);
1868            }
1869        }
1870    });
1871    done_tx
1872}
1873
1874/// One-call helper that registers a cancel-notify against the
1875/// caller's `opts.cancel_token` and spawns the stream cancel
1876/// watcher. Used by every streaming call shape (`call_streaming`,
1877/// `call_client_stream`, `call_duplex`) to keep their bodies free
1878/// of the three-step token/notify/spawn boilerplate.
1879///
1880/// When `opts.cancel_token` is `None` (or `Some(0)`), this is the
1881/// same no-op fast path as [`spawn_stream_cancel_watcher`].
1882fn arm_stream_cancel(
1883    mesh: &Arc<MeshNode>,
1884    opts: &CallOptions,
1885    pending: &Arc<crate::adapter::net::cortex::RpcClientPending>,
1886    call_id: u64,
1887) -> StreamCancelKeepAlive {
1888    let cancel_token = opts.cancel_token.unwrap_or(0);
1889    let cancel_notify = mesh.cancel_registry().register_notify(cancel_token);
1890    spawn_stream_cancel_watcher(
1891        cancel_notify,
1892        cancel_token,
1893        Arc::clone(mesh.cancel_registry()),
1894        Arc::clone(pending),
1895        call_id,
1896    )
1897}
1898
1899/// Side-effects + return value for the unary `call`'s cancel
1900/// branch. Releases the registry entry, records the Transport
1901/// outcome on the metrics guard, fires the Canceled observer
1902/// event, and returns `RpcError::Cancelled`. Both the
1903/// no-deadline and with-deadline `select!` arms invoke this so a
1904/// shape change to the cancel outcome (extra metric, new field
1905/// on the observer event) lands in exactly one place.
1906fn fire_unary_cancel_outcome(
1907    mesh: &Arc<MeshNode>,
1908    metrics_guard: &mut crate::adapter::net::mesh_rpc_metrics::CallMetricsGuard,
1909    cancel_token: u64,
1910    target_node_id: u64,
1911    service: &str,
1912    started_total: Instant,
1913    request_bytes_len: u32,
1914) -> RpcError {
1915    mesh.cancel_registry().release(cancel_token);
1916    metrics_guard.record(crate::adapter::net::mesh_rpc_metrics::CallOutcome::Transport);
1917    mesh.fire_rpc_observer_outbound(
1918        target_node_id,
1919        service,
1920        started_total.elapsed().as_millis() as u32,
1921        crate::adapter::net::cortex::rpc_observer::RpcCallStatus::Canceled,
1922        request_bytes_len,
1923        0,
1924    );
1925    RpcError::Cancelled
1926}
1927
1928// ============================================================================
1929// MeshNode extensions.
1930// ============================================================================
1931
1932impl MeshNode {
1933    /// Register an nRPC handler for `service` on this node.
1934    ///
1935    /// Subscribes this node to `<service>.requests` (so the local
1936    /// `register_rpc_inbound` dispatcher feeds inbound REQUEST
1937    /// events into the [`RpcServerFold`]) and wires the fold's
1938    /// RESPONSE-emit callback to publish on
1939    /// `<service>.replies.<caller_origin>` via the existing
1940    /// pub/sub path.
1941    ///
1942    /// **Local-only registration** (Phase 1). Multi-instance
1943    /// services that load-balance via `SubscriptionMode::QueueGroup`
1944    /// require each replica to call `serve_rpc` on its own node;
1945    /// the mesh-level subscriber roster + `dispatch_recipients`
1946    /// then routes one-of-N as designed. Each replica's local
1947    /// `serve_rpc` must use the same service name (which becomes
1948    /// the queue-group identifier).
1949    ///
1950    /// Returns a [`ServeHandle`] whose Drop tears down the
1951    /// registration. Concurrent registrations for the same service
1952    /// on one node return `Err(ServeError::AlreadyServing)`.
1953    pub fn serve_rpc<H: RpcHandler>(
1954        self: &Arc<Self>,
1955        service: &str,
1956        handler: Arc<H>,
1957    ) -> Result<ServeHandle, ServeError> {
1958        let request_channel = ChannelName::new(&format!("{service}.requests"))
1959            .map_err(|e| ServeError::InvalidServiceName(e.to_string()))?;
1960        let channel_hash = request_channel.hash();
1961
1962        // Bridge: a tokio mpsc the inbound dispatcher pushes into.
1963        // The bridge task drains it and runs each event through
1964        // the fold. Bounded so a runaway publisher can't OOM the
1965        // server; over-cap pushes drop the inbound event (which
1966        // surfaces to the caller as a timeout).
1967        let (tx, mut rx) = mpsc::channel::<RpcInboundEvent>(1024);
1968
1969        // T1.2 cache: maps each caller's wire `origin_hash` to the
1970        // AEAD-verified `from_node` of the session that delivered
1971        // its REQUEST. Populated by the bridge below; consumed by
1972        // the emit closure so [`publish_response_to_caller`] can
1973        // skip the roster fan-out on the response leg.
1974        let origin_node_cache: RpcOriginNodeCache = Arc::new(OriginKeyedLru::new());
1975
1976        // Build the emit closure. When the handler completes, the
1977        // fold calls this (synchronously) with `(caller_origin, call_id,
1978        // response)`. §8a: instead of `tokio::spawn`ing a task per response,
1979        // the closure builds the wire payload (cheap, no await) and hands a
1980        // job to a single per-service response drainer task (below), which
1981        // does the `.await` publish. A `tokio::spawn` per response cost
1982        // ~1–2 µs of scheduling on a wake-bound path; a channel send is a
1983        // fraction of that, and the drainer amortizes the wakeup.
1984        let service_for_emit = service.to_string();
1985        let server_origin = self.identity_origin_hash();
1986        let origin_node_cache_for_emit = Arc::clone(&origin_node_cache);
1987        // §8b reply-channel cache: the reply channel name is
1988        // `<service>.replies.<caller_origin:016x>` — deterministic from
1989        // `(service, caller_origin)`, and `service` is fixed for this
1990        // `serve_rpc`, so it varies only by `caller_origin`. `ChannelName` is
1991        // `Arc<str>`, so a cache hit is an Arc bump; this removes the per-
1992        // response `format!` String + `ChannelName::new` (`Arc<str>`) allocation
1993        // (and the per-call `service.clone()`) the emit closure used to pay on
1994        // every response. Keyed by the wire-claimed `caller_origin` and so
1995        // bounded the same way as `origin_node_cache` above — an
1996        // `OriginKeyedLru`, not an unbounded map, so a crafted-origin flood
1997        // can't amplify server memory (a miss just rebuilds the name).
1998        // PERF_AUDIT §3.10 — cache the triple (name, hash, stream_id)
1999        // per caller_origin so the per-response drainer doesn't
2000        // recompute xxh3 + publish_stream_id on every send.
2001        let reply_channel_cache: Arc<OriginKeyedLru<CachedReplyChannel>> =
2002            Arc::new(OriginKeyedLru::new());
2003        // §8a response drainer channel. Bounded like the inbound channel; a
2004        // full channel means the drainer can't keep up, so we drop (the
2005        // caller times out) rather than block the fold.
2006        let (resp_tx, mut resp_rx) = mpsc::channel::<RpcResponseJob>(1024);
2007        let emit: RpcResponseEmitter = Arc::new(move |caller_origin, call_id, resp| {
2008            let target_hint = origin_node_cache_for_emit.get(caller_origin);
2009            // Resolve the reply channel from cache (Arc bump on hit; one
2010            // `format!` + `ChannelName::new` the first time we see a caller).
2011            let cached = match reply_channel_cache.get(caller_origin) {
2012                Some(c) => c,
2013                None => {
2014                    let name = format!("{service_for_emit}.replies.{caller_origin:016x}");
2015                    match ChannelName::new(&name) {
2016                        Ok(channel_name) => {
2017                            // Compute hash + stream_id ONCE per caller_origin
2018                            // and stash them alongside the name.
2019                            let channel_id = ChannelId::new(channel_name.clone());
2020                            let triple = CachedReplyChannel {
2021                                hash: channel_id.hash(),
2022                                stream_id: MeshNode::publish_stream_id(&channel_id),
2023                                name: channel_name,
2024                            };
2025                            reply_channel_cache.insert(caller_origin, triple.clone());
2026                            triple
2027                        }
2028                        Err(e) => {
2029                            tracing::warn!(error = %e, channel = %name,
2030                                "rpc serve_rpc: invalid reply channel name");
2031                            return;
2032                        }
2033                    }
2034                }
2035            };
2036            // Build the RESPONSE event envelope (24-byte meta + encoded
2037            // payload) synchronously — pure CPU, no await — then hand it to
2038            // the drainer.
2039            let meta = EventMeta::new(
2040                super::cortex::DISPATCH_RPC_RESPONSE,
2041                0,
2042                server_origin,
2043                call_id,
2044                0,
2045            );
2046            let mut buf = Vec::with_capacity(EVENT_META_SIZE + 64);
2047            buf.extend_from_slice(&meta.to_bytes());
2048            resp.encode_into(&mut buf);
2049            if resp_tx
2050                .try_send(RpcResponseJob {
2051                    caller_origin,
2052                    call_id,
2053                    target_hint,
2054                    reply_channel: cached.name,
2055                    reply_channel_hash: cached.hash,
2056                    reply_stream_id: cached.stream_id,
2057                    payload: Bytes::from(buf),
2058                })
2059                .is_err()
2060            {
2061                tracing::debug!(
2062                    caller_origin = format!("{:#x}", caller_origin),
2063                    call_id,
2064                    "rpc serve_rpc: response drainer at capacity; dropping response"
2065                );
2066            }
2067        });
2068
2069        // Build the server fold and wrap it in an Arc<Mutex<...>>
2070        // so the bridge task can drive it (the trait takes
2071        // `&mut self`). Attach the per-service metrics handle so
2072        // the spawned handler tasks bump server-side counters.
2073        let metrics_handle = self.rpc_metrics_arc().for_service(service);
2074        // Keep a clone of the emit closure for the callee-side
2075        // capability-auth defense-in-depth path in the bridge
2076        // below — the fold owns its own clone, this one only
2077        // emits the `CapabilityDenied` rejection before the fold
2078        // sees the event.
2079        let emit_for_bridge = Arc::clone(&emit);
2080        // Clone the per-service metrics handle so the bridge can
2081        // bump `capability_denied_total` on gate rejection. The
2082        // fold's own clone (passed via `with_metrics`) handles the
2083        // handler-side counters; this one covers the path BEFORE
2084        // the handler runs, which the fold-side metrics never see.
2085        let metrics_for_bridge = Arc::clone(&metrics_handle);
2086        let fold = Arc::new(Mutex::new(
2087            RpcServerFold::new(handler as Arc<dyn RpcHandler>, emit).with_metrics(metrics_handle),
2088        ));
2089
2090        // Register the inbound dispatcher. Push into the mpsc;
2091        // the bridge task does the actual fold work.
2092        let dispatcher: RpcInboundDispatcher = Arc::new(move |ev| {
2093            // Best-effort send — over-cap means the bridge can't
2094            // keep up; drop and let the caller time out. Logging
2095            // here would spam.
2096            let _ = tx.try_send(ev);
2097        });
2098        // Register the service in `rpc_local_services` and refresh
2099        // the self-indexed announcement BEFORE installing the
2100        // dispatcher so the callee-side gate (in the bridge below)
2101        // sees a self-announcement carrying `nrpc:<service>` the
2102        // moment the first inbound event lands. Without this, the
2103        // gate was either silently permissive (no self-ann) or
2104        // silently denying (self-ann from a prior
2105        // `announce_capabilities` that pre-dated this service's
2106        // registration). See `docs/misc/CODE_REVIEW_2026_05_19_CAPABILITY_AUTH.md`
2107        // H1 + H2.
2108        self.rpc_local_services_arc().insert(service.to_string());
2109        self.index_self_with_local_services();
2110
2111        if self
2112            .register_rpc_inbound(channel_hash, dispatcher)
2113            .is_some()
2114        {
2115            return Err(ServeError::AlreadyServing(service.to_string()));
2116        }
2117
2118        // Spawn the bridge task. It reads inbound events, runs
2119        // the v0.4 capability-auth callee-side gate (defense in
2120        // depth — the caller-side gate inside `call_service`
2121        // covers the well-behaved client path), and on accept
2122        // feeds them to the fold.
2123        let mesh_for_bridge = Arc::clone(self);
2124        let service_for_bridge = service.to_string();
2125        let origin_node_cache_for_bridge = Arc::clone(&origin_node_cache);
2126        let bridge = tokio::spawn(async move {
2127            let tag = format!("nrpc:{}", service_for_bridge);
2128            use crate::adapter::net::behavior::fold::capability_bridge;
2129            while let Some(inbound) = rx.recv().await {
2130                // T1.2 cache populate. `from_node` is the
2131                // AEAD-verified session peer; `origin_hash` is the
2132                // wire-claimed entity (untrusted on its own but
2133                // bound here to a session peer we just authed).
2134                // `from_node == 0` is the loopback/test sentinel —
2135                // skip the insert so the response path falls back
2136                // to the roster lookup instead of trying to send to
2137                // node 0.
2138                if inbound.from_node != 0 {
2139                    origin_node_cache_for_bridge.insert(inbound.origin_hash, inbound.from_node);
2140                }
2141                // Defense-in-depth check. Skip only when the wire
2142                // session resolved no NodeId (`from_node == 0` is
2143                // the loopback / test sentinel per
2144                // `RpcInboundEvent::from_node` — production wire
2145                // delivery drops events that fail NodeId
2146                // resolution rather than passing 0). The cold-
2147                // start "no self-ann" skip the original
2148                // implementation carried was a permissive hole;
2149                // `index_self_with_local_services` above
2150                // guarantees a self-ann exists before the
2151                // dispatcher is wired, so denying when the gate
2152                // says no is now the safe failure mode.
2153                let self_node = mesh_for_bridge.node_id();
2154                let from_node = inbound.from_node;
2155                if from_node != 0
2156                    && !capability_bridge::may_execute(
2157                        mesh_for_bridge.capability_fold(),
2158                        self_node,
2159                        &tag,
2160                        from_node,
2161                    )
2162                {
2163                    // Decode the EventMeta so we can address the
2164                    // caller's reply channel (keyed on
2165                    // `caller_origin`) and tag the response with
2166                    // the correct `call_id`. A garbled meta means
2167                    // the request would have been rejected by the
2168                    // fold's own decode path too; drop silently
2169                    // to match the existing skip-on-malformed
2170                    // behavior there.
2171                    let Some(meta) = (if inbound.payload.len() >= EVENT_META_SIZE {
2172                        EventMeta::from_bytes(&inbound.payload[..EVENT_META_SIZE])
2173                    } else {
2174                        None
2175                    }) else {
2176                        continue;
2177                    };
2178                    let resp = super::cortex::RpcResponsePayload {
2179                        status: RpcStatus::CapabilityDenied,
2180                        headers: vec![],
2181                        body: Bytes::from(format!(
2182                            "callee-side capability-auth gate denied nrpc:{}",
2183                            service_for_bridge
2184                        )),
2185                    };
2186                    // Server-side metrics: bump `capability_denied_total`
2187                    // on the per-service counter. The fold-side
2188                    // metrics never see this path (the handler isn't
2189                    // invoked), so without this bump a noisy
2190                    // unauthorized caller is invisible to operators
2191                    // watching `nrpc_handler_invocations_total` —
2192                    // the dashboard sees "0 requests" while the
2193                    // caller sees `CapabilityDenied`.
2194                    metrics_for_bridge
2195                        .capability_denied_total
2196                        .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2197                    (emit_for_bridge)(meta.origin_hash, meta.seq_or_ts, resp);
2198                    continue;
2199                }
2200                let payload = inbound.payload;
2201                let entry = RedexEntry::new_heap(0, 0, payload.len() as u32, 0, 0);
2202                let ev = RedexEvent { entry, payload };
2203                if let Err(e) = fold.lock().apply(&ev, &mut ()) {
2204                    tracing::warn!(error = %e, "rpc serve_rpc: fold apply error");
2205                }
2206            }
2207        });
2208
2209        // §8a response drainer. Drains `resp_rx` and does the `.await`
2210        // publish that the emit closure used to `tokio::spawn` per response.
2211        // Exits on its own when `resp_tx` (held only by the emit closure,
2212        // which the fold owns) is dropped — i.e. when the bridge task ends
2213        // and the fold drops, the same teardown that stops `_bridge`.
2214        let response_drain_mesh = Arc::clone(self);
2215        let response_drain = tokio::spawn(async move {
2216            while let Some(job) = resp_rx.recv().await {
2217                if let Err(e) = publish_response_to_caller(
2218                    &response_drain_mesh,
2219                    job.caller_origin,
2220                    job.target_hint,
2221                    &job.reply_channel,
2222                    job.reply_channel_hash,
2223                    job.reply_stream_id,
2224                    job.payload,
2225                )
2226                .await
2227                {
2228                    tracing::warn!(
2229                        error = %e,
2230                        caller_origin = format!("{:#x}", job.caller_origin),
2231                        call_id = job.call_id,
2232                        "rpc serve_rpc: response publish failed"
2233                    );
2234                }
2235            }
2236        });
2237
2238        // Spawn an async re-announce so peers also learn about
2239        // the new service without the operator having to call
2240        // `announce_capabilities` manually. The local self-index
2241        // already happened above; this is purely for peer
2242        // visibility (the broadcast path also re-runs the
2243        // self-index, which is a cheap version bump).
2244        let mesh_for_announce = Arc::clone(self);
2245        let service_for_log = service.to_string();
2246        tokio::spawn(async move {
2247            let baseline = mesh_for_announce.user_caps_snapshot();
2248            if let Err(e) = mesh_for_announce.announce_capabilities(baseline).await {
2249                tracing::warn!(
2250                    error = %e,
2251                    service = %service_for_log,
2252                    "serve_rpc: auto re-announce failed",
2253                );
2254            }
2255        });
2256
2257        Ok(ServeHandle {
2258            channel_hash,
2259            service: service.to_string(),
2260            _bridge: bridge,
2261            _response_drain: Some(response_drain),
2262            mesh: Arc::clone(self),
2263        })
2264    }
2265
2266    /// Streaming variant of [`Self::serve_rpc`]. The handler
2267    /// receives an [`RpcResponseSink`](super::cortex::RpcResponseSink)
2268    /// it writes chunks to via `sink.send(body)`; returning
2269    /// `Ok(())` closes the stream cleanly, `Err(_)` closes with
2270    /// an error frame.
2271    ///
2272    /// Wire-level identical to the unary path apart from the
2273    /// per-chunk `nrpc-streaming` header markers
2274    /// (`continue` / `end`). Same auto-registration of
2275    /// `<service>.requests` + `<service>.replies.` prefix.
2276    pub fn serve_rpc_streaming<H: RpcStreamingHandler>(
2277        self: &Arc<Self>,
2278        service: &str,
2279        handler: Arc<H>,
2280    ) -> Result<ServeHandle, ServeError> {
2281        let request_channel = ChannelName::new(&format!("{service}.requests"))
2282            .map_err(|e| ServeError::InvalidServiceName(e.to_string()))?;
2283        let channel_hash = request_channel.hash();
2284        let (tx, mut rx) = tokio::sync::mpsc::channel::<RpcInboundEvent>(1024);
2285
2286        // T1.2 cache: bridge populates from inbound.from_node, emit
2287        // closure consults to skip roster fan-out. See the unary
2288        // serve_rpc above for the full rationale.
2289        let origin_node_cache: RpcOriginNodeCache = Arc::new(OriginKeyedLru::new());
2290
2291        let mesh_for_emit = Arc::clone(self);
2292        let service_for_emit = service.to_string();
2293        let server_origin = self.identity_origin_hash();
2294        let origin_node_cache_for_emit = Arc::clone(&origin_node_cache);
2295        // Async emit so the streaming fold's pump can `.await` each
2296        // publish — guarantees per-call chunk ordering on the wire.
2297        let emit: RpcAsyncResponseEmitter = Arc::new(move |caller_origin, call_id, resp| {
2298            let mesh = Arc::clone(&mesh_for_emit);
2299            let service = service_for_emit.clone();
2300            let target_hint = origin_node_cache_for_emit.get(caller_origin);
2301            Box::pin(async move {
2302                let reply_channel_name = format!("{service}.replies.{caller_origin:016x}");
2303                let reply_channel = match ChannelName::new(&reply_channel_name) {
2304                    Ok(c) => c,
2305                    Err(e) => {
2306                        tracing::warn!(error = %e, channel = %reply_channel_name,
2307                                "rpc serve_rpc_streaming: invalid reply channel name");
2308                        return;
2309                    }
2310                };
2311                let meta = EventMeta::new(
2312                    super::cortex::DISPATCH_RPC_RESPONSE,
2313                    0,
2314                    server_origin,
2315                    call_id,
2316                    0,
2317                );
2318                let mut buf = Vec::with_capacity(EVENT_META_SIZE + 64);
2319                buf.extend_from_slice(&meta.to_bytes());
2320                resp.encode_into(&mut buf);
2321                // PERF_AUDIT §3.10: compute hash + stream_id at the
2322                // call site. These legacy streaming paths don't yet
2323                // cache the triple via `OriginKeyedLru<CachedReplyChannel>`;
2324                // wiring them up is a follow-up — for now the
2325                // compute happens here per response, same as the
2326                // pre-fix in-function shape.
2327                let reply_channel_id = ChannelId::new(reply_channel.clone());
2328                let reply_channel_hash = reply_channel_id.hash();
2329                let reply_stream_id = MeshNode::publish_stream_id(&reply_channel_id);
2330                if let Err(e) = publish_response_to_caller(
2331                    &mesh,
2332                    caller_origin,
2333                    target_hint,
2334                    &reply_channel,
2335                    reply_channel_hash,
2336                    reply_stream_id,
2337                    Bytes::from(buf),
2338                )
2339                .await
2340                {
2341                    tracing::warn!(error = %e,
2342                            caller_origin = format!("{:#x}", caller_origin),
2343                            call_id,
2344                            "rpc serve_rpc_streaming: chunk publish failed");
2345                }
2346            })
2347        });
2348
2349        // Attach per-service metrics so the spawned handler tasks
2350        // + pump task bump server-side counters (including the
2351        // streaming-only `streaming_chunks_emitted_total`).
2352        let metrics_handle = self.rpc_metrics_arc().for_service(service);
2353        let fold = Arc::new(Mutex::new(
2354            RpcServerStreamingFold::new(handler as Arc<dyn RpcStreamingHandler>, emit)
2355                .with_metrics(metrics_handle),
2356        ));
2357        let dispatcher: RpcInboundDispatcher = Arc::new(move |ev| {
2358            let _ = tx.try_send(ev);
2359        });
2360        if self
2361            .register_rpc_inbound(channel_hash, dispatcher)
2362            .is_some()
2363        {
2364            return Err(ServeError::AlreadyServing(service.to_string()));
2365        }
2366        let origin_node_cache_for_bridge = Arc::clone(&origin_node_cache);
2367        let bridge = tokio::spawn(async move {
2368            while let Some(inbound) = rx.recv().await {
2369                if inbound.from_node != 0 {
2370                    origin_node_cache_for_bridge.insert(inbound.origin_hash, inbound.from_node);
2371                }
2372                let payload = inbound.payload;
2373                let entry = RedexEntry::new_heap(0, 0, payload.len() as u32, 0, 0);
2374                let ev = RedexEvent { entry, payload };
2375                if let Err(e) = fold.lock().apply(&ev, &mut ()) {
2376                    tracing::warn!(error = %e, "rpc serve_rpc_streaming: fold apply error");
2377                }
2378            }
2379        });
2380        self.rpc_local_services_arc().insert(service.to_string());
2381        Ok(ServeHandle {
2382            channel_hash,
2383            service: service.to_string(),
2384            _bridge: bridge,
2385            // Streaming/duplex variants still spawn per emit (§8a covers the
2386            // unary hot path); no drainer.
2387            _response_drain: None,
2388            mesh: Arc::clone(self),
2389        })
2390    }
2391
2392    /// Register a client-streaming nRPC handler for `service`.
2393    /// Mirror of [`Self::serve_rpc_streaming`] but using the
2394    /// request-side fold ([`RpcStreamingRequestFold`]) — the
2395    /// handler receives one stream of REQUEST_CHUNK bodies and
2396    /// emits one terminal RESPONSE.
2397    ///
2398    /// Wires two emit callbacks:
2399    /// - A sync [`RpcResponseEmitter`] for the terminal RESPONSE
2400    ///   (single emit per call, no ordering concern).
2401    /// - An [`RpcRequestGrantEmitter`] for upload-direction
2402    ///   credit grants, which publishes [`DISPATCH_RPC_REQUEST_GRANT`]
2403    ///   events on the caller's reply channel.
2404    ///
2405    /// Bidi streaming plan (Phase C).
2406    pub fn serve_rpc_client_stream<H: RpcClientStreamingHandler>(
2407        self: &Arc<Self>,
2408        service: &str,
2409        handler: Arc<H>,
2410    ) -> Result<ServeHandle, ServeError> {
2411        let request_channel = ChannelName::new(&format!("{service}.requests"))
2412            .map_err(|e| ServeError::InvalidServiceName(e.to_string()))?;
2413        let channel_hash = request_channel.hash();
2414        let (tx, mut rx) = tokio::sync::mpsc::channel::<RpcInboundEvent>(1024);
2415
2416        // T1.2 cache — see serve_rpc above for full rationale.
2417        let origin_node_cache: RpcOriginNodeCache = Arc::new(OriginKeyedLru::new());
2418
2419        let mesh_for_emit = Arc::clone(self);
2420        let service_for_emit = service.to_string();
2421        let server_origin = self.identity_origin_hash();
2422
2423        // Terminal RESPONSE emitter — sync because there's only
2424        // one RESPONSE per call (no per-call ordering concern that
2425        // would require an async-await between chunks).
2426        let emit_resp_mesh = Arc::clone(&mesh_for_emit);
2427        let emit_resp_service = service_for_emit.clone();
2428        let origin_node_cache_for_emit = Arc::clone(&origin_node_cache);
2429        let emit_resp: RpcResponseEmitter = Arc::new(move |caller_origin, call_id, resp| {
2430            let mesh = Arc::clone(&emit_resp_mesh);
2431            let service = emit_resp_service.clone();
2432            let target_hint = origin_node_cache_for_emit.get(caller_origin);
2433            tokio::spawn(async move {
2434                let reply_channel_name = format!("{service}.replies.{caller_origin:016x}");
2435                let reply_channel = match ChannelName::new(&reply_channel_name) {
2436                    Ok(c) => c,
2437                    Err(e) => {
2438                        tracing::warn!(error = %e, channel = %reply_channel_name,
2439                                "rpc serve_rpc_client_stream: invalid reply channel name");
2440                        return;
2441                    }
2442                };
2443                let meta = EventMeta::new(
2444                    super::cortex::DISPATCH_RPC_RESPONSE,
2445                    0,
2446                    server_origin,
2447                    call_id,
2448                    0,
2449                );
2450                let mut buf = Vec::with_capacity(EVENT_META_SIZE + 64);
2451                buf.extend_from_slice(&meta.to_bytes());
2452                resp.encode_into(&mut buf);
2453                // PERF_AUDIT §3.10: compute hash + stream_id at the
2454                // call site. These legacy streaming paths don't yet
2455                // cache the triple via `OriginKeyedLru<CachedReplyChannel>`;
2456                // wiring them up is a follow-up — for now the
2457                // compute happens here per response, same as the
2458                // pre-fix in-function shape.
2459                let reply_channel_id = ChannelId::new(reply_channel.clone());
2460                let reply_channel_hash = reply_channel_id.hash();
2461                let reply_stream_id = MeshNode::publish_stream_id(&reply_channel_id);
2462                if let Err(e) = publish_response_to_caller(
2463                    &mesh,
2464                    caller_origin,
2465                    target_hint,
2466                    &reply_channel,
2467                    reply_channel_hash,
2468                    reply_stream_id,
2469                    Bytes::from(buf),
2470                )
2471                .await
2472                {
2473                    tracing::warn!(error = %e,
2474                            caller_origin = format!("{:#x}", caller_origin),
2475                            call_id,
2476                            "rpc serve_rpc_client_stream: terminal RESPONSE publish failed");
2477                }
2478            });
2479        });
2480
2481        // REQUEST_GRANT emitter — coalesces per-chunk credits into
2482        // a single drainer task that batches by call_id. Avoids the
2483        // tokio::spawn-per-emit storm under bursting.
2484        let emit_grant = build_request_grant_emitter(
2485            Arc::clone(&mesh_for_emit),
2486            service_for_emit.clone(),
2487            server_origin,
2488            "serve_rpc_client_stream",
2489        );
2490
2491        let metrics_handle = self.rpc_metrics_arc().for_service(service);
2492        let fold = Arc::new(Mutex::new(
2493            RpcStreamingRequestFold::new(handler as Arc<dyn RpcClientStreamingHandler>, emit_resp)
2494                .with_grant_emitter(emit_grant)
2495                .with_metrics(metrics_handle),
2496        ));
2497        let dispatcher: RpcInboundDispatcher = Arc::new(move |ev| {
2498            let _ = tx.try_send(ev);
2499        });
2500        if self
2501            .register_rpc_inbound(channel_hash, dispatcher)
2502            .is_some()
2503        {
2504            return Err(ServeError::AlreadyServing(service.to_string()));
2505        }
2506        let origin_node_cache_for_bridge = Arc::clone(&origin_node_cache);
2507        let bridge = tokio::spawn(async move {
2508            while let Some(inbound) = rx.recv().await {
2509                if inbound.from_node != 0 {
2510                    origin_node_cache_for_bridge.insert(inbound.origin_hash, inbound.from_node);
2511                }
2512                let payload = inbound.payload;
2513                let entry = RedexEntry::new_heap(0, 0, payload.len() as u32, 0, 0);
2514                let ev = RedexEvent { entry, payload };
2515                if let Err(e) = fold.lock().apply(&ev, &mut ()) {
2516                    tracing::warn!(error = %e,
2517                        "rpc serve_rpc_client_stream: fold apply error");
2518                }
2519            }
2520        });
2521        self.rpc_local_services_arc().insert(service.to_string());
2522        Ok(ServeHandle {
2523            channel_hash,
2524            service: service.to_string(),
2525            _bridge: bridge,
2526            // Streaming/duplex variants still spawn per emit (§8a covers the
2527            // unary hot path); no drainer.
2528            _response_drain: None,
2529            mesh: Arc::clone(self),
2530        })
2531    }
2532
2533    /// Client-streaming variant of [`Self::call`]. Returns a
2534    /// [`ClientStreamCallRaw`] handle the caller pushes N items
2535    /// into via `send`, then `finish` to await the terminal
2536    /// RESPONSE.
2537    ///
2538    /// **Lazy initial REQUEST.** This method does NOT publish a
2539    /// REQUEST to the wire. It only ensures the caller's reply
2540    /// subscription is set up and registers the pending entry; the
2541    /// initial REQUEST is emitted by the first `send` (or by
2542    /// `finish` for the zero-item degenerate path).
2543    ///
2544    /// Sets `FLAG_RPC_CLIENT_STREAMING_REQUEST` on the initial
2545    /// REQUEST so the server's request-streaming fold knows to
2546    /// open a request-side stream. Optional `request_window_initial`
2547    /// header opts into upload-direction flow control.
2548    ///
2549    /// Bidi streaming plan (Phase C).
2550    pub async fn call_client_stream(
2551        self: &Arc<Self>,
2552        target_node_id: u64,
2553        service: &str,
2554        opts: CallOptions,
2555    ) -> Result<ClientStreamCallRaw, RpcError> {
2556        // `request_window_initial = Some(0)` would deadlock the
2557        // caller: every `send` awaits a credit, but the initial
2558        // REQUEST is lazy (not emitted until the first send), so
2559        // the server never sees the call and never publishes a
2560        // GRANT. Reject up front — `None` means "unbounded credit",
2561        // any positive value opts into flow control.
2562        if matches!(opts.request_window_initial, Some(0)) {
2563            return Err(RpcError::Codec {
2564                direction: CodecDirection::Encode,
2565                message: "request_window_initial must be None or >= 1; Some(0) deadlocks send"
2566                    .to_string(),
2567            });
2568        }
2569        // T1.3: per-service route cache (see PERF_AUDIT
2570        // 2026-05-19). One DashMap::get + Arc::clone instead of
2571        // 2 format! + 2 ChannelName::new + xxhash per call.
2572        let route = self.rpc_route_or_no_route(target_node_id, service)?;
2573        let self_origin = self.identity_origin_hash();
2574        self.ensure_reply_subscription(
2575            target_node_id,
2576            service,
2577            route.reply_channel.clone(),
2578            route.reply_hash,
2579        )
2580        .await?;
2581
2582        let call_id = mint_random_call_id();
2583        let pending = self.rpc_client_pending();
2584        let (terminal_rx, mut grant_rx) =
2585            pending.register_client_streaming(call_id, target_node_id);
2586
2587        // Build the header set + flags we'll queue for the initial
2588        // REQUEST (deferred to the first send / finish).
2589        let mut initial_flags = FLAG_RPC_CLIENT_STREAMING_REQUEST;
2590        let mut initial_headers: Vec<(String, Vec<u8>)> = Vec::new();
2591        if let Some(tc) = opts.trace_context.as_ref() {
2592            initial_flags |= FLAG_RPC_PROPAGATE_TRACE;
2593            initial_headers.extend(build_trace_headers(tc));
2594        }
2595        if let Some(window) = opts.request_window_initial {
2596            initial_headers.push((
2597                HEADER_NRPC_REQUEST_WINDOW_INITIAL.to_string(),
2598                window.to_string().into_bytes(),
2599            ));
2600        }
2601        initial_headers.extend(opts.request_headers.iter().cloned());
2602
2603        // Per-call credit semaphore when flow control is opted in.
2604        // Initial permits = the caller's declared window. Refilled
2605        // by REQUEST_GRANT events arriving on the reply channel,
2606        // pumped through `grant_rx` by the spawned `grant_pump`.
2607        let credit_sem = opts
2608            .request_window_initial
2609            .map(|n| Arc::new(tokio::sync::Semaphore::new(n as usize)));
2610        let grant_pump = credit_sem.as_ref().map(|sem| {
2611            let sem = Arc::clone(sem);
2612            tokio::spawn(async move {
2613                while let Some(credits) = grant_rx.recv().await {
2614                    add_request_grant_credits(&sem, credits);
2615                }
2616            })
2617        });
2618
2619        let deadline_ns = opts.deadline.map(instant_to_unix_nanos).unwrap_or(0);
2620        let observer = StreamingObserverState::new(Arc::clone(self), target_node_id, service, 0);
2621        let cancel_keep_alive = arm_stream_cancel(self, &opts, &pending, call_id);
2622        Ok(ClientStreamCallRaw {
2623            mesh: Arc::clone(self),
2624            target_node_id,
2625            request_channel: route.request_channel.clone(),
2626            request_channel_hash: route.request_channel_hash,
2627            request_stream_id: route.request_stream_id,
2628            self_origin,
2629            call_id,
2630            service: service.to_string(),
2631            initial_headers,
2632            initial_flags,
2633            deadline_ns,
2634            credit_sem,
2635            grant_pump,
2636            terminal_rx: Some(terminal_rx),
2637            state: ClientStreamState::JustOpened,
2638            started: Instant::now(),
2639            observer,
2640            _cancel_keep_alive: cancel_keep_alive,
2641        })
2642    }
2643
2644    /// Register a duplex nRPC handler for `service`. Composes
2645    /// [`Self::serve_rpc_client_stream`] (request-side stream)
2646    /// with [`Self::serve_rpc_streaming`] (response-side multi-
2647    /// fire emit) via [`RpcDuplexFold`].
2648    ///
2649    /// Wires THREE emit callbacks:
2650    /// - Async [`RpcAsyncResponseEmitter`] for response chunks +
2651    ///   the terminal frame (per-call ordering required because
2652    ///   the response side is multi-fire).
2653    /// - [`RpcRequestGrantEmitter`] for upload-direction credit
2654    ///   grants (one per consumed request chunk when flow
2655    ///   control is opted into).
2656    ///
2657    /// Bidi streaming plan (Phase D).
2658    pub fn serve_rpc_duplex<H: RpcDuplexHandler>(
2659        self: &Arc<Self>,
2660        service: &str,
2661        handler: Arc<H>,
2662    ) -> Result<ServeHandle, ServeError> {
2663        let request_channel = ChannelName::new(&format!("{service}.requests"))
2664            .map_err(|e| ServeError::InvalidServiceName(e.to_string()))?;
2665        let channel_hash = request_channel.hash();
2666        let (tx, mut rx) = tokio::sync::mpsc::channel::<RpcInboundEvent>(1024);
2667
2668        // T1.2 cache — see serve_rpc above for full rationale.
2669        let origin_node_cache: RpcOriginNodeCache = Arc::new(OriginKeyedLru::new());
2670
2671        let mesh_for_emit = Arc::clone(self);
2672        let service_for_emit = service.to_string();
2673        let server_origin = self.identity_origin_hash();
2674
2675        // Async response emitter — per-call ordering matters here
2676        // because the response side is multi-fire (same rationale
2677        // as serve_rpc_streaming).
2678        let emit_resp_mesh = Arc::clone(&mesh_for_emit);
2679        let emit_resp_service = service_for_emit.clone();
2680        let origin_node_cache_for_emit = Arc::clone(&origin_node_cache);
2681        let emit_resp: RpcAsyncResponseEmitter = Arc::new(move |caller_origin, call_id, resp| {
2682            let mesh = Arc::clone(&emit_resp_mesh);
2683            let service = emit_resp_service.clone();
2684            let target_hint = origin_node_cache_for_emit.get(caller_origin);
2685            Box::pin(async move {
2686                let reply_channel_name = format!("{service}.replies.{caller_origin:016x}");
2687                let reply_channel = match ChannelName::new(&reply_channel_name) {
2688                    Ok(c) => c,
2689                    Err(e) => {
2690                        tracing::warn!(error = %e, channel = %reply_channel_name,
2691                                "rpc serve_rpc_duplex: invalid reply channel name");
2692                        return;
2693                    }
2694                };
2695                let meta = EventMeta::new(
2696                    super::cortex::DISPATCH_RPC_RESPONSE,
2697                    0,
2698                    server_origin,
2699                    call_id,
2700                    0,
2701                );
2702                let mut buf = Vec::with_capacity(EVENT_META_SIZE + 64);
2703                buf.extend_from_slice(&meta.to_bytes());
2704                resp.encode_into(&mut buf);
2705                // PERF_AUDIT §3.10: compute hash + stream_id at the
2706                // call site. These legacy streaming paths don't yet
2707                // cache the triple via `OriginKeyedLru<CachedReplyChannel>`;
2708                // wiring them up is a follow-up — for now the
2709                // compute happens here per response, same as the
2710                // pre-fix in-function shape.
2711                let reply_channel_id = ChannelId::new(reply_channel.clone());
2712                let reply_channel_hash = reply_channel_id.hash();
2713                let reply_stream_id = MeshNode::publish_stream_id(&reply_channel_id);
2714                if let Err(e) = publish_response_to_caller(
2715                    &mesh,
2716                    caller_origin,
2717                    target_hint,
2718                    &reply_channel,
2719                    reply_channel_hash,
2720                    reply_stream_id,
2721                    Bytes::from(buf),
2722                )
2723                .await
2724                {
2725                    tracing::warn!(error = %e,
2726                            caller_origin = format!("{:#x}", caller_origin),
2727                            call_id,
2728                            "rpc serve_rpc_duplex: chunk publish failed");
2729                }
2730            })
2731        });
2732
2733        // Request-direction grant emitter — same coalescing
2734        // drainer shape as serve_rpc_client_stream.
2735        let emit_grant = build_request_grant_emitter(
2736            Arc::clone(&mesh_for_emit),
2737            service_for_emit.clone(),
2738            server_origin,
2739            "serve_rpc_duplex",
2740        );
2741
2742        let metrics_handle = self.rpc_metrics_arc().for_service(service);
2743        let fold = Arc::new(Mutex::new(
2744            RpcDuplexFold::new(handler as Arc<dyn RpcDuplexHandler>, emit_resp)
2745                .with_grant_emitter(emit_grant)
2746                .with_metrics(metrics_handle),
2747        ));
2748        let dispatcher: RpcInboundDispatcher = Arc::new(move |ev| {
2749            let _ = tx.try_send(ev);
2750        });
2751        if self
2752            .register_rpc_inbound(channel_hash, dispatcher)
2753            .is_some()
2754        {
2755            return Err(ServeError::AlreadyServing(service.to_string()));
2756        }
2757        let origin_node_cache_for_bridge = Arc::clone(&origin_node_cache);
2758        let bridge = tokio::spawn(async move {
2759            while let Some(inbound) = rx.recv().await {
2760                if inbound.from_node != 0 {
2761                    origin_node_cache_for_bridge.insert(inbound.origin_hash, inbound.from_node);
2762                }
2763                let payload = inbound.payload;
2764                let entry = RedexEntry::new_heap(0, 0, payload.len() as u32, 0, 0);
2765                let ev = RedexEvent { entry, payload };
2766                if let Err(e) = fold.lock().apply(&ev, &mut ()) {
2767                    tracing::warn!(error = %e,
2768                        "rpc serve_rpc_duplex: fold apply error");
2769                }
2770            }
2771        });
2772        self.rpc_local_services_arc().insert(service.to_string());
2773        Ok(ServeHandle {
2774            channel_hash,
2775            service: service.to_string(),
2776            _bridge: bridge,
2777            // Streaming/duplex variants still spawn per emit (§8a covers the
2778            // unary hot path); no drainer.
2779            _response_drain: None,
2780            mesh: Arc::clone(self),
2781        })
2782    }
2783
2784    /// Duplex variant of [`Self::call`]. Returns a
2785    /// [`DuplexCallRaw`] handle with both upload (`send`,
2786    /// `finish_sending`) and download (`next`, or impl
2787    /// `futures::Stream`) surfaces. Use `into_split` to peel off
2788    /// the two halves for the "encoder task + decoder task"
2789    /// shape.
2790    ///
2791    /// Initial REQUEST sets BOTH `FLAG_RPC_CLIENT_STREAMING_REQUEST`
2792    /// AND `FLAG_RPC_STREAMING_RESPONSE`. Lazy publish — the
2793    /// initial REQUEST flies on the first `send` (or on
2794    /// `finish_sending` for the zero-item degenerate path).
2795    ///
2796    /// Bidi streaming plan (Phase D).
2797    pub async fn call_duplex(
2798        self: &Arc<Self>,
2799        target_node_id: u64,
2800        service: &str,
2801        opts: CallOptions,
2802    ) -> Result<DuplexCallRaw, RpcError> {
2803        // Same deadlock guard as `call_client_stream`: Some(0)
2804        // means "send must await a credit that can never arrive"
2805        // because the initial REQUEST is lazy.
2806        if matches!(opts.request_window_initial, Some(0)) {
2807            return Err(RpcError::Codec {
2808                direction: CodecDirection::Encode,
2809                message: "request_window_initial must be None or >= 1; Some(0) deadlocks send"
2810                    .to_string(),
2811            });
2812        }
2813        // T1.3: per-service route cache (see PERF_AUDIT
2814        // 2026-05-19). One DashMap::get + Arc::clone instead of
2815        // 2 format! + 2 ChannelName::new + xxhash per call.
2816        let route = self.rpc_route_or_no_route(target_node_id, service)?;
2817        let self_origin = self.identity_origin_hash();
2818        self.ensure_reply_subscription(
2819            target_node_id,
2820            service,
2821            route.reply_channel.clone(),
2822            route.reply_hash,
2823        )
2824        .await?;
2825
2826        let call_id = mint_random_call_id();
2827        let pending = self.rpc_client_pending();
2828        let (chunks_rx, mut grant_rx) = pending.register_duplex(call_id, target_node_id);
2829
2830        let mut initial_flags = FLAG_RPC_CLIENT_STREAMING_REQUEST | FLAG_RPC_STREAMING_RESPONSE;
2831        let mut initial_headers: Vec<(String, Vec<u8>)> = Vec::new();
2832        if let Some(tc) = opts.trace_context.as_ref() {
2833            initial_flags |= FLAG_RPC_PROPAGATE_TRACE;
2834            initial_headers.extend(build_trace_headers(tc));
2835        }
2836        if let Some(window) = opts.request_window_initial {
2837            initial_headers.push((
2838                HEADER_NRPC_REQUEST_WINDOW_INITIAL.to_string(),
2839                window.to_string().into_bytes(),
2840            ));
2841        }
2842        if let Some(window) = opts.stream_window_initial {
2843            initial_headers.push((
2844                HEADER_NRPC_STREAM_WINDOW_INITIAL.to_string(),
2845                window.to_string().into_bytes(),
2846            ));
2847        }
2848        initial_headers.extend(opts.request_headers.iter().cloned());
2849
2850        let credit_sem = opts
2851            .request_window_initial
2852            .map(|n| Arc::new(tokio::sync::Semaphore::new(n as usize)));
2853        let grant_pump = credit_sem.as_ref().map(|sem| {
2854            let sem = Arc::clone(sem);
2855            tokio::spawn(async move {
2856                while let Some(credits) = grant_rx.recv().await {
2857                    add_request_grant_credits(&sem, credits);
2858                }
2859            })
2860        });
2861
2862        let deadline_ns = opts.deadline.map(instant_to_unix_nanos).unwrap_or(0);
2863        let observer = StreamingObserverState::new(Arc::clone(self), target_node_id, service, 0);
2864        // Cancel keep-alive lives on the shared Arc<DuplexInner>
2865        // so it survives into_split — the watcher exits only when
2866        // BOTH the sink AND stream halves drop, matching the
2867        // existing CANCEL-on-drop semantics.
2868        let cancel_keep_alive = arm_stream_cancel(self, &opts, &pending, call_id);
2869        let inner = Arc::new(DuplexInner {
2870            mesh: Arc::clone(self),
2871            target_node_id,
2872            request_channel: route.request_channel.clone(),
2873            request_channel_hash: route.request_channel_hash,
2874            request_stream_id: route.request_stream_id,
2875            self_origin,
2876            call_id,
2877            initial_sent: std::sync::atomic::AtomicBool::new(false),
2878            clean_close: std::sync::atomic::AtomicBool::new(false),
2879            observer,
2880            _cancel_keep_alive: Some(cancel_keep_alive),
2881        });
2882        let sink = DuplexSink {
2883            inner: Arc::clone(&inner),
2884            service: service.to_string(),
2885            initial_headers,
2886            initial_flags,
2887            deadline_ns,
2888            credit_sem,
2889            grant_pump,
2890            state: ClientStreamState::JustOpened,
2891        };
2892        let stream = DuplexStream {
2893            inner,
2894            chunks_rx,
2895            done: false,
2896        };
2897        Ok(DuplexCallRaw { sink, stream })
2898    }
2899
2900    /// Streaming variant of [`Self::call`]. Returns an
2901    /// [`RpcStream`] that yields chunks (as `Result<Bytes, RpcError>`)
2902    /// until the server closes the stream.
2903    ///
2904    /// Sets `FLAG_RPC_STREAMING_RESPONSE` on the request so the
2905    /// server's streaming fold knows to expect multi-fire emits.
2906    /// Same lazy reply-subscription + direct-unicast REQUEST
2907    /// as the unary `call` path.
2908    ///
2909    /// Cancellation: dropping the returned `RpcStream` emits a
2910    /// CANCEL to the server (best-effort) and discards any
2911    /// in-flight chunks.
2912    pub async fn call_streaming(
2913        self: &Arc<Self>,
2914        target_node_id: u64,
2915        service: &str,
2916        payload: Bytes,
2917        opts: CallOptions,
2918    ) -> Result<RpcStream, RpcError> {
2919        // `stream_window_initial = Some(0)` would deadlock the
2920        // RESPONSE direction by default: server's pump awaits one
2921        // credit per chunk, the caller's auto-grant only fires on
2922        // consumed chunks, and the first chunk can never be
2923        // delivered. `None` means "unbounded credit"; any positive
2924        // value opts into flow control. Reject up front — symmetric
2925        // with the request-direction guard in `call_client_stream`.
2926        if matches!(opts.stream_window_initial, Some(0)) {
2927            return Err(RpcError::Codec {
2928                direction: CodecDirection::Encode,
2929                message: "stream_window_initial must be None or >= 1; Some(0) deadlocks the response pump"
2930                    .to_string(),
2931            });
2932        }
2933        // T1.3: per-service route cache. One DashMap::get + Arc::clone
2934        // on the hot path instead of 2 format! + 2 ChannelName::new +
2935        // xxhash per call.
2936        let route = self.rpc_route_or_no_route(target_node_id, service)?;
2937        let self_origin = self.identity_origin_hash();
2938        self.ensure_reply_subscription(
2939            target_node_id,
2940            service,
2941            route.reply_channel.clone(),
2942            route.reply_hash,
2943        )
2944        .await?;
2945
2946        let call_id = mint_random_call_id();
2947        let pending = self.rpc_client_pending();
2948        // S-4 part 2: bind the pending entry to the wire-session
2949        // peer the request is dispatched to. The fold's deliver
2950        // gate rejects RESPONSE frames whose from_node doesn't
2951        // match, so a leaked call_id alone can't spoof a reply.
2952        let rx = pending.register_streaming(call_id, target_node_id);
2953
2954        // Build the REQUEST: STREAMING_RESPONSE flag plus optional
2955        // trace-context headers / propagate-trace flag, same as
2956        // unary `call`. Plus the optional flow-control header
2957        // (`nrpc-stream-window-initial`) when the caller opted in
2958        // via `CallOptions::stream_window_initial`.
2959        let mut flags = FLAG_RPC_STREAMING_RESPONSE;
2960        let mut headers = Vec::new();
2961        if let Some(tc) = opts.trace_context.as_ref() {
2962            flags |= FLAG_RPC_PROPAGATE_TRACE;
2963            headers.extend(build_trace_headers(tc));
2964        }
2965        if let Some(window) = opts.stream_window_initial {
2966            headers.push((
2967                HEADER_NRPC_STREAM_WINDOW_INITIAL.to_string(),
2968                window.to_string().into_bytes(),
2969            ));
2970        }
2971        // Append caller-supplied request headers (Phase 9b — same
2972        // semantics as the unary `call` path).
2973        headers.extend(opts.request_headers.iter().cloned());
2974        let req = RpcRequestPayload {
2975            service: service.to_string(),
2976            deadline_ns: opts.deadline.map(instant_to_unix_nanos).unwrap_or(0),
2977            flags,
2978            headers,
2979            body: payload.clone(),
2980        };
2981        let meta = EventMeta::new(DISPATCH_RPC_REQUEST, 0, self_origin, call_id, 0);
2982        let mut buf = Vec::with_capacity(EVENT_META_SIZE + req.body.len() + 32);
2983        buf.extend_from_slice(&meta.to_bytes());
2984        req.encode_into(&mut buf);
2985
2986        let payload_bytes = Bytes::from(buf);
2987        if let Err(e) = self
2988            .publish_to_peer(
2989                target_node_id,
2990                route.request_channel_hash,
2991                route.request_stream_id,
2992                /* reliable */ true,
2993                std::slice::from_ref(&payload_bytes),
2994            )
2995            .await
2996        {
2997            pending.cancel(call_id);
2998            return Err(RpcError::Transport(e));
2999        }
3000
3001        let request_bytes_len = payload_bytes.len() as u32;
3002        // Cancel keep-alive lives on the returned RpcStream so the
3003        // watcher exits cleanly when the stream drops without cancel.
3004        let cancel_keep_alive = arm_stream_cancel(self, &opts, &pending, call_id);
3005        Ok(RpcStream {
3006            mesh: Arc::clone(self),
3007            target_node_id,
3008            request_channel: route.request_channel.clone(),
3009            // PERF_AUDIT §3.10 — cache the channel hash + stream
3010            // id from `route` so per-chunk grants in `poll_next`
3011            // don't re-run `ChannelId::new` + xxh3.
3012            request_channel_hash: route.request_channel_hash,
3013            request_stream_id: route.request_stream_id,
3014            self_origin,
3015            call_id,
3016            inner: rx,
3017            done: false,
3018            stream_window: opts.stream_window_initial,
3019            grant_pending: 0,
3020            _cancel_keep_alive: cancel_keep_alive,
3021            observer: StreamingObserverState::new(
3022                Arc::clone(self),
3023                target_node_id,
3024                service,
3025                request_bytes_len,
3026            ),
3027        })
3028    }
3029
3030    /// Find every node currently advertising `service` via the
3031    /// `nrpc:<service>` capability tag. Returns node IDs in
3032    /// roster order; the caller picks one (or use [`Self::call_service`]
3033    /// for the round-robin shortcut).
3034    ///
3035    /// Pre-Phase 2: requires the target nodes to have called
3036    /// `serve_rpc` AND `announce_capabilities` so the
3037    /// `nrpc:<service>` tag has propagated through capability
3038    /// announcements. The local node's own services are NOT
3039    /// automatically included (callers don't typically invoke
3040    /// themselves via the network — for in-process invocation,
3041    /// the user has the handler directly).
3042    pub fn find_service_nodes(&self, service: &str) -> Vec<u64> {
3043        use crate::adapter::net::behavior::capability::CapabilityFilter;
3044        use crate::adapter::net::behavior::fold::capability_bridge;
3045        let tag = format!("nrpc:{service}");
3046        let filter = CapabilityFilter::default().require_tag(tag);
3047        capability_bridge::find_nodes_matching(self.capability_fold(), &filter)
3048    }
3049
3050    /// Issue an RPC call to `service`, picking one node from
3051    /// those advertising the `nrpc:<service>` tag in the local
3052    /// capability index according to `opts.routing_policy`.
3053    ///
3054    /// Returns `RpcError::NoRoute` if no nodes advertise the
3055    /// service (or if `opts.filter_unhealthy` is set and every
3056    /// candidate is unavailable per the local `ProximityGraph`).
3057    pub async fn call_service(
3058        self: &Arc<Self>,
3059        service: &str,
3060        payload: Bytes,
3061        opts: CallOptions,
3062    ) -> Result<RpcReply, RpcError> {
3063        let mut candidates = self.find_service_nodes(service);
3064        if candidates.is_empty() {
3065            return Err(RpcError::NoRoute {
3066                target: 0,
3067                reason: format!(
3068                    "no nodes advertise `nrpc:{service}` (have any servers \
3069                     for this service called serve_rpc + announce_capabilities?)"
3070                ),
3071            });
3072        }
3073
3074        // Health filtering. Skip candidates the proximity graph
3075        // marks unhealthy (`!is_available()`). Candidates with no
3076        // proximity entry at all are KEPT — absence of evidence
3077        // is not evidence of unhealth, and a freshly-announced
3078        // service shouldn't be filtered just because pingwaves
3079        // haven't propagated yet.
3080        //
3081        // The bridge: each candidate's session-layer `node_id: u64`
3082        // is mapped to the entity-layer `[u8; 32]` via
3083        // `MeshNode::entity_id_for_node`. The proximity graph is
3084        // keyed on the entity id.
3085        if opts.filter_unhealthy {
3086            let proximity = self.proximity_graph();
3087            candidates.retain(|node_id| match self.entity_id_for_node(*node_id) {
3088                Some(entity_id) => match proximity.get_node(&entity_id) {
3089                    Some(node) => node.is_available(),
3090                    None => true, // no proximity data → keep
3091                },
3092                None => true, // no entity-id mapping → keep
3093            });
3094            if candidates.is_empty() {
3095                return Err(RpcError::NoRoute {
3096                    target: 0,
3097                    reason: format!(
3098                        "every node advertising `nrpc:{service}` is marked \
3099                         unhealthy by the local proximity graph",
3100                    ),
3101                });
3102            }
3103        }
3104
3105        // Sort once so consistent-hash policies (Sticky) produce
3106        // a stable ordering across calls regardless of how the
3107        // capability index returned the candidates, and so the
3108        // LowestLatency-with-no-proximity-data fallback is
3109        // deterministic. Cheap — the candidate set is typically
3110        // small.
3111        candidates.sort_unstable();
3112
3113        // v0.4 capability-auth caller-side gate. Filter the
3114        // candidate set BEFORE target selection so the routing
3115        // policy never picks a peer the caller can't actually
3116        // reach. Pre-fix `select_target` could pick a denied
3117        // candidate even when authorized peers existed in the
3118        // set, and the resulting `CapabilityDenied` masked the
3119        // fact that the call would have succeeded against B or
3120        // C. Each candidate's own announcement lists
3121        // `nrpc:<service>` (otherwise it wouldn't be a
3122        // `find_service_nodes` candidate), so the gate's
3123        // `has_tag` arm short-circuits in the common case; the
3124        // new work is the allow-list scan. Permissive
3125        // announcements (all three lists empty) admit any
3126        // caller — the byte-identity wire-compat tests pin that
3127        // an unmodified peer's announcement stays unrestricted.
3128        // See `docs/plans/CAPABILITY_AUTH_PLAN.md` §3.
3129        let tag = format!("nrpc:{service}");
3130        use crate::adapter::net::behavior::fold::capability_bridge;
3131        let self_id = self.node_id();
3132        let any_candidate = candidates[0];
3133        let fold = self.capability_fold();
3134        // PERF_AUDIT §4.2 — batch the per-candidate gate so the
3135        // fold read lock is taken once and the caller's subnet +
3136        // groups are parsed once, not N times.
3137        let verdicts = capability_bridge::may_execute_batch(fold, &candidates, &tag, self_id);
3138        let mut iter = verdicts.into_iter();
3139        candidates.retain(|_| iter.next().unwrap_or(false));
3140        if candidates.is_empty() {
3141            return Err(RpcError::CapabilityDenied {
3142                // No authorized target; surface one of the
3143                // originally-advertised candidates so the caller
3144                // can correlate the denial with a real peer. The
3145                // semantic is "no peer advertising `nrpc:<service>`
3146                // authorizes this caller" — `any_candidate` is a
3147                // representative, not necessarily the strictest.
3148                target: any_candidate,
3149                capability: service.to_string(),
3150            });
3151        }
3152
3153        let target = self.select_target(&candidates, &opts.routing_policy);
3154        self.call(target, service, payload, opts).await
3155    }
3156
3157    /// Capability-routed server-streaming call. Same routing as
3158    /// [`call_service`] — capability-fold lookup, health filter,
3159    /// routing-policy sort, capability-auth gate, target selection —
3160    /// but the terminal step is [`call_streaming`] instead of
3161    /// [`call`]. Returns the substrate's `RpcStream` so callers can
3162    /// drive an `async for chunk in stream:` loop.
3163    ///
3164    /// Use cases: an agent invoking a long-running tool that emits
3165    /// progress + a terminal result, a fan-out subscriber that wants
3166    /// streaming chunks from whatever node currently advertises the
3167    /// service, any consumer that today reaches for
3168    /// `find_service_nodes` → manual target selection → `call_streaming`
3169    /// and ends up re-implementing the cap-auth gate `call_service`
3170    /// already enforces.
3171    ///
3172    /// Honors `CallOptions::cancel_token` (v3) and
3173    /// `CallOptions::deadline` exactly like `call_streaming`.
3174    ///
3175    /// [`call_service`]: Self::call_service
3176    /// [`call_streaming`]: Self::call_streaming
3177    /// [`call`]: Self::call
3178    pub async fn call_service_streaming(
3179        self: &Arc<Self>,
3180        service: &str,
3181        payload: Bytes,
3182        opts: CallOptions,
3183    ) -> Result<RpcStream, RpcError> {
3184        let mut candidates = self.find_service_nodes(service);
3185        if candidates.is_empty() {
3186            return Err(RpcError::NoRoute {
3187                target: 0,
3188                reason: format!(
3189                    "no nodes advertise `nrpc:{service}` (have any servers \
3190                     for this service called serve_rpc + announce_capabilities?)"
3191                ),
3192            });
3193        }
3194
3195        // Health filter — mirrors `call_service`. Candidates with no
3196        // proximity entry are kept (absence of evidence ≠ evidence of
3197        // unhealth); only candidates the proximity graph marks
3198        // explicitly unavailable get dropped.
3199        if opts.filter_unhealthy {
3200            let proximity = self.proximity_graph();
3201            candidates.retain(|node_id| match self.entity_id_for_node(*node_id) {
3202                Some(entity_id) => match proximity.get_node(&entity_id) {
3203                    Some(node) => node.is_available(),
3204                    None => true,
3205                },
3206                None => true,
3207            });
3208            if candidates.is_empty() {
3209                return Err(RpcError::NoRoute {
3210                    target: 0,
3211                    reason: format!(
3212                        "every node advertising `nrpc:{service}` is marked \
3213                         unhealthy by the local proximity graph",
3214                    ),
3215                });
3216            }
3217        }
3218
3219        // Deterministic ordering so Sticky / LowestLatency-fallback
3220        // pick stably across calls — mirrors `call_service`.
3221        candidates.sort_unstable();
3222
3223        // v0.4 capability-auth caller-side gate. Same as `call_service`:
3224        // filter the candidate set BEFORE target selection so the
3225        // routing policy never picks a peer the caller can't reach.
3226        let tag = format!("nrpc:{service}");
3227        use crate::adapter::net::behavior::fold::capability_bridge;
3228        let self_id = self.node_id();
3229        let any_candidate = candidates[0];
3230        let fold = self.capability_fold();
3231        // PERF_AUDIT §4.2 — batch the per-candidate gate. See the
3232        // mirror site at `:3093`.
3233        let verdicts = capability_bridge::may_execute_batch(fold, &candidates, &tag, self_id);
3234        let mut iter = verdicts.into_iter();
3235        candidates.retain(|_| iter.next().unwrap_or(false));
3236        if candidates.is_empty() {
3237            return Err(RpcError::CapabilityDenied {
3238                target: any_candidate,
3239                capability: service.to_string(),
3240            });
3241        }
3242
3243        let target = self.select_target(&candidates, &opts.routing_policy);
3244        self.call_streaming(target, service, payload, opts).await
3245    }
3246
3247    /// Select a single target from `candidates` according to
3248    /// `policy`. Caller has already ensured `candidates` is
3249    /// non-empty and sorted (so `Sticky` is consistent across
3250    /// calls).
3251    fn select_target(&self, candidates: &[u64], policy: &RoutingPolicy) -> u64 {
3252        match policy {
3253            RoutingPolicy::RoundRobin => {
3254                // `fetch_add(1)` on a dedicated cursor — NOT a
3255                // `load(call_id)` — so two concurrent
3256                // `call_service` invocations always observe
3257                // distinct values and pick distinct targets.
3258                let n = self
3259                    .rpc_round_robin_cursor_arc()
3260                    .fetch_add(1, Ordering::Relaxed);
3261                candidates[(n as usize) % candidates.len()]
3262            }
3263            RoutingPolicy::Random => {
3264                // Lightweight RNG via a fresh fetch_add (same
3265                // counter, separate per-call value) mixed through
3266                // xxh3. Sufficient for load distribution;
3267                // not cryptographically random.
3268                let n = self
3269                    .rpc_round_robin_cursor_arc()
3270                    .fetch_add(1, Ordering::Relaxed);
3271                let mixed = xxhash_rust::xxh3::xxh3_64(&n.to_le_bytes());
3272                candidates[(mixed as usize) % candidates.len()]
3273            }
3274            RoutingPolicy::Sticky { key } => {
3275                // Consistent-hash to a position in the (sorted)
3276                // candidate list. Same key + same candidate set =
3277                // same target. A change to the candidate set
3278                // (server failover) reshuffles roughly 1/N of keys.
3279                let h = xxhash_rust::xxh3::xxh3_64(&key.to_le_bytes());
3280                candidates[(h as usize) % candidates.len()]
3281            }
3282            RoutingPolicy::LowestLatency => {
3283                // Walk candidates, look up each via the bridge
3284                // → proximity graph, pick the smallest
3285                // `latency_us`. Candidates without a proximity
3286                // entry (no observed pingwave or no entity-id
3287                // mapping yet) are treated as `u64::MAX` so they
3288                // sort to the bottom — a known-fast node beats an
3289                // unknown one.
3290                //
3291                // Determinism on tie / no-data: `best_node` starts
3292                // at `candidates[0]` (the lexicographically first
3293                // sorted candidate), so all-ties or all-unknown
3294                // collapse to that consistent fallback.
3295                let proximity = self.proximity_graph();
3296                let mut best_node = candidates[0];
3297                let mut best_latency = u64::MAX;
3298                for &node_id in candidates {
3299                    let lat = self
3300                        .entity_id_for_node(node_id)
3301                        .and_then(|eid| proximity.get_node(&eid))
3302                        .map(|n| n.latency_us)
3303                        .unwrap_or(u64::MAX);
3304                    if lat < best_latency {
3305                        best_latency = lat;
3306                        best_node = node_id;
3307                    }
3308                }
3309                best_node
3310            }
3311        }
3312    }
3313
3314    /// Issue an RPC call to `target_node_id` for `service`.
3315    ///
3316    /// Phase 1 — direct entity-to-entity addressing. The caller
3317    /// specifies which target to send to; service discovery (the
3318    /// "find me a healthy instance of X" lookup) is Phase 2.
3319    ///
3320    /// Lazily subscribes the local node's `RpcClientFold` to
3321    /// `<service>.replies.<self_origin>` from `target_node_id` on
3322    /// the first call to that (target, service) pair. The
3323    /// subscription is reused across subsequent calls.
3324    ///
3325    /// On `opts.deadline` expiring OR the future being dropped,
3326    /// emits a CANCEL event so the server can drop the in-flight
3327    /// handler.
3328    pub async fn call(
3329        self: &Arc<Self>,
3330        target_node_id: u64,
3331        service: &str,
3332        payload: Bytes,
3333        mut opts: CallOptions,
3334    ) -> Result<RpcReply, RpcError> {
3335        // `started_total` brackets the entire call for the
3336        // `RpcObserver` latency field; the substrate-internal
3337        // `started` further down (set after the subscription
3338        // setup) drives the existing `RpcReply::latency_ns`
3339        // accounting so observers and Prometheus metrics
3340        // measure slightly different spans but stay consistent
3341        // within their own surface.
3342        let started_total = Instant::now();
3343        let request_bytes_len = payload.len() as u32;
3344        // Per-service route cache: one `DashMap::get(&str)` +
3345        // `Arc::clone` on the hot path instead of 2 `format!` +
3346        // 2 `ChannelName::new` + xxhash per call (T1.3 perf audit
3347        // — `docs/misc/PERF_AUDIT_2026_05_19_NRPC.md`).
3348        let route = self.rpc_route_or_no_route(target_node_id, service)?;
3349        let self_origin = self.identity_origin_hash();
3350
3351        // Caller-side metrics guard. Bumps `in_flight` immediately;
3352        // each early-return path calls `metrics_guard.record(...)`
3353        // with the outcome, and Drop records the latency + bumps
3354        // the matching counter. A future dropped before any
3355        // `record(...)` call (e.g. a hedge loser) leaves the guard
3356        // with `outcome = None` so `in_flight` decrements but no
3357        // outcome is double-counted.
3358        let metrics_registry = self.rpc_metrics_arc();
3359        let mut metrics_guard = CallMetricsGuard::new(metrics_registry.for_service(service));
3360
3361        // Lazy reply-channel subscription. Once per (target, service).
3362        // Reply channel + hash come from the cached `RpcRoute`; we
3363        // only `.clone()` the `ChannelName` (cheap — internally an
3364        // Arc<str>) instead of building it from scratch.
3365        if let Err(e) = self
3366            .ensure_reply_subscription(
3367                target_node_id,
3368                service,
3369                route.reply_channel.clone(),
3370                route.reply_hash,
3371            )
3372            .await
3373        {
3374            metrics_guard.record(CallOutcome::NoRoute);
3375            self.fire_rpc_observer_outbound(
3376                target_node_id,
3377                service,
3378                started_total.elapsed().as_millis() as u32,
3379                crate::adapter::net::cortex::rpc_observer::RpcCallStatus::Error(e.to_string()),
3380                request_bytes_len,
3381                0,
3382            );
3383            return Err(e);
3384        }
3385
3386        // Allocate a fresh call_id. Random u64 from getrandom; a
3387        // sequential counter would let any session peer that
3388        // observed one of their own call_ids predict the next-
3389        // allocated ids and ship spoofed RESPONSE frames on the
3390        // victim's reply channel. Random u64 collides with
3391        // probability 2^-64 per call and is unguessable from
3392        // another peer's perspective.
3393        let call_id = mint_random_call_id();
3394
3395        // Register the oneshot before publishing the REQUEST so a
3396        // very-fast RESPONSE doesn't arrive before we're ready.
3397        // S-4 part 2: bind the pending entry to target_node_id so
3398        // the fold's deliver gate rejects spoofed RESPONSE frames
3399        // arriving from any other session peer.
3400        let pending = self.rpc_client_pending();
3401        let rx = pending.register(call_id, target_node_id);
3402
3403        // Build the REQUEST envelope. If a trace context is set,
3404        // emit `traceparent` / `tracestate` headers and signal
3405        // via `FLAG_RPC_PROPAGATE_TRACE` so the server's fold
3406        // populates `RpcContext::trace_context`.
3407        let (flags, mut headers) = match opts.trace_context.as_ref() {
3408            Some(tc) => (FLAG_RPC_PROPAGATE_TRACE, build_trace_headers(tc)),
3409            None => (0u16, Vec::new()),
3410        };
3411        // Append caller-supplied request headers (e.g. the
3412        // `net-where` predicate header for Phase 9b
3413        // predicate-pushdown). Auto-generated headers come first
3414        // so name collisions resolve to caller-overrides via the
3415        // server-side `predicate_from_rpc_headers` first-match
3416        // semantics.
3417        // PERF_AUDIT §3.11 — `opts` is owned by this function and
3418        // its `request_headers` are unused after this point;
3419        // `Vec::append(&mut other)` drains `other` into `headers`
3420        // with zero allocation, vs the pre-fix
3421        // `.iter().cloned()` which deep-cloned each
3422        // `(String, Vec<u8>)` pair into a fresh entry.
3423        headers.append(&mut opts.request_headers);
3424        let req = RpcRequestPayload {
3425            service: service.to_string(),
3426            deadline_ns: opts.deadline.map(instant_to_unix_nanos).unwrap_or(0),
3427            flags,
3428            headers,
3429            body: payload.clone(),
3430        };
3431        let meta = EventMeta::new(DISPATCH_RPC_REQUEST, 0, self_origin, call_id, 0);
3432        let mut buf = Vec::with_capacity(EVENT_META_SIZE + req.body.len() + 32);
3433        buf.extend_from_slice(&meta.to_bytes());
3434        req.encode_into(&mut buf);
3435
3436        // Send the REQUEST directly to `target_node_id` via
3437        // `publish_to_peer`, bypassing the local subscriber roster
3438        // lookup. The roster-based `Mesh::publish` would consult
3439        // `dispatch_recipients(channel)` against the caller's local
3440        // roster, which has no knowledge of who serves this service
3441        // (no Subscribe message ever propagated from the server back
3442        // to the caller — `serve_rpc` is local-only). For Phase 1
3443        // direct addressing we know the target, so direct-send is
3444        // the right primitive.
3445        //
3446        // The receiver routes via the per-channel-hash dispatcher
3447        // hook (channel_hash is stamped on the wire by
3448        // publish_to_peer).
3449        let started = Instant::now();
3450        // Request channel hash + stream_id come from the cached
3451        // route — no `ChannelId::new` clone + xxhash per call.
3452        let payload_bytes = Bytes::from(buf);
3453        if let Err(e) = self
3454            .publish_to_peer(
3455                target_node_id,
3456                route.request_channel_hash,
3457                route.request_stream_id,
3458                /* reliable */ true,
3459                std::slice::from_ref(&payload_bytes),
3460            )
3461            .await
3462        {
3463            pending.cancel(call_id);
3464            // Distinguish "I don't know how to reach this peer"
3465            // from a generic transport blip: when the publish path
3466            // surfaces a no-session error, that's NoRoute (the
3467            // routing layer's job, retry won't help). Other
3468            // transport errors stay as Transport so retry is
3469            // applicable.
3470            let err = if classify_publish_no_session(&e) {
3471                metrics_guard.record(CallOutcome::NoRoute);
3472                RpcError::NoRoute {
3473                    target: target_node_id,
3474                    reason: e.to_string(),
3475                }
3476            } else {
3477                metrics_guard.record(CallOutcome::Transport);
3478                RpcError::Transport(e)
3479            };
3480            self.fire_rpc_observer_outbound(
3481                target_node_id,
3482                service,
3483                started_total.elapsed().as_millis() as u32,
3484                crate::adapter::net::cortex::rpc_observer::RpcCallStatus::Error(err.to_string()),
3485                request_bytes_len,
3486                0,
3487            );
3488            return Err(err);
3489        }
3490
3491        // From here on, the REQUEST is in flight on the server.
3492        // Wrap the rest of the call in an RAII guard whose Drop
3493        // fires CANCEL if `guard.completed` isn't set — covering:
3494        //  - the call future being dropped mid-flight (e.g. hedge
3495        //    loser, select!-cancelled future, caller awaiting a
3496        //    `JoinHandle` that gets cancelled).
3497        //  - the timeout path (we leave `completed=false` so Drop
3498        //    handles CANCEL emission; no need for a separate
3499        //    `send_rpc_cancel` call).
3500        //  - the cancel_token path (same: leave completed=false,
3501        //    Drop emits CANCEL).
3502        let mut guard = UnaryCallGuard {
3503            pending: Arc::clone(&pending),
3504            mesh: Arc::clone(self),
3505            target_node_id,
3506            request_channel: route.request_channel.clone(),
3507            self_origin,
3508            call_id,
3509            completed: false,
3510        };
3511
3512        // Substrate cancel-token plumbing (v3 / C-S1). When the
3513        // caller set `opts.cancel_token`, register a Notify against
3514        // the per-mesh cancel_registry. The select! arm below
3515        // observes the cancel signal and short-circuits to
3516        // RpcError::Cancelled, leaving guard.completed = false so
3517        // Drop fires CANCEL on the wire. Release the registry
3518        // entry once the call resolves so the registry doesn't
3519        // grow unboundedly.
3520        let cancel_token = opts.cancel_token.unwrap_or(0);
3521        let cancel_notify = self.cancel_registry().register_notify(cancel_token);
3522
3523        // Race the receiver against the deadline AND the cancel
3524        // signal. Each branch lifts to the same outcome shape
3525        // (Result<Result<RpcResponsePayload, _>, Elapsed>) so the
3526        // existing post-match logic stays unchanged for the ok /
3527        // timeout paths; the cancel arm returns early via
3528        // fire_unary_cancel_outcome — leaves guard.completed=false
3529        // so Drop emits CANCEL on the wire.
3530        let outcome: Result<Result<RpcResponsePayload, _>, tokio::time::error::Elapsed> =
3531            match opts.deadline {
3532                None => {
3533                    tokio::select! {
3534                        biased;
3535                        _ = cancel_notify.notified() => {
3536                            return Err(fire_unary_cancel_outcome(
3537                                self,
3538                                &mut metrics_guard,
3539                                cancel_token,
3540                                target_node_id,
3541                                service,
3542                                started_total,
3543                                request_bytes_len,
3544                            ));
3545                        }
3546                        r = rx => Ok(r),
3547                    }
3548                }
3549                Some(deadline) => {
3550                    let timeout_at = deadline.saturating_duration_since(Instant::now());
3551                    tokio::select! {
3552                        biased;
3553                        _ = cancel_notify.notified() => {
3554                            return Err(fire_unary_cancel_outcome(
3555                                self,
3556                                &mut metrics_guard,
3557                                cancel_token,
3558                                target_node_id,
3559                                service,
3560                                started_total,
3561                                request_bytes_len,
3562                            ));
3563                        }
3564                        r = tokio::time::timeout(timeout_at, rx) => r,
3565                    }
3566                }
3567            };
3568
3569        // Whichever non-cancel path won, release the registry
3570        // entry. Idempotent if the cancel arm already released.
3571        self.cancel_registry().release(cancel_token);
3572
3573        let resp = match outcome {
3574            Ok(Ok(resp)) => {
3575                guard.completed = true;
3576                resp
3577            }
3578            Ok(Err(_recv_err)) => {
3579                // Sender dropped externally — pending entry is
3580                // already gone (someone else removed it). Mark
3581                // completed so Drop doesn't fire a useless CANCEL
3582                // for a server that's no longer tracking this id.
3583                guard.completed = true;
3584                metrics_guard.record(CallOutcome::Transport);
3585                let err = RpcError::Transport(AdapterError::Connection(
3586                    "rpc client pending sender dropped (no response will arrive)".into(),
3587                ));
3588                self.fire_rpc_observer_outbound(
3589                    target_node_id,
3590                    service,
3591                    started_total.elapsed().as_millis() as u32,
3592                    crate::adapter::net::cortex::rpc_observer::RpcCallStatus::Error(
3593                        err.to_string(),
3594                    ),
3595                    request_bytes_len,
3596                    0,
3597                );
3598                return Err(err);
3599            }
3600            Err(_elapsed) => {
3601                // Timeout: leave `completed=false` so Drop emits
3602                // CANCEL automatically; surface Timeout to caller.
3603                metrics_guard.record(CallOutcome::Timeout);
3604                self.fire_rpc_observer_outbound(
3605                    target_node_id,
3606                    service,
3607                    started_total.elapsed().as_millis() as u32,
3608                    crate::adapter::net::cortex::rpc_observer::RpcCallStatus::Timeout,
3609                    request_bytes_len,
3610                    0,
3611                );
3612                return Err(RpcError::Timeout {
3613                    elapsed_ms: started.elapsed().as_millis() as u64,
3614                });
3615            }
3616        };
3617
3618        // Map the wire status onto the public Result type.
3619        if resp.status.is_ok() {
3620            metrics_guard.record(CallOutcome::Ok);
3621            let response_bytes_len = resp.body.len() as u32;
3622            self.fire_rpc_observer_outbound(
3623                target_node_id,
3624                service,
3625                started_total.elapsed().as_millis() as u32,
3626                crate::adapter::net::cortex::rpc_observer::RpcCallStatus::Ok,
3627                request_bytes_len,
3628                response_bytes_len,
3629            );
3630            Ok(RpcReply {
3631                body: resp.body,
3632                headers: resp.headers,
3633                latency_ns: started.elapsed().as_nanos() as u64,
3634            })
3635        } else {
3636            metrics_guard.record(CallOutcome::ServerError);
3637            let status = resp.status.to_wire();
3638            let response_bytes_len = resp.body.len() as u32;
3639            let message = String::from_utf8(resp.body.to_vec())
3640                .unwrap_or_else(|e| format!("<{} bytes of non-utf8 body>", e.into_bytes().len()));
3641            self.fire_rpc_observer_outbound(
3642                target_node_id,
3643                service,
3644                started_total.elapsed().as_millis() as u32,
3645                crate::adapter::net::cortex::rpc_observer::RpcCallStatus::Error(message.clone()),
3646                request_bytes_len,
3647                response_bytes_len,
3648            );
3649            // v0.4 capability-auth: callee-side defense-in-depth
3650            // surfaces as a wire `CapabilityDenied` status. Map it
3651            // back to the typed `RpcError::CapabilityDenied` so
3652            // application code sees the same variant regardless of
3653            // which side of the gate fired.
3654            if matches!(resp.status, RpcStatus::CapabilityDenied) {
3655                return Err(RpcError::CapabilityDenied {
3656                    target: target_node_id,
3657                    capability: service.to_string(),
3658                });
3659            }
3660            Err(RpcError::ServerError {
3661                status,
3662                message,
3663                headers: resp.headers,
3664            })
3665        }
3666    }
3667
3668    // ----------------------------------------------------------------
3669    // Internal helpers.
3670    // ----------------------------------------------------------------
3671
3672    /// Lazy-subscribe `reply_channel` from `target_node_id` and
3673    /// register an inbound dispatcher that drives the per-Mesh
3674    /// `RpcClientFold`. Idempotent — subsequent calls for the
3675    /// same (target, service) pair are no-ops.
3676    ///
3677    /// **Bounded** at [`MAX_REPLY_SUBSCRIPTIONS`]: a caller talking
3678    /// to many short-lived (target, service) pairs would otherwise
3679    /// grow the registry indefinitely. Past the cap we refuse the
3680    /// new subscription with `NoRoute` rather than evict an
3681    /// existing one (eviction could rip out a healthy in-flight
3682    /// reply path).
3683    ///
3684    /// **Dispatcher reuse**: the reply-channel name embeds the
3685    /// CALLER's `self_origin`, NOT the target's, so a single
3686    /// caller talking to multiple servers for the same service
3687    /// reuses the same reply channel (same hash). We register the
3688    /// dispatcher only if the slot is unoccupied; subsequent
3689    /// (target, service) pairs that hash to the same slot are
3690    /// allowed to share the existing dispatcher (which routes to
3691    /// the same per-Mesh `pending` map regardless of target). A
3692    /// genuine cross-service hash collision is detected at
3693    /// `serve_rpc` time (the AlreadyServing path) for the server
3694    /// side; on the caller side here, sharing the dispatcher is
3695    /// the correct behavior because all RESPONSE events route
3696    /// through the same `RpcClientPending` keyed by `call_id`.
3697    async fn ensure_reply_subscription(
3698        self: &Arc<Self>,
3699        target_node_id: u64,
3700        service: &str,
3701        reply_channel: ChannelName,
3702        reply_hash: ChannelHash,
3703    ) -> Result<(), RpcError> {
3704        let registry = self.rpc_reply_subscriptions_arc();
3705        // PERF_AUDIT §3.5 — DashMap keyed by
3706        // `(target, xxh3_64(service))`. Pre-fix this was a global
3707        // `Mutex<Vec<(u64, String)>>` that every concurrent RPC
3708        // caller took on every call to scan the Vec with a String
3709        // compare per entry — all callers serialized on it. Now the
3710        // hot path is one shard-local read with a single String
3711        // compare against the slot's stored service name (xxh3 is
3712        // not collision-free; see `reply_subscription_covers`).
3713        let service_hash = xxhash_rust::xxh3::xxh3_64(service.as_bytes());
3714        if reply_subscription_covers(&registry, target_node_id, service_hash, service) {
3715            return Ok(());
3716        }
3717        // Cap the registry. `len()` on DashMap is approximate under
3718        // concurrent churn (it sums shard counts under shard reads,
3719        // not a global lock), which is exactly the semantics we
3720        // want here — the cap is a soft guard against a runaway
3721        // caller, not a precise invariant. Past the cap, new
3722        // entries are refused.
3723        if registry.len() >= MAX_REPLY_SUBSCRIPTIONS {
3724            return Err(RpcError::NoRoute {
3725                target: target_node_id,
3726                reason: format!(
3727                    "reply-subscription registry at cap ({} entries); refusing new \
3728                     (target={target_node_id:#x}, service={service:?}). Caller should \
3729                     reuse an existing target+service pair or shrink the active set.",
3730                    MAX_REPLY_SUBSCRIPTIONS,
3731                ),
3732            });
3733        }
3734
3735        // Subscribe to our own reply channel from the target so the
3736        // target's roster has us as a subscriber when the server's
3737        // emit closure publishes the RESPONSE.
3738        self.subscribe_channel(target_node_id, reply_channel.clone())
3739            .await
3740            .map_err(|e| RpcError::NoRoute {
3741                target: target_node_id,
3742                reason: e.to_string(),
3743            })?;
3744
3745        // Register the inbound dispatcher only if the slot is
3746        // unoccupied. The reply-channel name embeds *self_origin*,
3747        // not the target, so multiple targets serving the same
3748        // service share one reply channel + one dispatcher. The
3749        // existing dispatcher routes to the same per-Mesh
3750        // `RpcClientPending` keyed by call_id, so reuse is safe.
3751        if !self.rpc_inbound_dispatcher_registered(reply_hash) {
3752            let pending = self.rpc_client_pending();
3753            let fold = Arc::new(Mutex::new(RpcClientFold::new(pending)));
3754            // S-4 part 2: use `apply_inbound` so the wire-session
3755            // peer's NodeId (resolved in mesh.rs's dispatch site)
3756            // flows into the fold's deliver gate. The legacy
3757            // `RedexFold::apply` shim delivers with from_node=0,
3758            // which would defeat the binding.
3759            let dispatcher: RpcInboundDispatcher = Arc::new(move |ev| {
3760                fold.lock().apply_inbound(&ev);
3761            });
3762            // Race-safe: a concurrent caller might have just
3763            // registered between our check and our insert. In that
3764            // case `register_rpc_inbound` returns the prior
3765            // dispatcher; our new fresh fold is dropped here, and
3766            // the prior dispatcher (which routes to the same
3767            // shared `pending`) keeps doing the job. No collision
3768            // — both folds are functionally equivalent.
3769            if let Some(prev) = self.register_rpc_inbound(reply_hash, dispatcher) {
3770                // Roll back: keep the prior dispatcher (it's
3771                // already wired to the same shared pending map).
3772                let _ = self.register_rpc_inbound(reply_hash, prev);
3773            }
3774        }
3775
3776        let _ = reply_hash; // captured into the dispatcher above; surfaced for debug
3777                            // `insert` is idempotent — a concurrent caller that beat us
3778                            // to it just overwrote the slot with the identical value.
3779                            // On a genuine xxh3 collision between two service names on
3780                            // the same target, the slot flips to whichever service
3781                            // subscribed last and the other re-subscribes on its next
3782                            // call (idempotent, correct, merely un-cached). Cap drift
3783                            // past MAX_REPLY_SUBSCRIPTIONS during a concurrent insert
3784                            // race is bounded by the number of concurrent callers,
3785                            // which operators tune separately.
3786        registry.insert((target_node_id, service_hash), Arc::from(service));
3787        Ok(())
3788    }
3789}
3790
3791/// PERF_AUDIT §3.5 — hot-path membership check for the
3792/// reply-subscription registry. Returns `true` only when the slot
3793/// for `(target, xxh3(service))` exists AND the stored service
3794/// name matches exactly. xxh3_64 is neither collision-free nor
3795/// cryptographic; a hash-only hit that skipped the subscribe for
3796/// a *different* service would silently drop that service's
3797/// replies — the reply channel name embeds the service, so being
3798/// in the target's roster for the colliding service's channel
3799/// does nothing for this one. Verifying the stored name turns a
3800/// collision into a per-call re-subscribe (idempotent, harmless)
3801/// instead of a correctness bug.
3802fn reply_subscription_covers(
3803    registry: &dashmap::DashMap<(u64, u64), Arc<str>>,
3804    target_node_id: u64,
3805    service_hash: u64,
3806    service: &str,
3807) -> bool {
3808    registry
3809        .get(&(target_node_id, service_hash))
3810        .is_some_and(|entry| entry.value().as_ref() == service)
3811}
3812
3813/// Hard cap on the number of distinct (target_node_id, service)
3814/// pairs the caller-side reply-subscription registry will hold.
3815/// Past the cap, the lazy-subscribe path inside [`MeshNode::call`]
3816/// refuses new entries with [`RpcError::NoRoute`]. 1024 is
3817/// generous for any realistic deployment — a caller that needs
3818/// more should reuse existing reply paths.
3819pub const MAX_REPLY_SUBSCRIPTIONS: usize = 1024;
3820
3821/// Mint a random 64-bit call_id. Used as the correlation token
3822/// for REQUEST/RESPONSE pairing. The fold keys pending oneshots on
3823/// this value; any session peer with publish access to the reply
3824/// channel could ship a forged RESPONSE if it could guess the
3825/// value. Sequential u64s are predictable from any peer that
3826/// observes a single allocation; random u64s collide with 2^-64
3827/// probability per call and are unpredictable to observing peers.
3828///
3829/// **PERF_AUDIT §3.8** — pre-fix this called `getrandom::fill` for
3830/// 8 bytes per RPC — one OS entropy syscall per call
3831/// (BCryptGenRandom on Windows, ~200-400 ns; somewhat cheaper on
3832/// Linux). Now each thread refills a small pool of raw OS entropy
3833/// ([`CALL_ID_ENTROPY_POOL_BYTES`]) with a single `getrandom`
3834/// syscall and hands out 8 bytes per call, amortizing the syscall
3835/// across [`CALL_ID_ENTROPY_POOL_BYTES`]/8 mints.
3836///
3837/// Every minted id is still raw OS entropy — NOT the output of a
3838/// userspace PRNG — so the unpredictability-to-peers property is
3839/// byte-for-byte identical to the pre-§3.8 per-call fill. (An
3840/// earlier draft of this fix streamed ids from a thread-local
3841/// SplitMix64; that was unsound for this threat model: call_ids
3842/// are sent to callees by design, and SplitMix64's output
3843/// finalizer is a public bijection, so a single observed id
3844/// reveals the generator state and with it every FUTURE call_id
3845/// minted on that thread — letting one callee forge responses to
3846/// races on calls addressed to other peers. Raw pooled entropy
3847/// has no such state to recover.)
3848///
3849/// If the pool refill fails, falls back to a process-global
3850/// monotonic counter rather than returning `0`: two concurrent
3851/// callers that both minted `0` would `register(0, …)` over each
3852/// other, so the first caller's oneshot closes with
3853/// `RecvError::Closed` (a spurious `Transport` error, not the clean
3854/// timeout the all-distinct path yields). The counter keeps ids
3855/// distinct (predictable on entropy failure, but the S-4
3856/// `from_node` gate still blocks cross-peer forgery, and such calls
3857/// time out anyway). `getrandom::fill` failure is a fatal-
3858/// environment signal (no `/dev/urandom`, broken syscall) and the
3859/// broader stack won't be functional anyway; the pool cursor is
3860/// left exhausted so the next mint retries the refill. `0` is
3861/// reserved as a sentinel and never returned.
3862fn mint_random_call_id() -> u64 {
3863    thread_local! {
3864        // (pool, cursor). Cursor starts exhausted so the first
3865        // mint on each thread performs the initial refill.
3866        static CALL_ID_ENTROPY_POOL: std::cell::RefCell<([u8; CALL_ID_ENTROPY_POOL_BYTES], usize)> = const {
3867            std::cell::RefCell::new(([0u8; CALL_ID_ENTROPY_POOL_BYTES], CALL_ID_ENTROPY_POOL_BYTES))
3868        };
3869    }
3870    CALL_ID_ENTROPY_POOL.with(|cell| {
3871        let mut pool = cell.borrow_mut();
3872        let (buf, cursor) = &mut *pool;
3873        if *cursor >= CALL_ID_ENTROPY_POOL_BYTES {
3874            if getrandom::fill(buf).is_err() {
3875                // Entropy unavailable. Do NOT return 0 — concurrent
3876                // callers would all mint 0 and clobber each other's
3877                // pending entries. A process-global counter keeps ids
3878                // distinct (starts at 1, so it is non-zero until it
3879                // wraps the full u64 range, at which point the 0 is
3880                // mapped to 1 below).
3881                static CALL_ID_FALLBACK: std::sync::atomic::AtomicU64 =
3882                    std::sync::atomic::AtomicU64::new(1);
3883                let id = CALL_ID_FALLBACK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3884                return if id == 0 { 1 } else { id };
3885            }
3886            *cursor = 0;
3887        }
3888        let mut id = [0u8; 8];
3889        id.copy_from_slice(&buf[*cursor..*cursor + 8]);
3890        *cursor += 8;
3891        // Reserve 0 as the "no correlation" sentinel: on the ~1-in-2^64
3892        // chance the pool yields all-zero bytes, remap to a fixed non-zero.
3893        match u64::from_le_bytes(id) {
3894            0 => 1,
3895            id => id,
3896        }
3897    })
3898}
3899
3900/// Per-thread OS-entropy pool size for [`mint_random_call_id`].
3901/// 64 ids (512 bytes) per `getrandom` syscall — the syscall cost
3902/// is dominated by the fixed kernel round-trip, so batching 64
3903/// mints recovers ~98% of the per-call overhead while keeping the
3904/// amount of buffered future-id entropy per thread small.
3905const CALL_ID_ENTROPY_POOL_BYTES: usize = 64 * 8;
3906
3907// ============================================================================
3908// Internal: tiny shims so the `serve_rpc` / `call` impls stay
3909// readable. The underlying state lives on `MeshNode`; these just
3910// rename the accessor methods locally.
3911// ============================================================================
3912
3913impl MeshNode {
3914    fn rpc_client_pending(&self) -> Arc<super::cortex::RpcClientPending> {
3915        self.rpc_client_pending_arc()
3916    }
3917    fn identity_origin_hash(&self) -> u64 {
3918        self.public_key_origin_hash()
3919    }
3920
3921    /// Caller-side helper that pairs `rpc_route_for_service` with
3922    /// the `RpcError::NoRoute { target, reason }` mapping every
3923    /// `Mesh::call*` entry point needs. Returning `Arc<RpcRoute>`
3924    /// keeps the hot-path allocation profile of the cache intact
3925    /// (one refcount bump per caller).
3926    fn rpc_route_or_no_route(
3927        &self,
3928        target_node_id: u64,
3929        service: &str,
3930    ) -> Result<Arc<super::mesh::RpcRoute>, RpcError> {
3931        self.rpc_route_for_service(service)
3932            .map_err(|reason| RpcError::NoRoute {
3933                target: target_node_id,
3934                reason,
3935            })
3936    }
3937}
3938
3939// `proximity_graph()` is already a public accessor on MeshNode
3940// (see the existing `pub fn proximity_graph(&self) -> &Arc<...>`).
3941// `select_target` uses it directly; no shim needed.
3942
3943// ============================================================================
3944// Errors.
3945// ============================================================================
3946
3947/// Errors returned by [`MeshNode::serve_rpc`].
3948#[derive(Debug, thiserror::Error)]
3949pub enum ServeError {
3950    /// The service name fails channel-name validation.
3951    #[error("invalid service name: {0}")]
3952    InvalidServiceName(String),
3953    /// A handler for this service is already registered on this
3954    /// node. Drop the prior `ServeHandle` to free the slot.
3955    #[error("already serving service `{0}` on this node")]
3956    AlreadyServing(String),
3957    /// The descriptor announces `pricing_terms`, but this serve path
3958    /// has no payment-admission gate — an announced price this path
3959    /// cannot enforce must never reach discovery (callers would see a
3960    /// priced tool that serves free). Serve paid tools via the SDK's
3961    /// `Mesh::serve_tool_paid` (native gate), or publish through the
3962    /// MCP adapter's `ServerPublisher::publish_tools` with a
3963    /// `payment_admission` gate.
3964    #[error(
3965        "tool `{0}` announces pricing_terms but this serve path cannot enforce payment — \
3966         serve paid tools via Mesh::serve_tool_paid, or publish via \
3967         ServerPublisher::publish_tools with payment_admission"
3968    )]
3969    UnenforceablePricing(String),
3970    /// The gated serve path (`Mesh::serve_tool_paid`) got a descriptor
3971    /// with **no** `pricing_terms`: a payment gate on an unannounced
3972    /// price means every caller is refused with no way to know why.
3973    /// Announce the price (the gate enforces it), or serve the tool
3974    /// free via `Mesh::serve_tool`.
3975    #[error(
3976        "tool `{0}` is served through the payment gate but announces no pricing_terms — \
3977         attach terms to the descriptor, or serve it free via Mesh::serve_tool"
3978    )]
3979    MissingPricingTerms(String),
3980}
3981
3982// ============================================================================
3983// Typed-call helper.
3984// ============================================================================
3985
3986/// Wire-shape failures from [`typed_call`]. Distinct variants
3987/// for transport (no route, timeout, etc.) vs codec (serde /
3988/// postcard) so service-specific client error enums can wrap
3989/// each independently. Server-level (application) errors are
3990/// decoded into `Resp` itself — the client matches on the
3991/// resulting `Resp::Error(...)` variant.
3992#[derive(Debug, thiserror::Error)]
3993pub enum TypedCallError {
3994    /// Transport-level failure surfaced by [`MeshNode::call`].
3995    #[error("transport: {0}")]
3996    Transport(#[from] RpcError),
3997    /// Request serialization or response deserialization failed.
3998    #[error("codec: {0}")]
3999    Codec(String),
4000}
4001
4002impl From<postcard::Error> for TypedCallError {
4003    fn from(e: postcard::Error) -> Self {
4004        Self::Codec(e.to_string())
4005    }
4006}
4007
4008/// Send a postcard-encoded request to a remote RPC service and
4009/// decode the postcard-encoded reply. The shared shape every
4010/// substrate-internal RPC client wants:
4011///
4012/// 1. `postcard::to_allocvec(request)` → wire body.
4013/// 2. `MeshNode::call(target, service, body, opts{deadline})`.
4014/// 3. `postcard::from_bytes::<Resp>(reply.body)`.
4015///
4016/// Caller wraps the returned `Resp` in its own typed-error
4017/// surface (typically a `Server` variant that holds the
4018/// service-specific error enum decoded from `Resp`). Returning
4019/// `TypedCallError` here keeps the wrapper code to a one-line
4020/// `From<TypedCallError>` impl per client.
4021pub async fn typed_call<Req, Resp>(
4022    mesh: &std::sync::Arc<crate::adapter::net::MeshNode>,
4023    target_node_id: u64,
4024    service: &str,
4025    request: &Req,
4026    deadline: std::time::Duration,
4027) -> Result<Resp, TypedCallError>
4028where
4029    Req: serde::Serialize,
4030    Resp: serde::de::DeserializeOwned,
4031{
4032    let body = postcard::to_allocvec(request)?;
4033    let opts = CallOptions {
4034        deadline: Some(std::time::Instant::now() + deadline),
4035        ..Default::default()
4036    };
4037    let reply = mesh
4038        .call(target_node_id, service, Bytes::from(body), opts)
4039        .await?;
4040    Ok(postcard::from_bytes(&reply.body)?)
4041}
4042
4043// ============================================================================
4044// Helpers.
4045// ============================================================================
4046
4047/// Detect the "no session to the target node id" sub-case of
4048/// [`AdapterError::Connection`]. The publish path can surface
4049/// this through one of two messages depending on which inner
4050/// helper landed it:
4051///
4052///   - `"publish: no session for subscriber {hash}"` — emitted
4053///     by `mesh.rs::publish_to_peer` when the subscriber-roster
4054///     path can't find an active session.
4055///   - `"no session to publisher {hash}"` — emitted by the lower
4056///     mesh.rs send path when there's no active session to the
4057///     target's publisher record at all.
4058///
4059/// Both mean "I can't reach this peer". When we observe either,
4060/// we surface as [`RpcError::NoRoute`] rather than `Transport`
4061/// because retrying the same target without a session is
4062/// pointless and the right behavior for a routing helper is to
4063/// try a different target.
4064fn classify_publish_no_session(err: &AdapterError) -> bool {
4065    match err {
4066        AdapterError::Connection(msg) => {
4067            msg.contains("no session for subscriber") || msg.contains("no session to publisher")
4068        }
4069        _ => false,
4070    }
4071}
4072
4073fn instant_to_unix_nanos(instant: Instant) -> u64 {
4074    // `Instant` is monotonic and not wall-clock — convert via the
4075    // delta from now plus current SystemTime. The result drifts
4076    // marginally with wall-clock skew but is good enough for
4077    // server-side deadline-already-passed short-circuits (which are
4078    // the only consumer of `deadline_ns`).
4079    let now_instant = Instant::now();
4080    let now_wall = std::time::SystemTime::now()
4081        .duration_since(std::time::UNIX_EPOCH)
4082        .map(|d| d.as_nanos() as u64)
4083        .unwrap_or(0);
4084    if instant >= now_instant {
4085        let delta = instant.duration_since(now_instant);
4086        now_wall.saturating_add(delta.as_nanos() as u64)
4087    } else {
4088        let delta = now_instant.duration_since(instant);
4089        now_wall.saturating_sub(delta.as_nanos() as u64)
4090    }
4091}
4092
4093#[allow(dead_code)]
4094fn _ensure_send_sync() {
4095    fn assert_send_sync<T: Send + Sync>() {}
4096    assert_send_sync::<ServeHandle>();
4097    assert_send_sync::<RpcCancellationToken>();
4098    assert_send_sync::<RpcContext>();
4099    assert_send_sync::<RpcHandlerError>();
4100    assert_send_sync::<RpcStatus>();
4101    assert_send_sync::<RpcReply>();
4102    assert_send_sync::<CallOptions>();
4103}
4104
4105#[cfg(test)]
4106mod origin_cache_tests {
4107    use super::*;
4108
4109    /// The crafted-origin memory-amplification guard (cubic P2): the reply-
4110    /// channel / origin-node caches are keyed by the *wire-claimed*
4111    /// `caller_origin`, which a single authed peer can vary freely. Spraying
4112    /// far more distinct origins than the capacity must NOT grow the cache —
4113    /// it stays pinned at `RPC_CALLER_CACHE_CAP`, evicting the coldest.
4114    #[test]
4115    fn origin_keyed_lru_bounds_under_crafted_origin_flood() {
4116        let cache: OriginKeyedLru<u64> = OriginKeyedLru::new();
4117        let flood = (RPC_CALLER_CACHE_CAP as u64) * 4;
4118        for origin in 0..flood {
4119            cache.insert(origin, origin);
4120        }
4121        assert_eq!(
4122            cache.0.lock().len(),
4123            RPC_CALLER_CACHE_CAP,
4124            "cache must stay at its capacity bound under a crafted-origin flood"
4125        );
4126        // The most-recently-seen window survives; the cold prefix is evicted.
4127        assert_eq!(cache.get(flood - 1), Some(flood - 1));
4128        assert_eq!(cache.get(0), None);
4129    }
4130
4131    /// PERF_AUDIT §3.8 — `mint_random_call_id` mints thousands of
4132    /// values from the pooled-entropy path, all of which must be
4133    /// distinct in practice (a duplicate would let two in-flight
4134    /// calls collide on the per-Mesh pending map). 100k samples is
4135    /// far below the 2^32 birthday-paradox boundary, so a
4136    /// regression that recycled pool bytes (cursor mis-advance,
4137    /// missed refill) would fail loudly. The loop also crosses the
4138    /// pool-refill boundary thousands of times (pool holds 64 ids),
4139    /// pinning the refill/cursor arithmetic.
4140    #[test]
4141    fn mint_random_call_id_produces_distinct_values_across_thousands_of_calls() {
4142        let mut seen = std::collections::HashSet::with_capacity(100_000);
4143        for _ in 0..100_000 {
4144            let id = super::mint_random_call_id();
4145            // 0 is the fallback sentinel — should not appear under
4146            // a working `getrandom` refill.
4147            assert_ne!(id, 0, "fallback-zero path triggered unexpectedly");
4148            assert!(seen.insert(id), "duplicate call_id minted: {:#x}", id);
4149        }
4150    }
4151
4152    /// PERF_AUDIT §3.8 — minted ids are raw OS entropy: count
4153    /// set-bits across 10k mints and assert the fraction is near
4154    /// 0.5. A pool-management bug that handed out the zeroed
4155    /// initial buffer (or re-served a stale window) would skew
4156    /// this hard; properly random 64-bit ids have expected ~0.5
4157    /// set bits per sample with O(1/sqrt(N)) tolerance.
4158    #[test]
4159    fn mint_random_call_id_set_bit_density_is_balanced() {
4160        let n = 10_000u64;
4161        let mut total_set: u64 = 0;
4162        for _ in 0..n {
4163            total_set += super::mint_random_call_id().count_ones() as u64;
4164        }
4165        let bits_total = n * 64;
4166        let fraction = total_set as f64 / bits_total as f64;
4167        // Expected 0.5; tolerance generous (~3σ) to keep the test
4168        // reliable while still catching collapsed-pool regressions.
4169        assert!(
4170            (fraction - 0.5).abs() < 0.02,
4171            "set-bit density {} is too far from 0.5 — pool may be mismanaged",
4172            fraction
4173        );
4174    }
4175
4176    /// PERF_AUDIT §3.5 — the reply-subscription registry's hot
4177    /// path must be a `(target, xxh3(service))` lookup, not a
4178    /// `Mutex<Vec<(u64, String)>>` linear scan. Pin the contract
4179    /// via `reply_subscription_covers` (the exact hot-path check):
4180    /// 1. distinct (target, service) pairs are distinct keys —
4181    ///    same service against two targets, and two services
4182    ///    against one target, never alias;
4183    /// 2. repeat insert of the same pair is idempotent and the
4184    ///    fast path keeps answering `true`;
4185    /// 3. an xxh3 COLLISION (same hash, different service name)
4186    ///    must NOT count as covered — a false positive here would
4187    ///    skip a needed subscribe and silently drop the colliding
4188    ///    service's replies. The stored-name verification turns it
4189    ///    into a re-subscribe instead;
4190    /// 4. the cap is enforced via `len()`, not a separate counter
4191    ///    that could drift.
4192    #[test]
4193    fn reply_subscriptions_keyed_by_target_and_service_hash() {
4194        use dashmap::DashMap;
4195        let registry: DashMap<(u64, u64), Arc<str>> = DashMap::new();
4196        let h_a = xxhash_rust::xxh3::xxh3_64(b"svc-a");
4197        let h_b = xxhash_rust::xxh3::xxh3_64(b"svc-b");
4198        // Same target, different services → distinct entries.
4199        registry.insert((0xAA, h_a), Arc::from("svc-a"));
4200        registry.insert((0xAA, h_b), Arc::from("svc-b"));
4201        assert!(super::reply_subscription_covers(
4202            &registry, 0xAA, h_a, "svc-a"
4203        ));
4204        assert!(super::reply_subscription_covers(
4205            &registry, 0xAA, h_b, "svc-b"
4206        ));
4207        // Same service, different targets → distinct entries.
4208        assert!(!super::reply_subscription_covers(
4209            &registry, 0xBB, h_a, "svc-a"
4210        ));
4211        registry.insert((0xBB, h_a), Arc::from("svc-a"));
4212        assert!(super::reply_subscription_covers(
4213            &registry, 0xBB, h_a, "svc-a"
4214        ));
4215        // Idempotent — repeat insert overwrites with the identical
4216        // value; the fast path keeps answering true.
4217        registry.insert((0xAA, h_a), Arc::from("svc-a"));
4218        assert!(super::reply_subscription_covers(
4219            &registry, 0xAA, h_a, "svc-a"
4220        ));
4221        assert_eq!(registry.len(), 3);
4222        // xxh3 collision: "svc-evil" hashing to h_a (forced here —
4223        // xxh3_64 collisions are computable offline since the hash
4224        // isn't cryptographic) must NOT cover "svc-a"'s slot, and
4225        // vice versa. The hash-only DashSet shape this replaced
4226        // answered `true` and silently skipped the subscribe.
4227        assert!(
4228            !super::reply_subscription_covers(&registry, 0xAA, h_a, "svc-evil"),
4229            "hash collision must not satisfy the membership check for a \
4230             different service name"
4231        );
4232        // After the colliding service legitimately subscribes (slot
4233        // overwritten), the original service degrades to
4234        // re-subscribe — covered must flip to false for it, never
4235        // silently true for both.
4236        registry.insert((0xAA, h_a), Arc::from("svc-evil"));
4237        assert!(super::reply_subscription_covers(
4238            &registry, 0xAA, h_a, "svc-evil"
4239        ));
4240        assert!(!super::reply_subscription_covers(
4241            &registry, 0xAA, h_a, "svc-a"
4242        ));
4243    }
4244
4245    /// PERF_AUDIT §3.3 — grant-stall backstop check for the
4246    /// window/2 auto-grant coalescing. Simulates the full
4247    /// credit loop for every window 1..=64: the server starts
4248    /// with `window` credits and consumes one per chunk; the
4249    /// client accumulates via `accumulate_auto_grant` and only
4250    /// flushes at the threshold. Asserts:
4251    /// 1. liveness — an actively-polling consumer never observes
4252    ///    the server starved (credits exhausted with nothing left
4253    ///    to poll), i.e. withholding sub-threshold credits cannot
4254    ///    deadlock the stream and no timer/drop backstop is needed;
4255    /// 2. coalescing — grant-packet count stays at
4256    ///    ~chunks / (window/2), and is strictly fewer than one
4257    ///    grant per chunk once window ≥ 4 (the integration suite
4258    ///    only exercises window=2, whose threshold degenerates
4259    ///    to per-chunk).
4260    #[test]
4261    fn auto_grant_coalescing_never_starves_the_server_pump() {
4262        for window in 1u32..=64 {
4263            let chunks = 1_000u32;
4264            let mut server_credits = window as u64;
4265            let mut pending = 0u32;
4266            let mut sent = 0u32;
4267            let mut delivered = 0u32;
4268            let mut grants = 0u32;
4269            while delivered < chunks {
4270                // Server pump: send while credits remain.
4271                while server_credits > 0 && sent < chunks {
4272                    server_credits -= 1;
4273                    sent += 1;
4274                }
4275                assert!(
4276                    sent > delivered,
4277                    "window {window}: server starved while the consumer is actively \
4278                     polling (credits {server_credits}, pending {pending}, \
4279                     sent {sent}, delivered {delivered})"
4280                );
4281                // Consumer polls exactly one chunk.
4282                delivered += 1;
4283                if let Some(amount) = super::accumulate_auto_grant(&mut pending, window) {
4284                    grants += 1;
4285                    server_credits += amount as u64;
4286                }
4287            }
4288            let threshold = (window / 2).max(1);
4289            assert!(
4290                grants <= chunks / threshold + 1,
4291                "window {window}: {grants} grant packets exceeds the \
4292                 coalesced cadence bound of {}",
4293                chunks / threshold + 1
4294            );
4295            if window >= 4 {
4296                assert!(
4297                    grants < chunks,
4298                    "window {window}: coalescing must emit fewer grants than chunks"
4299                );
4300            }
4301        }
4302    }
4303
4304    /// `get` promotes to most-recently-used, so a touched entry outlives an
4305    /// untouched one when the cache overflows by one — confirming the wrapper
4306    /// gives true LRU semantics (a hot caller isn't evicted out from under an
4307    /// in-flight exchange).
4308    #[test]
4309    fn origin_keyed_lru_get_promotes_to_mru() {
4310        let cache: OriginKeyedLru<u64> = OriginKeyedLru::new();
4311        for origin in 0..(RPC_CALLER_CACHE_CAP as u64) {
4312            cache.insert(origin, origin);
4313        }
4314        // Touch origin 0 (otherwise the LRU), then overflow by one entry.
4315        assert_eq!(cache.get(0), Some(0));
4316        cache.insert(u64::MAX, 1);
4317        assert_eq!(cache.get(0), Some(0), "touched entry must survive eviction");
4318        assert_eq!(cache.get(1), None, "the now-LRU entry (1) must be evicted");
4319    }
4320}