Bridge OpenClaw agent state directly to snarling display via HTTP API, with notification support
Install
openclaw plugins install clawhub:openclaw-interaction-bridge-v2OpenClaw Interaction Bridge — OpenClaw 2.0 Compatible
A plugin that bridges OpenClaw agent activity to any external program! Snarling for example — a Raspberry Pi + DisplayHAT Mini companion that shows what the agent is doing and lets you approve or reject actions with physical A/B buttons and lets agents send notifications with a feedback loop for attunement!
OpenClaw 2.0 Compatibility (2026.8+)
OpenClaw 2026.8.1 restructured the plugin runtime API. Key changes:
api.runtime.taskFlowmoved toapi.runtime.tasks.managedFlows— the TaskFlow API (withbindSessionandfromToolContext) is now nested underapi.runtime.tasksalongsideflowsandruns.- Plugin runtime keys in 2026.8+:
version,gateway,config,agent,subagent,system,media,mediaUnderstanding,tts,channel,events,logging,state,modelAuth,imageGeneration,videoGeneration,musicGeneration,llm,tasks.
The plugin resolves TaskFlow from api.runtime.taskFlow (via fromToolContext / bindSession) and delivers results through subagent.run — live-verified on 2026.8+ and 2026.9.x. If a future OpenClaw removes the top-level alias, switch to api.runtime.tasks.managedFlows (its noted 2026.8+ location).
General Methodology for OpenClaw 2.0 Plugin Updates
When OpenClaw major versions change, plugin APIs may shift. Here's the diagnostic approach:
- Check
api.runtimekeys — Addconsole.infologging to dumpObject.keys(api.runtime)from insideregister(api). This reveals the actual runtime surface available to plugins. - Check the type definitions — The canonical types are in
dist/plugin-entry-*.d.tsunder the OpenClaw install (~/.npm-global/lib/node_modules/openclaw/dist/). Look atPluginRuntimeandPluginRuntimeCorefor the full API. - Check
dist/runtime-CbG1wF9O2.js(or similar) for the actual runtime implementation — search forcreateRuntimeandcreatePluginRuntimeto see what's constructed. - Use
??fallback chains — Always try the new API path first, falling back to older paths. This lets one build work across versions. - Run
openclaw doctorafter upgrades — It catches config issues like missing plugin allowlists. - SIGUSR1 only hot-reloads config — Plugin JS changes require a full
openclaw gateway restart.
⚠️ Optional Config (OpenClaw 2026.4.26+)
For optional status update functionality, you must add hooks.allowConversationAccess: true to the plugin config entry. This is required due to permissions changes in version 2026.4.26. No conversation data is read, only state tracking for status updates.
{
"plugins": {
"entries": {
"openclaw-interaction-bridge-v2": {
"enabled": true,
"hooks": { "allowConversationAccess": true }
}
}
}
}
Without this setting, the agent_end hook is silently blocked and the display won't transition to sleeping. Approvals and notifications still work regardless.
What It Does
- State display: Automatically sends agent state changes (processing, communicating, sleeping) to Snarling's display
- Physical approvals: Registers a
request_user_approvaltool that routes yes/no decisions to Snarling's A/B buttons - Notifications: Registers a
send_notificationtool that sends alerts to the display with priority-based timeouts and full two-way feedback - Notification feedback: Receives callback data from Snarling (revealed, dismissed, timed out) with timing metrics, enabling notification attunement
- Approval tracking: Counts approval lifecycle events (requested, approved, rejected, timed out, errored)
Installation
openclaw plugins install clawhub:openclaw-interaction-bridge-v2
The managed install records the package source, so later updates follow the same tracked record:
openclaw plugins update openclaw-interaction-bridge-v2
For hardened installations using a restrictive plugins.allow list, allow the plugin id openclaw-interaction-bridge-v2; enabling the plugin from the Control UI records it into an existing allowlist automatically.
Prerequisites
- Snarling running on a Raspberry Pi with DisplayHAT Mini (state + approval server on port 5000)
- OpenClaw gateway >= 2026.3.24-beta.2
Configuration
No config needed for the default Snarling setup. The plugin works out of the box.
Custom Targets
To use a custom interaction surface (Tauri app, mobile web view, etc.), edit the constants at the top of index.ts:
const SNARLING_URL = "http://localhost:5000/state"; // → your state endpoint
const CALLBACK_BASE_URL = "http://localhost:18789"; // → your callback base URL
The approval secret defaults to a random UUID generated at gateway start. To pin it, set approvalSecret in the plugin config (plugins.entries.openclaw-interaction-bridge-v2.config.approvalSecret). The secret must be included in the JSON body of callback requests (not query params — the gateway strips those). Snarling receives the secret in the alert payload and echoes it back in callback bodies — no environment variable is needed on either side.
No config file yet — when there are multiple adapters, a config-driven system will make sense. For now, editing the source is honest and simple.
How It Works
State Updates
The plugin hooks into OpenClaw events and POSTs state to Snarling:
| OpenClaw Event | Snarling State | Meaning |
|---|---|---|
before_agent_run | processing | Agent run started (before model submission) |
before_tool_call | processing | Agent is using tools |
before_agent_reply | communicating | Agent is generating a response |
agent_end | sleeping | Agent finished its turn |
| 10s idle timeout | sleeping | No recent activity |
Duplicates are suppressed — only state changes are sent.
Approval Flow
When the agent calls request_user_approval:
- Plugin creates a TaskFlow and sets it to waiting state
- POSTs approval request directly to Snarling on port 5000 (
/approval/alert) — no middleman - Snarling displays the request on screen with A/B button prompt
- User presses A (approve) or B (reject)
- Snarling forwards the decision to the plugin's
/approval-callbackHTTP route - Plugin resumes the TaskFlow (bookkeeping:
resume→finish) and delivers the result to the agent viasubagent.run - Snarling also sends a WebSocket RPC wake to bypass the gateway's
requests-in-flightcheck
Delivery: The approval result is delivered to the agent via subagent.run, which creates a real agent turn in the target session. This ensures the result reaches the agent even when its session is idle or recently completed. If subagent.run is unavailable, the plugin falls back to enqueueSystemEvent + runHeartbeatOnce (note: this fallback is lossy — results can be silently dropped when the target session is done).
Only one approval at a time — subsequent requests are blocked until the current one is resolved (with a 30-minute stale timeout as a safety net).
Notification Flow
When the agent calls send_notification:
- Plugin creates a TaskFlow and sets it to waiting state
- POSTs notification to Snarling on port 5000 (
/approval/alert) withtype: "notification" - Snarling displays the notification on screen with priority-based face and banner behavior
- User interacts: A press reveals text, B press dismisses, or low-priority auto-dismisses after timeout
- Snarling forwards feedback (revealed/dismissed/timed out + timing) to the plugin's
/notification-callbackHTTP route - Plugin resumes the TaskFlow (bookkeeping) and delivers feedback to the agent
Delivery: Notification feedback is recorded in the TaskFlow state (resume → finish) and delivered as context on the agent's next turn. Notifications are informational — they do not wake the agent. If the agent is in an active conversation, the feedback appears as context; otherwise it's stored in the completed TaskFlow for later retrieval. This is intentional: notifications don't require immediate agent action, unlike approvals.
If TaskFlow is unavailable, the notification degrades to fire-and-forget (no feedback).
Parameters:
message(required) — the notification textpriority(optional) — "low", "normal" (default), or "high"duration(optional) — seconds before auto-clear (default: 0, which means Snarling decides based on priority)
The notification payload sent to Snarling:
{
"type": "notification",
"message": "Stove's been on 20 min",
"priority": "high",
"duration": 0,
"notification_id": "notify-1234567890-abc",
"callback_url": "http://localhost:18789/notification-callback",
"session_key": "agent:main:main",
"secret": "uuid"
}
The feedback payload received from Snarling:
{
"notification_id": "notify-1234567890-abc",
"revealed": true,
"time_to_reveal_sec": 42.5,
"dismissed": false,
"timed_out": false,
"secret": "uuid",
"sessionKey": "agent:main:main"
}
time_to_reveal_sec measures total time from when the notification was sent to when the user interacted with it — including any time spent queued behind other notifications.
Priority-Based Timeout Behavior
| Priority | Default Timeout | Behavior |
|---|---|---|
| high | None (0) | Stays until user interacts — never auto-dismisses |
| normal | None (0) | Stays until user interacts — never auto-dismisses |
| low | 28800s (8h) | Auto-dismisses after timeout, sends timed_out feedback |
The plugin sends duration: 0 by default, letting Snarling decide based on priority. No urgent or moderate notification should ever just disappear.
Notification Attunement
The feedback loop enables notification attunement — the agent learning when and how to reach out effectively. Each notification generates a data point: was it revealed (and how quickly), dismissed without reading, or timed out? Over time, the agent adjusts notification behavior based on what works — the beginning of a notification attunement framework.
Approval Tracker
The plugin tracks approval and notification lifecycle counts in memory:
| Counter | When it increments |
|---|---|
requested | Every time request_user_approval is called |
approved | Callback resolved as approved |
rejected | Callback resolved as rejected |
timedOut | Stale lock cleared after 30min timeout |
errored | Snarling notification POST failed |
Notification stats:
| Counter | When it increments |
|---|---|
sent | Every time send_notification is called |
revealed | User pressed A to reveal notification text |
dismissed | User pressed B to dismiss without reading |
timedOut | Low-priority notification auto-dismissed |
errored | Snarling notification POST failed |
Query the stats:
curl -s -X POST http://localhost:18789/approval-callback \
-H "Authorization: Bearer <gateway-token>" \
-H "Content-Type: application/json" \
-d '{"action":"stats"}'
Returns: {"stats":{"requested":2,"approved":1,"rejected":1,"timedOut":0,"errored":0}}
Stats are in-memory only — they reset on gateway restart.
Architecture
OpenClaw Agent
↓ (plugin hooks: before_agent_run, before_tool_call, before_agent_reply, agent_end)
Interaction Bridge Plugin
↓ (POST localhost:5000/state) ← state updates
↓ (POST localhost:5000/approval/alert) ← approval requests
↓ (POST localhost:5000/approval/alert) ← notification requests (type: "notification")
↑ (POST localhost:18789/approval-callback) ← approval responses
↑ (POST localhost:18789/notification-callback) ← notification feedback (revealed/dismissed/timed out)
↑ (POST localhost:18789/environmental-event) ← thermal/presence events from snarling
Snarling Display (Python service on port 5000)
↓ (WebSocket RPC wake) ← bypasses gateway requests-in-flight
No approval_server middleman — the plugin talks directly to Snarling. Snarling resolves approvals and notifications via its A/B buttons and POSTs the result back to the gateway.
Delivery Methods
The plugin uses two delivery methods for getting data back to the agent, depending on the flow:
| Flow | Primary Delivery | Fallback | Reliable? |
|---|---|---|---|
| Approvals | subagent.run | enqueueSystemEvent + runHeartbeatOnce | ✅ Yes |
| Environmental events | subagent.run | enqueueSystemEvent + runHeartbeatOnce | ✅ Yes |
| Notifications | enqueueSystemEvent + runHeartbeatOnce | None | ⚠️ Delayed if session is done |
Why subagent.run: The old enqueueSystemEvent + runHeartbeatOnce delivery path silently drops events when the target agent session is in done state (the session has no active turn to drain the event queue). subagent.run creates a real agent turn that executes regardless of session state, ensuring events are always delivered. This fix was applied to approvals and environmental events; notifications will be migrated in a future update.
Environmental Events (V2 Protocol)
The plugin receives presence and observation data from Snarling's thermal/environmental system via a POST /environmental-event route. This replaces the old V1 event types (presence_change, presence_settled) with a unified observation_report type.
V2 event format:
{
"type": "observation_report",
"trigger_reason": "presence_settled" | "scheduled" | "startup",
"present": true,
"absent_duration": "2h 15m",
"world_state": { "source_count": 3 },
"changes_since_last": {
"appeared": { "sensor_1": { ... } },
"disappeared": { ... },
"changed": { ... }
}
}
Trigger reasons:
presence_settled— thermal sensor confirmed someone arrived/stayed (was V1'spresence_settledevent)scheduled— 30-minute periodic observation tickstartup— first observation after service start
V1 backwards compatibility: The plugin still handles V1 events for transition:
presence_change→ formatted as a simple presence update (doesn't wake agent)presence_settled(withouttrigger_reason) → treated asobservation_reportwithtrigger_reason: "presence_settled"
Wake behavior: Only observation_report and V1 presence_settled wake the agent. presence_change is acknowledged but doesn't wake. A 5-second dedup window prevents double-wakes from V1/V2 overlap.
Event delivery: Wake events are delivered via subagent.run, which creates a real agent turn in the target session. This ensures events actually reach the agent even when its session is idle or recently completed. If subagent.run is unavailable, the plugin falls back to enqueueSystemEvent + a 300ms-delayed runHeartbeatOnce (note: this fallback is lossy — events can be silently dropped when the target session is done). This is the same delivery method used for approval callbacks.
Configuration: Set presenceTarget in plugin config to route events to a specific agent (default: main). Set to disabled to acknowledge events but not route them to any agent. The session key is constructed as agent:{presenceTarget}:main.
Reducing Agent Wake-ups (Data Kill Switches)
Two plugin config options let you reduce OpenClaw CPU usage by controlling how much thermal/environmental data flows through the system:
| Config Key | Type | Default | Effect |
|---|---|---|---|
environmentalEventsEnabled | boolean | true | When false, the /environmental-event HTTP route returns {status: "disabled"} immediately — no event processing, no agent wake calls, no observation reports. The route still exists (returns 200) so Snarling doesn't error. |
presenceTarget | string | "main" | Set to "disabled" to acknowledge events but skip agent routing — the event is logged but no agent session is woken. Lighter touch than disabling entirely; useful when you want Snarling to keep running but don't need the agent to react to presence changes. |
// Disable environmental events entirely — maximum CPU savings on OpenClaw
{
"openclaw-interaction-bridge-v2": {
"enabled": true,
"config": {
"environmentalEventsEnabled": false
}
}
}
// Or: keep the endpoint active but stop routing to any agent
{
"openclaw-interaction-bridge-v2": {
"enabled": true,
"config": {
"presenceTarget": "disabled"
}
}
}
Both switches are safe in any combination. Snarling's ENVIRONMENTAL_EVENTS_ENABLED flag (in snarling.py) is a separate gate on the sending side — when False, Snarling skips the HTTP POST entirely. The two layers work independently:
Snarling ENVIRONMENTAL_EVENTS_ENABLED | Bridge environmentalEventsEnabled | Bridge presenceTarget | Result |
|---|---|---|---|
True | true (default) | "main" (default) | Normal flow |
True | true | "disabled" | Events received but not routed |
True | false | any | Events rejected at bridge |
False | any | any | No events sent from Snarling |
These are not tied to the Snarling thermal camera Hz or display settings. They only control the data pipeline from Snarling → OpenClaw bridge → agent.
Migration note: Once Snarling is fully on V2, the V1 compat paths can be removed. During transition, both observation_report (V2) and presence_settled (V1) will wake the agent for the same semantic event — the dedup window prevents double-waking.
Environmental Event Flow
Snarling POSTs thermal/presence events to the plugin's /environmental-event HTTP route. The plugin formats them into system events and routes them to the configured target agent (default: main, configurable via presenceTarget plugin config).
Event routing: The bridge reads presenceTarget from plugin config. If set to environmental, events route to agent:environmental:main — the dedicated environmental agent session. If set to disabled, events are acknowledged but not routed to any agent. If unset or main, events route to the main agent session.
// Plugin config example
{ "presenceTarget": "environmental" }
This is how environmental events reach the environmental agent instead of the main agent — no hardcoded routing, just a config value.
Event types (V2, current):
| Event Type | When | Wake Agent? | Delivery Method | Payload |
|---|---|---|---|---|
observation_report | Presence settled, periodic check, or startup | Yes | subagent.run (fallback: enqueueSystemEvent) | trigger_reason, world_state, changes_since_last |
presence_change | Human arrived or left | No | enqueueSystemEvent only | present, absent_duration |
Only observation_report events wake the agent via subagent.run — this covers both arrivals (presence_settled) and periodic check-ins (scheduled). presence_change events (raw presence flips) are too frequent to warrant waking the agent and are enqueued for the next heartbeat instead.
Why subagent.run instead of enqueueSystemEvent: The old enqueueSystemEvent + runHeartbeatOnce delivery path silently drops events when the target agent session is in done state. subagent.run creates a real agent turn that executes regardless of session state, ensuring events are always delivered. This is the same fix applied to the voice bridge for the same bug (#86090).
Install from ClawHub
openclaw plugins install clawhub:openclaw-interaction-bridge-v2
Development
git checkout development
# make changes
git add .
git commit -m "feat: description"
git push origin development
Credits
Built by Snar for the OpenClaw ecosystem.
