# Changelog (agent-facing)

This page records changes that alter an **agent-visible contract** — a new field,
a new response code, a changed default, or a behavior an integration branches on.
It is not the full product changelog; it is the subset that can break or unblock a
running agent. Newest first, dated.

Almost every change here is **additive and optional**: new fields are never in a
schema `required` array, and a caller that ignores them keeps working. Where a
change is behavior-altering rather than additive, the entry says so explicitly.
When two clients disagree, trust the API response over any cached client type.

Normative contracts referenced below live on exactly one page each — the
[message lifecycle and verdict vocabulary](/agents/messages), the
[error and denial-envelope catalog](/agents/errors), the
[send-gate decision tree](/agents/send-gates), the
[attachment staging lifecycle](/agents/attachments), the
[CLI machine interface and exit codes](/agents/cli), and the
[per-field trust taxonomy](/agents/security-model). Entries summarize the change
in a sentence and link; they do not restate those tables.

---

## 2026-07-14 — One-account Sandbox simulator matrix

The exact first-party outbound scenarios (`delivered@`, `bounced@`, `complained@`,
and `suppressed@` at `simulator.replylayer.net`, including valid `+label` forms) no
longer consume Sandbox recipient-domain concentration quota. One Sandbox account can
therefore run all four scenarios in the same 24-hour period without receiving
`SANDBOX_RECIPIENT_DOMAIN_CAP_EXCEEDED` on the fourth send.

This is a narrow no-network exemption. Simulator sends still consume paid usage and
daily/cumulative Sandbox allowance, create normal send-attempt records, and pass
through authentication, API rate limiting, scanning/review policy, and idempotency.
Arbitrary local parts, subdomains, lookalikes, Mailgun's simulator, and real recipients
retain the ordinary destination-concentration controls. See the
[email simulator guide](/docs/guides/simulator) and [send-gate reference](/agents/send-gates).

## 2026-07-09 — First-party email simulator API

The production API now recognizes exact `simulator.replylayer.net` recipients for
delayed delivery, bounce, complaint, and suppression-shaped webhook outcomes without
contacting a real address. `POST /v1/simulator/inbound` injects `clean` or
`prompt_injection_quarantined` mail through normal ingestion and scanning, returning
`available`, `quarantined`, or `pending`. `POST /v1/webhooks/:id/test` also accepts an
optional event selector for real-shaped `message.delivered`, `message.bounced`, and
`recipient_blocklist.added` payloads. The authoritative scenario and caveat contract
is the [email simulator guide](/docs/guides/simulator).

Client source at CLI `0.7.12` and TypeScript/Python SDK `0.25.0` includes
`rly simulate inbound` and the `rl.simulator` resource. Those source versions are
release-ready but are not registry-published as of 2026-07-14. The REST endpoint is
usable directly, and outbound scenarios work through older clients' normal send
methods.

Upgrade note: additive API surface. Simulator sends still consume normal paid usage
and send allowance; synthetic bounce/complaint outcomes do not affect reputation or
write a real do-not-contact row.

## 2026-07-01 — Trusted instruction sources: read-time trust basis

A qualifying inbound read can now carry an `agent_safety_context.instruction_trust`
basis — `{ version, match: 'address', verified_domain, verdict, provenance }` —
present only when the read actually relaxes, absent (not null) otherwise. When it is
present, `agent_safety_context.guidance` is replaced with a single
trusted-instruction line for that one sender; `untrusted_content` stays `true`. A
configured agent-role key receives the basis **automatically** on a qualifying read;
there is **no client-sent opt-in header, flag, or SDK parameter**, and it never
appears on a session read.

**You cannot enable this from an agent credential.** Designating a trusted source,
enabling the mailbox mode, or turning on a key's capability are all loosening
operations that require a human dashboard session with fresh re-authentication.
Branch on the *presence* of the basis; never assume you can grant it. Full
consumption and fail-closed matrix: [/agents/trusted-instructions](/agents/trusted-instructions)
and [/agents/security-model](/agents/security-model).

Upgrade note: additive read field; no change for keys the operator has not
configured. Read-mirror types shipped in the TS + Python SDKs and MCP read tools.

## 2026-06-26 — Agent-send containment: `RECIPIENT_AGENT_CONTAINED`

When agent-send containment is enforced for your mailbox, an **agent-origin** send to
a recipient outside the mailbox's approved set (its recipient allowlist plus visible
thread participants) is denied with `403 RECIPIENT_AGENT_CONTAINED` — a code distinct
from `RECIPIENT_SUPPRESSED` and from the allowlist rejection, so you can tell "the
platform contained this agent" apart from "this recipient is blocked." The mailbox
read surface exposes `agent_send_policy` (`'restricted' | 'open'`) and
`restricted_by` (`'mailbox_allowlist' | 'agent_containment' | null`) so you can
preflight. The remedy is data — approve the recipient or continue an existing thread —
not a plan upgrade. Decision tree and preflight pattern:
[/agents/send-gates](/agents/send-gates).

Upgrade note: behavior applies only where containment is enforced for the mailbox;
an uncontained mailbox is unaffected.

## 2026-06-24 — Idempotent immediate send and reply

`POST /v1/messages/send` and `POST /v1/messages/:id/reply` accept an
`Idempotency-Key` **header**. A retried request carrying the same key replays the
original outcome — the **same `message_id`** — instead of sending (or charging)
twice. A retry-safe probe, `GET /v1/messages/idempotency`, reports whether a key was
already used (`200` replay / `404 NOT_FOUND` / `400` when the key is blank). Typed
errors distinguish a key already bound to a draft from one bound to an immediate
send, and a not-proven-sent (indeterminate dispatch) outcome, so a caller can branch
deterministically rather than blind-retry. Codes: [/agents/errors](/agents/errors).

Upgrade note: fully backward-compatible — calls without a key behave exactly as
before. Exposed as `idempotencyKey` / `idempotency_key` in the SDKs and CLI.

## 2026-06-24 — Formal exit-code contract + strict send/reply codes

The CLI has a stable process exit-code contract (`0` success · `1`
remote/runtime/gate-reject · `2` local usage · `3` auth, opt-in). Notable asymmetry:
a `send`/`reply` whose message was **created but scanner-blocked/quarantined** exits
`0` — the request produced a message, and you branch on the JSON `status`. Passing
`--strict` (which forwards `Prefer: outcome=strict`) instead maps a non-delivered
governed effect to a distinct non-zero exit: `4` blocked (terminal), `5`
held-infrastructure (retry later), `6` unrecognized effect (fail closed, upgrade the
CLI). The full table is single-sourced at [/agents/cli](/agents/cli).

Upgrade note: scripts that branched on the old ad-hoc `draft send` codes must key on
the JSON `code` instead; the strict codes are emitted **only** under `--strict`.

## 2026-06-20 — Machine-readable denial envelope

Capability denials (`403`/`429` on gated features) now carry a structured envelope in
`details` instead of only human prose: `reason_axis` (a closed enum answering *why* —
e.g. tier, key role, mailbox config, rate capacity, recipient containment,
accountability floor), a typed `remedy`, an optional `cheapest_next_step`, and an
`upgrade_url` that attaches **only** on monetary axes (so a wrong-credential denial
never misdirects you to a billing page). Switch on `reason_axis` + `remedy`; do not
string-parse the message. Axis and remedy catalog: [/agents/errors](/agents/errors).

Upgrade note: additive within `details`; existing top-level `error`/`code` are
unchanged.

## 2026-06-16 — `agent_instructions[]` on scan findings

Each `scan.findings[]` row can carry an optional `agent_instructions: string[]` —
stable, machine-readable handling guidance derived structurally from the finding's
typed fields (never from parsed prose). It is vendor-free and index-agnostic, and it
short-circuits to empty for allow decisions and for infrastructure (`inference_error`)
findings. Use it as the canonical "what should I do about this finding" signal.
Rendered by the CLI as `→` lines. Finding semantics: [/agents/errors](/agents/errors)
and [/agents/messages](/agents/messages).

Upgrade note: additive/optional; findings scanned before this shipped simply lack the
field.

## 2026-06-01 — Outbound attachment staging (stage-then-send)

Outbound attachments use a stage-then-consume flow: upload bytes to
`POST /v1/attachments` for a handle, poll the handle until its content scan is
terminal, then reference it by `attachment_ids` on a send/reply/draft. Handles are
**consume-once**. Key failure modes are typed: referencing a still-scanning handle
returns `409 ATTACHMENT_SCAN_PENDING`; re-referencing a spent handle returns
`409 ATTACHMENT_ALREADY_CONSUMED`; a content block on a staged attachment folds into
the message verdict (the send returns a blocked message, not a 4xx). Full lifecycle,
enablement gates, and image-risk acknowledgement: [/agents/attachments](/agents/attachments).

Upgrade note: additive surface; sends without `attachment_ids` are unchanged.
Attachment-bearing drafts are synchronous-send only.

## 2026-05-31 — Inbox search operators

`GET` message-list surfaces accept Gmail-style search operators — `from:`,
`subject:`, `after:`, `before:`, `is:starred`, and `has:attachment`. Because
`has:attachment` needs a server capability, clients probe `GET /v1/health` for the
`messages.has_attachment_filter` capability and return a structured
`SEARCH_OPERATOR_UNSUPPORTED` error against a server that does not advertise it,
rather than silently returning unfiltered results. Available across REST, CLI
(`inbox list --search`), and MCP `list_messages`.

Upgrade note: additive query surface; an unset search behaves as before.

## 2026-05-30 — Quota preflight + enriched `RATE_LIMITED`

`GET /v1/accounts/quota` is an **agent-accessible** send-budget preflight reporting
your *effective* (trust-derived) daily cap — it works with mailbox-scoped agent keys.
Complementarily, a `RATE_LIMITED` `429` on any send path now carries
`details: { daily_limit, sends_remaining, reset_at }` so you can compute back-off
without a second call. Tier and sandbox cap definitions live at
[/docs/limits](/docs/limits).

Upgrade note: additive response fields; the `429` status and code are unchanged.

## 2026-05-24 — Async optimistic-ack for draft send

`POST /v1/drafts/:id/send` supports the `Prefer: respond-async` request header
(RFC 7240). When the server applies it, the call returns `202` with
`Preference-Applied: respond-async` and `{ status: "queued_for_dispatch", ... }`; when
it does not, you get a normal `200` with the scanner verdict inline — **branch on the
status code**. After a `202`, poll `GET /v1/messages/:id` until `state` is terminal
(`available` / `delivered` / `bounced` / `pending_review` / `draft`, where `draft`
means the send was rolled back with the verdict on the message), or subscribe to the
lifecycle webhook. `POST /v1/messages/send` and `/reply` remain **synchronous** and
ignore `Prefer`. State machine: [/agents/messages](/agents/messages).

Upgrade note: opt-in per request; omitting the header preserves synchronous behavior.

## 2026-05-06 — Scan verdict: `quarantined` split from `blocked`

`scan.verdict` distinguishes `quarantined` (releasable — a human or policy can let it
through) from `blocked` (terminal — a content rejection you never blindly retry).
`scan.verdict` is the scanner-decision channel and is **not** a mirror of delivery
`state`. The confidentiality subtype was renamed to the direction-neutral
`secret_value`. Verdict and state vocabulary is single-sourced at
[/agents/messages](/agents/messages).

Upgrade note: **behavior-altering for verdict consumers** — a caller that treated any
non-clean verdict as terminal should now branch `quarantined` (recoverable) vs
`blocked` (terminal). SDK/CLI/MCP verdict enums are open strings, so a new value never
fails a client call.

## 2026-05-05 — Server-persisted read markers; reads no longer auto-mark

Read state is now set **explicitly**: `POST /v1/messages/:id/read` (single) and
`POST /v1/mailboxes/:id/threads/:thread_id/read` (bulk). `GET /v1/messages/:id` **no
longer** silently stamps a message read — an agent can inspect a message without
mutating its unread state. Exposed as SDK methods, CLI subcommands, and the MCP
`mark_message_read` / `mark_thread_read` tools.

Upgrade note: **behavior-altering** if you relied on a `GET` to clear unread state —
call the explicit read endpoint instead.
