1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
//! Command channel into a **running** client's connection loop.
//!
//! # The problem this exists to solve
//!
//! `Client::execute_action()` is only ever called from inside each client's own
//! connection loop — the task that owns the socket — and the only thing that
//! produces actions for it is the LLM answering a network event. Nothing
//! outside the loop (the dashboard's \[send\] button, a scheduled task, a
//! future LLM `send_client_action`) can put an action on the wire.
//!
//! # The shape of the fix
//!
//! Unlike [`crate::state::server_handles`], this registry is **not**
//! type-erased. What every caller wants from every client is identical —
//! "here is an action JSON, execute it against the live connection" — and the
//! `Client` trait already defines that uniform vocabulary. So the handle is a
//! plain typed command channel. Each client's loop adds a `tokio::select!` arm
//! receiving [`ClientCommand`]s (see `crate::client::command_support` for the
//! generic arm body) and executes them with the same machinery it already uses
//! for LLM-produced actions.
//!
//! The channel is **bounded** (capacity [`CLIENT_COMMAND_CAPACITY`]): commands
//! are user-initiated and low-rate, and "client busy" backpressure is the
//! correct failure mode — unlike the unbounded status channels, which carry
//! fire-and-forget log lines.
//!
//! Lifetime is tied to the client: [`AppState::remove_client`] drops the
//! handle, and a loop that exits drops its receiver, which makes any later
//! send fail fast with a clear error rather than hang. Clients that have not
//! adopted the channel simply never register — `has_client_handle` returns
//! false and the UI greys out \[send\].
use ;
/// Bounded command-channel capacity per client.
pub const CLIENT_COMMAND_CAPACITY: usize = 16;
/// Outcome of injecting one action into a running client's connection loop.
/// One injected command: the action JSON plus a best-effort reply slot.
///
/// A dropped `reply_tx` receiver is fine — fire-and-forget callers just don't
/// listen for the outcome.
/// Handle to a running client's connection loop.