Skip to main content

superfluid_daemon/
nodeagent.rs

1//! The node-agent.
2
3use 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
25/// How long the head may be silent on a connection before the node drops it: six of the
26/// head's heartbeats.
27pub const DEFAULT_IDLE_TIMEOUT: Duration = Duration::from_secs(30);
28
29struct AgentShared {
30    agent: AgentSession,
31    /// The connection each session was placed over, the one the head answers its renewals on.
32    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/// Time with at least one generation running, which the node's rates are measured over: a
42/// rate over wall time would read an idle node as a slow one.
43#[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    /// When this connection last asked for each lease it has not had granted yet.
85    asked: HashMap<u64, Duration>,
86}
87
88static NEXT_CONN: AtomicU64 = AtomicU64::new(1);
89
90const STATUS_INTERVAL_MS: u64 = 2_000;
91
92/// A renewal the head has not answered is asked for again after this long.
93const RENEW_RETRY: Duration = Duration::from_secs(1);
94
95/// How long one read of the link waits while a generation runs, which bounds how late its
96/// tokens leave.
97const 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    /// `idle` is how long the head may be silent before a connection is dropped; zero keeps
155    /// every connection. Keep it well above the head's heartbeat, 5 s.
156    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        // Dropped on the way out, which stops a generation still running.
290        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            // A head on version 2 sends no heartbeat while it waits on a generation, so only
337            // a connection with nothing running is idle.
338            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    /// Asks the head to renew the leases of the sessions placed over this connection, which is
350    /// where it answers them; again after [`RENEW_RETRY`] until a grant comes.
351    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                    // The head waits on one stage per connection; another is answered as one
476                    // the node could not run.
477                    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    /// Runs a stage forward on a thread of its own, as a generation runs, so a long one does not
499    /// silence the connection.
500    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    /// Starts `req` on a thread of its own, so this connection goes on reading the head
518    /// (cancels, lease grants, heartbeats) and sending status while it runs.
519    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    /// Sends what `run` produced since the last pass, coalesced, and its finish once it ends;
566    /// `true` once it has ended, or was fenced and goes silent.
567    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    /// Sends `run`'s pending tokens; `false` when its session is fenced, which sends nothing.
621    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/// What a connection has running off its own thread.
640#[derive(Default)]
641struct Work {
642    generation: Option<Running>,
643    /// A stage forward's answer, once it has one.
644    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
653/// A generation running on a connection; dropping it stops the generation at its next step.
654struct Running {
655    session_id: u64,
656    generation_id: u64,
657    max_new: u32,
658    cancel: Arc<AtomicBool>,
659    /// The head asked for the stop.
660    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}