orchestrata_ultimate.kernel.cli_watch
Reference › orchestrata_ultimate › kernel › cli_watch
The streaming client behind orchestrata watch, described in its own docstring as the Phase 0 proof consumer. It follows one pipeline thread: it replays the backlog of events and then tails the live ones until the run ends. cli_main imports it. It uses cli_client for the gateway address and token, and pipeline_events for the topic name. It serves SPEC-000 (REQ-000-097, story US-006).
The design rule is that the gateway WebSocket is the single runtime ingress: the watcher never reads Redis itself. stream_thread_events connects, authenticates, subscribes to the thread's pipeline topic, asks gateway.replay for up to count past events (100 by default), yields each replayed event, and then yields live frames from _live_frames. The handshake calls run under a timeout (15 seconds by default) and the live loop waits without one. _normalize_replay_entry and _normalize_live_payload convert both shapes to one event dictionary, extract_live_event pulls the event out of a live message for the topic, and _mark_new drops duplicates that appear in both the backlog and the live stream. is_terminal_event stops the watch at pipeline.complete or pipeline.cancelled, and render_event_line formats an event as one line with its timestamp, type, volk, driver and data. Errors (authentication failure, an unreachable gateway, a closed connection) surface as click errors with clean messages.
At a glance
| Source | src/orchestrata_ultimate/kernel/cli_watch.py |
| Lines of code | 340 |
| Classes | 0 |
| Functions | 11 |
| Imports (internal) | orchestrata_ultimate.core.base.traces, orchestrata_ultimate.kernel.cli_client, orchestrata_ultimate.kernel.pipeline_events |
| Imported by | orchestrata_ultimate.kernel.cli_main |
| Tests that import it | tests/kernel/test_cli_watch.py |
Class diagram
No classes; see Functions.
Dependency diagram
API
Functions
| Function | Signature | Summary |
|---|---|---|
is_terminal_event |
is_terminal_event(event: Mapping[str, Any]) -> bool |
True when event ends a watch (pipeline.complete/.cancelled). |
render_event_line |
render_event_line(event: Mapping[str, Any]) -> str |
Render one normalized event as a single human-readable line. |
_normalize_replay_entry |
_normalize_replay_entry(entry: Mapping[str, Any]) -> dict[str, Any] |
Normalize a gateway.replay entry to the shared render shape. |
_normalize_live_payload |
_normalize_live_payload(payload: Mapping[str, Any]) -> dict[str, Any] |
Normalize an already-decoded live notification payload to the same render shape as a replay entry (_decode_pipeline_event's output: data a dict, iteration an int, no entry_id --… |
extract_live_event |
extract_live_event(msg: Mapping[str, Any], topic: str) -> dict[str, Any] | None |
Extract a normalized pipeline event from a JSON-RPC frame IFF it is a live notification on topic. |
_dedup_key |
_dedup_key(event: Mapping[str, Any]) -> tuple[str, str] |
|
_mark_new |
_mark_new(event: Mapping[str, Any], seen: set[tuple[str, str]]) -> bool |
Record event as seen against seen; True iff this is the first time. |
_recv_frame traces |
async _recv_frame(ws: Any, *, timeout: float) -> dict[str, Any] |
|
_call_rpc traces |
async _call_rpc(ws: Any, msg_id: int, method: str, params: dict[str, Any], *, timeout: float, pending: deque[dict[str, Any]]) -> dict[str, Any] |
Send one JSON-RPC request over ws and return its result. |
_live_frames |
async _live_frames(ws: Any, pending: deque[dict[str, Any]]) -> AsyncGenerator[dict[str, Any]] |
Yield every live JSON-RPC frame: first drains anything queued during the handshake (the subscribe-before-replay overlap window), then reads fresh frames off the socket with NO timeout by design --… |
stream_thread_events traces |
async stream_thread_events(thread_id: str, *, count: int = 100, env: Mapping[str, str] | None = None, recv_timeout: float = 15.0) -> AsyncIterator[dict[str, Any]] |
Stream a pipeline thread's events: backlog then live, one event per yield, until a terminal event or the caller cancels iteration (Ctrl-C). |
Source
"""Streaming client for ``orchestrata watch`` -- the Phase 0 proof consumer.
Phase 0 (``plans/contracts/volkdriver-phase0.contract.json``, deliverable
P0-B) proves the pipeline event stream is actually consumable end to end:
a real thread's backlog plus its live tail, rendered one line per event,
through the gateway's WebSocket JSON-RPC surface -- never Redis directly
(C3). ``kernel/cli_main.py``'s ``watch`` command is the thin click wrapper
over :func:`stream_thread_events` below.
Protocol notes (measured 2026-08-12; corrected from a refuted first draft
-- see the contract's ``refuted_and_corrected`` block):
1. ``auth.authenticate`` MUST be the first frame -- the gateway
authenticates before registering the connection and before the
receive loop starts.
2. SUBSCRIBE FIRST, THEN REPLAY. The live tail starts at ``last_id="$"``
(new-only), so replaying first would leave a gap.
3. The subscribe method is the BARE ``subscribe`` -- ``gateway.subscribe``
is not registered and dispatching it raises RPCError -32601. Replay is
the namespaced ``gateway.replay``.
4. Live events arrive as JSON-RPC NOTIFICATIONS -- no ``id`` field. A
client that correlates on response ``id`` sees nothing; events are
matched on ``method`` (the topic) instead.
5. Replay entries and live payloads have asymmetric shapes: replay
entries are stringly-typed and carry ``entry_id``; live payloads are
already decoded (``data`` a dict, ``iteration`` an int, no
``entry_id``). Both are normalized to one render shape before yielding.
Author: Mohamed
"""
# Spec: 000 - Orchestrata CLI
from __future__ import annotations
import asyncio
import json
from collections import deque
from collections.abc import AsyncGenerator, AsyncIterator, Mapping
from typing import Any, cast
import click
from orchestrata_ultimate.core.base.traces import traces
from orchestrata_ultimate.kernel.cli_client import _mint_token, gateway_uri
from orchestrata_ultimate.kernel.pipeline_events import (
PipelineEventType,
pipeline_topic,
)
# C6: this set names two of the taxonomy's exactly-13 members; it does not
# add any.
TERMINAL_EVENT_TYPES = frozenset(
{PipelineEventType.COMPLETE, PipelineEventType.CANCELLED}
)
def is_terminal_event(event: Mapping[str, Any]) -> bool:
"""True when *event* ends a watch (``pipeline.complete``/``.cancelled``)."""
return event.get("event_type", "") in TERMINAL_EVENT_TYPES
def render_event_line(event: Mapping[str, Any]) -> str:
"""Render one normalized event as a single human-readable line.
Generic on purpose (REQ-000-096's counterpart for human output): only
``event_type``/``timestamp`` are assumed always present; ``volk`` and
``driver`` are shown when non-empty so this never hard-depends on the
P0-A ``driver`` field landing first.
"""
parts = [
str(event.get("timestamp", "") or "?"),
str(event.get("event_type", "") or "?"),
]
volk = event.get("volk", "")
if volk:
parts.append(f"volk={volk}")
driver = event.get("driver", "")
if driver:
parts.append(f"driver={driver}")
data = event.get("data")
if data:
parts.append(json.dumps(data, default=str, sort_keys=True))
return " ".join(parts)
def _normalize_replay_entry(entry: Mapping[str, Any]) -> dict[str, Any]:
"""Normalize a ``gateway.replay`` entry to the shared render shape.
Replay entries are stringly-typed and carry ``entry_id`` (measured
protocol fact 5) -- ``data`` is a JSON string, ``iteration`` a
stringified int.
"""
data: Any = entry.get("data", "{}")
if isinstance(data, str):
try:
data = json.loads(data) if data else {}
except json.JSONDecodeError, TypeError:
data = {}
iteration: Any = entry.get("iteration", 0)
if isinstance(iteration, str):
try:
iteration = int(iteration)
except TypeError, ValueError:
iteration = 0
return {
"event_type": entry.get("event_type", ""),
"thread_id": entry.get("thread_id", ""),
"volk": entry.get("volk", ""),
"driver": entry.get("driver", ""),
"data": data if isinstance(data, dict) else {},
"timestamp": entry.get("timestamp", ""),
"iteration": iteration if isinstance(iteration, int) else 0,
}
def _normalize_live_payload(payload: Mapping[str, Any]) -> dict[str, Any]:
"""Normalize an already-decoded live notification payload to the same
render shape as a replay entry (``_decode_pipeline_event``'s output:
``data`` a dict, ``iteration`` an int, no ``entry_id`` -- measured
protocol fact 5)."""
data = payload.get("data", {})
iteration = payload.get("iteration", 0)
return {
"event_type": payload.get("event_type", ""),
"thread_id": payload.get("thread_id", ""),
"volk": payload.get("volk", ""),
"driver": payload.get("driver", ""),
"data": data if isinstance(data, dict) else {},
"timestamp": payload.get("timestamp", ""),
"iteration": iteration if isinstance(iteration, int) else 0,
}
def extract_live_event(msg: Mapping[str, Any], topic: str) -> dict[str, Any] | None:
"""Extract a normalized pipeline event from a JSON-RPC frame IFF it is
a live notification on *topic*.
A8 regression guard: live events arrive as JSON-RPC NOTIFICATIONS --
no ``id`` field (measured protocol fact 4). A client that correlates
on response ``id`` would silently see nothing; this recognizes the
frame by ``method`` alone and returns ``None`` for anything else (a
response to one of our own requests, or a notification on a different
topic).
"""
if "id" in msg:
return None
if msg.get("method") != topic:
return None
params = msg.get("params")
if not isinstance(params, dict):
return None
payload = params.get("payload")
if not isinstance(payload, dict):
return None
return _normalize_live_payload(payload)
def _dedup_key(event: Mapping[str, Any]) -> tuple[str, str]:
return (str(event.get("timestamp", "")), str(event.get("event_type", "")))
def _mark_new(event: Mapping[str, Any], seen: set[tuple[str, str]]) -> bool:
"""Record *event* as seen against *seen*; True iff this is the first time.
Keyed on ``(timestamp, event_type)`` -- deliberately NOT ``entry_id``:
a replay entry and its live-notification twin for the SAME logical
event come from two different Redis streams (measured protocol fact
5) and never share one, so comparing raw ``entry_id`` would silently
defeat the dedup this exists for (the subscribe-before-replay overlap
window).
"""
key = _dedup_key(event)
if key in seen:
return False
seen.add(key)
return True
@traces(spec="000", story="US-006", reqs=["REQ-000-097"])
async def _recv_frame(ws: Any, *, timeout: float) -> dict[str, Any]:
raw = await asyncio.wait_for(ws.recv(), timeout=timeout)
return cast(dict[str, Any], json.loads(raw))
@traces(spec="000", story="US-006", reqs=["REQ-000-097"])
async def _call_rpc(
ws: Any,
msg_id: int,
method: str,
params: dict[str, Any],
*,
timeout: float,
pending: deque[dict[str, Any]],
) -> dict[str, Any]:
"""Send one JSON-RPC request over *ws* and return its result.
Any frame that arrives before the matching response -- a live
notification delivered during the subscribe-before-replay overlap
window (measured protocol fact 2) -- is queued onto *pending* rather
than discarded, so the caller's live tail sees it once the handshake
completes.
Raises:
click.ClickException: the gateway returned a JSON-RPC error for
*method* (REQ-000-097: a clean message, never a raw traceback).
"""
await ws.send(
json.dumps({"jsonrpc": "2.0", "id": msg_id, "method": method, "params": params})
)
while True:
msg = await _recv_frame(ws, timeout=timeout)
if msg.get("id") == msg_id:
if "error" in msg:
err = msg["error"]
raise click.ClickException(
f"{method} failed: {err.get('message', err)}"
)
result = msg.get("result", {})
return result if isinstance(result, dict) else {}
# A live notification arrived before our response -- queue it; the
# caller's tail loop drains it once the backlog has rendered.
if "id" not in msg:
pending.append(msg)
async def _live_frames(
ws: Any, pending: deque[dict[str, Any]]
) -> AsyncGenerator[dict[str, Any]]:
"""Yield every live JSON-RPC frame: first drains anything queued during
the handshake (the subscribe-before-replay overlap window), then reads
fresh frames off the socket with NO timeout by design -- watch blocks
until the next event or Ctrl-C, however long that takes."""
while pending:
yield pending.popleft()
while True:
raw = await ws.recv()
yield cast(dict[str, Any], json.loads(raw))
@traces(spec="000", story="US-006", reqs=["REQ-000-097"])
async def stream_thread_events(
thread_id: str,
*,
count: int = 100,
env: Mapping[str, str] | None = None,
recv_timeout: float = 15.0,
) -> AsyncIterator[dict[str, Any]]:
"""Stream a pipeline thread's events: backlog then live, one event per
yield, until a terminal event or the caller cancels iteration (Ctrl-C).
C3: the gateway WebSocket is the SINGLE runtime ingress -- this talks
only to the gateway's JSON-RPC surface, never Redis directly.
Args:
thread_id: CXP thread to watch.
count: Replay backlog depth (default 100, matches
``gateway.replay``'s own default).
env: Optional explicit environment mapping forwarded to
:func:`orchestrata_ultimate.kernel.cli_client.gateway_uri` --
used by tests to target a hermetic gateway.
recv_timeout: Timeout for the bounded auth/subscribe/replay
handshake calls. NOT applied to the live tail loop, which
blocks indefinitely by design (a watch waits for the next
event, however long that takes).
Raises:
click.ClickException: auth failure, an RPC error, or the gateway
being unreachable/closed -- always a clean message, never a
raw traceback (REQ-000-097).
"""
import websockets.exceptions
uri = gateway_uri(env=env)
topic = pipeline_topic(thread_id)
pending: deque[dict[str, Any]] = deque()
seen: set[tuple[str, str]] = set()
try:
async with websockets.connect(uri) as ws:
auth_result = await _call_rpc(
ws,
1,
"auth.authenticate",
{"token": _mint_token()},
timeout=recv_timeout,
pending=pending,
)
if not auth_result.get("authenticated", False):
raise click.ClickException(
"gateway authentication failed: "
f"{auth_result.get('error', 'unknown')}"
)
# 2/3: SUBSCRIBE FIRST (bare `subscribe`), THEN `gateway.replay`.
await _call_rpc(
ws,
2,
"subscribe",
{"topic": topic},
timeout=recv_timeout,
pending=pending,
)
replay_result = await _call_rpc(
ws,
3,
"gateway.replay",
{"topic": topic, "count": count},
timeout=recv_timeout,
pending=pending,
)
entries = replay_result.get("events", [])
if not isinstance(entries, list):
entries = []
for entry in entries:
event = _normalize_replay_entry(entry)
if not _mark_new(event, seen):
continue
yield event
if is_terminal_event(event):
return
# 4: the live tail -- notifications carry NO `id`.
async for live_msg in _live_frames(ws, pending):
live_event = extract_live_event(live_msg, topic)
if live_event is None or not _mark_new(live_event, seen):
continue
yield live_event
if is_terminal_event(live_event):
return
except click.ClickException:
raise
except (TimeoutError, OSError) as e:
raise click.ClickException(
f"gateway unreachable at {uri} ({e}) — is the runtime up? "
f"Start it with `orchestrata start` or scripts/start_test_services.py."
) from e
except websockets.exceptions.ConnectionClosed as e:
raise click.ClickException(f"gateway connection closed: {e}") from e