# Synapse integration

navi speaks to the [GNEXUS Synapse](https://handbook.gnexus) notification hub in
both directions:

- **Inbound** — Synapse delivers platform events to navi's webhook; each event
  can trigger an *agent reaction* (a background agent run).
- **Outbound** — navi emits low-level lifecycle events (reaction finished /
  failed, notify-tool pushes) into Synapse's ingest, where they are simply
  logged.

The `gnexus-synapse` package (vendored dependency) owns the wire format:
`verify_webhook` / `ack` / `make_signature` on the receiving side,
`AsyncSynapseClient` on the emitting side.

## Configuration

| 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`).

## Inbound: delivery gateway

`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.

## Inbound: reaction runner

`navi/synapse/reactions.py` — the point of the feature. Pipeline:

1. **Gate** — the matched target's user must have `reactions_enabled: true`;
   otherwise the event is accepted and discarded (`synapse.reaction_disabled`).
2. **Dispatcher meta-pass** — a single LLM call to the hidden `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).
3. **Profile resolution** — only *visible* profiles may host a reaction; a
   hidden or unknown id falls back to `secretary`.
4. **Special session** — a fresh session with `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.
5. **Headless run** — orchestrator `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.
6. **Finalise** — app push per `completion_notify` (`always` / `important` =
   only when the run had errors / `never`) and a low-level
   `reaction.finished` / `reaction.failed` event to Synapse (best effort).

## Outbound: source side

`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).

## notify tool

`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`.

## Per-user settings

`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.

## Service sessions in the UI

`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.