Skip to main content

superfluid_daemon/
lib.rs

1//! superfluid — the daemon.
2
3pub mod anthropic;
4pub mod api;
5pub mod apiweb;
6pub mod atem;
7pub mod bus;
8pub mod capabilities;
9pub mod cli;
10pub mod codec;
11mod exec;
12pub mod completion_bucket;
13pub mod files;
14pub mod harmony;
15pub mod keypolicy;
16pub mod launch;
17pub mod batches;
18pub mod response_store;
19pub mod ratelimit;
20pub mod registry;
21pub mod template_codec;
22pub mod tool_fragment;
23pub mod fleet;
24pub mod fleet_manager;
25pub mod fleet_join;
26pub mod fleet_mdns;
27pub mod fleet_token;
28#[cfg(feature = "tui")]
29pub mod tui;
30#[cfg(feature = "tui")]
31pub mod tui_top;
32pub mod nodeagent;
33mod inference;
34mod block_inference;
35mod generation_output;
36mod openai_output;
37mod http_state;
38mod http_generation;
39mod keepalive;
40mod responses;
41pub mod openai;
42pub mod ollama;
43pub mod fleet_openai;
44pub mod park;
45pub mod pins;
46pub mod runtime;
47pub mod runtime_pick;
48pub mod installs;
49pub mod pulls;
50pub mod runtimes;
51pub mod runtimes_report;
52pub mod linked;
53pub mod dylib_tokenizer;
54pub mod export;
55pub mod otlp;
56pub mod media;
57pub mod metrics;
58pub mod pressure;
59pub mod scheduler;
60pub mod serve;
61pub mod speculate_resolve;
62use superfluid_proto::linkw as proto_linkw;
63pub mod telemetry;
64pub mod store;
65pub mod store_id;
66pub mod wal;
67pub mod workerd;
68
69pub mod qos {
70    pub const INTERACTIVE_CHAT: u8 = 0;
71    pub const INLINE_COMPLETION: u8 = 1;
72    pub const FOREGROUND_AGENT: u8 = 2;
73    pub const BACKGROUND_AGENT: u8 = 3;
74
75    pub const NAMES: [&str; 4] = ["interactive", "completion", "agent", "background"];
76
77    pub fn parse(s: &str) -> Option<u8> {
78        match s.trim().to_ascii_lowercase().as_str() {
79            "interactive" | "interactive-chat" | "interactive_chat" | "0" => Some(INTERACTIVE_CHAT),
80            "completion" | "inline-completion" | "inline_completion" | "1" => Some(INLINE_COMPLETION),
81            "agent" | "foreground" | "foreground-agent" | "foreground_agent" | "2" => Some(FOREGROUND_AGENT),
82            "background" | "background-agent" | "background_agent" | "3" => Some(BACKGROUND_AGENT),
83            _ => None,
84        }
85    }
86
87    pub fn parse_class_lanes(spec: &str, max_lanes: usize) -> Result<[usize; 4], String> {
88        let mut caps = [0usize; 4];
89        let mut seen = [false; 4];
90        for part in spec.split(',').map(str::trim).filter(|p| !p.is_empty()) {
91            let (name, n) = part
92                .split_once('=')
93                .ok_or_else(|| format!("expected class=N, got {part:?}"))?;
94            let class = parse(name).ok_or_else(|| {
95                format!("unknown class {:?} (expected one of {})", name.trim(), NAMES.join(", "))
96            })? as usize;
97            if seen[class] {
98                return Err(format!("class {} is capped twice", NAMES[class]));
99            }
100            seen[class] = true;
101            let n: usize = n
102                .trim()
103                .parse()
104                .map_err(|_| format!("cap for {} must be a whole number, got {:?}", NAMES[class], n.trim()))?;
105            if n == 0 {
106                return Err(format!(
107                    "cap for {} must be at least 1 (omit the class to leave it uncapped)",
108                    NAMES[class]
109                ));
110            }
111            if n > max_lanes {
112                return Err(format!(
113                    "cap {n} for {} exceeds --max-batch {max_lanes}; it could never bind",
114                    NAMES[class]
115                ));
116            }
117            caps[class] = n;
118        }
119        Ok(caps)
120    }
121
122    #[cfg(test)]
123    mod tests {
124        use super::*;
125
126        #[test]
127        fn names_round_trip_and_aliases_parse() {
128            for (c, name) in NAMES.iter().enumerate() {
129                assert_eq!(parse(name), Some(c as u8));
130                assert_eq!(parse(&c.to_string()), Some(c as u8));
131            }
132            assert_eq!(parse(" Interactive-Chat "), Some(INTERACTIVE_CHAT));
133            assert_eq!(parse("foreground_agent"), Some(FOREGROUND_AGENT));
134            assert_eq!(parse("urgent"), None);
135            assert_eq!(parse("4"), None);
136        }
137
138        #[test]
139        fn class_lanes_spec_parses_and_refuses_bad_caps() {
140            assert_eq!(parse_class_lanes("", 8), Ok([0; 4]));
141            assert_eq!(parse_class_lanes("background=2, agent=6", 8), Ok([0, 0, 6, 2]));
142            assert_eq!(parse_class_lanes("interactive=8", 8), Ok([8, 0, 0, 0]));
143            assert!(parse_class_lanes("background", 8).unwrap_err().contains("class=N"));
144            assert!(parse_class_lanes("urgent=1", 8).unwrap_err().contains("unknown class"));
145            assert!(parse_class_lanes("agent=-1", 8).unwrap_err().contains("whole number"));
146            assert!(parse_class_lanes("agent=9", 8).unwrap_err().contains("exceeds"));
147            assert!(parse_class_lanes("background=0", 8).unwrap_err().contains("at least 1"));
148            assert!(parse_class_lanes("agent=1,foreground=2", 8).unwrap_err().contains("twice"));
149        }
150    }
151}
152
153pub use codec::{MockCodec, NoCodec, TextCodec, Utf8Stream};
154pub use superfluid_engine::StageOutput;
155pub use runtime::{EngineHost, WorkerSpec};
156pub use store::{CommittedEvent, SessionStore};
157pub use wal::{EventBody, GenParams};
158
159pub use codec::BundleCodec;
160
161#[derive(Debug, thiserror::Error)]
162pub enum DaemonError {
163    #[error("io: {0}")]
164    Io(#[from] std::io::Error),
165    #[error("wal encode/decode: {0}")]
166    Codec(#[from] postcard::Error),
167    #[error("wal record failed checksum")]
168    WalCorrupt,
169    #[error("unknown session {0}")]
170    UnknownSession(u64),
171    #[error("session {0} has no content to generate from")]
172    EmptySession(u64),
173    #[error(
174        "this model's maximum context length is {max} tokens, but your request has {len} \
175         tokens; shorten the prompt or drop earlier turns"
176    )]
177    StreamTooLong { len: u64, max: u64 },
178    #[error("model load: {0}")]
179    EngineLoad(String),
180    #[error("engine link: {0}")]
181    Agent(#[from] superfluid_agent::AgentError),
182    #[error("fleet link (Link F): {0}")]
183    Fleet(#[from] superfluid_linkf::LinkFError),
184    #[error("fleet node {addr}: {why}")]
185    FleetNode { addr: String, why: String },
186    #[error("engine rejected the plan with status {0}")]
187    Rejected(i32),
188    #[error("generation failed: {0}")]
189    Generation(String),
190    #[error("session {0} is generating; append/generate after it finishes or cancel it")]
191    SessionBusy(u64),
192    #[error("tool call {0} is unknown or already closed")]
193    UnknownToolCall(u64),
194    #[error("unknown message role {0}")]
195    UnknownRole(u32),
196    #[error("tool {index} is not a function declaration: {why}")]
197    InvalidTool { index: usize, why: String },
198    #[error("session {0} already has a prompt; AppendSystem must come before any other turn")]
199    SystemNotFirst(u64),
200    #[error("fork point {fork_at} is outside session {session}'s committed events (1..={max})")]
201    ForkPoint { session: u64, fork_at: u64, max: u64 },
202    #[error("tool call {0} belongs to an ancestor's timeline; only that session can close it")]
203    InheritedToolCall(u64),
204    #[error("session {session} metadata version is {actual}, command expected {expected}")]
205    MetaConflict { session: u64, expected: u64, actual: u64 },
206    #[error("session {session} generation is {actual}, command expected {expected}")]
207    GenerationConflict { session: u64, expected: u64, actual: u64 },
208    #[error("session {0} was purged")]
209    Purged(u64),
210    #[error("files quota exceeded: {stored} bytes stored, {incoming} more would pass the {max} limit")]
211    FilesQuota { stored: u64, incoming: u64, max: u64 },
212    #[error("rebase edit spec invalid: {0}")]
213    RebaseSpec(&'static str),
214    #[error("session {0} did not quiesce for purge (lane still running)")]
215    PurgeTimeout(u64),
216    #[error("permission request {0} is unknown or already answered")]
217    UnknownPermission(u64),
218    #[error("session {0} has an open permission request; answer it before generating")]
219    PermissionPending(u64),
220    #[error("tool call {0}'s executor lease expired; the result was recorded as expired, not applied")]
221    ToolLeaseExpired(u64),
222    #[error("tool call {0} already reached a terminal outcome; the late result was recorded as a reconciliation")]
223    ToolLateEffect(u64),
224    #[error("unknown block kind {0}")]
225    UnknownBlockKind(u32),
226    #[error("media blob {0} is not in the pool (put it first)")]
227    UnknownMedia(String),
228    #[error("image expands to {0} placeholders, beyond one prefill chunk")]
229    MediaTooLarge(u32),
230    #[error("this codec has no FIM dialect")]
231    NoFimDialect,
232    #[error("completion rate limit exceeded; retry in {retry_after_ms} ms")]
233    CompletionThrottled { retry_after_ms: u64 },
234    #[error("protocol: {0}")]
235    Protocol(&'static str),
236    #[error("constraint unsatisfiable: {0}")]
237    Constraint(String),
238    #[error("the model's chat template refused this conversation: {0}")]
239    TemplateRefused(String),
240    #[error("{0}")]
241    Unsupported(Refusal),
242    #[error("configuration: {0}")]
243    Config(&'static str),
244}
245
246#[derive(Debug, Clone, PartialEq, Eq)]
247pub struct Refusal {
248    pub code: &'static str,
249    pub param: Option<String>,
250    pub message: String,
251}
252
253impl Refusal {
254    pub fn new(code: &'static str, param: Option<&str>, message: impl Into<String>) -> Refusal {
255        Refusal { code, param: param.map(str::to_string), message: message.into() }
256    }
257}
258
259impl std::fmt::Display for Refusal {
260    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
261        f.write_str(&self.message)
262    }
263}
264
265#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
266pub struct SessionSummary {
267    pub id: u64,
268    pub parent: Option<u64>,
269    pub fork_at: u64,
270    pub title: Option<String>,
271    pub archived: bool,
272    pub meta_version: u64,
273    pub generation: u64,
274    pub events: u64,
275    pub tokens: u64,
276    pub open_tool_calls: u64,
277}
278
279#[derive(Debug)]
280pub struct GenerateOutcome {
281    pub events: Vec<CommittedEvent>,
282    pub tokens_generated: u32,
283    pub finish: u32,
284    pub warm_prefix: u64,
285    pub logprobs: Vec<scheduler::TokenLogprob>,
286    pub spec: (u64, u64),
287}
288
289#[derive(Debug, Clone, Default, PartialEq)]
290pub struct ModelSamplingDefaults {
291    pub temperature: Option<f32>,
292    pub top_p: Option<f32>,
293    pub top_k: Option<u32>,
294    pub min_p: Option<f32>,
295    pub do_sample: Option<bool>,
296    pub repetition_penalty: Option<f32>,
297}
298
299impl ModelSamplingDefaults {
300    pub fn from_json(v: &serde_json::Value) -> Self {
301        let f = |k: &str| v.get(k).and_then(|x| x.as_f64()).map(|x| x as f32);
302        Self {
303            temperature: f("temperature"),
304            top_p: f("top_p"),
305            top_k: v.get("top_k").and_then(|x| x.as_u64()).map(|x| x as u32),
306            min_p: f("min_p"),
307            do_sample: v.get("do_sample").and_then(|x| x.as_bool()),
308            repetition_penalty: f("repetition_penalty"),
309        }
310    }
311}
312
313thread_local! {
314    static SYNC_CLASS: std::cell::Cell<Option<u8>> = const { std::cell::Cell::new(None) };
315}
316
317pub fn with_sync_class<R>(class: Option<u8>, f: impl FnOnce() -> R) -> R {
318    let _scope = SyncClassScope::enter(class);
319    f()
320}
321
322pub struct SyncClassScope {
323    prev: Option<Option<u8>>,
324    _not_send: std::marker::PhantomData<*const ()>,
325}
326
327impl SyncClassScope {
328    pub fn enter(class: Option<u8>) -> SyncClassScope {
329        let prev = class.map(|c| SYNC_CLASS.with(|cell| cell.replace(Some(c))));
330        SyncClassScope { prev, _not_send: std::marker::PhantomData }
331    }
332}
333
334impl Drop for SyncClassScope {
335    fn drop(&mut self) {
336        if let Some(prev) = self.prev {
337            SYNC_CLASS.with(|c| c.set(prev));
338        }
339    }
340}
341
342fn ambient_sync_class() -> Option<u8> {
343    SYNC_CLASS.with(|c| c.get())
344}
345
346struct Serving {
347    capabilities: runtime::SharedCapabilities,
348    retired: std::sync::Arc<std::sync::Mutex<Option<String>>>,
349    model_sampling: std::sync::OnceLock<ModelSamplingDefaults>,
350    codec: std::sync::Arc<dyn TextCodec + Send + Sync>,
351    sched: scheduler::SchedulerHandle,
352    speculation: std::sync::Arc<scheduler::Speculation>,
353    staged_tokens: u64,
354}
355
356/// Sessions whose generation should stop. A cancel also asks the running
357/// tick to end at its next step boundary, so the lane stops within a step;
358/// a queued chat asks the same.
359#[derive(Default)]
360pub struct CancelSet {
361    set: std::sync::Mutex<std::collections::HashSet<u64>>,
362    wake: std::sync::RwLock<Option<runtime::YieldSlot>>,
363    /// When a latency-sensitive request last arrived (`monotonic_ms`; 0:
364    /// never).
365    latency_at_ms: std::sync::atomic::AtomicU64,
366}
367
368/// Milliseconds on a monotonic clock that starts at 1 with the process: a
369/// wall clock stepped back would hold latency mode on until it caught up.
370fn monotonic_ms() -> u64 {
371    static BASE: std::sync::OnceLock<std::time::Instant> = std::sync::OnceLock::new();
372    BASE.get_or_init(std::time::Instant::now).elapsed().as_millis() as u64 + 1
373}
374
375impl std::ops::Deref for CancelSet {
376    type Target = std::sync::Mutex<std::collections::HashSet<u64>>;
377
378    fn deref(&self) -> &Self::Target {
379        &self.set
380    }
381}
382
383impl CancelSet {
384    pub fn cancel(&self, session: u64) {
385        self.set.lock().expect("cancel registry").insert(session);
386        self.wake();
387    }
388
389    /// Asks the running tick to end at its next step boundary.
390    pub fn wake(&self) {
391        if let Some(slot) = self.wake.read().expect("wake slot").as_ref() {
392            runtime::raise_yield(slot);
393        }
394    }
395
396    pub(crate) fn wake_with(&self, slot: runtime::YieldSlot) {
397        *self.wake.write().expect("wake slot") = Some(slot);
398    }
399
400    /// A chat or an inline completion arrived: the worker runs short
401    /// prefill steps for a while, and the running tick ends at its next one.
402    pub fn latency_request(&self) {
403        self.latency_at_ms.store(monotonic_ms(), std::sync::atomic::Ordering::Release);
404        if let Some(slot) = self.wake.read().expect("wake slot").as_ref() {
405            runtime::set_latency(slot, true);
406        }
407        self.wake();
408    }
409
410    /// Whether a latency-sensitive request arrived within `hold`.
411    pub(crate) fn latency_recent(&self, hold: std::time::Duration) -> bool {
412        let at = self.latency_at_ms.load(std::sync::atomic::Ordering::Acquire);
413        at > 0 && monotonic_ms().saturating_sub(at) < hold.as_millis() as u64
414    }
415}
416
417struct Ground<'a> {
418    store: &'a std::sync::Arc<std::sync::Mutex<SessionStore>>,
419    cancels: &'a std::sync::Arc<CancelSet>,
420    active: &'a std::sync::Arc<std::sync::Mutex<std::collections::HashSet<u64>>>,
421    stats: &'a std::sync::Arc<scheduler::SchedStats>,
422    background_tick_divisor: &'a std::sync::Arc<std::sync::atomic::AtomicU32>,
423}
424
425impl Serving {
426    fn start(
427        host: EngineHost,
428        codec: std::sync::Arc<dyn TextCodec + Send + Sync>,
429        ground: Ground<'_>,
430        opts: DaemonOptions,
431    ) -> Result<Serving, DaemonError> {
432        let mut opts = opts;
433        let mut speculation = std::sync::Arc::new(scheduler::Speculation::default());
434        let mut host = host;
435        ground.cancels.wake_with(host.yield_slot());
436        ground
437            .stats
438            .kv_bits
439            .store(host.kv_bits() as u64, std::sync::atomic::Ordering::Relaxed);
440        if let Some(first) = opts.speculate.clone() {
441            let mut candidates = vec![first];
442            if opts.speculate_auto {
443                candidates.extend(opts.speculate_fallbacks.iter().cloned());
444            }
445            let picked = first_registrable(
446                &candidates,
447                opts.speculate_auto,
448                |strategy| {
449                    speculation = std::sync::Arc::new(scheduler::Speculation::default());
450                    scheduler::Speculation::register(&speculation, &mut host, strategy, &opts.speculation)
451                },
452                |strategy, e, next| {
453                    eprintln!(
454                        "superfluid: speculation: auto picked {strategy}, but the engine refused it ({e}) — {}",
455                        match next {
456                            Some(n) => format!("trying {n}"),
457                            None => "decoding plainly".to_string(),
458                        }
459                    );
460                },
461            )?;
462            if picked.is_none() {
463                speculation = std::sync::Arc::new(scheduler::Speculation::default());
464            } else if picked != opts.speculate {
465                eprintln!("superfluid: speculation: {}", picked.as_deref().unwrap_or_default());
466            }
467            opts.speculate = picked;
468        }
469        let staged_tokens = host.staged_tokens();
470        let capabilities = host.capabilities();
471        let retired = std::sync::Arc::new(std::sync::Mutex::new(None));
472        let sched = scheduler::Scheduler::spawn(
473            host,
474            std::sync::Arc::clone(ground.store),
475            std::sync::Arc::clone(&codec),
476            std::sync::Arc::clone(ground.cancels),
477            std::sync::Arc::clone(ground.active),
478            std::sync::Arc::clone(ground.stats),
479            std::sync::Arc::clone(ground.background_tick_divisor),
480            std::sync::Arc::clone(&speculation),
481            std::sync::Arc::clone(&retired),
482            opts,
483        );
484        {
485            let st = ground.store.lock().expect("store");
486            let now = wal::now_unix_ms();
487            for id in st.session_ids() {
488                let Ok(s) = st.session(id) else { continue };
489                if s.pin_deadline_unix_ms > now {
490                    let (tokens, want) = pins::session_target(&s.tokens);
491                    let _ = sched.tx.send(scheduler::SchedCmd::Pin(pins::PinReq {
492                        key: pins::PinKey::Session(id),
493                        tokens,
494                        want,
495                        deadline_unix_ms: s.pin_deadline_unix_ms,
496                        anchor: Some(id),
497                        reply: None,
498                    }));
499                }
500            }
501        }
502        Ok(Serving { capabilities, retired, model_sampling: std::sync::OnceLock::new(), codec, sched, speculation, staged_tokens })
503    }
504}
505
506pub struct Daemon {
507    serving: std::sync::RwLock<std::sync::Arc<Serving>>,
508    reloading: std::sync::Mutex<()>,
509    reload_hook: std::sync::OnceLock<Box<dyn Fn() + Send + Sync>>,
510    runtime_id: Option<&'static str>,
511    store: std::sync::Arc<std::sync::Mutex<SessionStore>>,
512    cancels: std::sync::Arc<CancelSet>,
513    active: std::sync::Arc<std::sync::Mutex<std::collections::HashSet<u64>>>,
514    purging: std::sync::Arc<std::sync::Mutex<std::collections::HashSet<u64>>>,
515    stats: std::sync::Arc<scheduler::SchedStats>,
516    trace_scope: otlp::TraceScope,
517    background_tick_divisor: std::sync::Arc<std::sync::atomic::AtomicU32>,
518    park_dir: Option<std::path::PathBuf>,
519    tool_lease_ms: u64,
520    media: media::MediaPool,
521    files: files::FileStore,
522    batches: std::sync::Arc<batches::BatchStore>,
523    responses: response_store::ResponseStore,
524    completion: std::sync::Arc<CompletionSatellite>,
525    fim_satellite: std::sync::OnceLock<FimSatellite>,
526    built_with: DaemonOptions,
527}
528
529impl CompletionSatellite {
530    fn cache_get(&self, key: u64) -> Option<(Vec<u32>, String, u32)> {
531        let mut c = self.cache.lock().expect("completion cache");
532        if let Some(v) = c.map.get(&key).cloned() {
533            c.lru.retain(|&k| k != key);
534            c.lru.push_back(key);
535            Some(v)
536        } else {
537            None
538        }
539    }
540    fn cache_put(&self, key: u64, tokens: Vec<u32>, text: String, finish: u32) {
541        let mut c = self.cache.lock().expect("completion cache");
542        if c.map.insert(key, (tokens, text, finish)).is_none() {
543            c.lru.push_back(key);
544        }
545        while c.lru.len() > self.cache_cap {
546            if let Some(old) = c.lru.pop_front() {
547                c.map.remove(&old);
548            }
549        }
550    }
551}
552
553struct FimSatellite {
554    name: String,
555    daemon: std::sync::Arc<Daemon>,
556}
557
558#[derive(Default)]
559struct CompletionCounters {
560    generated: std::sync::atomic::AtomicU64,
561    cached: std::sync::atomic::AtomicU64,
562    expired: std::sync::atomic::AtomicU64,
563    throttled: std::sync::atomic::AtomicU64,
564}
565
566struct CompletionSatellite {
567    store: std::sync::Mutex<SessionStore>,
568    cache: std::sync::Mutex<CompletionCache>,
569    deadline_ms: u64,
570    cache_cap: usize,
571    bucket: completion_bucket::BucketConfig,
572    counters: CompletionCounters,
573}
574
575#[derive(Default)]
576struct CompletionCache {
577    map: std::collections::HashMap<u64, (Vec<u32>, String, u32)>,
578    lru: std::collections::VecDeque<u64>,
579}
580
581#[derive(Debug, Clone, PartialEq)]
582pub struct Completion {
583    pub text: String,
584    pub tokens: Vec<u32>,
585    pub cached: bool,
586    pub expired: bool,
587    pub finish: u32,
588    pub prompt_tokens: u32,
589}
590
591#[derive(Debug, Clone)]
592pub struct FimRequest {
593    pub prefix: String,
594    pub suffix: String,
595    pub mode: u8,
596    pub max_tokens: u32,
597    pub params: GenParams,
598    pub extras: scheduler::GenExtras,
599}
600
601impl FimRequest {
602    pub fn greedy(prefix: &str, suffix: &str, mode: u8, max_tokens: u32) -> FimRequest {
603        FimRequest {
604            prefix: prefix.to_string(),
605            suffix: suffix.to_string(),
606            mode,
607            max_tokens,
608            params: GenParams::default(),
609            extras: scheduler::GenExtras::default(),
610        }
611    }
612}
613
614#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
615pub enum Tier {
616    Resident,
617    Parked { covered: u64, encoding: u8 },
618    Cold,
619}
620
621#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
622pub struct Inspection {
623    pub summary: SessionSummary,
624    pub params: GenParams,
625    pub epoch: u64,
626    pub last_finish: u32,
627    pub qos_class: u8,
628    pub batch_invariant: bool,
629    pub pin_deadline_unix_ms: u64,
630    pub behavior_fingerprint: Option<u64>,
631    pub open_tool_calls: Vec<(u64, String, String)>,
632    pub tier: Tier,
633    pub tokens: Vec<u32>,
634    pub events: Vec<api::EventMsg>,
635    pub wal_version: u8,
636    #[serde(default, deserialize_with = "de_trailing_opt")]
637    pub store_id: Option<store::StoreId>,
638}
639
640fn de_trailing_opt<'de, D, T>(d: D) -> Result<Option<T>, D::Error>
641where
642    D: serde::Deserializer<'de>,
643    T: serde::Deserialize<'de>,
644{
645    use std::cell::Cell;
646    struct V<'a, T>(&'a Cell<bool>, std::marker::PhantomData<T>);
647    impl<'de, T: serde::Deserialize<'de>> serde::de::Visitor<'de> for V<'_, T> {
648        type Value = Option<T>;
649        fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
650            f.write_str("an optional trailing field")
651        }
652        fn visit_none<E: serde::de::Error>(self) -> Result<Option<T>, E> {
653            self.0.set(true);
654            Ok(None)
655        }
656        fn visit_some<D2: serde::Deserializer<'de>>(self, d: D2) -> Result<Option<T>, D2::Error> {
657            self.0.set(true);
658            T::deserialize(d).map(Some)
659        }
660    }
661    let reached = Cell::new(false);
662    match d.deserialize_option(V(&reached, std::marker::PhantomData)) {
663        Ok(v) => Ok(v),
664        Err(e) if !reached.get() && e.to_string() == POSTCARD_UNEXPECTED_END => Ok(None),
665        Err(e) => Err(e),
666    }
667}
668
669const POSTCARD_UNEXPECTED_END: &str = "Hit the end of buffer, expected more data";
670
671#[derive(Debug, Clone, Default, PartialEq)]
672pub struct SpeculationOptions {
673    pub max_draft: Option<u32>,
674    pub adaptive: Option<bool>,
675    pub min_yield: Option<f64>,
676    pub yield_rounds: Option<u32>,
677    pub throughput_gate: Option<bool>,
678    pub gate_probe_tokens: Option<u32>,
679    pub min_speedup: Option<f64>,
680    pub gate_reprobe: Option<u32>,
681    pub gate_reprobe_max: Option<u32>,
682    pub dspark_confidence: Option<f32>,
683}
684
685#[derive(Debug, Clone)]
686pub struct DaemonOptions {
687    pub max_lanes: usize,
688    pub park_dir: Option<std::path::PathBuf>,
689    pub pressure_high_pct: u8,
690    pub pressure_low_pct: u8,
691    pub pin_budget_pct: u8,
692    pub park_lossy: bool,
693    pub park_budget_bytes: u64,
694    pub class_lanes: [usize; 4],
695    pub tick_decode_budget: u32,
696    pub tick_target_ms: u32,
697    pub runtime_id: Option<&'static str>,
698    pub prefill_budget: u32,
699    pub agent_starvation_ticks: u32,
700    pub pressure_source: pressure::PressureConfig,
701    pub completion_deadline_ms: u64,
702    pub completion_bucket: completion_bucket::BucketConfig,
703    pub media_dir: Option<std::path::PathBuf>,
704    pub files_max_bytes: Option<u64>,
705    pub files_expiry_secs: Option<u64>,
706    pub tool_lease_ms: u64,
707    pub speculate: Option<String>,
708    pub speculate_auto: bool,
709    pub speculate_fallbacks: Vec<String>,
710    pub speculation: SpeculationOptions,
711    pub trace_scope: otlp::TraceScope,
712}
713
714impl Default for DaemonOptions {
715    fn default() -> DaemonOptions {
716        DaemonOptions {
717            max_lanes: 8,
718            park_dir: None,
719            pressure_high_pct: 85,
720            pressure_low_pct: 70,
721            pin_budget_pct: 50,
722            park_lossy: false,
723            park_budget_bytes: 20 << 30,
724            class_lanes: [0; 4],
725            tick_decode_budget: 0,
726            tick_target_ms: 0,
727            runtime_id: None,
728            prefill_budget: scheduler::PREFILL_BUDGET,
729            agent_starvation_ticks: 8,
730            pressure_source: pressure::PressureConfig::None,
731            tool_lease_ms: 0,
732            speculate: None,
733            speculate_auto: false,
734            speculate_fallbacks: Vec::new(),
735            speculation: SpeculationOptions::default(),
736            files_max_bytes: None,
737            files_expiry_secs: None,
738            media_dir: None,
739            completion_deadline_ms: 0,
740            trace_scope: otlp::TraceScope::default(),
741            completion_bucket: completion_bucket::BucketConfig::default(),
742        }
743    }
744}
745
746struct PurgeFence {
747    set: std::sync::Arc<std::sync::Mutex<std::collections::HashSet<u64>>>,
748    ids: Vec<u64>,
749}
750
751impl Drop for PurgeFence {
752    fn drop(&mut self) {
753        let mut p = self.set.lock().expect("purging");
754        for id in &self.ids {
755            p.remove(id);
756        }
757    }
758}
759
760pub(crate) fn render_refusal(
761    codec: &dyn TextCodec,
762    messages: &[codec::ChatMessage],
763    tools: &[String],
764    kwargs: &serde_json::Map<String, serde_json::Value>,
765    fallback: &'static str,
766) -> DaemonError {
767    match codec.template_refusal(messages, tools, kwargs) {
768        Some(why) => DaemonError::TemplateRefused(why),
769        None => DaemonError::Protocol(fallback),
770    }
771}
772
773impl Daemon {
774    pub fn new(
775        store: SessionStore,
776        host: EngineHost,
777        codec: Box<dyn TextCodec + Send + Sync>,
778        max_lanes: usize,
779    ) -> Daemon {
780        Daemon::with_park(store, host, codec, max_lanes, None)
781    }
782
783    pub fn with_park(
784        store: SessionStore,
785        host: EngineHost,
786        codec: Box<dyn TextCodec + Send + Sync>,
787        max_lanes: usize,
788        park_dir: Option<std::path::PathBuf>,
789    ) -> Daemon {
790        Daemon::with_options(
791            store,
792            host,
793            codec,
794            DaemonOptions {
795                max_lanes,
796                park_dir,
797                ..DaemonOptions::default()
798            },
799        )
800        .expect("default watermarks are valid")
801    }
802
803    pub fn with_options(
804        store: SessionStore,
805        host: EngineHost,
806        codec: Box<dyn TextCodec + Send + Sync>,
807        opts: DaemonOptions,
808    ) -> Result<Daemon, DaemonError> {
809        if opts.pressure_high_pct == 0 || opts.pressure_high_pct > 100 {
810            return Err(DaemonError::Config(
811                "pressure_high_pct must be in 1..=100",
812            ));
813        }
814        if opts.pressure_low_pct >= opts.pressure_high_pct {
815            return Err(DaemonError::Config(
816                "pressure_low_pct must be below pressure_high_pct",
817            ));
818        }
819        if opts.max_lanes == 0 {
820            return Err(DaemonError::Config("max_lanes must be at least 1"));
821        }
822        let mut opts = opts;
823        let runtime_id = opts.runtime_id;
824        opts.pin_budget_pct = opts.pin_budget_pct.min(opts.pressure_low_pct);
825        opts.completion_bucket.validate().map_err(DaemonError::Config)?;
826        if matches!(opts.speculation.max_draft, Some(0 | 16..)) {
827            return Err(DaemonError::Config("speculation max_draft must be in 1..=15"));
828        }
829        if opts.speculation.min_yield.is_some_and(|v| !v.is_finite() || v < 0.0) {
830            return Err(DaemonError::Config("speculation min_yield must be finite and non-negative"));
831        }
832        if opts.speculation.yield_rounds == Some(0) {
833            return Err(DaemonError::Config("speculation yield_rounds must be at least 1"));
834        }
835        if opts.speculation.min_speedup.is_some_and(|v| !v.is_finite() || v < 1.0) {
836            return Err(DaemonError::Config(
837                "speculation min_speedup must be finite and at least 1.0",
838            ));
839        }
840        if opts.speculation.dspark_confidence.is_some_and(|v| !v.is_finite() || v <= 0.0) {
841            return Err(DaemonError::Config(
842                "speculation dspark_confidence must be finite and positive",
843            ));
844        }
845        opts.trace_scope.ns = export::store_trace_ns(&store.store_id());
846        let store = std::sync::Arc::new(std::sync::Mutex::new(store));
847        let built_with = opts.clone();
848        let cancels = std::sync::Arc::new(CancelSet::default());
849        let active = std::sync::Arc::new(std::sync::Mutex::new(std::collections::HashSet::new()));
850        let purging = std::sync::Arc::new(std::sync::Mutex::new(std::collections::HashSet::new()));
851        let stats = std::sync::Arc::new(scheduler::SchedStats::default());
852        let background_tick_divisor = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(1));
853        let park_dir = opts.park_dir.clone();
854        let tool_lease_ms = opts.tool_lease_ms;
855        let files_max_bytes = opts.files_max_bytes;
856        let files_expiry_secs = opts.files_expiry_secs;
857        let completion_deadline_ms = opts.completion_deadline_ms;
858        let completion_bucket = opts.completion_bucket;
859        let media_dir = opts.media_dir.clone().unwrap_or_else(|| {
860            park_dir
861                .as_ref()
862                .and_then(|p| p.parent().map(|d| d.join("media")))
863                .unwrap_or_else(|| {
864                    let uniq = std::time::SystemTime::now()
865                        .duration_since(std::time::UNIX_EPOCH)
866                        .map(|d| d.as_nanos())
867                        .unwrap_or(0);
868                    std::env::temp_dir().join(format!("superfluid-{}-{}", std::process::id(), uniq)).join("media")
869                })
870        });
871        let trace_scope = opts.trace_scope.clone();
872        let serving = Serving::start(
873            host,
874            std::sync::Arc::from(codec),
875            Ground {
876                store: &store,
877                cancels: &cancels,
878                active: &active,
879                stats: &stats,
880                background_tick_divisor: &background_tick_divisor,
881            },
882            opts,
883        )?;
884        Ok(Daemon {
885            trace_scope,
886            serving: std::sync::RwLock::new(std::sync::Arc::new(serving)),
887            reloading: std::sync::Mutex::new(()),
888            reload_hook: std::sync::OnceLock::new(),
889            runtime_id,
890            store,
891            cancels,
892            active,
893            purging,
894            stats,
895            background_tick_divisor,
896            park_dir,
897            tool_lease_ms,
898            files: files::FileStore::with_limits(
899                files::files_dir_for(media_dir.parent().unwrap_or(media_dir.as_path())),
900                files_max_bytes,
901                files_expiry_secs,
902            ),
903            batches: std::sync::Arc::new(batches::BatchStore::new(batches::batches_dir_for(
904                media_dir.parent().unwrap_or(media_dir.as_path()),
905            ))),
906            responses: response_store::ResponseStore::new(response_store::responses_dir_for(
907                media_dir.parent().unwrap_or(media_dir.as_path()),
908            )),
909            media: media::MediaPool::new(media_dir),
910            completion: std::sync::Arc::new(CompletionSatellite {
911                store: std::sync::Mutex::new(SessionStore::ephemeral()),
912                cache: std::sync::Mutex::new(CompletionCache::default()),
913                deadline_ms: completion_deadline_ms,
914                cache_cap: 256,
915                bucket: completion_bucket,
916                counters: CompletionCounters::default(),
917            }),
918            fim_satellite: std::sync::OnceLock::new(),
919            built_with,
920        })
921    }
922
923    fn serving(&self) -> std::sync::Arc<Serving> {
924        std::sync::Arc::clone(&self.serving.read().expect("serving"))
925    }
926
927    fn codec(&self) -> std::sync::Arc<dyn TextCodec + Send + Sync> {
928        std::sync::Arc::clone(&self.serving().codec)
929    }
930
931    fn sched_tx(&self) -> std::sync::mpsc::Sender<scheduler::SchedCmd> {
932        self.serving().sched.tx.clone()
933    }
934
935    pub fn reload_with(
936        &self,
937        build: impl FnOnce() -> Result<(EngineHost, Box<dyn TextCodec + Send + Sync>), DaemonError>,
938    ) -> Result<(), DaemonError> {
939        let _one_at_a_time = self.reloading.lock().expect("reloading");
940        self.shutdown();
941        {
942            let mut cancels = self.cancels.lock().expect("cancel registry");
943            let active = self.active.lock().expect("active set");
944            cancels.retain(|session| active.contains(session));
945        }
946        let started = build().and_then(|(host, codec)| {
947            Serving::start(
948                host,
949                std::sync::Arc::from(codec),
950                Ground {
951                    store: &self.store,
952                    cancels: &self.cancels,
953                    active: &self.active,
954                    stats: &self.stats,
955                    background_tick_divisor: &self.background_tick_divisor,
956                },
957                self.built_with.clone(),
958            )
959        });
960        match started {
961            Ok(serving) => {
962                *self.serving.write().expect("serving") = std::sync::Arc::new(serving);
963                Ok(())
964            }
965            Err(e) => {
966                let stopped = self.serving();
967                let mut why = stopped.retired.lock().expect("retired");
968                if why.is_none() {
969                    *why = Some(format!("the model's engine was stopped to load it again, which failed ({e}); it loads again on its next request, so retry"));
970                }
971                Err(e)
972            }
973        }
974    }
975
976    pub fn on_retired(&self, reload: Box<dyn Fn() + Send + Sync>) {
977        let _ = self.reload_hook.set(reload);
978    }
979
980    fn ensure_serving(&self) -> Result<(), DaemonError> {
981        if self.needs_reload().is_none() {
982            return Ok(());
983        }
984        if let Some(reload) = self.reload_hook.get() {
985            reload();
986        }
987        match self.needs_reload() {
988            None => Ok(()),
989            Some(why) => Err(DaemonError::Generation(why)),
990        }
991    }
992
993    pub fn max_stream_tokens(&self) -> u64 {
994        let serving = self.serving();
995        let record = serving.capabilities.read().expect("capability record");
996        runtime::stream_ceiling(serving.staged_tokens, &record)
997    }
998
999    fn check_stream_fits(&self, len: usize) -> Result<(), DaemonError> {
1000        let len = len as u64;
1001        let max = self.max_stream_tokens();
1002        if max > 0 && len > max {
1003            return Err(DaemonError::StreamTooLong { len, max });
1004        }
1005        Ok(())
1006    }
1007
1008    pub fn session_fits(&self, session: u64, thinking: Option<bool>) -> Result<(), DaemonError> {
1009        self.ensure_serving()?;
1010        let len = {
1011            let store = self.store.lock().expect("store");
1012            let s = store.session(session)?;
1013            s.tokens.len() + self.generation_opener(&s.events, thinking).map_or(0, |op| op.len())
1014        };
1015        self.check_stream_fits(len)
1016    }
1017
1018    pub fn fim_fits(&self, req: &FimRequest) -> Result<(), DaemonError> {
1019        let d = match self.fim_satellite.get() {
1020            Some(sat) => &sat.daemon,
1021            None => self,
1022        };
1023        d.ensure_serving()?;
1024        let fim = d
1025            .codec()
1026            .render_fim(&req.prefix, &req.suffix, req.mode)
1027            .ok_or(DaemonError::NoFimDialect)?;
1028        d.check_stream_fits(fim.len())
1029    }
1030
1031    pub fn complete(
1032        &self,
1033        prefix: &str,
1034        suffix: &str,
1035        mode: u8,
1036        max_tokens: u32,
1037    ) -> Result<Completion, DaemonError> {
1038        self.complete_streaming(&FimRequest::greedy(prefix, suffix, mode, max_tokens), |_, _| {
1039            std::ops::ControlFlow::Continue(())
1040        })
1041    }
1042
1043    pub fn complete_streaming(
1044        &self,
1045        req: &FimRequest,
1046        on_text: impl FnMut(&str, usize) -> std::ops::ControlFlow<()>,
1047    ) -> Result<Completion, DaemonError> {
1048        let result = match self.fim_satellite.get() {
1049            Some(sat) => sat.daemon.complete_here(req, on_text),
1050            None => self.complete_here(req, on_text),
1051        };
1052        if let Ok(c) = &result {
1053            use std::sync::atomic::Ordering::Relaxed;
1054            let n = &self.completion.counters;
1055            if c.expired {
1056                n.expired.fetch_add(1, Relaxed);
1057            } else if c.cached {
1058                n.cached.fetch_add(1, Relaxed);
1059            } else {
1060                n.generated.fetch_add(1, Relaxed);
1061            }
1062        }
1063        result
1064    }
1065
1066    pub fn attach_fim_satellite(
1067        &self,
1068        name: impl Into<String>,
1069        satellite: std::sync::Arc<Daemon>,
1070    ) -> Result<(), DaemonError> {
1071        if satellite.codec().render_fim("", "", codec::fim_mode::PSM).is_none() {
1072            return Err(DaemonError::NoFimDialect);
1073        }
1074        if satellite.fim_satellite.get().is_some() {
1075            return Err(DaemonError::Config("a FIM satellite cannot itself route to another"));
1076        }
1077        self.fim_satellite
1078            .set(FimSatellite {
1079                name: name.into(),
1080                daemon: satellite,
1081            })
1082            .map_err(|_| DaemonError::Config("a FIM satellite is already attached"))
1083    }
1084
1085    pub fn fim_satellite_name(&self) -> Option<String> {
1086        self.fim_satellite.get().map(|s| s.name.clone())
1087    }
1088
1089    pub fn fim_satellite(&self) -> Option<std::sync::Arc<Daemon>> {
1090        self.fim_satellite.get().map(|s| std::sync::Arc::clone(&s.daemon))
1091    }
1092
1093    pub fn fim_supported(&self) -> bool {
1094        self.fim_satellite.get().is_some()
1095            || self.codec().render_fim("", "", codec::fim_mode::PSM).is_some()
1096    }
1097
1098    pub fn completion_bucket(&self) -> completion_bucket::BucketConfig {
1099        self.completion.bucket
1100    }
1101
1102    pub fn note_completion_throttled(&self) {
1103        self.completion
1104            .counters
1105            .throttled
1106            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1107    }
1108
1109    fn complete_here(
1110        &self,
1111        req: &FimRequest,
1112        mut on_text: impl FnMut(&str, usize) -> std::ops::ControlFlow<()>,
1113    ) -> Result<Completion, DaemonError> {
1114        self.ensure_serving()?;
1115        let _span = self.raw_generate_span(req.max_tokens);
1116        let submitted = std::time::Instant::now();
1117        let fim = self
1118            .codec()
1119            .render_fim(&req.prefix, &req.suffix, req.mode)
1120            .ok_or(DaemonError::NoFimDialect)?;
1121        self.check_stream_fits(fim.len())?;
1122        let prompt_tokens = fim.len() as u32;
1123        let expired = || Completion {
1124            text: String::new(),
1125            tokens: Vec::new(),
1126            cached: false,
1127            expired: true,
1128            finish: superfluid_abi::finish::NONE,
1129            prompt_tokens,
1130        };
1131        let cacheable = req.params.temperature <= 0.0;
1132        let key = {
1133            let mut h = superfluid_fingerprint::RecordHasher::new();
1134            for t in &fim {
1135                h.field_u32(1, *t);
1136            }
1137            h.field_u32(2, req.mode as u32);
1138            h.field_u32(3, req.max_tokens);
1139            if let Some(fp) = self.codec().behavior_fingerprint() {
1140                h.field_u64(4, fp.digest());
1141            }
1142            let x = &req.extras;
1143            h.field_u32(5, x.presence_penalty.to_bits());
1144            h.field_u32(6, x.frequency_penalty.to_bits());
1145            h.field_u32(7, x.repeat_penalty.to_bits());
1146            h.field_u32(8, x.ignore_eos as u32);
1147            h.finish()
1148        };
1149        if cacheable {
1150            if let Some((tokens, text, finish)) = self.completion.cache_get(key) {
1151                let _ = on_text(&text, tokens.len());
1152                return Ok(Completion {
1153                    text,
1154                    tokens,
1155                    cached: true,
1156                    expired: false,
1157                    finish,
1158                    prompt_tokens,
1159                });
1160            }
1161        }
1162        if self.completion.deadline_ms > 0
1163            && submitted.elapsed().as_millis() as u64 > self.completion.deadline_ms
1164        {
1165            return Ok(expired());
1166        }
1167        let session = {
1168            let mut store = self.completion.store.lock().expect("completion store");
1169            let s = store.create(None, req.params)?;
1170            store.set_qos(s, qos::INLINE_COMPLETION, false)?;
1171            store.append(s, None, fim.clone())?;
1172            s
1173        };
1174        let (stream, params) = {
1175            let store = self.completion.store.lock().expect("completion store");
1176            let st = store.session(session)?;
1177            (st.tokens.clone(), st.params)
1178        };
1179        let mut produced: Vec<u32> = Vec::new();
1180        let mut emitted = 0usize;
1181        let result = scheduler::run_completion_streaming(
1182            &self.sched_tx(),
1183            stream,
1184            params,
1185            req.max_tokens,
1186            self.completion.deadline_ms,
1187            scheduler::GenExtras {
1188                grammar_handle: 0,
1189                logit_bias_handle: 0,
1190                want_logprobs: false,
1191                ..req.extras.clone()
1192            },
1193            |toks| {
1194                produced.extend_from_slice(toks);
1195                let text = self.codec().decode(&produced);
1196                let stable = text.trim_end_matches('\u{FFFD}');
1197                if stable.len() > emitted && stable.is_char_boundary(emitted) {
1198                    let piece = &stable[emitted..];
1199                    emitted = stable.len();
1200                    return on_text(piece, produced.len());
1201                }
1202                std::ops::ControlFlow::Continue(())
1203            },
1204        );
1205        {
1206            let mut store = self.completion.store.lock().expect("completion store");
1207            let _ = store.drop_session(session);
1208        }
1209        let (tokens, finish, was_expired) = result?;
1210        if was_expired {
1211            return Ok(expired());
1212        }
1213        let text = self.codec().decode(&tokens);
1214        if text.len() > emitted && text.is_char_boundary(emitted) {
1215            let _ = on_text(&text[emitted..], tokens.len());
1216        }
1217        if cacheable && finish != superfluid_abi::finish::CANCELLED {
1218            self.completion.cache_put(key, tokens.clone(), text.clone(), finish);
1219        }
1220        Ok(Completion {
1221            text,
1222            tokens,
1223            cached: false,
1224            expired: false,
1225            finish,
1226            prompt_tokens,
1227        })
1228    }
1229
1230    pub fn generate_tokens(
1231        &self,
1232        stream: Vec<u32>,
1233        params: GenParams,
1234        max_tokens: u32,
1235        deadline_ms: u64,
1236    ) -> Result<(Vec<u32>, bool), DaemonError> {
1237        self.ensure_serving()?;
1238        let _span = self.raw_generate_span(max_tokens);
1239        self.check_stream_fits(stream.len())?;
1240        let deadline = self.effective_deadline(deadline_ms);
1241        let (tokens, _finish, expired) =
1242            scheduler::run_completion(&self.sched_tx(), stream, params, max_tokens, deadline, 0)?;
1243        Ok((tokens, expired))
1244    }
1245
1246    pub fn generate_tokens_streaming(
1247        &self,
1248        stream: Vec<u32>,
1249        params: GenParams,
1250        max_tokens: u32,
1251        deadline_ms: u64,
1252        extras: scheduler::GenExtras,
1253        on_tokens: impl FnMut(&[u32]) -> std::ops::ControlFlow<()>,
1254    ) -> Result<(Vec<u32>, u32, bool), DaemonError> {
1255        self.ensure_serving()?;
1256        let _span = self.raw_generate_span(max_tokens);
1257        self.check_stream_fits(stream.len())?;
1258        let deadline = self.effective_deadline(deadline_ms);
1259        scheduler::run_completion_streaming(
1260            &self.sched_tx(),
1261            stream,
1262            params,
1263            max_tokens,
1264            deadline,
1265            extras,
1266            on_tokens,
1267        )
1268    }
1269
1270    pub fn forward_stage(
1271        &self,
1272        tokens: Vec<u32>,
1273        start_layer: u32,
1274        end_layer: u32,
1275        hidden_in: Option<Vec<u8>>,
1276    ) -> Result<superfluid_engine::StageOutput, DaemonError> {
1277        self.ensure_serving()?;
1278        match self.engine_sync(proto_linkw::StateSyncReqMsg::ForwardStage {
1279            tokens,
1280            start_layer,
1281            end_layer,
1282            hidden_in,
1283        })? {
1284            proto_linkw::StateSyncOkMsg::ForwardStage {
1285                hidden: Some(h),
1286                token: None,
1287            } => Ok(superfluid_engine::StageOutput::Hidden(h)),
1288            proto_linkw::StateSyncOkMsg::ForwardStage {
1289                hidden: None,
1290                token: Some(t),
1291            } => Ok(superfluid_engine::StageOutput::Token(t)),
1292            _ => Err(DaemonError::Protocol("mismatched ForwardStage reply")),
1293        }
1294    }
1295
1296    fn effective_deadline(&self, deadline_ms: u64) -> u64 {
1297        if deadline_ms == 0 {
1298            self.completion.deadline_ms
1299        } else if self.completion.deadline_ms == 0 {
1300            deadline_ms
1301        } else {
1302            deadline_ms.min(self.completion.deadline_ms)
1303        }
1304    }
1305
1306    pub fn generate_tokens_constrained(
1307        &self,
1308        stream: Vec<u32>,
1309        params: GenParams,
1310        max_tokens: u32,
1311        deadline_ms: u64,
1312        json_schema: &str,
1313    ) -> Result<(Vec<u32>, bool), DaemonError> {
1314        self.ensure_serving()?;
1315        let _span = self.raw_generate_span(max_tokens);
1316        let handle = match self.engine_sync(proto_linkw::StateSyncReqMsg::CreateGrammar {
1317            json_schema: json_schema.to_string(),
1318        })? {
1319            proto_linkw::StateSyncOkMsg::CreateGrammar { grammar_handle } => grammar_handle,
1320            _ => return Err(DaemonError::Protocol("mismatched CreateGrammar reply")),
1321        };
1322        let deadline = self.effective_deadline(deadline_ms);
1323        let result =
1324            scheduler::run_completion(&self.sched_tx(), stream, params, max_tokens, deadline, handle);
1325        let _ = self.engine_sync(proto_linkw::StateSyncReqMsg::FreeGrammar {
1326            grammar_handle: handle,
1327        });
1328        let (tokens, finish, expired) = result?;
1329        if expired {
1330            return Err(DaemonError::Generation(format!(
1331                "constrained generation hit the wall deadline with the grammar incomplete                  ({} tokens)",
1332                tokens.len()
1333            )));
1334        }
1335        if finish != superfluid_abi::finish::GRAMMAR {
1336            return Err(DaemonError::Generation(format!(
1337                "constrained generation did not complete the grammar (finish={finish},                  {} tokens); raise max_tokens",
1338                tokens.len()
1339            )));
1340        }
1341        Ok((tokens, false))
1342    }
1343
1344    pub fn put_media(&self, bytes: &[u8]) -> Result<String, DaemonError> {
1345        Ok(self.media.put(bytes)?)
1346    }
1347
1348    pub fn media_pool(&self) -> &media::MediaPool {
1349        &self.media
1350    }
1351
1352    fn engine_sync(&self, req: proto_linkw::StateSyncReqMsg) -> Result<proto_linkw::StateSyncOkMsg, DaemonError> {
1353        self.engine_sync_as(req, None)
1354    }
1355
1356    fn engine_sync_as(
1357        &self,
1358        req: proto_linkw::StateSyncReqMsg,
1359        class: Option<u8>,
1360    ) -> Result<proto_linkw::StateSyncOkMsg, DaemonError> {
1361        let class = class.or_else(ambient_sync_class);
1362        let (tx, rx) = std::sync::mpsc::channel();
1363        let cmd = scheduler::SchedCmd::Sync(req, tx);
1364        self.sched_tx()
1365            .send(match class {
1366                Some(c) => scheduler::SchedCmd::SyncAs(c, Box::new(cmd)),
1367                None => cmd,
1368            })
1369            .map_err(|_| DaemonError::Protocol("scheduler is gone"))?;
1370        rx.recv()
1371            .map_err(|_| DaemonError::Protocol("scheduler dropped the query"))?
1372            .map_err(DaemonError::Generation)
1373    }
1374
1375    pub fn append_image(
1376        &self,
1377        session: u64,
1378        role: u32,
1379        blob: &str,
1380        pre_text: &str,
1381        post_text: &str,
1382    ) -> Result<CommittedEvent, DaemonError> {
1383        if self.active.lock().expect("active set").contains(&session) {
1384            return Err(DaemonError::SessionBusy(session));
1385        }
1386        if !matches!(role, wal::role::SYSTEM | wal::role::USER | wal::role::ASSISTANT) {
1387            return Err(DaemonError::UnknownRole(role));
1388        }
1389        if !self.media.exists(blob) {
1390            return Err(DaemonError::UnknownMedia(blob.to_string()));
1391        }
1392        let path = self.media.path_of(blob);
1393        let info = match self.engine_sync(proto_linkw::StateSyncReqMsg::MediaProbe {
1394            image_path: path.to_string_lossy().into_owned(),
1395        })? {
1396            proto_linkw::StateSyncOkMsg::MediaProbe { info } => info,
1397            _ => return Err(DaemonError::Protocol("mismatched MediaProbe reply")),
1398        };
1399        if info.n_tokens == 0 || info.n_tokens as u64 > scheduler::PREFILL_BUDGET as u64 {
1400            return Err(DaemonError::MediaTooLarge(info.n_tokens));
1401        }
1402        let mut run = Vec::with_capacity(info.n_tokens as usize + 2);
1403        if info.boi_token_id != 0 {
1404            run.push(info.boi_token_id);
1405        }
1406        run.extend(std::iter::repeat_n(info.image_token_id, info.n_tokens as usize));
1407        if info.eoi_token_id != 0 {
1408            run.push(info.eoi_token_id);
1409        }
1410        let mut span = self.separator_for(session);
1411        let turn = self
1412            .codec()
1413            .render_turn_with_tokens(role, pre_text, &run, post_text)
1414            .ok_or(DaemonError::Protocol("codec has no chat dialect"))?;
1415        span.extend(turn);
1416        let first_in_span = span
1417            .iter()
1418            .position(|&t| t == info.image_token_id)
1419            .ok_or(DaemonError::Protocol("codec dropped the placeholder run"))?;
1420        let mut store = self.store.lock().expect("store");
1421        let stream_len = store.session(session)?.tokens.len() as u64;
1422        let payload = serde_json::json!({
1423            "blob": blob,
1424            "n_tokens": info.n_tokens,
1425            "image_token_id": info.image_token_id,
1426            "offset": stream_len + first_in_span as u64,
1427            "pre_text": pre_text,
1428            "post_text": post_text,
1429            "media_identity": {
1430                "preprocessing_fp": format!("{:016x}", info.preprocess_fp),
1431                "encoder": "bundle-weights-identity",
1432            },
1433        })
1434        .to_string();
1435        store.commit_block(
1436            session,
1437            role,
1438            wal::block_kind::IMAGE,
1439            payload,
1440            span,
1441            codec::MODEL_VISIBILITY_VERSION,
1442        )
1443    }
1444
1445    fn media_of(&self, s: &store::SessionState) -> Vec<(u64, std::path::PathBuf, u32)> {
1446        let mut v = Vec::new();
1447        for e in &s.events {
1448            if let EventBody::Block {
1449                kind, payload, ..
1450            } = &e.body
1451            {
1452                if *kind != wal::block_kind::IMAGE {
1453                    continue;
1454                }
1455                let p: serde_json::Value = serde_json::from_str(payload).unwrap_or_default();
1456                let blob = p.get("blob").and_then(|b| b.as_str()).unwrap_or("");
1457                let off = p.get("offset").and_then(|o| o.as_u64()).unwrap_or(0);
1458                let n = p.get("n_tokens").and_then(|o| o.as_u64()).unwrap_or(0) as u32;
1459                if !blob.is_empty() && n > 0 {
1460                    v.push((off, self.media.path_of(blob), n));
1461                }
1462            }
1463        }
1464        v
1465    }
1466
1467    pub fn gc_media(&self) -> Result<(usize, usize), DaemonError> {
1468        let store = self.store.lock().expect("store");
1469        let mut live = std::collections::HashSet::new();
1470        for id in store.session_ids() {
1471            let Ok(s) = store.session(id) else { continue };
1472            for e in &s.events {
1473                if let EventBody::Block { kind, payload, .. } = &e.body {
1474                    if *kind == wal::block_kind::IMAGE {
1475                        let p: serde_json::Value = serde_json::from_str(payload).unwrap_or_default();
1476                        if let Some(b) = p.get("blob").and_then(|b| b.as_str()) {
1477                            live.insert(b.to_string());
1478                        }
1479                    }
1480                }
1481            }
1482        }
1483        Ok(self.media.sweep(&live)?)
1484    }
1485
1486    pub fn speculation(&self) -> Option<scheduler::SpeculationGrant> {
1487        self.serving().speculation.grant()
1488    }
1489
1490    pub fn tool_lease_ms(&self) -> u64 {
1491        self.tool_lease_ms
1492    }
1493
1494    pub fn inspect(&self, session: u64) -> Result<Inspection, DaemonError> {
1495        // The active set before the store, in the order a generation takes them.
1496        let resident = self.active.lock().expect("active set").contains(&session);
1497        let store = self.store.lock().expect("store");
1498        let s = store.session(session)?;
1499        let tier = if resident {
1500            Tier::Resident
1501        } else {
1502            match self
1503                .park_dir
1504                .as_deref()
1505                .and_then(|d| park::read(d, session))
1506            {
1507                Some(p) => Tier::Parked {
1508                    covered: p.covered,
1509                    encoding: p.encoding,
1510                },
1511                None => Tier::Cold,
1512            }
1513        };
1514        Ok(Inspection {
1515            summary: SessionSummary {
1516                id: s.id,
1517                parent: if s.rerooted { None } else { s.parent },
1518                fork_at: s.base,
1519                title: s.title.clone(),
1520                archived: s.archived,
1521                meta_version: s.meta_version,
1522                generation: s.generation,
1523                events: s.events.len() as u64,
1524                tokens: s.tokens.len() as u64,
1525                open_tool_calls: s.open_tool_calls.len() as u64,
1526            },
1527            params: s.params,
1528            epoch: s.epoch,
1529            last_finish: s.last_finish,
1530            qos_class: s.qos_class,
1531            batch_invariant: s.batch_invariant,
1532            pin_deadline_unix_ms: s.pin_deadline_unix_ms,
1533            behavior_fingerprint: s.behavior_fp,
1534            open_tool_calls: s
1535                .open_tool_calls
1536                .iter()
1537                .map(|(id, (n, a))| (*id, n.clone(), a.clone()))
1538                .collect(),
1539            tier,
1540            tokens: s.tokens.clone(),
1541            events: s.events.iter().cloned().map(Into::into).collect(),
1542            wal_version: store.wal_version(),
1543            store_id: Some(store.store_id()),
1544        })
1545    }
1546
1547    pub fn files(&self) -> &files::FileStore {
1548        &self.files
1549    }
1550
1551    pub fn batches(&self) -> std::sync::Arc<batches::BatchStore> {
1552        std::sync::Arc::clone(&self.batches)
1553    }
1554
1555    pub fn responses(&self) -> &response_store::ResponseStore {
1556        &self.responses
1557    }
1558
1559    pub fn tokenize(&self, text: &str) -> Vec<u32> {
1560        self.codec().encode(text)
1561    }
1562
1563    pub fn grammar_create(&self, json_schema: &str) -> Result<u32, DaemonError> {
1564        match self.engine_sync(proto_linkw::StateSyncReqMsg::CreateGrammar {
1565            json_schema: json_schema.to_string(),
1566        })? {
1567            proto_linkw::StateSyncOkMsg::CreateGrammar { grammar_handle } => Ok(grammar_handle),
1568            _ => Err(DaemonError::Protocol("mismatched CreateGrammar reply")),
1569        }
1570    }
1571
1572    pub fn grammar_free(&self, grammar_handle: u32) {
1573        let _ = self.engine_sync(proto_linkw::StateSyncReqMsg::FreeGrammar { grammar_handle });
1574    }
1575
1576    pub fn logit_bias_create(&self, tokens: &[i32], values: &[f32]) -> Result<u32, DaemonError> {
1577        match self.engine_sync(proto_linkw::StateSyncReqMsg::CreateLogitBias {
1578            tokens: tokens.to_vec(),
1579            values: values.iter().map(|v| v.to_bits()).collect(),
1580        })? {
1581            proto_linkw::StateSyncOkMsg::CreateLogitBias { handle } => Ok(handle),
1582            _ => Err(DaemonError::Protocol("mismatched CreateLogitBias reply")),
1583        }
1584    }
1585    pub fn logit_bias_free(&self, handle: u32) {
1586        let _ = self.engine_sync(proto_linkw::StateSyncReqMsg::FreeLogitBias { handle });
1587    }
1588
1589    pub fn lora_load(&self, adapter_path: &str) -> Result<Option<String>, DaemonError> {
1590        self.lora_reply(proto_linkw::StateSyncReqMsg::LoraLoad {
1591            adapter_path: adapter_path.to_string(),
1592        })
1593    }
1594
1595    pub fn lora_unload(&self) -> Result<Option<String>, DaemonError> {
1596        self.lora_reply(proto_linkw::StateSyncReqMsg::LoraUnload)
1597    }
1598
1599    pub fn lora_id(&self) -> Result<Option<String>, DaemonError> {
1600        self.lora_reply(proto_linkw::StateSyncReqMsg::LoraId)
1601    }
1602
1603    fn lora_reply(&self, req: proto_linkw::StateSyncReqMsg) -> Result<Option<String>, DaemonError> {
1604        match self.engine_sync(req)? {
1605            proto_linkw::StateSyncOkMsg::LoraId { id } => Ok(id),
1606            _ => Err(DaemonError::Protocol("mismatched LoraId reply")),
1607        }
1608    }
1609
1610    pub fn try_model_sampling_defaults(&self) -> Option<ModelSamplingDefaults> {
1611        let serving = self.serving();
1612        if let Some(cached) = serving.model_sampling.get() {
1613            return Some(cached.clone());
1614        }
1615        let descriptor = self.capability_descriptor()?;
1616        let resolved = descriptor
1617            .get("sampling_defaults")
1618            .map(ModelSamplingDefaults::from_json)
1619            .unwrap_or_default();
1620        let _ = serving.model_sampling.set(resolved.clone());
1621        Some(resolved)
1622    }
1623
1624    pub fn model_sampling_defaults(&self) -> ModelSamplingDefaults {
1625        self.try_model_sampling_defaults().unwrap_or_default()
1626    }
1627
1628    pub fn capability_descriptor(&self) -> Option<serde_json::Value> {
1629        self.serving().capabilities.read().expect("capability record").with_runtime(self.runtime_id)
1630    }
1631
1632    pub fn runtime_id(&self) -> Option<&'static str> {
1633        self.runtime_id
1634    }
1635
1636    pub fn needs_reload(&self) -> Option<String> {
1637        self.serving().retired.lock().expect("retired").clone()
1638    }
1639
1640    pub fn capabilities(&self) -> capabilities::Capabilities {
1641        self.serving().capabilities.read().expect("capability record").clone()
1642    }
1643
1644    pub fn require(&self, section: &str, key: &str, code: &'static str, param: Option<&str>, what: &str) -> Result<(), DaemonError> {
1645        let why = match self.serving().capabilities.read().expect("capability record").get(section, key) {
1646            capabilities::Cap::No(why) => why.to_string(),
1647            capabilities::Cap::Yes | capabilities::Cap::Unknown => return Ok(()),
1648        };
1649        Err(DaemonError::Unsupported(Refusal::new(code, param, format!("{} {what}: {why}", self.described()))))
1650    }
1651
1652    fn described(&self) -> String {
1653        let model = &self.trace_scope.model;
1654        let runtime = self.capability_descriptor().and_then(|r| r["runtime"]["id"].as_str().map(str::to_string));
1655        match (model.is_empty(), runtime) {
1656            (false, Some(r)) => format!("model '{model}' on runtime {r}"),
1657            (false, None) => format!("model '{model}'"),
1658            (true, Some(r)) => format!("this model (runtime {r})"),
1659            (true, None) => "this model".to_string(),
1660        }
1661    }
1662
1663    pub fn chat_template(&self) -> Option<String> {
1664        self.codec().chat_template_source()
1665    }
1666
1667    pub fn transcribe(
1668        &self,
1669        audio_path: &str,
1670        params: &superfluid_engine::TranscribeParams,
1671    ) -> Result<superfluid_engine::Transcription, DaemonError> {
1672        self.transcribe_as(audio_path, params, None)
1673    }
1674
1675    pub fn transcribe_as(
1676        &self,
1677        audio_path: &str,
1678        params: &superfluid_engine::TranscribeParams,
1679        class: Option<u8>,
1680    ) -> Result<superfluid_engine::Transcription, DaemonError> {
1681        self.ensure_serving()?;
1682        self.require_transcription(params)?;
1683        Self::decode_transcription(self.engine_sync_as(Self::transcribe_req(audio_path, params, false), class)?)
1684    }
1685
1686    fn require_transcription(&self, params: &superfluid_engine::TranscribeParams) -> Result<(), DaemonError> {
1687        if params.translate {
1688            self.require("modalities", "whisper_translate", "unsupported_model", Some("model"), "does not translate speech")
1689        } else {
1690            self.require("modalities", "whisper_transcribe", "unsupported_model", Some("model"), "does not transcribe speech")
1691        }
1692    }
1693
1694    #[allow(clippy::type_complexity)]
1695    pub fn transcribe_streaming(
1696        &self,
1697        audio_path: &str,
1698        params: &superfluid_engine::TranscribeParams,
1699    ) -> Result<
1700        (
1701            std::sync::mpsc::Receiver<proto_linkw::TranscribeSegmentMsg>,
1702            std::sync::mpsc::Receiver<Result<superfluid_engine::Transcription, DaemonError>>,
1703        ),
1704        DaemonError,
1705    > {
1706        self.transcribe_streaming_as(audio_path, params, None)
1707    }
1708
1709    #[allow(clippy::type_complexity)]
1710    pub fn transcribe_streaming_as(
1711        &self,
1712        audio_path: &str,
1713        params: &superfluid_engine::TranscribeParams,
1714        class: Option<u8>,
1715    ) -> Result<
1716        (
1717            std::sync::mpsc::Receiver<proto_linkw::TranscribeSegmentMsg>,
1718            std::sync::mpsc::Receiver<Result<superfluid_engine::Transcription, DaemonError>>,
1719        ),
1720        DaemonError,
1721    > {
1722        self.ensure_serving()?;
1723        self.require_transcription(params)?;
1724        let (seg_tx, seg_rx) = std::sync::mpsc::channel();
1725        let (res_tx, res_rx) = std::sync::mpsc::channel();
1726        let (raw_tx, raw_rx) = std::sync::mpsc::channel();
1727        self.sched_tx()
1728            .send({
1729                let cmd = scheduler::SchedCmd::SyncStream(Self::transcribe_req(audio_path, params, true), seg_tx, raw_tx);
1730                match class.or_else(ambient_sync_class) {
1731                    Some(c) => scheduler::SchedCmd::SyncAs(c, Box::new(cmd)),
1732                    None => cmd,
1733                }
1734            })
1735            .map_err(|_| DaemonError::Protocol("scheduler is gone"))?;
1736        std::thread::spawn(move || {
1737            let out = match raw_rx.recv() {
1738                Ok(Ok(ok)) => Self::decode_transcription(ok),
1739                Ok(Err(e)) => Err(DaemonError::Generation(e)),
1740                Err(_) => Err(DaemonError::Protocol("scheduler dropped the query")),
1741            };
1742            let _ = res_tx.send(out);
1743        });
1744        Ok((seg_rx, res_rx))
1745    }
1746
1747    fn transcribe_req(
1748        audio_path: &str,
1749        params: &superfluid_engine::TranscribeParams,
1750        stream: bool,
1751    ) -> proto_linkw::StateSyncReqMsg {
1752        proto_linkw::StateSyncReqMsg::Transcribe {
1753            audio_path: audio_path.to_string(),
1754            language: params.language.clone(),
1755            translate: params.translate,
1756            timestamps: params.timestamps,
1757            prompt: params.prompt.clone(),
1758            stream,
1759        }
1760    }
1761
1762    fn decode_transcription(
1763        ok: proto_linkw::StateSyncOkMsg,
1764    ) -> Result<superfluid_engine::Transcription, DaemonError> {
1765        match ok {
1766            proto_linkw::StateSyncOkMsg::Transcribe { text, language, duration_ms, segments } => {
1767                Ok(superfluid_engine::Transcription {
1768                    text,
1769                    language,
1770                    duration_ms,
1771                    segments: segments
1772                        .into_iter()
1773                        .map(|s| superfluid_engine::TranscriptSegment {
1774                            start_ms: s.start_ms,
1775                            end_ms: s.end_ms,
1776                            text: s.text,
1777                            avg_logprob: f32::from_bits(s.avg_logprob_bits),
1778                            no_speech_prob: f32::from_bits(s.no_speech_prob_bits),
1779                            compression_ratio: f32::from_bits(s.compression_ratio_bits),
1780                            temperature: f32::from_bits(s.temperature_bits),
1781                        })
1782                        .collect(),
1783                })
1784            }
1785            _ => Err(DaemonError::Protocol("engine returned a non-Transcribe reply")),
1786        }
1787    }
1788
1789    pub fn embed(&self, text: &str) -> Result<Vec<f32>, DaemonError> {
1790        self.embed_as(text, None)
1791    }
1792
1793    pub fn embed_as(&self, text: &str, class: Option<u8>) -> Result<Vec<f32>, DaemonError> {
1794        self.ensure_serving()?;
1795        self.require("workload", "embedding", "unsupported_model", Some("model"), "serves no embeddings")?;
1796        self.embed_tokens_as(self.codec().encode(text), class)
1797    }
1798
1799    pub(crate) fn embed_tokens_as(&self, tokens: Vec<u32>, class: Option<u8>) -> Result<Vec<f32>, DaemonError> {
1800        self.ensure_serving()?;
1801        self.require("workload", "embedding", "unsupported_model", Some("model"), "serves no embeddings")?;
1802        match self.engine_sync_as(proto_linkw::StateSyncReqMsg::Embed { tokens }, class)? {
1803            proto_linkw::StateSyncOkMsg::Embed { embedding } => Ok(embedding
1804                .as_chunks::<4>()
1805                .0
1806                .iter()
1807                .map(|b| f32::from_le_bytes(*b))
1808                .collect()),
1809            _ => Err(DaemonError::Protocol("engine returned a non-Embed reply")),
1810        }
1811    }
1812
1813    pub fn detokenize(&self, tokens: &[u32]) -> String {
1814        self.codec().decode(tokens)
1815    }
1816
1817    fn raw_generate_span(&self, max_tokens: u32) -> tracing::span::EnteredSpan {
1818        tracing::info_span!(
1819            parent: None,
1820            "generate",
1821            max_tokens,
1822            raw = true,
1823            model = self.trace_scope.model.as_str(),
1824            store_ns = self.trace_scope.ns
1825        )
1826        .entered()
1827    }
1828
1829    pub fn metrics_text(&self) -> String {
1830        let mut out = metrics::render_prometheus(&self.stats);
1831        out.push_str(&format!(
1832            "# HELP superfluid_telemetry_dropped_total Telemetry records dropped by the bounded sinks (log lines and OTLP spans).\n# TYPE superfluid_telemetry_dropped_total counter\nsuperfluid_telemetry_dropped_total {}\n",
1833            telemetry::dropped_total()
1834        ));
1835        let g = |a: &std::sync::atomic::AtomicU64| a.load(std::sync::atomic::Ordering::Relaxed);
1836        out.push_str(&format!(
1837            "# HELP superfluid_otlp_spans_exported_total Operational spans accepted by the OTLP collector.\n# TYPE superfluid_otlp_spans_exported_total counter\nsuperfluid_otlp_spans_exported_total {}\n",
1838            g(&otlp::EXPORTED)
1839        ));
1840        out.push_str(&format!(
1841            "# HELP superfluid_otlp_spans_dropped_total Operational spans dropped instead of delaying a tick.\n# TYPE superfluid_otlp_spans_dropped_total counter\nsuperfluid_otlp_spans_dropped_total{{reason=\"queue_full\"}} {}\nsuperfluid_otlp_spans_dropped_total{{reason=\"export_failed\"}} {}\nsuperfluid_otlp_spans_dropped_total{{reason=\"rejected\"}} {}\n",
1842            g(&otlp::DROPPED_QUEUE_FULL),
1843            g(&otlp::DROPPED_EXPORT_FAILED),
1844            g(&otlp::DROPPED_REJECTED)
1845        ));
1846        out.push_str(&self.completion_metrics_text());
1847        out
1848    }
1849
1850    fn completion_metrics_text(&self) -> String {
1851        use std::sync::atomic::Ordering::Relaxed;
1852        let n = &self.completion.counters;
1853        let mut out = String::from(
1854            "# HELP superfluid_fim_completions_total FIM completions by outcome.\n\
1855             # TYPE superfluid_fim_completions_total counter\n",
1856        );
1857        for (label, v) in [
1858            ("generated", &n.generated),
1859            ("cached", &n.cached),
1860            ("expired", &n.expired),
1861            ("throttled", &n.throttled),
1862        ] {
1863            out.push_str(&format!(
1864                "superfluid_fim_completions_total{{result=\"{label}\"}} {}\n",
1865                v.load(Relaxed)
1866            ));
1867        }
1868        if let Some(sat) = self.fim_satellite.get() {
1869            let s = &sat.daemon.stats;
1870            let model = sat.name.replace('\\', "\\\\").replace('"', "\\\"").replace('\n', "\\n");
1871            out.push_str(&format!(
1872                "# HELP superfluid_fim_satellite_info The --fim-model satellite serving completions.\n\
1873                 # TYPE superfluid_fim_satellite_info gauge\n\
1874                 superfluid_fim_satellite_info{{model=\"{model}\"}} 1\n\
1875                 # HELP superfluid_fim_satellite_decode_tokens_total Tokens the satellite decoded.\n\
1876                 # TYPE superfluid_fim_satellite_decode_tokens_total counter\n\
1877                 superfluid_fim_satellite_decode_tokens_total {}\n\
1878                 # HELP superfluid_fim_satellite_lanes_active Satellite lanes resident after its last tick.\n\
1879                 # TYPE superfluid_fim_satellite_lanes_active gauge\n\
1880                 superfluid_fim_satellite_lanes_active {}\n\
1881                 # HELP superfluid_fim_satellite_pool_blocks_used Satellite KV paged blocks in use.\n\
1882                 # TYPE superfluid_fim_satellite_pool_blocks_used gauge\n\
1883                 superfluid_fim_satellite_pool_blocks_used {}\n\
1884                 # HELP superfluid_fim_satellite_pool_blocks_total Satellite KV paged blocks total.\n\
1885                 # TYPE superfluid_fim_satellite_pool_blocks_total gauge\n\
1886                 superfluid_fim_satellite_pool_blocks_total {}\n",
1887                s.decode_tokens.load(Relaxed),
1888                s.lanes_active.load(Relaxed),
1889                s.pool_blocks_used.load(Relaxed),
1890                s.pool_blocks_total.load(Relaxed),
1891            ));
1892        }
1893        out
1894    }
1895
1896    pub fn set_background_tick_divisor(&self, divisor: u32) {
1897        self.background_tick_divisor
1898            .store(divisor.max(1), std::sync::atomic::Ordering::Relaxed);
1899    }
1900
1901    pub fn set_qos(
1902        &self,
1903        session: u64,
1904        class: u8,
1905        batch_invariant: bool,
1906    ) -> Result<CommittedEvent, DaemonError> {
1907        self.store
1908            .lock()
1909            .expect("store")
1910            .set_qos(session, class, batch_invariant)
1911    }
1912
1913    pub fn sched_stats(&self) -> std::sync::Arc<scheduler::SchedStats> {
1914        std::sync::Arc::clone(&self.stats)
1915    }
1916
1917    pub fn cancel_registry(&self) -> std::sync::Arc<CancelSet> {
1918        std::sync::Arc::clone(&self.cancels)
1919    }
1920
1921    pub fn active_registry(
1922        &self,
1923    ) -> std::sync::Arc<std::sync::Mutex<std::collections::HashSet<u64>>> {
1924        std::sync::Arc::clone(&self.active)
1925    }
1926
1927    pub fn store(&self) -> std::sync::Arc<std::sync::Mutex<SessionStore>> {
1928        std::sync::Arc::clone(&self.store)
1929    }
1930
1931    pub fn bus(&self) -> std::sync::Arc<bus::SessionBus> {
1932        self.store.lock().expect("store").bus()
1933    }
1934
1935    pub fn shutdown(&self) {
1936        self.serving().sched.shutdown();
1937    }
1938
1939    pub fn signal_shutdown(&self) {
1940        self.serving().sched.signal_shutdown();
1941    }
1942
1943    pub fn create(&self, parent: Option<u64>, params: GenParams) -> Result<u64, DaemonError> {
1944        self.store.lock().expect("store").create(parent, params)
1945    }
1946
1947    pub fn fork(
1948        &self,
1949        parent: u64,
1950        at_event: u64,
1951        params: Option<GenParams>,
1952    ) -> Result<u64, DaemonError> {
1953        self.refuse_if_purging(parent)?;
1954        self.store.lock().expect("store").fork(parent, at_event, params)
1955    }
1956
1957    pub fn tree(&self) -> Vec<(u64, Option<u64>, u64)> {
1958        self.sessions(false)
1959            .into_iter()
1960            .map(|s| (s.id, s.parent, s.fork_at))
1961            .collect()
1962    }
1963
1964    pub fn sessions(&self, include_archived: bool) -> Vec<SessionSummary> {
1965        let store = self.store.lock().expect("store");
1966        store
1967            .session_ids()
1968            .into_iter()
1969            .filter_map(|id| store.session(id).ok())
1970            .filter(|s| include_archived || !s.archived)
1971            .map(|s| SessionSummary {
1972                id: s.id,
1973                parent: if s.rerooted { None } else { s.parent },
1974                fork_at: s.base,
1975                title: s.title.clone(),
1976                archived: s.archived,
1977                meta_version: s.meta_version,
1978                generation: s.generation,
1979                events: s.events.len() as u64,
1980                tokens: s.tokens.len() as u64,
1981                open_tool_calls: s.open_tool_calls.len() as u64,
1982            })
1983            .collect()
1984    }
1985
1986    pub fn set_meta(
1987        &self,
1988        session: u64,
1989        expected_version: u64,
1990        title: Option<String>,
1991        archived: Option<bool>,
1992    ) -> Result<u64, DaemonError> {
1993        self.store
1994            .lock()
1995            .expect("store")
1996            .set_meta(session, expected_version, title, archived)
1997    }
1998
1999    pub fn rebase(
2000        &self,
2001        parent: u64,
2002        edits: Vec<wal::RebaseEdit>,
2003        params: Option<GenParams>,
2004    ) -> Result<u64, DaemonError> {
2005        use wal::RebaseEdit;
2006        if edits.is_empty() {
2007            return Err(DaemonError::RebaseSpec("no edits"));
2008        }
2009        self.refuse_if_purging(parent)?;
2010        let range = |e: &RebaseEdit| match e {
2011            RebaseEdit::Drop { from, to } | RebaseEdit::Replace { from, to, .. } => (*from, *to),
2012        };
2013        let mut store = self.store.lock().expect("store");
2014        let (max, parent_events) = {
2015            let p = store.session(parent)?;
2016            (p.next_event_id(), p.events.clone())
2017        };
2018        let mut last_to = 0;
2019        for e in &edits {
2020            let (from, to) = range(e);
2021            if from == 0 {
2022                return Err(DaemonError::RebaseSpec("edits never include Created (id 0)"));
2023            }
2024            if from >= to {
2025                return Err(DaemonError::RebaseSpec("empty range"));
2026            }
2027            if to > max {
2028                return Err(DaemonError::RebaseSpec("range beyond the parent's committed events"));
2029            }
2030            if from < last_to {
2031                return Err(DaemonError::RebaseSpec("ranges must be sorted and non-overlapping"));
2032            }
2033            if let RebaseEdit::Replace { role, .. } = e {
2034                if !matches!(*role, wal::role::SYSTEM | wal::role::USER | wal::role::ASSISTANT) {
2035                    return Err(DaemonError::UnknownRole(*role));
2036                }
2037            }
2038            last_to = to;
2039        }
2040        let first = range(&edits[0]).0;
2041        let child = store.rebase_branch(parent, first, params, edits.clone())?;
2042        let mut tool_ids: std::collections::HashMap<u64, u64> = std::collections::HashMap::new();
2043        let mut i = first as usize;
2044        while i < parent_events.len() {
2045            let id = i as u64;
2046            if let Some(e) = edits.iter().find(|e| {
2047                let (from, to) = range(e);
2048                id >= from && id < to
2049            }) {
2050                let (from, to) = range(e);
2051                if let RebaseEdit::Replace { role, text, .. } = e {
2052                    if id == from {
2053                        let span = self.render_for(&store, child, *role, text)?;
2054                        store.append_message(child, *role, text.clone(), span)?;
2055                    }
2056                }
2057                i = to as usize;
2058                continue;
2059            }
2060            match &parent_events[i].body {
2061                EventBody::Appended { text, span } => {
2062                    store.append(child, text.clone(), span.clone())?;
2063                }
2064                EventBody::Message { role, text, span } => {
2065                    store.append_message(child, *role, text.clone(), span.clone())?;
2066                }
2067                EventBody::GenerationPrompt { span } => {
2068                    store.commit_generation_prompt(child, span.clone())?;
2069                }
2070                EventBody::Generated {
2071                    span,
2072                    text,
2073                    channel,
2074                    finish,
2075                } => {
2076                    store.commit_generated(child, span.clone(), text.clone(), *channel, *finish)?;
2077                }
2078                EventBody::ToolUse { name, arguments } => {
2079                    let e = store.commit_tool_use(child, name.clone(), arguments.clone())?;
2080                    tool_ids.insert(id, e.event_id);
2081                }
2082                EventBody::ToolResult {
2083                    call_id,
2084                    content,
2085                    span,
2086                } => match tool_ids.get(call_id) {
2087                    Some(new_id) => {
2088                        store.commit_tool_result(child, *new_id, content.clone(), span.clone())?;
2089                    }
2090                    None => {
2091                        store.append_message(child, wal::role::TOOL, content.clone(), span.clone())?;
2092                    }
2093                },
2094                EventBody::ToolParseFailure { raw } => {
2095                    store.commit_tool_parse_failure(child, raw.clone())?;
2096                }
2097                EventBody::Created { .. }
2098                | EventBody::EpochBump
2099                | EventBody::GenerationFingerprint { .. }
2100                | EventBody::Forked { .. }
2101                | EventBody::Rebased { .. }
2102                | EventBody::MetaUpdated { .. }
2103                | EventBody::ToolExpired { .. }
2104                | EventBody::Purged { .. }
2105                | EventBody::Rerooted { .. }
2106                | EventBody::QosSet { .. } => {}
2107                EventBody::Block {
2108                    role,
2109                    kind,
2110                    payload,
2111                    span,
2112                    visibility_version,
2113                } => {
2114                    store.commit_block(child, *role, *kind, payload.clone(), span.clone(), *visibility_version)?;
2115                }
2116                EventBody::PermissionRequest { .. }
2117                | EventBody::PermissionResponse { .. }
2118                | EventBody::ToolCancelRequested { .. }
2119                | EventBody::ToolOutcome { .. }
2120                | EventBody::ToolReconciliation { .. }
2121                | EventBody::ToolLease { .. }
2122                | EventBody::Pinned { .. } => {}
2123            }
2124            i += 1;
2125        }
2126        Ok(child)
2127    }
2128
2129    pub fn trim(&self, session: u64, to_event: u64) -> Result<u64, DaemonError> {
2130        let max = {
2131            let store = self.store.lock().expect("store");
2132            store.session(session)?.next_event_id()
2133        };
2134        if to_event == 0 || to_event > max {
2135            return Err(DaemonError::ForkPoint {
2136                session,
2137                fork_at: to_event,
2138                max,
2139            });
2140        }
2141        if to_event == max {
2142            return self.fork(session, to_event, None);
2143        }
2144        self.rebase(
2145            session,
2146            vec![wal::RebaseEdit::Drop {
2147                from: to_event,
2148                to: max,
2149            }],
2150            None,
2151        )
2152    }
2153
2154    pub fn pin(&self, session: u64, ttl_ms: u64) -> Result<u64, DaemonError> {
2155        let deadline = if ttl_ms == 0 {
2156            0
2157        } else {
2158            wal::now_unix_ms().saturating_add(ttl_ms)
2159        };
2160        let (tokens, want) = {
2161            let mut store = self.store.lock().expect("store");
2162            store.set_pin(session, deadline)?;
2163            pins::session_target(&store.session(session)?.tokens)
2164        };
2165        self.send_pin(
2166            pins::PinKey::Session(session),
2167            tokens,
2168            want,
2169            deadline,
2170            Some(session),
2171        )?;
2172        Ok(deadline)
2173    }
2174
2175    pub fn pin_prefix(
2176        &self,
2177        anchor: Option<u64>,
2178        stream: &[u32],
2179        upto: usize,
2180        ttl_ms: u64,
2181    ) -> Result<u64, DaemonError> {
2182        let upto = upto.min(stream.len());
2183        let key = pins::PinKey::Prefix(pins::prefix_digest(&stream[..upto]));
2184        let tokens = if upto < stream.len() {
2185            stream[..=upto].to_vec()
2186        } else {
2187            pins::session_target(stream).0
2188        };
2189        let deadline = wal::now_unix_ms().saturating_add(ttl_ms.max(1));
2190        self.send_pin(key, tokens, upto as u64, deadline, anchor)
2191    }
2192
2193    pub fn pinned_prefix_tokens(
2194        &self,
2195        stream: &[u32],
2196        uptos: &[usize],
2197    ) -> Result<u64, DaemonError> {
2198        let keys = uptos
2199            .iter()
2200            .map(|&u| {
2201                let upto = u.min(stream.len());
2202                pins::PinKey::Prefix(pins::prefix_digest(&stream[..upto]))
2203            })
2204            .collect();
2205        let (tx, rx) = std::sync::mpsc::channel();
2206        self.sched_tx()
2207            .send(scheduler::SchedCmd::PinHeld(keys, tx))
2208            .map_err(|_| DaemonError::Protocol("scheduler is gone"))?;
2209        rx.recv()
2210            .map_err(|_| DaemonError::Protocol("scheduler dropped the query"))
2211    }
2212
2213    pub fn unanchor_pins(&self, session: u64) {
2214        let _ = self.sched_tx().send(scheduler::SchedCmd::PinUnanchor(session));
2215    }
2216
2217    fn send_pin(
2218        &self,
2219        key: pins::PinKey,
2220        tokens: Vec<u32>,
2221        want: u64,
2222        deadline_unix_ms: u64,
2223        anchor: Option<u64>,
2224    ) -> Result<u64, DaemonError> {
2225        let (tx, rx) = std::sync::mpsc::channel();
2226        self.sched_tx()
2227            .send(scheduler::SchedCmd::Pin(pins::PinReq {
2228                key,
2229                tokens,
2230                want,
2231                deadline_unix_ms,
2232                anchor,
2233                reply: Some(tx),
2234            }))
2235            .map_err(|_| DaemonError::Protocol("scheduler is gone"))?;
2236        rx.recv()
2237            .map_err(|_| DaemonError::Protocol("scheduler dropped the pin"))
2238    }
2239
2240    fn render_for(
2241        &self,
2242        store: &SessionStore,
2243        session: u64,
2244        role: u32,
2245        text: &str,
2246    ) -> Result<Vec<u32>, DaemonError> {
2247        let s = store.session(session)?;
2248        let mut span = if s.tokens.is_empty() {
2249            self.codec().stream_prologue()
2250        } else {
2251            match Self::last_stream_record(&s.events) {
2252                Some(EventBody::Generated { finish, .. })
2253                    if *finish == superfluid_abi::finish::EOS =>
2254                {
2255                    self.codec().turn_separator().unwrap_or_default()
2256                }
2257                _ => Vec::new(),
2258            }
2259        };
2260        match self.codec().render_message(role, text) {
2261            Some(rendered) => span.extend(rendered),
2262            None => {
2263                let encoded = self.codec().encode(text);
2264                if encoded.is_empty() {
2265                    return Err(DaemonError::Protocol("codec produced no span for the replacement"));
2266                }
2267                span.extend(encoded);
2268            }
2269        }
2270        Ok(span)
2271    }
2272
2273    pub fn purge(
2274        &self,
2275        session: u64,
2276        expected_generation: u64,
2277        mode: wal::PurgeMode,
2278    ) -> Result<Vec<u64>, DaemonError> {
2279        let targets: Vec<u64> = {
2280            let store = self.store.lock().expect("store");
2281            let s = store.session(session)?;
2282            if s.generation != expected_generation {
2283                return Err(DaemonError::GenerationConflict {
2284                    session,
2285                    expected: expected_generation,
2286                    actual: s.generation,
2287                });
2288            }
2289            let mut t = vec![session];
2290            if mode == wal::PurgeMode::Cascade {
2291                t.extend(store.descendants_of(session));
2292            }
2293            t
2294        };
2295        {
2296            let _a = self.active.lock().expect("active set");
2297            let mut p = self.purging.lock().expect("purging");
2298            p.extend(targets.iter().copied());
2299        }
2300        let _fence = PurgeFence {
2301            set: std::sync::Arc::clone(&self.purging),
2302            ids: targets.clone(),
2303        };
2304        for &t in &targets {
2305            self.quiesce(t)?;
2306        }
2307        let purged = {
2308            let mut store = self.store.lock().expect("store");
2309            for &t in &targets {
2310                let open: Vec<u64> = store.session(t)?.open_tool_calls.keys().copied().collect();
2311                for call in open {
2312                    store.expire_tool_call(t, call, "purged")?;
2313                }
2314            }
2315            store.purge(session, expected_generation, mode)?
2316        };
2317        let bus = self.bus();
2318        for &p in &purged {
2319            bus.end_session(p, bus::EndReason::Purged);
2320        }
2321        for &p in &purged {
2322            let _ = self.send_pin(pins::PinKey::Session(p), Vec::new(), 0, 0, None);
2323        }
2324        Ok(purged)
2325    }
2326
2327    fn refuse_if_purging(&self, session: u64) -> Result<(), DaemonError> {
2328        let _a = self.active.lock().expect("active set");
2329        if self.purging.lock().expect("purging").contains(&session) {
2330            return Err(DaemonError::Purged(session));
2331        }
2332        Ok(())
2333    }
2334
2335    fn quiesce(&self, session: u64) -> Result<(), DaemonError> {
2336        {
2337            let mut c = self.cancels.lock().expect("cancel registry");
2338            if self.active.lock().expect("active set").contains(&session) {
2339                c.insert(session);
2340            } else {
2341                return Ok(());
2342            }
2343        }
2344        self.cancels.wake();
2345        for _ in 0..2000 {
2346            if !self.active.lock().expect("active set").contains(&session) {
2347                return Ok(());
2348            }
2349            std::thread::sleep(std::time::Duration::from_millis(5));
2350        }
2351        Err(DaemonError::PurgeTimeout(session))
2352    }
2353
2354    pub fn append(
2355        &self,
2356        session: u64,
2357        text: Option<String>,
2358        span: Vec<u32>,
2359    ) -> Result<CommittedEvent, DaemonError> {
2360        {
2361            let active = self.active.lock().expect("active set");
2362            if active.contains(&session) {
2363                return Err(DaemonError::SessionBusy(session));
2364            }
2365            if self.purging.lock().expect("purging").contains(&session) {
2366                return Err(DaemonError::Purged(session));
2367            }
2368        }
2369        let span = if span.is_empty() {
2370            match &text {
2371                Some(t) => {
2372                    let encoded = self.codec().encode(t);
2373                    if encoded.is_empty() {
2374                        return Err(DaemonError::Protocol(
2375                            "append had no span and the codec produced none",
2376                        ));
2377                    }
2378                    encoded
2379                }
2380                None => return Err(DaemonError::Protocol("append carried neither text nor span")),
2381            }
2382        } else {
2383            span
2384        };
2385        self.store.lock().expect("store").append(session, text, span)
2386    }
2387
2388    fn separator_for(&self, session: u64) -> Vec<u32> {
2389        let store = self.store.lock().expect("store");
2390        let Ok(s) = store.session(session) else {
2391            return Vec::new();
2392        };
2393        if s.tokens.is_empty() {
2394            return self.codec().stream_prologue();
2395        }
2396        match Self::last_stream_record(&s.events) {
2397            Some(EventBody::Generated { finish, .. })
2398                if *finish == superfluid_abi::finish::EOS =>
2399            {
2400                self.codec().turn_separator().unwrap_or_default()
2401            }
2402            _ => Vec::new(),
2403        }
2404    }
2405
2406    pub fn append_message(
2407        &self,
2408        session: u64,
2409        role: u32,
2410        text: String,
2411    ) -> Result<CommittedEvent, DaemonError> {
2412        if self.active.lock().expect("active set").contains(&session) {
2413            return Err(DaemonError::SessionBusy(session));
2414        }
2415        if !matches!(role, wal::role::SYSTEM | wal::role::USER | wal::role::ASSISTANT) {
2416            return Err(DaemonError::UnknownRole(role));
2417        }
2418        let mut span = self.separator_for(session);
2419        span.extend(
2420            self.codec()
2421                .render_message(role, &text)
2422                .ok_or(DaemonError::Protocol("codec has no chat dialect"))?,
2423        );
2424        self.store
2425            .lock()
2426            .expect("store")
2427            .append_message(session, role, text, span)
2428    }
2429
2430    pub fn renders_per_message(&self) -> bool {
2431        self.codec().renders_per_message()
2432    }
2433
2434    pub fn append_system_full(
2435        &self,
2436        session: u64,
2437        system: Option<String>,
2438        tool_jsons: Vec<String>,
2439        kwargs: &serde_json::Map<String, serde_json::Value>,
2440    ) -> Result<CommittedEvent, DaemonError> {
2441        if self.active.lock().expect("active set").contains(&session) {
2442            return Err(DaemonError::SessionBusy(session));
2443        }
2444        let span = self
2445            .codec()
2446            .render_system_full(system.as_deref(), &tool_jsons, kwargs)
2447            .ok_or(DaemonError::Protocol("codec has no chat dialect"))?;
2448        self.store.lock().expect("store").append_message(
2449            session,
2450            wal::role::SYSTEM,
2451            system.unwrap_or_default(),
2452            span,
2453        )
2454    }
2455
2456    // Chat templates declare tools only in the opening system turn, so one rendered later would
2457    // put the model's input in a shape it was not trained on; it is refused instead.
2458    pub fn append_system(
2459        &self,
2460        session: u64,
2461        system: Option<String>,
2462        tools: Vec<String>,
2463    ) -> Result<CommittedEvent, DaemonError> {
2464        if system.is_none() && tools.is_empty() {
2465            return Err(DaemonError::Protocol("AppendSystem carried neither text nor tools"));
2466        }
2467        let tool_jsons = tools
2468            .iter()
2469            .enumerate()
2470            .map(|(index, t)| {
2471                codec::function_tool(t).map_err(|why| DaemonError::InvalidTool { index, why })
2472            })
2473            .collect::<Result<Vec<_>, _>>()?;
2474        if self.active.lock().expect("active set").contains(&session) {
2475            return Err(DaemonError::SessionBusy(session));
2476        }
2477        let codec = self.codec();
2478        let mut span = codec.stream_prologue();
2479        span.extend(
2480            codec
2481                .render_system_full(system.as_deref(), &tool_jsons, &serde_json::Map::new())
2482                .ok_or(DaemonError::Protocol("codec has no chat dialect"))?,
2483        );
2484        let mut store = self.store.lock().expect("store");
2485        if !store.session(session)?.tokens.is_empty() {
2486            return Err(DaemonError::SystemNotFirst(session));
2487        }
2488        store.append_message(session, wal::role::SYSTEM, system.unwrap_or_default(), span)
2489    }
2490
2491    pub(crate) fn render_conversation(
2492        &self,
2493        messages: &[codec::ChatMessage],
2494        tools: &[String],
2495    ) -> Option<Vec<u32>> {
2496        self.codec()
2497            .render_prompt_structured_with(messages, tools, &serde_json::Map::new())
2498    }
2499
2500    pub fn supports_reasoning_effort(&self) -> bool {
2501        self.codec().supports_reasoning_effort()
2502    }
2503
2504    pub fn reasoning_effort_levels(&self) -> Vec<&'static str> {
2505        self.codec().reasoning_effort_levels()
2506    }
2507
2508    pub fn supports_enable_thinking(&self) -> bool {
2509        self.codec().supports_enable_thinking()
2510    }
2511
2512    pub fn validate_template_kwargs(
2513        &self,
2514        kwargs: &serde_json::Map<String, serde_json::Value>,
2515    ) -> Result<(), String> {
2516        self.codec().validate_template_kwargs(kwargs)
2517    }
2518
2519    fn commit_opener(&self, session: u64, opener: Vec<u32>) -> Result<(), DaemonError> {
2520        if opener.is_empty() {
2521            return Ok(());
2522        }
2523        self.store
2524            .lock()
2525            .expect("store")
2526            .commit_generation_prompt(session, opener)
2527            .map(|_| ())
2528    }
2529
2530    pub fn append_conversation(
2531        &self,
2532        session: u64,
2533        messages: &[crate::codec::ChatMessage],
2534        tools: &[String],
2535        kwargs: &serde_json::Map<String, serde_json::Value>,
2536    ) -> Result<CommittedEvent, DaemonError> {
2537        let codec = self.codec();
2538        let mut span = codec
2539            .render_prompt_structured_with(messages, tools, kwargs)
2540            .ok_or_else(|| render_refusal(&*codec, messages, tools, kwargs, "codec has no chat dialect"))?;
2541        let opener_len = codec.trailing_generation_prompt(messages, tools, kwargs, &span);
2542        let opener: Vec<u32> = if opener_len > 0 && opener_len < span.len() {
2543            span.split_off(span.len() - opener_len)
2544        } else {
2545            Vec::new()
2546        };
2547        let text = messages.last().map(|m| m.content.clone());
2548        let blobs: Vec<(u32, String)> = messages
2549            .iter()
2550            .flat_map(|m| {
2551                m.parts.iter().filter_map(move |p| match p {
2552                    crate::codec::ContentPart::Image { blob } => Some((m.role, blob.clone())),
2553                    crate::codec::ContentPart::Audio { blob } => Some((m.role, blob.clone())),
2554                    crate::codec::ContentPart::Text(_) => None,
2555                })
2556            })
2557            .collect();
2558        if blobs.is_empty() {
2559            let ev = self.append(session, text, span)?;
2560            self.commit_opener(session, opener)?;
2561            return Ok(ev);
2562        }
2563        let mut probes = Vec::with_capacity(blobs.len());
2564        for (_, blob) in &blobs {
2565            if !self.media.exists(blob) {
2566                return Err(DaemonError::UnknownMedia(blob.clone()));
2567            }
2568            let path = self.media.path_of(blob);
2569            let info = match self.engine_sync(proto_linkw::StateSyncReqMsg::MediaProbe {
2570                image_path: path.to_string_lossy().into_owned(),
2571            })? {
2572                proto_linkw::StateSyncOkMsg::MediaProbe { info } => info,
2573                _ => return Err(DaemonError::Protocol("mismatched MediaProbe reply")),
2574            };
2575            if info.n_tokens == 0 || info.n_tokens as u64 > scheduler::PREFILL_BUDGET as u64 {
2576                return Err(DaemonError::MediaTooLarge(info.n_tokens));
2577            }
2578            probes.push(info);
2579        }
2580        let mut out = Vec::with_capacity(span.len() + 256);
2581        let mut offsets: Vec<usize> = Vec::with_capacity(probes.len());
2582        let mut img = 0usize;
2583        for (idx, &t) in span.iter().enumerate() {
2584            if img == probes.len() || t != probes[img].image_token_id {
2585                if probes.iter().any(|p| p.image_token_id == t) {
2586                    return Err(DaemonError::Protocol(
2587                        "rendered prompt contains a media marker with no matching pending part",
2588                    ));
2589                }
2590                out.push(t);
2591                continue;
2592            }
2593            let info = &probes[img];
2594            if info.boi_token_id != 0 && out.last() != Some(&info.boi_token_id) {
2595                out.push(info.boi_token_id);
2596            }
2597            offsets.push(out.len());
2598            out.extend(std::iter::repeat_n(info.image_token_id, info.n_tokens as usize));
2599            if info.eoi_token_id != 0 && span.get(idx + 1) != Some(&info.eoi_token_id) {
2600                out.push(info.eoi_token_id);
2601            }
2602            img += 1;
2603        }
2604        if img != probes.len() {
2605            return Err(DaemonError::Protocol(
2606                "this model's chat template did not emit an image marker for every image part;                  it cannot place media",
2607            ));
2608        }
2609        let stream_len = {
2610            let store = self.store.lock().expect("store");
2611            store.session(session)?.tokens.len() as u64
2612        };
2613        let ev = self.append(session, text, out)?;
2614        for (((role, blob), info), off) in blobs.iter().zip(&probes).zip(&offsets) {
2615            let payload = serde_json::json!({
2616                "blob": blob,
2617                "n_tokens": info.n_tokens,
2618                "image_token_id": info.image_token_id,
2619                "offset": stream_len + *off as u64,
2620                "pre_text": "",
2621                "post_text": "",
2622                "media_identity": {
2623                    "preprocessing_fp": format!("{:016x}", info.preprocess_fp),
2624                    "encoder": "bundle-weights-identity",
2625                },
2626            })
2627            .to_string();
2628            let mut store = self.store.lock().expect("store");
2629            store.commit_block(
2630                session,
2631                *role,
2632                wal::block_kind::IMAGE,
2633                payload,
2634                Vec::new(),
2635                codec::MODEL_VISIBILITY_VERSION,
2636            )?;
2637        }
2638        self.commit_opener(session, opener)?;
2639        Ok(ev)
2640    }
2641
2642    pub fn response_format_tag(&self, json_schema: &str) -> Option<String> {
2643        self.codec().response_format_tag(json_schema)
2644    }
2645
2646    pub fn tool_structural_tag(&self, tool_jsons: &[String], at_least_one: bool) -> Option<String> {
2647        self.codec().structural_tag(tool_jsons, at_least_one)
2648    }
2649
2650    pub fn grammar_create_structural(&self, tag_json: &str) -> Result<u32, DaemonError> {
2651        match self.engine_sync(proto_linkw::StateSyncReqMsg::CreateGrammarStructural {
2652            tag_json: tag_json.to_string(),
2653        })? {
2654            proto_linkw::StateSyncOkMsg::CreateGrammar { grammar_handle } => Ok(grammar_handle),
2655            _ => Err(DaemonError::Protocol("mismatched CreateGrammar reply")),
2656        }
2657    }
2658
2659    pub fn parse_tool_call(&self, raw: &str) -> Option<(String, String)> {
2660        self.codec().parse_tool_call(raw)
2661    }
2662
2663    pub fn named_tool_call(&self, raw: &str) -> Option<(String, String)> {
2664        self.codec().named_tool_call(raw)
2665    }
2666
2667    pub fn parse_tool_call_with(
2668        &self,
2669        raw: &str,
2670        schemas: Option<&crate::codec::ToolSchemas>,
2671    ) -> Option<(String, String)> {
2672        self.codec().parse_tool_call_with(raw, schemas)
2673    }
2674
2675    pub fn tool_fragment_mode(&self) -> crate::tool_fragment::ToolFragmentMode {
2676        self.codec().tool_fragment_mode()
2677    }
2678
2679    pub fn tool_call_grammar(&self, tool_jsons: &[String]) -> Option<String> {
2680        self.codec().call_grammar(tool_jsons)
2681    }
2682
2683    pub fn append_system_with_tools(
2684        &self,
2685        session: u64,
2686        system: Option<String>,
2687        tool_jsons: Vec<String>,
2688    ) -> Result<CommittedEvent, DaemonError> {
2689        if self.active.lock().expect("active set").contains(&session) {
2690            return Err(DaemonError::SessionBusy(session));
2691        }
2692        let span = self
2693            .codec()
2694            .render_system_with_tools(system.as_deref(), &tool_jsons)
2695            .ok_or(DaemonError::Protocol("codec has no tool dialect"))?;
2696        self.store.lock().expect("store").append_message(
2697            session,
2698            wal::role::SYSTEM,
2699            system.unwrap_or_default(),
2700            span,
2701        )
2702    }
2703
2704    /// `after_query`: no user query follows this turn in the conversation
2705    /// (a step of a tool loop), which some templates render differently.
2706    pub fn append_assistant_with_tool_calls(
2707        &self,
2708        session: u64,
2709        content: String,
2710        calls: Vec<(String, String)>,
2711        after_query: bool,
2712    ) -> Result<CommittedEvent, DaemonError> {
2713        if self.active.lock().expect("active set").contains(&session) {
2714            return Err(DaemonError::SessionBusy(session));
2715        }
2716        let mut span = self.separator_for(session);
2717        let codec = self.codec();
2718        let turn = if after_query {
2719            codec.render_assistant_with_tool_calls_after_query(&content, &calls)
2720        } else {
2721            codec.render_assistant_with_tool_calls(&content, &calls)
2722        };
2723        span.extend(turn.ok_or(DaemonError::Protocol("codec has no tool dialect"))?);
2724        self.store.lock().expect("store").append_message(
2725            session,
2726            wal::role::ASSISTANT,
2727            content,
2728            span,
2729        )
2730    }
2731
2732    pub fn append_tool_result(
2733        &self,
2734        session: u64,
2735        call_id: u64,
2736        content: String,
2737    ) -> Result<CommittedEvent, DaemonError> {
2738        if self.active.lock().expect("active set").contains(&session) {
2739            return Err(DaemonError::SessionBusy(session));
2740        }
2741        let name = {
2742            let mut store = self.store.lock().expect("store");
2743            let s = store.session(session)?;
2744            if call_id < s.base {
2745                return Err(DaemonError::InheritedToolCall(call_id));
2746            }
2747            match s.ledger.get(&call_id) {
2748                None => return Err(DaemonError::UnknownToolCall(call_id)),
2749                Some(e)
2750                    if e.state == wal::ledger_state::OPEN
2751                        || e.state == wal::ledger_state::CANCEL_REQUESTED =>
2752                {
2753                    if let Some(deadline) = e.lease_deadline_unix_ms {
2754                        if wal::now_unix_ms() > deadline {
2755                            store.expire_tool_call(session, call_id, "lease expired")?;
2756                            return Err(DaemonError::ToolLeaseExpired(call_id));
2757                        }
2758                    }
2759                    e.name.clone()
2760                }
2761                Some(e) if e.state == wal::ledger_state::SUCCEEDED => {
2762                    return Err(DaemonError::UnknownToolCall(call_id));
2763                }
2764                Some(_) => {
2765                    store.commit_tool_reconciliation(
2766                        session,
2767                        call_id,
2768                        format!("late result: {} bytes", content.len()),
2769                    )?;
2770                    return Err(DaemonError::ToolLateEffect(call_id));
2771                }
2772            }
2773        };
2774        let mut span = self.separator_for(session);
2775        span.extend(
2776            self.codec()
2777                .render_tool_result(&name, &content)
2778                .ok_or(DaemonError::Protocol("codec has no tool dialect"))?,
2779        );
2780        self.store
2781            .lock()
2782            .expect("store")
2783            .commit_tool_result(session, call_id, content, span)
2784    }
2785
2786    pub fn append_block(
2787        &self,
2788        session: u64,
2789        role: u32,
2790        kind: u32,
2791        payload: String,
2792    ) -> Result<CommittedEvent, DaemonError> {
2793        if self.active.lock().expect("active set").contains(&session) {
2794            return Err(DaemonError::SessionBusy(session));
2795        }
2796        if !matches!(role, wal::role::SYSTEM | wal::role::USER | wal::role::ASSISTANT | wal::role::TOOL) {
2797            return Err(DaemonError::UnknownRole(role));
2798        }
2799        if codec::project_block_text(kind, &payload).is_none() {
2800            return Err(DaemonError::UnknownBlockKind(kind));
2801        }
2802        let projected = self
2803            .codec()
2804            .render_block(role, kind, &payload)
2805            .ok_or(DaemonError::Protocol("codec has no chat dialect"))?;
2806        let span = if projected.is_empty() {
2807            Vec::new()
2808        } else {
2809            let mut span = self.separator_for(session);
2810            span.extend(projected);
2811            span
2812        };
2813        self.store.lock().expect("store").commit_block(
2814            session,
2815            role,
2816            kind,
2817            payload,
2818            span,
2819            codec::MODEL_VISIBILITY_VERSION,
2820        )
2821    }
2822
2823    pub fn request_permission(
2824        &self,
2825        session: u64,
2826        call_id: u64,
2827        text: String,
2828    ) -> Result<CommittedEvent, DaemonError> {
2829        let active = self.active.lock().expect("active set");
2830        if active.contains(&session) {
2831            return Err(DaemonError::SessionBusy(session));
2832        }
2833        if self.purging.lock().expect("purging").contains(&session) {
2834            return Err(DaemonError::Purged(session));
2835        }
2836        self.store
2837            .lock()
2838            .expect("store")
2839            .commit_permission_request(session, call_id, text)
2840    }
2841
2842    pub fn respond_permission(
2843        &self,
2844        session: u64,
2845        request_id: u64,
2846        granted: bool,
2847    ) -> Result<CommittedEvent, DaemonError> {
2848        self.store
2849            .lock()
2850            .expect("store")
2851            .commit_permission_response(session, request_id, granted)
2852    }
2853
2854    pub fn request_tool_cancel(&self, session: u64, call_id: u64) -> Result<CommittedEvent, DaemonError> {
2855        self.store
2856            .lock()
2857            .expect("store")
2858            .commit_tool_cancel_request(session, call_id)
2859    }
2860
2861    pub fn append_tool_outcome(
2862        &self,
2863        session: u64,
2864        call_id: u64,
2865        outcome: u8,
2866        note: String,
2867    ) -> Result<CommittedEvent, DaemonError> {
2868        self.store
2869            .lock()
2870            .expect("store")
2871            .commit_tool_outcome(session, call_id, outcome, note)
2872    }
2873
2874    pub fn ledger(&self, session: u64) -> Result<Vec<(u64, store::LedgerEntry)>, DaemonError> {
2875        Ok(self
2876            .store
2877            .lock()
2878            .expect("store")
2879            .session(session)?
2880            .ledger
2881            .iter()
2882            .map(|(id, e)| (*id, e.clone()))
2883            .collect())
2884    }
2885
2886    pub fn append_tool_history(
2887        &self,
2888        session: u64,
2889        content: String,
2890    ) -> Result<CommittedEvent, DaemonError> {
2891        if self.active.lock().expect("active set").contains(&session) {
2892            return Err(DaemonError::SessionBusy(session));
2893        }
2894        let mut span = self.separator_for(session);
2895        span.extend(
2896            self.codec()
2897                .render_tool_result("", &content)
2898                .ok_or(DaemonError::Protocol("codec has no tool dialect"))?,
2899        );
2900        self.store.lock().expect("store").append_message(
2901            session,
2902            wal::role::TOOL,
2903            content,
2904            span,
2905        )
2906    }
2907
2908    pub fn open_tool_calls(
2909        &self,
2910        session: u64,
2911    ) -> Result<Vec<(u64, String, String)>, DaemonError> {
2912        Ok(self
2913            .store
2914            .lock()
2915            .expect("store")
2916            .session(session)?
2917            .open_tool_calls
2918            .iter()
2919            .map(|(id, (n, a))| (*id, n.clone(), a.clone()))
2920            .collect())
2921    }
2922
2923    pub fn generate(&self, session: u64, max_tokens: u32) -> Result<GenerateOutcome, DaemonError> {
2924        self.generate_streaming(session, max_tokens, |_| Ok(()))
2925    }
2926
2927    pub fn generate_streaming(
2928        &self,
2929        session: u64,
2930        max_tokens: u32,
2931        on_event: impl FnMut(&CommittedEvent) -> Result<(), DaemonError>,
2932    ) -> Result<GenerateOutcome, DaemonError> {
2933        self.generate_streaming_ex(session, max_tokens, scheduler::GenExtras::default(), on_event)
2934    }
2935
2936    pub fn generate_streaming_ex(
2937        &self,
2938        session: u64,
2939        max_tokens: u32,
2940        extras: scheduler::GenExtras,
2941        on_event: impl FnMut(&CommittedEvent) -> Result<(), DaemonError>,
2942    ) -> Result<GenerateOutcome, DaemonError> {
2943        self.generate_streaming_ex_lp(session, max_tokens, extras, on_event, |_| {})
2944    }
2945
2946    pub fn generate_streaming_ex_lp(
2947        &self,
2948        session: u64,
2949        max_tokens: u32,
2950        extras: scheduler::GenExtras,
2951        on_event: impl FnMut(&CommittedEvent) -> Result<(), DaemonError>,
2952        on_logprobs: impl FnMut(&[scheduler::TokenLogprob]),
2953    ) -> Result<GenerateOutcome, DaemonError> {
2954        self.generate_streaming_inner(session, max_tokens, extras, on_event, on_logprobs, |_, _, _| {})
2955    }
2956
2957    pub fn generate_streaming_delta(
2958        &self,
2959        session: u64,
2960        max_tokens: u32,
2961        mut extras: scheduler::GenExtras,
2962        on_event: impl FnMut(&CommittedEvent) -> Result<(), DaemonError>,
2963        on_logprobs: impl FnMut(&[scheduler::TokenLogprob]),
2964        on_delta: impl FnMut(u32, &str, u32),
2965    ) -> Result<GenerateOutcome, DaemonError> {
2966        extras.wants_deltas = true;
2967        self.generate_streaming_inner(session, max_tokens, extras, on_event, on_logprobs, on_delta)
2968    }
2969
2970    fn generation_opener(&self, events: &[store::CommittedEvent], thinking: Option<bool>) -> Option<Vec<u32>> {
2971        match Self::last_stream_record(events) {
2972            Some(EventBody::Message { .. }) | Some(EventBody::ToolResult { .. }) => self
2973                .codec()
2974                .generation_prefix_with(codec::TurnState::AfterInput, thinking),
2975            Some(EventBody::Generated { finish, .. }) if *finish == superfluid_abi::finish::EOS => self
2976                .codec()
2977                .generation_prefix_with(codec::TurnState::AfterFinishedTurn, thinking),
2978            _ => None,
2979        }
2980    }
2981
2982    /// Where a continuation of `session` forks (one past its last event),
2983    /// and whether its last turn ended: an append after an unended turn
2984    /// lands inside it.
2985    pub(crate) fn session_end(&self, session: u64) -> Option<(u64, bool)> {
2986        let store = self.store.lock().expect("store");
2987        let s = store.session(session).ok()?;
2988        let ended = matches!(
2989            Self::last_stream_record(&s.events),
2990            Some(EventBody::Generated { finish, .. }) if *finish == superfluid_abi::finish::EOS
2991        );
2992        Some((s.next_event_id(), ended))
2993    }
2994
2995    fn last_stream_record(events: &[store::CommittedEvent]) -> Option<&EventBody> {
2996        events
2997            .iter()
2998            .rev()
2999            .map(|e| &e.body)
3000            .find(|b| b.span().is_some_and(|s| !s.is_empty()))
3001    }
3002
3003    fn open_turn_tokens(events: &[store::CommittedEvent]) -> Vec<u32> {
3004        let mut spans: Vec<&[u32]> = Vec::new();
3005        for e in events.iter().rev() {
3006            match &e.body {
3007                EventBody::Generated { span, finish, .. } => {
3008                    if spans.is_empty() && *finish == superfluid_abi::finish::EOS {
3009                        return Vec::new();
3010                    }
3011                    spans.push(span);
3012                }
3013                EventBody::GenerationPrompt { span } => {
3014                    spans.push(span);
3015                    break;
3016                }
3017                b => match b.span() {
3018                    None | Some([]) => continue,
3019                    Some(_) => break,
3020                },
3021            }
3022        }
3023        spans.into_iter().rev().flatten().copied().collect()
3024    }
3025
3026    fn generate_streaming_inner(
3027        &self,
3028        session: u64,
3029        max_tokens: u32,
3030        extras: scheduler::GenExtras,
3031        on_event: impl FnMut(&CommittedEvent) -> Result<(), DaemonError>,
3032        on_logprobs: impl FnMut(&[scheduler::TokenLogprob]),
3033        on_delta: impl FnMut(u32, &str, u32),
3034    ) -> Result<GenerateOutcome, DaemonError> {
3035        self.ensure_serving()?;
3036        let _span = tracing::info_span!(
3037            parent: None,
3038            "generate",
3039            session,
3040            max_tokens,
3041            model = self.trace_scope.model.as_str(),
3042            store_ns = self.trace_scope.ns
3043        )
3044        .entered();
3045        let (stream, params, class, batch_invariant, media) = {
3046            let mut cancels = self.cancels.lock().expect("cancel registry");
3047            let mut active = self.active.lock().expect("active set");
3048            if self.purging.lock().expect("purging").contains(&session) {
3049                return Err(DaemonError::Purged(session));
3050            }
3051            if self.tool_lease_ms > 0 {
3052                let now = crate::wal::now_unix_ms();
3053                let mut store = self.store.lock().expect("store");
3054                let elapsed: Vec<u64> = {
3055                    let s = store.session(session)?;
3056                    s.open_tool_calls
3057                        .keys()
3058                        .filter(|id| {
3059                            s.ledger
3060                                .get(id)
3061                                .and_then(|e| e.lease_deadline_unix_ms)
3062                                .is_some_and(|d| d <= now)
3063                        })
3064                        .copied()
3065                        .collect()
3066                };
3067                for call_id in elapsed {
3068                    let _ = store.expire_tool_call(session, call_id, "lease elapsed");
3069                }
3070            }
3071            let snap = {
3072                let store = self.store.lock().expect("store");
3073                let s = store.session(session)?;
3074                if !s.open_permissions.is_empty() {
3075                    return Err(DaemonError::PermissionPending(session));
3076                }
3077                if !s.open_tool_calls.is_empty() {
3078                    tracing::warn!(
3079                        session,
3080                        open_tool_calls = s.open_tool_calls.len(),
3081                        "generating with unanswered tool calls: the model cannot see its own \
3082                         tool output and will re-issue the call. Return a result for each open \
3083                         call id (OpenAI: a `role: \"tool\"` message carrying its `tool_call_id`), \
3084                         or run with --tool-lease-ms so unanswered calls expire instead of \
3085                         accumulating."
3086                    );
3087                }
3088                (
3089                    s.tokens.clone(),
3090                    s.params,
3091                    s.qos_class,
3092                    s.batch_invariant,
3093                    self.media_of(s),
3094                )
3095            };
3096            if snap.0.is_empty() {
3097                return Err(DaemonError::EmptySession(session));
3098            }
3099            self.check_stream_fits(snap.0.len())?;
3100            if max_tokens == 0 {
3101                return Ok(GenerateOutcome {
3102                    events: Vec::new(),
3103                    tokens_generated: 0,
3104                    finish: superfluid_abi::finish::NONE,
3105                    warm_prefix: 0,
3106                    logprobs: Vec::new(),
3107                    spec: (0, 0),
3108                });
3109            }
3110            if !active.insert(session) {
3111                return Err(DaemonError::SessionBusy(session));
3112            }
3113            // A cancel aimed at the previous generation can land after its
3114            // lane stopped reading cancels (finishing, or already retired);
3115            // it must not stop this one.
3116            cancels.remove(&session);
3117            snap
3118        };
3119        if let Some(mut fp) = self.codec().behavior_fingerprint() {
3120            fp.speculation = self.serving().speculation.identity_for_fingerprint();
3121            let commit = self
3122                .store
3123                .lock()
3124                .expect("store")
3125                .commit_generation_fingerprint(session, fp.digest());
3126            if let Err(e) = commit {
3127                self.active.lock().expect("active set").remove(&session);
3128                return Err(e);
3129            }
3130        }
3131        let mut stream = stream;
3132        let (opener, open_turn) = {
3133            let store = self.store.lock().expect("store");
3134            let events = &store.session(session)?.events;
3135            let opener = self.generation_opener(events, extras.thinking);
3136            let open_turn: Vec<u32> = if opener.is_some() {
3137                Vec::new()
3138            } else {
3139                Self::open_turn_tokens(events)
3140            };
3141            (opener, open_turn)
3142        };
3143        let opener_len = opener.as_ref().map(Vec::len);
3144        let input_end = if opener.is_some() { stream.len() as u64 } else { 0 };
3145        if let Some(op) = opener {
3146            if let Err(e) = self.check_stream_fits(stream.len() + op.len()) {
3147                self.active.lock().expect("active set").remove(&session);
3148                return Err(e);
3149            }
3150            let commit = self
3151                .store
3152                .lock()
3153                .expect("store")
3154                .commit_generation_prompt(session, op.clone());
3155            if let Err(e) = commit {
3156                self.active.lock().expect("active set").remove(&session);
3157                return Err(e);
3158            }
3159            stream.extend_from_slice(&op);
3160        }
3161        let mut extras = extras;
3162        if extras.open_channel == 0 {
3163            let scope: &[u32] = match opener_len {
3164                Some(n) => &stream[stream.len() - n..],
3165                None => &open_turn,
3166            };
3167            let open = codec::trailing_open_channel(scope, &self.codec().channel_markers());
3168            if open != 0 {
3169                extras.open_channel = open;
3170            }
3171        }
3172        let result = scheduler::run_job(
3173            &self.sched_tx(),
3174            session,
3175            stream,
3176            input_end,
3177            params,
3178            max_tokens,
3179            class,
3180            batch_invariant,
3181            media,
3182            extras,
3183            &self.cancels,
3184            on_event,
3185            on_logprobs,
3186            on_delta,
3187        );
3188        match result {
3189            Ok((events, tokens_generated, finish, warm_prefix, logprobs, spec)) => Ok(GenerateOutcome {
3190                events,
3191                tokens_generated,
3192                finish,
3193                warm_prefix,
3194                logprobs,
3195                spec,
3196            }),
3197            Err(e) => {
3198                self.active.lock().expect("active set").remove(&session);
3199                Err(e)
3200            }
3201        }
3202    }
3203
3204    pub fn read(&self, session: u64, cursor: u64) -> Result<Vec<CommittedEvent>, DaemonError> {
3205        Ok(self.store.lock().expect("store").read(session, cursor)?.to_vec())
3206    }
3207
3208    pub fn session_ids(&self) -> Vec<u64> {
3209        self.store.lock().expect("store").session_ids()
3210    }
3211}
3212
3213#[cfg(test)]
3214mod open_turn_tests {
3215    use super::*;
3216    use superfluid_abi::finish;
3217
3218    fn ev(event_id: u64, body: EventBody) -> store::CommittedEvent {
3219        store::CommittedEvent { event_id, epoch: 0, ts_unix_ms: 0, body }
3220    }
3221
3222    fn generated(span: &[u32], finish: u32) -> EventBody {
3223        EventBody::Generated { span: span.to_vec(), text: String::new(), channel: 0, finish }
3224    }
3225
3226    #[test]
3227    fn open_turn_skips_every_token_less_record() {
3228        let events = vec![
3229            ev(0, EventBody::Message { role: 1, text: "hi".into(), span: vec![900, 901] }),
3230            ev(1, EventBody::GenerationPrompt { span: vec![1, 2] }),
3231            ev(2, generated(&[3, 4], finish::NONE)),
3232            ev(3, EventBody::ToolUse { name: "x".into(), arguments: "{".into() }),
3233            ev(4, EventBody::ToolLease { call_id: 3, deadline_unix_ms: 1 }),
3234            ev(5, EventBody::ToolReconciliation { call_id: 3, note: String::new() }),
3235            ev(6, EventBody::EpochBump),
3236            ev(7, EventBody::Pinned { deadline_unix_ms: 1 }),
3237            ev(8, EventBody::Block { role: 1, kind: 7, payload: "{}".into(), span: vec![], visibility_version: 1 }),
3238            ev(9, generated(&[5], finish::LENGTH)),
3239        ];
3240        assert_eq!(Daemon::open_turn_tokens(&events), vec![1, 2, 3, 4, 5]);
3241    }
3242
3243    #[test]
3244    fn token_less_records_do_not_reopen_a_closed_turn() {
3245        let closed = vec![
3246            ev(0, EventBody::GenerationPrompt { span: vec![1] }),
3247            ev(1, generated(&[2], finish::EOS)),
3248            ev(2, EventBody::ToolLease { call_id: 9, deadline_unix_ms: 1 }),
3249            ev(3, EventBody::QosSet { class: 0, batch_invariant: false }),
3250        ];
3251        assert!(Daemon::open_turn_tokens(&closed).is_empty());
3252        let after_result = vec![
3253            ev(0, EventBody::GenerationPrompt { span: vec![1] }),
3254            ev(1, generated(&[2], finish::NONE)),
3255            ev(2, EventBody::ToolUse { name: "x".into(), arguments: "{}".into() }),
3256            ev(3, EventBody::ToolResult { call_id: 2, content: "ok".into(), span: vec![7, 8] }),
3257            ev(4, EventBody::ToolLease { call_id: 2, deadline_unix_ms: 1 }),
3258        ];
3259        assert!(Daemon::open_turn_tokens(&after_result).is_empty());
3260    }
3261
3262    #[test]
3263    fn the_last_stream_record_is_the_last_one_with_tokens() {
3264        let events = vec![
3265            ev(0, EventBody::GenerationPrompt { span: vec![1] }),
3266            ev(1, generated(&[2], finish::EOS)),
3267            ev(2, EventBody::ToolUse { name: "x".into(), arguments: "{}".into() }),
3268            ev(3, EventBody::ToolLease { call_id: 2, deadline_unix_ms: 1 }),
3269            ev(4, EventBody::Block { role: 1, kind: 7, payload: "{}".into(), span: vec![], visibility_version: 1 }),
3270            ev(5, EventBody::GenerationFingerprint { digest: 9 }),
3271        ];
3272        assert!(matches!(
3273            Daemon::last_stream_record(&events),
3274            Some(EventBody::Generated { finish: finish::EOS, .. })
3275        ));
3276        assert!(Daemon::last_stream_record(&events[2..]).is_none());
3277    }
3278}
3279
3280fn first_registrable<E>(
3281    candidates: &[String],
3282    auto: bool,
3283    mut register: impl FnMut(&str) -> Result<(), E>,
3284    mut refused: impl FnMut(&str, &E, Option<&str>),
3285) -> Result<Option<String>, E> {
3286    for (i, c) in candidates.iter().enumerate() {
3287        match register(c) {
3288            Ok(()) => return Ok(Some(c.clone())),
3289            Err(e) if auto => refused(c, &e, candidates.get(i + 1).map(String::as_str)),
3290            Err(e) => return Err(e),
3291        }
3292    }
3293    Ok(None)
3294}
3295
3296#[cfg(test)]
3297mod speculate_fallback_tests {
3298    use super::first_registrable;
3299
3300    fn strs(v: &[&str]) -> Vec<String> {
3301        v.iter().map(|s| s.to_string()).collect()
3302    }
3303
3304    #[test]
3305    fn auto_tries_each_candidate_in_order_until_one_registers() {
3306        let mut tried = Vec::new();
3307        let mut reported = Vec::new();
3308        let picked = first_registrable(
3309            &strs(&["dspark:a", "dflash:b", "eagle3:c"]),
3310            true,
3311            |s| {
3312                tried.push(s.to_string());
3313                if s == "dflash:b" { Ok(()) } else { Err(format!("no {s}")) }
3314            },
3315            |s, e, next| reported.push((s.to_string(), e.clone(), next.map(str::to_string))),
3316        )
3317        .unwrap();
3318        assert_eq!(picked.as_deref(), Some("dflash:b"));
3319        assert_eq!(tried, ["dspark:a", "dflash:b"]);
3320        assert_eq!(reported, [("dspark:a".to_string(), "no dspark:a".to_string(), Some("dflash:b".to_string()))]);
3321    }
3322
3323    #[test]
3324    fn auto_with_every_candidate_refused_serves_plainly() {
3325        let mut last_next = Some("x".to_string());
3326        let picked = first_registrable(
3327            &strs(&["dspark:a", "dflash:b"]),
3328            true,
3329            |_| Err::<(), _>("refused"),
3330            |_, _, next| last_next = next.map(str::to_string),
3331        )
3332        .unwrap();
3333        assert_eq!(picked, None);
3334        assert_eq!(last_next, None, "the last refusal says it is decoding plainly");
3335    }
3336
3337    #[test]
3338    fn an_explicit_strategy_fails_on_the_first_refusal() {
3339        let mut reported = 0;
3340        let r = first_registrable(
3341            &strs(&["dspark:a", "dflash:b"]),
3342            false,
3343            |s| if s == "dspark:a" { Err("refused") } else { Ok(()) },
3344            |_, _, _| reported += 1,
3345        );
3346        assert_eq!(r, Err("refused"));
3347        assert_eq!(reported, 0);
3348    }
3349}
3350
3351#[cfg(test)]
3352mod latency_tests {
3353    use super::CancelSet;
3354    use std::time::Duration;
3355
3356    #[test]
3357    fn a_latency_request_raises_the_word_and_holds_it_for_a_while() {
3358        let ctl = superfluid_shm::ControlSegment::create().unwrap();
3359        let worker = superfluid_shm::ControlSegment::attach(
3360            superfluid_shm::ShmSegment::from_fd(
3361                {
3362                    use std::os::fd::FromRawFd;
3363                    // SAFETY: a fresh duplicate of the segment's fd, owned here.
3364                    unsafe { std::os::fd::OwnedFd::from_raw_fd(libc::dup(ctl.raw_fd())) }
3365                },
3366                superfluid_shm::CONTROL_BYTES,
3367            )
3368            .unwrap(),
3369        )
3370        .unwrap();
3371        let cancels = CancelSet::default();
3372        cancels.wake_with(std::sync::Arc::new(std::sync::RwLock::new(Some(ctl.signal().unwrap()))));
3373        assert!(!cancels.latency_recent(Duration::from_secs(300)), "nothing has come");
3374        cancels.latency_request();
3375        assert!(worker.latency_wanted() && worker.yield_requested(), "short steps, and the running tick ends at its next");
3376        assert!(cancels.latency_recent(Duration::from_secs(300)));
3377        assert!(!cancels.latency_recent(Duration::ZERO), "past the hold");
3378    }
3379}