Reference

Rust client

One connection, one outstanding request, enforced by the type system.

Spawning a process per observation is wasteful for an agent that observes repeatedly, and it makes the connection bound hard to reason about. eensh::client exposes the protocol directly over one reusable connection.

src/main.rs
use eensh::client::{EenshClient, ImageSpec, SessionSpec};
use eensh::realtime::RealtimeOptions;

let mut client = EenshClient::connect_default()?;
let session = client.create_session(":1", &SessionSpec::desktop().with_history(4))?;

let stack = client.realtime(
    session.id(),
    &RealtimeOptions::default(),
    ImageSpec::metadata_only(),
)?;

println!("{} frames, newest {} µs old",
    stack.captured_frames(),
    stack.newest_frame_age_us());

One outstanding request per connection, enforced by the type system

Every method takes &mut self. That is not a formality — it makes one outstanding request per connection a compile-time property rather than a convention, so a client cannot accidentally pipeline requests it has no way to correlate.

The same protocol the CLI speaks

A caller can mix the CLI and the client freely against one service, and every session command has a corresponding method. Request identifiers are unique per connection and every response echoes the one it answers, so a mismatched reply is an error rather than a confusing success.

How much the client saves, measured

Measured over twelve operations at 640×480, with presentation held identical on every path so the comparison is of transport rather than of codec:

cost, per frame
Standalone, process + connection per frame      23.8 ms
Session CLI, one session, process per frame    22.3 ms
Direct client, one connection, no process      19.3 ms

The difference is smaller than it looks, for an honest reason: the CLI is one process per invocation, so both CLI paths still pay for process startup. Only the direct client avoids it. Speaking the protocol removes the connection setup and the process — which is why it is meaningfully faster rather than marginally so.

Presentation over the client

The Phase 4/5 method signatures are unchanged, and presentation is available through parallel methods rather than by adding a parameter to the existing ones:

additive, not breaking
// Unchanged from before presentation existed:
client.capture(session_id, image)?;
client.frame(session_id, frame_id, image)?;
client.diff(session_id, before, after, &options)?;
client.realtime(session_id, &realtime, image)?;

// Added alongside them:
client.capture_presented(session_id, image, &policy)?;
client.frame_presented(session_id, frame_id, image, Some(&policy))?;
client.diff_with_changed_region(session_id, before, after, &options, changed)?;
client.realtime_presented(session_id, &realtime, image, &policy)?;

The additive shape matters: an existing caller compiles and behaves identically without being touched, which is the property that let the presentation layer be added without rewriting any earlier test.

How many connections the service accepts at once

The service bounds simultaneous connections — 64 by default — and answers beyond the bound with service_overloaded (exit 24). That is a deliberate limit rather than an unbounded thread spawn, and the error is explicit so a caller can retry or fall back to sharing a connection.