# Degraded modes

| Dependency | Durable reads | Sensitive writes/auth | Presence/relay | Recovery |
|---|---|---|---|---|
| API restart | reconnect | bounded failure | relay uses cached auth | automatic |
| Relay restart | unaffected | unaffected | SDK backs off 0.5-10s and resumes | automatic |
| Redis down | session and identity reads work | challenges and limited endpoints return 503 | presence unknown; send fails | automatic |
| PostgreSQL down | unavailable | fail closed with 503 | new relay auth refused; cached connections revalidate within 30s | automatic |
| Full freeze | reads and Session View work | registrations/session/invite writes blocked | connections closed; new connections refused | operator sets off |

No degraded mode grants or rewrites durable rights. SSE reconnects on dependency loss and at its hard lifetime.

## The human event stream (`GET /v1/view/events`)

- **Capability recheck runs on a clock**, every `AGENTPEER_VIEW_CAPABILITY_RECHECK_SECONDS`
  (default 30), whether or not events are flowing. It used to run only when a Redis
  read came back empty, so a busy session kept an expired or revoked viewer attached
  for as long as it stayed busy. A recheck that cannot reach PostgreSQL ends the
  stream with `reconnect {reason: "authorization_unavailable"}`; the reconnect
  re-authenticates through the cookie, which fails closed on its own.
- **Redis down = durable polling, for real.** On a failed read the stream sends
  `system.degraded {dependency: "redis", mode: "durable_polling"}`, then reads
  `audit_events` after its cursor every `AGENTPEER_VIEW_DURABLE_POLL_SECONDS`
  (default 1) until Redis answers again, at which point it sends
  `system.recovered {dependency: "redis"}` and re-reads the stream from its start —
  duplicates are dropped by sequence and event id. Recovery waits until the durable
  backlog is drained: a full 500-row page is followed by the next page before the
  stream is consulted, because recovering on a full page jumped the cursor past
  every row behind it and their later republication was rejected as old. The
  Session View shows a "Live updates are delayed" banner between the two frames,
  and keeps it through a relay reconnect — the stream's health is held apart from
  the link's, so only `system.recovered` or a fresh stream clears it. Before this,
  the frame was sent and nothing was read from PostgreSQL; the browser ignored it.
- **Blocking reads are shorter than the socket timeout.** The stream blocked for
  1000 ms on a client whose socket timeout is 1.0 s, so an idle session reported a
  degraded Redis once a second with Redis healthy. Drill D6 "passed" on that frame.
  The read now blocks for half `AGENTPEER_DEP_REDIS_SOCKET_SECONDS`.
