> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.loreos.app/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.loreos.app/_mcp/server.

# Proactive messages

A character can start a message on its own — a "cold initiate" after a long silence, or a nudge
on a thread the user left unanswered. Proactive sends arrive as ordinary `message.created` events
on the session event log, so you consume them exactly like replies (poll / SSE / signed webhook).

## Turn it on

Proactive is **off by default**. Enable it per session when you create the session:

```bash
curl -X POST https://api.loreos.app/v1/sessions \
  -H "Authorization: Bearer $LOREOS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"character": "yura", "external_user_ref": "u_8123", "proactive_enabled": true}'
```

`proactive_enabled` is the master on/off for that session. The character's
`behavioral_thresholds` (set at authoring time) provide the defaults for *when* and *how often* it
initiates. To change an existing session without recreating it:

```bash
curl -X PATCH https://api.loreos.app/v1/sessions/$SESSION_ID/proactive-controls \
  -H "Authorization: Bearer $LOREOS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"proactive_enabled": true}'
```

Send `false` to disable the session. Send `null` to clear the session-level override and fall back
to the character/app defaults.

## App-wide policy

Use app-wide policy for operations defaults that apply before character/session narrowing:

```bash
curl -X PATCH https://api.loreos.app/v1/apps/$APP_ID/proactive-policy \
  -H "Authorization: Bearer $LOREOS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "enabled": true,
    "quiet_hours": { "timezone": "America/Los_Angeles", "start": "22:00", "end": "08:00" },
    "max_per_day": 3,
    "allowed_reasons": ["remembered_detail", "open_loop", "offscreen_event"],
    "channel_policy": "webhook_only",
    "cost_cap_usd_per_day": 10
  }'
```

Read it back with `GET /v1/apps/{app_id}/proactive-policy`. Session preferences and
character behavioral thresholds can narrow the app policy; they should not widen it.

## Tune when and how often

Override the conditions for a single session with `PATCH /v1/sessions/{id}/proactive-controls`.
Send only the fields you want to change:

```bash
curl -X PATCH https://api.loreos.app/v1/sessions/$SESSION_ID/proactive-controls \
  -H "Authorization: Bearer $LOREOS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "proactivity_tolerance": 0.8,
    "cold_initiate_enabled": true,
    "cold_initiate_min_idle_minutes": 180,
    "min_gap_between_initiate_minutes": 240,
    "daily_initiate_cap": 3
  }'
```

| Field                                                  | Meaning                                                                                                                                        |
| ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `proactive_enabled`                                    | Enable/disable proactive for this existing session. `null` clears the session override.                                                        |
| `proactivity_tolerance` (0–1)                          | How willing the character is to initiate at all. `0` = never; `1` = most eager. The master gate — at `0` nothing fires regardless of the rest. |
| `cold_initiate_enabled`                                | Allow starting a FRESH conversation after a long silence.                                                                                      |
| `session_resume_enabled`                               | Allow nudging an unanswered thread back to life.                                                                                               |
| `cold_initiate_min_idle_minutes` (5–1440)              | Minimum silence before a cold initiate may fire.                                                                                               |
| `session_resume_min_idle_minutes` (0–1440)             | Minimum silence before a resume nudge may fire.                                                                                                |
| `session_resume_max_unanswered_character_turns` (0–20) | Stop resuming after this many unanswered character turns (anti-nag).                                                                           |
| `min_gap_between_initiate_minutes` (0–1440)            | Minimum gap between any two proactive messages (rate limit).                                                                                   |
| `daily_initiate_cap` (0–50)                            | Maximum proactive messages per day for this session.                                                                                           |

The full request schema (with these descriptions) is in `GET /v1/openapi.json`.

## Receiving proactive

Proactive is not a separate channel — it lands on the same event log as replies. But *how* you
consume it matters more here, because proactive fires at an **unpredictable time** (no user action
precedes it):

* **Managed channel (e.g. Telegram)** — LoreOS delivers the message straight to the end-user. Zero
  delivery code; the most convenient option when your surface is a managed channel.
* **Signed webhook — recommended for custom apps.** Register `POST /v1/sessions/{id}/channels {url,
  secret}`; LoreOS then pushes every event (proactive included) to your URL, signed
  (`x-auto-dating-signature: sha256=<hex>`, where `<hex> = HMAC-SHA256(secret, raw-body-bytes)`),
  at-least-once with retry, dedupe on `x-auto-dating-event-id`. You're notified the moment a
  proactive message fires — no polling idle sessions. The body is `{"event": {…}}` — the same event
  shape the events endpoint returns. For the **retry/backoff and `dead_letter` policy** (30s →
  ×2 → 1h cap, max 8 attempts) and how to inspect it with
  `GET /v1/sessions/{session_id}/delivery`, see
  [Delivery, metering, and observability](./delivery-metering-observability#proactive-delivery-contract).
* **Polling / SSE** — fine for reply-time, but a poor fit for proactive: you'd have to poll every
  idle session indefinitely (or hold a stream open) to catch an unpredictable send. Use them as a
  fallback, not the primary proactive path.

## Can a proactive message include a selfie?

Yes. A proactive message runs through the same engine as a reply, so it can attach a selfie
(delivered as an `image.ready` event, just like a reply image) when the moment fits. Control it with
the character's `behavioral_thresholds` at `POST /v1/characters` (typed, with per-field descriptions
in the schema):

* `image_proactivity` (0–1) — how eagerly the character sends images at all.
* `selfie_comfort` (0–1) — how comfortable it is sending self-portraits.
* `daily_image_cap` (0–20) — max images per day.

The character also needs an `identity` visual asset for a selfie to be generated. The decision is
contextual: a cold-initiate intro is usually plain text, and a selfie tends to appear once there's
rapport or when it reads as natural — the same way reply selfies work.

## Test it now (testflight)

You don't have to wait for the idle timers. Force ONE proactive message immediately:

```bash
curl -X POST https://api.loreos.app/v1/sessions/$SESSION_ID/proactive-test \
  -H "Authorization: Bearer $LOREOS_API_KEY"
```

This bypasses the idle / cap / tolerance gating and makes the character send a cold-initiate message
now. It returns a `run_ref`; the message lands as a `message.created` event within a few seconds —
poll `GET /v1/sessions/{id}/events?since=<cursor>` (or your webhook) to receive it.

> **Note**
>
> `proactive-test` is for development. Real proactive sends fire on the schedule, governed by the
> controls above. The test endpoint ignores those so you can verify delivery and tune the character's
> voice in one shot.