netget/server/tcp/mod.rs
1//! TCP server implementation
2pub mod actions;
3
4use anyhow::{Context, Result};
5use bytes::Bytes;
6use std::collections::HashMap;
7use std::net::SocketAddr;
8use std::sync::Arc;
9use std::time::Duration;
10use tokio::io::{AsyncReadExt, AsyncWriteExt};
11use tokio::net::TcpStream;
12use tokio::sync::{mpsc, Mutex};
13use tracing::{debug, info};
14
15use super::connection::ConnectionId;
16use crate::llm::action_helper::call_llm;
17use crate::llm::ollama_client::OllamaClient;
18use crate::llm::ActionResult;
19use crate::logging::emit::Log;
20use crate::protocol::Event;
21use crate::server::TcpProtocol;
22use crate::state::app_state::AppState;
23use actions::{TCP_CONNECTION_OPENED_EVENT, TCP_DATA_RECEIVED_EVENT};
24
25/// Most bytes that may pile up in one connection's `queued_data` while its LLM call is in
26/// flight.
27///
28/// An LLM call takes seconds, and every byte the peer sends during it is appended to
29/// `queued_data` with nothing to stop it: a peer that streams for the length of one call can
30/// push NetGet's memory as fast as its link allows, pre-authentication, on the protocol every
31/// other protocol copies. 8 MiB is far more than any request/response exchange this server is
32/// for, and reaching it means the peer is not waiting for answers at all — so the connection is
33/// closed rather than the queue trimmed, which would hand the model a truncated message it had
34/// no way to know was truncated.
35pub const MAX_QUEUED_BYTES: usize = 8 * 1024 * 1024;
36
37/// How long to wait for the first byte from a peer that has only connected.
38///
39/// Generic TCP is client-speaks-first: `send_first` is opt-in and off by default, so a peer
40/// that has connected and sent nothing has made no claim on this server at all. Thirty seconds
41/// is far longer than any real client needs to put its first request on the wire once its
42/// `connect()` has returned, and short enough that an unauthenticated socket, task and
43/// `AppState` entry cannot be held for free.
44///
45/// The connect event is not an exception either: the task answering `tcp_connection_opened`
46/// holds [`ConnectionActivity::busy`] for the whole of its LLM round-trip, so the clock does
47/// not run while a peer is legitimately waiting to be spoken to.
48///
49/// **This was 30 seconds and that broke a real flow.** The argument for 30 was that generic TCP
50/// is client-speaks-first, so a silent peer has made no claim — true of a stranger, and false of
51/// the peer this server most often has. The dashboard offers `[ + tcp client ]` under a server's
52/// peers and `[ send message ]` beneath it: that client connects, says nothing, and waits for a
53/// person to type. Thirty seconds is less than a person takes, so the peer was dropped while the
54/// operator was still looking at it. `tests/mcp_stdio_test.rs::client_tools_manage_a_real_connection`
55/// is the same shape and failed deterministically — it takes ~38s to drive the MCP surface, and
56/// the client it had just created was gone before it asked for its status.
57///
58/// 300 seconds is the window a `manual` rule gives a human to answer one event
59/// (`src/state/intercepts.rs`), which is the number this product already uses for "how long
60/// someone might take". The stranger it was protecting against is still bounded, and still
61/// capped at [`MAX_CONNECTIONS`]; what changed is which of the two cases the default serves.
62/// A listener genuinely exposed to strangers should set `first_byte_timeout_secs` low.
63const FIRST_BYTE_READ_TIMEOUT: Duration = Duration::from_secs(300);
64
65/// How long to wait for further bytes once the peer has sent something.
66///
67/// Longer than the first bound by a wide margin, because the two are different claims: "has
68/// said nothing at all" is a peer that may not be a client, while "has gone quiet mid-session"
69/// is a client between requests, and this is the generic byte-stream protocol every kind of
70/// session is built on top of. Fifteen minutes is three times the default a `manual` rule gives
71/// a human to answer one event (`src/state/intercepts.rs`, 300s), so even a session whose last
72/// exchange was composed by hand at the dashboard has minutes of ordinary think-time left
73/// afterwards before the connection is reclaimed.
74const IDLE_BETWEEN_MESSAGES_TIMEOUT: Duration = Duration::from_secs(900);
75
76/// Concurrent connections this server admits.
77///
78/// [`crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS`]. Each connection may queue up to
79/// [`MAX_QUEUED_BYTES`] while its LLM call is in flight, so this is the multiplier that turns
80/// that per-connection bound into a total one.
81const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
82
83/// What a peer over [`MAX_CONNECTIONS`] is told before the socket closes: nothing.
84///
85/// Generic TCP has no vocabulary — there is no framing, no status code and no error message
86/// this server could write that a peer would not read as *payload*, and a fabricated payload is
87/// worse than silence for the same reason twenty protocols here stay silent on an LLM failure.
88/// The refusal is still recorded: `accept_bounded` logs it at WARN with
89/// `decision=fail_closed_connection_cap`, and the peer sees a clean EOF rather than a reset.
90const CONNECTION_CAP_REFUSAL: &[u8] = b"";
91
92/// Read from `reader` with a deadline that a connection doing work can never trip.
93///
94/// Returns `None` when the peer has genuinely been silent for `bound`, and the read's own
95/// result otherwise. TCP needs this rather than a bare `tokio::time::timeout` because its read
96/// loop is the one thing here that does *not* stop while a request is answered: each message is
97/// handed to a spawned task and the loop goes straight back to `read()`, so at any moment the
98/// connection may be parked on an LLM round-trip, or on a `manual` rule waiting for a human,
99/// with nothing arriving on the socket. Consulting [`ConnectionActivity::idle_for`] — which
100/// reports a connection with work in flight as not idle at all — is what keeps the deadline
101/// from closing the connection it is in the middle of answering.
102async fn read_bounded<R>(
103 reader: &mut R,
104 buffer: &mut [u8],
105 activity: &crate::server::accept_bounded::ConnectionActivity,
106 bound: Duration,
107) -> Option<std::io::Result<usize>>
108where
109 R: tokio::io::AsyncRead + Unpin,
110{
111 loop {
112 match tokio::time::timeout(bound, reader.read(buffer)).await {
113 Ok(result) => return Some(result),
114 Err(_) => match activity.idle_for() {
115 // Work in flight: the peer is waiting on us, not the other way round.
116 None => continue,
117 // Something crossed the connection inside the window; wait again from there.
118 Some(idle) if idle < bound => continue,
119 Some(_) => return None,
120 },
121 }
122 }
123}
124
125/// Connection state for LLM processing
126#[derive(Debug, Clone, PartialEq)]
127enum ConnectionState {
128 Idle,
129 Processing,
130 Accumulating,
131}
132
133/// Per-connection data for LLM handling
134struct ConnectionData {
135 state: ConnectionState,
136 queued_data: Vec<u8>,
137 memory: String,
138 write_half: Arc<Mutex<tokio::io::WriteHalf<TcpStream>>>,
139}
140
141/// TCP server that listens for incoming connections
142pub struct TcpServer;
143
144impl TcpServer {
145 /// Spawn the TCP server with integrated LLM actions
146 pub async fn spawn_with_llm_actions(
147 listen_addr: SocketAddr,
148 llm_client: OllamaClient,
149 app_state: Arc<AppState>,
150 status_tx: mpsc::UnboundedSender<String>,
151 send_first: bool,
152 server_id: crate::state::ServerId,
153 first_byte_timeout_secs: Option<u64>,
154 idle_timeout_secs: Option<u64>,
155 ) -> Result<SocketAddr> {
156 let first_byte_timeout = first_byte_timeout_secs
157 .map(Duration::from_secs)
158 .unwrap_or(FIRST_BYTE_READ_TIMEOUT);
159 let idle_timeout = idle_timeout_secs
160 .map(Duration::from_secs)
161 .unwrap_or(IDLE_BETWEEN_MESSAGES_TIMEOUT);
162 // Create and bind TCP server
163 let listener =
164 crate::server::socket_helpers::create_reusable_tcp_listener(listen_addr).await?;
165 let local_addr = listener.local_addr()?;
166 Log::new(Some(&status_tx)).info(format!("TCP server listening on {}", local_addr));
167
168 let connections = Arc::new(Mutex::new(HashMap::new()));
169 let protocol = Arc::new(TcpProtocol::new());
170
171 // Spawn accept loop
172 let task_registrar = app_state.clone();
173 let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
174 let accept_handle = tokio::spawn(async move {
175 loop {
176 match crate::server::accept_bounded::accept_bounded(
177 &listener,
178 &limiter,
179 CONNECTION_CAP_REFUSAL,
180 "TCP",
181 Some(&status_tx),
182 )
183 .await
184 {
185 Ok((stream, remote_addr, permit)) => {
186 let connection_id =
187 ConnectionId::new(app_state.get_next_unified_id().await);
188 let local_addr_conn = stream.local_addr().unwrap_or(local_addr);
189 info!("Accepted connection {} from {}", connection_id, remote_addr);
190
191 // Split stream
192 let (read_half, write_half) = tokio::io::split(stream);
193 let write_half_arc = Arc::new(Mutex::new(write_half));
194
195 // Whether this connection is doing work the peer is waiting on. The
196 // read deadline below consults it, so an LLM round-trip or a `manual`
197 // rule parked for a human can never be mistaken for an idle peer.
198 let activity =
199 Arc::new(crate::server::accept_bounded::ConnectionActivity::new());
200
201 // Add connection to ServerInstance
202 use crate::state::server::{
203 ConnectionState as ServerConnectionState, ConnectionStatus,
204 ProtocolConnectionInfo,
205 };
206 let now = crate::utils::clock::Instant::now();
207 let conn_state = ServerConnectionState {
208 id: connection_id,
209 remote_addr,
210 local_addr: local_addr_conn,
211 bytes_sent: 0,
212 bytes_received: 0,
213 packets_sent: 0,
214 packets_received: 0,
215 last_activity: now,
216 status: ConnectionStatus::Active,
217 status_changed_at: now,
218 protocol_info: ProtocolConnectionInfo::new(serde_json::json!({
219 "state": "Idle"
220 })),
221 };
222 app_state
223 .add_connection_to_server(server_id, conn_state)
224 .await;
225 let _ = status_tx.send("__UPDATE_UI__".to_string());
226
227 // Register the connection HERE, before either task is spawned.
228 //
229 // This used to be the first thing the banner task did, racing the
230 // reader task spawned immediately after it:
231 // handle_data_with_actions returns silently when the connection is
232 // not in the map, so a client that wrote before the server accepted
233 // (the normal case - connect() returns as soon as the kernel
234 // completes the handshake) lost that payload with no response, no
235 // error and no log line. Inserting synchronously in the accept loop
236 // closes the window: the reader task does not exist yet.
237 //
238 // A `send_first` server starts it in `Processing`: `tcp_connection_opened`
239 // is being answered, and anything the peer sends meanwhile queues behind
240 // it rather than raising a second, concurrent model call on the same
241 // connection. The connect task moves it to `Idle` and hands over the
242 // queue. Without `send_first` no connect event is raised, so it starts
243 // `Idle`.
244 connections.lock().await.insert(
245 connection_id,
246 ConnectionData {
247 state: if send_first {
248 ConnectionState::Processing
249 } else {
250 ConnectionState::Idle
251 },
252 queued_data: Vec::new(),
253 memory: String::new(),
254 write_half: write_half_arc.clone(),
255 },
256 );
257
258 // Peer messaging: the dashboard can inject an action into
259 // THIS connection (send_tcp_data through the same
260 // executor the model's actions use). The task ends when
261 // the handle is dropped — by the close paths below or by
262 // server teardown.
263 let peer_rx = crate::server::peer_support::register_peer_channel(
264 &app_state,
265 server_id,
266 connection_id.as_u32(),
267 )
268 .await;
269 crate::server::peer_support::spawn_peer_command_task(
270 peer_rx,
271 protocol.clone(),
272 app_state.clone(),
273 server_id,
274 connection_id.as_u32(),
275 write_half_arc.clone(),
276 status_tx.clone(),
277 );
278
279 // Raise `tcp_connection_opened` only for a `send_first` server, where the
280 // peer is owed a greeting and nothing is read until it is on the wire.
281 //
282 // Raising it for every connection was measured and rejected: generic TCP
283 // is client-speaks-first, so a model-driven server paid one extra model
284 // call per connection to learn there was nothing to say, and the real-model
285 // eval showed the cost in behaviour as well as time - told to upper-case
286 // the client's text, llama3.1:8b answered the empty connect event with a
287 // greeting and the client's real answer arrived seconds later. A greeting
288 // is what `send_first` is for, and its description says so.
289 let (opened_tx, opened_rx) = tokio::sync::oneshot::channel::<()>();
290 // Dropped by the reader task on every exit path (EOF, error, deadline,
291 // abort), which tells the connect task its peer is gone.
292 let (reader_alive_tx, reader_alive_rx) =
293 tokio::sync::oneshot::channel::<()>();
294 if send_first {
295 let llm_client_clone = llm_client.clone();
296 let app_state_clone = app_state.clone();
297 let status_tx_clone = status_tx.clone();
298 let connections_clone = connections.clone();
299 let write_half_for_conn = write_half_arc.clone();
300 let protocol_clone = protocol.clone();
301 // The peer may be waiting to be greeted, so the first-byte deadline
302 // must not run while the model is answering the connect event.
303 let opened_busy = activity.busy();
304 // Tracked, not detached: it holds the write half and makes an LLM
305 // call, so a detached one keeps talking to a peer after the
306 // operator stopped the server.
307 let state_for_spawn = app_state.clone();
308 state_for_spawn
309 .spawn_server_task(server_id, async move {
310 let _opened_busy = opened_busy;
311 Self::handle_connection_opened(
312 connection_id,
313 server_id,
314 send_first,
315 opened_tx,
316 reader_alive_rx,
317 llm_client_clone,
318 app_state_clone,
319 status_tx_clone,
320 connections_clone,
321 write_half_for_conn,
322 protocol_clone,
323 )
324 .await;
325 })
326 .await;
327 } else {
328 drop(opened_tx);
329 drop(reader_alive_rx);
330 }
331
332 // Spawn reader task
333 let llm_client_clone = llm_client.clone();
334 let app_state_clone = app_state.clone();
335 let status_tx_clone = status_tx.clone();
336 let connections_clone = connections.clone();
337 let protocol_clone = protocol.clone();
338 let activity_clone = activity.clone();
339 // The per-connection reader is registered with the server, so
340 // `stop_server` aborts it along with the accept loop. Registering
341 // prunes finished handles, so one entry per live connection is the
342 // steady state however many connections have come and gone.
343 //
344 // Without this, stopping released the listening socket and left every
345 // in-flight connection running: still reading, still answering, still
346 // spending LLM budget on a server the operator had stopped.
347 let state_for_reader = app_state.clone();
348 state_for_reader
349 .spawn_server_task(server_id, async move {
350 // Held for the life of the connection: releasing it here
351 // would cap the accept rate rather than the number of live
352 // connections.
353 let _permit = permit;
354 let _reader_alive = reader_alive_tx;
355 let mut buffer = vec![0u8; 8192];
356 let mut read_half = read_half;
357 let mut seen_bytes = false;
358
359 // `send_first`: nothing is read until the connect event has
360 // been answered, so the greeting is on the wire before the
361 // peer's first bytes are even looked at. Without it the read
362 // proceeds and anything that arrives queues behind the
363 // connect event instead (the connection starts `Processing`).
364 if send_first {
365 let _ = opened_rx.await;
366 } else {
367 drop(opened_rx);
368 }
369
370 loop {
371 let bound = if seen_bytes {
372 idle_timeout
373 } else {
374 first_byte_timeout
375 };
376 // The deadline wraps this read and nothing else.
377 let read = match read_bounded(
378 &mut read_half,
379 &mut buffer,
380 &activity_clone,
381 bound,
382 )
383 .await
384 {
385 Some(result) => result,
386 None => {
387 connections_clone.lock().await.remove(&connection_id);
388 app_state_clone
389 .remove_peer_handle(
390 server_id,
391 connection_id.as_u32(),
392 )
393 .await;
394 app_state_clone
395 .close_connection_on_server(
396 server_id,
397 connection_id,
398 )
399 .await;
400 Log::new(Some(&status_tx_clone)).info(format!(
401 "Connection {connection_id} sent nothing for \
402 {}s; closing idle connection",
403 bound.as_secs()
404 ));
405 let _ =
406 status_tx_clone.send("__UPDATE_UI__".to_string());
407 break;
408 }
409 };
410 match read {
411 Ok(0) => {
412 // Connection closed
413 connections_clone.lock().await.remove(&connection_id);
414 app_state_clone
415 .remove_peer_handle(
416 server_id,
417 connection_id.as_u32(),
418 )
419 .await;
420 app_state_clone
421 .close_connection_on_server(
422 server_id,
423 connection_id,
424 )
425 .await;
426 Log::new(Some(&status_tx_clone))
427 .info(format!("Connection {connection_id} closed"));
428 let _ =
429 status_tx_clone.send("__UPDATE_UI__".to_string());
430 break;
431 }
432 Ok(n) => {
433 seen_bytes = true;
434 activity_clone.touch();
435 let data = Bytes::copy_from_slice(&buffer[..n]);
436
437 // Data summary + full payload. These are FileOnly:
438 // the tcp_data_received event template renders the
439 // equivalent lines to the TUI (see actions.rs), so
440 // streaming the payload here too would duplicate it
441 // and load the unbounded status channel.
442 let log = Log::new(Some(&status_tx_clone));
443 if data.iter().all(|&b| {
444 b.is_ascii_graphic() || b.is_ascii_whitespace()
445 }) {
446 let data_str = String::from_utf8_lossy(&data);
447 let preview = if data_str.len() > 100 {
448 format!("{}...", &data_str[..100])
449 } else {
450 data_str.to_string()
451 };
452 log.debug(format!(
453 "TCP received {} bytes on {}: {}",
454 n, connection_id, preview
455 ));
456 log.trace(format!(
457 "TCP data (text): {:?}",
458 data_str
459 ));
460 } else {
461 log.debug(format!(
462 "TCP received {} bytes on {} (binary data)",
463 n, connection_id
464 ));
465 log.trace(format!(
466 "TCP data (hex): {}",
467 hex::encode(&data)
468 ));
469 }
470
471 // Keep the connection's counters live: the
472 // dashboard and /status read these, and TCP
473 // was the one server never updating them.
474 app_state_clone
475 .update_connection_stats(
476 server_id,
477 connection_id,
478 Some(n as u64),
479 None,
480 Some(1),
481 None,
482 )
483 .await;
484
485 // Handle data in separate task
486 let llm_clone = llm_client_clone.clone();
487 let state_clone = app_state_clone.clone();
488 let status_clone = status_tx_clone.clone();
489 let conns_clone = connections_clone.clone();
490 let protocol_clone = protocol_clone.clone();
491 // The per-request handler makes the LLM call and
492 // writes the reply, so it is the task that must not
493 // outlive a stop.
494 let state_for_data = app_state_clone.clone();
495 // Busy for the whole of the answer — the LLM call,
496 // a script, or a `manual` rule parked for a human —
497 // so the read deadline above cannot close the
498 // connection this is an answer for.
499 let busy = activity_clone.busy();
500 state_for_data
501 .spawn_server_task(server_id, async move {
502 let _busy = busy;
503 Self::handle_data_with_actions(
504 connection_id,
505 server_id,
506 data,
507 llm_clone,
508 state_clone,
509 status_clone,
510 conns_clone,
511 protocol_clone,
512 )
513 .await;
514 })
515 .await;
516 }
517 Err(e) => {
518 Log::new(Some(&status_tx_clone)).error(format!(
519 "Read error on {}: {}",
520 connection_id, e
521 ));
522 connections_clone.lock().await.remove(&connection_id);
523 app_state_clone
524 .remove_peer_handle(
525 server_id,
526 connection_id.as_u32(),
527 )
528 .await;
529 break;
530 }
531 }
532 }
533 })
534 .await;
535 }
536 Err(e) => {
537 Log::new(Some(&status_tx)).error(format!("Accept error: {}", e));
538 break;
539 }
540 }
541 }
542 });
543
544 task_registrar
545 .register_server_task(server_id, accept_handle)
546 .await;
547
548 Ok(local_addr)
549 }
550
551 /// Answer `tcp_connection_opened` for a new connection, then release whatever the peer
552 /// sent while it was being answered.
553 ///
554 /// The accept loop calls this only for a `send_first` server, whose peer is owed a greeting:
555 /// no bytes is `decision=model_silent` (a warning), and a backend failure half-closes the
556 /// connection, because the peer is waiting for something that is not coming. The branch for
557 /// a server without `send_first` (no bytes is `decision=model_no_actions`, a failure is
558 /// logged `decision=connect_event_failed`) is kept so the function stays correct if it is
559 /// ever called for one.
560 ///
561 /// `opened` is signalled once the answer is on the wire; a `send_first` reader waits for it.
562 /// `reader_alive` resolves when the reader task ends, and abandons the call: a peer that
563 /// has already gone is not greeted.
564 /// The connection is registered as `Processing` by the accept loop, so data that arrives
565 /// meanwhile is queued; this moves it to `Idle` and processes that queue.
566 #[allow(clippy::too_many_arguments)]
567 async fn handle_connection_opened(
568 connection_id: ConnectionId,
569 server_id: crate::state::ServerId,
570 send_first: bool,
571 opened: tokio::sync::oneshot::Sender<()>,
572 reader_alive: tokio::sync::oneshot::Receiver<()>,
573 llm_client: OllamaClient,
574 app_state: Arc<AppState>,
575 status_tx: mpsc::UnboundedSender<String>,
576 connections: Arc<Mutex<HashMap<ConnectionId, ConnectionData>>>,
577 write_half: Arc<Mutex<tokio::io::WriteHalf<TcpStream>>>,
578 protocol: Arc<TcpProtocol>,
579 ) {
580 let log = Log::new(Some(&status_tx));
581 let event = Event::new(
582 &TCP_CONNECTION_OPENED_EVENT,
583 crate::protocol::event_type::connect_event_data(),
584 );
585
586 // A peer that connects and leaves before the connect event is answered has nobody
587 // left to greet. The call is abandoned rather than finished, so a port scan against a
588 // `send_first` server does not cost one model call per probe, each running on after its
589 // socket was gone.
590 let answer = tokio::select! {
591 answer = call_llm(
592 &llm_client,
593 &app_state,
594 server_id,
595 Some(connection_id),
596 &event,
597 protocol.as_ref(),
598 ) => answer,
599 _ = reader_alive => {
600 log.debug(format!(
601 "Connection {connection_id} closed before its connect event was answered: \
602 decision=peer_left_before_answer"
603 ));
604 return;
605 }
606 };
607
608 match answer {
609 Ok(execution_result) => {
610 debug!("LLM TCP connect-event response received");
611 for msg in execution_result.messages {
612 let _ = status_tx.send(msg);
613 }
614
615 let mut wrote = false;
616 let mut close = false;
617 for protocol_result in execution_result.protocol_results {
618 match protocol_result {
619 ActionResult::Output(output_data) => {
620 let mut write = write_half.lock().await;
621 if let Err(e) = write.write_all(&output_data).await {
622 log.error(format!("Failed to send greeting: {}", e));
623 } else if let Err(e) = write.flush().await {
624 log.error(format!("Failed to flush greeting: {}", e));
625 } else {
626 drop(write);
627 // Sent-data summary + payload are FileOnly: the send_tcp_data
628 // action template already reports the send to the TUI.
629 if output_data
630 .iter()
631 .all(|&b| b.is_ascii_graphic() || b.is_ascii_whitespace())
632 {
633 let data_str = String::from_utf8_lossy(&output_data);
634 log.debug(format!(
635 "TCP sent {} bytes to {}: {}",
636 output_data.len(),
637 connection_id,
638 crate::utils::truncate_for_log(&data_str, 100)
639 ));
640 log.trace(format!("TCP sent (text): {:?}", data_str));
641 } else {
642 log.debug(format!(
643 "TCP sent {} bytes to {} (binary data)",
644 output_data.len(),
645 connection_id
646 ));
647 log.trace(format!(
648 "TCP sent (hex): {}",
649 hex::encode(&output_data)
650 ));
651 }
652 app_state
653 .update_connection_stats(
654 server_id,
655 connection_id,
656 None,
657 Some(output_data.len() as u64),
658 None,
659 Some(1),
660 )
661 .await;
662 wrote = true;
663 }
664 }
665 ActionResult::CloseConnection => close = true,
666 _ => {}
667 }
668 }
669
670 if close {
671 {
672 let mut write = write_half.lock().await;
673 let _ = write.shutdown().await;
674 }
675 connections.lock().await.remove(&connection_id);
676 app_state
677 .close_connection_on_server(server_id, connection_id)
678 .await;
679 log.info(format!(
680 "Closed connection {connection_id} on connect: decision=model_close"
681 ));
682 let _ = opened.send(());
683 return;
684 }
685
686 if wrote {
687 log.debug(format!(
688 "Greeted {connection_id} on connect: decision=model_answer"
689 ));
690 } else if send_first {
691 // Asked to speak first and said nothing: the peer sits at a blank
692 // connection. A real answer, but not the one send_first promised.
693 log.warn(format!(
694 "No greeting for {connection_id}: decision=model_silent (send_first \
695 was requested but the model produced no bytes)"
696 ));
697 } else {
698 log.debug(format!(
699 "No greeting for {connection_id}: decision=model_no_actions"
700 ));
701 }
702 }
703 Err(e) => {
704 let failure = crate::utils::WireFailure::classify(&e);
705 let class = if failure.is_overloaded() {
706 "overloaded"
707 } else {
708 "unavailable"
709 };
710 if send_first {
711 // The full error goes to the log and the status stream, where an
712 // operator looks. Nothing derived from it reaches the peer.
713 log.warn(format!(
714 "TCP greeting failed for {connection_id}: decision=fail_closed_llm_error \
715 class={class} error={e}"
716 ));
717 // A send_first server owes this peer a greeting and now has none. Raw
718 // TCP has no error frame, so the only honest signal is FIN: half-close
719 // so the peer's next read returns EOF immediately instead of blocking
720 // until its own timeout. Same shape as the data path.
721 {
722 let mut write = write_half.lock().await;
723 let _ = write.shutdown().await;
724 }
725 connections.lock().await.remove(&connection_id);
726 app_state
727 .close_connection_on_server(server_id, connection_id)
728 .await;
729 log.info(format!(
730 "Closed connection {connection_id} after greeting LLM error"
731 ));
732 let _ = opened.send(());
733 return;
734 }
735 // Nobody asked this server to speak first, so the peer has been failed by
736 // nothing yet: its own request will get the data path's answer (and the data
737 // path's fail-closed, if the backend is still down).
738 log.warn(format!(
739 "TCP connect event for {connection_id} failed: \
740 decision=connect_event_failed class={class} error={e}"
741 ));
742 }
743 }
744
745 let _ = opened.send(());
746
747 // Hand over whatever arrived while the connect event was being answered.
748 let queued = {
749 let mut conns = connections.lock().await;
750 let Some(conn) = conns.get_mut(&connection_id) else {
751 return;
752 };
753 conn.state = ConnectionState::Idle;
754 !conn.queued_data.is_empty()
755 };
756 if queued {
757 Self::handle_data_with_actions(
758 connection_id,
759 server_id,
760 Bytes::new(),
761 llm_client,
762 app_state,
763 status_tx,
764 connections,
765 protocol,
766 )
767 .await;
768 }
769 }
770
771 /// Handle data received on a connection with LLM actions
772 #[allow(clippy::too_many_arguments)]
773 async fn handle_data_with_actions(
774 connection_id: ConnectionId,
775 server_id: crate::state::ServerId,
776 data: Bytes,
777 llm_client: OllamaClient,
778 app_state: Arc<AppState>,
779 status_tx: mpsc::UnboundedSender<String>,
780 connections: Arc<Mutex<HashMap<ConnectionId, ConnectionData>>>,
781 protocol: Arc<TcpProtocol>,
782 ) {
783 // Check connection state
784 let current_state = {
785 let conns = connections.lock().await;
786 if let Some(conn_data) = conns.get(&connection_id) {
787 conn_data.state.clone()
788 } else {
789 // Never silent. A miss here means the connection was torn down between the
790 // read and this lookup (peer reset, or close_this_connection on another
791 // task) - legitimate, but indistinguishable from a registration race, which
792 // is exactly what made the bitcoin accept-order bug so hard to find: the
793 // read loop logged "received N bytes" and then nothing at all.
794 debug!(
795 "TCP connection {} is no longer registered; dropping {} received bytes",
796 connection_id,
797 data.len()
798 );
799 return;
800 }
801 };
802
803 // If processing, queue the data — up to MAX_QUEUED_BYTES.
804 if current_state == ConnectionState::Processing {
805 let (queued_len, write_half) = {
806 let mut conns = connections.lock().await;
807 let Some(conn) = conns.get_mut(&connection_id) else {
808 return; // Connection closed while we were waiting for the lock
809 };
810 conn.queued_data.extend_from_slice(&data);
811 (conn.queued_data.len(), conn.write_half.clone())
812 };
813
814 let log = Log::new(Some(&status_tx));
815 if queued_len > MAX_QUEUED_BYTES {
816 log.warn(format!(
817 "Connection {connection_id} queued {queued_len} bytes while awaiting a \
818 response (limit {MAX_QUEUED_BYTES}); closing: decision=queue_overflow"
819 ));
820 // Same signal as every other server-side close on raw TCP: FIN, so the peer
821 // reads EOF now instead of blocking on an answer that is never coming.
822 {
823 let mut write = write_half.lock().await;
824 let _ = write.shutdown().await;
825 }
826 connections.lock().await.remove(&connection_id);
827 app_state
828 .close_connection_on_server(server_id, connection_id)
829 .await;
830 let _ = status_tx.send("__UPDATE_UI__".to_string());
831 return;
832 }
833
834 log.debug(format!(
835 "Queued {} bytes for {} ({} queued)",
836 data.len(),
837 connection_id,
838 queued_len
839 ));
840 return;
841 }
842
843 // Merge any queued data with new data.
844 //
845 // The lock was released after the state check above, so the reader task may have
846 // removed this connection in the meantime - a client that writes and immediately
847 // closes does exactly that. Unwrapping here panicked the task on that race (15 of 64
848 // such clients in a burst), and a panicked socket task is silent while the server
849 // still reports Running.
850 let mut all_data = {
851 let mut conns = connections.lock().await;
852 let Some(conn_data) = conns.get_mut(&connection_id) else {
853 return; // Connection closed while we were waiting for the lock
854 };
855 conn_data.state = ConnectionState::Processing;
856 let mut merged = conn_data.queued_data.clone();
857 merged.extend_from_slice(&data);
858 conn_data.queued_data.clear();
859 Bytes::from(merged)
860 };
861
862 loop {
863 // Get memory
864 let memory = {
865 let conns = connections.lock().await;
866 conns
867 .get(&connection_id)
868 .map(|c| c.memory.clone())
869 .unwrap_or_default()
870 };
871
872 // Get write_half for context
873 let write_half = {
874 let conns = connections.lock().await;
875 conns.get(&connection_id).map(|c| c.write_half.clone())
876 };
877
878 let Some(write_half) = write_half else {
879 debug!(
880 "TCP connection {} went away before its response could be written",
881 connection_id
882 );
883 return;
884 };
885
886 // Format data for event parameter. Printable ASCII is passed through as text,
887 // anything else is hex-encoded. `encoding` tells the LLM which one it got, so it
888 // can echo the payload back with a matching `encoding` on send_tcp_data.
889 let (data_str, data_encoding) = if all_data
890 .iter()
891 .all(|&b| b.is_ascii_graphic() || b.is_ascii_whitespace())
892 {
893 (String::from_utf8_lossy(&all_data).to_string(), "utf8")
894 } else {
895 (hex::encode(&all_data), "hex")
896 };
897
898 // Create data received event
899 let event = Event::new(
900 &TCP_DATA_RECEIVED_EVENT,
901 serde_json::json!({
902 "data": data_str,
903 "encoding": data_encoding
904 }),
905 );
906
907 // Call LLM
908 match call_llm(
909 &llm_client,
910 &app_state,
911 server_id,
912 Some(connection_id),
913 &event,
914 protocol.as_ref(),
915 )
916 .await
917 {
918 Ok(execution_result) => {
919 let log = Log::new(Some(&status_tx));
920 debug!("LLM TCP response received");
921
922 // Update memory
923 connections
924 .lock()
925 .await
926 .entry(connection_id)
927 .and_modify(|conn| conn.memory = memory.clone());
928
929 // Display messages
930 for msg in execution_result.messages {
931 let _ = status_tx.send(msg);
932 }
933
934 // Handle protocol results
935 let mut should_close = false;
936 let mut should_wait = false;
937 let mut wrote_output = false;
938
939 for protocol_result in execution_result.protocol_results {
940 match protocol_result {
941 ActionResult::Output(output_data) => {
942 let mut write = write_half.lock().await;
943 if let Err(e) = write.write_all(&output_data).await {
944 log.error(format!("Failed to send response: {}", e));
945 } else if let Err(e) = write.flush().await {
946 log.error(format!("Failed to flush response: {}", e));
947 } else {
948 // Sent-data summary + payload are FileOnly: the
949 // send_tcp_data action template already reports the
950 // send to the TUI (see actions.rs).
951 if output_data
952 .iter()
953 .all(|&b| b.is_ascii_graphic() || b.is_ascii_whitespace())
954 {
955 let data_str = String::from_utf8_lossy(&output_data);
956 let preview = if data_str.len() > 100 {
957 format!("{}...", &data_str[..100])
958 } else {
959 data_str.to_string()
960 };
961 log.debug(format!(
962 "TCP sent {} bytes to {}: {}",
963 output_data.len(),
964 connection_id,
965 preview
966 ));
967 log.trace(format!("TCP sent (text): {:?}", data_str));
968 } else {
969 log.debug(format!(
970 "TCP sent {} bytes to {} (binary data)",
971 output_data.len(),
972 connection_id
973 ));
974 log.trace(format!(
975 "TCP sent (hex): {}",
976 hex::encode(&output_data)
977 ));
978 }
979 log.debug(format!(
980 "Sent {} bytes to {}",
981 output_data.len(),
982 connection_id
983 ));
984 app_state
985 .update_connection_stats(
986 server_id,
987 connection_id,
988 None,
989 Some(output_data.len() as u64),
990 None,
991 Some(1),
992 )
993 .await;
994 wrote_output = true;
995 }
996 }
997 ActionResult::CloseConnection => {
998 should_close = true;
999 }
1000 ActionResult::WaitForMore => {
1001 should_wait = true;
1002 }
1003 _ => {}
1004 }
1005 }
1006
1007 // Handle wait_for_more.
1008 //
1009 // The fragment the model was just shown goes back to the *head* of the
1010 // queue, ahead of anything that arrived during the call. `wait_for_more`
1011 // means "these bytes are an incomplete message"; dropping them was the one
1012 // thing that could not be right, because the model would never be shown the
1013 // front of the message again and could only reassemble it by having copied
1014 // it into memory first. Now the accumulation the action's name promises
1015 // actually happens.
1016 if should_wait {
1017 let (queued_len, arrived_during_call, write_half_for_close) = {
1018 let mut conns = connections.lock().await;
1019 let Some(conn) = conns.get_mut(&connection_id) else {
1020 return;
1021 };
1022 let arrived_during_call = !conn.queued_data.is_empty();
1023 let mut merged = all_data.to_vec();
1024 merged.append(&mut conn.queued_data);
1025 conn.queued_data = merged;
1026 conn.state = ConnectionState::Accumulating;
1027 (
1028 conn.queued_data.len(),
1029 arrived_during_call,
1030 conn.write_half.clone(),
1031 )
1032 };
1033
1034 // Same bound as the Processing queue: a model that answers
1035 // `wait_for_more` to everything must not be a way to grow memory
1036 // without limit either.
1037 if queued_len > MAX_QUEUED_BYTES {
1038 log.warn(format!(
1039 "Connection {connection_id} accumulated {queued_len} bytes across \
1040 wait_for_more (limit {MAX_QUEUED_BYTES}); closing: \
1041 decision=queue_overflow"
1042 ));
1043 {
1044 let mut write = write_half_for_close.lock().await;
1045 let _ = write.shutdown().await;
1046 }
1047 connections.lock().await.remove(&connection_id);
1048 app_state
1049 .close_connection_on_server(server_id, connection_id)
1050 .await;
1051 let _ = status_tx.send("__UPDATE_UI__".to_string());
1052 return;
1053 }
1054
1055 // If bytes arrived while the model was thinking, they *are* the "more"
1056 // it asked for. Returning here would park them until the next read,
1057 // which may never come — the peer has sent its whole message and is
1058 // waiting for us. Go round again with the joined payload instead.
1059 if arrived_during_call {
1060 let joined = {
1061 let mut conns = connections.lock().await;
1062 let Some(conn) = conns.get_mut(&connection_id) else {
1063 return;
1064 };
1065 conn.state = ConnectionState::Processing;
1066 std::mem::take(&mut conn.queued_data)
1067 };
1068 log.debug(format!(
1069 "wait_for_more on {connection_id}: {} bytes already waiting, \
1070 continuing",
1071 joined.len()
1072 ));
1073 all_data = Bytes::from(joined);
1074 continue;
1075 }
1076
1077 log.debug(format!(
1078 "Waiting for more data from {connection_id} ({queued_len} bytes held)"
1079 ));
1080 return;
1081 }
1082
1083 // Handle close_connection
1084 if should_close {
1085 connections.lock().await.remove(&connection_id);
1086 // The model answered by hanging up — distinct in the log from
1087 // "answered nothing" and from an LLM failure.
1088 log.info(format!(
1089 "Closed connection {connection_id}: decision=model_close"
1090 ));
1091 return;
1092 }
1093
1094 // The model answered, but with no bytes and no lifecycle action.
1095 // On raw TCP that is a legitimate answer ("say nothing, keep
1096 // listening"), so the connection stays open — but it must not be
1097 // confused with the backend having failed.
1098 if !wrote_output {
1099 log.debug(format!(
1100 "No response bytes for {connection_id}: decision=model_no_actions"
1101 ));
1102 }
1103
1104 // Check for queued data
1105 let has_queued = {
1106 let conns = connections.lock().await;
1107 conns
1108 .get(&connection_id)
1109 .map(|c| !c.queued_data.is_empty())
1110 .unwrap_or(false)
1111 };
1112
1113 if has_queued {
1114 // Take the queue and make it the next iteration's payload.
1115 // Leaving it in place re-sent the SAME bytes to the model on
1116 // every pass and never emptied the queue: one response per
1117 // iteration, forever, for a single line of input.
1118 let queued = {
1119 let mut conns = connections.lock().await;
1120 match conns.get_mut(&connection_id) {
1121 Some(conn) => std::mem::take(&mut conn.queued_data),
1122 None => return,
1123 }
1124 };
1125 if queued.is_empty() {
1126 connections
1127 .lock()
1128 .await
1129 .entry(connection_id)
1130 .and_modify(|conn| conn.state = ConnectionState::Idle);
1131 return;
1132 }
1133 all_data = Bytes::from(queued);
1134 } else {
1135 // Go to Idle state
1136 connections
1137 .lock()
1138 .await
1139 .entry(connection_id)
1140 .and_modify(|conn| conn.state = ConnectionState::Idle);
1141 return;
1142 }
1143 }
1144 Err(e) => {
1145 let log = Log::new(Some(&status_tx));
1146 let failure = crate::utils::WireFailure::classify(&e);
1147 let class = if failure.is_overloaded() {
1148 "overloaded"
1149 } else {
1150 "unavailable"
1151 };
1152 // Full error to the log/status stream only — never to the peer.
1153 log.warn(format!(
1154 "LLM error for TCP data on {connection_id}: decision=fail_closed_llm_error class={class} error={e}"
1155 ));
1156
1157 // Say *something* on the wire. Raw TCP has no error frame, so
1158 // the only honest signal is FIN: half-close the connection so
1159 // the peer's next read returns EOF immediately.
1160 //
1161 // This path used to reset the state to Idle and write
1162 // nothing, which left the peer blocked until its own timeout
1163 // with no indication anything had gone wrong — the visible
1164 // half of the concurrency-drop bug, and the same shape as the
1165 // "reset to Idle and write nothing" pattern noted in
1166 // CLAUDE.md's known systemic issues.
1167 if failure.is_overloaded() {
1168 log.warn(format!(
1169 "TCP connection {} closed: LLM capacity exhausted",
1170 connection_id
1171 ));
1172 }
1173 {
1174 let mut write = write_half.lock().await;
1175 let _ = write.shutdown().await;
1176 }
1177 connections.lock().await.remove(&connection_id);
1178 app_state
1179 .close_connection_on_server(server_id, connection_id)
1180 .await;
1181 log.info(format!("Closed connection {connection_id} after LLM error"));
1182 return;
1183 }
1184 }
1185 }
1186 }
1187}
1188
1189/// Send data on a TCP connection
1190pub async fn send_data(stream: &mut TcpStream, data: &[u8]) -> Result<()> {
1191 stream
1192 .write_all(data)
1193 .await
1194 .context("Failed to write data")?;
1195 stream.flush().await.context("Failed to flush stream")?;
1196 Ok(())
1197}