1use std::collections::HashMap;
4use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
5use std::sync::{mpsc, Arc, Mutex};
6use std::time::{Duration, Instant};
7
8use superfluid_abi::finish;
9use superfluid_linkf::{AgentAction, AgentSession, Endpoint, LinkFError, Liveness};
10use superfluid_proto::handshake::{CapacitySummary, HelloAckF};
11use superfluid_proto::linkf::{self, msg_type, LinkFLimits, TokenEventPayload};
12
13use crate::fleet::{decode_params, decode_tokens};
14use crate::wal::GenParams;
15use crate::{Daemon, DaemonError};
16
17pub const FINISH_EXPIRED: u32 = u32::MAX;
18
19pub const FINISH_REFUSED: u32 = u32::MAX - 1;
20
21pub const FINISH_DECLINED: u32 = u32::MAX - 2;
22
23pub const DEFAULT_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(10);
24
25pub const DEFAULT_IDLE_TIMEOUT: Duration = Duration::from_secs(30);
28
29struct AgentShared {
30 agent: AgentSession,
31 owners: HashMap<u64, u64>,
33 contexts: HashMap<u64, Vec<u32>>,
34 params: HashMap<u64, GenParams>,
35 last_load_sample: Option<(u64, u64, u64)>,
36 decode_rate_ewma: u32,
37 prefill_rate_ewma: u32,
38 busy: Busy,
39}
40
41#[derive(Default)]
44struct Busy {
45 running: u32,
46 since: Option<Instant>,
47 total: Duration,
48}
49
50impl Busy {
51 fn start(&mut self) {
52 if self.running == 0 {
53 self.since = Some(Instant::now());
54 }
55 self.running += 1;
56 }
57
58 fn stop(&mut self) {
59 self.running = self.running.saturating_sub(1);
60 if self.running == 0 {
61 if let Some(t) = self.since.take() {
62 self.total += t.elapsed();
63 }
64 }
65 }
66
67 fn ms(&self) -> u64 {
68 (self.total + self.since.map(|t| t.elapsed()).unwrap_or_default()).as_millis() as u64
69 }
70}
71
72pub struct NodeAgent {
73 daemon: Arc<Daemon>,
74 shared: Arc<Mutex<AgentShared>>,
75 identity: String,
76 model_name: String,
77 start: Instant,
78 handshake_timeout: Duration,
79 idle_timeout: Duration,
80 auth: Vec<u8>,
81 max_lanes: u32,
82 last_status_ms: u64,
83 conn: u64,
84 asked: HashMap<u64, Duration>,
86}
87
88static NEXT_CONN: AtomicU64 = AtomicU64::new(1);
89
90const STATUS_INTERVAL_MS: u64 = 2_000;
91
92const RENEW_RETRY: Duration = Duration::from_secs(1);
94
95const POLL_RUNNING: Duration = Duration::from_millis(2);
98
99const POLL_IDLE: Duration = Duration::from_millis(20);
100
101const EMIT_COALESCE_TOKENS: usize = 16;
102
103const EMIT_COALESCE_MS: u64 = 200;
104
105impl NodeAgent {
106 pub fn new(daemon: Arc<Daemon>, identity: impl Into<String>, model_name: impl Into<String>) -> NodeAgent {
107 NodeAgent {
108 max_lanes: 0,
109 last_status_ms: 0,
110 conn: NEXT_CONN.fetch_add(1, Ordering::Relaxed),
111 asked: HashMap::new(),
112 daemon,
113 shared: Arc::new(Mutex::new(AgentShared {
114 agent: AgentSession::new(),
115 owners: HashMap::new(),
116 contexts: HashMap::new(),
117 params: HashMap::new(),
118 last_load_sample: None,
119 decode_rate_ewma: 0,
120 prefill_rate_ewma: 0,
121 busy: Busy::default(),
122 })),
123 identity: identity.into(),
124 model_name: model_name.into(),
125 start: Instant::now(),
126 handshake_timeout: DEFAULT_HANDSHAKE_TIMEOUT,
127 idle_timeout: DEFAULT_IDLE_TIMEOUT,
128 auth: Vec::new(),
129 }
130 }
131
132 pub fn with_max_lanes(mut self, lanes: u32) -> NodeAgent {
133 self.max_lanes = lanes;
134 self
135 }
136
137 pub fn connection(&self) -> NodeAgent {
138 NodeAgent {
139 daemon: Arc::clone(&self.daemon),
140 shared: Arc::clone(&self.shared),
141 identity: self.identity.clone(),
142 model_name: self.model_name.clone(),
143 start: self.start,
144 handshake_timeout: self.handshake_timeout,
145 idle_timeout: self.idle_timeout,
146 auth: self.auth.clone(),
147 max_lanes: self.max_lanes,
148 last_status_ms: 0,
149 conn: NEXT_CONN.fetch_add(1, Ordering::Relaxed),
150 asked: HashMap::new(),
151 }
152 }
153
154 pub fn with_timeouts(mut self, handshake: Duration, idle: Duration) -> NodeAgent {
157 self.handshake_timeout = handshake;
158 self.idle_timeout = idle;
159 self
160 }
161
162 pub fn with_auth(mut self, auth: Vec<u8>) -> NodeAgent {
163 self.auth = auth;
164 self
165 }
166
167 fn sample_load(&mut self) -> (u64, u64, u32, u32, u32, u32, u32, u64) {
168 let mut sh = self.shared.lock().expect("agent shared");
169 let stats = self.daemon.sched_stats();
170 let now = self.start.elapsed().as_millis() as u64;
171 let decoded = stats.decode_tokens.load(std::sync::atomic::Ordering::Relaxed);
172 let prefilled = stats.prefill_tokens.load(std::sync::atomic::Ordering::Relaxed);
173
174 let smooth = |prev: u32, inst: u32| -> u32 {
175 match prev {
176 0 => inst,
177 p => ((p as u64 * 3 + inst as u64) / 4) as u32,
178 }
179 };
180 const MIN_BUSY_MS: u64 = 200;
181 let busy = sh.busy.ms();
182 match sh.last_load_sample {
183 Some((prev_busy, prev_decoded, prev_prefilled)) => {
184 let dt_ms = busy.saturating_sub(prev_busy);
185 if dt_ms >= MIN_BUSY_MS {
186 let rate = |n: u64, prev: u64| (n.saturating_sub(prev).saturating_mul(1000) / dt_ms) as u32;
187 let (d, pf) = (rate(decoded, prev_decoded), rate(prefilled, prev_prefilled));
188 if d > 0 {
189 sh.decode_rate_ewma = smooth(sh.decode_rate_ewma, d);
190 }
191 if pf > 0 {
192 sh.prefill_rate_ewma = smooth(sh.prefill_rate_ewma, pf);
193 }
194 sh.last_load_sample = Some((busy, decoded, prefilled));
195 }
196 }
197 None => sh.last_load_sample = Some((busy, decoded, prefilled)),
198 }
199 let (rate, prefill_rate) = (sh.decode_rate_ewma, sh.prefill_rate_ewma);
200
201 let queue: u64 = stats.queue_depth.iter().map(|q| q.load(std::sync::atomic::Ordering::Relaxed)).sum();
202 (
203 stats.pool_blocks_used.load(std::sync::atomic::Ordering::Relaxed),
204 stats.pool_blocks_total.load(std::sync::atomic::Ordering::Relaxed),
205 stats.lanes_active.load(std::sync::atomic::Ordering::Relaxed) as u32,
206 self.max_lanes,
207 queue as u32,
208 rate,
209 prefill_rate,
210 now,
211 )
212 }
213
214 fn capacity(&mut self) -> CapacitySummary {
215 let (kv_used, kv_total, lanes, max_lanes, queue, rate, prefill_rate, at) =
216 self.sample_load();
217 CapacitySummary {
218 gpu_memory_bytes: 0,
219 gpu_memory_free_bytes: 0,
220 host_memory_bytes: 0,
221 active_sessions: self.shared.lock().expect("agent shared").contexts.len() as u32,
222 max_sessions: u32::MAX,
223 max_context_tokens: self.daemon.max_stream_tokens(),
224 kv_blocks_used: kv_used,
225 kv_blocks_total: kv_total,
226 lanes_active: lanes,
227 max_lanes,
228 queue_depth: queue,
229 decode_tokens_per_s: rate,
230 prefill_tokens_per_s: prefill_rate,
231 sampled_at_ms: at,
232 }
233 }
234
235 fn send_status(&mut self, endpoint: &mut Endpoint) {
236 let capacity = self.capacity();
237 let msg = linkf::NodeStatusMsg {
238 mem: superfluid_proto::linkw::MemCountersMsg {
239 allocated_bytes: 0,
240 host_retained_bytes: 0,
241 pool_blocks_total: capacity.kv_blocks_total,
242 pool_blocks_used: capacity.kv_blocks_used,
243 pool_bytes_evictable: 0,
244 tentative_bytes: 0,
245 },
246 capacity,
247 loadable_models: vec![self.model_name.clone()],
248 };
249 let _ = endpoint.send(msg_type::NODE_STATUS, &msg);
250 self.last_status_ms = self.start.elapsed().as_millis() as u64;
251 }
252
253 fn ackf(&mut self) -> HelloAckF {
254 HelloAckF {
255 chosen_version: superfluid_linkf::driver::PROTO_VERSION,
256 node_identity: self.identity.clone(),
257 capacity: self.capacity(),
258 loadable_models: vec![self.model_name.clone()],
259 feature_flags: 0,
260 limits: LinkFLimits {
261 max_frame: superfluid_proto::MAX_FRAME,
262 transfer_window: 8,
263 max_transfer: 1 << 26,
264 transfer_budget: 1 << 28,
265 },
266 }
267 }
268
269 pub fn serve(&mut self, mut endpoint: Endpoint) -> Result<(), DaemonError> {
270 let hs = if self.handshake_timeout.is_zero() {
271 endpoint.answer_hello_as_agent(&self.ackf(), &self.auth)
272 } else {
273 endpoint.answer_hello_as_agent_within(&self.ackf(), self.handshake_timeout, &self.auth)
274 };
275 match hs {
276 Ok(()) => {}
277 Err(LinkFError::TimedOut) => {
278 tracing::warn!("node-agent dropped a silent connection (no Hello)");
279 return Ok(());
280 }
281 Err(LinkFError::Unauthorized) => return Err(LinkFError::Unauthorized.into()),
282 Err(e) => return Err(e.into()),
283 }
284 let heartbeat = match self.idle_timeout {
285 t if t.is_zero() => Liveness::DEFAULT.heartbeat,
286 t => Liveness::DEFAULT.heartbeat.min(t / 3),
287 };
288 endpoint.keep_alive(Liveness { heartbeat, silence: self.idle_timeout });
289 let mut work = Work::default();
291 self.serve_link(&mut endpoint, &mut work)
292 }
293
294 fn serve_link(&mut self, endpoint: &mut Endpoint, work: &mut Work) -> Result<(), DaemonError> {
295 loop {
296 let now = self.start.elapsed();
297 self.renew(endpoint, now)?;
298 self.fence(now);
299 if now.as_millis() as u64 >= self.last_status_ms + STATUS_INTERVAL_MS {
300 self.send_status(endpoint);
301 }
302 let was_busy = work.busy();
303 if let Some(run) = work.generation.as_mut() {
304 if self.forward(endpoint, run, now)? {
305 work.generation = None;
306 }
307 }
308 if let Some(stage) = &work.stage {
309 match stage.try_recv() {
310 Ok(resp) => {
311 work.stage = None;
312 endpoint.send(msg_type::STAGE_FORWARD_RESP, &resp)?;
313 }
314 Err(mpsc::TryRecvError::Empty) => {}
315 Err(mpsc::TryRecvError::Disconnected) => work.stage = None,
316 }
317 }
318 if was_busy && !work.busy() {
319 endpoint.poll_every(POLL_IDLE)?;
320 }
321 let frames = match endpoint.pump() {
322 Ok(f) => f,
323 Err(LinkFError::Closed) => return Ok(()),
324 Err(LinkFError::TimedOut) => {
325 tracing::warn!(
326 quiet_ms = endpoint.quiet_for().as_millis() as u64,
327 "node-agent dropped a connection the head went silent on"
328 );
329 return Ok(());
330 }
331 Err(e) => return Err(e.into()),
332 };
333 for frame in frames {
334 self.handle(endpoint, frame, work)?;
335 }
336 if endpoint.version() < 3
339 && !work.busy()
340 && !self.idle_timeout.is_zero()
341 && endpoint.quiet_for() >= self.idle_timeout
342 {
343 tracing::info!("node-agent dropped an idle connection");
344 return Ok(());
345 }
346 }
347 }
348
349 fn renew(&mut self, endpoint: &mut Endpoint, now: Duration) -> Result<(), DaemonError> {
352 let due: Vec<_> = {
353 let sh = self.shared.lock().expect("agent shared");
354 sh.agent
355 .needs_renew(now)
356 .into_iter()
357 .filter(|r| sh.owners.get(&r.session_id) == Some(&self.conn))
358 .collect()
359 };
360 for renew in due {
361 if self.asked.get(&renew.session_id).is_some_and(|&at| now.saturating_sub(at) < RENEW_RETRY) {
362 continue;
363 }
364 endpoint.send(msg_type::LEASE_RENEW, &renew)?;
365 self.asked.insert(renew.session_id, now);
366 }
367 Ok(())
368 }
369
370 fn fence(&mut self, now: Duration) {
371 let mut sh = self.shared.lock().expect("agent shared");
372 for action in sh.agent.poll(now) {
373 match action {
374 AgentAction::SelfFence { session_id } => {
375 tracing::warn!(session_id, "node-agent self-fenced on lease expiry");
376 sh.contexts.remove(&session_id);
377 sh.params.remove(&session_id);
378 sh.owners.remove(&session_id);
379 }
380 }
381 }
382 }
383
384 fn handle(
385 &mut self,
386 endpoint: &mut Endpoint,
387 frame: superfluid_proto::Frame,
388 work: &mut Work,
389 ) -> Result<(), DaemonError> {
390 let now = self.start.elapsed();
391 match frame.msg_type {
392 msg_type::RECONCILE => {
393 let m: linkf::ReconcileMsg = postcard::from_bytes(&frame.payload)?;
394 let mut sh = self.shared.lock().expect("agent shared");
395 let (ack, retransmissions) = sh.agent.on_reconcile(&m, now);
396 for s in ack.sessions.iter().filter(|s| s.held) {
397 sh.owners.insert(s.session_id, self.conn);
398 }
399 drop(sh);
400 endpoint.send(msg_type::RECONCILE_ACK, &ack)?;
401 for batch in retransmissions {
402 endpoint.send(msg_type::TOKEN_EVENTS, &batch)?;
403 }
404 }
405 msg_type::SESSION_ASSIGN => {
406 let m: linkf::SessionAssignMsg = postcard::from_bytes(&frame.payload)?;
407 let mut sh = self.shared.lock().expect("agent shared");
408 if sh.agent.on_assign(&m, now) {
409 sh.params.insert(m.session_id, decode_params(&m.params));
410 sh.owners.insert(m.session_id, self.conn);
411 self.asked.remove(&m.session_id);
412 }
413 }
414 msg_type::SESSION_REVOKE => {
415 let m: linkf::SessionRevokeMsg = postcard::from_bytes(&frame.payload)?;
416 let mut sh = self.shared.lock().expect("agent shared");
417 if sh.agent.on_revoke(&m) {
418 sh.contexts.remove(&m.session_id);
419 sh.params.remove(&m.session_id);
420 sh.owners.remove(&m.session_id);
421 self.asked.remove(&m.session_id);
422 }
423 }
424 msg_type::APPEND_CONTENT => {
425 let m: linkf::AppendContentMsg = postcard::from_bytes(&frame.payload)?;
426 let mut sh = self.shared.lock().expect("agent shared");
427 if sh.agent.on_append(m.session_id, m.epoch, m.command_id) {
428 let tokens = decode_tokens(&m.blocks)?;
429 sh.contexts.insert(m.session_id, tokens);
430 }
431 }
432 msg_type::GENERATE_REQ => {
433 let m: linkf::GenerateReqMsg = postcard::from_bytes(&frame.payload)?;
434 let declined = work.generation.is_some() || !self.shared.lock().expect("agent shared").agent.on_generate(&m);
435 if declined {
436 tracing::warn!(
437 session = m.session_id,
438 generation = m.generation_id,
439 epoch = m.epoch,
440 "declining a generate (unknown session, superseded epoch, replayed \
441 command, or one already running on this connection); reporting it \
442 rather than going silent"
443 );
444 let msg = linkf::TokenEventsMsg {
445 events: vec![linkf::TokenEvent {
446 session_id: m.session_id,
447 epoch: m.epoch,
448 generation_id: m.generation_id,
449 event_seq: 0,
450 payload: TokenEventPayload::Finish {
451 reason: FINISH_DECLINED,
452 },
453 }],
454 };
455 endpoint.send(msg_type::TOKEN_EVENTS, &msg)?;
456 } else {
457 work.generation = Some(self.start_generation(&m));
458 endpoint.poll_every(POLL_RUNNING)?;
459 }
460 }
461 msg_type::GENERATE_CANCEL => {
462 let m: linkf::GenerateCancelMsg = postcard::from_bytes(&frame.payload)?;
463 if let Some(run) = work
464 .generation
465 .as_mut()
466 .filter(|r| r.session_id == m.session_id && r.generation_id == m.generation_id)
467 {
468 run.cancelled = true;
469 run.cancel.store(true, Ordering::Relaxed);
470 }
471 }
472 msg_type::STAGE_FORWARD_REQ => {
473 let m: linkf::StageForwardReqMsg = postcard::from_bytes(&frame.payload)?;
474 if work.stage.is_some() {
475 let resp = linkf::StageForwardRespMsg { request_id: m.request_id, hidden: None, token: None };
478 endpoint.send(msg_type::STAGE_FORWARD_RESP, &resp)?;
479 } else {
480 work.stage = Some(self.start_stage(m));
481 endpoint.poll_every(POLL_RUNNING)?;
482 }
483 }
484 msg_type::WATERMARK_ACK => {
485 let m: linkf::WatermarkAckMsg = postcard::from_bytes(&frame.payload)?;
486 self.shared.lock().expect("agent shared").agent.on_watermark_ack(&m);
487 }
488 msg_type::LEASE_GRANT => {
489 let m: linkf::LeaseGrantMsg = postcard::from_bytes(&frame.payload)?;
490 self.shared.lock().expect("agent shared").agent.on_lease_grant(&m, now);
491 self.asked.remove(&m.session_id);
492 }
493 _ => { }
494 }
495 Ok(())
496 }
497
498 fn start_stage(&self, m: linkf::StageForwardReqMsg) -> mpsc::Receiver<linkf::StageForwardRespMsg> {
501 let (tx, rx) = mpsc::channel();
502 let daemon = Arc::clone(&self.daemon);
503 std::thread::Builder::new()
504 .name("node-stage".into())
505 .spawn(move || {
506 let (hidden, token) = match daemon.forward_stage(m.tokens, m.start_layer, m.end_layer, m.hidden_in) {
507 Ok(crate::StageOutput::Hidden(h)) => (Some(h), None),
508 Ok(crate::StageOutput::Token(t)) => (None, Some(t)),
509 Err(_) => (None, None),
510 };
511 let _ = tx.send(linkf::StageForwardRespMsg { request_id: m.request_id, hidden, token });
512 })
513 .expect("spawn a node stage");
514 rx
515 }
516
517 fn start_generation(&self, req: &linkf::GenerateReqMsg) -> Running {
520 let (context, params) = {
521 let sh = self.shared.lock().expect("agent shared");
522 (
523 sh.contexts.get(&req.session_id).cloned().unwrap_or_default(),
524 sh.params.get(&req.session_id).copied().unwrap_or_default(),
525 )
526 };
527 let max_new = req.budgets.max_new_tokens.min(u32::MAX as u64) as u32;
528 let max_wall_ms = req.budgets.max_wall_ms;
529 let cancel = Arc::new(AtomicBool::new(false));
530 let (tx, out) = mpsc::channel();
531 let (daemon, shared, stop) = (Arc::clone(&self.daemon), Arc::clone(&self.shared), Arc::clone(&cancel));
532 shared.lock().expect("agent shared").busy.start();
533 std::thread::Builder::new()
534 .name("node-generation".into())
535 .spawn(move || {
536 let generated = daemon.generate_tokens_streaming(
537 context,
538 params,
539 max_new,
540 max_wall_ms,
541 crate::scheduler::GenExtras::default(),
542 |batch| {
543 if stop.load(Ordering::Relaxed) || tx.send(Out::Tokens(batch.to_vec())).is_err() {
544 return std::ops::ControlFlow::Break(());
545 }
546 std::ops::ControlFlow::Continue(())
547 },
548 );
549 shared.lock().expect("agent shared").busy.stop();
550 let _ = tx.send(Out::Done(generated));
551 })
552 .expect("spawn a node generation");
553 Running {
554 session_id: req.session_id,
555 generation_id: req.generation_id,
556 max_new,
557 cancel,
558 cancelled: false,
559 out,
560 pending: Vec::with_capacity(EMIT_COALESCE_TOKENS),
561 last_flush: self.start.elapsed(),
562 }
563 }
564
565 fn forward(&mut self, endpoint: &mut Endpoint, run: &mut Running, now: Duration) -> Result<bool, DaemonError> {
568 let mut done = None;
569 loop {
570 match run.out.try_recv() {
571 Ok(Out::Tokens(t)) => {
572 run.pending.extend_from_slice(&t);
573 if run.pending.len() >= EMIT_COALESCE_TOKENS && !self.flush(endpoint, run, now)? {
574 return Ok(true);
575 }
576 }
577 Ok(Out::Done(r)) => {
578 done = Some(r);
579 break;
580 }
581 Err(mpsc::TryRecvError::Empty) => break,
582 Err(mpsc::TryRecvError::Disconnected) => {
583 done = Some(Err(DaemonError::Generation("the generation ended without a result".into())));
584 break;
585 }
586 }
587 }
588 let due = done.is_some() || now.saturating_sub(run.last_flush).as_millis() as u64 >= EMIT_COALESCE_MS;
589 if due && !run.pending.is_empty() && !self.flush(endpoint, run, now)? {
590 return Ok(true);
591 }
592 let mut sh = self.shared.lock().expect("agent shared");
593 if sh.agent.is_fenced(run.session_id) != Some(false) {
594 return Ok(true);
595 }
596 let Some(generated) = done else { return Ok(false) };
597 let reason = match generated {
598 Ok(_) if run.cancelled => finish::CANCELLED,
599 Ok((_, _, true)) => FINISH_EXPIRED,
600 Ok((tokens, _, _)) if tokens.len() as u32 >= run.max_new => finish::LENGTH,
601 Ok(_) => finish::EOS,
602 Err(e) if is_client_refusal(&e) => {
603 tracing::warn!(
604 session = run.session_id,
605 generation = run.generation_id,
606 error = %e,
607 "refusing a generation the node cannot stage; \
608 the head should have caught this before dispatch"
609 );
610 FINISH_REFUSED
611 }
612 Err(e) => return Err(e),
613 };
614 if let Some(batch) = sh.agent.emit(run.session_id, run.generation_id, TokenEventPayload::Finish { reason }) {
615 endpoint.send(msg_type::TOKEN_EVENTS, &batch)?;
616 }
617 Ok(true)
618 }
619
620 fn flush(&mut self, endpoint: &mut Endpoint, run: &mut Running, now: Duration) -> Result<bool, DaemonError> {
622 run.last_flush = now;
623 let tokens = std::mem::take(&mut run.pending);
624 let batch = self.shared.lock().expect("agent shared").agent.emit(
625 run.session_id,
626 run.generation_id,
627 TokenEventPayload::Tokens(tokens),
628 );
629 match batch {
630 Some(batch) => {
631 endpoint.send(msg_type::TOKEN_EVENTS, &batch)?;
632 Ok(true)
633 }
634 None => Ok(false),
635 }
636 }
637}
638
639#[derive(Default)]
641struct Work {
642 generation: Option<Running>,
643 stage: Option<mpsc::Receiver<linkf::StageForwardRespMsg>>,
645}
646
647impl Work {
648 fn busy(&self) -> bool {
649 self.generation.is_some() || self.stage.is_some()
650 }
651}
652
653struct Running {
655 session_id: u64,
656 generation_id: u64,
657 max_new: u32,
658 cancel: Arc<AtomicBool>,
659 cancelled: bool,
661 out: mpsc::Receiver<Out>,
662 pending: Vec<u32>,
663 last_flush: Duration,
664}
665
666impl Drop for Running {
667 fn drop(&mut self) {
668 self.cancel.store(true, Ordering::Relaxed);
669 }
670}
671
672enum Out {
673 Tokens(Vec<u32>),
674 Done(Result<(Vec<u32>, u32, bool), DaemonError>),
675}
676
677fn is_client_refusal(e: &DaemonError) -> bool {
678 matches!(
679 e,
680 DaemonError::StreamTooLong { .. } | DaemonError::Constraint(_)
681 )
682}