Skip to main content
The NSP daemon broadcasts a StateChangeNotification over WebSocket every time a tracked process’s state changes. The Python SDK delivers these events through StateStream — an async iterator you consume inside an async for loop. This lets you react to application state changes in real time with sub-100 ms latency.

When to Use Streaming vs. Polling

Use streaming (session.watch())

When you need sub-second reaction to state changes, are building a monitoring loop, or want to trigger an action the moment a condition changes.

Use polling (session.refresh())

When you only need periodic reads (e.g. every 5 seconds) or are running a one-shot agent task that does not need to react to changes live.

Opening a Stream

Use session.watch() for the most ergonomic experience. It automatically binds the stream to the session’s PID:

Via Client (Raw Stream)

Use client.watch(pid=...) when you do not have a session object, or when you need a raw stream for a specific PID:

StateChangeNotification Fields

pid
int
OS process ID of the application that changed.
app_id
str
Stable semantic app identifier, e.g. "gmail-chrome-12847".
sequence
int
Monotonically increasing counter. A gap between consecutive sequence numbers means one or more events were dropped (queue overflow or reconnection).
changed_keys
list[str]
The state keys that changed since the previous notification, e.g. ["inbox.unread_count", "inbox.emails[0].is_read"].
capture_id
str
Identifier for the state capture that produced this notification.
prev_capture_id
str
Identifier for the previous capture, enabling diff lookups.
captured_at
datetime
UTC timestamp of the state capture that produced this event.

Refreshing State After Events

When you receive a StateChangeNotification, session.state is not automatically updated unless you pass auto_refresh_on_event=True. You control when to refresh:
Do not call session.refresh() unconditionally on every event. Check event.changed_keys first and only refresh when a key you care about has changed. This reduces unnecessary round-trips to the daemon on high-frequency streams.

Filtering by Changed Keys

React only to the parts of state you care about by inspecting changed_keys before doing any work:

Reconnection and Sequence Gaps

By default, StateStream reconnects automatically if the WebSocket drops. To detect reconnections, watch for gaps in event.sequence:
Set auto_reconnect=False to receive a NSPConnectionError instead of a transparent reconnect when the connection drops.

Queue Configuration

Events are buffered in an internal asyncio queue. If your consumer is slower than the daemon’s emit rate, the oldest events are dropped and reflected as sequence gaps:

Timeout and Cancellation

Limit how long you watch using asyncio.timeout (Python 3.11+):
Cancel a watching task explicitly:

Watching Multiple Apps Simultaneously

Open one stream per session and run them concurrently with asyncio.gather:

Low-Level: open_state_stream(pid, client)

For advanced use cases, use open_state_stream() directly to obtain a StateStream without a session:

WebSocket Wire Format

The stream connects to:
Each message is a JSON object:

Complete Example: Reactive Inbox Agent

This agent opens a stream, watches for new emails, and immediately archives any message matching a filter — all in real time: