Skip to main content
The NSP daemon emits a StateChangeNotification over WebSocket every time a tracked application’s state changes. Instead of polling session.refresh() in a loop and burning through your rate limit budget, you open a single persistent connection and react to changes the instant they arrive.

Why Streaming Beats Polling

The /watch WebSocket endpoint is not subject to the daemon’s rate limiter. You can hold as many concurrent streams open as you need without affecting your API key’s request quota.

The StateChangeNotification Object

Every event your stream yields is a StateChangeNotification with these fields:
pid
int
OS process ID of the app that changed.
app_id
str
Stable semantic identifier for the app (e.g. "gmail-chrome-12847").
sequence
int
Monotonically increasing counter. A gap between two consecutive sequence numbers means events were dropped (queue overflow) or a reconnect occurred.
changed_keys
list[str]
State keys that changed since the previous notification. Use this to filter work — only process events relevant to your agent.
captured_at
datetime
Timestamp at which the daemon captured the changed state.

Basic Streaming with session.watch()

The recommended pattern is session.watch(auto_refresh_on_event=True). This refreshes session.state automatically on every event, so you can call session.get_*() directly inside the loop without a separate refresh() call:

Filtering with changed_keys

Always check event.changed_keys before doing any work. Most state change events will touch fields your agent doesn’t care about:

Reconnection Handling

By default session.watch() reconnects automatically if the WebSocket connection drops. You can detect reconnections by watching for gaps in event.sequence:
After detecting a sequence gap, call session.refresh(fresh_probe=True) rather than relying on cached state. The gap means you missed some transitions — a fresh probe gives you the current ground truth.

Combining Streaming with session.refresh()

For most agents, auto_refresh_on_event=True is all you need. If you need to force a full re-probe (for example, after acting on the state), call refresh() explicitly:

Watching Multiple Apps Simultaneously

Open one stream per app and gather them with asyncio.gather:

Streaming + Periodic Polling Together

Sometimes you want real-time reaction to events and a periodic summary. Run both patterns concurrently with asyncio.gather:

Low-Level Access: open_state_stream()

If you need direct WebSocket access — for example, to forward events to a non-Python consumer or to build your own buffering layer — use the low-level client.open_state_stream() function:

Raw WebSocket endpoint

The underlying WebSocket URL is:
You can connect to this directly from any WebSocket client. Each message is a JSON object:
To test the stream from the command line with websocat:
The raw WebSocket endpoint does not validate changed_keys against an app schema. If the session is torn down and re-created (e.g. the app restarts), prev_capture_id may not be meaningful across the boundary. Use sequence gaps to detect these transitions.

session.watch() Parameter Reference

auto_refresh_on_event
bool
default:"false"
When true, the session’s cached state is automatically refreshed every time an event arrives. This is the most ergonomic option for most agents.
auto_reconnect
bool
default:"true"
Automatically re-establishes the WebSocket connection if it drops. Sequence gaps indicate where reconnects occurred.
max_queue
int
default:"128"
Size of the internal event buffer. When the buffer is full, the oldest event is dropped. Increase this if your handler is slower than the daemon’s emit rate.