> 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.

# Character state

Character quality depends on state. LoreOS separates the pieces of state that make a
character feel consistent, then compiles them into a runtime view when a session runs.

## Main state surfaces

### Persona and voice

The character's public personality, speech style, and baseline behavior.

### Character direction

The character's long-running direction: what they want, what kind of life they are
moving through, and what should make future behavior feel coherent.

### Character facts and memory

Character facts are the stronger layer: stable facts about the character and world.
Memory is the larger event history: conversations, moments, preferences, and repeated
patterns that may or may not become stable character facts.

### Relationship state

LoreOS tracks each end-user separately. The same character can have different
relationship state across different sessions or external users.

Developer-facing relationship state is exposed as public-safe cards, not raw internal
ledgers. For a session, start with:

```text
GET /v1/sessions/{session_id}/relationship-snapshot
```

That snapshot includes qualitative relationship state, important memories, rituals, open
threads, supporting cast, recent life-feed cards, identity-continuity anchors, and Telegram
binding status. If your app needs a narrower surface, use the dedicated endpoints:

```text
GET    /v1/sessions/{session_id}/memories
POST   /v1/sessions/{session_id}/memories
PATCH  /v1/sessions/{session_id}/memories/{memory_id}
DELETE /v1/sessions/{session_id}/memories/{memory_id}
GET    /v1/sessions/{session_id}/open-loops
GET    /v1/sessions/{session_id}/life-feed
GET    /v1/sessions/{session_id}/identity-continuity
GET    /v1/sessions/{session_id}/reachout-reasons
GET    /v1/sessions/{session_id}/proactive-preferences
PATCH  /v1/sessions/{session_id}/proactive-preferences
POST   /v1/sessions/{session_id}/telegram/bind-token
POST   /v1/sessions/{session_id}/telegram/managed-child-bot
GET    /v1/sessions/{session_id}/telegram/binding
DELETE /v1/sessions/{session_id}/telegram/binding
```

If your product needs numeric relationship state, use the explicit score endpoint:

```text
GET /v1/me

PATCH /v1/apps/{app_id}
{ "expose_relational_numerics": true }

GET /v1/sessions/{session_id}/relationship-scores
```

For dating-style characters, the score keys include `affection_toward_user`,
`romantic_interest`, `perceived_user_affection`, `trust_level`, `comfort_level`,
`engagement_level`, `boundary_level`, and `reciprocity_uncertainty`. Values are final
0..1 state values. Each score includes `updated_at` and `version` when a relational
state exists. The same opt-in meter also appears on
`GET /v1/sessions/{session_id}/state` as `relational_meter`, but the dedicated
`relationship-scores` endpoint is the clearest API to use when you are building product
logic around these scores.

#### Dashboard export (all users / one user across characters)

The session endpoint above is one session. For a dashboard or CRM-style job, use the two
**export** endpoints — same opt-in
(`PATCH /v1/apps/{app_id} {"expose_relational_numerics": true}`), one final meter per
session, no raw text:

```text
GET /v1/characters/{slug}/relationship-scores?limit=50            # all users for one character
GET /v1/external-users/{external_user_ref}/relationship-scores?character={slug}&limit=50  # one user across characters
```

Both return `relationship_score_exports[]`, where each row is the latest final relational
state for a session and carries:

| Field                   | Meaning                                                                                                                                                               |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `session_id`            | the session the meter belongs to                                                                                                                                      |
| `character`             | character slug                                                                                                                                                        |
| `external_user_ref`     | **your** id for the end-user                                                                                                                                          |
| `updated_at`            | when the state last changed (ISO 8601)                                                                                                                                |
| `version`               | change marker — **equals the `updated_at` timestamp**, so use it as an incremental-sync cursor (a row changed iff its `version` advanced), not as an integer sequence |
| `relationship_scores[]` | the `0..1` dimension values (keys as on the session endpoint)                                                                                                         |

Each export is paginated with `next_cursor` / `has_more`. The character endpoint is for
character-level segments or leaderboards; the external-user endpoint is for one user's
standing across characters. Neither returns raw conversation text or model traces; drill
into a specific session with `GET /v1/sessions/{session_id}/events?since=0` when you need
the redacted event log.

`score_contract` explains the scale and movement semantics. Scores are not monotonic:
they can move down as the relationship changes and may decay. They are directionally
comparable within the same app/schema, but they are not a universal cross-app
leaderboard score.

After opt-in, post-turn score commits emit `relationship_score.changed` on the same
session event log used by polling, SSE, and webhook push. The payload contains final
0..1 values only. It does not expose raw extractor deltas, patch history, signal
events, critic reasoning, or prompt text.

To inject product-owned relationship context without mutating canon/persona, use:

```text
PATCH /v1/sessions/{session_id}/relationship-context
```

```json
{
  "relationship_label": "founding_friend",
  "relationship_context_key": "tier:founding_friend",
  "write_mode": "replace_key",
  "speakable_relationship_facts": ["The user is a founding friend."],
  "private_relationship_context": ["Treat this as warm trust, not exclusivity."],
  "forbidden_to_reveal": ["Do not say they are the only special user."],
  "permanent": true
}
```

This writes a session context event. Use `relationship_context_key` with
`write_mode="replace_key"` to replace one active app-owned context row. Use
`permanent=true` only for context that should stay active until deleted.

```text
GET    /v1/sessions/{session_id}/relationship-context
DELETE /v1/sessions/{session_id}/relationship-context/{context_event_id}
```

Relationship-context writeback does not overwrite character canon, persona, or
learned relational scores. It is planning-guarded, so supplied relationship facts
should not be contradicted by generic first-meeting or "too early to say"
assumptions. The guard does not authorize romance, permanence, exclusivity, or
claims beyond the supplied facts.

**Use this, not `memories`, for a product-owned tier.** A memory is a lived event the
character *learned* — it can be cited and embellished back to the user. A product tier
("founding friend", "inner-circle", a paid plan) is a label **you** own that should shape
the character's stance *without* becoming a spoken discovery. So write tiers as
relationship-context (the speakable + private + forbidden lanes), not as
`POST /v1/sessions/{session_id}/memories`. Put the stance you want voiced in
`speakable_relationship_facts`, the stance that should only steer behavior in
`private_relationship_context`, and hard "never say" lines in `forbidden_to_reveal`.

**TTL vs permanent.** A context row expires after **about 10 minutes** by default
(`ttl_seconds`, max 86400). Set `permanent: true` to keep it active until you `DELETE` it.
Use **permanent** for durable tiers (the "founding friend" label should not silently lapse
mid-conversation), and the **default TTL** for transient context (today's mood, the scene
the user is currently in).

**`write_mode` (avoid duplicate tiers).** To update a tier without stacking duplicate
active rows, send a stable `relationship_context_key` (e.g. `"tier:founding_friend"`) with
`write_mode: "replace_key"` — LoreOS soft-deletes the active rows that share that key
before storing the new one (`replace_key` **requires** the key). Omit `write_mode` (it
defaults to `append`) when you intend to *add* context alongside what is already active.

### Safety and session lifecycle

Use lifecycle controls when your product needs pause, block, report, or deletion flows:

```text
PATCH /v1/sessions/{session_id}/lifecycle
POST  /v1/sessions/{session_id}/block
POST  /v1/sessions/{session_id}/report
DELETE /v1/sessions/{session_id}/memories/{memory_id}
POST  /v1/external-users/{external_user_ref}/delete
```

`PATCH /lifecycle` accepts `action: "pause" | "resume"`. Paused sessions reject new
messages with `409 session_paused` and are skipped by active-session runtime/proactive
work. Pause does not delete memory, mutate canon, or erase usage records.

`POST /block` exits only this user-character relationship. It emits `session.exited`,
sets `lifecycle_state=exited`, and cannot be resumed. It is not an app-wide user block
and it does not delete memory.

`POST /report` emits a `safety.reported` event for your support/moderation workflow. It
does not automatically pause, block, forward, or delete anything; pair it with lifecycle,
block, memory deletion, or external-user deletion according to your own product policy.

These endpoints are app-scoped and projection-only. They do not expose raw prompts, critic
traces, private Story Room plans, raw world-model rows, raw offscreen transcripts, or
provider payloads. The numeric score endpoint also does not expose raw extractor deltas,
patch history, signal events, or critic reasoning.

### World context

World context describes recurring places, people, routines, obligations, and event
sources that make a character's life feel grounded.

### Safe runtime context

Not every internal fact should be visible to the chat actor. LoreOS builds a safe runtime
context that excludes private plans, future forecasts, and unapproved candidates.

## Why this matters

A simple prompt tends to flatten everything into one context window. LoreOS keeps state
as typed ledgers and compiled projections, so a character can:

* remember different users separately;
* avoid leaking private future plans;
* evolve slowly instead of changing personality every turn;
* produce replies and images that match the current world state;
* expose inspectable state through API and observability surfaces.

Use `GET /v1/characters/{slug}/runtime-preview` to inspect what the runtime will use for
a character, `GET /v1/characters/{slug}/readiness` before launch, and
`GET /v1/sessions/{session_id}/relationship-snapshot` when you need relationship evidence
for one end-user session.