pub mod actions;
use anyhow::{Context, Result};
use bytes::Bytes;
use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::{mpsc, Mutex};
use tokio_rustls::TlsAcceptor;
use tracing::{debug, error, info};
use super::connection::ConnectionId;
use crate::llm::action_helper::call_llm;
use crate::llm::ollama_client::OllamaClient;
use crate::llm::ActionResult;
use crate::logging::emit::Log;
use crate::protocol::Event;
use crate::server::TlsProtocol;
use crate::state::app_state::AppState;
use crate::utils::WireFailure;
use actions::{TLS_CONNECTION_OPENED_EVENT, TLS_DATA_RECEIVED_EVENT};
const HANDSHAKE_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(60);
const FIRST_RECORD_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(300);
#[derive(Clone, Copy)]
struct ReadDeadlines {
handshake: std::time::Duration,
first_byte: std::time::Duration,
idle: std::time::Duration,
}
impl ReadDeadlines {
fn resolve(
handshake_secs: Option<u64>,
first_byte_secs: Option<u64>,
idle_secs: Option<u64>,
) -> Self {
Self {
handshake: handshake_secs
.map(std::time::Duration::from_secs)
.unwrap_or(HANDSHAKE_READ_TIMEOUT),
first_byte: first_byte_secs
.map(std::time::Duration::from_secs)
.unwrap_or(FIRST_RECORD_READ_TIMEOUT),
idle: idle_secs
.map(std::time::Duration::from_secs)
.unwrap_or(IDLE_AFTER_DATA_TIMEOUT),
}
}
}
const IDLE_AFTER_DATA_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(300);
const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
pub const MAX_QUEUED_BYTES: usize = 1024 * 1024;
const LINGER_DRAIN_BYTES: usize = 8 * 1024 * 1024;
const LINGER_DRAIN_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
const CONNECTION_CAP_REFUSAL: &[u8] = &[0x15, 0x03, 0x03, 0x00, 0x02, 0x02, 0x50];
fn llm_error_decision(err: &anyhow::Error) -> &'static str {
match WireFailure::classify(err) {
WireFailure::Overloaded => "fail_closed_llm_error_overloaded",
WireFailure::Unavailable => "fail_closed_llm_error_unavailable",
}
}
#[derive(Debug, Clone, PartialEq)]
enum ConnectionState {
Idle,
Processing,
Accumulating,
}
struct ConnectionData {
state: ConnectionState,
queued_data: Vec<u8>,
write_half: Arc<Mutex<tokio::io::WriteHalf<tokio_rustls::server::TlsStream<TcpStream>>>>,
}
pub struct TlsServer;
impl TlsServer {
#[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>,
send_first: bool,
server_id: crate::state::ServerId,
tls_config: Option<Arc<rustls::ServerConfig>>,
handshake_timeout_secs: Option<u64>,
first_byte_timeout_secs: Option<u64>,
idle_timeout_secs: Option<u64>,
) -> Result<SocketAddr> {
let deadlines = ReadDeadlines::resolve(
handshake_timeout_secs,
first_byte_timeout_secs,
idle_timeout_secs,
);
let tls_config = if let Some(config) = tls_config {
config
} else {
crate::server::tls_cert_manager::generate_default_tls_config()
.context("Failed to generate TLS configuration")?
};
let listener = TcpListener::bind(listen_addr)
.await
.context("Failed to bind TLS TCP listener")?;
let local_addr = listener.local_addr()?;
Log::new(Some(&status_tx)).info(format!(
"TLS server (action-based) listening on {}",
local_addr
));
let connections = Arc::new(Mutex::new(HashMap::new()));
let protocol = Arc::new(TlsProtocol::new());
let acceptor = TlsAcceptor::from(tls_config);
let task_registrar = app_state.clone();
let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
let accept_handle = tokio::spawn(async move {
loop {
match crate::server::accept_bounded::accept_bounded(
&listener,
&limiter,
CONNECTION_CAP_REFUSAL,
"TLS",
Some(&status_tx),
)
.await
{
Ok((stream, remote_addr, permit)) => {
let connection_id =
ConnectionId::new(app_state.get_next_unified_id().await);
let local_addr_conn = stream.local_addr().unwrap_or(local_addr);
debug!("TLS TCP connection from {}", remote_addr);
let acceptor = acceptor.clone();
let llm_client_clone = llm_client.clone();
let app_state_clone = app_state.clone();
let status_tx_clone = status_tx.clone();
let connections_clone = connections.clone();
let protocol_clone = protocol.clone();
let task_owner = app_state.clone();
task_owner
.spawn_server_task(server_id, async move {
let _permit = permit;
let handshake = tokio::time::timeout(
deadlines.handshake,
acceptor.accept(stream),
);
let tls_stream = match handshake.await {
Err(_) => {
Log::new(Some(&status_tx_clone)).warn(format!(
"TLS handshake with {} produced nothing within {}s; \
closing",
remote_addr,
deadlines.handshake.as_secs()
));
return;
}
Ok(Ok(stream)) => stream,
Ok(Err(e)) => {
Log::new(Some(&status_tx_clone)).warn(format!(
"TLS handshake failed with {}: {}",
remote_addr, e
));
return;
}
};
Log::new(Some(&status_tx_clone))
.debug(format!("TLS handshake complete with {}", remote_addr));
info!(
"Accepted TLS connection {} from {}",
connection_id, remote_addr
);
let (read_half, write_half) = tokio::io::split(tls_stream);
let write_half_arc = Arc::new(Mutex::new(write_half));
let activity = Arc::new(
crate::server::accept_bounded::ConnectionActivity::new(),
);
use crate::state::server::{
ConnectionState as ServerConnectionState, ConnectionStatus,
ProtocolConnectionInfo,
};
let now = crate::utils::clock::Instant::now();
let conn_state = ServerConnectionState {
id: connection_id,
remote_addr,
local_addr: local_addr_conn,
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",
"tls_handshake": "complete"
})),
};
app_state_clone
.add_connection_to_server(server_id, conn_state)
.await;
let _ = status_tx_clone.send("__UPDATE_UI__".to_string());
connections_clone.lock().await.insert(
connection_id,
ConnectionData {
state: if send_first {
ConnectionState::Processing
} else {
ConnectionState::Idle
},
queued_data: Vec::new(),
write_half: write_half_arc.clone(),
},
);
let peer_rx = crate::server::peer_support::register_peer_channel(
&app_state_clone,
server_id,
connection_id.as_u32(),
)
.await;
crate::server::peer_support::spawn_peer_command_task(
peer_rx,
protocol_clone.clone(),
app_state_clone.clone(),
server_id,
connection_id.as_u32(),
write_half_arc.clone(),
status_tx_clone.clone(),
);
let (opened_tx, opened_rx) = tokio::sync::oneshot::channel::<()>();
let (reader_alive_tx, reader_alive_rx) =
tokio::sync::oneshot::channel::<()>();
if send_first {
let llm_client_for_conn = llm_client_clone.clone();
let app_state_for_conn = app_state_clone.clone();
let status_tx_for_conn = status_tx_clone.clone();
let connections_for_conn = connections_clone.clone();
let write_half_for_conn = write_half_arc.clone();
let protocol_for_conn = protocol_clone.clone();
let opened_busy = activity.busy();
let task_owner = app_state_clone.clone();
task_owner
.spawn_server_task(server_id, async move {
let _opened_busy = opened_busy;
Self::handle_connection_opened(
connection_id,
server_id,
send_first,
opened_tx,
reader_alive_rx,
llm_client_for_conn,
app_state_for_conn,
status_tx_for_conn,
connections_for_conn,
write_half_for_conn,
protocol_for_conn,
)
.await;
})
.await;
} else {
drop(opened_tx);
drop(reader_alive_rx);
}
let llm_client_for_read = llm_client_clone.clone();
let app_state_for_read = app_state_clone.clone();
let status_tx_for_read = status_tx_clone.clone();
let connections_for_read = connections_clone.clone();
let protocol_for_read = protocol_clone.clone();
let activity_for_read = Arc::clone(&activity);
let write_half_for_read = write_half_arc.clone();
let task_owner = app_state_clone.clone();
task_owner
.spawn_server_task(server_id, async move {
let _reader_alive = reader_alive_tx;
let mut buffer = vec![0u8; 8192];
let mut read_half = read_half;
let mut seen_data = false;
if send_first {
let _ = opened_rx.await;
} else {
drop(opened_rx);
}
loop {
let read_timeout = if seen_data {
deadlines.idle
} else {
deadlines.first_byte
};
let read = loop {
match tokio::time::timeout(
read_timeout,
read_half.read(&mut buffer),
)
.await
{
Ok(read) => break Some(read),
Err(_) => {
if activity_for_read.idle_for().is_none() {
continue;
}
Log::new(Some(&status_tx_for_read)).info(
format!(
"TLS connection {connection_id} sent nothing \
for {}s; closing idle connection",
read_timeout.as_secs()
),
);
break None;
}
}
};
let Some(read) = read else {
connections_for_read
.lock()
.await
.remove(&connection_id);
app_state_for_read
.close_connection_on_server(
server_id,
connection_id,
)
.await;
let _ = status_tx_for_read
.send("__UPDATE_UI__".to_string());
break;
};
match read {
Ok(0) => {
connections_for_read
.lock()
.await
.remove(&connection_id);
app_state_for_read
.close_connection_on_server(
server_id,
connection_id,
)
.await;
Log::new(Some(&status_tx_for_read)).info(
format!(
"TLS connection {connection_id} closed"
),
);
let _ = status_tx_for_read
.send("__UPDATE_UI__".to_string());
break;
}
Ok(n) => {
seen_data = true;
let queued_state = {
let conns =
connections_for_read.lock().await;
conns.get(&connection_id).map(|c| {
(c.state.clone(), c.queued_data.len())
})
};
if let Some((
ConnectionState::Processing,
queued,
)) = queued_state
{
if queued.saturating_add(n)
> MAX_QUEUED_BYTES
{
Self::refuse_queued_data_overflow(
connection_id,
server_id,
queued.saturating_add(n),
&mut read_half,
&write_half_for_read,
&connections_for_read,
&app_state_for_read,
&status_tx_for_read,
)
.await;
break;
}
}
let data = Bytes::copy_from_slice(&buffer[..n]);
app_state_for_read
.update_connection_stats(
server_id,
connection_id,
Some(n as u64),
None,
Some(1),
None,
)
.await;
let log = Log::new(Some(&status_tx_for_read));
if data.iter().all(|&b| {
b.is_ascii_graphic()
|| b.is_ascii_whitespace()
}) {
let data_str =
String::from_utf8_lossy(&data);
let preview = if data_str.len() > 100 {
format!("{}...", &data_str[..100])
} else {
data_str.to_string()
};
log.debug(format!(
"TLS received {} bytes on {}: {}",
n, connection_id, preview
));
log.trace(format!(
"TLS data (text): {:?}",
data_str
));
} else {
log.debug(format!(
"TLS received {} bytes on {} (binary data)",
n, connection_id
));
log.trace(format!(
"TLS data (hex): {}",
hex::encode(&data)
));
}
let llm_clone = llm_client_for_read.clone();
let state_clone = app_state_for_read.clone();
let status_clone = status_tx_for_read.clone();
let conns_clone = connections_for_read.clone();
let protocol_clone = protocol_for_read.clone();
activity_for_read.begin_work();
let activity_for_handler =
Arc::clone(&activity_for_read);
let task_owner = app_state_for_read.clone();
task_owner
.spawn_server_task(server_id, async move {
Self::handle_data_with_actions(
connection_id,
server_id,
data,
llm_clone,
state_clone,
status_clone,
conns_clone,
protocol_clone,
)
.await;
activity_for_handler.end_work();
})
.await;
}
Err(e) => {
Log::new(Some(&status_tx_for_read)).error(
format!(
"Read error on {}: {}",
connection_id, e
),
);
connections_for_read
.lock()
.await
.remove(&connection_id);
app_state_for_read
.close_connection_on_server(
server_id,
connection_id,
)
.await;
let _ = status_tx_for_read
.send("__UPDATE_UI__".to_string());
break;
}
}
}
app_state_for_read
.remove_peer_handle(server_id, connection_id.as_u32())
.await;
})
.await;
})
.await;
}
Err(e) => {
Log::new(Some(&status_tx)).error(format!("Accept error: {}", e));
break;
}
}
}
});
task_registrar
.register_server_task(server_id, accept_handle)
.await;
Ok(local_addr)
}
#[allow(clippy::too_many_arguments)]
async fn handle_connection_opened(
connection_id: ConnectionId,
server_id: crate::state::ServerId,
send_first: bool,
opened: tokio::sync::oneshot::Sender<()>,
reader_alive: tokio::sync::oneshot::Receiver<()>,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
connections: Arc<Mutex<HashMap<ConnectionId, ConnectionData>>>,
write_half: Arc<Mutex<tokio::io::WriteHalf<tokio_rustls::server::TlsStream<TcpStream>>>>,
protocol: Arc<TlsProtocol>,
) {
let log = Log::new(Some(&status_tx));
let event = Event::new(
&TLS_CONNECTION_OPENED_EVENT,
crate::protocol::event_type::connect_event_data(),
);
let answer = tokio::select! {
answer = call_llm(
&llm_client,
&app_state,
server_id,
Some(connection_id),
&event,
protocol.as_ref(),
) => answer,
_ = reader_alive => {
log.debug(format!(
"TLS connection {connection_id} closed before its connect event was \
answered: decision=peer_left_before_answer"
));
return;
}
};
match answer {
Ok(execution_result) => {
debug!("LLM TLS connect-event response received");
for msg in execution_result.messages {
let _ = status_tx.send(msg);
}
let mut wrote = false;
for protocol_result in execution_result.protocol_results {
match protocol_result {
ActionResult::Output(output_data) => {
let mut write = write_half.lock().await;
if let Err(e) = write.write_all(&output_data).await {
log.error(format!("Failed to send greeting: {}", e));
} else {
drop(write);
log.debug(format!(
"TLS sent {} bytes to {}",
output_data.len(),
connection_id
));
app_state
.update_connection_stats(
server_id,
connection_id,
None,
Some(output_data.len() as u64),
None,
Some(1),
)
.await;
wrote = true;
}
}
ActionResult::CloseConnection => {
connections.lock().await.remove(&connection_id);
if let Err(e) = write_half.lock().await.shutdown().await {
debug!("TLS shutdown on {} returned: {}", connection_id, e);
}
log.info(format!(
"Closed TLS connection {connection_id} on connect: \
decision=model_close"
));
let _ = opened.send(());
return;
}
_ => {}
}
}
if wrote {
log.debug(format!(
"TLS connection {connection_id} greeted: decision=model_answer"
));
} else if send_first {
log.warn(format!(
"TLS connection {connection_id} decision=model_silent: send_first was \
requested but the model produced no greeting"
));
} else {
log.debug(format!(
"TLS connection {connection_id} connect: decision=model_no_actions"
));
}
}
Err(e) if send_first => {
let decision = llm_error_decision(&e);
error!(
"TLS connection {} decision={}: LLM call failed generating greeting: {:#}",
connection_id, decision, e
);
log.warn(format!(
"TLS connection {connection_id} decision={decision}: no greeting \
generated, closing with close_notify"
));
{
let mut write = write_half.lock().await;
if let Err(shutdown_err) = write.shutdown().await {
debug!(
"TLS shutdown on {} returned: {}",
connection_id, shutdown_err
);
}
}
connections.lock().await.remove(&connection_id);
app_state
.remove_peer_handle(server_id, connection_id.as_u32())
.await;
app_state
.close_connection_on_server(server_id, connection_id)
.await;
let _ = opened.send(());
return;
}
Err(e) => {
log.warn(format!(
"TLS connect event for {connection_id} failed: \
decision=connect_event_failed error={e}"
));
}
}
let _ = opened.send(());
let queued = {
let mut conns = connections.lock().await;
let Some(conn) = conns.get_mut(&connection_id) else {
return;
};
conn.state = ConnectionState::Idle;
!conn.queued_data.is_empty()
};
if queued {
Self::handle_data_with_actions(
connection_id,
server_id,
Bytes::new(),
llm_client,
app_state,
status_tx,
connections,
protocol,
)
.await;
}
}
#[allow(clippy::too_many_arguments)]
async fn refuse_queued_data_overflow<R>(
connection_id: ConnectionId,
server_id: crate::state::ServerId,
would_be: usize,
read_half: &mut R,
write_half: &Arc<Mutex<tokio::io::WriteHalf<tokio_rustls::server::TlsStream<TcpStream>>>>,
connections: &Arc<Mutex<HashMap<ConnectionId, ConnectionData>>>,
app_state: &Arc<AppState>,
status_tx: &mpsc::UnboundedSender<String>,
) where
R: tokio::io::AsyncRead + Unpin,
{
Log::new(Some(status_tx)).warn(format!(
"TLS connection {connection_id} decision=fail_closed_queued_data_overflow: peer \
queued {would_be} bytes while an answer was in flight, limit is {MAX_QUEUED_BYTES}; \
closing with close_notify (TLS record_overflow is not reachable through rustls)"
));
{
let mut write = write_half.lock().await;
if let Err(e) = write.shutdown().await {
debug!(
"TLS shutdown on {} after overflow returned: {}",
connection_id, e
);
}
}
let mut drained = 0usize;
let mut scratch = vec![0u8; 64 * 1024];
let _ = tokio::time::timeout(LINGER_DRAIN_TIMEOUT, async {
while drained < LINGER_DRAIN_BYTES {
match read_half.read(&mut scratch).await {
Ok(0) | Err(_) => break,
Ok(n) => drained = drained.saturating_add(n),
}
}
})
.await;
debug!("TLS {connection_id}: drained {drained} bytes after refusing the queue");
connections.lock().await.remove(&connection_id);
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());
}
async fn handle_data_with_actions(
connection_id: ConnectionId,
server_id: crate::state::ServerId,
data: Bytes,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
connections: Arc<Mutex<HashMap<ConnectionId, ConnectionData>>>,
protocol: Arc<TlsProtocol>,
) {
let current_state = {
let conns = connections.lock().await;
if let Some(conn_data) = conns.get(&connection_id) {
conn_data.state.clone()
} else {
debug!(
"TLS connection {} is no longer registered; dropping {} received bytes",
connection_id,
data.len()
);
return;
}
};
if current_state == ConnectionState::Processing {
let mut refused = None;
connections
.lock()
.await
.entry(connection_id)
.and_modify(|conn| {
let would_be = conn.queued_data.len().saturating_add(data.len());
if would_be > MAX_QUEUED_BYTES {
refused = Some(would_be);
} else {
conn.queued_data.extend_from_slice(&data);
}
});
match refused {
Some(would_be) => Log::new(Some(&status_tx)).warn(format!(
"TLS connection {connection_id} decision=fail_closed_queued_data_overflow: \
dropped {} bytes that would have taken the queue to {would_be}, limit is \
{MAX_QUEUED_BYTES}",
data.len()
)),
None => Log::new(Some(&status_tx)).debug(format!(
"Queued {} bytes for {}",
data.len(),
connection_id
)),
}
return;
}
let mut all_data = {
let mut conns = connections.lock().await;
let Some(conn_data) = conns.get_mut(&connection_id) else {
return; };
conn_data.state = ConnectionState::Processing;
let mut merged = conn_data.queued_data.clone();
merged.extend_from_slice(&data);
conn_data.queued_data.clear();
Bytes::from(merged)
};
loop {
let write_half = {
let conns = connections.lock().await;
conns.get(&connection_id).map(|c| c.write_half.clone())
};
let Some(write_half) = write_half else {
debug!(
"TLS connection {} went away before its response could be written",
connection_id
);
return;
};
let printable = all_data
.iter()
.all(|&b| b.is_ascii_graphic() || b.is_ascii_whitespace());
let (data_str, encoding) = if printable {
(String::from_utf8_lossy(&all_data).to_string(), "utf8")
} else {
(hex::encode(&all_data), "hex")
};
let event = Event::new(
&TLS_DATA_RECEIVED_EVENT,
serde_json::json!({
"data": data_str,
"encoding": encoding
}),
);
match call_llm(
&llm_client,
&app_state,
server_id,
Some(connection_id),
&event,
protocol.as_ref(),
)
.await
{
Ok(execution_result) => {
debug!("LLM TLS response received");
for msg in execution_result.messages {
let _ = status_tx.send(msg);
}
let mut should_close = false;
let mut should_wait = false;
let mut acted = false;
for protocol_result in execution_result.protocol_results {
match protocol_result {
ActionResult::Output(output_data) => {
acted = true;
let mut write = write_half.lock().await;
let log = Log::new(Some(&status_tx));
if let Err(e) = write.write_all(&output_data).await {
log.error(format!("Failed to send response: {}", e));
} else {
if output_data
.iter()
.all(|&b| b.is_ascii_graphic() || b.is_ascii_whitespace())
{
let data_str = String::from_utf8_lossy(&output_data);
let preview = if data_str.len() > 100 {
format!("{}...", &data_str[..100])
} else {
data_str.to_string()
};
log.debug(format!(
"TLS sent {} bytes to {}: {}",
output_data.len(),
connection_id,
preview
));
log.trace(format!("TLS sent (text): {:?}", data_str));
} else {
log.debug(format!(
"TLS sent {} bytes to {} (binary data)",
output_data.len(),
connection_id
));
log.trace(format!(
"TLS sent (hex): {}",
hex::encode(&output_data)
));
}
log.debug(format!(
"Sent {} bytes to {}",
output_data.len(),
connection_id
));
app_state
.update_connection_stats(
server_id,
connection_id,
None,
Some(output_data.len() as u64),
None,
Some(1),
)
.await;
}
}
ActionResult::CloseConnection => {
acted = true;
should_close = true;
}
ActionResult::WaitForMore => {
acted = true;
should_wait = true;
}
_ => {}
}
}
if !acted {
Log::new(Some(&status_tx)).warn(format!(
"TLS connection {connection_id} decision=model_no_action: model \
produced no usable action, nothing sent"
));
}
if should_wait {
connections
.lock()
.await
.entry(connection_id)
.and_modify(|conn| conn.state = ConnectionState::Accumulating);
Log::new(Some(&status_tx))
.debug(format!("Waiting for more data from {connection_id}"));
return;
}
if should_close {
connections.lock().await.remove(&connection_id);
if let Err(e) = write_half.lock().await.shutdown().await {
debug!("TLS shutdown on {} returned: {}", connection_id, e);
}
Log::new(Some(&status_tx))
.info(format!("Closed TLS connection {connection_id}"));
return;
}
let has_queued = {
let conns = connections.lock().await;
conns
.get(&connection_id)
.map(|c| !c.queued_data.is_empty())
.unwrap_or(false)
};
if has_queued {
let queued = {
let mut conns = connections.lock().await;
match conns.get_mut(&connection_id) {
Some(conn) => std::mem::take(&mut conn.queued_data),
None => return,
}
};
if queued.is_empty() {
connections
.lock()
.await
.entry(connection_id)
.and_modify(|conn| conn.state = ConnectionState::Idle);
return;
}
all_data = Bytes::from(queued);
} else {
connections
.lock()
.await
.entry(connection_id)
.and_modify(|conn| conn.state = ConnectionState::Idle);
return;
}
}
Err(e) => {
let decision = llm_error_decision(&e);
error!(
"TLS connection {} decision={}: LLM call failed for received data: {:#}",
connection_id, decision, e
);
Log::new(Some(&status_tx)).warn(format!(
"TLS connection {connection_id} decision={decision}: no response \
generated, closing with close_notify"
));
{
let mut write = write_half.lock().await;
if let Err(shutdown_err) = write.shutdown().await {
debug!(
"TLS shutdown on {} returned: {}",
connection_id, shutdown_err
);
}
}
connections.lock().await.remove(&connection_id);
app_state
.remove_peer_handle(server_id, connection_id.as_u32())
.await;
app_state
.close_connection_on_server(server_id, connection_id)
.await;
Log::new(Some(&status_tx)).info(format!(
"Closed TLS connection {connection_id} after LLM error"
));
return;
}
}
}
}
}