WebSocket Endpoint
Connect to the watch endpoint using the tracked process’s PID:Find the PID for your target app by calling
GET /substrate/v1/apps first, or by reading session.state.pid from the Python SDK after calling client.attach().Message Format
The server sends one JSON message per probe cycle in which it detects state changes. Messages are never batched — each message represents one detection event:Behavior Notes
Understanding how the watch stream behaves helps you write robust reconnection and event-handling logic:- Cadence: The server fires one event per successful probe cycle that detects changes. With the default
poll_interval_secs = 2, you should expect at most one event every two seconds when the app is active. - Process exit: If the tracked process exits, the server closes the WebSocket connection with close code
1000(Normal Closure). Treat this as a clean shutdown, not an error. - Daemon restart: If the NSP daemon itself restarts, your connection is dropped without a clean close frame. Your client should detect this and reconnect.
- No heartbeat required: The server does not send ping frames and does not expect pong frames. The stream is event-driven — silence means no changes were detected.
- Sequence gaps: If
event.sequencejumps by more than 1 between consecutive messages (for example, from 142 to 145), it means the daemon detected state changes in intermediate cycles but your client did not receive them — possibly due to network buffering or a brief reconnect. Treat a gap as a signal that you may have missed intermediate updates and re-fetch full state if you need to be certain.
Python SDK
The Python SDK wraps the raw WebSocket connection in a typed async stream. Usesession.watch() as an async context manager and iterate over the events it yields.
1
Attach to the target app
Call
client.attach() with the app’s name. The SDK resolves the PID automatically and returns a session with session.state already populated:2
Open the watch stream
Use
session.watch() as an async context manager. The stream opens a WebSocket connection to the /watch/{pid} endpoint and keeps it alive for the duration of the async with block:3
Iterate over events
Use
async for to receive each event as it arrives. Each event object exposes event.changed_keys (list of strings) and event.sequence (integer):4
Call session.refresh() to get updated values
The event tells you which keys changed, but not their new values. Call
session.refresh() inside your handler to pull the latest state snapshot from the daemon, then use the typed session.get_*() helpers to read specific keys:Full Python Example
JavaScript Example
Use the browser’s nativeWebSocket API (or Node’s ws package) to connect to the stream. Parse each incoming message as JSON and react to the changed keys:
Reconnection Strategy
The watch connection can drop if the daemon restarts, the machine sleeps and wakes, or there is a brief network hiccup. Implement an exponential backoff reconnection loop to handle this automatically:- Reset the
attemptcounter after a successful connection so the backoff stays short after a clean reconnect. - Cap the backoff at a sensible ceiling (60 seconds here) so you do not wait forever if the daemon is down for maintenance.
- Distinguish between a connection error (retry) and an app-not-found error (app not running yet — also retry, but with a fixed short delay).
Handling Sequence Gaps
Track the sequence number across events and alert when a gap appears:Performance Notes
Use watch instead of polling
The watch stream wakes your code only when something changes. Polling
GET /state/{pid} every 2 seconds makes 1 800 requests per hour even when nothing is happening. Watch events arrive in the same timeframe with zero wasted requests.No action rate limit impact
Watch events are delivered over a persistent WebSocket connection and are counted separately from REST requests. They do not consume any portion of your 10 actions/second or 100 state reads/second rate limit budgets.
