Issue commands and use results
Product code issues work through AppNode. Use a user-scoped view where possible so the effective user boundary is established once and every operation stays inside it.
Send a command
from servercheetah.servers import SendOptions
scoped_app = app_node.for_user("user-42")
command_id = await scoped_app.send_command(
client_id="browser-main",
command={
"name": "list_tabs",
"params": {},
},
options=SendOptions(),
)
The command object requires name and may contain target and params. Put tab, window, flow, or other execution-context identifiers under target; do not hide routing data among ordinary parameters.
send_command() returns the generated command ID after the dispatcher accepts responsibility for delivery. In the Redis composition, the command is then retained in a logical-client mailbox within its delivery and retention policy; a node notification merely wakes the likely socket owner. Keep the ID for status, history, diagnostics, and tracing. Acceptance is not proof that a socket write began, the client acknowledged the frame, the handler ran, or an external effect completed.
Before dispatch, AppNode resolves the user boundary and applies configured central authorization before revealing presence. It proceeds with a fresh connection or can wait a bounded time for a recently seen logical client to reconnect. An unknown client, a sighting older than the configured staleness limit, a reconnect timeout, or exhausted waiter capacity fails before any command, RPC registration, or durable reservation is created. The App role then prepares parser definitions and supported payload offloading, validates the command, and registers correlation when requested.
Wait for a terminal signal when the request needs one
resolution = await scoped_app.send_command_and_wait(
client_id="browser-main",
command={
"name": "read_page_title",
"target": {"tab_id": 42},
},
options=SendOptions(timeout_ms=15_000),
)
send_command_and_wait() forces RPC correlation and returns lightweight terminal metadata. It does not return the handler payload. The resolution identifies the command, terminal kind, history stream when available, and trace relationship. Read the full accepted message from history using the command ID.
The command timeout sets the registered RPC deadline after sending begins; delivery has a separate pre-send budget. It does not race every client handler, send automatic cancellation, or prove that external work stopped. A separate await_result(command_id, timeout_ms=...) call can cap only that local observation and raises RpcAwaitTimeoutError without cancelling the registered RPC. A late result can still enter history after a local or RPC waiter expires.
Inspect delivery progress
status = await scoped_app.get_delivery_status(
client_id="browser-main",
command_id=command_id,
)
The payload-free status distinguishes queued work, a started send, transport acceptance, client ACK, an uncertain attempt, expiry before an attempt, and target unavailability. Work that is still provably unattempted can follow the stable logical client to a replacement runtime. The actual runtime instance is bound only when sending starts; an uncertain started attempt is terminal and is not replayed automatically.
Read retained evidence
History is organized into user-scoped streams. The default command-response stream contains the command ID and accepted progress or terminal messages associated with it. Use the application-facing history operations to query the effective user's records and then select the message whose command ID and terminal kind you need.
Treat history as retained Cheetah evidence with configured limits, not as the product's business database or an infinite audit log. If a result changes domain state, write the product fact transactionally in the system that owns that domain.
Handle failures by meaning
The application layer distinguishes several boundaries:
| Failure | Meaning |
|---|---|
ConnectionNotFoundError | the client is unknown, stale, did not reconnect in time, or bounded waiter capacity is full; inspect reason_code |
AuthorizationError | central policy denied or could not evaluate safely |
DispatchError | preparation or dispatcher handoff failed |
DispatchAdmissionUnknownError | dispatcher admission could not be confirmed; the command ID may already exist and automatic retry is unsafe |
RpcTimeoutError | this waiter's observation deadline expired |
RpcAwaitTimeoutError | one explicit local await_result limit expired while the registered RPC remained active |
InstanceRestartedError | the runtime bound to the attempt was replaced or fenced |
CommandError | the client returned a terminal error |
Import application-layer exceptions from servercheetah.servers; correlation lifecycle exceptions live under servercheetah.interfaces.
Design retries around the action
Retry automatically only when the first attempt is provably unattempted or the action is idempotent under a stable product key. A network failure after sending begins can leave the outcome unknown. In that state, inspect retained history and the external system before deciding whether another attempt is safe.
For a non-repeatable action, return enough product identity to reconcile the effect later. For a repeatable read, a bounded retry may be appropriate. Cheetah preserves the uncertainty instead of silently converting it into duplicate execution.