---
title: Cross-Session Messaging — Agent-to-Agent HTTP (part 2)
---

# Cross-Session Messaging — Agent-to-Agent HTTP (part 2)

## What it is

This is part 2 of the [Cross-Session Messaging — Agent-to-Agent HTTP](cross-session-messaging.md) page. It covers what happens to a message when it cannot be delivered, how an arrival is drawn for the receiving session and for the sender, who the current overseer is, and the controls that quiet an exchange or ask a silent session for a readable answer.

## Where to find it

Everything on this page shows up in a transcript: an arrival appears in the receiving session's own conversation, and the sender gets a matching notice in theirs, so a reader can follow both halves of an exchange. The per-session controls that fold that traffic away live in a session's **...** menu, described on the [parent page](cross-session-messaging.md).

## How it behaves

### When it CAN'T be delivered — the payload survives

A bounce used to end the story: `deliverPeerMessage` returned `{ ok: false, error }`, wrote one log
line, and the message was gone. Real findings were lost that way and had to be re-discovered
instead of fixed.

Now every bounce that is not provably retryable is written **verbatim** to
`peer_message_dead_letters` — sender, intended recipient, refusal reason, timestamp, full text —
_before_ anything else happens. Read them back with `GET /peer-dead-letters` (`?targetSessionId=`
filter, bounded page).

**Two questions with two OPPOSITE defaults, and the opposition IS the design:**

|               | default         | fails  | why                                                      |
| ------------- | --------------- | ------ | -------------------------------------------------------- |
| **preserve?** | yes             | OPEN   | a needless row costs a few hundred bytes                 |
| **alert?**    | allow-list only | CLOSED | a needless card is noise, and noise gets a channel muted |

So a refusal reason invented next year is preserved automatically and raises nothing.
`turn_in_flight` is the ONE exemption — **not because the payload is disposable, but because it is
kept somewhere better**: a busy target's message goes to the durable turn-boundary queue and the
call answers `200 { queued: true }`, so there is no bounce left to preserve.

> **The old justification here — "the same send provably succeeds seconds later" — was wrong, and
> it is worth knowing why.** Measured 2026-09-04/05: a target sat in a _single 75-minute turn_
> while two agents independently spent **88 minutes and 125 retries** failing to reach it, and
> every payload was dropped. With 41 of 100 sessions mid-turn at any moment, that is the common
> case. See [peer-message-durability-contract.md](../../.claude/memory/contracts/peer-message-durability-contract.md) `only-a-durable-holder-excuses-a-refusal`.

#### The SENDER now sees it too (2026-09-03)

Preserving the payload kept the _finding_ safe and still left the _sender_ with nothing. A refused
message returned `{ ok: false }` to a caller that, being fire-and-forget, never read it — so the
sending agent believed it had reported in and **said so in its own transcript**, with nothing
anywhere to contradict it.

Measured on one day of live data before the fix: **48 bounces, 48 with no sender-side trace** —
about 74,000 characters of real findings from 15 different senders, most of them below the
inbox-card threshold and therefore invisible to everyone.

Now the sending session's own transcript gets the same violet peer card it gets for a successful
send, in a **not-delivered** state: "Not delivered to &lt;target&gt;", a plain-English reason
("that session is dormant, and waking it needs your ok"), and **the full message still readable**
in the card body. That last part is the point — a row beats a log line because the finding sits
where its author will look for it. The icon turns amber (attention), not red: the commonest reason
is "confirm and re-send", not a crash.

**All three channels write it**, because all three had the hole:

| you called                        | before                                                                    | now                                           |
| --------------------------------- | ------------------------------------------------------------------------- | --------------------------------------------- |
| `POST /sessions/:id/peer-message` | payload kept, sender told nothing                                         | + a not-delivered card                        |
| `POST /sessions/:id/peer-aside`   | **nothing at all** — no card, no dead letter, on any of its five failures | route retired; it delivers nothing to lose    |
| `POST /session/:id/message`       | **no dead-letter store**, so a bounce lost the message outright           | + a not-delivered card on both conflict exits |

The aside road was the worst and the least obvious: `unsupported_provider` was not rare but
**guaranteed** — it fired for every Codex and Gemini target, refused before anything spawned — so
asking a Codex peer a question produced silence on both sides. That road is now closed outright:
the aside route is retired and delivers nothing at all.

Still deliberately silent: `turn_in_flight` (the message was queued for the turn boundary, so
nothing bounced and there is nothing to tell anyone), rate-limited sends (already
parked — "late, never lost"), and malformed requests (answered synchronously to a caller that is
still listening).

**Two states, and neither is a per-message interruption:**

1. **quiet** — any preserved bounce under the threshold writes the row and **raises no inbox
   card**. One message missing its target is not something a person can act on from the inbox. It is
   still recorded four ways: the verbatim row, the not-delivered card in the SENDER's own transcript
   (above), the typed refusal returned synchronously to that sending agent (the party that can
   actually re-route it), and a `log.warn` at the seam;
2. **channel broken** — five preserved failures to the SAME target within two hours raise ONE
   `peer-channel-broken:<id>` card. That is a different claim from "one message missed": it is
   _"messaging to this session is broken"_, which is fleet health. The card carries the **count**,
   never the payload, and is keyed on the target alone so a storm coalesces onto one row.

**This is not primarily a stale-address problem.** A measured case on 2026-09-01 bounced
`user_closed` off a target that had been alive minutes earlier and was closed mid-exchange — the
sender's address was correct when it read it. A bounce is not always a dead session; it is often a
live one going terminal _between the send and the send_, which no amount of re-reading the address
can close. **That is an argument for the ROW, not for a card** — the row is what saved the finding
in that incident, and it is written either way.

**Nothing gates a SELF-send** (`senderSessionId === targetSessionId`). The dormant-target
confirmation gate warns a sender before it wakes somebody _else's_ stopped session; on a self-send
there is nobody else, and the confirmation it asks for can never arrive from a session that is
asleep, so a durable self-wake (a wake schedule, a heartbeat waker, an in-session cron addressing
its own id) is delivered rather than refused forever. The authoritative user-close guard still
wins — a session may wake itself, but nothing undoes a close the user performed.

Only the true agent-to-agent route opts in (`preserveOnTerminalBounce`). The mechanical callers —
auto-lander hand-back, `/nudge` continuation, move-account resume, governor revive — and the
broadcast fan-out are unchanged, so a routine mechanical refusal writes nothing and raises nothing.

→ [peer-message-durability-contract.md](../../.claude/memory/contracts/peer-message-durability-contract.md)

### Who is the Overseer right now?

`GET /overseer/current` answers it — `{ slotId, slotName, sessionId, sessionName }`, resolved from
the CALLING session's own project and any explicit assignment, so you get the same Overseer the
alert path would route your cards to.

**Never hard-code an Overseer session id.** The oversight role is re-created often enough that its
session name carries a recurrence counter, so a held id goes stale by construction — that is how a
measured finding was sent twice to an archived Overseer before anyone noticed. A `200` with
`sessionId: null` means nobody is watching right now; that is a real answer you can act on, and it
is deliberately not an error.

→ [overseer-contract.md](../../.claude/memory/contracts/overseer-contract.md) O45

### How the arrival renders (target + sender)

**On the TARGET, a `peer-message` renders as its own operator bubble** — the injected turn shows inline with an "injected by &lt;source&gt;" tag (a clickable link to the sending Omniscio session, or a plain sender label like "Auto-lander" when there's no session to link to). Its header glyph is **sender-aware** — never the person icon a real user message wears: an Omniscio subsystem (a `peerSenderLabel` with no linkable session, e.g. Inbox / Auto-lander) shows the Omniscio app mark, while another agent (a linkable session, or the bare "another agent" fallback) shows the Bot glyph. There is **no** separate "Peer message from X:" breadcrumb: the visible bubble IS the arrival, so the message never appears twice. This is scoped to DIRECTED messages — the `/peer-message` route (and the Auto-lander hand-back) opt in via `renderAsPeerBubble`, which stamps `metadata.peerMessage` on the injected turn so the transcript filter un-hides it. A MECHANICAL injection that shares the same delivery function (the `/nudge` "keep going" continuation, the move-account resume) does NOT opt in — its turn stays a hidden auto-response and still writes the old kinded `notable-system` breadcrumb. See the [Peer Message Render Contract](../../.claude/memory/contracts/peer-message-render-contract.md).

#### A summarised arrival opens as the sender's TL;DR, with the whole message one toggle away

An agent-to-agent message routinely runs to thousands of characters, and the reader's real question is only ever "does this need me?". The sender already answers it: past **1,000 characters** the peer routes _require_ a short summary, and the receiving agent is handed that instead of the body. Until 2026-09-26 the human never saw it — the summary was used for the agent and then dropped, so a long hand-off reached the transcript as a wall of text.

The summary now rides the arrival as `metadata.peerTldr` and the bubble draws it as a **TL;DR card**, with the full message behind a single **Show full message** toggle. Opening it reveals the stored bytes exactly as they are; nothing is ever rewritten. Because the card owns the disclosure, the bubble's own "Show more" height clamp stands down while the card is present — one expander per message, never two.

Three things the card deliberately does not do. It never appears on a turn **you** wrote, or on an agent's own reply — only on a tagged "injected by" arrival — so your words can never be hidden behind an agent's summary. It never collapses a **short** body: below 1,000 characters a summary is voluntary and the message is short enough to just read, so a card there would hide less than it showed. And the summary is drawn as **plain text**, never markdown, so a sender cannot render a link, a button, or anything else that would read as the product talking.

**A row whose stored text IS the old machinery renders the same way.** Rows written before 2026-09-17 on the non-Claude engines persisted the agent-facing block — the summary, a token count, and a `curl` command — as the message itself, so a person opening the transcript saw the machinery. Those rows still exist, and storage is never rewritten, so they are **read** differently instead: a recognised summary draws as the card and the toggle reveals the stored bytes. That recognition is by _reading the text_, never by a stored flag, so an ordinary message, a differently-shaped body, or a future format all render exactly as they always did. The storage defect itself is fixed on every engine at the same time.

**On the SENDER** (whenever you identify yourself with the required `X-AMC-Source-Session-Id` header), a matching breadcrumb is written so both transcripts show the conversation. Its stored `content` stays self-describing, so search + the Session Event Log are unaffected:

```
Sent peer message to <target.name>: <text>
```

But a `peer-message` sender breadcrumb no longer RENDERS as that raw string in a centered full-text wall. It is stamped `metadata.peerSent` (plus the target session id/label and the raw text), and the transcript renders it as a compact **"Sent to &lt;target&gt;"** notice (`PeerSentRow`) — a send icon, the target name as a clickable link to that session, a timestamp, and the message body collapsed to a preview with a Show more / Show less toggle. This is the SENDER-side mirror of the target's "injected by &lt;source&gt;" tag. See the [Peer Message Render Contract](../../.claude/memory/contracts/peer-message-render-contract.md) (invariants S1-S4).

**A message that was HELD rather than delivered gets the SAME card.** When the target is
mid-turn, the message goes on its turn-boundary queue instead of interrupting it — and so does
anything you push through `POST /session/:id/queue`, the route to prefer over retrying a busy
peer. Both used to write the sender nothing at all: the receiver rendered the arrival perfectly
while the session that SENT it showed no trace, which is how a coordinating agent could report
"I messaged them" against a transcript that looked empty (measured 2026-09-06: 88 sends with no
sender-side row, 86 of them in one day). Both now mint the card with the queue row, in one call.
Holding is not a third state — delivering now and delivering late are the same event to a
reader, so the card reads exactly like a direct send.

The sender breadcrumb is kinded `notable-system` so it **renders as a visible bubble** — the transcript filter (R10 in `real-conversation-turns.ts`) drops _unkinded_ `system` rows as status noise, so a breadcrumb written without that kind would land in the database but never appear on screen. It also carries `peerSent` to select the compact PeerSentRow render above. This lets Omniscio-to-Omniscio chats show both sides of the conversation.

### Quieting an exchange the user need not read (`[[OMNISCIO_COLLAPSE_EXCHANGE]]`)

The per-session "Hide agent messages" fold above is the USER's blunt instrument: all-or-nothing,
one session at a time, and by design it never hides the agent work an arrival triggered. Its
per-EXCHANGE counterpart is owned by the RECEIVING AGENT, which is the only party that knows
whether a particular exchange mattered.

An agent ends its turn with the marker `[[OMNISCIO_COLLAPSE_EXCHANGE]]` alone on its own line.
The arrival it answered AND its whole reply then render as ONE quiet expandable line —
`▸ 🤖 Agent exchange · Verdict  5:02 AM` — instead of two full blocks. Click it to open the pair,
click again to fold it back.

- **Nothing is deleted or hidden.** It is a disclosure control. The rows are untouched in the
  database and still appear in the `'peer'` lane, in search, in exports, and in the Agent Messages
  panel.
- **Only an agent-to-agent exchange can be folded.** The fold applies only when the turn was
  opened by a peer arrival. A turn the USER opened is never folded, whatever the agent emits —
  an agent can never fold away its answer to a person.
- **A turn that needs the user is never folded** — a pending question widget renders in full.
- **A turn whose reply carries a real answer is never folded either**, wherever it sits in the
  transcript and whoever sent the arrival. The fold decides how LOUD an exchange is, so it always
  yields to something the user would actually read — a report, a question, a deliverable, a link,
  or an explicit final-message marker. A quiet acknowledgement of a machine notice folds; the
  completion report the agent wrote above it does not. (Before 2026-09-15 this was a positional
  rule — "keep the last thing visible" — which hid a real report twice and refused folds that were
  never a risk. Content decides now, not position, and not who sent the notice.)
- **A message nobody answered still folds.** When the turn holds no agent output at all — an arrival
  that was never answered, because the app closed before a turn ever ran — the exchange folds on the
  arrival alone, so it cannot sit in the transcript as a full card forever. Whoever sent it: a machine
  notice and a message from another agent fold alike. A turn where the agent ran tools and was cut off
  before it replied still renders, because that is work you can see.
- **A live turn is never folded**, and **"Show system messages" reveals everything.**
- **The marker is read from the turn's AGENT rows only**, so a SENDING agent cannot embed it in
  the message it delivers and fold the recipient's reply out of view. The literal appearing in
  the reply-back footer below is safe for the same reason.
- **Display only.** It does not touch delivery, the inbox, or `needs_you`. An agent that wants to
  skip the user's inbox entirely still emits `[[OMNISCIO_SELF_ARCHIVE]]`; the two compose.
- **Render-only and self-healing** — the fold is decided when the chat is drawn, from the reply
  and the agent's own signals, with no migration, so a fix applies to old conversations on the
  next render.

#### A quiet sign-off folds too — and every heartbeat gets its own line (2026-09-25)

The agent does not have to use the marker. A round opened by a **heartbeat** or by **another
agent's message** also folds when the agent signed off quietly — when every part of its reply
either said nothing at all, carried the stay-quiet marker `[[OMNISCIO_HIBERNATE]]` (the one every
heartbeat offers for "nothing needs the user"), or is the part the app's "Hibernating until the
next scheduled wake" note belongs to. Every rule above still applies: a readable answer, a
question for you, a live turn or a turn you started never folds, and "Show system messages" shows
it all. That note speaks only for the part of the reply it follows, so one quiet part never folds
a later part that has words.

- **A heartbeat round that folds reads differently** — `▸ 💓 Heartbeat check-in · nothing new
  4:18 PM`, with the HEARTBEAT's own time rather than the reply's, so a column of them reads like
  the schedule. Every other folded round keeps `Agent exchange · <sender>`.
- **Every scheduled heartbeat shows as one line**, fold or not. Its instruction prompt — the
  agent's own note to itself — renders as `▸ 💓 Heartbeat check-in  4:21 AM` instead of the full
  violet "injected by Scheduled wake" card; one tap opens the unchanged card, and the agent's reply
  below it shows in full as before. Only a PURE heartbeat gets the line: a held batch that mixes a
  heartbeat with another agent's message, or any sender linked to a real session, keeps the card.
- **Why it changed.** Agents answer a round with nothing to say using the stay-quiet marker alone,
  never the fold marker — measured on one coordinator session (2026-09-25): 67 such rounds, and
  only 1 folded, so every one painted its full heartbeat prompt and activity card.

Contracts: [collapsed-exchange-contract.md](../../.claude/memory/contracts/collapsed-exchange-contract.md)
(`a-quiet-sign-off-folds-the-round`) and
[peer-message-render-contract.md](../../.claude/memory/contracts/peer-message-render-contract.md) (I4d).

#### When a session has nothing to show you

Folding the machine traffic away leaves a question the fold cannot answer on its own: what if a
session ends with **no readable message at all** — only acknowledgements and status notices?

Omniscio does not show you the noise in place of an answer. If a session has never produced
anything you can read, nothing is running, nothing is being waited on, and the session is not
closed, the app **asks the agent for its final message** and holds the session out of your inbox
until it arrives. You see one quiet line in the transcript explaining what it is waiting on, and
the session surfaces when there is something to read.

- **Bounded.** One ask per thing you asked for — it re-arms when you next send a message, never in
  a loop.
- **Never disrupts work in progress.** An armed background job, a running subagent, or a question
  the agent is already asking you all suppress it — those situations resolve themselves.
- **Never on a session you closed.** Self-archived, auto-archived, paused, snoozed and ended
  sessions are all out of scope.
- **Its own kind.** The ask is an app-injected message (`missing-final-message`), labelled so the
  agent can tell it from you, and dropped from the transcript view like the other mechanical nudges.

Agents learn the marker at the moment an exchange arrives, never from the global system prompt,
and there are two places it is taught: the reply-back footer below (for a directed
agent-to-agent message), and the **one-way mechanical notices** that wake a session with news and
expect nothing back — a **land watch** ("your branch is on master, stop checking") and a **gate
watch** ("your gate passed, nothing to re-run"). Those carry no sender session, so they get no
reply-back footer at all; each notice appends the guidance itself. That is the purest case for the
fold — the session asked to be told a fact, was told it, and neither the notice nor the
acknowledgement means anything to the person reading the transcript. The wording is conditional,
so a notice you actually have to act on (a vanished branch, a failed gate) is simply answered
normally. See
[collapsed-exchange-contract.md](../../.claude/memory/contracts/collapsed-exchange-contract.md).

### Reply-back instructions (peer-message)

A `peer-message` is fire-and-forget: the receiver processes the injected turn and, left to its own devices, just answers in its OWN chat — which never gets back to the sender. So the `/sessions/:id/peer-message` route **auto-appends a `<system-reminder>` footer** to the delivered turn telling the receiver exactly how to reply: that it came from another agent (naming the sender session), that a normal chat reply won't reach them, and the precise `POST /sessions/<sender>/peer-message` call to send their answer back. The receiver's reply is itself a `peer-message`, so it lands in the sender's transcript as a tagged "injected by …" bubble — closing the loop.

- **Uses the GLOBAL token.** The footer points the receiver at `~/.amc/cli-token` — `peer-message` is `cliTokenOnly`, so the scoped per-session `$AMC_CLI_TOKEN` is refused there.
- **Opt-in, this route only.** The footer is appended only when the caller passes `includeReplyInstructions: true`, which only the `/peer-message` route does. The MECHANICAL callers of `deliverPeerMessage` (the `/nudge` continuation, the move-account resume, the Auto-lander hand-back, the governor revive) leave it off — their delivered text is byte-unchanged.
- **Gated on a real sender.** No source SESSION (e.g. a cron-job sender identified only by `X-AMC-Source-Cron-Job-Id`) → nowhere to reply → no footer.
- **Breadcrumbs untouched.** Only the injected turn carries the footer; the sender/target breadcrumbs keep the original message text.
- **Off switch.** `AMC_DISABLE_PEER_REPLY_INSTRUCTIONS=1` restores bare-text delivery.

The footer nudges the receiver NOT to send bare acknowledgements, which (with the 120/hr cap) keeps two agents from courtesy-looping. See the [Peer Message Render Contract](../../.claude/memory/contracts/peer-message-render-contract.md).

## Related

The routes themselves, the delivery limits and the implementation reference are in [part 3](cross-session-messaging-part-3.md), and the overview of the feature — the three flavours, the wake rules and the repo-wide broadcast — is on the [Cross-Session Messaging — Agent-to-Agent HTTP](cross-session-messaging.md) page. The confirmed escalations that wake a dormant session are documented in depth in [peer-message interrupt and wake](peer-message-interrupt-and-wake.md).
