navi speaks to the GNEXUS Synapse notification hub in both directions:
The gnexus-synapse package (vendored dependency) owns the wire format: verify_webhook / ack / make_signature on the receiving side, AsyncSynapseClient on the emitting side.
| Env var | Purpose |
|---|---|
SYNAPSE_SOURCE_URL |
Synapse base URL for the outgoing source API |
SYNAPSE_SOURCE_API_KEY |
syn_* key registered in the Synapse admin panel |
SYNAPSE_SOURCE_NAME |
source name shown in Synapse (default navi) |
Without a source key nothing outgoing is sent, and Synapse-linked UI options are disabled (source_ready: false in settings responses).
Inbound deliveries need a per-user target: a token_ref + shared secret created in the Synapse admin panel and registered by the user in navi (POST /synapse-targets).
POST /webhooks/synapse (navi/api/routes/synapse.py) — verifies the signature against every active target secret (a mismatch never tells which target failed → 401), dedupes by event_id (Synapse retries until 2xx; replays are re-acked but never acted on twice), logs synapse.delivery_accepted, and hands the envelope to schedule_reaction() fire-and-forget, so the ack never waits for agent work.
navi/synapse/reactions.py — the point of the feature. Pipeline:
reactions_enabled: true; otherwise the event is accepted and discarded (synapse.reaction_disabled).dispatcher profile (navi/profiles/dispatcher/, temperature 0.2, one shot, no tools). Input: the user's reaction instructions + the event envelope. Output: strict JSON {profile_id, task, understood} or {skip: true}. Unparseable output or a backend failure = skip (logged, never a crash).secretary.special=True, name Synapse: {event_type} and session_metadata["synapse"] (event id/type/ dispatcher's understanding). Session + opening message are persisted before the run, so a crash mid-run still leaves a visible trail.create_run + run_agent with the target user's tool context. No live subscriber, but the run registry means the user watching the session in the UI sees the stream live.completion_notify (always / important = only when the run had errors / never) and a low-level reaction.finished / reaction.failed event to Synapse (best effort).navi/synapse/outbound.py — emit_low_level(subject, action, payload, priority, dedup_key): silently no-ops when the source is not configured, otherwise emits through AsyncSynapseClient and closes it per call.
Users of it: the reaction runner (reaction, finished/failed) and the notify tool (navi-notification, action = push level).
navi/tools/notify.py — the agent's signal channel. Parameters: message (required) + level (info | warning | intervention). Reads the user's push_target and delivers through both legs, then answers honestly: Delivered: app. Skipped: synapse (source key not configured) — the agent can see what actually happened and never retries into a wall. Priorities map info→normal, warning→high, intervention→critical.
navi/synapse/settings_store.py (postgres synapse_settings, synapse_instruction_versions):
reactions_enabled (default false),push_target: app | app_synapse | synapse (default app),completion_notify: always | important | never (default important),instructions: reaction instructions, editable by both the user and navi (synthesize_instructions tool edits with edited_by="navi"), every change recorded in the version history (last 100 kept, with author + reason).REST: GET/PUT /synapse-settings (PUT records a version when instructions change), GET /synapse-settings/versions?limit=. GET/PUT replies include the non-persisted source_ready flag.
special=True sessions are excluded from the regular session list at the REST layer (GET /sessions?special=false|true; omitting the param shows everything — admin/API callers see all). The webclient sidebar has a service-sessions toggle that fetches special=true; special items render muted with a robot icon.