Skip to main content

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}