use std::io::{BufRead, BufReader, Read, Write};
use std::sync::mpsc::{Receiver, RecvTimeoutError, SyncSender, TryRecvError, sync_channel};
use std::sync::{Arc, Condvar, Mutex, MutexGuard, OnceLock, PoisonError};
use std::time::{Duration, Instant};
use super::semantic_detectors::NotInspected;
use crate::core::config::IntelligenceRuntimeConfig;
pub(super) const IDLE_SECONDS: u64 = 600;
const DRAIN_BOUND: Duration = Duration::from_secs(3);
const RESTART_BACKOFF: [Duration; 3] = [
Duration::from_secs(5),
Duration::from_secs(30),
Duration::from_mins(2),
];
const MAX_WAITING: usize = 1;
const MAX_REQUEST_LINE: usize = 1024 * 1024;
const MAX_WORKER_LINE: u64 = 1024 * 1024 + 4096;
const EVENT_QUEUE: usize = 4;
const STDERR_PREFIX: usize = 256;
pub(super) enum Event {
Line(Vec<u8>),
Closed,
Violation,
}
pub(super) trait Link: Send {
fn send(&mut self, line: Vec<u8>) -> bool;
fn stop(&mut self);
}
pub(super) struct Started {
pub(super) link: Box<dyn Link>,
pub(super) events: Receiver<Event>,
pub(super) version: &'static str,
pub(super) takes_lease: bool,
pub(super) lease: Option<String>,
}
pub(super) trait Launcher: Send + Sync {
fn launch(&self, config: &IntelligenceRuntimeConfig) -> Option<Started>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Phase {
Cold,
Warming,
Warm,
Failed,
}
impl Phase {
pub(super) const fn as_str(self) -> &'static str {
match self {
Self::Cold => "cold",
Self::Warming => "warming",
Self::Warm => "warm",
Self::Failed => "failed",
}
}
}
#[derive(Clone, Copy)]
pub(super) struct Timing {
pub(super) idle: Duration,
pub(super) drain: Duration,
pub(super) backoff: [Duration; 3],
}
impl Timing {
const PRODUCT: Self = Self {
idle: Duration::from_secs(IDLE_SECONDS),
drain: DRAIN_BOUND,
backoff: RESTART_BACKOFF,
};
}
struct Session {
link: Box<dyn Link>,
events: Receiver<Event>,
version: &'static str,
takes_lease: bool,
lease: Option<String>,
idle_since: Instant,
owed: Option<Instant>,
}
impl Drop for Session {
fn drop(&mut self) {
self.link.stop();
}
}
struct Inner {
key: Option<[u8; 32]>,
generation: u64,
phase: Phase,
session: Option<Session>,
busy: bool,
waiting: usize,
failures: usize,
retry_at: Option<Instant>,
versions: Vec<String>,
}
type Answer = Result<(String, bool), NotInspected>;
pub(super) struct Supervisor {
inner: Mutex<Inner>,
freed: Condvar,
launcher: Box<dyn Launcher>,
timing: Timing,
}
impl Supervisor {
pub(super) fn with(launcher: Box<dyn Launcher>, timing: Timing) -> Arc<Self> {
Arc::new(Self {
inner: Mutex::new(Inner {
key: None,
generation: 0,
phase: Phase::Cold,
session: None,
busy: false,
waiting: 0,
failures: 0,
retry_at: None,
versions: Vec::new(),
}),
freed: Condvar::new(),
launcher,
timing,
})
}
fn lock(&self) -> MutexGuard<'_, Inner> {
self.inner.lock().unwrap_or_else(PoisonError::into_inner)
}
fn retire_locked(inner: &mut Inner, key: Option<[u8; 32]>) {
inner.session = None;
inner.generation = inner.generation.wrapping_add(1);
inner.phase = Phase::Cold;
inner.busy = false;
inner.failures = 0;
inner.retry_at = None;
inner.versions.clear();
inner.key = key;
}
fn cool(inner: &mut Inner) {
inner.session = None;
inner.generation = inner.generation.wrapping_add(1);
inner.phase = Phase::Cold;
inner.versions.clear();
}
fn fail(&self, inner: &mut Inner, now: Instant) {
inner.session = None;
inner.generation = inner.generation.wrapping_add(1);
inner.busy = false;
inner.versions.clear();
inner.phase = Phase::Failed;
inner.retry_at = self
.timing
.backoff
.get(inner.failures)
.map(|wait| now + *wait);
inner.failures = inner.failures.saturating_add(1);
}
fn observe(&self, inner: &mut Inner, now: Instant) {
if inner.busy {
return;
}
loop {
let Some(session) = inner.session.as_mut() else {
return;
};
let event = match session.events.try_recv() {
Ok(event) => event,
Err(TryRecvError::Empty) => break,
Err(TryRecvError::Disconnected) => Event::Closed,
};
match (inner.phase, event) {
(Phase::Warming, Event::Line(line)) => {
let Some(versions) = ready(&line, session.version) else {
tracing::warn!(
"semantic worker failed: its ready line does not name contract {}",
session.version
);
return self.fail(inner, now);
};
inner.versions = versions;
inner.phase = Phase::Warm;
}
(Phase::Warm, Event::Line(_)) if session.owed.is_some() => session.owed = None,
(Phase::Warm, Event::Closed)
if session.owed.is_none()
&& now.saturating_duration_since(session.idle_since)
>= self.timing.idle =>
{
return Self::cool(inner);
}
(phase, event) => {
let what = match event {
Event::Line(_) => "an unrequested line",
Event::Closed => "its output ended",
Event::Violation => "a line outside the protocol",
};
tracing::warn!("semantic worker failed while {}: {what}", phase.as_str());
return self.fail(inner, now);
}
}
}
let overdue = inner
.session
.as_ref()
.and_then(|session| session.owed)
.is_some_and(|bound| now >= bound);
if overdue {
self.fail(inner, now);
}
}
fn start(self: &Arc<Self>, inner: &mut Inner, config: &IntelligenceRuntimeConfig) {
inner.session = None;
inner.generation = inner.generation.wrapping_add(1);
inner.phase = Phase::Warming;
inner.retry_at = None;
inner.versions.clear();
let generation = inner.generation;
let supervisor = Arc::clone(self);
let config = config.clone();
let spawned = std::thread::Builder::new()
.name("semantic-worker-start".to_owned())
.spawn(move || {
let session = supervisor.launcher.launch(&config).map(|started| Session {
link: started.link,
events: started.events,
version: started.version,
takes_lease: started.takes_lease,
lease: started.lease,
idle_since: Instant::now(),
owed: None,
});
let mut inner = supervisor.lock();
if inner.generation != generation {
return;
}
match session {
Some(session) => inner.session = Some(session),
None => supervisor.fail(&mut inner, Instant::now()),
}
});
if spawned.is_err() {
self.fail(inner, Instant::now());
}
}
pub(super) fn request(
self: &Arc<Self>,
config: &IntelligenceRuntimeConfig,
key: [u8; 32],
input: &str,
deadline: Instant,
deadline_unix_ms: u64,
) -> Answer {
let mut inner = self.lock();
if inner.key != Some(key) {
if inner.key.is_some() {
tracing::info!("semantic worker retired: the runtime settings changed");
}
Self::retire_locked(&mut inner, Some(key));
self.freed.notify_all();
}
let (mut session, generation) = loop {
let now = Instant::now();
self.observe(&mut inner, now);
match inner.phase {
Phase::Warm => {}
Phase::Warming => return Err(NotInspected::Warming),
Phase::Failed if inner.retry_at.is_none_or(|at| now < at) => {
return Err(NotInspected::Unavailable);
}
Phase::Cold | Phase::Failed => {
self.start(&mut inner, config);
return Err(NotInspected::Warming);
}
}
if !inner.busy {
let Some(session) = inner.session.take() else {
self.fail(&mut inner, now);
return Err(NotInspected::Unavailable);
};
inner.busy = true;
break (session, inner.generation);
}
let wait = deadline.saturating_duration_since(now);
if inner.waiting >= MAX_WAITING || wait.is_zero() {
return Err(NotInspected::Unavailable);
}
inner.waiting += 1;
let (guard, _) = self
.freed
.wait_timeout(inner, wait)
.unwrap_or_else(PoisonError::into_inner);
inner = guard;
inner.waiting -= 1;
};
drop(inner);
let (answer, intact) = exchange(
&mut session,
input,
deadline,
deadline_unix_ms,
self.timing.drain,
);
let mut inner = self.lock();
if inner.generation == generation {
inner.busy = false;
if intact {
if answer.is_ok() {
inner.failures = 0;
}
inner.session = Some(session);
} else {
self.fail(&mut inner, Instant::now());
}
}
drop(inner);
self.freed.notify_all();
answer
}
pub(super) fn retire(&self) {
let mut inner = self.lock();
if inner.key.is_some() {
tracing::info!("semantic worker retired: runtime switched off or package moved");
Self::retire_locked(&mut inner, None);
self.freed.notify_all();
}
}
pub(super) fn phase(&self, key: [u8; 32]) -> (Phase, Vec<String>) {
let mut inner = self.lock();
if inner.key != Some(key) {
return (Phase::Cold, Vec::new());
}
self.observe(&mut inner, Instant::now());
(inner.phase, inner.versions.clone())
}
}
fn exchange(
session: &mut Session,
input: &str,
deadline: Instant,
deadline_unix_ms: u64,
drain: Duration,
) -> (Answer, bool) {
let unavailable = |intact: bool| (Err(NotInspected::Unavailable), intact);
if let Some(bound) = session.owed {
let wait = bound
.min(deadline)
.saturating_duration_since(Instant::now());
match session.events.recv_timeout(wait) {
Ok(Event::Line(_)) => session.owed = None,
Err(RecvTimeoutError::Timeout) => return unavailable(Instant::now() < bound),
Ok(Event::Closed | Event::Violation) | Err(RecvTimeoutError::Disconnected) => {
return unavailable(false);
}
}
}
if session.takes_lease {
let current = super::lease::current_lease_b64();
if let Some(lease) = current.as_deref()
&& current != session.lease
{
if !session.link.send(lease_line(lease)) {
return unavailable(false);
}
session.lease = current;
}
}
let Some(line) = request_line(session.version, deadline_unix_ms, input) else {
return unavailable(true);
};
let now = Instant::now();
let wait = deadline.saturating_duration_since(now);
if wait.is_zero() {
return unavailable(true);
}
session.idle_since = now;
if !session.link.send(line) {
return unavailable(false);
}
match session.events.recv_timeout(wait) {
Ok(Event::Line(line)) => match reply(&line) {
Reply::Output(output) => (Ok((output, session.version == "1.1.0")), true),
Reply::Refused => unavailable(true),
Reply::Invalid => unavailable(false),
},
Err(RecvTimeoutError::Timeout) => {
session.owed = Some(Instant::now() + drain);
unavailable(true)
}
Ok(Event::Closed | Event::Violation) | Err(RecvTimeoutError::Disconnected) => {
unavailable(false)
}
}
}
fn lease_line(lease: &str) -> Vec<u8> {
let mut line = serde_json::json!({ "lease_b64": lease })
.to_string()
.into_bytes();
line.push(b'\n');
line
}
fn request_line(version: &str, deadline_unix_ms: u64, input: &str) -> Option<Vec<u8>> {
let mut line = serde_json::to_vec(&serde_json::json!({
"version": version,
"deadline_unix_ms": deadline_unix_ms,
"input": input,
}))
.ok()?;
line.push(b'\n');
(line.len() <= MAX_REQUEST_LINE).then_some(line)
}
fn ready(line: &[u8], negotiated: &str) -> Option<Vec<String>> {
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct Ready {
schema_version: u32,
status: String,
versions: Vec<String>,
}
let ready: Ready = serde_json::from_slice(line).ok()?;
let valid = {
let named: Vec<&str> = ready.versions.iter().map(String::as_str).collect();
ready.schema_version == 1
&& ready.status == "ready"
&& matches!(named.as_slice(), ["1.1.0"] | ["1.0.0", "1.1.0"])
&& named.contains(&negotiated)
};
valid.then_some(ready.versions)
}
enum Reply {
Output(String),
Refused,
Invalid,
}
fn reply(line: &[u8]) -> Reply {
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct Wire {
status: String,
#[serde(default)]
output: Option<String>,
#[serde(default)]
reason: Option<String>,
}
let Ok(wire) = serde_json::from_slice::<Wire>(line) else {
return Reply::Invalid;
};
match (wire.status.as_str(), wire.output, wire.reason.as_deref()) {
("ok", Some(output), None) => Reply::Output(output),
("refused", None, Some("entitlement" | "unsupported" | "input" | "inference")) => {
Reply::Refused
}
_ => Reply::Invalid,
}
}
struct ProcessLauncher;
struct ProcessLink {
child: Option<std::process::Child>,
lines: Option<SyncSender<Vec<u8>>>,
prepared: Option<super::health::Prepared>,
}
impl Link for ProcessLink {
fn send(&mut self, line: Vec<u8>) -> bool {
self.lines
.as_ref()
.is_some_and(|lines| lines.try_send(line).is_ok())
}
fn stop(&mut self) {
self.lines = None;
if let Some(mut child) = self.child.take() {
let _ = child.kill();
let _ = child.wait();
}
if let Some(prepared) = self.prepared.take() {
let _ = prepared.close();
}
}
}
impl Drop for ProcessLink {
fn drop(&mut self) {
self.stop();
}
}
impl Launcher for ProcessLauncher {
fn launch(&self, config: &IntelligenceRuntimeConfig) -> Option<Started> {
let step = std::cell::Cell::new("trust_key");
let started = Self::launch_at(config, &step);
if started.is_none() {
tracing::warn!("semantic worker not started: step {}", step.get());
}
started
}
}
impl ProcessLauncher {
fn launch_at(
config: &IntelligenceRuntimeConfig,
step: &std::cell::Cell<&'static str>,
) -> Option<Started> {
use std::process::Stdio;
let root = std::path::Path::new(&config.root);
let trust = super::trust_key(&config.trust_key_hex).ok()?;
step.set("package_authentication");
let package =
super::install::verified_active(root, &config.manifest_sha256, &trust).ok()?;
step.set("contract_version");
let version = super::health::semantic_detectors_version(&package)?;
step.set("prepare");
let (prepared, mut command) = super::health::prepare(&package, root).ok()?;
step.set("model_set");
super::health::name_semantic_models(&package, &mut command)
.ok()
.flatten()?;
step.set("lease_configuration");
command
.arg("--semantic-serve")
.arg(IDLE_SECONDS.to_string())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let takes_lease = super::session::takes_lease(&package);
let lease = takes_lease.then(super::lease::current_lease_b64).flatten();
#[cfg(any(unix, windows))]
if package.description["entitlement_required"] == true {
match super::license::configuration_path(root, &config.manifest_sha256, &trust) {
Ok(configuration) => {
command.env("LEANCTX_INTELLIGENCE_LICENSE_CONFIG", configuration);
}
Err(_) if lease.is_some() => {}
Err(_) => return None,
}
}
step.set("spawn");
let child = command.spawn().ok()?;
step.set("pipes");
let mut link = ProcessLink {
child: Some(child),
lines: None,
prepared: Some(prepared),
};
let child = link.child.as_mut()?;
let input = child.stdin.take()?;
let output = child.stdout.take()?;
let errors = child.stderr.take()?;
let (lines, queued) = sync_channel::<Vec<u8>>(2);
let (events, incoming) = sync_channel(EVENT_QUEUE);
on_thread("semantic-worker-input", move || write_lines(input, &queued))?;
on_thread("semantic-worker-output", move || {
read_lines(output, &events);
})?;
on_thread("semantic-worker-errors", move || drain_errors(errors))?;
link.lines = Some(lines);
if let Some(lease) = lease.as_deref()
&& !link.send(lease_line(lease))
{
return None;
}
Some(Started {
link: Box::new(link),
events: incoming,
version,
takes_lease,
lease,
})
}
}
fn on_thread(name: &str, work: impl FnOnce() + Send + 'static) -> Option<()> {
std::thread::Builder::new()
.name(name.to_owned())
.spawn(work)
.ok()
.map(drop)
}
fn write_lines(mut input: impl Write, queued: &Receiver<Vec<u8>>) {
for line in queued {
if input.write_all(&line).and_then(|()| input.flush()).is_err() {
return;
}
}
}
fn read_lines(output: impl Read, events: &SyncSender<Event>) {
let mut reader = BufReader::new(output);
loop {
let mut line = Vec::new();
let read = (&mut reader)
.take(MAX_WORKER_LINE + 1)
.read_until(b'\n', &mut line);
let event = match read {
Ok(0) => Event::Closed,
Ok(_) if line.last() == Some(&b'\n') => Event::Line(line),
_ => Event::Violation,
};
let last = !matches!(event, Event::Line(_));
if events.send(event).is_err() || last {
return;
}
}
}
fn drain_errors(mut errors: impl Read) {
let mut prefix = [0_u8; STDERR_PREFIX];
let mut kept = 0;
while kept < prefix.len() {
match errors.read(&mut prefix[kept..]) {
Ok(0) | Err(_) => break,
Ok(read) => kept += read,
}
}
let _ = std::io::copy(&mut errors, &mut std::io::sink());
if kept > 0 {
let line: String = prefix[..kept]
.iter()
.take_while(|byte| **byte != b'\n')
.map(|byte| {
if byte.is_ascii_graphic() || *byte == b' ' {
char::from(*byte)
} else {
'?'
}
})
.collect();
tracing::warn!("semantic worker ended: {line}");
}
}
static GLOBAL: OnceLock<Arc<Supervisor>> = OnceLock::new();
fn key_of(config: &IntelligenceRuntimeConfig) -> Option<[u8; 32]> {
let settings = serde_json::to_vec(config).ok()?;
Some(*blake3::hash(&settings).as_bytes())
}
pub(super) fn request(
config: &IntelligenceRuntimeConfig,
input: &str,
deadline: Instant,
deadline_unix_ms: u64,
) -> Answer {
let key = key_of(config).ok_or(NotInspected::Unavailable)?;
GLOBAL
.get_or_init(|| Supervisor::with(Box::new(ProcessLauncher), Timing::PRODUCT))
.request(config, key, input, deadline, deadline_unix_ms)
}
pub(super) fn retire() {
if let Some(supervisor) = GLOBAL.get() {
supervisor.retire();
}
}
pub(super) fn state(config: &IntelligenceRuntimeConfig) -> (&'static str, Vec<String>) {
let observed = key_of(config)
.zip(GLOBAL.get())
.map(|(key, supervisor)| supervisor.phase(key));
match observed {
Some((phase, versions)) => (phase.as_str(), versions),
None => (Phase::Cold.as_str(), Vec::new()),
}
}