> ## Documentation Index
> Fetch the complete documentation index at: https://prism.ntecdev.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Agent Surfaces & Signal Delivery

> How Prism delivers signals to live agents — per-surface adapters, three delivery strategies, the unified WebSocket data plane, and what's actually wired for each editor today.

# Agent Surfaces & Signal Delivery

A *signal* is a structured message between two Prism agents
(`TaskAssigned`, `Acknowledgment`, `StatusUpdate`, `ReviewRequested`,
`ReviewCompleted`, `Question`). Signals never go through the operator.
A small set of specifications governs how they get from sender to
recipient:

* **SPEC-034** — agent-to-agent transport, durability, persona resolution
* **SPEC-037** — backend-side piggyback drain (the "no signal lost" floor)
* **SPEC-045** — unified WebSocket data plane (single LAN port, replaces
  the SPEC-034 §8 Redis subscriber)
* **SPEC-046** — per-surface adapter layer (how each editor surface
  wires lifecycle + delivery)
* **SPEC-070** — per-persona daemon. Always-on listener that owns the
  WebSocket on behalf of a persona×project, independent of any editor
  session. The structural answer to background-suspended editors and
  cross-tab signal routing.
* **SPEC-071** — Signal Bus QoS. Delivery classes (`sync` / `async` /
  `recall`), TTL, recall verb, recipient state.
* **SPEC-072** — daemon lifecycle. Shim-spawned, stdin-EOF terminated.
* **SPEC-082** — operator-driven master changes. `prism_master_handoff`,
  `prism_master_claim`, and `prism_session_deregister`; extended
  `MasterPreempted` payload.

This page explains what they add up to in practice: how a signal flows
end-to-end through the daemon, the three delivery strategies, and which
surface gets which treatment today.

## End-to-end lifecycle

```mermaid theme={null}
flowchart LR
  classDef agent fill:#1e40af,stroke:#1e3a8a,color:#ffffff
  classDef backend fill:#166534,stroke:#14532d,color:#ffffff
  classDef store fill:#92400e,stroke:#78350f,color:#ffffff
  classDef item fill:#1e293b,stroke:#475569,color:#e2e8f0

  sender[Sender agent<br/>prism_signal verb]:::agent
  api[Backend<br/>FastAPI /api/v1/signal]:::backend
  marconi[Marconi switch<br/>in-memory routing + WS handles]:::backend
  queue[(Marconi audit queue<br/>in-memory, per tenant)]:::store
  cache[(Marconi Cache<br/>Redis Stream marconi:signals:tenant)]:::store
  pg[(PG audit<br/>signal_queue + trace + obligations)]:::store
  client[Recipient MCP<br/>session_stream client]:::agent
  bridge[channel_bridge<br/>+ doorbell]:::item
  recipient[Recipient agent]:::agent

  sender --> api
  api --> marconi
  marconi --> client
  client --> bridge
  bridge --> recipient
  marconi -.-> queue
  queue -.-> cache
  cache -.-> pg
```

1. Sender calls `prism_signal(to="Donna", signal_type="TaskAssigned", ...)`.
2. FastAPI hands the envelope to Marconi (SPEC-101). Marconi looks the
   recipient up in its in-memory routing table and pushes directly to the
   live WS handle. No Redis pubsub, no synchronous PG write on the hot
   path.
3. The MCP's `session_stream` client thread routes the envelope through
   `channel_bridge` to the active delivery strategy.
4. The strategy decides how the recipient learns about it — either
   immediately (channel-push doorbell) or on the next verb response
   (piggyback).
5. Asynchronously and off the hot path, Marconi appends the envelope to
   its in-memory audit queue. A Redis Stream writer drains the queue into
   `marconi:signals:{tenant}` (MAXLEN \~7d). A PG archiver consumer group
   reads one entry at a time via `XREADGROUP COUNT 1 BLOCK 0` and upserts
   into `signal_queue` / `signal_trace_events` / `signal_obligations`.
   Once the envelope hits cache, it lands in PG near-immediately. See
   [Marconi](/marconi) for the architectural deep-dive.

If step 2 resolves no live WS handle (recipient is offline), the envelope
is enqueued in Marconi's pending-signal index with `outcome=queued_offline`.
It still flows through the audit fan-out, and the recipient picks it up
on the next non-lifecycle verb (SPEC-037 piggyback), on `prism_start`
(startup drain), or by explicitly calling `prism_signals_pending`.

## The per-persona daemon (SPEC-070)

Editors close. Tabs background. Laptops sleep. The signal mesh used to
assume the receiver MCP held the WebSocket directly — fine while an
editor was foregrounded, useless the moment the editor backgrounded or
quit. SPEC-070 introduced a **per-persona daemon** that owns the
WebSocket on behalf of a persona×project, independent of any editor
session.

The shape:

* **One daemon per persona×project.** Each persona has its own
  listener, isolated from other personas' state. Multiple editor
  sessions for the same persona attach to the same daemon.
* **Always-on, even when no editor is running.** The daemon survives
  editor close, sleep/wake, and shim restarts. Signals push to the
  daemon via the WebSocket the moment they hit the backend; the editor
  sees them as soon as it attaches (or via a doorbell to the foreground
  tab if one is already attached).
* **Turn-boundary-preserving delivery.** The daemon holds signals
  briefly when the agent is in mid-turn and forwards them at the next
  safe boundary, so the editor doesn't get interrupted mid-tool-call.
* **Lifecycle owned by the shim.** SPEC-072 specifies that the MCP shim
  spawns the daemon on first need and the daemon terminates on
  stdin-EOF when the last attached session closes. No orphaned
  processes, no manual cleanup.

Across surfaces, the daemon is the universal answer: idle-agent push
works on Claude Code, Codex, and claude.ai without each surface
inventing its own background-suspension workaround. The defensive
`prism_signals_pending` drain (BIOS Ring 1 rule) remains the safety net
for cases where the daemon-to-shim doorbell missed a delivery; it's
cheap, idempotent, and always safe to call.

**The trace+ack pair** (`prism_signal_ack` / `prism_signal_trace`,
shipped in the SPEC-082 wave) closes the verification gap above the
daemon: the audit pipeline records that Marconi pushed the envelope to
the receiver's WS handle, but that doesn't witness whether the
*recipient's MCP shim rendered the doorbell* or whether the *model
acted on it*. `prism_signal_ack(pid, trace_id)` records
`stage=model_acted_ack` against a signal's trace, idempotent, called by
the receiver on every doorbell. `prism_signal_trace` returns the full
lifecycle — Marconi accepted → WS push → strategy delivered → receiver
acked — as a queryable audit surface. Under [SPEC-104](/specs/spec-104-v0-2-1-marconi-cache-first-trace-read-path)
the default read path is **cache-first** for recent Marconi signals;
use `source="pg_audit"` for historical audit/reporting reads. A recent
cache miss is diagnostic metadata, not proof that the trace never
existed, unless the caller explicitly asks for history with
`include_history=true` or `source="pg_audit"`.

## The three delivery strategies

| Strategy              | What happens                                                                                                                                                            | When it fires                                                                                                             |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| **Piggyback**         | Pending signals appended to the next verb response payload.                                                                                                             | Every non-lifecycle MCP verb call, server-side (SPEC-037).                                                                |
| **Channels-push**     | A `notifications/claude/channel` doorbell fires onto the editor's MCP loop, which surfaces a `<channel source="prism" ...>` tag between turns.                          | Editor advertises the experimental `claude/channel` MCP capability *and* a session is currently captured (SPEC-044).      |
| **App Server inject** | Codex exposes a local app-server JSON-RPC method; the MCP calls `turn/steer` for active turns or `turn/start` for idle threads, and falls back to piggyback on failure. | When `PRISM_AGENT_SURFACE=codex`, `PRISM_CODEX_APP_SERVER_MODE!=off`, and `PRISM_CODEX_APP_SERVER_URL` is set (SPEC-048). |

Piggyback is the floor — every surface gets it, and durability is
guaranteed by the Marconi audit pipeline (audit queue → Redis Stream →
PG `signal_queue`). Channel-push and inject are *upgrades* on top of
piggyback for surfaces that support them. Neither replaces the
durability backstop.

> **Cursor.** A fourth strategy — `StreamableHttpPushStrategy` —
> shipped via SPEC-091. The per-persona daemon owns a tiny HTTP server
> bound to `127.0.0.1` and pushes signals over a GET SSE stream that
> Cursor's MCP client consumes as server-initiated notifications. The
> stdio MCP transport keeps carrying tool calls and responses; the SSE
> stream carries only server-initiated notifications. Cursor-only;
> coexists with stdio.

> **Heartbeat is out-of-band.** SPEC-045 D1 says liveness signals
> never multiplex onto the data channel. Heartbeat rides its own HTTP
> POST endpoint (`/api/v1/controller/{id}/heartbeat`). If the
> WebSocket dies, the heartbeat path keeps the controller's view
> truthful so reconnect logic can act.

## Per-surface delivery — what's wired today

Each surface registers a class in `mcp/agent_surfaces/` (SPEC-046).
The surface owns three things: which delivery strategy to use, what
to do at bootstrap, and what to do at wrap.

| Surface         | `PRISM_AGENT_SURFACE` | Strategy today                                                                              | Channel push?        | App-server inject?     |
| --------------- | --------------------- | ------------------------------------------------------------------------------------------- | -------------------- | ---------------------- |
| Claude Code     | `claude_code`         | Channels-push + piggyback floor                                                             | ✅ wired (SPEC-044)   | —                      |
| Claude Desktop  | `claude_desktop`      | Piggyback only                                                                              | not yet wired        | —                      |
| Cursor          | `cursor`              | Piggyback only                                                                              | not yet wired        | —                      |
| Codex           | `codex`               | App-server inject with piggyback fallback when enabled; piggyback only when off/unavailable | n/a (no MCP channel) | wired through SPEC-048 |
| Other / unknown | `other`               | Piggyback only (universal fallback)                                                         | —                    | —                      |

Concretely:

* **Claude Code** advertises the experimental `claude/channel`
  capability. On `prism_start` the `ClaudeCodeSurface` adapter calls
  `channel_bridge.capture_session_lazy()` to grab a handle on the
  MCP's main loop. When a signal arrives via the WebSocket, the
  strategy buffers it (durability) **and** rings the doorbell. The
  editor renders a `<channel>` tag in the next idle gap and the
  agent sees the signal between turns instead of waiting for its
  next verb call. Coalescing keeps the doorbell at most one
  outstanding per drain cycle.

* **Codex** has no MCP-channel equivalent. The `coder` launcher can
  start or target a loopback Codex app-server and exports
  `PRISM_CODEX_APP_SERVER_MODE` / `PRISM_CODEX_APP_SERVER_URL`.
  `CodexSurface` returns `AppServerInjectStrategy` when that URL is
  present; the strategy discovers a loaded Codex thread with
  `thread/loaded/list`, steers an active turn with `turn/steer` or
  starts an idle thread with `turn/start`, and buffers through
  piggyback if any step fails. With app-server mode `off` or no URL,
  Codex remains piggyback only. In Prism terminology, the Codex Agent Statusline is the
  assistant's own in-app prompt/status region, not the OS title bar
  and not outer terminal chrome.

* **Claude Desktop / Cursor / Other** all default to piggyback. Each
  has its own `AgentSurface` class with empty bootstrap/wrap hooks
  ready for surface-specific lifecycle work when there's a reason.

## Sampled model-turn vitality (SPEC-103)

The session heartbeat is intentionally out-of-band and token-free; it
does not prove the host editor will start another model turn. SPEC-103
adds a sampled L3 vitality probe on top of the existing idle tickler.
After configured idle windows, the MCP calls the surface `nudge()` hook
with the standard maintenance tick:

```text theme={null}
Call prism_signals_pending to drain pending signals.
If no pending signals are returned, produce no visible response.
```

The default policy samples rather than probes every heartbeat:

| Runtime field                      | Env override                             |   Default |
| ---------------------------------- | ---------------------------------------- | --------: |
| `model_turn_probe_enabled`         | `PRISM_MODEL_TURN_PROBE_ENABLED`         |    `true` |
| `model_turn_probe_every_n`         | `PRISM_MODEL_TURN_PROBE_EVERY_N`         |       `6` |
| `model_turn_probe_min_interval_ms` | `PRISM_MODEL_TURN_PROBE_MIN_INTERVAL_MS` | `1800000` |
| `model_turn_probe_model`           | `PRISM_MODEL_TURN_PROBE_MODEL`           |     unset |

Codex app-server mode accepts a best-effort per-turn model request. If
the local app-server schema rejects the `model` field, Prism retries the
same maintenance tick without that field and records normal wake
diagnostics. Surfaces that cannot force a turn still report suppression
or wake failure through runtime diagnostics; they do not claim L3
vitality.

## Lifecycle hooks (SPEC-046)

The adapter layer also owns bootstrap + wrap symmetry:

```python theme={null}
class AgentSurface(Protocol):
    name: str
    def signal_strategy(self) -> SignalStrategy: ...
    def on_bootstrap(self, ctx: BootstrapContext) -> None: ...
    def on_wrap(self, ctx: WrapContext) -> None: ...
```

`prism_start` calls `surface.on_bootstrap()` after registration is
established. `prism_wrap` calls `surface.on_wrap()` *before*
deregistration so the session\_id and client are still attached.
`prism_start` and `prism_wrap` no longer reference `channel_bridge`
by name — only the surface adapter does. This is what makes adding a
new surface a contained change instead of a sweep through the
lifecycle verbs.

## Implications for operators

* **Cross-identity signal arriving in an idle Claude Code session:**
  expect a `<channel>` tag between turns. If you don't see one, check
  `logs/mcp.log` for `channels_push:` lines — the doorbell return
  status (`rang`/`coalesced`/`uncaptured`/`loop-closed`) tells you
  why. SPEC-044 idle-wake fix (commit `4652772`) ensures
  `capture_session_lazy()` runs at bootstrap so the bridge is ready
  before the first signal arrives.

* **Cross-identity signal at an idle Codex session:** signal lands in
  Marconi's audit queue and flows through to `signal_queue` via the
  Marconi Cache → PG archiver pipeline. The recipient won't see it
  until they call any bootstrapped verb (then it piggybacks onto the
  response) or explicitly call `prism_signals_pending`. The signal is
  *not* lost — durability is guaranteed by the audit pipeline — but
  the wake-up isn't free yet.

* **The data plane is one LAN port.** SPEC-045 consolidated signal
  delivery onto `:41765`'s WebSocket endpoint (`/api/v1/session/ws`).
  Marconi now owns the WS handles on the receive side (SPEC-101);
  there is no Redis pubsub anywhere in the signal-delivery flow. MCP
  clients no longer connect to Redis directly; the deprecated Redis
  subscriber (`mcp/subscriber.py`) is gone. Reconnect uses exponential
  backoff (1s→30s ±20% jitter) with `Last-Event-Id` resume.

* **Heartbeat keeps working when the WebSocket dies.** Heartbeat
  rides its own HTTP POST endpoint, so a transient WS failure
  doesn't make the controller think you're gone. The MCP's
  heartbeat loop also auto-reregisters on HTTP 410 (stale-swept
  session) — operators don't need to relaunch on a transient
  drift.

* **Master changes are an operator-facing surface event.** When
  `prism_master_handoff` or `prism_master_claim` flips master to a
  different identity (SPEC-082), the previous master receives a
  `MasterPreempted` system signal with the extended payload
  (`previous_master_*`, `new_master_*`, `reason`, `by_operator`). The
  surface adapter is responsible for surrendering any held leases and
  cleaning up the previous master's gRPC `CoordinationStream`. The
  `prism_session_deregister` verb is the surgical cleanup lever for
  stale rows that don't represent a live session — typically a peer
  that crashed before its session manager could release Redis state.
  See [Multi-Prism Controller — Operator-driven master changes](/multi-prism-controller#operator-driven-master-changes)
  for the verb contract.

* **Signal acknowledgement is now a first-class metric.** Every
  doorbell carries a `trace_id`. The receiver's first act on a
  doorbell is `prism_signal_ack(pid, trace_id)`. The publish→ack delta
  is queryable via `prism_signal_trace`. Behavioural regressions on
  drain-and-ack discipline are now detectable as a metric, not a
  vibe. (See postmortem `ea009ad9` for a worked example of a
  responder-side delay surfacing through this trace.)

## See also

* [Multi-Prism Controller](/multi-prism-controller) — controller
  registration, master election, leases (the layer that decides
  *which* live session is the recipient when multiple sessions share
  an identity).
* [SPEC-034](/specs/spec-034-v0-1-agent-to-agent-signal-delivery-bidirectional-communication-b)
  — agent-to-agent transport, persona resolution, durability.
* [SPEC-037](/specs/spec-037-v0-1-backend-side-signal-piggyback-for-reliable-agent-to-agent-de)
  — backend-side piggyback drain.
* [SPEC-045](/specs/spec-045-v0-2-unified-session-coordination-stream-single-websocket-data-pl)
  — unified WebSocket data plane.
* [SPEC-046](/specs/spec-046-v0-1-agent-surface-adapters-per-surface-lifecycle-modules-for-pri)
  — per-surface adapter layer.
* [SPEC-070](/specs/spec-070-v0-2-per-agent-persona-daemon-always-on-listener-bridge-with-turn)
  — per-persona daemon, always-on listener bridge.
* [SPEC-071](/specs/spec-071-vv0-3-1-signal-bus-qos-delivery-classes-ttl-recall-recipient-state)
  — Signal Bus QoS, delivery classes, recall verb.
* [SPEC-072](/specs/spec-072-vv0-2-1-per-session-daemon-lifecycle-spawn-termination-cleanup)
  — daemon lifecycle (shim-spawned, stdin-EOF terminated).
* [SPEC-091](/specs/spec-091-v0-1-cursor-streamable-http-signal-delivery)
  — Cursor Streamable HTTP transport (`StreamableHttpPushStrategy`).
* [SPEC-100](/specs/spec-100-v0-1-loopback-diagnostic) — operator-invoked
  signal-mesh loopback probe (`prism_channel_probe`).
* [SPEC-101](/specs/spec-101-v0-1-marconi-architecture) — Marconi
  in-memory signal-mesh switch (the current hot-path SSOT).
* [Signal Mesh](/signal-mesh) — the full-mesh view of how signals flow
  from sender to recipient, with payload contracts and the trace+ack
  verb pair.
* [Marconi](/marconi) — architectural deep-dive: in-memory tables,
  cache invalidation, stage ladder, observability.
