pub mod actions;
pub mod jetstream;
use anyhow::Result;
use std::collections::{BTreeMap, HashMap};
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::sync::{mpsc, oneshot, Mutex};
use crate::llm::action_helper::call_llm;
use crate::llm::actions::protocol_trait::ActionResult;
use crate::llm::ollama_client::OllamaClient;
use crate::logging::emit::Log;
use crate::protocol::Event;
use crate::server::connection::ConnectionId;
use crate::state::app_state::AppState;
use actions::{
NatsProtocol, NATS_CONNECT_EVENT, NATS_PUBLISH_EVENT, NATS_SUBSCRIBE_EVENT,
NATS_UNSUBSCRIBE_EVENT,
};
pub const MAX_CONTROL_LINE: usize = 4096;
const MAX_BUFFERED_SLACK: usize = MAX_CONTROL_LINE + 16;
const MAX_CONSECUTIVE_ACCEPT_ERRORS: u32 = 64;
const FRAME_QUEUE_CAPACITY: usize = 256;
const FIRST_FRAME_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
const IDLE_BETWEEN_FRAMES_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(600);
const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
const CONNECTION_CAP_REFUSAL: &[u8] = b"-ERR 'Maximum Connections Exceeded'\r\n";
pub struct NatsServer;
#[derive(Debug, Clone, PartialEq)]
pub enum Frame {
Connect(serde_json::Value),
Publish {
subject: String,
reply_to: Option<String>,
headers: BTreeMap<String, String>,
payload: Vec<u8>,
},
Subscribe {
subject: String,
queue_group: Option<String>,
sid: String,
},
Unsubscribe {
sid: String,
max_msgs: Option<u64>,
},
Ping,
Pong,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FrameError {
UnknownProtocolOperation,
MaximumControlLineExceeded,
MaximumPayloadViolation,
InvalidConnectConfig,
InvalidSubject,
}
impl FrameError {
pub fn wire_text(self) -> &'static str {
match self {
Self::UnknownProtocolOperation => "Unknown Protocol Operation",
Self::MaximumControlLineExceeded => "Maximum Control Line Exceeded",
Self::MaximumPayloadViolation => "Maximum Payload Violation",
Self::InvalidConnectConfig => "Invalid Connect Config",
Self::InvalidSubject => "Invalid Subject",
}
}
}
impl std::fmt::Display for FrameError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.wire_text())
}
}
pub fn parse_frame(buf: &[u8], max_payload: u64) -> Result<Option<(Frame, usize)>, FrameError> {
let skipped = blank_line_prefix_len(buf);
match parse_one_frame(&buf[skipped..], max_payload)? {
Some((frame, consumed)) => Ok(Some((frame, skipped + consumed))),
None => Ok(None),
}
}
pub fn blank_line_prefix_len(buf: &[u8]) -> usize {
let mut offset = 0;
while let Some(nl) = buf[offset..].iter().position(|&b| b == b'\n') {
let line_end = offset + nl;
let line = &buf[offset..line_end];
let line = line.strip_suffix(b"\r").unwrap_or(line);
if !line.iter().all(|b| b.is_ascii_whitespace()) {
break;
}
offset = line_end + 1;
}
offset
}
fn parse_one_frame(buf: &[u8], max_payload: u64) -> Result<Option<(Frame, usize)>, FrameError> {
let Some(nl) = buf.iter().position(|&b| b == b'\n') else {
if buf.len() > MAX_CONTROL_LINE {
return Err(FrameError::MaximumControlLineExceeded);
}
return Ok(None);
};
if nl > MAX_CONTROL_LINE {
return Err(FrameError::MaximumControlLineExceeded);
}
let mut line_end = nl;
if line_end > 0 && buf[line_end - 1] == b'\r' {
line_end -= 1;
}
let after_line = nl + 1;
let line = std::str::from_utf8(&buf[..line_end])
.map_err(|_| FrameError::UnknownProtocolOperation)?
.trim();
let mut parts = line.split_whitespace();
let verb = parts.next().ok_or(FrameError::UnknownProtocolOperation)?;
let args: Vec<&str> = parts.collect();
if verb.eq_ignore_ascii_case("PING") {
return Ok(Some((Frame::Ping, after_line)));
}
if verb.eq_ignore_ascii_case("PONG") {
return Ok(Some((Frame::Pong, after_line)));
}
if verb.eq_ignore_ascii_case("CONNECT") {
let json_start = line
.find(char::is_whitespace)
.ok_or(FrameError::InvalidConnectConfig)?;
let doc: serde_json::Value = serde_json::from_str(line[json_start..].trim())
.map_err(|_| FrameError::InvalidConnectConfig)?;
if !doc.is_object() {
return Err(FrameError::InvalidConnectConfig);
}
return Ok(Some((Frame::Connect(doc), after_line)));
}
if verb.eq_ignore_ascii_case("SUB") {
let (subject, queue_group, sid) = match args.len() {
2 => (args[0], None, args[1]),
3 => (args[0], Some(args[1].to_string()), args[2]),
_ => return Err(FrameError::UnknownProtocolOperation),
};
if subject.is_empty() {
return Err(FrameError::InvalidSubject);
}
return Ok(Some((
Frame::Subscribe {
subject: subject.to_string(),
queue_group,
sid: sid.to_string(),
},
after_line,
)));
}
if verb.eq_ignore_ascii_case("UNSUB") {
let sid = args.first().ok_or(FrameError::UnknownProtocolOperation)?;
let max_msgs = match args.get(1) {
Some(v) => Some(
v.parse::<u64>()
.map_err(|_| FrameError::UnknownProtocolOperation)?,
),
None => None,
};
if args.len() > 2 {
return Err(FrameError::UnknownProtocolOperation);
}
return Ok(Some((
Frame::Unsubscribe {
sid: sid.to_string(),
max_msgs,
},
after_line,
)));
}
let is_hpub = verb.eq_ignore_ascii_case("HPUB");
if is_hpub || verb.eq_ignore_ascii_case("PUB") {
let (subject, reply_to, header_len, total_len) = if is_hpub {
match args.len() {
3 => (args[0], None, parse_len(args[1])?, parse_len(args[2])?),
4 => (
args[0],
Some(args[1].to_string()),
parse_len(args[2])?,
parse_len(args[3])?,
),
_ => return Err(FrameError::UnknownProtocolOperation),
}
} else {
match args.len() {
2 => (args[0], None, 0, parse_len(args[1])?),
3 => (args[0], Some(args[1].to_string()), 0, parse_len(args[2])?),
_ => return Err(FrameError::UnknownProtocolOperation),
}
};
if subject.is_empty() {
return Err(FrameError::InvalidSubject);
}
if header_len > total_len {
return Err(FrameError::UnknownProtocolOperation);
}
if total_len as u64 > max_payload {
return Err(FrameError::MaximumPayloadViolation);
}
let Some(body_end) = after_line.checked_add(total_len) else {
return Err(FrameError::MaximumPayloadViolation);
};
if buf.len() <= body_end {
return Ok(None);
}
let terminator = match buf[body_end] {
b'\r' => {
if buf.len() < body_end + 2 {
return Ok(None);
}
if buf[body_end + 1] != b'\n' {
return Err(FrameError::UnknownProtocolOperation);
}
2
}
b'\n' => 1,
_ => return Err(FrameError::UnknownProtocolOperation),
};
let headers = if header_len > 0 {
parse_headers(&buf[after_line..after_line + header_len])
} else {
BTreeMap::new()
};
let payload = buf[after_line + header_len..body_end].to_vec();
return Ok(Some((
Frame::Publish {
subject: subject.to_string(),
reply_to,
headers,
payload,
},
body_end + terminator,
)));
}
Err(FrameError::UnknownProtocolOperation)
}
fn parse_len(token: &str) -> Result<usize, FrameError> {
token
.parse::<usize>()
.map_err(|_| FrameError::UnknownProtocolOperation)
}
pub fn parse_headers(block: &[u8]) -> BTreeMap<String, String> {
let mut out = BTreeMap::new();
let text = String::from_utf8_lossy(block);
for (index, line) in text.split('\n').enumerate() {
let line = line.trim_end_matches('\r');
if index == 0 || line.trim().is_empty() {
continue;
}
if let Some((name, value)) = line.split_once(':') {
let name = name.trim();
if !name.is_empty() {
out.insert(name.to_string(), value.trim().to_string());
}
}
}
out
}
pub fn subject_matches(pattern: &str, subject: &str) -> bool {
let pattern_tokens: Vec<&str> = pattern.split('.').collect();
let subject_tokens: Vec<&str> = subject.split('.').collect();
for (i, token) in pattern_tokens.iter().enumerate() {
if *token == ">" {
return i + 1 == pattern_tokens.len() && subject_tokens.len() > i;
}
if i >= subject_tokens.len() {
return false;
}
if *token != "*" && *token != subject_tokens[i] {
return false;
}
}
pattern_tokens.len() == subject_tokens.len()
}
#[derive(Debug, Clone)]
struct Subscription {
subject: String,
queue_group: Option<String>,
}
impl NatsServer {
#[allow(clippy::too_many_arguments)]
pub async fn spawn_with_llm_actions(
listen_addr: SocketAddr,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
server_id: crate::state::ServerId,
server_name: String,
max_payload: u64,
jetstream: Option<Arc<jetstream::Shared>>,
) -> Result<SocketAddr> {
let listener =
crate::server::socket_helpers::create_reusable_tcp_listener(listen_addr).await?;
let local_addr = listener.local_addr()?;
Log::new(Some(&status_tx)).info(format!(
"NATS server listening on {} (server_name={}, max_payload={})",
local_addr, server_name, max_payload
));
let protocol = Arc::new(NatsProtocol::new());
let task_registrar = app_state.clone();
let accept_handle = tokio::spawn(async move {
let mut consecutive_accept_errors = 0u32;
let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
loop {
match crate::server::accept_bounded::accept_bounded(
&listener,
&limiter,
CONNECTION_CAP_REFUSAL,
"NATS",
Some(&status_tx),
)
.await
{
Ok((stream, peer_addr, permit)) => {
consecutive_accept_errors = 0;
let connection_id =
ConnectionId::new(app_state.get_next_unified_id().await);
let local_addr_conn = stream.local_addr().unwrap_or(local_addr);
if let Err(e) = accept_connection(
stream,
peer_addr,
local_addr_conn,
connection_id,
server_id,
&llm_client,
&app_state,
&status_tx,
&protocol,
&server_name,
max_payload,
jetstream.clone(),
permit,
)
.await
{
Log::new(Some(&status_tx)).error(format!(
"NATS could not set up connection from {}: {}",
peer_addr, e
));
}
}
Err(e) => {
consecutive_accept_errors += 1;
if consecutive_accept_errors >= MAX_CONSECUTIVE_ACCEPT_ERRORS {
Log::new(Some(&status_tx)).error(format!(
"NATS accept failed {} times in a row ({}); listener stopping",
consecutive_accept_errors, e
));
break;
}
Log::new(Some(&status_tx))
.warn(format!("NATS accept error: {} - retrying", e));
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
}
}
}
});
task_registrar
.register_server_task(server_id, accept_handle)
.await;
Ok(local_addr)
}
}
#[allow(clippy::too_many_arguments)]
async fn accept_connection(
stream: TcpStream,
peer_addr: SocketAddr,
local_addr: SocketAddr,
connection_id: ConnectionId,
server_id: crate::state::ServerId,
llm_client: &OllamaClient,
app_state: &Arc<AppState>,
status_tx: &mpsc::UnboundedSender<String>,
protocol: &Arc<NatsProtocol>,
server_name: &str,
max_payload: u64,
jetstream: Option<Arc<jetstream::Shared>>,
permit: crate::server::accept_bounded::ConnectionPermit,
) -> Result<()> {
let permit = Arc::new(permit);
let (read_half, write_half) = tokio::io::split(stream);
let write_half = Arc::new(Mutex::new(write_half));
use crate::state::server::{
ConnectionState as ServerConnectionState, ConnectionStatus, ProtocolConnectionInfo,
};
let now = crate::utils::clock::Instant::now();
app_state
.add_connection_to_server(
server_id,
ServerConnectionState {
id: connection_id,
remote_addr: peer_addr,
local_addr,
bytes_sent: 0,
bytes_received: 0,
packets_sent: 0,
packets_received: 0,
last_activity: now,
status: ConnectionStatus::Active,
status_changed_at: now,
protocol_info: ProtocolConnectionInfo::new(serde_json::json!({
"state": "Idle"
})),
},
)
.await;
let _ = status_tx.send("__UPDATE_UI__".to_string());
let log = Log::new(Some(status_tx));
log.info(format!("NATS client connected from {}", peer_addr));
let peer_rx = crate::server::peer_support::register_peer_channel(
app_state,
server_id,
connection_id.as_u32(),
)
.await;
crate::server::peer_support::spawn_peer_command_task(
peer_rx,
protocol.clone(),
app_state.clone(),
server_id,
connection_id.as_u32(),
write_half.clone(),
status_tx.clone(),
);
let info = actions::build_info_json(
server_name,
max_payload,
&local_addr.ip().to_string(),
local_addr.port(),
connection_id.as_u32() as u64,
&peer_addr.ip().to_string(),
jetstream.is_some(),
);
let greeting = format!("INFO {}\r\n", info);
log.debug(format!("NATS -> {} INFO greeting", peer_addr));
log.trace(format!("NATS INFO: {}", info));
write_counted(
&write_half,
greeting.as_bytes(),
app_state,
server_id,
connection_id,
)
.await?;
let (frame_tx, frame_rx) = mpsc::channel::<Frame>(FRAME_QUEUE_CAPACITY);
let (close_tx, close_rx) = oneshot::channel::<()>();
let reader_permit = Arc::clone(&permit);
let reader = run_reader(
read_half,
write_half.clone(),
frame_tx,
close_rx,
app_state.clone(),
status_tx.clone(),
server_id,
connection_id,
peer_addr,
max_payload,
);
let reader_handle = tokio::spawn(async move {
let _permit = reader_permit;
reader.await
});
let dispatcher = run_dispatcher(
frame_rx,
close_tx,
write_half,
llm_client.clone(),
app_state.clone(),
status_tx.clone(),
server_id,
connection_id,
peer_addr,
protocol.clone(),
jetstream,
);
let dispatcher_handle = tokio::spawn(async move {
let _permit = permit;
dispatcher.await
});
app_state
.register_server_task(server_id, reader_handle)
.await;
app_state
.register_server_task(server_id, dispatcher_handle)
.await;
Ok(())
}
async fn write_counted<W>(
write_half: &Arc<Mutex<W>>,
data: &[u8],
app_state: &AppState,
server_id: crate::state::ServerId,
connection_id: ConnectionId,
) -> std::io::Result<()>
where
W: tokio::io::AsyncWrite + Unpin,
{
{
let mut writer = write_half.lock().await;
writer.write_all(data).await?;
writer.flush().await?;
}
app_state
.update_connection_stats(
server_id,
connection_id,
None,
Some(data.len() as u64),
None,
Some(1),
)
.await;
Ok(())
}
#[allow(clippy::too_many_arguments)]
async fn run_reader(
mut read_half: tokio::io::ReadHalf<TcpStream>,
write_half: Arc<Mutex<tokio::io::WriteHalf<TcpStream>>>,
frame_tx: mpsc::Sender<Frame>,
mut close_rx: oneshot::Receiver<()>,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
server_id: crate::state::ServerId,
connection_id: ConnectionId,
peer_addr: SocketAddr,
max_payload: u64,
) {
let log = Log::new(Some(&status_tx));
let mut buf: Vec<u8> = Vec::with_capacity(8192);
let mut chunk = vec![0u8; 8192];
let mut verbose = false;
let mut spoke_once = false;
'session: loop {
let read_deadline = if spoke_once {
IDLE_BETWEEN_FRAMES_TIMEOUT
} else {
FIRST_FRAME_READ_TIMEOUT
};
let n = tokio::select! {
_ = &mut close_rx => break 'session,
_ = tokio::time::sleep(read_deadline) => {
log.info(format!(
"NATS client {} sent nothing for {}s; closing \
decision=fail_closed_idle_timeout",
peer_addr,
read_deadline.as_secs()
));
let _ = write_counted(
&write_half,
b"-ERR 'Stale Connection'\r\n",
&app_state,
server_id,
connection_id,
)
.await;
let _ = write_half.lock().await.shutdown().await;
break 'session;
}
result = read_half.read(&mut chunk) => match result {
Ok(0) => {
log.info(format!("NATS client {} disconnected", peer_addr));
break 'session;
}
Ok(n) => n,
Err(e) => {
log.error(format!("NATS read error from {}: {}", peer_addr, e));
break 'session;
}
}
};
spoke_once = true;
buf.extend_from_slice(&chunk[..n]);
app_state
.update_connection_stats(
server_id,
connection_id,
Some(n as u64),
None,
Some(1),
None,
)
.await;
log.debug(format!("NATS received {} bytes from {}", n, peer_addr));
loop {
let blank = blank_line_prefix_len(&buf);
if blank > 0 {
buf.drain(..blank);
}
match parse_frame(&buf, max_payload) {
Ok(None) => {
if buf.len() as u64 > max_payload.saturating_add(MAX_BUFFERED_SLACK as u64) {
log.warn(format!(
"NATS buffered {} bytes from {} with no complete frame \
decision=fail_closed_buffer_too_large - closing connection",
buf.len(),
peer_addr
));
let _ = write_counted(
&write_half,
format!(
"-ERR '{}'\r\n",
FrameError::MaximumPayloadViolation.wire_text()
)
.as_bytes(),
&app_state,
server_id,
connection_id,
)
.await;
let _ = write_half.lock().await.shutdown().await;
break 'session;
}
break;
}
Ok(Some((frame, consumed))) => {
buf.drain(..consumed);
match &frame {
Frame::Ping => {
log.debug(format!("NATS PING from {} -> PONG", peer_addr));
if write_counted(
&write_half,
b"PONG\r\n",
&app_state,
server_id,
connection_id,
)
.await
.is_err()
{
break 'session;
}
continue;
}
Frame::Pong => {
log.trace(format!("NATS PONG from {}", peer_addr));
continue;
}
Frame::Connect(doc) => {
verbose = doc
.get("verbose")
.and_then(|v| v.as_bool())
.unwrap_or(false);
}
_ => {}
}
if verbose
&& write_counted(
&write_half,
b"+OK\r\n",
&app_state,
server_id,
connection_id,
)
.await
.is_err()
{
break 'session;
}
if frame_tx.send(frame).await.is_err() {
break 'session;
}
}
Err(e) => {
let decision = if e == FrameError::MaximumPayloadViolation {
"fail_closed_payload_too_large"
} else {
"fail_closed_protocol_error"
};
log.warn(format!(
"NATS protocol error from {}: {} decision={} - closing connection",
peer_addr, e, decision
));
let _ = write_counted(
&write_half,
format!("-ERR '{}'\r\n", e.wire_text()).as_bytes(),
&app_state,
server_id,
connection_id,
)
.await;
let _ = write_half.lock().await.shutdown().await;
break 'session;
}
}
}
}
app_state
.remove_peer_handle(server_id, connection_id.as_u32())
.await;
app_state
.close_connection_on_server(server_id, connection_id)
.await;
let _ = status_tx.send("__UPDATE_UI__".to_string());
}
#[allow(clippy::too_many_arguments)]
async fn run_dispatcher(
mut frame_rx: mpsc::Receiver<Frame>,
close_tx: oneshot::Sender<()>,
write_half: Arc<Mutex<tokio::io::WriteHalf<TcpStream>>>,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
server_id: crate::state::ServerId,
connection_id: ConnectionId,
peer_addr: SocketAddr,
protocol: Arc<NatsProtocol>,
jetstream: Option<Arc<jetstream::Shared>>,
) {
let log = Log::new(Some(&status_tx));
let mut subscriptions: HashMap<String, Subscription> = HashMap::new();
let mut close_tx = Some(close_tx);
while let Some(frame) = frame_rx.recv().await {
if let (
Some(js),
Frame::Publish {
subject,
reply_to,
headers,
payload,
},
) = (&jetstream, &frame)
{
if let Some(kind) = js.classify(subject) {
let sid_for = |inbox: &str| {
subscriptions
.iter()
.find(|(_, sub)| subject_matches(&sub.subject, inbox))
.map(|(sid, _)| sid.clone())
};
let ask = async |event: Event| {
call_llm(
&llm_client,
&app_state,
server_id,
Some(connection_id),
&event,
protocol.as_ref(),
)
.await
};
let bytes = jetstream::answer(
js,
kind,
jetstream::Frame {
subject,
reply_to: reply_to.as_deref(),
headers,
payload,
},
sid_for,
ask,
&log,
)
.await;
if !bytes.is_empty()
&& write_counted(&write_half, &bytes, &app_state, server_id, connection_id)
.await
.is_err()
{
break;
}
continue;
}
}
let event = match &frame {
Frame::Connect(doc) => Event::new(
&NATS_CONNECT_EVENT,
serde_json::json!({
"options": doc,
"client_name": doc.get("name").and_then(|v| v.as_str()).unwrap_or(""),
"lang": doc.get("lang").and_then(|v| v.as_str()).unwrap_or(""),
"verbose": doc.get("verbose").and_then(|v| v.as_bool()).unwrap_or(false),
}),
),
Frame::Publish {
subject,
reply_to,
headers,
payload,
} => {
let (payload_str, encoding) = if payload
.iter()
.all(|&b| b.is_ascii_graphic() || b.is_ascii_whitespace())
{
(String::from_utf8_lossy(payload).to_string(), "utf8")
} else {
(hex::encode(payload), "hex")
};
let matching: Vec<serde_json::Value> = subscriptions
.iter()
.filter(|(_, sub)| subject_matches(&sub.subject, subject))
.map(|(sid, sub)| {
serde_json::json!({
"sid": sid,
"subject": sub.subject,
"queue_group": sub.queue_group,
})
})
.collect();
Event::new(
&NATS_PUBLISH_EVENT,
serde_json::json!({
"subject": subject,
"reply_to": reply_to,
"payload": payload_str,
"payload_encoding": encoding,
"headers": headers,
"matching_subscriptions": matching,
}),
)
}
Frame::Subscribe {
subject,
queue_group,
sid,
} => {
subscriptions.insert(
sid.clone(),
Subscription {
subject: subject.clone(),
queue_group: queue_group.clone(),
},
);
Event::new(
&NATS_SUBSCRIBE_EVENT,
serde_json::json!({
"subject": subject,
"queue_group": queue_group,
"sid": sid,
}),
)
}
Frame::Unsubscribe { sid, max_msgs } => {
let subject = subscriptions.get(sid).map(|s| s.subject.clone());
if max_msgs.is_none() {
subscriptions.remove(sid);
}
Event::new(
&NATS_UNSUBSCRIBE_EVENT,
serde_json::json!({
"sid": sid,
"subject": subject,
"max_msgs": max_msgs,
}),
)
}
Frame::Ping | Frame::Pong => continue,
};
let outcome = call_llm(
&llm_client,
&app_state,
server_id,
Some(connection_id),
&event,
protocol.as_ref(),
)
.await;
let mut should_close = false;
match outcome {
Ok(result) => {
for message in &result.messages {
log.info(message);
}
let mut wrote_output = false;
let mut stack: Vec<ActionResult> = result.protocol_results;
stack.reverse();
while let Some(item) = stack.pop() {
match item {
ActionResult::Output(bytes) => {
log.debug(format!("NATS sent {} bytes to {}", bytes.len(), peer_addr));
log.trace(format!(
"NATS sent: {}",
crate::utils::truncate_for_log(
&String::from_utf8_lossy(&bytes),
512
)
));
if write_counted(
&write_half,
&bytes,
&app_state,
server_id,
connection_id,
)
.await
.is_err()
{
should_close = true;
break;
}
wrote_output = true;
}
ActionResult::Multiple(items) => {
for inner in items.into_iter().rev() {
stack.push(inner);
}
}
ActionResult::CloseConnection => should_close = true,
_ => {}
}
}
if !wrote_output && !should_close {
log.debug(format!(
"No NATS frame for {} on {}: decision=model_no_actions",
event.id(),
connection_id
));
}
}
Err(e) => {
let failure = crate::utils::WireFailure::classify(&e);
let class = if failure.is_overloaded() {
"overloaded"
} else {
"unavailable"
};
log.warn(format!(
"NATS LLM error for {} on {}: decision=fail_closed_llm_error class={class} error={e}",
event.id(),
connection_id
));
let _ = write_counted(
&write_half,
format!("-ERR '{}'\r\n", failure.prefixed_text()).as_bytes(),
&app_state,
server_id,
connection_id,
)
.await;
should_close = true;
}
}
if should_close {
let _ = write_half.lock().await.shutdown().await;
app_state
.remove_peer_handle(server_id, connection_id.as_u32())
.await;
app_state
.close_connection_on_server(server_id, connection_id)
.await;
let _ = status_tx.send("__UPDATE_UI__".to_string());
log.info(format!(
"NATS connection {} closed by the server",
connection_id
));
break;
}
}
if let Some(tx) = close_tx.take() {
let _ = tx.send(());
}
}